KINTOテクノロジーズのブログ - TECH PLAY

TECH PLAY

KINTOテクノロジーズ

KINTOテクノロジーズ の技術ブログ

全1129件

はじめに こんにちは、2026年6月入社のhyokoです! 本記事では、2026年6月に入社したメンバー2名の入社直後の感想をまとめました。 KINTOテクノロジーズ(以下、KTC)に興味のある方、そして、今回参加したメンバーの振り返りとして有益なコンテンツになればいいなと思います! hyoko 自己紹介 KINTO開発部で契約管理システムを担当しています。 今後、要件のとりまとめやリリースまでの進行管理を担当する予定です。 これまでは、SIer(2社)、ITコンサル、事業会社の社内SEを経験してきました。 趣味は音楽フェスに行くことです。ここ数年はFuji RockやSummer Sonicに行っています。 よろしくお願いします! 所属チームの体制は? 室町オフィスに6人、Osaka Tech Labに1人の合計7人のチームです。 3週間ごとの定期リリースで、新規機能の開発や既存機能への改修要望対応、システム改善を並行して実施しています。業務影響のある不具合が見つかった場合は臨時にリリースしています。 他プロダクトを含むプロジェクトはウォーターフォール、既存機能の改修要望はアジャイルで進めるハイブリッド形式です。技術スタックは、フロントエンドがJavaScript + HTML、バックエンドはPythonです。 KTCの入社動機や入社前後のギャップは? 事業会社の社内SEとして、自身が構築に携わったプロダクトに長く関わり、事業の成長に貢献したいという思いがありました。前職でも実現はできていたのですが、プラットフォーム依存があったため、よりオープンな環境で挑戦したいと考えKTCに入社しました。 前職の経験から社内SEのイメージを持っていたのですが、想像以上に高いスキルを持つ優秀な方が多く刺激を受けています。 現場の雰囲気はどんな感じ? わいわいと賑やかで、雑談を含めたチーム内のコミュニケーションが活発なチームです。 一方で職人気質でこだわりのある雰囲気もあり、SIer時代を懐かしく思い出しています。 オフィスで気に入っているところ 最寄り駅が三越前で、自宅から1時間以内で通勤できるアクセス性の良さです。 オフィス内にグリーンが多くて癒されます。よく使う出入口にあるサボテン(多分、金鯱)は特にお気に入りです。 髙田さんからの 質問:休憩の取り方、おススメの過ごし方があれば教えてください。 昼食は一人で行くことが多いのですが、時間があるときは行ったことのないお店や方面に足を延ばし、新たな発見を楽しむようにしています。仕事中は座っていることが多いので、少しでも歩いてリフレッシュしています。 時間がないときは、お弁当などを買って休憩室で食べています。休憩室はラジオが流れていて適度な雑音があり、とても過ごしやすい空間です。 Takada 自己紹介 モビリティプロダクト開発部 DXソリューショングループに所属しており、主に国内販売店へ向けたプロダクト開発提案を行っています。 所属チームの体制は? スタッフは大阪2名、名古屋1名、室町4名、合計7名体制です。 KTCの入社動機や入社前後のギャップは? 前職は自動車業界におりまして、これまでの経験を国内最大の四輪販売会社ネットワークであるトヨタ系列で活かしてみたいという思いがあり入社させていただきました。自身は開発者ではないのですが、周りのスタッフ皆さんのプロダクト開発における知見の幅や意識の高さに驚いています。毎日が学びですね。 現場の雰囲気はどんな感じ? コミュニケーションはとてもフラットです。そして自分で調べても分からないことは周囲の誰かしらが手を差し伸べてくださり、助け合いの文化を感じています。 オフィスで気に入っているところ 最最寄りが三越前駅なので自宅からのアクセスがとても良いです。便利すぎて毎日の歩数が極端に減ってしまい運動不足になってしまいました。 事務所っぽくない、カフェのような過ごしやすい空間がお気に入りです。 横山さんからの 質問:情報をキャッチアップする際に工夫されていることがあれば教えてください。 自分は何が分からないのか、いつまでにどんな情報が必要があれば組み立てられるのか、を一旦整理するようにしています。業務に必要な販売店情報については、先方と直接会話をすると肌感覚のような言語化しにくい情報も得ることができますので積極的に訪問したいですね。その他、販売店現場の最前線で何が起きているのか仮説を立てるために頻繁にAIを活用しています。 さいごに 今回は、2026年6月に入社したメンバー2名の入社直後の感想をご紹介しました。 本記事が、KTCに興味のある方にとって、入社後の雰囲気や働くイメージを知るきっかけになればうれしいです。最後までお読みいただき、ありがとうございました! そして、KINTOテクノロジーズでは、まだまださまざまな部署・職種で一緒に働ける仲間を募集しています! 詳しくは こちら からご確認ください!
はじめに こんにちは! KINTOテクノロジーズのデジタル戦略部DataOpsグループ所属の三浦です。 普段は、社内のデータ分析基盤の開発・保守・運用に加え、Google Analytics 4(以下、GA4)やGoogleタグマネージャー(以下、GTM)を利用した、Webサイトのアクセス解析や計測環境の整備を担当しています。 WebサイトでGA4やGoogle広告を計測する場合、一般的にはGoogleドメインからGoogleタグを読み込み、Googleドメインへ計測リクエストを送信します。 一方、ブラウザのプライバシー保護機能や広告ブロッカーなどの影響により、Googleタグの読み込みや計測リクエストがブロックされ、イベントが欠損することがあります。 今回、この課題への対応として、既存のAmazon CloudFrontを利用し、Googleタグゲートウェイを導入しました。 本記事では、Googleタグゲートウェイを導入した背景や構成、社内での役割分担、検証時に確認したポイント、導入後に確認できた効果について紹介します。 本記事の対象者 本記事は、以下のような方を対象としています。 GTMやGA4を利用したWebサイトの計測を担当している方 Googleタグゲートウェイの導入を検討している方 Amazon CloudFrontをCDNとして利用している方 本記事では、CloudFrontの細かな設定手順ではなく、導入時の考え方や進め方、検証内容を中心に紹介します。 Googleタグゲートウェイ導入の背景と概要 背景 当グループでは、Webサイトの利用状況や広告施策の成果を把握するため、GTMを利用してGA4やGoogle広告などの計測タグを配信しています。 通常の構成では、WebサイトからGoogleドメイン上のGoogleタグを読み込み、計測したイベントをGoogleドメインへ送信します。 通常のGoogleタグ配信の構成 しかし、Googleドメインへの通信は、ブラウザのプライバシー保護機能や広告ブロッカーなどによって、制限されることがあります。 計測タグや計測リクエストがブロックされると、ユーザーが実際にページを閲覧したり、コンバージョンしたりしていても、GA4やGoogle広告へイベントが送信されません。 その結果、以下のような課題が発生する可能性があります。 GA4で確認できるイベント数が、実際の発生件数より少なくなる コンバージョン数が正しく計測されない Webサイトの施策評価に使用するデータが不完全になる 広告経由の成果を正しく評価できない 広告配信の最適化に使用するコンバージョンデータが不足する 特に広告配信では、計測したコンバージョンデータが、自動入札や配信対象の最適化に利用されます。 そのため、イベントの欠損は、レポート上の数値が少なくなるだけでなく、広告配信の学習や最適化にも影響する可能性があります。 実際に対象サイトでは、GA4で計測しているコンバージョンページのイベント数と、バックエンドのデータベースに保存されているコンバージョン件数に差がありました。 もちろん、GA4とバックエンドのデータベースでは、計測の仕組みや集計条件が異なります。 そのため、数値が完全に一致するものではありません。 一方で、差が大きい状態では、実際に発生したコンバージョンの一部をGA4で取得できていない可能性があり、広告施策やサイト改善を評価するうえで課題となっていました。 そこで、Googleタグへの通信を自社ドメイン経由に変更し、タグやイベントがブロックされる影響を軽減することを目的として、Googleタグゲートウェイを導入しました。 概要 対象のWebサイトでは、CDNとしてすでにAmazon CloudFrontを利用していました。 そのため、新たにサーバーを構築するのではなく、既存のCloudFrontを利用してGoogleタグゲートウェイへの通信を中継する構成を採用しました。 導入後の構成は以下のとおりです。 flowchart TB Browser[Webブラウザ] --> Domain[Webサイトと同じ自社ドメイン] Domain --> CF[Amazon CloudFront] CF -->|通常のWebコンテンツ| Origin[既存の配信先] CF -->|Googleタグゲートウェイ用のパス| GTG[Googleタグゲートウェイ] GTG --> GA4[GA4・Google広告など] Webサイト上にGoogleタグゲートウェイ用のパスを用意し、そのパスへの通信をCloudFrontからGoogle側へ転送します。 ブラウザから見ると、Googleタグの読み込み先や対象となる計測リクエストの送信先が、Googleドメインではなく、Webサイトと同じ自社ドメインになります。 Googleタグゲートウェイ導入前後の構成図 要素技術・アーキテクチャの紹介 ここからは、今回の導入で利用したGoogleタグゲートウェイとAmazon CloudFrontについて紹介します。 Googleタグゲートウェイとは? Googleタグゲートウェイ( Google tag gateway for advertisers )は、自社が管理するCDNやロードバランサーなどのインフラを利用して、Googleタグを自社ドメイン経由で配信する仕組みです。 通常はGoogleドメインから読み込まれるGoogleタグを、自社ドメイン上の特定のパスから読み込めるようにします。導入前後の経路の違いは、前述の「 概要 」節の構成図のとおりです。 Googleタグゲートウェイでは、GA4のイベント設計や、GTMコンテナ内のタグの処理内容を大きく変更するのではなく、Googleタグへアクセスする経路を変更します。 そのため、既存のGTMコンテナ内のタグ、トリガー、変数などを維持したまま導入できます。 Amazon CloudFrontとは? Amazon CloudFront は、AWSが提供するCDNサービスです。 Webサイトのコンテンツを利用者に近い拠点から配信することで、表示速度や可用性の向上に利用されます。 CloudFrontでは、アクセスされたURLのパスに応じて、異なる配信先へリクエストを振り分けられます。 今回はこの仕組みを利用し、Googleタグゲートウェイ用のパスへの通信のみを、Google側へ転送するようにしました。 サーバーサイドGTMとの違い Googleタグゲートウェイと混同しやすい仕組みに、サーバーサイドGTMがあります。 サーバーサイドGTMでは、サーバー用のGTMコンテナを構築し、サーバー側で計測リクエストを処理し、送信先を制御します。 一方、Googleタグゲートウェイは、Googleタグの読み込みや対象となる計測リクエストを、自社ドメイン経由に変更する仕組みです。 観点 Googleタグゲートウェイ サーバーサイドGTM 変更するもの Googleタグへの通信経路 計測リクエストの処理・送信先 追加で必要なもの CDN / ロードバランサーの設定 サーバー用GTMコンテナと実行環境 既存のWeb用GTMコンテナ そのまま利用 サーバー用コンテナ向けの設定追加が必要 今回の目的は、既存のGTM構成を維持しながら、Googleタグへの通信経路を変更することでした。 そのため、新たなサーバー用GTMコンテナは構築せず、既存のCloudFrontとWeb用のGTMコンテナを利用する構成を採用しました。 導入の進め方 各担当者の役割分担 今回の導入では、計測担当だけでなく、インフラ担当、プロダクトチームのサイト管理者と連携して進めました。 大きな役割分担は以下のとおりです。 DataOpsグループ・計測担当 Googleタグゲートウェイの導入目的と要件の整理 対象ドメインや対象環境の整理 Googleタグゲートウェイ用パスの検討 ブラウザやGA4を利用した動作確認 既存の計測への影響確認 導入前後のイベント数の比較 インフラ担当 CloudFrontへのGoogleタグゲートウェイ用設定の追加 Googleタグゲートウェイへの転送設定 検証環境と本番環境への設定反映 必要に応じたCloudFrontやWAFの調査 プロダクトチーム・サイト管理者 Webサイトに設置しているGTMコードスニペットの差し替え 検証環境と本番環境へのリリース Googleタグゲートウェイは、GTMやGA4だけで完結する機能ではありません。 CDNなどの配信基盤と、Webサイトへ設置しているGTMコードスニペットの両方に対応が必要です。 そのため、導入前に各担当者の作業範囲と、検証時の確認項目を整理しました。 GTMコンテナ側の設定は変更しない Amazon CloudFrontを利用した今回の構成では、GTMコンテナ側でGoogleタグゲートウェイ用の設定変更は行っていません。 既存の本番用GTMコンテナを、そのまま利用しました。 変更したのは、Webサイトに設置されているGTMコードスニペットの読み込み先です。 変更前 GoogleドメインからGTMコンテナを読み込む 変更後 自社ドメイン上のGoogleタグゲートウェイ用パスから 同じGTMコンテナを読み込む GTMコンテナ内のGA4タグ、Google広告タグ、トリガー、変数などは変更していません。 GTMコードスニペットの差し替えについては、Webサイトを管理しているプロダクトチームへ依頼し、検証環境と本番環境へ反映してもらいました。 通信経路のみを変更する構成だったため、既存の計測設計への変更を抑えて導入を進められました。 検証用GTMコンテナは作成しない 今回の導入では、Googleタグゲートウェイの検証用に、新しいGTMコンテナは作成しませんでした。 実際に本番環境で利用しているGTMコンテナを、検証環境でもそのまま利用しました。 導入の流れは以下のとおりです。 検証環境のCloudFrontへ設定 検証環境のGTMコードスニペットを差し替え 検証環境で動作確認 本番環境のCloudFrontへ設定 本番環境のGTMコードスニペットを差し替え 本番環境で動作確認 先に検証環境のCDNへGoogleタグゲートウェイの設定を追加し、プロダクトチームに検証環境のGTMコードスニペットを差し替えてもらいました。 そこで問題がないことを確認した後、本番環境のCloudFront設定とGTMコードスニペットを変更しました。 本番用のGTMコンテナをそのまま使用したことで、検証用コンテナと本番用コンテナの間に設定差分が発生しません。 実際に本番で使用するタグ構成のまま、通信経路の変更による影響を確認できました。 検証時のポイント Googleタグが自社ドメインから読み込まれているか 最初に、ブラウザの開発者ツールにあるNetworkタブを利用し、GTMコンテナの読み込み先を確認しました。 導入前はGoogleドメインから読み込まれていたGTMコンテナが、導入後は自社ドメイン上のGoogleタグゲートウェイ用パスから読み込まれていることを確認します。 導入前 https://www.googletagmanager.com/... 導入後 https://www.example.com/<Googleタグゲートウェイ用パス>/... 読み込み先だけでなく、HTTPステータスコードが正常であることや、ページ表示時にGTMコンテナが問題なく実行されていることも確認しました。 既存のタグがこれまでどおり発火するか Googleタグの読み込み先を変更しても、GTMコンテナ内のタグやトリガーが、これまでどおり動作する必要があります。 GTMのプレビューモードを利用し、以下を確認しました。 ページ表示時に必要なタグが発火すること クリックなどのイベントタグが発火すること GA4へ必要なイベントが送信されること イベントパラメータの値が導入前と変わっていないこと 同一イベントが重複して送信されていないこと 今回、GTMコンテナ内の設定自体は変更していません。 そのため、通信経路を変更したことで、既存のタグ発火やイベント送信に影響が出ていないかを中心に確認しました。 GA4でイベントを確認できるか ブラウザ上でタグが発火していても、最終的にGA4へイベントが到達していなければ、計測は成立しません。 GA4のDebugViewやリアルタイムレポートを利用し、以下を確認しました。 ページビューが送信されていること 主要なイベントが送信されていること イベントパラメータが欠損していないこと page_location などの値が正しいこと コンバージョンページのイベントが送信されていること 導入前と比較して、想定外の重複や減少がないこと Googleタグゲートウェイを経由した場合でも、GA4上では、これまでと同じイベント名やパラメータとして確認できます。 SPAの画面遷移も確認する 対象サイトには、ページ全体を再読み込みせずに画面を切り替えるSPA形式のページもありました。 SPAでは、初回表示時にGTMコンテナが正常に読み込まれていても、その後の画面遷移でページビューやイベントが正しく送信されるとは限りません。 そのため、以下を分けて確認しました。 ページを最初に表示したときの計測 ページ内で画面遷移したときの計測 画面遷移後の page_location 仮想ページビューの発火 前画面の値が dataLayer に残っていないか 同じイベントが重複して送信されていないか Googleタグゲートウェイ自体は通信経路を変更する仕組みですが、導入を機に、既存のSPA計測が正常に動作していることも合わせて確認しました。 プレビューモードだけで判断しない GTMのプレビューモードでは正常に見えていても、プレビューモードを利用しない通常アクセスでは、想定した通信が発生していない場合があります。 そのため、以下の両方で確認しました。 GTMのプレビューモードを利用した状態 プレビューモードを利用しない通常アクセス 問題が発生した場合は、以下の順番で確認すると、原因を切り分けやすくなります。 Webサイト上でGTMコードスニペットが実行されているか GTMコンテナが読み込まれているか 対象のタグが発火しているか ブラウザから計測リクエストが送信されているか CloudFrontへリクエストが到達しているか Google側へ転送されているか GA4でイベントを確認できるか ブラウザのNetworkタブにリクエスト自体が表示されていない場合は、CloudFrontより前のWebサイトやGTMの処理を確認します。 一方、ブラウザからリクエストが送信されているものの、エラーになっている場合は、インフラ担当と連携して、CloudFrontやWAFなどを確認します。 すべての通信が自社ドメイン経由になるわけではない 検証時には、ブラウザのNetworkタブからGoogle関連の通信を確認しました。 その結果、GA4の計測では自社ドメイン経由になっている通信を確認できましたが、一部の通信は、引き続きGoogleドメインへ送信されていました。 :::message Googleタグゲートウェイを導入したからといって、Googleに関係するすべての通信が自社ドメイン経由になるわけではありません。 ::: そのため、Googleドメインへの通信が完全になくなるかどうかではなく、対象となるGoogleタグや計測リクエストが、想定どおり自社ドメインを経由しているかを確認しました。 導入後の効果 Googleタグゲートウェイの導入後、タグの発火状況と、コンバージョンページのイベント数を確認しました。 タグの発火状況が改善した 導入前は、Googleタグの読み込みや計測リクエストをGoogleドメインへ直接送信していました。 導入後は、対象通信が自社ドメインを経由する構成へと変更されました。 これにより、広告ブロッカー等に起因するタグの未発火が抑制され、GA4へ送信されるイベント数が前月比・前週比で約10〜15%増加しました。 :::message 旧環境との完全な並行稼働による厳密な差分比較はできないため、前後期間の傾向に基づく推定値です。 ::: Googleタグゲートウェイを導入しても、すべてのユーザー環境で必ずタグが発火するようになるわけではありません。 例えば、以下のような要因によって、引き続きイベントが送信されない可能性があります。 JavaScriptが無効化されている ユーザーの同意状況によって計測が制限されている ページ表示が完了する前にユーザーが離脱した ブラウザやネットワークの状態によって通信に失敗した 一方で、Googleドメインへの直接通信がブロックされることによるイベント欠損については、一定の改善効果を確認できました。 GA4とバックエンドデータベースの数値差が縮小した 今回、導入効果を確認するため、GA4で計測したコンバージョンページのイベント数と、バックエンドのデータベースに保存されているコンバージョン件数を比較しました。 導入前は、バックエンドのデータベース上ではコンバージョンが記録されているものの、GA4では対応するイベントを確認できないケースがあり、両者の数値に大きな差がありました。 導入前 バックエンドDBのコンバージョン件数 > GA4のコンバージョンページイベント数 Googleタグゲートウェイの導入後は、GA4で確認できるコンバージョンページのイベント数が増加し、バックエンドのデータベースに記録されている件数へ大きく近づきました。 導入後 バックエンドDBのコンバージョン件数 ≒ GA4のコンバージョンページイベント数 バックエンドのデータベースとGA4では、計測の仕組みや集計条件が異なるため、両者の数値が完全に一致するとは限りません。 例えば、コンバージョン処理がバックエンドで完了していても、ユーザーが完了ページを表示する前に離脱した場合は、GA4のページイベントが発生しない可能性があります。 また、同意設定やブラウザ環境などによって、GA4への送信が制限される場合もあります。 それでも、導入後に両者の数値差が大きく縮小したことから、これまで欠損していた一部のイベントを取得できるようになったと考えています。 広告施策の評価に利用するデータが改善した コンバージョンイベントの取得数が増えたことで、GA4やGoogle広告で確認できるデータが、実際のコンバージョン状況により近づきました。 これにより、広告施策ごとの成果を、導入前より正確に評価できるようになりました。 また、Google広告へ連携されるコンバージョンデータの欠損が減ることで、自動入札や広告配信の最適化に利用できるデータ量の改善も期待できます。 ただし、広告の成果や配信精度は、以下のような複数の要因に左右されます。 広告予算 入札戦略 ターゲティング 広告クリエイティブ ランディングページ 市場環境 コンバージョン設定 そのため、Googleタグゲートウェイの導入だけで広告成果や広告精度が向上したとは断定していません。 今回の主な成果は、広告施策の評価や配信最適化に利用する、コンバージョンデータの欠損を減らせたことだと考えています。 振り返り GTMコンテナ内の変更を抑えて導入できた 今回の構成では、GTMコンテナ内のGA4タグ、Google広告タグ、トリガー、変数などを変更していません。 CloudFront側の通信経路と、Webサイトに設置しているGTMコードスニペットの読み込み先を変更することで導入できました。 既存の計測設計への影響を抑えながら導入できた点は、大きなメリットでした。 検証環境から段階的に導入することが重要 いきなり本番環境へ反映するのではなく、先に検証環境のCloudFront設定とGTMコードスニペットを変更しました。 検証環境では、GTMコンテナの読み込み、GA4イベント、SPA遷移、コンバージョンページなどを確認しました。 問題がないことを確認した後に本番環境へ展開することで、既存計測への影響を抑えながら導入できました。 複数チームの連携が必要 Googleタグゲートウェイは、GTMやGA4だけでなく、CloudFrontなどの配信基盤と、Webサイトに設置されるコードにも関係します。 今回の導入では、DataOpsグループ、インフラ担当、プロダクトチームがそれぞれ以下を担当しました。 DataOpsグループ:要件整理、計測確認、効果検証 インフラ担当:CloudFrontの設定 プロダクトチーム:GTMコードスニペットの差し替え 事前に担当範囲を整理したことで、検証環境から本番環境への展開をスムーズに進められました。 レイヤーごとに切り分けることが重要 Googleタグゲートウェイ導入後の通信には、複数の要素が関係します。 Webサイト ↓ GTMコードスニペット ↓ GTMコンテナ ↓ ブラウザ ↓ Amazon CloudFront ↓ Googleタグゲートウェイ ↓ GA4・Google広告など 問題が発生した場合は、最初からすべてを調査するのではなく、どこまで正常に処理されているかを順番に確認することが重要です。 ブラウザのNetworkタブ、GTMのプレビューモード、GA4のDebugViewなどを組み合わせることで、問題がWebサイト側にあるのか、GTMにあるのか、インフラ側にあるのかを切り分けやすくなりました。 おわりに 今回は、Amazon CloudFrontを利用して、Googleタグゲートウェイを導入した事例を紹介しました。 Googleタグゲートウェイの導入だけで、すべてのイベント欠損を防げるわけではありません。 一方で、Googleドメインへの直接通信がブロックされることによる欠損を軽減し、サイト改善や広告施策の判断に利用するデータを、実態に近づける効果を確認できました。 Amazon CloudFrontを利用してGoogleタグゲートウェイの導入を検討されている方にとって、本記事が少しでも参考になれば幸いです。
こんにちは、モバイルアプリ開発部の Yao です。 Web サイトの URL とアクセス認証情報を渡すと、サイトをクロールして API 仕様を逆生成し、Compose Multiplatform(CMP)の Android/iOS アプリを自動生成して、原サイトとのスクリーンショット比較で検収まで行う——そんなローカル AI パイプラインを作り、実際に自社サービス KINTO のステージング環境を動くアプリに変換してみました。結論から言うと、 アプリファーストはもはや予算の問題ではなく、ツールチェーンの問題 です。本記事ではパイプラインの設計、生成されたアーキテクチャ、実際のコード、原サイトとの並列比較スクリーンショットまで、その全工程を紹介します。 この記事でわかること Web サイトは、それ自体が 仕様書 (ページとフロー)+ テストデータ (実 API トラフィック)+ 受け入れ基準 (モバイルスクリーンショット)である ローカル AI パイプラインが「クロール → 仕様のリバースエンジニアリング → コード生成 → スクリーンショット照合検収」を閉ループで回す 成果:16 画面の仕様化、P0 の 4 画面をエンドツーエンド実装、API 31 オペレーション生成、 テスト 107 本全グリーン 、Android エミュレーター + iPhone シミュレーターで動作 DTO は録画済み JSON から機械的に導出し、 バックエンドを想像で補わない オフラインデモ(Recorded モード)は同一の Ktor クライアントを MockEngine に載せ替えるだけで、 プロダクションのパース経路をそのまま通る :::message alert 本記事のパイプラインは 自社サイト (認証情報と権利を自分が持つサイト)を変換するためのものです。他社サイトのスクレイピングに使うものではありません。クロール対象はすべて自社のステージング環境です。 ::: 1. なぜアプリファーストは避けられないのか まずデータから。日本では、スマートフォンでインターネットを利用する個人の割合が 2016 年の 57.9% から 2025 年には 74.3% へ上昇し、同期間に PC は 58.6% → 45.6% に下落しました( 総務省 通信利用動向調査 )。 利用時間の差はさらに顕著です。全年代のインターネット平均利用時間(平日)はモバイル機器が 116.8 分/日 に対してパソコンは 50.0 分/日 と 2 倍以上、休日には 133.5 分 vs 23.6 分 と 5 倍超まで開きます( 総務省情報通信政策研究所「令和6年度 情報通信メディアの利用時間と情報行動に関する調査」 )。 さらにスマートフォンの上では、ユーザーはブラウザではなくアプリの中にいます。米国の成人はモバイルのインターネット利用時間の 約 9 割 をアプリに費やしています( eMarketer )。 米国市場では、この議論は 10 年以上前に決着しています。 Instagram :長年、Web 版は閲覧専用だった Uber :Web クライアントはアプリを動かせない端末向けの軽量フォールバック Venmo :2018 年に決済機能を Web サイトから丸ごと撤去 プッシュ通知はリテンションの生命線、ホーム画面のアイコンはブランドの恒久的な陣地、OS 統合(決済・生体認証・ウォレット・共有シート)は当然の前提。あちらでは、サービスにアプリが「要るかどうか」を問う人はもういません。 「PWA でいいのでは?」という反論には先に答えておきます。Web Push は確かに存在します(iOS も 16.4 からホーム画面追加済みの Web アプリに開放)が、ユーザーがまずページを手動で「インストール」する必要があり、配信の信頼性も OS 統合の深さもネイティブプッシュには遠く及びません。10 年経ってもギャップは開いたままで、リテンションの勝負は依然ネイティブ側で決まっています。 2. では、なぜ皆やっていないのか 古典的なコスト表が残酷だからです。 項目 内容 デザイン工程 コードを書く前に、Web の体験をモバイル向けに再設計 コードベース × 2 Swift と Kotlin で同じプロダクトを二度実装 チーム × 2 2 種類のスキルセットで採用 ロジックの二重化 同じバグを二度直す。同じ機能が別々の時期に出る。仕様合わせ会議が永遠に続く その上に QA 倍増 + アプリストア運用 本当の急所は v1 ではありません。 v1 以降のすべての機能が、永遠に二度ずつリリースされる ことです。この繰り返し課税が、「Web で十分」を先送りから既定方針へと硬化させてきました。 3. 計算式を変える 2 つの技術 最近 2 つのことが起き、しかも互いに増幅し合っています。 AI が労働コストを消した。 エージェントは既存の Web サイト——レンダリング済みページ、スクリーンショット、実 API トラフィック——を読み取り、その 証拠 からアプリを書けるようになりました。Web サイトはすでに仕様書でありテストデータであり受け入れ基準です。あとは誰かが読むだけ。エージェントは疲れずに読みます。 CMP が二重化コストを消した。 1 つの Kotlin コードベースから Android アプリと iOS アプリが生まれます。「すべての機能を二度リリースする」税は、規律ではなく 構造 によって消滅します。 ここで決定的なのは AI 側の形態です。魔法のプロンプト 1 発ではなく、 クロール → 仕様のリバースエンジニアリング → 仕様からコード生成 → スクリーンショットで照合検収 という パイプライン であること。以降はこれを実サイトに適用した記録ですが、その前に 2 つの設計判断——なぜ ローカル なのか、なぜ CMP なのか——を片付けます。 4. なぜ「ローカル」AI パイプラインなのか クラウド型の変換 SaaS が不可能とは言いませんが、ローカルを選んだ理由は 2 つ、どちらもモデル性能とは無関係です。 ① 権限と認証情報がローカルに留まる。 使うのは環境変数から読む認証情報だけ(gitignore 済みの .env と Playwright のログイン状態)。今回スコープに入れた 16 画面はすべて公開ページで、セッションなしでも到達できます(一部のパスには別系統のアクセス制限がかかっており、そちらの認証を渡していないので後述の 401 になります)。社外に預ける判断は重く、自分のラップトップで動くなら ただの社内ツール で済みます。 境界も明確です。オーケストレーション(今回は Claude Code)・スクリプト実行・認証情報はローカル、 モデルはクラウドなのでページ内容と API サンプルはマシンを離れます 。クロール対象は会員エリアを含まない公開相当のページに限り、API ログはマスキング済みです。 ② ローカルのモバイル開発環境にそのまま繋がる。 Xcode、Android SDK、エミュレーターが同じマシンにあるので、 クロール → 仕様 → コード生成 → ビルド → スクリーンショット検収を 1 回の実行で通せます 。同じパイプラインがエミュレーターを操作して撮影し、原サイトと並べた差分を P0/P1/P2 に分類、深刻なものはループ内で修正(iOS の操作は現状手動)。クラウドでやるなら、2 つの OS のビルドツールチェーンとデバイスファームを自前で抱えることになります。 ローカルかどうかとは独立した、パイプライン自体の方針も 2 つ。 証拠であって、推測ではない。 Playwright が収集するのは レンダリング済み の 60 ページ——HTML、スクリーンショット、 実 API トラフィック (マスキング済みサンプル)。仕様はここから導かれます。 成果物は契約、フェーズはゲート。 各フェーズは成果物を書き出してから次へ( artifacts/ → spec/ → app/ → report/ )。決定的に実行できる工程はエージェント自身のスクリプトで、各ゲートを人間が見ます。 :::message パイプラインの鉄則: バックエンドを想像で補わない。 トラフィックで観測されなかったフィールドは、存在しない。 ::: 5. なぜ Compose Multiplatform なのか 私は KMM の時代から KMP / CMP を追いかけてきました( これまでに書いた記事はこちら )。今回ターゲットに CMP を選んだ理由は 3 つです。 一貫性。 KMP のデータ層とドメイン層が 1 つで両 OS に仕えるので、iOS と Android のビジネスロジックは 乖離しようがない 。実装が 1 つ、テストスイートも 1 つだからです。 効率。 2 倍ではなく約 1 倍の工数。v1 でも、それ以降のすべての機能でも。1 チームで両ストアに出荷。 性能。 Android では——日本はさておき世界のスマホの過半です——Compose は Google が Android の標準 UI ツールキットとして提供しているものそのもので、ファーストパーティアプリと同じ ART 上、同じ描画パイプライン上で動きます。つまり CMP アプリの Android 側は、 プラットフォーム標準の外に別のレンダリング層を積みません (Compose 自身は View ツリーではなく自前のコンポジションを描画しますが、その先は Android のグラフィックススタックです)。iOS では CMP も Skia で描画します——独自エンジンを積むという点は他のクロスプラットフォームフレームワークと同じアーキテクチャ上の賭けです——が、Kotlin/Native が共有コードをマシンコードにコンパイルし、Android 側は標準ツールキットのまま。今回ベンチマークを取ったわけではないので優劣は断言しませんが、 Android 側で追加の描画層を背負わない という構造上の差は残ります。 スライドに収まらないが実務で効く理由: 既存チームには Android 側がタダ同然。 Compose はすでに Android の標準。CMP アプリの Android 半分は、いまの Android エンジニアが初日から読める慣用的なネイティブコード。Flutter は全員に Dart 乗り換えを要求します。 段階導入できる。 データ層だけの共有も、UI 全体の共有も可。既存ネイティブアプリへの埋め込みも SwiftUI/UIKit との相互運用も可。移行の道筋であって「全面書き直しか、さもなくば」ではない。 両陣営のファーストパーティが支える。 CMP は JetBrains 製。Google は KMP を公式サポートし、自社プロダクションで使用し( Google Docs が採用済み 、Workspace 他アプリも追随中)、androidx の KMP 版(Lifecycle / ViewModel / Room / DataStore。Navigation の CMP 版は JetBrains 管理)を出荷。CMP の iOS 対応は 2025 年 5 月、1.8.0 で stable 到達 。プラットフォームの所有者と言語の生みの親が同じスタックに収斂している——次の 10 年の投資先を示すこれ以上ないシグナルです。 プラットフォーム API に直接アクセス。 expect/actual で Keychain / EncryptedSharedPreferences をネイティブに呼ぶ。待たされるプラグイン層なし。 スタック全体が 1 言語。 Kotlin で端から端まで、Ktor でバックエンドまで。採用市場もツールチェーンも成熟。 そして本記事全体を貫く理由がこれです。 :::message CMP は AI コード生成の理想的なターゲットである。 強く型付けされた単一コードベースでは、コンパイラがエージェントの全出力に対する無料の検証器になる。 kotlinx.serialization の DTO は記録済み JSON から機械的に導出できる。1 回の生成が全プラットフォームをカバーする。AI と CMP は足し算ではなく 掛け算 で効く。 ::: 6. ケーススタディ:KINTO ステージング → 動く CMP アプリ 6.1 パイプライン全体像 5 フェーズ。各フェーズは成果物を書き出してから次へ進み、フェーズ間に人間のレビューゲートがあります。 CRAWL — サイトをモバイル開発のリファレンスに変える 1 ページを 4 つの成果物として記録します。 モバイル(390×844 @2x、iPhone UA、 isMobile / hasTouch )とデスクトップ(1440×900)のフルページスクリーンショット、 networkidle 後の レンダリング済み DOM 、そのページが実際に叩いた API、そして各エンドポイントのレスポンス実体。モバイル側が変換の正解データで、Phase 5 の検収でも同じ画像と並べます。 サイトのモバイル表示がそのまま設計書になる ので、「モバイル向けに作り直す」工程が消えます。 API はページ単位で記録します。 各エントリに「どのページが引き起こしたか」が付くので、画面 → 必要データ → エンドポイントが機械的に繋がります(見積りシミュレーションの ?step=1&memberType=corporate&term=7&bonus=0 が car-models/{carCode}/selectable-contract を叩く、という粒度で)。値を型トークンに置き換えたレスポンス形状( float / iso8601 / url / null )も保存し、これが DTO の型付けの根拠になります。実体サンプルはエンドポイントごとに最大 3 件、ハッシュで重複排除——複数サンプルの和集合が「どのフィールドが nullable か」を決め、同じファイルが後で MockEngine のリプレイと契約テストにも使われます。 URL をテンプレート化して画面を数えます。 パス 1 セグメントずつ、識別子らしいものを型に畳むだけです。 function templatize(pathname: string): string { return pathname.split("/").map((seg) => { if (/^\d+$/.test(seg)) return "{id}"; if (UUID_RE.test(seg)) return "{uuid}"; // CAR-0000003908 → {carCode}、GRD-… → {grdCode} if (PREFIXED_CODE_RE.test(seg)) return `{${seg.split("-")[0].toLowerCase()}Code}`; if (isOpaqueToken(seg)) return "{token}"; return seg; }).join("/") || "/"; } これで同型ページは最大 3 インスタンスだけ辿れば済み、60 ページは 58 テンプレートに整理されて、これが 16 画面の候補になります。ページごとの outlinks はナビゲーショングラフに、 img / table / iframe /繰り返しカードの構造シグネチャはコンポーネント一覧に、参照されている CSS とフォントは実ファイルとして取得してデザイントークンに回します。 Phase 2 で、サイトは機械可読な契約に変わります。 app-spec.json の 16 画面それぞれに、ソース URL・優先度・コンポーネント・必要データ・明示的なモバイル適応プランが与えられます。本件のようにサイトがすでにレスポンシブ対応済みなら、この適応設計はモバイルビューポートのクロール結果から直接導出され、 パイプラインに吸収されます 。 { "id": "Home", "priority": "P0", "components": ["TopBar", "HeroCarousel", "CarCard", "SectionHeader", ...], "dataNeeds": ["CarouselSlide", "RecommendedCarList", "CorpContent", ...], "mobileAdaptation": "Hero carousel → HorizontalPager with page indicator, 16:9 slides using the imageUrl_x2 asset. The desktop 3-column grid collapses to 2 columns ... 51 images on this page: everything below the first two sections must be lazily loaded via AsyncImage." } 念のため補足すると、この抜粋は 実ファイルのままで、正しい仕様ではありません 。 16:9 slides という横長前提が誤りで、CMS が実際に配信しているのは正方形素材でした。6.4 で触れるレターボックス不具合(実装は 16:10 の枠になっていました)の根っこは、この一行です。仕様そのものも検収ループの検査対象だということです。 API 契約は記録トラフィックから合成した OpenAPI で、そのことをファイル自身がヘッダーで宣言しています。 info: title: KINTO (staging) — reverse-engineered API description: |- Reverse-engineered from real traffic captured in artifacts/api-log.json. Nothing here is invented: every schema is the union of observed samples, a field missing from any sample is nullable. No mutation endpoints exist in this spec: the crawl observed GET traffic only. :::message この仕様は自身のカバレッジの穴も正直に記録しています。取得できたのは 60 ページで、それとは別に 12 の URL は取得できませんでした——404 が 6、アクセス制限による 401 が 6( /kinto_one/lineup/ と SUBARU 系。4 章で触れた別系統の認証が必要なパスです)。取得できなかったものは「未取得」として仕様に残し、その画面には手を出していません。 ::: デザイントークンも同じ証拠基準です。色・タイポグラフィ・余白はサイトの 24 個の CSS ファイル(2.3MB)から出現頻度順に抽出、WCAG コントラスト比は 計算 済み。ブランドカラーがモバイルで AA を満たさない箇所には、トークンファイルがアクセシブルな代替色を用意してテキストへの使用を強制します。 6.2 生成されたアーキテクチャ Clean Architecture + MVI、すべて commonMain です。 押さえるべきポイントは 1 つだけ。 :::message Recorded モードはモック実装ではない。 第二のコードパスは存在しない。同じ生成済み ApiClient 、同じ JSON 設定、同じマッパーが両モードで動き、Koin が差し替えるのは足元の Ktor エンジンだけ。 ::: メカニズムの全文がこれです。 single<HttpClientEngine> { val config: AppConfig = get() when (config.dataMode) { DataMode.Recorded -> recordedEngine(get()) // MockEngine replaying the crawl DataMode.Live -> platformHttpEngine() // OkHttp / Darwin } } つまりオフラインデモは プロダクションのパース経路をそのまま通ります 。実ペイロードをパースできない DTO は、ライブサーバー相手とまったく同じように Recorded モードでも失敗します。 もう 1 つ、どんなコードレビューでも擁護したいディテール:リクエストに合致するサンプルがないとき、リプレイエンジンは空の 200 ではなく 501 を返します。沈黙の成功を許すと、実際には何も流れていない画面が「実装済み」に見えてしまうからです。 ビジネスロジックは 100% commonMain 。 expect/actual は本当に避けられない箇所(セキュアなトークン保存 → Keychain / EncryptedSharedPreferences、プラットフォーム HTTP エンジン)にのみ現れ、各箇所に理由がドキュメント化されています。 6.3 AI が生成した CMP コードの実物 DTO は導出されるもので、幻覚ではない。 車種カタログのエンドポイントは、CMS 風味の混沌としたネーミングのフィールドを 80 個以上持つオブジェクトを返します。ジェネレーターは記録済みサンプル全体の和集合を取ります。全サンプルに存在するフィールドは非 null、どれか 1 つでも欠けていればデフォルト値付き nullable。 @Serializable data class CarCatalogEntryDto( val name: String, // in every sample → required @SerialName("fuel_label") val fuelLabel: String? = null, // absent in some → nullable @SerialName("year3_bonus0m_taxin") val year3Bonus0mTaxin: String, // … 80+ fields, mechanically derived from recorded JSON ) このクラスを喜んで手書きする人間はいません。書く必要もありませんでした——そして、どのフィールドも 推測 されていません。判定はこの 1 行だけです。 // そのフィールドが現れたオブジェクト数 === 観測したオブジェクト数 のときだけ required const req = (s.propSeen?.get(k) ?? 0) === s.seen; パイプラインの原理はここに凝縮されています: 記録 → 全サンプルの和集合 → 型 → コード 。どの矢印もモデルの判断ではなくスクリプトの計算なので、同じ記録を入れれば同じ DTO が出ます。モデルの仕事は、この機械的な出力の上に画面を組むことだけです。 API 面は型付きで、自分の出自に正直。 生成されたクライアントの全関数が、どの記録済みリクエスト由来かをドキュメント化しています。 /** * Contract terms and bonus-payment options available for a car model * Recorded as: GET /api/price/v1/web/car-models/{carCode}/selectable-contract */ suspend fun getSelectableContract(carCode: String): ApiResult<SelectableContractDto> = runCatchingApi { client.get(hosts.main + "/api/price/v1/web/car-models/$carCode/selectable-contract") .bodyOrThrow() } すべての画面が同じ固定の MVI 形状。 sealed な Intent が入り、 StateFlow が出て、ナビゲーションはワンショットの Effect。 sealed interface HomeIntent { data object Load : HomeIntent data class CarClicked(val car: CarSummary) : HomeIntent // … } class HomeViewModel( private val getHomeFeed: GetHomeFeedUseCase, private val hosts: ApiHosts, ) : ViewModel() { private val _state = MutableStateFlow(HomeUiState()) val state: StateFlow<HomeUiState> = _state.asStateFlow() fun onIntent(intent: HomeIntent) { /* exhaustive when */ } } この均一性は美学ではなく、 画面生成を安全に並列化できる理由そのもの です。テーマ・ナビゲーショングラフ・共有コンポーネントが契約としてコミットされた後は、独立したエージェントが 1 画面ずつ引き受けられます。 契約テストが、すべてを現実に釘付けにする。 クライアントと同時に生成され、オペレーションごとに 1 本、 すべての 記録済みサンプルをプロダクションのクライアントに通します。 /** Contract terms and bonus-payment options — 3 recorded sample(s). */ @Test fun `getSelectableContract parses every recorded sample`() = runTest { val results = ContractTestEnv.forEachSample("getSelectableContract") { env -> env.api.getSelectableContract(carCode = SAMPLE_CAR_CODE) } // サンプル 0 件のまま緑になる事故を防ぐ。件数は生成時に KDoc と同じ値で埋め込まれる assertTrue(results.isNotEmpty(), "no recorded sample was replayed") assertEquals(3, results.size, "recorded sample count changed") results.forEach { assertTrue(it is ApiResult.Success, "failed to parse: $it") } } 件数のアサートは、あとから付け足した保険ではありません。 forEachSample が空リストを返すと——オペレーション ID の綴りを 1 文字間違えるだけで起こります—— forEach は何も検証せずに通ってしまい、「107 本グリーン」が何も意味しなくなります。生成器はサンプル数を知っているので、その数をテストに焼き込みます。 テスト環境は意図的にプロダクションのオブジェクトを使います——本物のクライアントファクトリ、本物の JSON 設定、本物のリプレイエンジン。スイート全体(生成契約テスト + mapper + 回帰)で 107 テスト、失敗 0 。釘付けにしているのは記録された現実なので、後日ライブ API がずれれば、次の再クロール時に同じスイートが捕まえます——本番クラッシュではなく、 赤くなったテスト として。 6.4 Web vs アプリ、並べて比較 検収ループは Android エミュレーター(API 35、1080×2400)上でアプリ自身のナビゲーションを操作し、実装済み画面を撮影します。全工程 Recorded モードなので、ライブ API には一度もリクエストを出していません(画像だけはサイトの CDN から読み込みます)。 各ペアは原サイトとセクション単位で照合します。上の 4 枚で実際に確認できるのは、ホームのヒーローコピーと CTA、PICK UP セクションの実 CMS バナー(後述の iOS スクリーンショットでは 14 枚ぶんのページインジケーターが見えます)、ラインアップの TOYOTA / LEXUS タブとカテゴリチップ、車種詳細の月額料金と納期目処、見積りのプラン選択と下部固定サマリーバーです。差分は P0/P1(コンテンツ欠落、構造・ナビゲーション破損)→ ループ内で修正、P2(ピクセルレベルの磨き込み)→ 記録、と分類処理します。 このスクリーンショットは差分も正直に写しています。ホームのヒーローは、原サイトでは背景ビジュアルに商品写真の装飾イラストが載っていますが、アプリ側はコピーと CTA だけのテキストブロックになっています。装飾素材がクロールの成果物から機械的に取り出せなかったためで、現時点では既知の差分(8 章の P2 リスト)です。 ループが実際に機能した例も 1 つ。ホームのカルーセルは当初バナーを 16:10 の枠にレターボックス表示していましたが、CMS が実際に配信しているのは 630×630 の正方形素材でした。スクリーンショット比較で発覚 → 修正 → 再撮影 → 解消。 :::message スクリーンショット内の料金・納期・キャンペーン表記は、 2026 年 8 月時点のステージング環境の内容 です。実際のサービスの料金や取扱内容は変わります。最新の情報は KINTO 公式サイト をご確認ください。 ::: そして同じコードベースが、 画面コードの追加ゼロ で 2 つ目のプラットフォームに乗ります。 ![同じホーム画面が iPhone シミュレーターで動く様子](/assets/blog/authors/yao.xie/2026-09-24/web2cmp-ios-home.png =400x) 6.5 正直な数字ボックス 項目 実績 仕様にリバースエンジニアリングされた画面数 16 (優先度・コンポーネント・必要データつき) 現時点でエンドツーエンド実装済みの画面数 4 ——P0 セット:Home、Lineup、CarDetail、Estimate 生成された API オペレーション数 31 、4 ホスト横断——すべて GET。ミューテーションは観測されず、想像で足してもいない テスト(契約 + mapper + 回帰) 107 本、全グリーン 動作環境 Android エミュレーター + iPhone シミュレーター——ライブ API 不使用。画像のみサイトの CDN から読み込み 検収中のライブ API への接触 一度もなし (画像取得のみ CDN にアクセス) :::message alert セキュリティに関わる 3 つの決定: DTO は記録データからのみ導出(バックエンドを想像で補わない) ミューテーション系エンドポイントは、仮に観測されても生成 + モックカバレッジまで。明示的な承認なしにライブサーバーへは撃たない 認証情報(ステージングへのアクセス認証。会員ログインではない)は gitignore 済みの環境ファイルにのみ存在し、実行時に環境変数として読み込む。コードにもモデルのコンテキストにも入らない ::: 7. AI を信頼できるものにした工学(デモの手品ではなく) モデルの能力は必要条件であって、十分条件ではありません。実際に重さを支えたのは、その周りを固める工学でした。 固定アーキテクチャという契約。 Clean Architecture + MVI、Koin、Ktor、Coil——生成の前に決定済み。エージェントは即興で形を発明せず、既知の形に中身を埋める。だから成果物はレビュー可能で、失敗は診断可能。 手作業よりスクリプト。 決定的に実行できるものはすべて、エージェントが書いて自分でデバッグするスクリプト。スクリーンショット取得スクリプトも、アプリのコードと同じくパイプラインの産物。 全コミットにゲート。 iOS ターゲットは全コミットでコンパイル必須——クロスプラットフォームの腐敗は最後ではなく数分で捕まる。 1 画面 = 1 タスク = 1 コミット。 並列化は共有契約(テーマ、ナビゲーション、コンポーネント)のコミット後にのみ解禁。 検収は事後ではなくループの内側に。 人間がレビューする前に、エージェントのスクリーンショットは原サイトのスクリーンショットと対決を済ませている。 この 5 つの根底にある姿勢は 1 つです。 :::message 生成ステップは信頼しない——包囲する。 決定的にできるもの(スクリプト、コンパイルゲート、契約テスト)はすべて決定的にし、エージェントの自己採点によるスクリーンショット比較は、すべてのゲートで人間が再レビューする。 ::: 8. 限界を、正直に 16 画面中 12 画面はまだプレースホルダー。 画面単位で線形にスケールする 作業 であって、一発の魔法ではない。 埋め込みコンテンツ (動画、地図、サードパーティウィジェット)はプレースホルダー + expect/actual の移行プランどまり。共通コードに WebView という逃げ道はない。 見た目の既知の差分(P2)。 クロールで取得できないアイコン素材とヒーローの装飾イラスト、フォントウェイトの機微。見積り画面は固定バーぶんの content inset がなく、下端のコンテンツが隠れます(6.4 のスクリーンショット)。iOS のジェスチャーの質感は実機検証が必要。 クロールが捉えるのはハッピーパス。 エラー状態、深いページネーション、パーソナライズ、A/B バリアントは記録に含まれない。 読み取り専用。 31 オペレーションすべて GET、会員エリアはスコープ外。見積りの「保存」「次へ」もアプリ内では書き込まず、選択内容を積んだ URL で原サイトに引き渡します—— ないものは想像で補わず web に戻す 。トランザクション系は次フェーズで、多くのプロダクトにとってはそちらが難しい半分。 適用範囲は自社サイトのみ。 認証情報と権利を自分が持つサイト向けの道具であって、他社プロダクトのスクレイピング道具ではない。 9. ロードマップにとっての意味 コスト曲線は反転しました。かつてアプリ化で最も高くついた部分——モバイル再デザイン(サイトがレスポンシブ版を持つ場合)、ボイラープレート、二重のデータ層、仕様合わせの維持——が、いまや最も安い部分です。人間の労力は本当に判断が要る場所に集中します:どの画面が P0 か、ブランドは何を要求するか、何を Web に残すか。 現実的なチーム像: エンジニア 1 人 + パイプライン で、両プラットフォームで動くレビュー可能なビルドに到達(本記事の範囲では P0 の 4 画面まで。残り 12 画面は同じ手順の反復です)。ネイティブのスペシャリストは最後の 10%(ジェスチャーの質感、埋め込みプレイヤー、ストア向けの磨き込み)で合流。「2 チームで 1 年」とはまったく別次元の予算会話です。 まとめ アプリファーストは、予算の問題であることをやめ、 ツールチェーンの問題 になりました。 あなたがすでに運用しているその Web サイトは、実は 3 つのものを兼ねています。 仕様書 (ページとフロー) テストデータ (実 API トラフィック) 受け入れ基準 (モバイルスクリーンショット) AI パイプラインの仕事はこの 3 つを読み込むことだけ。Compose Multiplatform の仕事は、その結果をすべての端末で動かすことだけ。 プロダクトの居場所とユーザーの居場所のあいだの距離は、いまや年単位ではなく 日単位 です。埋めにいきましょう。 参考リンク 総務省 通信利用動向調査(令和7年調査) 総務省情報通信政策研究所 令和6年度 情報通信メディアの利用時間と情報行動に関する調査(概要) eMarketer — Mobile Internet: Average Time Spent in the US, App vs. Browser JetBrains — Compose Multiplatform 1.8.0: iOS Is Stable and Production-Ready Android Developers Blog — Android's Kotlin Multiplatform announcements at Google I/O and KotlinConf 25 WebKit — Web Push for Web Apps on iOS and iPadOS
こんにちは。KINTO テクノロジーズの DBRE チーム所属の makoto.sato です。2026年3月に中途入社し、普段は Aurora(MySQL / PostgreSQL)の運用・信頼性向上や、社内向け DB 作業基盤の開発をしています。前職では長く DBA としてオンプレの MySQL / PostgreSQL / Oracle を運用していました。 はじめに KINTO テクノロジーズの DBRE チームでは、DB 作業の申請〜承認〜一時的な作業環境の自動構築までを Slack から完結させる社内ツール「PowerPole」を開発・運用しています。全体像は Amazon Web Services ブログの クルマのサブスク「KINTO」のアジリティとガバナンスを両立した DBRE の取り組み で紹介しました。 仕組みをかいつまんで言うと、Slack から申請 → 承認 → EC2 と一時的な DB 認証情報が自動発行 → 作業完了後に自動削除、という流れで、踏み台構築から利用終了までのガバナンスと作業スピードを両立させるものです。 PowerPole はこれまで Aurora MySQL のみ対応でしたが、社内での Aurora PostgreSQL 利用の広がりに合わせて PostgreSQL 対応を進めることになりました。本記事では、その中で遭遇した「権限を付けたはずなのに効かない」問題と、その根底にあった MySQL と PostgreSQL の権限モデルの違いについて書きます。 なお、扱う挙動はすべてコミュニティ版 MySQL / PostgreSQL 共通の仕様で、Aurora 固有のものではありません。検証環境は Aurora PostgreSQL 17 と MySQL 8.0 です。MySQL のロール機能は 8.0 で追加されたものなので、5.7 では本記事の MySQL 側の記述は成立しません。 CREATEROLE は PostgreSQL 16 で意味が大きく変わっているため、15 以前の環境では本記事の構成をそのまま適用しないでください。 症状: \du には権限が見えているのに、権限エラーになる あるロールに CREATEDB を付与し、 \du (psql でロールの一覧と属性を表示するコマンド)でも確かに付いていることを確認しました。 List of roles Role name | Attributes -----------+------------------------- dbuser | Create role, Create DB ところが、その dbuser で接続してデータベースを作ろうとすると失敗します。 CREATE DATABASE test_db; -- ERROR: permission denied to create database 属性は付いている。しかし効いていない。一見矛盾した状況です。 MySQL 版ではどう書いていたか PowerPole には、申請時に選べる権限レベルの 1 つとして「DDLOperator」があります。テーブルやデータベースの作成・変更を一時的に許可する権限レベルです。 MySQL 版では、 pp_ddl_operator という共有ロールに必要な権限をすべて集約し、発行する一時ユーザーにこのロールを付与しています(一部抜粋)。 CREATE ROLE 'pp_ddl_operator'; GRANT CREATE, ALTER, DROP, SELECT, INSERT, UPDATE, DELETE, INDEX, CREATE VIEW, CREATE ROUTINE, ALTER ROUTINE, CREATE TEMPORARY TABLES ON *.* TO 'pp_ddl_operator'; GRANT CREATE USER ON *.* TO 'pp_ddl_operator'; -- ユーザーとロールの作成・削除 GRANT ROLE_ADMIN ON *.* TO 'pp_ddl_operator'; -- ロールの付与・剥奪 -- 一時ユーザーへロールを付与し、次回ログイン時に自動で有効になるよう既定ロールにする -- (MySQL は activate_all_roles_on_login が既定 OFF なので、付与しただけでは有効にならない) GRANT 'pp_ddl_operator' TO 'dbuser'; ALTER USER 'dbuser' DEFAULT ROLE 'pp_ddl_operator'; ここで注目してほしいのは、 権限の種類が違っても書き方が変わらない 点です。テーブル操作のようなオブジェクト権限も、DB 作成やユーザー作成のようなサーバー全体に関わる能力も、すべて同じ GRANT ... ON *.* で表現できています。 PostgreSQL 版でも同じ設計を踏襲し、共有ロールに権限を集約して一時ユーザー dbuser にロールを付与する構成にしました。テーブルやスキーマへのオブジェクト権限は GRANT で集約でき、ここまでは順調でした。 つまずき①: GRANT CREATEDB という書き方はできない 問題は「データベース作成」「ロール作成」でした。MySQL の感覚でこう書きたくなりますが、エラーになります。 GRANT CREATEDB TO pp_ddl_operator; -- ERROR: role "createdb" does not exist PostgreSQL の GRANT <名前> TO <ロール> は ロールメンバーシップの付与 として解釈されます。PostgreSQL は「createdb という名前のロール」を探し、存在しないためエラーになります。 さらに紛らわしいのが次の書き方です。構文としては通りますが、意味はまったく違います。 GRANT CREATE ON DATABASE mydb TO pp_ddl_operator; -- 通るが「DB作成権限」ではない これは「mydb の 中に スキーマなどを作る権限」であって、データベースを作る権限ではありません。 データベース作成・ロール作成は、 GRANT とは別の「 ロール属性 」という仕組みで表現されていて、 ALTER ROLE (または CREATE ROLE 時のオプション)でしか付与できません。 ALTER ROLE <ロール名> CREATEDB CREATEROLE; つまずき②:属性を付けたのに効かない ALTER ROLE で付ければよいと分かったので、次はどのロールに付けるかです。最初は一時ユーザーの dbuser に付与しました。 ALTER ROLE dbuser CREATEDB CREATEROLE; dbuser は一定期間を過ぎると削除される使い捨てのユーザーなので、属性もユーザーごと消えます。常設の共有ロールに永続的な属性を持たせるより影響範囲が狭く、妥当な選択に思えました。 その結果が、冒頭に書いた「 \du には見えているのに権限エラー」でした。 原因調査 PowerPole が生成しているユーザー作成処理では、このように権限を付与しています。 GRANT pp_ddl_operator TO dbuser; ALTER ROLE dbuser SET role = 'pp_ddl_operator'; -- role パラメータの値として渡すのでクォートしている(識別子でも書ける) 2 行目は role パラメータの既定値をロール単位で設定するもので、 dbuser でログインするとセッション開始時に自動的に SET ROLE pp_ddl_operator された状態になります。利用者が接続後に毎回 SET ROLE を打たなくても、付与されたロールの権限ですぐ作業を始められるように入れているものです。 この SET ROLE が怪しいので、実際に接続して確認してみます。 SELECT current_user, session_user; -- current_user | session_user -- ------------------+-------------- -- pp_ddl_operator | dbuser session_user (ログインしたロール)は dbuser のままですが、 current_user (権限チェックの対象になるロール)は pp_ddl_operator に切り替わっていました。 PostgreSQL のロール属性チェックは、 current_user そのものの属性だけを見ます。 ( CREATEDB / CREATEROLE / SUPERUSER のように SQL の実行時に評価される属性の話です。 LOGIN は接続を確立する時点でログインロールに対して評価されるので、 SET ROLE の影響を受けません。) CREATE DATABASE 実行時に評価されるのは pp_ddl_operator の属性であって、 dbuser の属性ではありません。 dbuser にいくら CREATEDB を付与しても、既定の current_user では評価対象になっていなかったのです。 メンバーシップで継承されるのはオブジェクト権限だけ 「 dbuser は pp_ddl_operator のメンバーなのだから、属性もどこかで繋がるのでは?」と思うかもしれません。しかし、継承されるのは オブジェクト権限 (テーブルの SELECT / INSERT など)に限られ、 CREATEDB / CREATEROLE / LOGIN / SUPERUSER といった ロール属性は継承対象外 です。これはドキュメントにも明記されています。 The role attributes LOGIN , SUPERUSER , CREATEDB , and CREATEROLE can be thought of as special privileges, but they are never inherited as ordinary privileges on database objects are. You must actually SET ROLE to a specific role having one of these attributes in order to make use of the attribute. — PostgreSQL Documentation: Role Membership つまり、同じ「権限」と呼ばれるものの中に、評価のされ方が違う 2 種類が同居しています。 評価の対象 継承 ロール属性 ( CREATEDB など) current_user そのもの の属性だけ されない オブジェクト権限 (テーブルの SELECT など) current_user と、それが継承しているロール群 される 図にすると次のようになります。 ![CREATE DATABASE の権限判定で参照されるのは current_user であり、session_user の属性もメンバーシップ経由の属性も使われない](/assets/blog/authors/makoto-sato/2026-10-01-aurora-postgresql-role-attributes-powerpole/images/current-user-attribute-check.png =700x) \du で属性が見えていたのに効かなかった理由は、この 2 つの仕様の組み合わせでした。 dbuser の属性は、 SET ROLE 後の current_user ( pp_ddl_operator )の評価では参照されない メンバーシップがあっても、属性は継承されない この非対称性は、Aurora / RDS を使っていれば身近なところに現れています。マスターユーザーは rds_superuser のメンバーですが、その定義は AWS のドキュメントで次のように示されています。 CREATE ROLE postgres WITH LOGIN NOSUPERUSER INHERIT CREATEDB CREATEROLE NOREPLICATION VALID UNTIL 'infinity' — Amazon Aurora ユーザーガイド: rds_superuser ロールを理解する CREATEDB と CREATEROLE が マスターユーザー自身に直接 付与されています。 rds_superuser のメンバーであることで属性が得られるなら、この直接付与は要りませんでした。 対処 原因は「既定の current_user が共有ロールになっていること」なので、属性を付ける先を、実際に current_user として振る舞うロールに変えます。 ALTER ROLE pp_ddl_operator CREATEDB CREATEROLE; これで期待どおりデータベース・ロールの作成ができるようになりました。結果として、MySQL 版と同じ「権限はすべて共有ロールに集約し、ユーザーにはロールを渡すだけ」という形に属性の置き場所も揃いました。 共有ロールに属性が残り続けることが気になるかもしれませんが、 pp_ddl_operator は NOLOGIN で直接ログインできず、属性を使えるのはメンバーシップを持つロールに限られます。付与と剥奪は申請のライフサイクルで管理されています。逆に言えば 実際の境界は NOLOGIN ではなくメンバーシップ なので、恒久ユーザーにこのロールを付与しないことが前提になります。 なぜ GRANT に乗らないのか ―― オブジェクト空間の違い ここまでが実際にハマった話です。最後に、そもそもなぜ CREATEDB が GRANT で書けないのかを整理します。 MySQL では Database と Schema は同義で、サーバーの中はフラットな構造です。 Server(MySQL サーバー全体)← GRANT ... ON *.* がここ全体を覆う └── Database(= Schema。同義) └── Table / View / Procedure / ... DB を作ることも、その中にテーブルを作ることも同じ空間の中の出来事なので、この記事で扱う権限セットはすべて GRANT ... ON *.* の 1 つの語彙で書けます。 一方 PostgreSQL は 3 層構造です。 Cluster(PostgreSQL サーバー全体)← Database作成・Role作成はここ(ロール属性で管理) └── Database(完全に分離された名前空間) └── Schema(Database 内の名前空間) └── Table / View / Function / ... GRANT で付与するオブジェクト権限は、Database や Schema といった 入れ物ごとに指定する 形になります。MySQL の ON *.* に相当する全 Database 一括指定が無いのは、テーブルなどのカタログが Database ごとに分離していて、接続中の Database の外にあるオブジェクトを指定できないためです。 では Database や Role はどうかというと、これらはクラスタ全体で共有されるカタログに属します。ただし 層が違うから GRANT に乗らない、という単純な話ではありません。 GRANT ... ON DATABASE や GRANT ... ON TABLESPACE は、クラスタ全体に関わる対象を GRANT で扱えています。 違いは 権限を書き留める先があるかどうか です。 GRANT は権限を対象オブジェクトのアクセス制御リスト(ACL)に書き込みますが、 CREATE DATABASE と CREATE ROLE が相手にするクラスタ自身は、ACL を持つオブジェクトとして存在しません。そのため PostgreSQL はこれらを ロール自身の属性 として保持し、 ALTER ROLE で操作します。ただし定義済みロールという抜け道はあり、PostgreSQL 16 の pg_create_subscription は「まだ存在しないものを作る能力」をメンバーシップで配っています。 CREATEDB / CREATEROLE が属性側にあるのは原理的な制約ではなく、仕組みが古いという経緯です。 いずれにしても、MySQL で 1 つの GRANT に乗っていた権限セットは、PostgreSQL では「オブジェクトの ACL に書く権限」と「ロール自身に持たせる属性」の 2 系統に分かれます。 同じ権限セットを書き比べると DDLOperator 相当の権限セット(DDL/DML + DB 作成 + ロール作成)を共有ロールに集約すると、こうなります。 MySQL はすべて GRANT で完結します。 CREATE ROLE 'pp_ddl_operator'; -- オブジェクト権限もサーバーレベルの能力も、すべて同じ GRANT GRANT CREATE, ALTER, DROP, SELECT, INSERT, UPDATE, DELETE, INDEX, CREATE VIEW, CREATE ROUTINE, ALTER ROUTINE, CREATE TEMPORARY TABLES ON *.* TO 'pp_ddl_operator'; GRANT CREATE USER ON *.* TO 'pp_ddl_operator'; -- ユーザーとロールの作成も GRANT GRANT ROLE_ADMIN ON *.* TO 'pp_ddl_operator'; -- ロールの付与・剥奪も GRANT -- ユーザーへはロールを付与し、次回ログイン時に有効になるよう既定ロールにする GRANT 'pp_ddl_operator' TO 'dbuser'; ALTER USER 'dbuser' DEFAULT ROLE 'pp_ddl_operator'; PostgreSQL は 2 系統に分かれます。 CREATE ROLE pp_ddl_operator NOLOGIN; -- オブジェクト権限は GRANT。ただし *.* のような全体指定は無く、 -- データベース・スキーマごとに付与する(ALL TABLES は実行時点の既存テーブルのみ) GRANT ALL PRIVILEGES ON SCHEMA myschema TO pp_ddl_operator; GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA myschema TO pp_ddl_operator; GRANT TEMPORARY ON DATABASE mydb TO pp_ddl_operator; -- データベース作成・ロール作成は GRANT では書けない。ALTER ROLE で属性として付与する ALTER ROLE pp_ddl_operator CREATEDB CREATEROLE; -- ユーザーへはメンバーシップを付与 GRANT pp_ddl_operator TO dbuser; 先頭の CREATE ROLE を除くと、MySQL 側は最後の 1 行以外すべて GRANT で、PostgreSQL 側は ALTER ROLE の 1 行だけが別系統に落ちます。この 1 行が今回のハマりどころでした。 やりたいことを軸に並べると、対応はこうなります。 やりたいこと MySQL PostgreSQL テーブルの DML GRANT ... ON *.* GRANT ... ON ALL TABLES IN SCHEMA x (既存テーブルのみ。以後の分は ALTER DEFAULT PRIVILEGES ) テーブルの ALTER / DROP GRANT ALTER, DROP ON *.* GRANT では不可 。所有ロールのメンバーになる 一時テーブル GRANT CREATE TEMPORARY TABLES ON *.* GRANT TEMPORARY ON DATABASE x データベース作成 GRANT CREATE ON *.* ALTER ROLE x CREATEDB ロール・ユーザー作成 GRANT CREATE USER ON *.* ALTER ROLE x CREATEROLE ロールの付与 GRANT 'role' TO 'user' GRANT role TO user ロールの有効化 ALTER USER 'user' DEFAULT ROLE 'role' ALTER ROLE user SET role = 'role' (任意) MySQL 側は GRANT 主体で埋まり、PostgreSQL 側はデータベース作成とロール作成の 2 行だけ ALTER ROLE に落ちます。 もう 1 つ、 GRANT に乗らないものは CREATEDB / CREATEROLE だけではありません。テーブルの ALTER / DROP は PostgreSQL では 所有権に固有の権利で、 GRANT では切り出せません 。MySQL の ALTER / DROP 権限に相当するものが無いので、DDL を打たせたい場合は対象の所有ロールに参加させる形になります。所有者かどうかの判定は所有ロールのメンバーであれば満たされるので、ここは属性と違って共有ロール経由で渡せます。 MySQL 側も実は 2 系統ある MySQL は「すべて GRANT で書ける」ように見えますが、これは構文の見せ方の話です。 MySQL でも CREATE USER や ROLE_ADMIN のようなグローバル権限は、オブジェクトの ACL ではなく アカウントに紐づいて 格納されています(静的権限は mysql.user の列、 ROLE_ADMIN のような動的権限は mysql.global_grants の行として)。この点は PostgreSQL のロール属性と本質的に同じです。さらに次のものは GRANT では書けず、 CREATE USER / ALTER USER で指定します。 ACCOUNT LOCK / UNLOCK PASSWORD EXPIRE などのパスワードポリシー MAX_QUERIES_PER_HOUR などのリソース制限 REQUIRE SSL 、認証プラグインの指定 DEFAULT ROLE ( SET DEFAULT ROLE でも設定できる) なお SSL 要件・リソース制限・認証方式は 5.7 までは GRANT でも書けましたが、5.7 で非推奨になり 8.0.11 で削除されました。 つまり 2 系統に分かれているのは PostgreSQL だけではありません 。違いは系統の数ではなく、 「データベースを作る」「ロールを作る」という能力がどちら側に置かれているか です。 SET ROLE の意味論も違う もう 1 つ、MySQL にも SET ROLE はありますが意味論が違います。 MySQL PostgreSQL ロール有効化後の現在のユーザー 変わらない (有効ロールは CURRENT_ROLE() で確認) 切り替わる ( current_user が置き換わる) 権限の合成 アカウント自身の権限と有効ロールの権限の 和集合 current_user 基準。ロール属性は継承されない MySQL は加算的なので、「ユーザー側に付けた権限がロール有効化で見えなくなる」という事故は起きません。 「 SET ROLE があるかどうか」ではなく「 SET ROLE が current_user を置き換えるかどうか」 が、今回の落とし穴の分かれ目でした。 CREATEROLE はバージョンで危険度が違う PostgreSQL 15 以前では、 CREATEROLE を持つロールが他の非スーパーユーザーのパスワードを変更でき、定義済みロールを自分に付与できました 。つまり CREATEROLE はスーパーユーザーに準じる強さを持っていました(既存のスーパーユーザーには手を出せません)。 PostgreSQL 16 でこの挙動は変更され、権限の範囲が自分が管理するロールに限定されています。本記事の構成は 16 以降を前提としているので、15 以前の環境で共有ロールに CREATEROLE を付与するのは避けてください。 まとめ PostgreSQL の CREATEDB / CREATEROLE は GRANT では書けず、 ALTER ROLE で付与する「ロール属性」 ロール属性は current_user そのものだけを見て、継承されない 。一方オブジェクト権限は current_user と継承ロール群を見る。この非対称性が落とし穴の正体 したがって SET ROLE を使う構成では、属性の付与先は current_user を基準に決める 移植のチェックポイントとして一般化するなら、MySQL で ON *.* と書いていた GRANT 文を洗い出し、 どれがオブジェクト権限でどれがロール属性かを最初に仕分ける ことです。そして権限が効かないときに最初に打つべきは SELECT current_user, session_user; です。この 1 行で切り分けられる問題は、思っているより多いはずです。 参考 CREATE ROLE privilege cannot be inherited?! – select * from depesz; ―― 2009 年の記事。今回と同じ落とし穴が当時から記録されています PostgreSQL: Documentation: Role Attributes ―― ロール属性の一覧と、それぞれが何を許可するか PostgreSQL 16でのロールに関する変更点 ―― CREATEROLE の変更点と、 GRANT の SET / INHERIT オプション 「MySQL」と「PostgreSQL」のセキュリティ機能を比較する ―― 権限管理を含む、より広い範囲での MySQL / PostgreSQL 比較
はじめに こんにちは。KINTOテクノロジーズの JR.Liang です。 本記事では、 AIと対話するだけでプレゼン動画が作れるアプリ を題材に、生成AIアプリ向けのフレームワーク LangGraph を使ってこのアプリをどう構築するのかを解説します。特に、LangGraph の機能で 複雑な処理の流れ(フロー)をどう制御したか に焦点を当て、使った機能を一つずつ説明していきます。 まず、私たちが作りたかったものはこんなアプリです。 使う人 :生成AIも動画編集も、知識と経験が少ない人。 やること :アプリの質問に答えていくと、簡単に 自分のプレゼン動画が1本出来上がる 。 ユーザー体験としては 「質問に答える → できた物を見て、気になれば直す → 完成」 という流れ、とてもシンプルです。こちらが、AI(LLM)部分の実行基盤として Claude Code(Agent SDK)を組み込んだアプリの操作イメージです。 左が動画プレビュー、右がチャット。対話 → 台本・構成確認 → 生成 → チャットで修正 → 完成の流れ この シンプルな見た目 を成り立たせる裏側の複雑さを、LangGraph が引き受けて管理しやすいようにしました。 生成AIアプリはなぜ「一直線」で書けないのか 通常のプログラムは「A を呼んで、次に B、次に C」と 一直線 に書けます。ところが生成AIが絡むと、返答・結果物が毎回ブレる(非決定的)ため、一回で完璧にならず、 確認・修正 の工程が欠かせません。 その結果、ユーザーが満足できる動画を生成するまでの過程は「行ったり来たり」する流れになります。 制作途中で確認したい: 人間に確認させる 品質が悪い・修正したい: 前の工程に戻ってやり直す ユーザーのさまざまな要望で: 流れ内の工程が変わる いつでも編集を続けたい: 中断・再開 これらの状態と分岐を整理し、管理するツールが LangGraph です。 LangGraph とは LangGraph は、 AIアプリの「処理の流れ」を組み立てるオープンソースのライブラリ です。ChatGPT 連携で知られる LangChain チームが作っています。 @ card 特徴は、処理を 3つの要素の組み合わせ=Graph として書けること。 Node :1つの作業単位。この記事では「◯◯する係」と呼びます。 Edge :Node から Node へ引く矢印。次にどこへ進むか。 State(記録シート) :全 Node で共有する1枚の記録シート。各 Node が状態を書き込み、読み取ります。 Node を1つずつ実行して進む ので、「分岐・ループ・途中で確認・中断再開」が絡まらずに書けます。この記事で使う技術は次の5つです。 LangGraph の技術 どんなものか 何が嬉しいか 共有 State 全 Node で記録シートを共有 工程をまたいだ受け渡しが破綻しない 動的な分岐・ループ 実行時に次の Node を選ぶ / 前に戻る 制作途中のやり直し、前の工程に戻れる 人への割り込み(interrupt) 途中で一時停止して人に聞く 工程を進めながらユーザーが確認できる 保存(checkpointer) Node ごとに状態を保存 中断・再起動しても工程を続けられる 並行実行 複数 Node を同時に進める いくつかの工程の待ち時間を短縮できる ここからは、この5つの技術が どの流れで、何のために使われているか を1ステップずつ見ていきます。 全体の流れ 設計方針はこうです。 複雑さは全部 LangGraph 側に仕込んで、ユーザーには理解しやすい流れを見せる。 ユーザーが体験するのは、次の5ステップです。 flowchart LR A[① 対話で<br/>ヒアリング] --> B[② 台本・構成を<br/>見て直す] B --> C[③ 計画と<br/>デザインを確認] C --> D[④ 動画生成して<br/>採点・確認] D --> E[⑤ 完成] B -. 戻る .-> A C -. 戻る .-> B D -. 戻る .-> C E -. 戻る .-> D D -. 修正 .-> D 基本の流れとして各ステップを 一問一答の形で 進めます。加えて、 どのステップからでも前に戻ってやり直せます (図の点線)。ユーザーの一言次第で、1つ前にも、さらに前にも戻れます。この「行ったり来たり」を破綻させずに扱えるのも LangGraph の強みです。 流れ① 何を作りたいか、対話で聞く ユーザー体験 空欄のフォームではなく、 1問ずつ答えるだけ で動画制作に必要な情報:「内容・素材・尺・対象者」などを集めます。AIに不慣れでも、アプリが親切に案内してくれます。 使う技術:人への割り込み(interrupt)+ 保存(checkpointer) 質問の度に実行を 一時停止 して人に聞き、答えをもらって続きます(Human-in-the-Loop)。答えは鵜呑みにせず、 AIが「動画を作れる情報を聞き出したか」を判定 し、足りなければ 具体的に聞き直します 。質問を1問=1Node に分け、進むごとに保存するので、途中で閉じても続きから再開できます。 コードサンプル from langgraph.graph import StateGraph, START from langgraph.types import interrupt, Command # 「話す内容を聞く係」のNode def 話す内容を聞く係(state): 回答 = interrupt("どんな内容の動画を作りたいですか?") # 一時停止して人に聞く 判定 = AIで確認("話す内容として十分か?", 回答) # 答えが十分かAIが判定 if not 判定.OK: # 足りなければ… return Command(goto="話す内容を聞く係") # 同じNodeに戻って聞き直し return {"企画書": 回答} # OKなら記録シート(企画書)に保存 → 次へ builder = StateGraph(State) builder.add_node("話す内容を聞く係", 話す内容を聞く係) builder.add_node("使う素材を聞く係", 使う素材を聞く係) builder.add_edge(START, "話す内容を聞く係") # ← ここから開始(入口) # … 残りの Node・Edge を追加 … graph = builder.compile(checkpointer=checkpointer) # 全部つないでから compile(各Nodeの後に自動保存) 使っている部品 interrupt(質問) :一時停止して人に聞く Command(goto=…) :次に進む Node を指定(同じ Node を指定すれば聞き直し) compile(checkpointer=…) :Node ごとに保存 sequenceDiagram participant N as 質問する係 participant H as ユーザー N->>N: 一時停止して質問 H-->>N: 回答 N->>N: AIが「答えは十分か」を判定 alt 足りない N-->>H: 具体的に聞き直す else 十分 N->>N: 記録シートに保存 → 次へ end 流れ② 台本・構成をAIが下書きし、確認して直せる ユーザー体験 集めた情報から 台本と構成の下書きをAIが作って 見せます。ユーザーは 「OK」や「ここを直して」など自分の言葉で確認・修正の指示 ができます。 使う技術:人への割り込み(確認ゲート)+ 動的な分岐 下書きを見せて 一時停止 し、返事を受け取ります。ここがポイントで、ユーザーの自由な一言の 意味をAIが判定 し、「どの Node に戻すか」を 実行時に選びます (動的な分岐)。「2枚目のシーンを短くする」なら構成をやり直す、「柔らかい言い方にする」なら台本をやり直す、「OK」なら次へ。 コードサンプル # 「確認ゲート」のNode def 確認ゲート(state): 返事 = interrupt("この台本でよろしいですか? 直したいところがあれば教えてください") 行き先 = AIで振り分け(返事, ["台本を直す", "構成を直す", "次へ"]) # 意味を判定 return Command(goto=行き先) # ← 返事次第で戻る先が変わる(動的な分岐) 使っている部品 Command(goto=…) :次の Node を 実行時に 選ぶ。行き先を固定せず返事次第で変えられる=これが「動的な分岐」。 flowchart LR gate["確認ゲート<br/>(一時停止して確認)"] --> route["意味をAIで判定"] route -. "構成を直す" .-> align[構成の係] route -. "台本を直す" .-> speech[台本の係] route -. "OK" .-> next[次へ] 流れ③ 計画(シーン構成)の確認とデザインの準備を同時に進める ユーザー体験 「どんなシーン構成で組み立てるか」の計画を確認します。この確認の裏で 次のステップのデザインを同時に進め、デザインの確認ができるまでの待ち時間を短縮します 。 使う技術:並行実行 ポイントは、 「計画の確認」と「デザインの準備」は互いに独立している こと。計画の確認は人間の返事を待つ必要がありますが、デザインの下準備はその返事に関係なく進められます。そこで1つの Node から 矢印(Edge)を2本同時に 出し、片方が「人間の確認待ち」で止まっている裏で、もう片方が 次に必要なデザインの下準備を先回りで計算 しておきます。OK を出す頃には準備が済んでいるので、 全体の作業時間が短くなります 。 コードサンプル # 構成ができたら 2 本の矢印を同時に出す(並行実行) builder.add_edge("構成を作る係", "計画の確認") # 片方:人間の確認待ち builder.add_edge("構成を作る係", "デザイン先読み係") # もう片方:裏でデザインを先読み # 先読み係:確認待ちの裏でデザインを計算し、記録シートに置いておく def デザイン先読み係(state): return {"先読みデザイン": デザインを生成(state)} # 計画の確認がOKなら、次の「デザインの確認」へ進む builder.add_edge("計画の確認", "デザインの確認") # デザインの確認:先読み済みがあれば再計算せず、すぐ表示する def デザインの確認(state): デザイン = state.get("先読みデザイン") or デザインを生成(state) interrupt(("このデザインでよろしいですか?", デザイン)) return {} 使っている部品 add_edge(元, 先) :矢印を1本引く。 同じ元から2本のEdgeを設定する ことで、複数のNodeを並行して実行できる(=並行実行)。 state["先読みデザイン"] :先読みした結果を記録シートに残し、デザインの確認ですぐ取り出す(共有 State)。 flowchart LR align[構成を作る係] --> gate["計画の確認<br/>(人間の待ち時間)"] align --> pre["デザインを裏で先読み"] gate --> design["デザインの確認<br/>(先読み済みをすぐ表示)"] pre -. 先読み結果 .-> design 流れ④ 動画生成して採点し、問題点・直すところをユーザーが確認 ユーザー体験 最初の動画を組み立て、 ユーザーの要望に合わせて生成動画を採点します 。ユーザー自身が「どこか自分の要望に合っていないところはないか」を確かめられるようになります。ユーザーは仕上がりを見て「OK」か「直して」を言うだけです。 使う技術:人への割り込み + 共有 State + ループ 「動画生成 → 採点 → 点数と問題点を提示 → ユーザーが『どこを直すか』を確認(割り込み) → 修正 → もう一度採点…」というループを回します。ポイントは 採点のものさし :共有 State から、今までの流れで 聞き取れたユーザーの要望 を読み出し、できた動画を要望と照らし合わせて、点数とアドバイスを提示します。 コードサンプル # 「採点する係」のNode(採点だけ。割り込みは持たない) def 採点する係(state): 点数, 問題点 = AIで採点( state["video"], state["企画書"] ) # 記録シートの要望を"ものさし"に採点 if 点数.合格: # 要望どおり return Command(goto="完成の確認") # → ループを抜ける # 点数と問題点を共有 State に載せて、確認役の Node へ渡す return Command( goto="ユーザーに確認する係", update={"点数": 点数, "問題点": 問題点}, ) # 「ユーザーに確認する係」のNode(interrupt を先頭に置く) def ユーザーに確認する係(state): # 点数と問題点を見せて、ユーザーに「どこを直すか」を聞く(割り込み) 直す指示 = interrupt( ("点数と問題点はこちらです。どこを直しますか?", state["点数"], state["問題点"]) ) return Command( goto="修正する係", # → 直してもう一度採点(ループ) update={"直す指示": 直す指示}, ) 使っている部品 state["企画書"] :今までの流れで保存した要望を 読み出す (共有 State) interrupt(…) :点数と問題点を見せて「どこを直すか」を聞く Command(goto=…) :「修正する係」に戻して ループ を作る flowchart LR compose[動画を生成する係] --> qa{採点する係} qa -->|不合格| show["ユーザーに確認する係<br/>(割り込みで点数と問題点を提示)"] show -->|直す指示| iterate[修正する係] --> qa qa -->|合格| gate[完成の確認へ] 流れ⑤ 動画を書き出して完成 ユーザー体験 最後に「この動画で完成にしますか?」と 一度だけ 確認します。 使う技術:人への割り込み(最終ゲート) 最終確認も 一時停止 のゲート。 止まってユーザーに聞き 、承認されたら動画を書き出して完成です。 コードサンプル from langgraph.graph import END def 完成の確認(state): interrupt("この動画で完成にしますか?") # 承認されるまで進まない return {} builder.add_edge("完成の確認", "書き出す係") # 承認後は書き出しへ(分岐なし) builder.add_edge("書き出す係", END) # 書き出しが終わったら完成 flowchart LR qa[品質OKの動画] --> gate["完成にしていい?<br/>(最終確認)"] gate -->|承認| out[書き出し → 完成] 実装で難しかった点:ユーザーのバラバラな要望を、どの段階でも適切に受け止めて対応できるか 実装において一番難しかったのが、 ユーザーが自由に打ち込む一言 の適切な扱いです。言い方は無数にあるため、 同じ「直して」でも戻るべき工程(Node)は違う 場合があります。 ユーザーの一言 戻る先の係 「文字をもっと大きく」 軽い修正の係 「2枚目のシーンの背景を緑にして」 シーン単位の修正係 「順番を入れ替えて」 構成を作り直す係 「全体的に作り直して」 全部作り直す係 理想は、 ユーザーが今どのステップに居ても、どんな要望でも、それに合わせて処理できる こと。ユーザー体験をできるだけ簡単にするために、その一言を「どの Node(処理の係)に戻して直すか」を正しく選ぶのが重要ポイントです。そのため「自由文を解釈し、ちょうどいい係(ステップ)に戻す」必要がありました。 私たちが考えた解決方法の軸は 「行き先の候補はコードが決めて、意味の解釈はAIに任せる」 こと。 戻れる候補を宣言 → AIがどう解釈しても、宣言外へは飛べない(暴走しない)。 自由文の意味はAIが分類 → 候補から一番近いものを選ぶ。 迷ったら聞き返す → 曖昧な推測をせず、もう一度確認する。 戻せる先は、今居るステップごとに変える :候補は確認ゲートごとに別々に宣言してあり、今止まっているゲートの候補が、そのまま戻せる範囲になります。 今居るステップ 戻せる先 計画の確認 台本・構成の変更 / デザインの変更 / 最初のヒアリング プレビューの確認 色を変更 / 1シーンの修正 / 構成を変える / 全部作り直し 完成の確認 修正 / 作り直し コードサンプル from typing import Literal # 戻れる候補を宣言 → 変な所へは飛ばせない def 確認ゲート( state, ) -> Command[ Literal[ "軽い修正の係", "シーン単位の修正係", "構成を作り直す係", "全部作り直す係", "次へ", ] ]: 返事 = interrupt("直したいところはありますか?") 判定 = AIで振り分け( 返事, ["軽い修正の係", "シーン単位の修正係", "構成を作り直す係", "全部作り直す係", "次へ"], ) if 判定.曖昧: # 迷ったら return Command(goto="確認ゲート") # もう一度聞き返す return Command(goto=判定.行き先) # はっきりしていれば最短のNodeへ flowchart TD reply["ユーザーの自由文"] --> router[意味をAIで判定] router -->|はっきり| dest["適切なNodeへ戻す"] router -->|曖昧| clarify["1回だけ聞き返す"] clarify --> reply 最後に このアプリでユーザーに見せるのは「質問に答える → 確認して直す → 完成」という 簡単な流れ 。その裏にある 分岐・やり直し・確認・中断からの再開 という複雑さを、LangGraph( 共有 State・動的な分岐・ループ・割り込み・保存 )がまるごと引き受けてくれました。 おかげで私たちは、 どこで人に確認するか・どこでAIに処理させるか という アプリならではの判断 の設計に集中できました。生成AIを組み込んだアプリを開発する際に、本記事で用いた概念と技術が参考になれば幸いです。
はじめに こんにちは、クラウドセキュリティグループの小林です。 普段は AWS を中心としたクラウド環境のセキュリティ運用と改善を担当しています。 本記事では、GuardDuty のアラートを全件目視で確認していた運用を、AWS Security Incident Response(以下 SIR)の導入と自作の Slack 連携でどのように変えたかを紹介します。 本記事の対象読者 GuardDuty のアラート対応に負担を感じている方 AWS Security Incident Response の導入を検討している方 インシデント対応の導線を Slack に統合したい方 背景 当社はマルチアカウントの AWS 環境を GuardDuty で監視しています。 SIR 導入前の運用は、アラートが発火するたびにマネジメントコンソールを開き、GuardDuty の検出内容・関連リソース・CloudTrail ログなどを確認し、実害のある検知なのか過検知なのかを毎回判断する運用でした。 アラート 1 件ごとの作業は短時間でも、アカウント数に比例して検出件数が増えるため、確認に使う時間は無視できない量になっていました。 特に 2026 年 5 月〜6 月、GitHub Actions に起因する検知アラートが目立つようになりました。 GitHub がホストするランナーの IP アドレスは多数の利用者で共有されており、他の利用者による悪用などを原因としてこの IP が GuardDuty の参照する脅威インテリジェンスに登録されると、同じ IP 帯から実行される正規の GitHub Actions の通信まで脅威由来として検知され、アラートが多発します。 このアラートへの対応が、SIR の導入を検討したきっかけになりました。 AWS Security Incident Response とは? AWS Security Incident Response は、セキュリティイベントの検知から対応までを支援する AWS のマネージドサービスです。 SIR は GuardDuty の検出結果と、Security Hub 経由で連携したサードパーティー製品の検出結果を取り込み、自動でトリアージします。 過検知と判定されたものはアーカイブされ、緊急度の高いものだけがケースとして起票されます。 これにより、それまで人手で行っていた確認とアーカイブの作業の大部分をサービスに置き換えられます。 なお、一部の Finding タイプは自動トリアージの対象外です。 ケース化された検出結果は、分析から封じ込め、クローズまでのライフサイクルがケース上に記録され、対応状況を一元的に追跡できます。 また、AWS サポートが必要なケースについては、SIR エンジニア(セキュリティ脅威のトリアージ・調査・封じ込めを支援する AWS の専門エンジニア)から 15 分以内に初回応答があり、ケースクローズまで継続して調査の支援を受けられます。 例えば、アカウント乗っ取りのような明確なインシデントだけでなく、疑わしい検知が本物の侵害かどうかの調査や、GuardDuty のトリアージ設定・抑制ルールに関する問い合わせなども AWS サポートが必要なケースとして起票できます。 費用面では、エンタープライズサポート契約があれば SIR のサービス全体を追加費用なしで利用できます。 Slack 連携 当社はエンタープライズサポートを契約していたため、SIR は追加コストなしですぐに導入できました。 しかし、運用に載せると導線の問題が見えてきました。 SIR ケースの確認や操作にはマネジメントコンソールへのログインが必要なため、社内標準のコミュニケーションツールである Slack での会話とマネジメントコンソール上の記録を行き来する手間があり、ケースの変化に気づくのが遅れる懸念もありました。 そこで AWS 公式のサンプル実装( sample-aws-security-incident-response-integrations )を参考に、Slack との双方向連携を作成しました。 起票は Slack のスラッシュコマンドから行います。 /sir create を実行すると入力モーダルが開き、ケースタイプやタイトル、影響を受けるアカウントなどを入力してケースを作成できます。 説明欄には「何が起きたか」「現在の影響」などの定型見出しが初期表示されるため、埋めるだけで報告の体裁が整います。 起票は誰でもできる一方、ステータス変更や内容の編集といった影響の大きい操作は管理者に限定しており、変更時、誰が何のアクションを実行したか Slack 上に記録するようにしています。 起点が Slack か SIR かを問わず、ケースが作成されるとチャンネルに初報が投稿され、指定した個人やグループへメンションが送られます。 以後の更新やコメントの通知はすべて初報へのスレッド返信として集約しています。 対応中はスレッド内の発言や画像が SIR ケースに自動で記録され、SIR 側のコメントもスレッドに届くため、スレッドがそのまま作業記録になり、Slack から SIR へ転記する手間と記録漏れがなくなります。 複数のケースが動いているときは /sir list でオープン中のケースを一覧でき、一覧の各ケースに対して内容の編集やステータス変更も行えます。 ケースは対応完了まで Open のまま残り、クローズ時にもスレッドへ通知されます。 アーキテクチャと設計上の注意点 この連携は API Gateway、Lambda(4 関数)、EventBridge カスタムバス、DynamoDB に、Slack の認証情報などコードに含めたくない設定を保管する Secrets Manager を加えたサーバーレス構成で、Terraform で管理しています。 主要な流れは次の図のとおりです。 SIR のイベント通知機能はメールのみで、ケースの変化をイベントとして受け取る手段がありません。 そのため SIR から Slack への方向は、poller が SIR の API を定期的に呼び、DynamoDB に保存した前回のスナップショットと比較して差分を検知するポーリング構成になります。 ポーリング間隔は、未クローズのケースがある間は 1 分、なければ 5 分になるよう、poller が自身をトリガーする EventBridge ルールを実行のたびに書き換えて調整しています。 対応中の即時性と平常時の API 呼び出し削減を両立するためですが、それでも通知には最大 1〜5 分の遅延が残ります。 逆方向の Slack から SIR への経路では、コマンドやボタン操作への応答を 3 秒以内に返すという Slack の制約が存在するため、入口では即座に応答だけを返し、実処理は後段の Lambda へ非同期で委譲する構成となっています。 ただし、モーダルの入力エラーのように応答の内容そのものを返す場面は非同期にできないため、API Gateway のルートは同期・非同期の 2 本に分けています。 また、モーダルを開くための trigger_id も 3 秒で失効するため、EventBridge で 5 分ごとに Lambda を空起動してコールドスタートを避けています。 なお、リクエストの真正性は入口での Slack 署名の検証で確認しています。 双方向に同期させると、Slack からの変更を poller が SIR 側の変化として検知し、Slack へ通知し返すループが起こり得ます。 これは、Slack 起点の変更時に DynamoDB へ書き込むケース単位のフラグを poller が確認して通知をスキップする仕組みと、SIR へ書き込むコメントにタグを付けて同期対象から除外する仕組みで防いでいます。 導入後の効果 導入後は SIR の自動トリアージが機能し、多い月では過検知の 70% が自動アーカイブされていました。 その大半は、背景で挙げた GitHub Actions 起因の検知です。 導入のきっかけになったアラートを、人手を介さずに処理できるようになりました。 人が確認するのは、緊急度が高いと判定されてケース化されたアラートだけになり、その対応は Slack のスレッドの中で完結するため、対応漏れも起こりにくくなりました。 残っている課題と今後の対応 現時点で見えている課題は 2 点あります。 SIR の自動トリアージの対象外となる Finding タイプがあることです。対象外の検知は SIR を経由しないため、これまでどおり人が確認する必要があります。 自動アーカイブした履歴が SIR 側に残らないことです。何がいつ自動アーカイブされたのかを確認するには、CloudTrail のログを調べるしかないのが現状です。 今後は、この自動アーカイブ履歴を可視化する仕組みづくりに取り組む予定です。 SIR が過検知と判定したものでも社内の基準では調査が必要になるケースが考えられるため、SIR の判定を後から確認できる状態にしておきたいと考えています。 まとめ GuardDuty のアラートを全件目視する運用は、件数が増えると維持が難しくなります。そこで当社では、過検知の仕分けを SIR に任せて人が判断する対象を絞り、残ったケースの操作と通知を Slack に集約することで、本当に対応が必要なアラートに集中できる体制にしました。 エンタープライズサポート契約があれば SIR は追加費用なしで有効化できるので、まずは自動トリアージがどの程度の過検知を仕分けられるかを確かめてみてください。そのうえでマネジメントコンソール中心の導線に不便を感じたら、公式サンプルを土台に Slack 連携を検討することをおすすめします。 なお、アラート分析そのものについても、SOC AI Agent による自動化で、分析の高速化・属人化の解消・担当者の負荷軽減を進めています。詳細は「 アラート疲弊からの脱却へ ― SOC業務におけるAIエージェント活用の実践 」をご覧ください。 本記事が、同様の課題を抱えている方の参考になれば幸いです。 参考資料 AWS Security Incident Response ドキュメント sample-aws-security-incident-response-integrations (aws-samples)
この記事でわかること AivisSpeech(無料のローカル音声合成)+ HyperFrames + Claude Code の3つで、ナレーション・字幕・音声付きの解説動画が作れます。動画編集ソフトは使いません。 人間がやることは 「フォルダを1つ作る」「指示を2つ出す」「Claudeの質問に答える」 だけ。後は動画の完成まで待つだけで動画ができました。 ただし前提として Node.js と FFmpeg が必要です。入っているかは Claude が最初に点検してくれるので、足りなければその場で追加して貰えます。 音声モデルには ACML(Aivis Common Model License) が適用されます。 営利利用が禁止された ACML-NC 1.0 のモデルもある ので、使う前にどちらが適用されるかを 条文 で確認してください。 できたもの:3分26秒の解説動画 「生成AIの基礎」を、博士役と生徒役の会話で説明する動画です。中央のSVG図解が、そのとき話しているセリフに合わせて動きます。 https://www.youtube.com/watch?v=2lvo5-Ze4EU 中身の数字はこんな感じです。 項目 数字 尺 3分26秒(7トピック) 総フレーム数 5,400枚 MP4書き出し時間 1分55秒 着手から完成まで 約2時間 僕はイラストを描いていないし、音声も録っていないし、動画編集ソフトも開いていません。台本も書いていません。 :::message 本来は、できた動画の内容や台本を人の目で確認してから公開すべきです。ただ今回は「AIに任せるとこんな動画ができる」を知ってもらうため、 AIが作った動画をあえて無修正のまま 載せています。 実際、字幕で「生成AI」と書くべきところが「生成エーアイ」になっているミスが残っています。最後は人の目での確認が欠かせないことも、動画を見てもらえれば伝わるはずです。 ::: 役割分担:HyperFrames・AivisSpeech・Claude Code 登場人物が3つあるので、先に整理します。 ツール 役割 AivisSpeech 日本語の音声合成。テキストを渡すと喋ってくれる HyperFrames HTMLから動画(MP4)を書き出すフレームワーク。HeyGen製 Claude Code 上の2つを実際に組み立てる担当。台本もHTMLもコードも書く つまり「HyperFramesで動画を組む人」がClaude Code です。僕はClaudeに日本語で作りたい動画について指示して、Claudeからの質問に答えているだけです。 作業の全体像:7ステップ 細かい話の前に、全体の流れです。ここから先はこの順番で進みます。 # やること どこで 1 AivisSpeech をダウンロードして起動する アプリ 2 AivisHub から音声モデルを2つ以上追加する アプリ 3 フォルダを1つ作って Claude Code で開く ターミナル 4 指示を2つ送る Claude Code 5 Claude の質問に答える Claude Code 6 プレビューで確認する ブラウザ 7 MP4 に書き出す Claude Code 手を動かすのは1〜4までで、5から先はほとんど待っているだけです。 手順1:AivisSpeechをダウンロードして起動する 公式サイトからダウンロードしてインストールします。無料です。 https://aivis-project.com/ 起動するとこの画面になります。左に選択中の話者、右に話速や音高のスライダーがあります。 起動直後。この時点でテキストを打って再生ボタンを押せば、もう喋ってくれます ここで一度、適当なテキストを入れて再生してみてください。声が出れば準備OKです。 :::message このアプリは起動しているだけでいい 、というのが大事なポイントです。 AivisSpeechは裏側で音声合成のサーバー( 127.0.0.1:10101 )を立てていて、Claudeが書いたスクリプトはそこに話しかけて音声を作ります。人間がこのアプリを操作する場面は、実は一度もありません。 ::: 手順2:AivisHubから音声モデルを追加する デフォルトの声だけでもいいのですが、今回は「博士」と「生徒」の2人が会話するので、 声が2つ以上必要 です。声は AivisHub というサイトから追加します。 追加のしかたは2通りあります。アプリ内から探す方法と、ファイルをダウンロードして入れる方法です。メニューバーの「音声合成モデル」から両方に行けます。 「音声合成モデルのインストール・管理」と「AivisHubで音声合成モデルを探す」の2つが入口です AivisHubで探して選ぶ 「AivisHubで音声合成モデルを探す」を選ぶと、モデル一覧が開きます。ダウンロード数順に並んでいて、キャラクターの絵と「若い女性の声」「中年男性の声」といったタグ、それにスタイル数が見えます。 ダウンロード数の多い順。「6スタイル」「7スタイル」という表記が、その声で使える喋り方の種類です 気になったモデルをクリックすると詳細ページに行きます。今回博士役に使った「阿井田 茂」はこんなページです。 ボイスサンプルが聴けます。右側の「詳細情報」にライセンスとファイルサイズ(251.14MB)が書かれています ここで見るべきは3つです。 スタイル … ノーマル / Calm / Far / Heavy / Mid / Shout / Surprise のように、同じ声で複数の喋り方が使えます。今回は落ち着いて断定してほしかったので Calm を選びました。 ボイスサンプル … 実際の声を聴けます。声の印象は説明文だけではわからないので、聴いて頂くと、どれを使うかのイメージが湧きます。 ライセンス … ACML 1.0 と書かれています。ここは飛ばさないでください。 ACML-NC 1.0 のモデルは営利目的の利用が禁止 されているので、業務で使うなら必ず見る必要があります(詳しくは後述)。 AIVMXファイルをダウンロードして入れる 詳細ページの「AIVMX をダウンロード」を押すと .aivmx というファイルが落ちてきます。これを AivisSpeech に読み込ませる方法です。 Aivis Speechを開き、「音声合成モデル」→「音声合成モデルのインストール・管理」→ 右上の「インストール / 更新」を開きます。 「ファイルからインストール」でダウンロードした.aivmxを選ぶだけ。URL直指定もできます 入れ終わったら、管理画面で確認します。 5モデル入った状態。「まお」は6スタイルで、クレジット表記の指定まで書かれています 2つ以上入っていれば会話形式の動画が作れます。 手順3:フォルダを1つ作ってClaude Codeで開く ここからターミナルです。1行だけです。 mkdir hyperframes 動画制作用のフォルダを用意します。フォルダ名は何でもいいです。 空のフォルダで始めて、後はClaudeがこの中に必要なファイルを全部作ってくれます。 後は作ったフォルダをClaudeのアプリのClaude Codeで開くだけ。 手順4:指示は2つだけ 実際に打った文章をそのまま載せます。コピペして使えます。 Hyperframesで動画を作る為のプロジェクトのセットアップを行なって。公式のスキルもあるからそれもセットアップを行なって これを送るだけ。「公式のスキルも」と付けるのがポイントです HyperFramesには公式のスキル(Claudeへの取扱説明書のようなもの)が用意されていて、これを入れておくとClaudeがHyperFrames特有のルールを踏み外さなくなります。逆に、これがないとClaudeは手探りになります。 Aivis Speechを使って生徒役と博士役で会話しながら生成AIの基礎知識の説明動画を作成して。その際にSVGのアニメーションを最大限使って視覚的にもわかりやすい工夫もして。 この一文に入れた指定は4つだけです。 指定 書いた言葉 声をどうするか 「Aivis Speechを使って」 誰が喋るか 「生徒役と博士役で会話しながら」 何の動画か 「生成AIの基礎知識の説明動画」 見せ方 「SVGのアニメーションを最大限使って」 尺も、シーン数も、どの声を使うかも指定していません。 それはこの後Claudeが聞いてきます。 手順5:Claudeの3つの質問に答える 2つ目の指示を送ると、Claudeはいきなり作り始めません。まず環境を調べて、使える声を一覧にして、それから質問してきました。 質問1:尺とカバー範囲 動画の尺とカバーする範囲はどれくらいにしますか?(ここで台本量・シーン数・SVG図解の点数が全部決まります) 選択肢は「約90秒 / 3トピック」「約2分 / 4トピック」「約3分 / 6トピック(推奨)」「約5分 / 9トピック」。 推奨の3分を選びました。 質問2:声の配役 声の配役はどの組み合わせにしますか?(上のサンプルMP3を聴いてから選んでください) ここが良かったところで、 Claudeが先にサンプル音声を作って聴かせてくれました。 各モデルの説明文はAPIから取れなかったそうで、代わりに「各音声が自分の名前を名乗ってから博士/生徒のセリフを読む」音声を実際に合成してくれたんです。 聴いた上で「博士=阿井田 茂 Calm / 生徒=まお ふつー」を選びました。 質問3:画面の見せ方 画面の見せ方(SVGの使いどころ)はどれにしますか? 「左右にSVGキャラ常駐+中央に図解」「キャラなし・全画面図解+話者バッジ」「キャラは小さくコーナー+図解を大きく」「全身SVGキャラ+ホワイトボード演出」の4択。選択肢にアスキーアートのレイアウト案が付いていたので、イメージしやすかったです。 「左右にキャラ+中央に図解」を選びました。 台本のレビューは飛ばしてOK この後Claudeは台本( SCRIPT.md )を書いて、「ここで一度レビューを挟みます」と言ってきます。 :::message まず動画が作れるかどうかを試したいだけなら、 ここは読まずに「進めて」で大丈夫です。 台本を直したくなったら後からいくらでも直せます(詳しくは後述)。僕も1本目は流しました。 ::: 手順6:プレビューで確認する 台本ができて、音声が合成されて、HTMLが組み上がると、ブラウザで確認できます。ブラウザが立ち上がらなかったらClaudeにブラウザの立ち上げてプレビューしたい旨使えればOKです! HyperFramesの編集画面(Studio)が開きます。 左が各シーンのファイル、中央がプレビュー、下がタイムライン。00:00 / 03:25 と尺が出ています この画面で見るところは3つです。 中央のプレビュー … 再生ボタンで実際に動きます。音も出ます。 下のタイムライン … Topic 1 〜 Topic 7 が時間順に並んでいます。どのシーンが何秒から何秒までかがひと目でわかります。 左の一覧 … 各シーンが compositions/topic-1.html のような別ファイルになっています。1つのシーンだけ直しても他に影響しません。 直したいところがあれば、Claudeに日本語で言えば直してくれます。「シーン3の図解が速すぎる」でも通じます。 手順7:MP4に書き出す プレビューでOKなら書き出します。動画を書き出してと伝えるだけでOKです! 3分26秒の動画で 1分55秒 で終わりました。5,400フレームを1枚ずつ描いて繋げているので、尺が伸びるとその分時間がかかります。 これで out/seisei-ai-no-kiso.mp4 ができあがり。 完成度を上げるなら:台本チェックとキャラクターの用意 1本目は「作れるかどうか」の検証なので、流れに乗るだけでいいと思います。そのうえで、ちゃんとしたものを作るなら追加でやることが2つあります。 1. 台本(SCRIPT.md)をレビューする SCRIPT.md がこの動画の肝です。セリフはもちろん、尺・字幕・口パク・図解のタイミングまで、全部ここから逆算されて動画が出来上がります。 内容の正しさは人間しか判断できないので、社外に出す動画なら必ずここを読んでください 2. キャラクターを自分で用意する 今回のキャラクターは Claudeが描いたSVG です。動くし表情も変わりますが、見た目は簡素です。図解の方は十分見られるものになったので、伸びしろがあるのはキャラの絵の方でした。 作り込むなら、PSDなどで パーツごとにレイヤーを分けて 描くのがいいと思います。 ここのキャラクターの作り方はまた別の機会で試せたら紹介しますね! 顔・体・目(開閉)・口(あ / い / う / え / お / 閉じ)を別レイヤーにする パーツごとに PNG か SVG で書き出す 書き出したファイルを1つのフォルダでまとめてClaudeに「このキャラに差し替えて」と伝える 口の形は「あいうえお+閉じ」の6種類あれば足ります。AivisSpeechは音声を作るときに「どの母音を、いつ、どれだけの長さ発音するか」というデータを返してくれるので、そこから口の形が自動で切り替わります。だからパクパク動いているだけの口ではなく、実際の発音に合った口になります。 留意事項:社内ルールとライセンスの確認 ここは飛ばさずに読んでください。 各ツールの利用は所属会社のルールに沿って この記事で使ったツールはどれも外部サービスです。 業務で使う場合は、必ず所属する会社のルールや利用可否の基準に沿って利用してください。 何を入力していいのか、成果物をどこまで公開していいのかは会社ごとに違います。 AivisSpeechの音声モデルはライセンスを確認する 音声モデルには ACML(Aivis Common Model License) が適用されます。条文はGitHubで全文公開されているので、 使う前に必ずここを読んでください。 https://github.com/Aivis-Project/ACML/tree/master 一番大事なのは、 ACMLには2種類ある ということです。 ライセンス 営利利用 ACML 1.0 個人・法人・非営利・営利すべて可。 商用利用OK ACML-NC 1.0 営利目的の利用は禁止。 個人の私的な創作活動や、教育機関での教育・研究といった非営利利用のみ 同じAivisHubに並んでいても、 どちらが適用されるかはモデルごとに違います。 会社のブログやサービスで使うなら営利利用に当たる可能性があるので、 -NC が付いていないかを最初に確認してください。 ライセンス名は AivisHubの詳細ページの「詳細情報」欄 と、 AivisSpeechの管理画面 の両方に書かれています。今回博士役に使った「阿井田 茂」は ACML 1.0 でした。 禁止事項は両ライセンス共通で、次の7項目です。 話者や他者の本人・原作者・公式関係者であると誤解させる利用 話者のイメージ・尊厳・品位・社会的評価を傷つける利用 実在の人物・団体・商品などを批判・攻撃・嫌がらせ・誹謗中傷・差別する活動 虚偽の情報やコンテンツを流布する活動 虚偽・誇大表現によるマーケティング、倫理的に問題のあるビジネス 特定の政治的立場・宗教・陰謀論への賛同や反対を呼びかける活動 反社会的・犯罪目的での利用 クレジット表記は、ACML 1.0 と ACML-NC 1.0 のどちらでも「任意」 と明記されています。ただしモデル側から表記のしかたの希望が書かれている場合があるので都度確認をお願いします。 :::message alert 上のまとめは2026年9月時点の条文を読んだものです。条文は更新される可能性があるので、判断するときは必ず ACMLの原文 を確認してください。 ::: まとめ やったことを並べ直すと、こうなります。 AivisSpeechを入れて起動する AivisHubから声を2つ以上追加する フォルダを1つ作って、Claude Codeで開く 指示を2つ送る Claudeの質問に答える プレビューで見る 動画を書き出す 動画編集ソフトを開いていません。イラストも描いていません。台本も書いていません。 それでも音声・字幕・図解が揃った3分26秒の動画が出てきました。 一番の発見は、 「動画を作る」が日本語で発注できる作業になっていた ことでした。「生徒役と博士役で会話しながら」「SVGアニメーションを最大限使って」——このくらいの粒度で伝わります。細かいことは向こうが聞いてきます。 作りたい動画のイメージが頭にあるなら、フォルダを1つ作るところから試してみてください。1本目は台本レビューも飛ばして、まず出てくるかどうかを見るのがおすすめです。
この記事でわかること MCPサーバーはPythonやTypeScriptだけでなく、Spring AIを使えばJava(Spring Boot)でも構築できる MCPサーバー機能は、Spring AIの公式スターターを使えば簡単に既存のSpring Bootアプリに組み込める(サーバー設定はapplication.ymlのみ) 既存アプリがSpring SecurityのOAuth2 Resource Serverで認証していれば、MCPの認可に必要なメタデータ公開(RFC 9728)も含めてSpring Securityの標準機能で対応できる AIエージェントがCMDBの構成情報を自分で参照できるようになると、Atlassian MCPなど他のMCPとも組み合わさって、脆弱性対応やEOL対応といった業務がエージェントとの1つの会話で完結する はじめに こんにちは。KINTOテクノロジーズ(以下、KTC) プラットフォームグループ Platform Engineeringチームの山田です。普段は社内向けのCMDBを内製開発していて、ここ最近はCMDBと生成AIを組み合わせる取り組みを続けています。 CMDBそのものやSpring AIについては過去の記事で紹介しているので、あわせてご覧ください!(CMDBの記事は少し古いですが、雰囲気は伝わるかと思います) https://blog.kinto-technologies.com/posts/2023-12-14-CMDB/ https://blog.kinto-technologies.com/posts/2025-06-11-springAI/ 今回はその続きとして、CMDB本体(Spring Boot製)にMCPサーバー機能を追加し、AIエージェントから社内の構成情報を直接検索できるようにしました。 背景 Claude CodeなどのAIエージェントだけで業務を進める場面が、この1年で一気に増えてきていると思います。社内でも各種MCPサーバーやスキルなどの整備が進み、調査・実装・ドキュメント作成の多くをエージェントに任せられるようになっています。 一方で、困っていたのが、今回の主役であるCMDB (Configuration Management Database: 構成管理データベース) のデータです。CMDBは、プロダクト・チーム・GitHubリポジトリ・脆弱性・SBOMといった社内の構成情報を一元管理するデータベースで、KTCでは内製開発しています(詳しくは冒頭の過去記事をご覧ください)。せっかく構成情報を集約しているのに、これまでの参照手段はWebアプリだけでした。そのため、エージェントで作業をしていてCMDBのデータが必要になるたびに、以下の作業が発生していました。 CMDBのWebアプリを開いてキーワード検索する 検索結果のテキストをコピーして、エージェントのプロンプトに貼り付ける この間、エージェントの作業は止まり、人がWebアプリとエージェントの間でデータを受け渡すだけの作業に時間を取られます。せっかく構成情報を一元化したのに、AIエージェントからは参照できない状態でした。 この手作業での受け渡しをなくすため、CMDBをMCPサーバー化することにしました。 CMDBのMCPサーバーで何ができるようになったか 先に、できるようになったことの紹介です。 たとえば、あるライブラリに脆弱性が見つかったとします。SBOM・GitHubリポジトリ・プロダクト・チーム・ユーザーなどを管理するCMDBのMCPサーバーができたことで、こうした脆弱性対応をAIエージェントとの1つの会話の中で完結できるようになりました。具体的には、SBOM情報から脆弱性のあるバージョンのライブラリを使っているGitHubリポジトリを洗い出し、リポジトリに紐づくプロダクトと管理チーム・担当者を特定し、担当者向けのJiraチケットの起票、さらには脆弱性を解消するPRの作成まで進められます。 ①〜③と⑤がCMDBのMCPツール、④はAtlassian MCP、⑥はエージェント自身のコーディング機能です。 ポイントは、CMDBのMCPサーバー単体で完結するのではなく、他のMCPやエージェントの機能と組み合わさることで業務が一気通貫になることです。AIエージェントは、参照できるコンテキストが増えるほど、対応できる業務の幅が広がります。CMDBにはエージェントが持っていない社内の構成情報が集まっています。これまで人が都度補っていたこれらの情報をMCPで提供することで、エージェントに任せられる範囲そのものを広げることができました。 脆弱性対応の他にも、たとえばEOL(サポート期限)対応にも同じ要領で使えます。期限切れが近いコンポーネントとそれを使っているプロダクトをCMDBのMCPツールで洗い出し、担当チームの特定からJiraチケットの起票までを、そのまま1つの会話で進められます。 MCPサーバーの実装にSpring AIを選んだ理由 MCPサーバーの構築というと、PythonやTypeScriptのイメージが強いかと思います。公式SDKの充実度やFastMCPのようなフレームワークの存在もあり、実装記事もその2つに偏っている印象です。 ただ、今回MCPサーバー化したい対象は、Spring Bootで実装済みの既存アプリでした。別プロセスでPythonやTypeScriptのMCPサーバーを立ててAPIを中継させる構成も考えられますが、Spring AIを使って既存アプリにMCPサーバー機能を組み込めば、以下の資産をそのまま使い回せます。 REST APIのために実装してきたビジネスロジック(service層) Spring Securityを使った認証認可の仕組み ECSへのデプロイや監視などの運用の仕組み 唯一の不安は「Entra IDを使った既存の認証認可が、AIエージェント相手でも同じように使えるのか」でした。MCPの認可仕様はOAuth 2.1ベースで比較的新しく、検証するまでは正直手探りでしたが、結論から言うとSpring Securityの標準機能で対応できました(詳細は実装の節でお話しします)。 もしSpring Bootでアプリケーションを開発・運用しているチームであれば、MCPサーバーの構築にSpring AIを選ぶメリットは大きいと思います。 技術スタック Java 21 Spring Boot 4.0 Spring AI 2.0 (spring-ai-starter-mcp-server-webmvc) Spring Security (OAuth2 Resource Server) Microsoft Entra ID (認可サーバー) AWS (CloudFront + ALB + ECS など) システム構成 既存のCMDBバックエンド(Spring Boot)に、MCPエンドポイント /mcp を追加した構成です。MCPサーバーのために新しく追加したインフラはありません。なお、図はMCPサーバーに関係する部分に絞っており、フロントエンドの配信経路などは省略しています。 実装 ここからは実装内容を紹介します。なお、記事中のコードは一部、省略・編集しています。 依存関係の設定 build.gradleにSpring AIのBOMとMCPサーバー用スターターを追加します。追加する依存関係はこれだけです。 ext { set('springAiVersion', "2.0.0") } dependencies { implementation 'org.springframework.ai:spring-ai-starter-mcp-server-webmvc' } dependencyManagement { imports { mavenBom "org.springframework.ai:spring-ai-bom:${springAiVersion}" } } サーバー設定はapplication.ymlのみ MCPサーバーとしての設定はapplication.ymlに書くだけで、Javaの設定クラスは1つも作っていません。あわせて、JWT検証(OAuth2 Resource Server)の設定も抜粋します。 spring: security: oauth2: resourceserver: jwt: issuer-uri: https://login.microsoftonline.com/${TENANT_ID}/v2.0 ai: mcp: server: name: cmdb-mcp version: 0.0.1 instructions: >- CMDB は KINTO Technologies (略: KTC) 社内の構成管理データベースのこと。 プロダクト・チーム・リポジトリ・脆弱性などの社内構成情報を検索できる。 protocol: STATELESS type: SYNC stateless: mcp-endpoint: /mcp annotation-scanner: enabled: true 設定のポイントを2つ紹介します。 1つ目は instructions です。ここに書いた文章はMCPの初期化時に一度だけエージェントへ渡されます。「CMDBとは何か」のような前提知識をここに置いておくと、各ツールのdescriptionで同じ説明を繰り返さずに済み、ツール定義のトークンを節約できます。 2つ目は annotation-scanner です。これを有効にすると、後述する @McpTool の付いたメソッドが自動でツールとして登録されるため、ツール登録用のBean定義も不要になります。 @McpToolでツールの実装 ツールは @McpTool を付けたメソッドとして実装します。プロダクト検索ツールを例に、実装の全体像を紹介します。 @Component public class ProductMcpTools { private final ProductService productService; public ProductMcpTools(ProductService productService) { this.productService = productService; } @McpTool( name = "searchProducts", description = """ プロダクトを検索します。一覧・絞り込み・詳細取得はすべてこのツール\ (productId 指定で1件の詳細、未指定で一覧)。""", annotations = @McpTool.McpAnnotations( readOnlyHint = true, destructiveHint = false, idempotentHint = true, openWorldHint = false)) public ProductSearchMcpResult searchProducts( @McpToolParam(description = "プロダクトID。指定すると1件の詳細を返す。一覧結果の productId を渡す", required = false) Integer productId, @McpToolParam(description = "プロダクトの所属部署(グループID)で絞り込み", required = false) String groupId, @McpToolParam(description = "true で削除済みプロダクトを検索。削除済みはこの指定時のみ取得可(既定: false)", required = false) Boolean deleteFlag) { SearchProductServiceParameter parameter = new SearchProductServiceParameter(); parameter.setProductId(productId); parameter.setGroupId(groupId); parameter.setDeleteFlag(Boolean.TRUE.equals(deleteFlag)); // 既存のREST API用serviceをそのまま呼び出す SearchProductServiceResult result = productService.searchProduct(parameter); return productId != null ? ProductSearchMcpResult.ofDetail(result) : ProductSearchMcpResult.ofList(result); } } このクラスの役割はControllerと同じです。引数を組み立てて既存のserviceを呼び、結果をエージェント向けに整形するだけで、ビジネスロジックは置きません。検索ロジック本体は、REST APIのために実装済みの ProductService をそのまま呼んでいます。 これだけでMCPサーバーのツールが実装できてしまいます。 今回実装した検索ツール12個のほとんどは、既存のservice層を変更することなく再利用できました。REST APIのために実装してきたservice層を、MCPツールからそのまま再利用できるのが、既存アプリに組み込む構成のいちばんのメリットだと思います。 ツール 概要 searchProducts プロダクトの一覧・詳細検索 searchGroups 部署(グループ)の一覧取得 searchTeams チームとメンバーの検索 searchUsers ユーザーの検索 searchDomains ドメインの検索 searchGithubRepositories GitHubリポジトリの検索 searchVulnerabilitySummaries 脆弱性サマリの横断検索 searchVulnerabilityDetails 脆弱性詳細の検索 searchSbom SBOM(ソフトウェア部品表)の検索 searchEol EOL(サポート期限)の検索 searchArn AWSリソース(ARN)の検索 searchSchedules 開発環境のコスト削減を目的とした、AWSリソース起動停止スケジュールの検索 実装にあたって、3点補足します。 1つ目はツールヒント(annotations)です。MCPの仕様では、ヒントが未指定のツールは readOnlyHint=false ・ destructiveHint=true 、つまり「破壊的な操作をするかもしれないツール」というデフォルトで扱われます。エージェントが検索ツールの利用をためらわないよう、読み取り専用であることをヒントで明示しています。 2つ目は引数の形です。引数は1つのオブジェクトにまとめず、フラットに並べています。Spring AIはメソッドの引数をそれぞれinputSchemaのプロパティに変換するため、REST APIのようにパラメータクラスへまとめると、エージェントから見えるJSONスキーマに余計な階層ができてしまいます。既存のパラメータクラスの流儀とは意図的に変えている部分です。 3つ目はレスポンスの件数です。CMDBは、SBOMのように件数が膨大なデータも持っています(執筆時点で50万件超)。こうしたデータをそのまま返すと、エージェントのコンテキストを圧迫し、MCPの結果サイズ上限にも引っかかります。件数の多いデータを扱うツールでは絞り込み条件を必須にし、1ページ50件のページングと総件数(totalCount)の返却をあわせて実装しています。ページを分けても、データが足りなければエージェントがpageを変えて自分でツールを呼び直してくれるため、複数回の呼び出しで必要なデータを集められます。 認証認可 一番不安だった認証認可ですが、結論としては自前の認可サーバーを実装する必要はなく、既存のEntra IDをそのまま認可サーバーとして使えました。MCPの認可仕様の登場人物と流れは以下のとおりです。 sequenceDiagram participant Agent as AIエージェント<br/>(Claude Code等) participant MCP as CMDBバックエンド<br/>(MCPサーバー) participant Entra as Microsoft Entra ID Agent->>MCP: ① POST /mcp(トークンなし) MCP-->>Agent: ② 401 + WWW-Authenticate<br/>(メタデータの場所を案内) Agent->>MCP: ③ GET /.well-known/oauth-protected-resource MCP-->>Agent: ④ メタデータ応答<br/>(認可サーバー = Entra ID の場所) Agent->>Entra: ⑤ 認可コードフロー(PKCE)<br/>ブラウザでSSOログイン Entra-->>Agent: ⑥ アクセストークン(JWT) Agent->>MCP: ⑦ Bearerトークン付きでツール実行 MCP->>MCP: ⑧ JWT検証(署名・issuer・有効期限) MCP-->>Agent: ⑨ 検索結果 重要なのが③④のProtected Resource Metadata (RFC 9728)です。MCPサーバーは /.well-known/oauth-protected-resource でメタデータを公開し、「このサーバーの認可サーバーはどこか」をエージェントに教えます。エージェントはこれを読んで、自動的にEntra IDのログイン画面(ブラウザのSSO)へユーザーを誘導してくれます。 Spring側の実装は、ほぼ標準機能の組み合わせで済みました。 JWT検証: Spring SecurityのOAuth2 Resource Server。application.ymlの issuer-uri の設定だけで、Entra IDが発行したJWTの検証が動きます メタデータの公開: Spring Securityが標準でRFC 9728をサポートしており( OAuth2ProtectedResourceMetadataFilter )、認可サーバーの場所などをJavaの設定コード数行のカスタマイズだけで公開できます 1点だけインフラ側の対応が必要でした。RFC 9728のメタデータはドメインルートの /.well-known/ 配下で公開する必要がありますが、バックエンドは /api のコンテキストパスで動いています。そこで、CloudFrontで /.well-known/oauth-protected-resource* へのリクエストをALB(オリジンパス /api )にルーティングして解決しました。 ちなみに、Spring AIのドキュメントには、MCPサーバーの認証認可をコミュニティモジュールの mcp-security で実現する方法も紹介されています。 なお、AIエージェントによってOAuth周りの実装には微妙に差があり、あるエージェントでは問題なく接続できても、別のエージェントではエラーになることがありました。MCPサーバーを社内に展開する場合は、利用が想定されるエージェントで早めに接続確認をしておくと安心です。 AOPによる利用状況ログ 社内展開後の利用状況を把握するため、ツール呼び出しのログも仕込みました。 @McpTool アノテーションを対象にしたAOPのアスペクトを1つ書くだけで、ツールクラスが今後増えても自動でログ対象になります。ログにはツール名・ユーザーID(JWTのクレームから取得)・検索条件を構造化フィールドとして出力し、ダッシュボードで「誰がどのツールをどんな条件で使っているか」を集計できるようにしています。 実行結果 最後に、社内情報のためお見せできる部分が少なくなってしまいますが、実際の使用感をご紹介します。 接続 (Claude Codeの場合) 以下のコマンドでClaude CodeにCMDBのMCPを登録します。 claude mcp add --transport http cmdb-mcp https://xxxxx/api/mcp \ --client-id xxx \ --callback-port 29352 サーバー一覧でCMDBのMCPを選択し、認証を開始します。 ブラウザで認証をします。 ツールを呼び出してみる 試しにSpring Bootで構築しているプロダクト一覧を出してみます。 「Spring Bootで構築しているプロダクトを洗い出して」と聞いてみると、CMDBのMCPサーバーにあるSBOM検索ツールを spring-boot-autoconfigure のコンポーネントを検索条件に入れて呼び出します。 こちらがレスポンスの一部になります。全部で31プロダクトがSpring Bootを採用しているそうです。多いですね。 次のステップ 更新系ツールへの拡張: 現在は検索専用ですが、登録・更新系のツールも要望があります。誤操作のリスクが検索とは段違いなので、権限制御や実行前の確認の仕組みとセットで検討していきたいと思います さいごに 今回は、Spring Boot製の社内CMDBをSpring AIでMCPサーバー化し、AIエージェントから社内の構成情報を検索できるようにした取り組みについてお話ししました。 MCPサーバーの構築はPythonやTypeScriptの情報が多いですが、既存のSpring Bootアプリを持っているなら、service層も認証もインフラもそのまま使い回せるSpring AIは十分有力な選択肢です。この記事が、JavaでのMCPサーバー構築を検討している方の参考になれば嬉しいです。 AIエージェントを中心とした働き方はまだまだ進化していくと思うので、キャッチアップと実践を繰り返しながら、Platform Engineeringチームとしてツールの整備を進めていきたいと思います。
この記事でわかること Compose Multiplatform(CMP)の動画再生は、Android では AndroidView + Media3 PlayerView 、iOS では UIKitView + AVPlayerLayer 、Web では canvas の裏に置いた <video> 要素と、プラットフォームごとにまったく異なる Interop 機構で動いている 「どのフレームワークがネイティブか」は見せかけの問いで、デコードと撮像の層は Flutter も React Native も CMP もすべて OS の同じネイティブ API を使っている。本当の分岐点は「ネイティブが描いた 1 フレームを、フレームワーク自身の UI とどう合成するか」にある Android の SurfaceView / TextureView を画面ごとに選べる自由は CMP(と React Native)にはあり、Flutter の video_player には長らくなかった。一方、実際のプロジェクトでは角丸プレビューのために、あえて Flutter と同じテクスチャコピー経路( TextureView )を選んだ CameraK 0.4 やコミュニティ製の動画プレイヤーといった「若いエコシステム」の現実と、その不足を expect/actual で各プラットフォームのネイティブ API に直接下りて補うという対処法を、実プロジェクトのコードで示す はじめに こんにちは、モバイルアプリ開発部の Yao です。 私は以前から Kotlin Multiplatform(KMP)/ Compose Multiplatform を継続的に研究していて、このテーマで何本か記事を書いてきました。興味のある方は 過去の記事一覧 もご覧ください。本記事では、 CMP の弱点として真っ先に名前が挙がる「動画再生」と「カメラ」を、Android・iOS・Web の 3 プラットフォームでどう実装したか を書きます。 題材は、社内で開発した 動画アバター生成アプリ です。自分の顔を動画で録画してデジタル分身(アバター)を作り、テキストを入力するとその分身が自分の声で話す動画を生成する、というアプリです。録画・録音・再生というメディア機能がプロダクトの根幹にあり、「CMP でメディアをやるとどうなるか」を検証するには格好の題材でした。 生成された動画を確認・承認する VideoDetailScreen (本記事のアプリ画面はいずれもデザインモックで、人物はすべてダミーです) CMP のエコシステムはまだ若いです。この記事では、その若さがどこでどう痛むのか、そしてその痛みが expect/actual というアーキテクチャでどこまで緩和できるのかを、実際の行数・バージョン番号・コードで示します。形容詞ではなく数字で語ることを目標にします。 このアプリのアーキテクチャはどうなっているか 本アプリは 1 つの Kotlin コードベースから Android / iOS / Web(wasmJs)の 3 プラットフォームに配信しています。モジュール構成は次のとおりです。 androidApp ┐ iosApp ├─→ sharedUI ─→ sharedLogic ─→ apiClient webApp ┘ モジュール Kotlin ファイル数 役割 sharedLogic 113 Compose 依存ゼロ の純 Kotlin。Ktor / Serialization / Coroutines / Koin-core sharedUI 154 モバイル向け共有 CMP 画面 + 3 プラットフォームの actual 実装 webApp 35 モバイルのレイアウトは再利用せず、デスクトップ向けに再構築 apiClient 191 OpenAPI Generator 7.22.0 で生成しリポジトリにコミットした自社バックエンドのクライアント androidApp / iosApp 6 / 4(Swift) エントリポイントのみ 設計は Clean Architecture + MVI です。UseCase が 32 個(1 クラス 1 操作)、Repository が 7 個、ViewModel が 12 個あり、各 ViewModel は MutableStateFlow<XxxUiState> と不変 data class による単一の状態ソース(Single Source of Truth)を持ちます。プラットフォーム能力は 20 個の expect 宣言 に集約しています。 技術スタックはかなり新しめです。Kotlin 2.3.10 / CMP 1.10.1 / AGP 9.0.1 / Ktor 3.4.0 / Koin 4.1.1 / Navigation 3 1.0.0-alpha06 / Media3 1.9.0 / Coil3 3.3.0 / CameraK 0.4 / FileKit 0.13.0、そして wasmJs ターゲット。エコシステムの最前線に立つと何が起きるかは、後半で具体的に書きます。 プロジェクトの特徴を 1 つだけ挙げると、 ロジックは 100% 共有、UI は形態ごとに分岐 という方針です。Web はモバイルのレイアウトを再利用せず 33 個の独立した Web*Screen をデスクトップ向けに構築していますが、ViewModel の状態機械はモバイルとまったく同じものを使います。この「ロジックとプレゼンテーションの分離」が、後述するメディア実装の 3 プラットフォーム分岐を支える土台になっています。 なぜ Compose Multiplatform を選んだのか Flutter や React Native ではなく CMP を選んだ理由は 3 つあります。 1 つ目は、ロジックの複雑さが UI ではなく状態機械にあったこと。 本アプリのビジネスロジックには、本人確認(consent)のブラウザ連携の状態遷移、アバター生成状態の 3 値導出( status × generationRetryable × retryCount → GENERATING / RETRYABLE / TERMINAL )、動画生成とアバター訓練それぞれの 60 秒間隔ポーリングといった、型で守りたい遷移が多くあります。この種のロジックは、sealed interface と data class を持つ Kotlin で書くのが Dart や TypeScript より安全だと判断しました。 2 つ目は、チームのスキルセット。 メンバーは Kotlin / Android 出身で、UseCase 32 個 + StateFlow の MVI という構成は学習コストゼロで書き始められました。 3 つ目が最も重要で、「漸進的にネイティブへ戻れる」こと。 sharedLogic は Compose 依存ゼロの純 Kotlin なので、 Swift から直接呼べます 。実際 iOS はすでに SwiftUI のネイティブシェル + CMP コンテンツ画面というハイブリッド構成です。将来 UI をすべて SwiftUI / Jetpack Compose に置き換えることになっても、ロジック側は 1 行も変わりません。Flutter や React Native では、UI フレームワークを捨てる = ほぼ全部書き直しですが、KMP ではロジック資産がそのまま残ります。この退路の存在が、以前の記事 「Kotlin Multiplatform ハイブリッドモード」 で書いたハイブリッドモードの実利です。 「動画再生」と「カメラ」——若いエコシステムで最初にぶつかる壁 CMP でアプリを作ると宣言したとき、最初に聞かれるのが「動画とカメラはどうするの?」です。もっともな質問です。Flutter には flutter.dev 公式チームが保守する video_player と camera があり、React Native には無数のプロダクションで数年動いている react-native-video と vision-camera があります。CMP にはそれに相当する「公式の定番」がまだありません。 エコシステムの現実を数字で見る 本アプリの動画再生は io.github.kdroidfilter.composemediaplayer (ComposeMediaPlayer)というコミュニティ製ライブラリをベースにしています。Flutter の video_player のような公式チーム保守のプラグインではないので、挙動の細部を確かめたいときはライブラリの公開ソースを読み解くことになります。この記事の Interop 解説も、そのソースリーディングの成果です。 カメラは CameraK を使いました。バージョンは 0.4 です。Flutter の camera が公式チームの保守下にあり、 vision-camera が成熟していることと比べると、この数字がエコシステムの若さを端的に表しています。 それでもこの構成で 3 プラットフォームのアプリは出荷できました。なぜ成立したのかを説明するために、まず前提となる誤解を 1 つ解きます。 そもそも「ネイティブかどうか」は論点ではない クロスプラットフォームの比較記事では「Flutter は独自レンダリングだから」「RN はネイティブだから」という議論をよく見ますが、動画とカメラに関しては、 デコードと撮像の層は 3 者とも完全に同じ です。自前で H.264 デコーダやカメラドライバを書いているフレームワークは 1 つもありません。 フレームワーク / ライブラリ 再生のデコード カメラの撮像 Flutter: video_player / camera (いずれも flutter.dev 公式プラグイン) ExoPlayer / AVPlayer CameraX・Camera2 / AVCaptureSession React Native: react-native-video / vision-camera ExoPlayer / AVPlayer CameraX / AVCaptureSession 本アプリ(CMP) Media3 ExoPlayer / AVPlayer / <video> CameraK 経由の CameraX / AVCaptureSession、Web は getUserMedia を直接 どのフレームワークを選んでも、動画をデコードするのは ExoPlayer と AVPlayer で、カメラを制御するのは CameraX と AVCaptureSession です。つまり「画質」「デコード性能」「対応コーデック」でフレームワーク間に差は出ません。 では何が違うのか。 ネイティブ API がデコードした 1 フレームを、フレームワーク自身が描いた UI(ボタン、テキスト、角丸カード)とどう合成するか 。ここがすべての分岐点です。 本当の分岐点:ネイティブのフレームをどう合成するか 合成戦略は大きく 2 路線に分かれます。 路線 A:テクスチャ搬送。 ネイティブプレイヤーの出力を GPU テクスチャとしてフレームワーク側にコピーし、フレームワークが自分のシーングラフの中で 1 枚の画像として描く。Flutter の video_player は従来この方式が既定でした。 路線 B:ネイティブ view の埋め込み。 ネイティブの view( SurfaceView / UIView / DOM 要素)をそのまま画面に重ね、フレームワークの描画面と OS のコンポジタで合成する。React Native と、CMP の Interop( AndroidView / UIKitView )はこちらです。 路線 A は毎フレームのコピーと引き換えに自由な変換を、路線 B はゼロコピー直結と引き換えに手動のレイヤー管理を受け入れる 両者の得失は明確に非対称です。 観点 路線 A:テクスチャ搬送(Flutter) 路線 B:ネイティブ view 埋め込み(RN / CMP) 角丸・クリップ・回転・拡縮 ✅ 動画はただの画像なので自由に変換できる ⚠️ ネイティブ層は親の clip を受けない( SurfaceView の場合。 TextureView なら可) 他 UI との z 順の重なり ✅ 常に正しい ⚠️ 手動管理。Web ではほぼ不可能 リスト内スクロール ✅ フレーム同期が自然に取れる ⚠️ ネイティブ層がずれて「浮く」ことがある GPU 負荷 / 遅延 ⚠️ コピーと同期で約 1 フレーム分のコスト ✅ ゼロコピーでハードウェア合成に直結( SurfaceView の場合) PiP(iOS)/ AirPlay / ロック画面操作 / ネイティブ字幕 ⚠️ 失われる。追加プラグインが必要 ✅ ネイティブプレイヤーの機能がそのまま使える DRM のセキュアパス(Widevine L1) ❌ テクスチャ経路では取得できない ✅ 可能( SurfaceView が必要) この表を頭に入れたうえで、CMP が 3 プラットフォームでそれぞれどう実装しているかを見ていきます。本アプリでは ComposeMediaPlayer の上に 272 行のラッパー VideoPlayer.kt を被せ、成果物動画の再生( VideoDetailScreen )、アバターのプレビュー再生( AvatarDetailScreen )、録画素材の見直し( AvatarCreateVideoReviewScreen )とその Web 版、計 6 画面で使っています。 3 つのプラットフォーム、3 つの Interop Android:AndroidView と、SurfaceView / TextureView の選択権 Android 実装は AndroidView で Media3 の PlayerView を Compose ツリーに埋め込みます。興味深いのは、このライブラリが 動画を描く面(surface)を 2 種類用意していて、XML レイアウトで切り替えられる ことです。 <androidx.media3.ui.PlayerView android:layout_width="match_parent" android:layout_height="match_parent" app:surface_type="surface_view" app:use_controller="false" /> <androidx.media3.ui.PlayerView android:layout_width="match_parent" android:layout_height="match_parent" app:surface_type="texture_view" app:use_controller="false" /> SurfaceView は OS のハードウェアコンポジタに直結した独立レイヤーで、ゼロコピーかつ低遅延、DRM のセキュアパスもここを通ります。ただし 独立レイヤーであるがゆえに、親 View のクリップに参加しません 。一方 TextureView は動画フレームを GPU テクスチャとして View ヒエラルキーの描画に合流させるため、1 コピー分のコストと引き換えに、普通の View と同じように角丸クリップや変換が効きます。 本アプリのデフォルトは TextureView です。 @Composable actual fun VideoPlayerSurface( playerState: VideoPlayerState, modifier: Modifier, contentScale: ContentScale, overlay: @Composable () -> Unit ) { VideoPlayerSurfaceInternal( playerState = playerState, modifier = modifier, contentScale = contentScale, overlay = overlay, surfaceType = SurfaceType.TextureView ) } 理由は具体的です。アバター詳細画面( AvatarDetailScreen )のプレビューは 350×600 の角丸カードで、 SurfaceView を使うと角丸の内側から四角い動画がはみ出して、カードの角を突き破ります。デザインを守るには TextureView しかありませんでした。 問題の角丸プレビューカード。この 4 つの角を守るために TextureView を選んだ :::message ここに直感に反する事実があります。前節の表のとおり、 TextureView の「GPU テクスチャとしてコピーする」経路は、 まさに Flutter の video_player が従来常に通ってきた路線 A そのもの です。つまり本アプリは Android では、角丸というデザイン要件のために、Flutter と同じ性能特性を自ら受け入れています。 ::: ただし Flutter との違いは「選べること」にあります。 video_player も現在は VideoViewType.platformView で platform view を選べますが、この選択肢が入ったのは登場から長い年月が経ってからでした。CMP の路線 B では、 surfaceType 引数 1 つで 画面ごとに SurfaceView (性能・DRM 優先)と TextureView (クリップ・変換優先)を選び分けられます。角丸カードには TextureView 、全画面再生には SurfaceView 、という使い分けが同じアプリの中でできます。この選択権こそが路線 B の実利で、本アプリの角丸プレビューはそれを行使した実例です。 iOS:UIKitView と layerClass = AVPlayerLayer iOS 実装は 3 プラットフォームの中で最も素直です。CMP の UIKitView でネイティブ UIView を埋め込みますが、その UIView の backing layer 自体を AVPlayerLayer に差し替える という UIKit の古典的なテクニックを使っています。 private class PlayerUIView : UIView { companion object : UIViewMeta() { override fun layerClass(): ObjCClass = AVPlayerLayer } @OverrideInit constructor(frame: CValue<CGRect>) : super(frame) var player: AVPlayer? get() = (layer as? AVPlayerLayer)?.player set(value) { (layer as? AVPlayerLayer)?.player = value } } Objective-C の +layerClass オーバーライドを Kotlin/Native の UIViewMeta でそのまま書けている点に注目してください。view の layer が最初から AVPlayerLayer なので、余計なサブレイヤー管理が不要で、AVPlayer のデコード結果は Core Animation のコンポジタにゼロコピーで渡ります。あとは UIKitView(factory = { PlayerUIView(frame = cValue<CGRect>()) }, ...) で Compose に埋め込むだけです。 きれいな構図ですが、路線 B の代償はここにもあります。CMP の UIKitView はネイティブ view を Compose のシーングラフの中に描くのではなく、 Compose が描画している MTKView の兄弟としてその上に重ねます 。つまり動画 view は Compose の描画世界の外にいて、タッチイベントの透過、レイヤーの前後関係、クリップはすべて Interop の境界で個別に面倒を見ることになります。ComposeMediaPlayer がこの境界を吸収するために、再生コントロールなどを載せるための overlay スロットを API として用意しているのは、その裏返しです。 Web:canvas の裏の video 要素と、BlendMode.Clear で開ける穴 Web(wasmJs)実装は、この記事でいちばん劇的な部分です。CMP の Web ターゲットは画面全体を 1 枚の canvas(Skia)に描画します。では <video> 要素はどこに置くのか。答えは「 canvas の裏 」です。 internal fun HTMLVideoElement.applyInteropBehindCanvas() { val wrapper = parentElement as? HTMLElement ?: return wrapper.style.apply { zIndex = "-1" // <video> を Compose の canvas の背後へ setProperty("pointer-events", "none") backgroundColor = "transparent" } } z-index: -1 で動画をページの最背面に沈めます。当然このままでは canvas に隠れて見えません。そこでアプリ側のコードで、Compose の canvas に 文字どおり穴を開けます 。 // BlendMode.Clear zeroes every pixel (including alpha) in the Compose canvas // over this area, punching a transparent hole so the video element behind // the canvas shows through. Box(modifier = Modifier.fillMaxSize().drawBehind { drawRect(color = Color.Transparent, size = size, blendMode = BlendMode.Clear) }) BlendMode.Clear はその領域のピクセルをアルファ値ごとゼロにするので、canvas のその部分が完全に透明になり、背後の <video> が透けて見える、という仕掛けです。カメラのライブプレビューも同じ機構で、 getUserMedia() で取得した MediaStream を WebElementView 経由で HTMLVideoElement に接続し、canvas の穴から覗かせています。 動画は常に最背面。見えるのは canvas に開けた穴の領域だけで、UI は必ず動画より上に載る :::message これは 2 つ目の、直感に反する事実につながります。この方式の帰結として、 Web では動画は構造的に、すべての Compose コンテンツより下にしか存在できません 。「動画の上に UI を重ねる」ことは(canvas 側に描く限り)可能ですが、「UI の下に動画、そのさらに下に別の UI」のような自由な z 順は原理的に組めません。録画中のタイマー表示や停止ボタンは、この制約を前提に「穴の上に Compose で描く」設計になっています。これはバグや実装の手抜きではなく、canvas レンダリング + DOM 動画という構成の構造的な限界です。 ::: 公平を期すために書いておくと、 Web の動画は 3 フレームワークとも体面を保てていません 。Flutter Web の video_player も HtmlElementView で DOM の <video> を重ねる同類の方式で、canvas と DOM の合成に相応の制約とコストを抱えます。React Native には公式の動画プレイヤーがそもそもなく、 react-native-video の Web 対応もまだ発展途上です。ブラウザでは動画デコードとページ合成の主導権がブラウザ自身にあるため、どのフレームワークも <video> 要素と「同居」する方法を探すしかないのです。 カメラと録音:expect/actual が第一級市民である強み 動画再生はコミュニティ製ライブラリで戦いましたが、カメラと録音はもっと直接的に、 expect/actual で各プラットフォームのネイティブ API に下りて実装しました。まず実装量の実態を見てください。 能力 Android iOS Web 動画録画画面 AvatarCreateVideoRecordingScreen.mobile.kt 467 行 (Android / iOS 共有) 同左 AvatarCreateVideoRecordingScreen.wasmJs.kt 425 行 カメラ実装 CameraK 0.4(CameraX ベース) CameraK 0.4(AVCaptureSession ベース) getUserMedia() + MediaRecorder + WebElementView AudioRecorder 83 行( MediaRecorder ) 108 行( AVAudioRecorder ) 11 行の stub MicrophoneDeviceProvider 70 行 17 行 12 行の stub AudioPlayer 53 行 63 行 57 行( HTMLAudioElement ) モバイル側の録画は CameraK に任せました。フロントカメラ固定・HD 画質・録画プラグインという構成が、Android / iOS 共通の 1 ファイルで書けます。 val cameraConfig = remember { CameraConfiguration( flashMode = FlashMode.OFF, cameraLens = CameraLens.FRONT, ) } val cameraState = rememberCameraKState( config = cameraConfig, setupPlugins = { holder -> holder.attachPlugin(plugin) }, // VideoRecorderPlugin ) CameraKScreen( modifier = Modifier.fillMaxSize(), cameraState = cameraState.value, content = { _ -> RecordingOverlay(/* 録画 UI */) }, ) 録画中の画面。カメラプレビュー(ネイティブ層)の上に、タイマー・スクリプト・停止ボタンを Compose の overlay として重ねている Web はライブラリなしで、 getUserMedia() と MediaRecorder を wasmJs から直接呼びます。mimeType のネゴシエーション(ブラウザごとに対応コーデックが違う)もブラウザ API の流儀のまま書きました。 表の中の「11 行の stub」は隠さず書いておきます。Web 版の AudioRecorder と MicrophoneDeviceProvider は中身のない空実装で、音声クローン用の録音フローはモバイル専用と割り切りました。3 プラットフォーム対応とは「全機能 × 全プラットフォーム」ではなく、プラットフォームごとに提供範囲を選ぶことでもあります。 expect/actual はこの割り切りも型として明示できます。 同じ状態機械、違うトリガー:ConsentReturnFromBrowserEffect expect/actual の使いどころとして、私が最も気に入っている例を挙げます。アバター訓練には本人確認(consent)があり、外部ブラウザで確認を完了してからアプリに戻ると、アプリ側が「戻ってきた」ことを検知してステータスの確認を始めます。この「外部ブラウザから戻ってきた」というイベントは、プラットフォームによって観測手段がまったく違います。 @Composable expect fun ConsentReturnFromBrowserEffect(onReturned: () -> Unit) Android / iOS (mobileMain・36 行):Compose の lifecycle を観測し、 ON_PAUSE → ON_RESUME の順で来たときだけ発火。ブラウザを開けば必ずアプリが pause するので、初回 composition や無関係な resume を誤検知しません Web (wasmJs・36 行): window の blur → focus を監視。wasmJs では lifecycle の ON_RESUME が確実には発火しないため、専用の経路を用意しました consent の状態機械そのもの(待機 → 確認中 → 完了)は ViewModel にあり、3 プラットフォームで完全に同一です。 違うのはトリガーの観測方法だけで、そこだけが actual として差し替わる 。状態機械のテスト・修正・拡張は 1 か所で済みます。 :::message ここが 3 つ目の、そしてこの記事で最も伝えたい、直感に反する洞察です。「CMP はエコシステムが若く、公式プラグインがない」という弱点は事実ですが、そのダメージは KMP のアーキテクチャによって 構造的にかなりの部分まで相殺されています 。Flutter で MethodChannel や PlatformView を書くのは「プラグインがないときの例外的な脱出路」という位置づけですが、KMP の actual は日常の文法そのものです。同じ AudioRecorder クラスを Android 83 行 / iOS 108 行 / Web 11 行で書き分ける作業は、型安全で、IDE 補完が効き、ビジネスコードと同じモジュールの中で完結します。「ライブラリがなければネイティブ API を直接呼べばいい」が、儀式なしで成立するのです。 ::: Flutter / React Native と比べたときの CMP の強みは何か 先に明確にしておきます。 「CMP の動画プレイヤーやカメラは Flutter より優れている」という主張は成立しません。 成熟度では公式保守の video_player / camera や実績豊富な vision-camera に及びません。そのうえで、このプロジェクトを通して実感した CMP 固有の強みは次の 3 点です。 1. Interop 路線の選択権がある。 CMP は路線 B(ネイティブ view 埋め込み)ですが、Android では SurfaceView / TextureView を画面単位で選べます。角丸が要る画面は TextureView 、性能や DRM が要る画面は SurfaceView 。Flutter の video_player には、このスイッチに相当する選択肢が長らく存在しませんでした。 AvatarDetailScreen の角丸プレビューカードは、この選択権の行使例です。 2. 「ネイティブに下りる」ことが第一級市民である。 前節のとおり、 expect/actual は例外処理ではなく日常の文法です。エコシステムに欠けている部品は、待つのではなく 83 行書けば埋まります。 3. ネイティブ API サーフェス全体に直接触れられる。 本アプリが使っているのは、Media3 PlayerView の app:surface_type 、UIView の layerClass 差し替え、 MediaRecorder の mimeType ネゴシエーションといった、各プラットフォームの 素の API です。プラグイン作者が切り出した「最大公約数的な抽象」を経由しないので、プラグインが公開していない機能のために上流(upstream)のリリースを待ったり PR を出したりする必要がありません。 引き換えに何を受け入れたか 強みだけ書いて終わる記事は信用できないので、CMP を選んだ引き換えに受け入れたデメリットも正直に書きます。 動画再生をコミュニティ製ライブラリ 1 本に依存している。 公式チーム保守という後ろ盾がなく、挙動の細部を確かめたり問題を切り分けたりするたびに、ライブラリ内部の Interop 実装まで読み解く理解コストがかかる カメラライブラリのバージョンは 0.4。 公式チームが保守する Flutter camera や、長年プロダクションで検証されてきた vision-camera と比べ、実績の厚みが違う Web の録音まわりは stub。 AudioRecorder 11 行、 MicrophoneDeviceProvider 12 行の空実装で、録音フローはモバイル専用に割り切った Web の動画は構造的に最背面。 BlendMode.Clear の穴あけ方式である限り、動画と Compose UI の z 順は「動画が常に下」で固定される これらを踏まえた私の判断はこうです。 プロダクトの重心がメディア能力そのものにあるなら、 vision-camera を持つ React Native がおそらく最も成熟した実用的な選択肢でしょう。 一方、重心が複雑なビジネス状態機械にあり、長期的にネイティブ UI へ回帰する可能性を残したいなら、CMP は正しい選択です。メディア層で余分に書いたコードは、「 sharedLogic を Swift から直接呼べる」「いつでもネイティブ UI に戻れる」という自由への入場料だったと総括しています。 まとめ 動画・カメラのデコードと撮像の層は、Flutter / React Native / CMP のどれを選んでも同じネイティブ API に行き着く。差が出るのは「ネイティブのフレームと自前 UI の合成方法」で、テクスチャ搬送(路線 A)とネイティブ view 埋め込み(路線 B)は得失が非対称 CMP は路線 B に立ちつつ、Android では SurfaceView / TextureView を画面ごとに選べる。本アプリは角丸プレビューのために、あえて Flutter と同じテクスチャ経路( TextureView )をデフォルトにした iOS は UIKitView + layerClass = AVPlayerLayer でゼロコピー再生。Web は canvas 背面の <video> に BlendMode.Clear で穴を開けて見せる方式で、動画が常に UI の下という構造的制約を受け入れた エコシステムの若さ(コミュニティ製の動画プレイヤー、CameraK 0.4、Web 録音の stub)は実在するコストである。同時に、 expect/actual によって「ネイティブ API に直接下りる」ことが日常の文法になっているため、そのダメージはアーキテクチャで大きく緩和できる CMP のメディアエコシステムは、あと数年は「自分の手を動かして埋める」フェーズが続くと思います。それでも、埋めるための道具立て—— AndroidView / UIKitView / WebElementView 、そして expect/actual ——は既に揃っていて、実プロダクトを 3 プラットフォームに出荷できる水準にあります。この記事の数字とコードが、同じ判断を迫られている方の材料になれば幸いです。 最後まで読んでいただき、ありがとうございました!
こんにちは。KINTOテクノロジーズ(KTC)で、ユーザーファースト推進活動の運営に関わっているる かーびー です。 KTCでは 「ユーザーファースト」 を会社の重点方針のひとつに掲げており、社内勉強会「ユーザーに寄りそわNight!」の開催もその取り組みのひとつです。 Vol.02 のレポートに続き、今回はVol.03の内容をお伝えします。 テーマは「クイックなリサーチ」。サービスに感じた「このまま進めていいのかな」という違和感を、すぐに改善案にせず、手元にある情報から一度確かめてみる進め方です。 サービスの企画や改善に関わっていると、「この動線で、必要な情報までたどり着けるのだろうか」「説明の途中で迷う人がいるのではないか」と感じる場面があります。 こうした違和感は改善を考えるきっかけになりますが、その時点では本当に問題が起きているのか、ユーザーにどんな影響があるのかまではわかりません。 生煮えのままチームに改善案をあげたとして、関係者の立場によって持っている情報や前提が異なるため、提案に至った背景から理解を取り付ける必要があります。一定の共感は得られるとしても、関係者が多いほど時間がかかるものです。 Vol.03で取り上げたのは、この違和感と改善案のあいだに、「確かめる」を一段はさむ実践でした。 第3回の寄りそわNightでは、デジタル戦略部の上田貴弘さんに登壇していただきました。オンライン相談やVoC(Voice of Customer:お客様の声)の分析、ユーザビリティテストなどを通じて、ユーザーの声や行動を捉える業務に取り組んでいます。当日は、オンライン相談やVoCから見えてきた仮説を、新入社員とのユーザビリティテストで確かめた事例が紹介されました。 新入社員に実際のサービスを使ってもらう 今回の事例の出発点は、オンライン相談やVoCを見る中で生まれた「サービスの利用途中に、いくつかのつまずきが起きているのではないか」という仮説でした。 ただ、相談や問い合わせからわかるのは、ユーザーが言葉にした困りごとが中心です。実際の画面上のどこで手が止まり、何が理解しにくかったのかまではつかめません。 そこで登壇者の上田さんが紹介したのが、新入社員に協力してもらうユーザビリティテストでした。 入社したばかりのメンバーは、業務やサービスに関する前提知識がまだ少なく、社内では当たり前になっている言葉や流れにも、初めてサービスに触れる人に近い反応を示します。社内メンバーなので確認したい内容を共有しやすく、短い準備で実施できる。実践のしやすさも、この方法が選ばれた理由の一つでした。 もちろん、新入社員がそのまま実際のお客様を代表するわけではありません。 今回は、サービスのどこにつまずきがありそうかをまず確かめるための検証です。 テストでは、実際に画面を操作してもらいながら、利用中の発話や迷い、説明を読んだあとの反応を観察していきます。説明を何度も読み返す、次に進まずに考え込む、前の画面に戻って情報を探す。 操作の様子を追うことで、どの場面で立ち止まったのか、どの説明が伝わりにくかったのか、判断するために何の情報が足りなかったのかが具体的になっていきます。 設計上は、説明を読めばそのまま次の画面へ進める想定でも、実際には同じ箇所を何度も読み返している。反対に、作る側が課題だと考えていた箇所では、ほとんど迷いが起きていない。登壇を通して繰り返し語られていたのは、この「想定」と「実際に起きていること」を分けて見ることの大切さでした。 紹介された例のひとつが、本人確認書類の画像アップロードです。最新のスマートフォンで撮ったきれいな写真が容量の上限を超え、エラーになる。設計した時点では問題のなかった仕様でも、ユーザーの利用環境が変わることで、思わぬところにつまずきが生まれます。実際の利用を見て初めてわかるつまずきは、こうした単純なところにも潜んでいます。 エラーに遭遇した人は、そのあと撮り直して先へ進めているのか。それとも、そこで利用をやめてしまっているのか。実際の行動が見えると、次に確かめたいことが具体的になります。こうして、ひとりの違和感が、チームで確かめられる問いに変わっていきます。 オンライン相談やVoCから感じていた違和感に実際の行動が重なったことで、「使いにくそう」という感覚だけでは見えていなかった部分が確認できた。そんな事例でした。 提案の前に、同じ景色を見る 確かめた結果を、チームでどう使うのか。この話題で登壇のキーワードになっていたのが「同じ景色を見る」という言葉です。 ここでいう「同じ景色」とは、全員が同じ意見になることではありません。改善案を挟んで向かい合うのではなく、ユーザーの行動や声を真ん中に置いて、隣に並んで見る。ユーザーに何が起きていたのか。どの情報から、課題かもしれないと考えたのか。何が変われば、困りごとが解消されたと言えそうなのか。立場や役割が違っていても、まずユーザーを共通の起点にして考える、という意味です。 改善案だけが先に出てくると、受け取る側は、何を解決しようとしているのか、なぜ今扱う必要があるのかを、まず確認しなければなりません。ユーザーの行動や発言、問い合わせ、ログが先に共有されていれば、改善案への賛否を決める前に、「何が起きていそうか」から話し始められます。 直したい箇所のリストは、開発の現場ではいつも長くなりがちです。 「一部の環境で画像のアップロードがエラーになることがある」という一行の報告と、目の前でユーザーが何度も写真を撮り直し、手を止めてしまう様子とでは、同じ事実でも受け取る重みが違います。 実際の行動を関係者が一緒に見ることで、ユーザーへの影響を起点に、何から手をつけるかを話せるようになります。 最初に置いた仮説が、正しいとは限りません。確かめてみると、想定とは別の場所で迷っていたり、課題だと思っていたことが実際にはそれほど影響していなかったりもします。 それでも、見ていた事実と仮説が共有されていれば、想定と違う結果が出たときに、次に何を確認するかを考えやすくなります。 事実は、提案を通すためだけの材料ではなく、チームで次の判断を下すための材料にもなります。 今回の事例を貫いていたのは、この考え方だったように思います。 参加者から寄せられた質問 イベント後のアンケートや質問フォームには、今回の内容を実践する際に迷いそうな点について、具体的な質問が寄せられました。たとえば、次のような内容です。 「売上を伸ばす」のような大きな目的から、どの粒度まで課題を小さくして調査を設計するのか アンケートで集めた声を、設問の意図や対象者を踏まえてどう読み取るのか 集めた事実を、聞き手が判断できる量にどう整理するのか また、「ユーザー中心の視点でサービス開発を進めるには、何から始めるとよいのか」「プロジェクト名など、普段使う言葉をユーザー起点に変えることにも意味があるのか」といった質問もありました。 内容を理解して終わるのではなく、自分の業務で試すとしたらどこで迷うのか。その段階まで踏み込んだ質問が多かったことが印象に残っています。ここで挙がった論点は、次回以降のテーマを考える際の材料にしていく予定です。 まず、手元にあるものを見てみる ユーザーファーストの実践というと、大がかりな調査や専門的なリサーチから始めるもののように感じるかもしれません。 今回の事例の起点は、オンライン相談やVoCなど、すでに手元にあった情報でした。そこから気になったことを、新入社員とのユーザビリティテストで確かめる。日々の業務で生まれる「このままでいいのかな」という違和感も、事実とセットにすると、チームで検討できる問いになります。 私自身、気になることがあると、つい解決策まで一気に考えてしまいます。今回の話を聞きながら、答えを出す前に、まず何が起きているのかを一度見てみることを忘れないようにしたいと思いました。 次に何かが気になったとき、まずは手元のログや問い合わせをひとつ確認してみる。今回のレポートが、そんな一歩につながればうれしいです。 関連記事 『ユーザーに寄りそわNight!』Vol.02レポート ── my route開発チームのフィールドワーク事例 KINTO Technologiesにおけるユーザーファースト推進の取り組み 定量データと定性データを行き来する ── ユーザーインタビューわいわい会
こんにちは。Engineering Officeのアクセシビリティアドボケート、辻勝利です。 普段使っているSlackに、ある時こんな投稿が流れてきました。 この投稿は、実際に流れてきた社内のお知らせと同じ構造で作った架空のメッセージです。以降の読み上げの引用と秒数は、すべてこの投稿を実測した値です。 目で見ると、気合いの入ったお知らせです。 NVDAというスクリーンリーダーで聴くと、こうなります。 クラッカー 絵文字ピカピカ 絵文字マイク 絵文字【募集】社内勉強会の登壇者を募集しますマイク 絵文字ピカピカ 絵文字クラッカー 絵文字 クラッカー 絵文字ピカピカ 絵文字マイク 絵文字クラッカー 絵文字ピカピカ 絵文字マイク 絵文字クラッカー 絵文字ピカピカ 絵文字マイク 絵文字クラッカー 絵文字ピカピカ 絵文字マイク 絵文字クラッカー 絵文字 ニテンリーダ なぜならば ニテンリーダ ニテンリーダ なぜならば ニテンリーダ ニテンリーダ なぜならば ニテンリーダ なぜならば ニテンリーダ なぜならば ニテンリーダ 来月の社内勉強会で話してくれる方を募集します。テーマは自由です。15 分の枠を 3 つ用意しています。(以下省略) 意味として受け取れるものは何もないまま音が続き、本文の最後の一文「迷っている方も、まずは気軽に声をかけてください。」を読み終わる頃には、2分31秒が過ぎていました。 1. スクリーンリーダーで読む、ということ スクリーンリーダーは、私たち視覚障害者が音声と点字で画面の情報を受け取るソフトウェアです。 基本的には、画面に表示されている内容を記号や絵文字を含めて一字一句そのまま読み上げるように設計されている支援技術です。冒頭のメッセージをNVDAで聴くと、何が起きているか。もう少し丁寧に辿ってみます。 フォーカスが投稿に当たった瞬間、読み上げが始まります。最初に届くのは「クラッカー 絵文字ピカピカ 絵文字マイク 絵文字」——タイトルの前に置かれた3つの絵文字です。そこを抜けてようやくタイトルの「【募集】社内勉強会の登壇者を募集します」に辿り着き、その後ろにも「マイク 絵文字ピカピカ 絵文字クラッカー 絵文字」が続きます。次の行は絵文字だけの装飾行で、13個ぶんの「クラッカー 絵文字」「ピカピカ 絵文字」「マイク 絵文字」が一つずつ読まれます。厄介なのはその次です。 ‥∵‥‥∵‥ のように記号が並んでいる箇所も一つひとつ読まれ、「ニテンリーダ なぜならば ニテンリーダ ニテンリーダ なぜならば ニテンリーダ ニテンリーダ なぜならば ニテンリーダ なぜならば ニテンリーダ なぜならば ニテンリーダ」と続きます。意味として受け取れるものは何もなく、思考を混乱させるような音だけが続きます。本文に入ってからも、小見出しのような行の前後に同じ3つの絵文字が挟まっているので、読み進めるあいだ何度も同じ音が割り込みます。ようやく最後の一文「迷っている方も、まずは気軽に声をかけてください。」に到達し、読み終える頃には、2分31秒が経っています。 目で見ると、装飾はメッセージの「気合い」や「テンション」として届きます。タイトルを目立たせ、絵文字で季節感を添える——投稿者の意図はきちんと伝わる、工夫された書き方です。一方、耳で聴く場面では同じ工夫が、本文の前に積み重なる、情報として届かない音として受け取られます。SlackでもMicrosoft Teamsでも、メッセージに絵文字や装飾が入っていれば、同じことが起きます。 情報の中身は変わらないのに、辿り着くまでの時間が変わる。 絵文字だけではありません。SlackやTeamsにXの投稿を含むなんらかのURLだけが投稿された場合、アプリがタイトル情報を取得できないと、NVDAは忠実にそのURLの文字列を読んでいました。画面にはリンク先の情報のプレビューが表示されているのにです。 「目で見る工夫」が「耳で聴く情報」になる場面では、届き方が変わる——それが、日常のチャットのなかで静かに起きていることです。 Slackは仕事のやり取りの大半が流れる場所です。そこで情報に辿り着くたびに時間がかかる——それが、日常の中で静かに積み重なっていました。 2. 1分29秒で届くようになった その違和感を解消するために作ったのが、NVDAアドオンの Message Sweeper です。 これまで私たちユーザーができることといえば、「スクリーンリーダーで読みづらいので、絵文字や記号の使用を控えてください」と投稿者にお願いすることでした。投稿者の表現の選択を制限する依頼です。Message Sweeper は、その構図を変えようとしています。投稿者が込めた「気づいてほしい」という意図はそのままに、スクリーンリーダーユーザーには読みやすい形で届ける——双方の意図が成立する場所を目指しました。 本音を言えば、読み上げのためにテキストを加工することは、あまり好きではありません。スクリーンリーダーは画面の情報をそのまま届けるために設計された技術であり、本来は発信側やアプリの側が変わることで解決されるべきものだと思っているからです。 それでも、仕事の現場には、その改善を待つ余裕がないほどの情報の波があります。できるだけ時間をかけずに処理しなければならない現実がある。Message Sweeper はその現実への応答です。だからこそ、発信する側と受け取る側、双方の歩み寄りがこれからも大切だと思っています。 同じメッセージを通すとこう届きます。 【募集】社内勉強会の登壇者を募集します 来月の社内勉強会で話してくれる方を募集します。テーマは自由です。15 分の枠を 3 つ用意しています。 こんな発表をお待ちしています (……中略……) メッセージの絵文字: クラッカー16こ、ピカピカ14こ、マイク14こ 情報を削るのではなく、再配置する。絵文字で気合いを入れてくれた投稿者のテンションは末尾のサマリーとしてちゃんと届き、本文は最初から順番に耳に入ってきます。2分31秒かかっていた読み上げが、1分29秒で届くようになりました。 読み上げが実際にどう変わるのかは、4分ほどの動画にもまとめています。読み上げの音そのものが本編なので、音を出して聴いてみてください。 https://youtu.be/1oQD78-KO7Q 動画は、公開版のMessage Sweeper 1.0.0で、冒頭に挙げたのと同じ架空の投稿を題材に撮影したものです。前半が処理をオフにしたときの読み上げで2分31秒、後半がオンにしたときの読み上げで1分29秒です。読み上げの速度も設定も、前半と後半で変えていません。 URLも同じです。Message Sweeper はリンクのURLをページタイトルに差し替えます。「エイチ ティー ティー ピー エス コロン スラッシュ スラッシュ...」と読まれていたものが、リンク先の内容として届くようになりました。URLだけが読まれていた頃は、ページを開いてみるまで、その情報が自分に関係あるかどうかさえわかりませんでした。ページタイトルが届くようになってから、今読むべきか、あとでいいか、読み飛ばしてもいいか——リンクを開く前に自分で判断できるようになりました。 変化は Slack だけにとどまりません。Teams を使う場面は主に音声会議で、会議中に参加者がチャットへリンクを貼ることがあります。話を耳で追いながら、URL文字列の読み上げが割り込んでくる——その状況は、Slack 以上に切実でした。Teams でも同じように整えて読み上げるようになって、使う道具が増えるたびに我慢が増えるのではなく、どのチャットでも同じ手触りで情報を受け取れる——その感覚が、思いのほか仕事のテンポを変えました。メッセージを開くたびに「今日はどれだけ長い読み上げが来るだろう」と身構えることがなくなり、流れてくる情報にそのまま乗れるようになりました。 NVDA+V で読み上げ処理のON/OFFをサッと切り替えたり、加工済みのテキストを点字ディスプレイにも同じ形で表示したりといった細部も、日常の中で少しずつ積み上げてきたものです。この機能群は、AIコーディング支援ツール Claude Code との共同作業で組み上がってきました。 3. 一人で開発していたら、仲間ができた 開発を進める中で、X(旧Twitter)のURLだけがどうしても取得できずにいました。Slack や Teams のメッセージやニュースサイトの情報は処理できるようになっていたのに、X のポストへのリンクが貼られると、中身が届かないまま読み上げられてしまう。そこで詰まっているとき、同僚の大園さんが「それなら手伝えるかもしれませんよ」と声をかけてくれました。 数日後、X のポスト取得を担当する PR が上がってきました。 竹中さんからはあるとき、冒頭に挙げたのと同じような装飾たっぷりのアドベントカレンダー募集の投稿について、こんなメッセージが届きました。 「アドベントカレンダーの募集メッセージ、普通に読み上げられると思ったら最悪の投稿ですね、いつも気付きをありがとうございます」 Message Sweeper がその投稿に対応できたとき、竹中さんはこう書き添えてくれました。 「いろんな人が全方位的に対応できる術を身につけて、誰かに負担を押し付けないのが大事だと実感させられました」 どちらも、こちらから求めたのではありませんでした。私が夢中で開発しているのを見て、興味を持ってくれたのです。 4. 職場で小さな循環が回り始めた 視覚障害のある知人がMessage Sweeper を使ってみてくれたとき、こんな言葉をもらいました。 「おかげで Slack を見てみる気になった。これまでは基本的に見ていなかった」 それだけで、作った甲斐があったと思いました。 その一連の流れを振り返ると、何かひとつの動きが見えてきます。当事者が作り、仲間が育て、別の誰かが使い始める。 この経験を、「配慮を求めない配慮」と呼びたい気持ちがあります。制度や義務として外から持ち込んだのではなく、日常の仕事の中で自然に育っていった——そういう感覚が残っています。その感覚が、私がこの記事を書きたかった理由でもあります。 その循環が、もっと広い場所でも起きてほしいと思っています。 Message Sweeper は、誰でもインストールして使えるかたちで公開しました。ソースコードも公開しているので、自分のメッセージに対して何をしているのかを確かめてから使えます。 リポジトリ: https://github.com/kinto-technologies/message-sweeper アドオンのダウンロード: https://github.com/kinto-technologies/message-sweeper/releases/latest インストールの手順、設定できる項目、動作を確認した環境は、リポジトリのREADMEにまとめています。不具合の報告や要望は、リポジトリのIssueで受け付けています。もしチャットツールの読み上げで「これは届きにくいな」と感じた体験があれば、ぜひ教えてください。
はじめに こんにちは、サイバーセキュリティと生成AI活用推進を担当しているたなちゅーです。 KINTOテクノロジーズ(以下、KTC)では、 以前の記事 で紹介した全社横断プロジェクト「AI-Native Dev」を中心に、AIネイティブな開発・業務プロセスへの変革を進めています。いまでは職種を問わず多くのメンバーが、日常業務のさまざまな場面でClaudeを利用しています。そうした環境の中で「パワーポイント作成もAI-Nativeにしたい」と考えるメンバーが現れ、それぞれがClaudeで利用しやすいパワポテンプレを自作するようになりました。こうした自作テンプレが社内で活発に使われている様子を目にしたことが、今回の取り組みを始める大きなきっかけです。 一方で、この動きには課題もありました。 自作テンプレは 個人ベース・非公式 にとどまり、社内に複数の様式が並行して存在している AI経由で生成したスライドの 見た目がバラバラで、会社の資料としての統一感が出ない そこで、これらの個人の創意工夫を束ねて、 「AIが利用しやすい会社公式パワポテンプレ」として一本化・全社展開する プロジェクトを立ち上げました。この記事では、その取り組みを紹介します。 取り組みの概要 目指した状態 プロジェクトのゴールはシンプルで、 「Claudeに指示するだけで、KTC公式デザインに揃ったスライドが出てくる」状態を全社員に提供する ことです。これにより、次の3つの価値を狙いました。 狙い 内容 時間短縮 資料作成をそのままAIへ渡せるようになり、作成時間を短縮できる デザインの統一 AI経由でも、見た目の揃ったKTCらしい資料になる 個人の取り組みの組織化 AI-Nativeな個人の創意工夫を、組織としての成果に引き上げる 3つ目が本プロジェクトの特徴的なポイントです。単にテンプレを作るのではなく、 すでに個人が生み出していたAI-Nativeな工夫を「会社公式」の標準にする ことを重視しました。AI-Native Devが文化醸成の軸として掲げてきた「個人の知見を組織全体で活かす」を、パワポテンプレという誰にとっても身近な題材で実践した形です。 体制と進め方 推進はAI-Native Devが担い、テンプレートのデザインはクリエイティブグループが担当する協業体制で進めました。今後は技術広報グループとも連携しながら、利用促進に取り組んでいく予定です。 全体像は次のとおりです。 進め方として特に大事にしたのは、次の合意です。 「AIで再現しやすいフォーマット / スタイルガイドはどうあるべきか」を AI利用者側から逆算してデザイナーへインプット し、デザイナーがそれを受けてフォーマットを設計する。 デザインが完成してからAIに対応させるのではなく、設計の段階から「AIが出力しやすい構造」を要件に含めています。この取り組みを「AI-Native」と呼んでいるのは、こうした進め方によるものです。 フェーズは次のように区切りました。 フェーズ 内容 Phase 0 パワポテンプレへの期待値整理 Phase 1 たたき台テンプレの社内展開 & フィードバック収集 Phase 2 公式テンプレの作成と、AIが利用しやすい形(コンポーネント化・スキル化)への変換 Phase 3 全社アナウンス Phase 4 利用者ヒアリングと継続的なメンテナンス いきなり完成品を作るのではなく、まず有志の自作テンプレをたたき台として展開してフィードバックを集め(公開後50名以上が閲覧・ダウンロード)、その声をもとに公式版を作る、という2段階を踏んでいます。 AIが利用しやすいテンプレートの設計 完成したテンプレートには、AIとの相性を高めるための工夫がいくつも入っています。 ヘッダー+ボディのコンポーネント構造 スライドを「ヘッダー+ボディ(シングル / スプリット / マルチカラム)」というコンポーネントの組み合わせとして構造化しました。AIは自由なレイアウトを毎回ゼロから作るよりも、 決められた部品の組み合わせを選ぶ方が安定して高品質な出力を返せる ためです。 レイアウト選択ルールのスキル化 「どんな内容のときに、どのレイアウトを選ぶべきか」というルールをClaudeのSkill(スキル)として整備しました。これにより、毎回テンプレファイルを渡さなくても、Claudeが自律的に適切なレイアウトを選択してスライドを組み立てられます。 使い方は3通り 展開にあたっては、作業スタイルに合わせて3通りの使い方を用意しました。 利用方法 向いている人・場面 方法1:テンプレPPTXをClaudeへ渡す まず試したい人。PowerPointでの手動編集も併用したい人 方法2:Skillを使う 資料作成の頻度が高い人。毎回ファイルを用意する手間をなくしたい人 方法3:Claude Designを使う 見た目を対話しながら細かく詰めたい人。1枚ものやビジュアル重視の資料 方法1は、Claude Coworkやチャットにテンプレファイルとアウトラインを渡して「このテンプレを踏襲して」と依頼する、いちばん手軽な入口です。 方法2のSkillにはテンプレのデザイン情報が組み込まれているため、社内のプラグインマーケットプレイスから一度インストールすれば、ファイルを用意しなくても依頼するだけで公式デザインの資料が生成されます。 方法3のClaude Design向けには、配色・フォント・レイアウトを定義したデザインシステム(ダーク・ライトの2トーン)を用意しました。pptxファイルとして配布・編集したい資料は方法1・2、見た目の作り込みを優先したい資料は方法3、という使い分けです。 「迷ったら方法1から」という導線にして、AIでの資料作成に馴染みのない社員でも一歩目を踏み出しやすくしています。 おすすめの進め方は「先にアウトラインを固める」 どの方法でも共通して案内しているのが、 いきなりpptxを作らせず、先にClaudeと壁打ちしてアウトラインをMarkdownで固める 進め方です。生成されたpptxに「2枚目を直して」「構成を変えて」と修正を繰り返すと、そのたびにファイルの再生成が走り、時間もトークンも大きく消費します。内容の試行錯誤はMarkdown上で済ませ、pptxの生成は最後の1回だけにすることで、トークン消費を大きく抑えられます。 また、生成された資料の数値・固有名詞・事実関係は必ず人の目で確認する、という運用もあわせて案内しています。テンプレ自体も作って終わりではなく、利用者へのヒアリングやフィードバックフォームを通じて、レイアウトの追加なども含めて継続的にブラッシュアップしていく予定です。 おわりに 「AIに指示するだけで、会社公式デザインのスライドが出てくる」のは、使ってみるとシンプルに便利です。そして、それを実現する鍵は高度なAI技術ではなく、 AIが扱いやすい形にテンプレートという「型」を設計し直すこと 、そして その型をデザイナーとAI利用者が一緒に作ること でした。デザインルールをどこまで厳密に縛り、どこを緩めるかの塩梅も、この過程で見えてきた収穫です。 個人の創意工夫として始まったAI-Nativeな動きを、組織の公式な成果に引き上げる。このモデルはパワポテンプレに限らず、さまざまな社内資産に適用できるはずです。この記事が、同じような取り組みを検討している方の参考になれば嬉しいです。 AI-Native Devの活動やナレッジは、引き続きテックブログで発信していきます。最後まで読んでいただきありがとうございました!
この記事でわかること 2026年7月31日、KINTOテクノロジーズ(以下、KTC)のOsaka Tech Lab JCTを会場として、Anthropic公認のコミュニティイベント「Claude Managed Agents Workshop」が開催され、約30名が参加しました。主催はClaude Community Ambassadorであり、KTC社員でもある森田さんです。 Claude Managed Agentsは、エージェントを動かすための土台(実行制御・サンドボックス・履歴やファイルの保持)をAnthropic側が用意してくれるサービスです。利用者はエージェントの設定とメッセージの送信に集中できます。 ワークショップの教材は誰でも参照できる形で公開されています。イベントに参加していなくても、同じ手順でエージェントを作って動かせます。 ハンズオンで唯一つまずいたのはClaude側ではなく、外部サービスであるJina AIのAPIキーでした。キーに無料トークンが残っているとは限らないので、教材を試す前に残トークンを確認しておくと足を止めずに進められます。 はじめに こんにちは、KINTO開発部でフロントエンドエンジニアをしている 齋藤 です。普段はKINTOのシミュレーションや申し込み、マイページの開発を担当しています。 2026年7月31日(金)、KTCのOsaka Tech Labを会場として、 Claude Community Event「Claude Managed Agents Workshop」 が開催されました。私は会場提供・運営サポートを担当しつつ、参加者としてハンズオンにも取り組みました。 本記事は、その開催報告です。 ![KINTOテクノロジーズOsaka Tech Labのエントランスと、Claude Managed Agents Workshop Osakaの案内ボード](/assets/blog/authors/dowod/2026-08-06/cover.jpg =600x) 当日のエントランスの様子 開催したイベントの概要 項目 内容 イベント名 Osaka | Claude Managed Agents Workshop (Claude Community Event) 開催日 2026年7月31日(金)18:30〜21:05 会場 KINTOテクノロジーズ Osaka Tech Lab JCT(大阪市北区梅田3-1-3 ノースゲートビルディング20階) 主催 森田さん(Claude Community Ambassador) 会場提供 KINTOテクノロジーズ 参加者 約30名 タイムテーブルは次のとおりです。 時間 内容 18:30〜19:00 受付・交流 19:00〜19:10 オープニング・会場説明 19:10〜19:30 Claude Managed Agentsの解説 19:30〜20:45 ハンズオン(個人ペース) 20:45〜21:00 ネットワーキング 21:00〜21:05 クロージング 会場提供の経緯 主催の 森田さん は Anthropicから認定されたClaude Community Ambassador であり、同時にKTCのPrincipal Generative AI Engineerでもあります。社内向けのAIエージェント構築ワークショップを開催した話は、 こちらの記事 で紹介されています。 今回のイベントは、その森田さんが場所貸しイベントとして社内で起案し、運営協力者の募集がかかったところに私が手を挙げた、という流れで実現しました。 会場:Osaka Tech Lab JCT Osaka Tech Labは、東京・名古屋に次ぐKTCの第3の拠点です。2022年に心斎橋で開設され、2025年夏にJR大阪駅直結のノースゲートビルディングへ移転しました。「集GO!発SHIN!CO-LAB」というコンセプトを掲げていて、会議室にGARAGEやPIT、モータープールといった車にちなんだ名前が付いていたり、執務室の床がレーシングラインを模した遊び心のあるデザインになっていたりします。 今回使用した Osaka Tech Lab JCT は、その20階にあるイベントスペースです。40名ほどを収容できる広さで、今回の参加者は約30名でした。JR大阪駅直結という立地なので、金曜夜のイベントでもアクセスしやすい会場です。 アクセス方法は KINTOテクノロジーズOsaka Tech Labにご来場のかたへ で写真つきで案内しています。 当日の様子 当日は、Anthropicでコミュニティ活動を担当されている 辻さん にもご来場いただき、会の冒頭にご挨拶をいただきました。大阪でのアンバサダー主催イベントは今回が初めてであること、今後も関西でコミュニティを盛り上げていきたいというお話をされていました。 続く30分は、森田さんによるClaude Managed Agentsの解説パートです。 ![スクリーンにワークショップ教材を投影して解説する様子](/assets/blog/authors/dowod/2026-08-06/session.jpg =600x) 解説パートの様子。スクリーンに投影されているのが当日使用した教材です その後はハンズオンに移ります。各自のペースで進める形式だったので、全員が黙々とキーボードを叩いている時間と、手が止まった人が周りに聞いて質問が飛び交う時間が、交互に訪れていました。 各テーブルにはお菓子が用意されていて、つまみながらハンズオンを進められました。 ![参加者がそれぞれのノートPCでハンズオンに取り組んでいる](/assets/blog/authors/dowod/2026-08-06/handson-overview.jpg =600x) ハンズオン中の会場。各自のペースで進めます もくもくと作業が進むなか、森田さんが会場を回って、困っている人がいないか声をかけてサポートに入る場面もありました。 手を挙げれば主催者が直接見に来てくれる という体制だったので、普段Claude Codeを使っていない人でも安心して参加できるイベントだったと思います。 ![参加者のノートPCをのぞき込んでサポートしている様子](/assets/blog/authors/dowod/2026-08-06/support.jpg =600x) サポートに入る森田さん 参加者の年齢層は幅広く、学生の方も参加されていました。 ![別アングルから見た会場の様子](/assets/blog/authors/dowod/2026-08-06/handson-wide.jpg =600x) Osaka Tech Lab JCTは40名ほどを収容できます 参加者には、Claude Managed Agentsを試すための$50相当のAPIクレジットが配布されました。 さらに辻さんからは、Anthropic公式のノベルティも提供していただきました。 ![オレンジ色のぬいぐるみがテーブルに並べられている](/assets/blog/authors/dowod/2026-08-06/novelty-plush.jpg =600x) Claude Codeのマスコットキャラクター「Clawd」のぬいぐるみ。サングラス姿と、白衣にフラスコを持った科学者姿の2種類がありました ![箱に入ったM5Stack Cardputer ADV](/assets/blog/authors/dowod/2026-08-06/novelty-cardputer.jpg =600x) M5Stack Cardputer ADV 数に限りのあるノベルティは希望者が用意された数を上回ったため、 じゃんけん大会 でもらえる人を決めました。会場が一番盛り上がった瞬間だったかもしれません。 ![参加者が一斉に手を挙げてじゃんけんをしている](/assets/blog/authors/dowod/2026-08-06/janken.jpg =600x) じゃんけん大会の様子 ![参加者全員での集合写真](/assets/blog/authors/dowod/2026-08-06/group-photo.jpg =600x) 最後に参加者の皆さんと。手にしているのがClawdのぬいぐるみです Claude Managed Agentsとは ここで、当日のテーマだったClaude Managed Agentsについて、私が理解した範囲で整理しておきます。 AIエージェントを自分でゼロから作ろうとすると、アプリケーション側で用意しなければならないものが意外と多くあります。モデルを呼んでツールを実行し、その結果をまたモデルに渡す、という繰り返しの制御。ツールを安全に動かすための実行環境。途中で生成されたファイルや会話履歴の保持。Claude Managed Agentsは、この土台をAnthropic側がまるごと用意してくれるサービスです。利用者が用意するのは「どんなエージェントにするか」という設定と、「何をしてほしいか」というメッセージだけになります。 Messages APIとの棲み分けは、公式ドキュメントでは きめ細かい制御が欲しいならMessages API、数分から数時間かかる長時間実行タスクや非同期処理ならClaude Managed Agents という整理になっています。 登場する概念は4つです。 概念 何を指すか エージェント 使うモデル、システムプロンプト、ツール、MCPサーバー、スキルをまとめた設定。一度作ればIDでセッションから参照できる 環境 エージェントを走らせる場所。Anthropic側のクラウドサンドボックスと、自分たちで管理するインフラ上で動かすセルフホスト型が選べる セッション 環境の中で実際に動いているエージェントの実体。タスクを実行して成果物を残す イベント アプリケーションとエージェントのあいだで交換されるメッセージ。ユーザーの指示、ツールの実行結果、ステータス更新などが該当し、レスポンスはSSEでストリーミングされる ハンズオンも「エージェントを作る → 環境を作る → セッションを起動する → メッセージを送る」という順番をそのままたどる構成になっていたので、この4つの関係は手を動かしているうちに自然と頭に入りました。 サンドボックスの中では、bash、ファイルの読み書き・編集、glob・grep、Web検索・取得といったツールが最初から使えます。ここにMCPサーバーを繋げば外部サービスのツールも呼び出せる、という組み立てです。 料金は次の2軸です。 セッションのなかでモデルが使ったトークンぶんの、通常のAPI料金(プロンプトキャッシングの乗数も同じように適用されます) セッションランタイム(セッション時間あたり$0.08) セッションランタイムはミリ秒単位で計測され、セッションが running のあいだだけ加算されます。次のメッセージを待っている idle の時間は加算されません。裏を返せば、 エージェントを長時間放置しても待機中のコストは増えない ということで、これは長時間タスク向けという位置づけと整合しています。なお、セッション内でWeb検索が走った場合は1,000検索あたり$10が別途かかります。 本記事執筆時点でClaude Managed Agentsはベータ版です。詳細は 公式ドキュメント と 料金ページ を参照してください。 ハンズオンをやってみた 私は受付を担当していたため、ハンズオンの開始が少し遅れました。さらにイベントの様子を撮影しながら進めていたので、上級には届かず 中級まで の到達です。 教材は初級・中級・上級の3段階構成になっていて、内容は次のようなものでした。 初級 :テンプレート「Deep researcher」からリサーチエージェントを作る、チャットで設定を自動生成してMicrosoft LearnのMCPサーバーを使うクイズエージェントを作る、自由演習 中級 :Jina AIのMCPサーバーにつながるWebリサーチエージェントを、 コンソール・CLI( ant )・Python SDK の3通り で作る 上級 :分析レポート(スライド生成)、スケジュール実行でGitHub Pagesを毎朝更新、Gmail連携、記憶するエージェント、マルチエージェント 同じエージェントをコンソール・CLI・SDKの3通りで作らせる中級の設計が特に良くできていて、画面操作で理解した概念がそのままAPIのリソース構造に対応していることが体感できました。 そして特筆すべきは 教材の完成度 です。説明が丁寧で画像も豊富に用意されていて、公式が提供した教材と言われても遜色ないなと感じる内容でした。おかげでClaude Managed Agentsの部分ではまったくつまずくことなく進められました。 つまずいたのはJina AIのAPIキー 強いて挙げるなら、中級で使う Jina AIのAPIキー が唯一の詰まりどころでした。私のAPIキーには無料トークンの残りがなく、Jina AIのツールを呼び出せませんでした。 中級で作るのはWeb検索するリサーチエージェントで、検索は Jina AI の MCPサーバー 経由で実行します。この検索系のツールはAPIキーが必須です。APIキー自体はアカウント作成もクレジットカード登録もなしにトップページから取得できるのですが、 そのキーに無料トークンが残っているとは限りません 。 :::message これから教材を試す方は、先に jina.ai の「API KEY & BILLING」タブでAPIキーの残トークンを確認しておくと、中級で足を止めずに進められます。 ::: イベントに参加していなくても教材を試せます このワークショップの教材は、森田さんが作成したものが公開されています。 Claude Managed Agents Workshop(教材サイト) イベントに参加していなくても、事前準備から上級まで同じ手順をたどれます。Claude Managed Agentsを触ってみたい方には、公式ドキュメントを読み込むより先にこちらを一周するのがおすすめです。 :::message この教材はAnthropicの公式コンテンツではなく、非公式の学習教材です。ライセンスがCC BY-NC-ND 4.0のため、本記事では中身の転載はせず、リンクの紹介にとどめています。 ::: 8月にも同じ内容のワークショップが開催されます 今回のイベントは申込がキャンセル待ちまで伸び、参加できなかった方が多く出ました。そうした方に向けて、 同じ内容のワークショップが8月にも開催されます 。 項目 内容 イベント名 Osaka | Claude Workshop 開催日 2026年8月18日(火) 会場 クラスメソッド株式会社 大阪オフィス(大阪市中央区伏見町3-6-3 三菱UFJ信託銀行大阪ビル8階) 主催 森田さん(Claude Community Events) 解説のあとハンズオンという流れは7月31日と同じ構成です。申込には承認が必要なので、詳細は イベントページ を確認してください。 おわりに Claude Managed Agentsは、エージェントの設定とメッセージの送信だけで自律エージェントを動かせるプラットフォームです。教材が公開されているので、興味を持たれた方はぜひ手を動かしてみてください。 Osaka Tech Labでは今後もイベント開催を予定しています。 KINTOテクノロジーズのconnpass もあわせてチェックしていただけると嬉しいです。 参考リンク Claude Managed Agents Workshop(当日使用した教材) Claude Managed Agentsの概要(公式ドキュメント) Jina AI 公式MCPサーバー 社内でAIエージェント構築ワークショップを開催しました。(森田さんの記事) KINTOテクノロジーズOsaka Tech Labにご来場のかたへ
はじめに こんにちは、2026年5月入社のyaMayuです! 本記事では、2026年5月に入社したメンバー2名に入社直後の感想をお伺いし、まとめました。 KINTOテクノロジーズ(以下、KTC)に興味のある方、そして、今回参加してくださったメンバーへの振り返りとして有益なコンテンツになればいいなと思います! Y.Q 自己紹介 グループコアシステム部 ビジネスディベロップメントGに配属されました。 自動車部品メーカー→ITコンサル→当社 といったキャリアを歩んでいます。 所属チームの体制は? グループコアシステム部の人数が多いですが、ビジネスディベロップメントGは割と少数精鋭です。(現在6人) 同じグループのメンバーは出身地が多様で(欧州、北米、アジアetc.)グループ内の公用語は英語です。 KTCの入社動機や入社前後のギャップは? トヨタグループとしての安定感と、若い会社らしいベンチャーカルチャーの両方に魅力を感じたこと、またモビリティサービス領域でのビジネスディベロップメントに携わりたいと考えたことが入社の動機です。 今のところ、大きなギャップは感じていません。 現場の雰囲気はどんな感じ? 真面目な雰囲気とフラットな雰囲気の、良いバランスが取れていると感じます。 オフィスで気に入っているところ Water Serverがある。 周りにランチできるお店が多い。(立地) yaMayuさんからの 質問:旅行が趣味とのことなので、おすすめの場所やまた行きたい場所があればお伺いしたいです!(日本国内でも、海外でもどちらでもOKです!) 小笠原諸島です!片道1日ほどかかる長い船旅を経て訪れる分、非日常な体験が待っていました(ウミガメの産卵ツアーもおすすめです!)。太平洋に浮かぶ夜の星空や朝日にも感動し、また訪れたいと思っています。 yaMayu 自己紹介 プラットフォーム開発部 Quality Engineering GでQAエンジニアをやっています。 異業種からSESに転職し、第三者検証を経てKTCに入社しました。 京都市出身で今は都内在住、室町オフィス勤務です。 趣味と呼べるものはあまりないですが、移動中はよくオーディオブックを聴いています。読書も好きです。ミステリー多めです。たまに映画を観に行ったりします。 所属チームの体制 Web QA, アプリQA, SETチーム合わせて21名です。 6月にSETチームが新設され、大幅に人が増えました。 QA, SETチームとも室町オフィスとOsaka Tech Labにメンバーがいます。 私はWeb QAチームに所属しています。 現場の雰囲気 大阪のメンバーとは拠点が離れていますが、Slackやzoomで随時連絡や相談をしています。 オフィスに出社しているときは、一緒にランチ行くこともあり和やかな雰囲気だと思います。 KTCへの入社動機や入社前後のギャップ 前職、前々職ではお客様先のプロダクトに携わっていたので、自社プロダクトのQAに携わりたいと思い、転職しました。 プロダクトのジャンルはあまり絞っていなかったのですが、サブスクサービスはいろんなジャンルでどんどん広まっているので、今後も需要がありそう = 多くの人に使ってもらえるプロダクトに携われそう、と思い入社しました。 カジュアル面談~面接~オファー面談でいろいろお話を聞くことができていたので、入社後の大きなギャップは今のところないです。 オフィスで気に入っているところ 地下鉄の駅から直結なので、雨でも濡れない&日差しも怖くないところ。 周辺においしいお店が多いところ。(お値段は安くはないですが…。) フリードリンクでコーヒーが飲めるところ。 Y.Qさんからの質問:京都出身者として、観光客があまり知らないおススメの場所や楽しみ方をご教示ください! 最近はネットやSNSですぐ広まってしまうので難しいですね…笑 志津屋のカルネ:もうすっかり有名になってしまいましたが、昔から好きで、今も実家に帰ると必ず食べます! 法輪寺 電電宮:電気・電波の神様を祀る神社です。(日本唯一らしいです!)IT関連の人にも人気があります。嵐山にありますが、渡月橋から少し離れているので比較的空いていると思います。 明智越:明智光秀が愛宕神社に参詣する際に通った道で、本能寺を攻める際にも通ったと言われています。今はハイキングコースになっていて、歴史と自然を感じられます。(台風や豪雨の影響で道が荒れている場合もあるようなので、訪れる際はご注意を。) さいごに みなさま、入社後の感想を教えてくださり、ありがとうございました! KINTOテクノロジーズでは日々、新たなメンバーが増えています! 今後もいろんな部署のいろんな方々の入社エントリが増えていきますので、楽しみにしていただけましたら幸いです。 そして、KINTOテクノロジーズでは、まだまださまざまな部署・職種で一緒に働ける仲間を募集しています! 詳しくは こちら からご確認ください!
はじめに こんにちは、2026年4月入社の山本です! 本記事では、2026年4月入社メンバーに入社直後の感想をお伺いし、まとめてみました。 前の方からの質問に次の方が答えるリレー形式でお届けします。 KINTO テクノロジーズ(以下、KTC)に興味のある方、そして、今回参加下さったメンバーへの振り返りとして有益なコンテンツになればいいなと思います! もっさん ![もっさんのプロフィール画像](/assets/blog/authors/yuta.yamamoto/mossan.png =300x) 自己紹介 KINTO開発部 開発推進Gに所属しています。 案件全体の管理や、AIを活用したサイト構築のプロジェクトを担当しています。 スポーツ観戦が趣味で、最近は地元のBリーグクラブを応援しています。せっかくなのでKINTOと縁のあるアルバルク東京もこれから推していきたいです! 所属チームの体制は? 開発推進Gは8名体制で、KINTOサービスに関わる開発案件のプロジェクトマネジメントを主に行っています。 職種はPMが中心で、事業部側のメンバーや開発メンバーと連携しながら、ものづくりを進めています。 KTCへ入社したときの第一印象は?ギャップはあった? 前職の同僚が在籍していたこともあり、入社前から気軽に質問でき、カジュアル面談でも疑問をその場で解消できたので、不安なく入社できました。 入社後はフットサルや筋トレなど、いろいろな活動を楽しんでいるアクティブな方が多く、いい意味でのギャップを感じました。 エンジニアの勉強会など学びや発信の場が多いのも印象的で、自分の経験がない分野でも興味があれば気軽に参加させてもらっています。 現場の雰囲気はどんな感じ? 休憩室では豆を挽いてコーヒーを淹れている方がいて、香りにつられてふらふらと混ざりに行きました。とてもウェルカムな雰囲気で、業務でまだ関わりのない方ともゆるく繋がれるのがありがたいです。 全員が中途採用ということもあり各分野のプロフェッショナルが揃っていて、日々の会話から多くの刺激をもらっています。 オフィスで気に入っているところ 室町オフィス勤務です。 オフィスから日本橋をわたって八重洲・銀座あたりを歩くのがお気に入りで、街のにぎわいを感じながらの散歩がいい気分転換になっています。 寿司と紅茶さん ⇒ もっさんへの質問 生まれ変わったらなりたい職業とその理由を教えてください 料理人やパティシエですね。今はチームでものづくりをしていますが、生まれ変わったら一人で黙々と何かを突き詰めてみたいです! つじたく ![つじたくさんのプロフィール画像](/assets/blog/authors/yuta.yamamoto/tsujitaku.jpeg =300x) 自己紹介 新サービス開発部 FACTORY EC開発G所属です。 趣味は筋トレです。体を大きくしたいです。 所属チームの体制は? 新サービス開発部 FACTORY EC開発Gで、TOYOTA/LEXUS UPGRADE FACTORYのEC基盤を開発・運用しています。 フロントエンド、バックエンド、PdM、SRE、QA、ディレクター、マネージャーなど合わせて15名ほどの体制です。 KTCへ入社したときの第一印象は?ギャップはあった? 入社前の面接の時も色々話をしていたので大きなギャップはなかったです! 現場の雰囲気はどんな感じ? フロントエンド、バックエンド、PdMなど各方面のメンバーが揃っているので提案→実行までが素早くできて働きやすいです。 オフィスで気に入っているところ 駅から近く、改札出てから5分もたたずに自席に座れるところ。 オフィスが綺麗なところ。 もっさん ⇒ つじたくさんへの質問 TOYOTA UPGRADE FACTORY 担当されていますが、こんなアップグレードあったらいいなと思うのはどんなものですか? 自分の運転スタイルを学習して、燃費・快適性のバランスを自動最適化するAIモードみたいなアップグレードがあったらいいなと思います。 山本雄太 ![山本さんのプロフィール画像](/assets/blog/authors/yuta.yamamoto/yuta_yamamoto.png =300x) 自己紹介 新サービス開発部 プロジェクト推進グループ所属の山本雄太です。ロールはPjMです。 仕事は寄り添い伴走するようなスタイルが性に合っており、誰かの為にだとより力が出るタイプ。 趣味はゴルフ・釣り・サウナ・キャンプなどアクティブ系広め、お酒も大好きです。最近はゴルフにハマってます。やっと100を切りはじめました! 所属チームの体制は? プロジェクト推進グループは10名にも満たない小規模体制ですが、トヨタグループ各社に対してKTCの技術力を活かして支援を主導するグループでもあり、やりがいは大きいです。 最先端な取り組みにも関与できるチャンスがあるので、KTCの中でも一番面白いグループなのではとも思っています。 KTCへ入社したときの第一印象は?ギャップはあった? 第一印象は…オフィスがどの拠点も立派!笑 ギャップは想像以上にスキルフルなメンバーが多い点です。 現場の雰囲気はどんな感じ? 少数チームという性質もあり、各自別のプロジェクトで作業しており実質的な関わりは少ないです。 ただ、定例MTGは情報交換が活発に行われており、相互に助言できる程よい雰囲気に感じます。 オフィスで気に入っているところ 私はFukuoka Tech Lab所属で、大名のリッツ・カールトンの真下にオフィスがあり景色が良い!博多湾を一望できて、天気が良い日には志賀島や能古島も綺麗に見える窓際がお気に入り、良いリフレッシュゾーンになっています。 つじたくさん ⇒ 山本さんへの質問 おすすめの日本酒教えてください! 福岡は酒蔵が意外と多くて迷いますが、寒北斗をおすすめします! 北斗七星をモチーフにしたパッケージやお猪口もあったり、見た目から愛せます。 寿司と紅茶 ![寿司と紅茶さんのプロフィール画像](/assets/blog/authors/yuta.yamamoto/sushitokocha.jpeg =300x) 自己紹介 デジタル戦略部データグロースGでマネージャーをしています。 Webエンジニア → EM・PdM → 事業責任者 → VPoE といったキャリアを歩んでいます。 所属チームの体制は? 1チームに機能横断している方々が揃っているのが特徴です。 PdM、エンジニア、デザイナーといったプロダクトトリオが揃っているため、多角的な視点から機能改善や案件進行ができるのが強みです。 KTCへ入社したときの第一印象は?ギャップはあった? リファラルなので事前に質問できていた点とカジュアル面談も複数回実施させていただいたため、ギャップはありませんでした。 現場の雰囲気はどんな感じ? 比較的スタートアップのようにワイワイやっている雰囲気が強いですね。 会社規模で考えると総合的に珍しいかもしれません。 オフィスで気に入っているところ フリードリンクが用意されているところ、宅配弁当が頼めるところですね。 山本さん ⇒ 寿司と紅茶さんへの質問 これまでで一番歯ごたえのあった仕事はどんな仕事でしたか? ゼロから開発組織を作ることになり、未経験から採用要件、スカウト業務、AG対応、カジュアル面談、面接、オファー対応などすべてやってました。 さいごに みなさま、入社後の感想を教えてくださり、ありがとうございました! KINTOテクノロジーズでは日々、新たなメンバーが増えています! 今後もいろんな部署のいろんな方々の入社エントリが増えていきますので、楽しみにしていただけましたら幸いです。 そして、KINTOテクノロジーズでは、まだまださまざまな部署・職種で一緒に働ける仲間を募集しています! 詳しくは こちら からご確認ください!
こんにちは! Principal Generative AI Engineerの森田です。私の所属するAIファーストGでは、社内の生成AI活用にとどまらず、販売店やトヨタグループにおけるAI活用支援を行っております。 社内でAIエージェントの活用が進む中、ある部署から「AIエージェント開発に取り組みたいが、どこから始めていいかわからない」と相談をもらいました。座学だけでは手触り感が得られないので、実際に手を動かすワークショップ形式で開催することにしました。 弊社はAWSを主なクラウド基盤としているため、AWSが提供するAIエージェントの構築・運用基盤である Amazon Bedrock AgentCore と、エージェントのツール連携プロトコルである MCP(Model Context Protocol) を中心テーマに据えました。 教材として使用した書籍 ゼロから資料を作るよりも、体系的にまとまった書籍のハンズオンをベースにした方が、参加者が後から復習しやすいと考えました。今回は以下の2冊を使用しています。 『 Amazon Bedrock AgentCore 実践入門 ── Strands Agentsで構築するAIエージェント 』 (SBクリエイティブ) ※以下、AgentCore本 『 AWSではじめるMCP実践ガイド ── 基礎からAIエージェント構築まで徹底解説 』 (技術評論社) ※以下、MCP本 AgentCore本でエージェントフレームワークであるStrands AgentsやAgentCoreの各機能を学び、MCP本でMCPサーバーの自作とAgentCore Gatewayによるツール統合を学ぶ、という組み合わせです。 実は、どちらも私が共著者として執筆に携わった書籍です。手前味噌で恐縮なのですが、中身を隅々まで把握できている分、参加者がハンズオンで詰まったときにも補足しやすく、教材として選びました。 開発環境 今回は環境を揃えるため、 GitHub Codespaces 上に開発環境を用意しました。書籍の手順をベースに、以下の工夫で環境構築にかかる時間を短縮しています。 ツール管理は mise に統一し、 mise use aws-cli node uv claude でAWS CLI・Node.js・uv・Claude Codeを一括導入 AWSへは aws configure sso でSSOログイン コーディングのお供として Claude Code をセットアップ サンプルコードは書籍著者が公開しているGitHubリポジトリを git clone して参照できるようにし、すべてのコードを手打ちしなくていいようにしました。 ワークショップの構成 1日のワークショップとして、午前・午後に分けて以下の流れで進めました。 午前の部(1.5時間)・・・Strands Agentsに触れる # タイトル 書籍・該当箇所 概要 1 Strands Agents入門 AgentCore本 3.4節 ツール定義、エージェント作成、会話ループの実装など基本を体験 2 リサーチエージェントの構築 AgentCore本 4章 Webから情報収集・要約するエージェントを構築し、ツール連携の開発フローを掴む 任意 AgentCoreハーネス AgentCore本 5.2節 時間に余裕がある人向けに、AgentCoreハーネスを体験 午後の部(3時間)・・・AgentCoreとMCPを実践する # タイトル 書籍・該当箇所 概要 3 AgentCoreランタイム入門 AgentCore本 5.3節 agentcore create → agentcore deploy でエージェントをクラウドにデプロイ 4 アンビエントエージェントの構築 AgentCore本 15章 S3への領収書アップロードをトリガーに、解析→Confluence記録→メール通知するイベント駆動型エージェントをCDKで構築 5 MCPサーバーの構築 MCP本 4.2節 自前のMCPサーバーとホストを実装 6 AgentCore Gateway MCP本 6.2節 MCPサーバーをAgentCore Gatewayに登録し、認証・認可を統合 社内実施にあたって変更が必要だったポイント 書籍のハンズオンをそのまま社内で実施するにあたり、いくつか環境差異への対応が必要でした。 :::message メールアドレスのエイリアス 15章のアンビエントエージェントでは、サンプルデータ内の複数ユーザーのメールアドレスにそれぞれ通知を送る設計になっています。しかし社内のメール環境ではエイリアスが使えなかったため、 data/users.json のメールアドレスをすべて自分のアドレスに統一しました。その結果、同じアドレスでSNSサブスクリプションが重複してしまうため、 chapter15/cdk/lib/expense-agent-stack.ts で重複を排除するよう修正しました。 + const uniqueEmails = [...new Set(emailAddresses)]; - emailAddresses.forEach((email, i) => { + uniqueEmails.forEach((email, i) => { // 各アドレスごとに SNS サブスクリプションを作成 }); ::: :::message Googleカレンダー連携が使えない環境 13章のフルスタックエージェント構築ハンズオンはGoogleカレンダー連携(Google認証)を前提としているため、社内環境では実施が難しく割愛しました。興味がある方には自宅での実施を案内しています。 ::: :::message AWSアカウントの共有 複数人で1つのAWS環境を使ったため、リソース名が他の人と重複・混同しないよう、各自の名前を付与するルールにしました。特にS3バケット名はアカウント内で一意である必要があり、そのままだと衝突してしまいます。CDKスタックにハードコードされたバケット名( expense-agent-${account} )に自分の名前を付けて回避しました。AgentCoreランタイムも、 agentcore create で付ける名前を handsonmorita のように各自で区別できるものにしています。 ::: :::message alert リージョンの差異 AgentCore本とMCP本で、使用リージョンが異なっているため、単一リージョンで実施するには読み替えが必要でした。GitHubで公開されているサンプルソースを利用して進めていたのですが、どこを修正すればよいかが見落としやすいポイントでした。 ::: 参加者からもらった質問 ワークショップ中、参加者からいくつか印象的な質問がありました。 Q. ツールの定義はPythonでもできるのか? Strands Agentsのツール定義はPythonのデコレータ @tool で行えます。関数のdocstringがそのままツールの説明文(description)としてLLMに渡されるため、Pythonで完結します。MCPサーバーとして公開する場合もPython SDKが利用可能です。 Q. Swarmエージェントはどうやってタスクを振り分けているのか? マルチエージェント構成(Swarm)では、オーケストレーターとなるエージェントがLLMの判断に基づいて、各サブエージェントにタスクをルーティングします。各エージェントのnameやdescriptionに基づき、他のエージェントの役割を把握したうえで、作業を委譲するエージェントを選択します。 Q. マルチエージェントにするか、シングルエージェントにするか、どのように決めるのが良いか? 個人的な感覚としては、最初から厳密に切り分けようとしない方がうまくいきます。シングルエージェントとして作り始めて、複雑なタスクをうまく処理できなくなったり、コンテキストを分けた方が動きが安定すると感じた時点で、サブエージェントへの分割を考える、というのが実際の進め方に近いです。 Q. エージェントで使用するモデルはどのような基準で選んでいるのか? コストを最初から意識しすぎないことが大事だと思っています。Sonnetをベースにまず作ってみて、判断の精度が足りない部分はOpusに切り替え、単純作業の部分だけHaikuでコストを下げる、という順番です。先にHaikuでコストを抑えようとすると、実現できる機能のレベルが下がってしまうので、まずは「エージェントとして実現できるか」を確認してから、コスト最適化に移る方がスムーズに進みます。 参加者の声 ワークショップの締めに参加者で「振り返り会」を行い、そこで出た声の中から印象的だったものをいくつか紹介します。 ある参加者は、「エージェントの動きが、これまでふんわりとマジックのように感じていたのに、こういう仕組みで動いていたのかと腹落ちした」と話してくれました。難しさの本質はプロンプトの作り込みにある、という気づきを得られたようです。 別の参加者は、ハーネスの手軽さに驚いたそうです。テンプレートに「あなたはプロ野球に詳しい人です」のような一行を書くだけでエージェントが動いてしまうのを見て、拍子抜けするほど単純だったと話していました。 MCPサーバーの自作に取り組んだ参加者は、午後の中でも特に難しかったと振り返っていました。それでも自分の手で実装したことで、普段使っているMCPの裏側の動きまで想像できるようになった、という言葉が印象的でした。 これまで固定パイプライン的な実装(情報を取得し、LLMに渡し、整形して出力する)に慣れていたという参加者は、エージェントが入力に応じて処理のルートを動的に決めていく発想自体が新鮮だったと語っていました。同じ入力でも出力のパスが事前に決まっていない、というところに面白さを感じたそうです。 ワークショップを終えて 1日にかなりの内容を詰め込んだこともあり、進み具合には個人差が出ました。午前の任意項目だったAgentCoreハーネス(5.2節)まで到達できた方もいれば、そこまで手が回らなかった方もいます。午後はそれぞれの興味に合わせて、アンビエントエージェントに取り組むグループとMCPサーバー構築に取り組むグループに分かれて進めてもらいました。 今回のワークショップを通して、書籍のハンズオンをベースにしたことで準備の負担を抑えつつ、実践的な内容を届けられたのは良かったです。本記事が、これからAIエージェント開発に取り組む方や、同様のワークショップを企画する方の参考になれば幸いです。最後までお読みいただき、ありがとうございました。
こんにちは! KINTOテクノロジーズ(以下、KTC)のAIファーストグループで、生成AIの活用推進を担当している和田です。 先日、JDLA(日本ディープラーニング協会)の資格合格者コミュニティ「CDLE」の業種別勉強会にお招きいただき、「個人の発見を、組織の知恵に」というテーマで登壇してきました。本記事は、その内容を再構成したものです。 https://jdla.connpass.com/event/393970/ 1. はじめに KTCはトヨタ自動車のグループ会社で、クルマのサブスク「KINTO」をテックの力で支える内製開発の会社です。 2023年の春、GPT-4とAPI版が出てきたタイミングで内製の生成AIチャットを立ち上げて以来、3年以上にわたって生成AIの活用推進を続けてきました。最近ではKTC/KINTOで培った技術力を、トヨタグループの各社へとアドバイジングや開発支援を通じた提供もしています。 その立場から国内の状況を眺めると、対照的な数字があります。生成AIを「導入済み」と回答した国内企業は57.7%^[ NRI「IT活用実態調査(2025年)」 ]。導入の壁は、もうほとんど越えられています。一方で、AIが利益(EBIT)に5%以上効いていて、かつ大きな価値を生んでいると答えられる企業は6%^[ McKinsey "The State of AI in 2025" ]。導入はしたが、成果を出していると言い切れる会社はまだ一握りです。 おそらくこの記事を読んでいるような、新しい技術への感度が高い方は、すでに仕事が大きく変わっているはずです。メールの下書き、議事録の要約、コーディング支援。少なくとも、これらを全てカタカタ手で打っている人は、かなり減っているのではないでしょうか。しかし問題はその先です。あなたの「周りの人」はどうでしょうか。個人としては価値が出ている。でも、組織としてはどうでしょう。今回のテーマは、個人の成果と組織の成果の間にある、この溝についてです。 2. キャズムのどこに手を打つか ― 今日は「B」の話 本題に入る前に、組織へ新しいものを広げるときのイメージを共有させてください。何度も見たであろう、キャズム理論^[ジェフリー・ムーアが提唱した、新技術の普及プロセスを説明する理論。利用者をイノベーター/アーリーアダプター/アーリーマジョリティなどの層に分け、層の間にある溝(キャズム)を越えることの難しさを論じたものです。]の図です。 新しい技術や文化を組織へ広げるときの手法。A・B・Cのどこに手を打つか これは生成AIの話だから持ち出した図ではありません。DXのときも、RPAのときも、新しい技術や文化を大きな組織に入れる場面では、いつもこの概念を使ってきました。 図にはA・B・Cという3つの矢印を描いています。それぞれが別の打ち手です。みなさんの組織は、いまどの矢印に手を打っているでしょうか。そしてご自身はどの層にいて、どの矢印ならコミットできそうでしょうか。そんなことを考えながら見てもらえればと思います。 A:イノベーターに自由を渡す。 新しいものは、放っておいても勝手に調べ、勝手に始め、勝手に実験してしまう人たちがいます。彼らにできる限り自由な環境を渡す。ただ自由なだけでなく、ガードレールを敷いて「ここでなら安心して遊んでいい」という空間にするのがAです。 B:キャズムに橋をかける。 イノベーターやアーリーアダプターが見つけた価値に、アーリーマジョリティ以降の人たちが追従できるよう、崖になっているキャズムへ橋をかける取り組みです。 C:後ろ向きな層を動かす。 配ってもなかなか触ってくれない、興味を持ってもらいにくい層に、どう使ってもらうか。ここに悩んでいる会社さんは、きっと多いはずです。 今日お話しするのは、このうちBが中心です。Bをやるには、その手前でAも回っている必要があるのですが、本記事では「見つかった価値を、キャズムの向こう側へどう渡すか」に軸足を置きます。 3. 価値創出の両輪 ― 探索と実装 組織で価値を出すには、2つの循環が要る、と私は考えています。 1つは探索。イノベーターやアーリーアダプターにあたる人たちが自由に価値を探せる環境を用意し、「この使い方は効く」という発見を生んでもらうフェーズです。もう1つは実装。見つかった0→1の発見を、組織に固定して10にも100にもするフェーズです。藪まみれの中をしらみつぶしに歩いてゴールを見つけるのが探索だとすれば、見つかった道を舗装して誰でも歩けるようにするのが実装です。 探索だけでは、個人の発見で止まります。IRレポートに載るようなインパクトは、個人技からは出ません。逆に、発見のない組織でいきなり実装(仕組み化)から入ると、舗装すべき道がどこにあるのか分からないまま工事が始まります。これらは両輪であってどちらが欠けても前進することはできません。 ここからは、KTCがこの両輪をどう回しているか、探索→実装の順でお話しします。 4. 探索:トークンマキシング ― 自転車の乗り方は、本では学べない 探索の打ち手の一つとして「トークンマキシング(Tokenmaxxing)」^[あるものを極限まで盛るというネットスラング "-maxxing" を、トークン消費にくっつけた言葉です。]を紹介します。社内のAIのトークン消費を、とにかく最大化する。価値創出はいったん脇に置いて、まず使う量を増やす施策群のことです。 なぜ価値創出にこだわると言っておきながら、消費量に注目するのでしょう。生成AIの正しい使い方は、机上で学べないからです。私はよく自転車の乗り方に例えるのですが、自転車の乗り方を本で学んだ人は、おそらくいません。補助輪をつけて、サポーターをつけて、河原で何度も転んで、身体で覚えたはずです。「AIにこう頼むとうまくいく」「これはAIには苦手だ」という感覚も同じで、試行錯誤からしか生まれません。だから、まずたくさん漕いで、たくさん転ぶことで、AIと効率よく協業する感覚が磨かれます。 トークンマキシングを構成する施策は、「消費を増やす施策」と「消費量を観測する施策」で構成されます。 消費を増やす施策の例としては「手作業コーディング禁止」があります。一定期間、人手でのコーディングを禁止して実装はAIエージェントに任せ、人間は指示と検証に集中する、というものです。コーディング界隈で広まった施策ですが、「手作業での資料作成禁止」のように事務系へのアレンジも利きます。 私自身は資料作成へのこだわりが強く、今でもつい手作業で熱中してしまう時があるのですが、最初の1割は人間が作り、そこから7〜8割まではAIに持っていかせて、最後にまた、人間のこだわりを入れるようにしています。何度も失敗しながら最近ようやくちょうど良いAIとの協業感覚を掴めてきています。 KINTOテクノロジーズで実施した手作業コーディングを禁止する施策「Vibe Coding Week」については、Findyさんによるインタビューブログでも取り上げていただきました。 https://jp.findy-team.io/blog/ai-casestudy/kintotechnologies_vibecodingweek/ もう1つの要素が観測です。増やしっぱなしではコストが爆発するので、誰が・どれだけ使っているかを可視化する。この文脈で有名になったのがMetaの「Claudeonomics」で、8.5万人超の従業員がトークン消費量でランク付けされ、上位250名にはRPG風の称号が与えられていたそうです^[ Fortune「A Meta employee created a dashboard so coworkers can compete to be the company's No. 1 AI token user」(2026/04/09) ]。KTCでも、Claude Codeのメトリクスを各ユーザーから収集し、個人と組織それぞれの使い方を分析する仕組みを動かしています。ツールは配ったけれどその後を見ていない、という組織は、まずここから始めるのを勧めます。 KTCで運用しているClaude Codeメトリクスのダッシュボード(数値はダミーデータ) 5. ただし、トークン消費はハック可能 ・・・ただしトークン消費量は、あくまで間接指標です。 たくさん使った≠価値が出た。実際、Metaの番付では、順位のためにAIを空回しして消費量を水増しする従業員が現れたという情報もあります。そりゃそうですよね。指標は必ずハックされます。入力量で成果を測るのは、印刷したページ数で文章の質を測るようなものなので、報酬や人事評価に直接ひもづけるのは慎重であるべきです。トークンマキシングは、短期的に組織のモメンタムを作る旗印としては効きますが、ずっと続けるものではありません。習熟が進んで消費が落ち着いてくるところまでがセットです。 消費量はあくまで間接指標。指標は必ずハックされる そしてもう1つ、この打ち手には賞味期限があります。これまでのコーディングエージェントの多くは月額定額、いわば携帯のパケ放題のような契約でした。「とにかく使え」が安心して言えたのは、この建て付けがあったからです。ところが課金体系は従量制へ動いています。 GitHub Copilotは2026年6月1日から使用量ベースの課金へ移行します し、他のエージェントも続々と後を追っています。 Uberが2026年のAI予算をわずか4か月で使い果たし、コーディングエージェントの利用に従業員一人当たりの月額上限を設けた という報道も出始めました。 従量課金の世界で大事になるのは、トークンマネジメントや最適化、つまり妥当なコストで成果を増やす考え方です。難しいのは、マキシングを経験しないまま従量課金に入ってしまった組織で、転んだことのないまま管理から始めることになります。もしいま手元に使い放題のプランがあるなら、それは最後のモラトリアムかもしれません。プランが生きているうちに、探索をやり切ることをお勧めします。 定額制(パケ放題)から従量課金へ。「とにかく使え」が言えた時代は終わりつつある ここまでが探索の話。次は、見つけた発見をどう組織に固定するかです。 6. 実装:発見をAgent Skillに固める ― 発見した本人に、文書化まで背負わせない どの組織にも、キャズムでいうイノベーターやアーリーアダプターにあたる人たちがいます。新しいものを勝手に調べ、勝手に試し、「この頼み方ならうまくいく」という良い使い方を見つけてくる人たちです。問題は、その発見が本人の中にしかないことです。 そこでKTCで今増えているのが、 Agent Skill です。Agent Skillとは、AIエージェントに特定タスクの「やり方」を教える再利用可能な手順書のことで、いつ何をするかを書いた指示書(SKILL.md)、手順とOK/NGの線引き、テンプレートやスクリプトといった参照ファイルを1つのパッケージにまとめたものです。属人的だったカンコツを取り出して、誰でも再現できる形に固める。暗黙知やワザを、Skillという形で全員に配ることができます。 Agent Skillは指示書・手順/判断基準・参照ファイルの3点セット KTCの例でいうと、トヨタグループには「物と情報の流れ図(物情)」という業務の可視化手法があるのですが、このドラフトをAIに作らせるSkillを固めて、社内に配布しています。先行する人たちの発見を、お湯を注げば誰でも食べられるインスタント食品に加工して配る、というイメージです。 一方でこうした探索の担い手は、新しい使い方を探すこと自体は好きでも、それを手順書に書き起こすことには関心が薄かったりします。であれば、横串の推進組織が本人のところへ出向いて、「文書化はうちらが代わりにやります」と引き受けてしまうのはどうでしょうか。発見した本人に、文書化の手間まで負わせる必要はないかもしれません。 7. 運用と文化 ― 「手でプロンプトを打たない」という逆説 Skillは作っておしまいではなく、運用が必要です。誰でもSkillを探して使える場所(Plugin Market)を作る。命名ルールを決める・・・検索できないSkillは、存在しないのと同じだからです。ブランチ名や関数名に払っている気遣いを思い出してください。あれと同じ気遣いがSkillにも必要です。ただ、Skillの命名規則のベストプラクティスはまだ世の中に整備されていないので、会社ごとに独自で決めてしまうのが有効だと思います。それから、定期的なメンテナンス。数か月で前提が変わる領域なので、古いSkillは放置すれば負債になります。 Skill運用を支える4つの仕組み(Plugin Market・命名ルール・定期メンテ・対象発掘) そして、そのメンテナンスをどう回すか。ここが地味に大変なところなのですが、KTCではSkillの保守そのものをSkillにしてしまうことを試しています。いわば、Skillを点検・整備するためのSkill群です^[公開されているmizchiさんの「 waxa 」を参考にしています。]。役割を分けた、4つのモードがあります。 検証する:書いたばかりのSkillを、まっさらな別セッションに白紙で読ませ、「ここが伝わらない」という曖昧さや暗黙知を炙り出す。 棚卸しする:Skill群ぜんぶを複数の観点で健康診断し、どれから手を入れるべきかのリストを作る。迷ったら、まずここから。 命名を整える:名前とdescriptionだけを命名規則に合わせて直す。何をするSkillか一目で理解でき、検索で見つかる状態を保つための整備になる。 本文を直す:古いSkill名やモデル名、URLといった陳腐化した参照を、機械的に一括置換する。 コツは、各修正のポリシーきっちり分けて、1つのセッションに何もかもやらせないこと。さきほど「検索できないSkillは存在しないのと同じ」と書きましたが、その状態を保つ作業自体を、人間の根性ではなくSkillに肩代わりさせるわけです。運用とは、こういう地味な仕組みの積み重ねなのだと思います。 Skillの保守そのものをSkillにする。役割を分けた4モードで運用を回す(参考: mizchiさんのwaxa) 文化の面では、事例共有会や勉強会といった地道な取り組みを続けてください。「こんな効くSkill作ったぜ」を見せ合う場は、評価制度ではなく、つい誰かに見せたくなる気持ちで回り始めます。地味ですが、文化醸成はこういう積み重ねでしか進まないと思っています。 最後に1つ、逆説的な話を。個人の仕事を組織の仕事にする上で、プロンプトエンジニアリングのスキルが邪魔をすることがあります。個人が勘とコツでプロンプトを丁寧に調整し、エージェントをいい感じに動かすのは、良いようでいて横展開が非常にしにくい。プロンプト頼みの業務は、それ自体が属人化です。なのでKTCでは最近、組織の仕事にする場合は手でプロンプトを打つのをできるだけやめて、スラッシュコマンドやSkillの呼び出しだけで完結させることを推奨しています。上手に打てる人ほど、打たない。妙な話ですが、組織化とはそういうことだと考えています。 なお、ここまでの打ち手は、KTCがクラウド・AI領域で新しいことに踏み込みやすい立場にある、という前提と切り離せません。新しいことを試し、うまくいったものを少しずつ周囲へ広げていく——そんな意識で取り組んでいるので、キャズムでいう上位層に意識的に時間を寄せています。どの層にどれだけ時間をかけるかのポートフォリオは、自社の立ち位置や方針に従って決め、経営層と握っておくのが筋だと思います。 8. まとめ ― 個人の発見を、組織の知恵に 「個人の発見を、組織の知恵に」今回お伝えしたかったのは、結局この一行です。探索のフェーズでは、トークンマキシングでたくさん試して、転ぶことを恐れない。正しい使い方は、試行錯誤からしか生まれないからです。実装のフェーズでは、発見をAgent Skillのような形に固め、誰でも再現できるようにして配る。そして、仕組みと文化で回し続ける。 探索→実装→仕組み・文化。個人の発見を、組織の知恵に G検定やE資格を持っているような方は、すでに一本のスペシャリティがある状態です。AIは自力にレバレッジをかける道具なので、自力が10の人と100の人では、掛けた後の差がまるで違います。ご自身のドメイン知識と、資格を通じて学んだ知識と、生成AIやAIエージェントを掛け算して、まずはたくさん転ぶところから。そして、転んで見つけた発見を、ぜひ組織に配ってください。 ここまで読んでいただき、ありがとうございました! あなたや周囲の人の発見が、組織の知恵になっていくことを願っています。
こんにちは。Engineering Officeのアクセシビリティアドボケート、辻勝利です。 6月のある朝、私は名古屋のあるビルの一室で、植木鉢を両手で抱え、聞こえてくる水の音を頼りに歩いていました。隣では竹中さんが猫の鳴き声を追いかけ、浜谷さんは焼き網の上で肉が焼ける音に、息を詰めて耳をすませています。三人が手にしていたのは、どれも映像のない、「音」だけで遊ぶゲームでした。 傍から見たら、なかなか不思議な大人三人組だったと思います。でもその場にいた私たちは、ただただ夢中でした。目で何かを確かめるのではなく、耳をすませて、音だけを頼りに遊ぶ。その新鮮さに、すっかり夢中になっていたのです。 私たちが訪れていたのは、「オーディオゲームセンター」という展示でした。映像ではなく「音」からゲームをつくり、音だけで遊ぶ——そんなユニークな作品が集まった場所です。名古屋で開かれていることを知り、出張のついでに同僚を誘って足を運んでみました。 制限時間内に、音だけを頼りに花へ水をあげる「はなちゃんを救え」。会場で最初に夢中になった作品です。 この記事では、その朝のことを書いてみたいと思います。 なぜ、この三人で行こうと思ったのか 少しだけ、前日の話をさせてください。私と竹中さんは名古屋で仕事があり、前日から出張していました。 せっかく名古屋まで足を運ぶのだから、この出張をアクセシビリティの活動にもつなげられないか——そう考えていたときに、ちょうどオーディオゲームセンターの展示が名古屋で開かれていることを知りました。アクセシビリティのアドボケートとして、これはぜひこの耳で確かめておきたい展示です。そう考えた私は、竹中さんに加えて、名古屋オフィスで働く浜谷さんにも声をかけました。浜谷さんは、ドライバーに向けたサウンド設計を仕事にされている方です。「音」を扱うプロと一緒に、音のゲームを体験できる。これ以上ない組み合わせだと思いました。 正直に打ち明けると、この「お誘い」には、私なりの小さな狙いもありました。 アクセシビリティのアドボケートとして、私が会社のなかで担いたい役割は、製品やサービスを使いやすくすることだけではありません。その手前にある、「アクセシビリティの文化」そのものを社内に少しずつ根づかせていくことだと考えています。 そのためには、ガイドラインやチェックリストと向き合う時間だけでなく、まだ見たこと・触れたことのない領域のアクセシビリティに、同僚たちが自然と出会えるきっかけをつくりたい。そんな思いが、以前からずっとありました。今回の展示は、そのきっかけにぴったりだと感じたのです。 「音」を共通言語にして遊んだ、あの日の記憶 この場所に同僚を連れて行きたかった理由は、もうひとつあります。それは、私自身の忘れられない原体験です。 ずいぶん前のことになりますが、私はかつて東京で、「オーディオゲームをつくるハッカソン」に参加したことがありました。視覚障害のある人も、目の見える人も、一緒になって音だけのゲームをつくり、その場で遊ぶ。そんなイベントでした。 そこで私が感じたのは、なんとも言えない心地よさでした。その場には、難しい「アクセシビリティ」の話は、ほとんど出てこなかったのです。「視覚障害者のために」とか「配慮しなければ」といった肩に力の入った言葉ではなく、ただ純粋に、「音を中心にしたゲームって面白いよね」という一点で人が集まっていました。 「音」という共通言語の前では、目が見えるかどうかは、その場の主役ではありませんでした。みんなが同じように耳をすませ、同じように戸惑い、同じように笑う。あの対等でフラットな空気が、私にはとても新鮮で、心地よかったのです。 この感覚を、ぜひ同僚にも味わってほしい。難しい理屈ではなく、まず「面白い」から入ってもらえる体験として——そう思ったことが、今回のお誘いの根っこにありました。 当日、私たちを夢中にさせた作品たち さて、当日の会場には、いくつもの作品が展示されていました。どれも、思わず「お、これは!」と声が出てしまうような、ユニークなものばかりです。 1つめは、 植木鉢を抱えて、音を頼りに水を探すゲーム 。鉢を持って歩き回り、聞こえてくる音を手がかりに、水のありかを探し当てます。制限時間内に花へ十分なお水をあげられるかは、私たちの耳と方向感覚にかかっています。冒頭で私が抱えていたのが、この鉢でした。 2つめは、 音だけで進行する人狼ゲーム 。誰が人狼なのか、表情ではなく声と音だけで推理していく緊張感がありました。それぞれのプレーヤーには、小さなスピーカーを通して別々の振動が伝えられ、どんな振動が届いたのかを話し合いながら、誰が人狼なのかを推理していきます。自分に届いた振動のリズムを、そのまま声に出してしまわないよう気をつけながら、慎重に人狼を探しました。 声と音だけで人狼を推理する「サウンドウルフ」と、鳴き声から動物を当てるクイズ。プレーヤーには小さなスピーカーから別々の振動が届きます。 3つめは、 猫の鳴き声を頼りに、迷路の中で猫を探して捕まえるゲーム 。「ニャー」という声を追いかけて夢中になっている竹中さんの様子が、その声の弾み方から伝わってきて、なんとも微笑ましく感じました。見つけたと思った猫が、ふっと別の方向へ鳴きながら逃げていく。その手応えのなさに、私は昔飼っていたやんちゃな犬のことを思い出しました。 「Echolocation Maze ― 迷路でねこ探し」。アルミのフレームの中に入り、耳をすませて猫を探しているところ。目ではなく、音に集中する時間です。 4つめは、 音を頼りに、ちょうどよい焼き加減を狙って焼肉を焼くゲーム 。お肉が焼ける音の変化だけで、食べごろを見極める。サウンド設計を仕事にしている浜谷さんが、ここでいちばん真剣になっているのが、その張りつめた集中ぶりから伝わってきました。三人で同時にお肉を取り出すとボーナス点がもらえることもあって、それぞれが真剣にタイミングを見計らいながらゲームを進めました。 お肉が焼ける音の変化だけで、食べごろを見極める焼肉ゲーム。サウンド設計が本職の浜谷さんが、いちばん真剣に聞き入っていました。 そしてもうひとつ、ゲームというより、 動かすと振動に合わせて笑い出す提灯のおもちゃ もありました。手のなかで震えながら笑う提灯に、思わずこちらまで笑ってしまいました。子どもの頃に遊んだ「笑い袋」を思い出すような、ちょっと懐かしい作品です。 どの作品も、目で見て楽しむものではありません。耳をすませ、手で感じ、音の変化に身をゆだねて遊ぶ。気がつけば三人とも、すっかり童心に返って盛り上がっていました。 「面白い」が、いちばん最初にあっていい 会場を出たあと、私はふと、あの東京のハッカソンで感じた心地よさが、そのままここにもあったことに気づきました。 この朝、私たちは一度も、肩肘張った「アクセシビリティ」の話をしませんでした。ただ、音のゲームが面白くて、三人で笑い合っていただけです。目が見える二人と、見えない私とが、まったく同じスタートラインで戸惑い、同じように夢中になれる。そういう体験を、言葉ではなく、身体で分かち合えたことが、私にはとても嬉しかったのです。 アクセシビリティというテーマは、ともすると「正しさ」や「やらなければいけないこと」として語られがちです。それももちろん大切なことです。でも、その入り口に、こんなふうに「ただ純粋に面白い」という体験があってもいいはずだと、あらためて思いました。難しい話の前に、まず一緒に楽しんでしまう。そこから自然と、「音だけでこんなに遊べるんだ」「目に頼らない世界にも、こんな豊かさがあるんだ」という気づきが生まれていく。 これからも私は、社内のいろんな人と、こういう「触れるきっかけ」を少しずつ増やしていきたいと思っています。難しい顔で身構える前に、まず一緒に耳をすませて、笑ってしまう。アクセシビリティの文化は、案外そういう楽しい時間の積み重ねから根づいていくのかもしれない——名古屋出張の締めくくりに、そんなことを思ったのでした。 もうひとつの出会い ― 鼓動を伝えるモビリティ「QUENELLE」 会場には、オーディオゲームとは別に、もうひとつ強く印象に残った展示がありました。「QUENELLE(くねる)」という、小型EVのコンセプト作品です。 これは、乗る人の鼓動を読み取り、それを音や光、振動として映し出す乗り物でした。乗り物を「操作する道具」としてではなく、感覚でつながる相手のように感じさせてくれる——そんな試みです。私も実際にまたがらせてもらいました。 「QUENELLE」にまたがらせてもらいました。なお、同席されたスタッフの方など、ご本人の同意を確認できていない方のお顔は画像処理をしています。 鼓動を音・光・振動で映し出す、小型EVのコンセプト作品。乗り物を「操作する道具」から「ともにある存在」へと近づけようとする試みです。 ドライバーに向けたサウンド設計を仕事にする浜谷さんが、この作品の前で何を感じていたのか。それは、次の感想に譲りたいと思います。 一緒に行った二人から 最後に、同行してくれた二人に、当日の感想を一言ずつ書いてもらいました。 現地に行くまでは「音のゲーム」というものがまったく想像できず、どちらかといえば、見た目には地味で質素な作品をイメージしていました。でも、実際にプレーしてみると、自分自身が夢中になって遊んでしまうほど面白くて驚きました。子どもから大人まで、幅広い世代の人たちが一緒に楽しめる——オーディオゲームには、そんな可能性があると感じました。(竹中) 車を運転中のドライバーは、とても不自由です。ずっと前を見ていないといけないし、好きに動くこともできない。ほとんど唯一の愉しみは「音」ですが、音楽を流すか、ラジオを聴くか、同乗者とのお喋りか。それって数十年前からほとんど何も変わっていない。「音を愉しむ」って、もっと自由で、色んな可能性があるんじゃないか?と、ずっと考えていました。一緒に体験したオーディオゲームセンターの作品は、自分の中にあった漠然とした仮説を、確信へと一歩近づけてくれました。(浜谷) 目を使わずに「音」だけで遊ぶ時間は、私にとって、アクセシビリティの新しい入り口を確かめ直す時間でもありました。もし機会があれば、ぜひ一度、耳だけを頼りに遊んでみてください。きっと、思っているよりずっと豊かな世界が広がっています。 参考リンク オーディオゲームセンター: https://artscape.jp/exhibitions/64694/ オーディオゲームをつくるハッカソン(CCBT): https://ccbt.rekibun.or.jp/events/audiogamecenter_ccbt_hackathon
はじめに QEグループでモバイルチームに所属しているmです。 前職までは約7年ほどモバイルアプリの開発をメインに従事していました。 今はQAとしてプロダクト開発に関わっています。 この記事では、QAが設計や実装といった開発側との共通言語を持っておくと、テスト設計でも不具合対応でも有効である、という話を書きます。 共通言語があると問題の切り分けがしやすくなり、開発とのコミュニケーションコストも下がるため、本来集中すべき業務に工数を割きやすくなると考えています。 なお現在も対応中の取り組みなので、効果については「こう感じている」という話も含みます。 課題:内部の変更は、影響範囲が外から見えづらい まず、この記事の出発点となる課題から整理します。 内部実装だけが変わる対応、特にユーザーから見える動作は変えないリファクタリングなどは、影響範囲が外から見えづらいという特徴があります。 例えば、Swift6対応やKMP対応、UIライブラリの移行などが該当するかと思います。 挙動レベルの影響は開発から共有されるものの、外から触っているだけではどこがどう変わったのかを把握しにくいです。 そのため、テスト設計では「影響範囲が分からないので、念のため全項目をテストしよう」となりやすく、 不具合対応では「ゼロから原因を探そう」という方向に傾きやすくなります。 ただ、実際には期日などの制約もあるため、すべてを見るわけにもいかない場合があります。 この「影響範囲が外から見えづらい」という点が、このあと述べる2つの工程(テスト設計と不具合対応)に共通する課題になります。 前提:アプリ側の設計・実装を把握して見る 課題への向き合い方として、まず普段行っている見方を記載します。 アプリは一枚岩ではなく、機能や役割ごとに分かれて作られています。 たとえば、画面の見た目を作る部分、データを加工する部分、サーバーと通信する部分、データを保存する部分、といった具合です。 何かが起きたとき、「画面上どう見えるか」だけで捉えるのではなく、 「それはどの責務の話なのか」「どことつながっているのか」を、アプリの作りの上で考えるようにしています。 これが、本記事で言う「構造から見る」という見方です。 たとえば「ログイン後の表示がたまにおかしい」という事象であれば、見た目を作る部分ではなく、 データを取得する通信や処理の部分が怪しいのではないか、と当たりをつける、といった具合です。 もちろん開発経験があるため、コードや設計資料を見れば、どの部分がどのように動き、どこにつながっているかは比較的把握しやすいほうだと思います。 ただ、この見方だけに頼るのではなく、開発側にも影響範囲の資料を展開していただいています。 そして、この見方がまず効くのが、テスト設計です。 テスト設計:見るべき範囲を、根拠を持って絞る 先ほどの「つい全項目をテストしたくなる」場面を、前項の考え方で設計します。 ここで行うのは、実装を隅々まで読むことではありません。「この変更は何をしようとしているのか」「だとすれば、何が壊れそうか」を、設計レベルで把握することです。 ここが分かると、何を見るか・どこまで見るかの手がかりになります。 具体的には、次の3つを問いとして使っています。 ①動作は変わるのか、変えないはずなのか 変えないはずの変更であれば、見るべき軸は「変更前と同じか(同等性)」になります。見た目や操作感が、変更前と変わっていないかを確認する、ということです。 ②技術的にどこへ影響のある変更なのか 並行処理なのか、UIの作りなのか、通信まわりなのか。 影響する領域が分かると、その領域で起きやすい問題が、見るべき観点になります。 たとえば並行処理が変わるのであれば、断続的に固まる・クラッシュする・反映が遅延する、といった点です。 ③どの単位で入る変更なのか 画面単位なのか、モジュール単位なのか。これが分かると、見る範囲の単位が定まります。 たとえば、今回改修した画面のみを対象にする、といった具合です。 この3つの問いで根拠を持って絞れると、見る範囲が現実的なところに収まります。 「全部やる」から「変更によって壊れそうな箇所を確かめる」へ変わる、という感覚です。 あわせて、開発との「どこまで実施するか」のすり合わせも速くなると考えます。 後工程でも効く:不具合の起票と切り分け ここまではテスト設計の話でしたが、メリットはその先の工程にも続くと考えています。 テスト設計の段階で把握した「この変更はどの実装箇所に影響があるか」という理解は、不具合の起票や切り分けの場面でも、そのまま活用できます。 一度読み込んだ作りが、ここでも改めて効いてくる、という形です。 そのため、案件にもよりますが、起票を見た時点で「このあたりが怪しいのではないか」と当たりをつけ、仮説つきで起票できます。 たとえば、表示崩れ・断続的に固まる、iOS・Android共通の不具合、といった切り口で、どの部分の話なのかを見立てられます。 重要なのは、これは「QAが原因を完璧に特定できる」という話ではない、ということです。 あくまで当たりをつけ、仮説を立てるところまでです。ただ、その仮説があるかないかで、その後の動きはかなり変わると考えています。 「画面が固まりました」とだけ書かれた起票と、 「ここは非同期処理の変更が入った画面なので、そのあたりが怪しいかもしれません」という仮説つきの起票とでは、 開発側が状況を把握する速さも、誰に依頼すべきかの的確さも変わってきます。 切り分けが、「ゼロから探す」から「仮説を検証する」へ変化し、開発とQA双方にとってメリットが生まれるかと思います。 実装に寄りすぎない ひとつ、意識していることがあります。 現在の実装に寄りすぎると、「すでに実装されているもの」をなぞるだけになり、本来あるべきなのに、実装として抜けている観点を見落とすおそれがあります。 そのため、開発から共有された「本来こう動くべき」という挙動レベルの情報と、 コードや資料から読み取った「現状こうなっている」という状態を、突き合わせるようにしています。 構造理解で当たりをつけつつ、「ユーザーから見てどうあるべきか」という挙動ベースの視点も手放さない。 このバランスが大事だと考えます。 まとめ 今回の内容を要約すると、こうなります。 構造を理解しておくと、テスト設計でも後工程でも、QAの動きが「探す」から「検証する」へ変わる。一度の歩み寄りが、複数の工程で効いてくる。 もちろん、自身の背景などから知見があったという側面はありますが、それがないと無理な話ではないと考えています。 まずはプロジェクトのアーキテクチャ図や設計資料を読んでみる、詳細設計の境界を意識してみる、変更がどの設計箇所に影響するのかを開発に聞いてみる。 そうした小さなところからでも、見立ては変わってくるかと思います。 内部実装の変更で「全部見るしかない」となりがちな場面でも、構造から攻めることで、影響範囲の見立ての精度を上げられます。 同じような場面に立っている方の参考になれば嬉しいです。
0. はじめに 弊社では Claude Code を全社的に導入しており、Claude Code のメトリクスとログを OpenTelemetry で収集し、Grafana で可視化しています。ところがある日、こんな声が飛んできました。 「ダッシュボード、また固まってるんだけど」 社内に Claude Code を展開して、いざ「みんなどれくらい使ってる?」を可視化しようとしたら、開いたダッシュボードがくるくる回り続けて何も出てこない。タイムアウト。リロードしてもまた回る。 原因は Claude Code が吐く 1 つのメトリクス claude_code_token_usage_tokens_total でした。これがカーディナリティ爆発(カーディナリティについては後述)を起こして、Thanos Query のメモリで使い倒していました。 この記事は、その障害を Grafana Managed Recording Rule × Prometheus × Thanos の構成でどう捌いたか、の記録です。打ち手を 3 案で比較した話と、採用案で実際にハマった「書き込み先(write 先)問題」を 2 段階でほどいた話がメインです。同じように AI ツールの利用状況を可視化したい人、そして高カーディナリティに殴られた経験のある Observability 担当者の参考になればと思います。 :::message この記事の構成は「検証で動作確認し、現在は短縮稼働で運用している」段階のものです。本番フル稼働の数値ではなく、構成判断のプロセスと再現できる設計を中心にまとめています。 ::: 1. 何が起きていたか:claude_code_token_usage_tokens_total のカーディナリティ爆発 Claude Code は OpenTelemetry 経由でトークン使用量などのメトリクスを出力できます。その中心が claude_code_token_usage_tokens_total です。 問題はラベルでした。このメトリクスには session_id が付きます。セッションごとにユニークな ID が振られるので、 利用が増えれば増えるほど系列(time series)が線形に増え続ける 。さらに user_email 、 model 、 type (input/output/cacheRead など)も掛け算で効いてくる。 claude_code_token_usage_tokens_total{ session_id="...", # ← セッションごとにユニーク。爆発の主犯 user_email="...", model="claude-...", type="output" } :::details カーディナリティとは メトリクスにおけるカーディナリティとは、あるラベルが取りうる値の種類数のことです。たとえば type ラベルの値が input ・ output ・ cacheRead の 3 種類しか振られなければカーディナリティは低い。一方 session_id のようにセッションごとに異なる値が振られるラベルは、セッションが増えるほど無限に種類が増えていく「高カーディナリティ」なラベルです。ラベルの組み合わせが時系列(time series)の数になるため、高カーディナリティなラベルが 1 つあるだけで系列数が爆発的に膨れ上がります。 ::: session_id のような無限に増える値(unbounded label)が 1 つ混じるだけで、系列数は天井知らずになります。 そして Thanos Query は、クエリ時に対象系列を メモリ上に展開してから 集計します。系列が数十万を超えてくると、 sum by (user_email) のような一見シンプルな集計でもメモリを食い尽くし、タイムアウトか OOM で落ちる。ダッシュボードが固まっていた正体はこれでした。 2. KTC のモニタリング基盤の構成 本題に入る前に、今回の話に登場するコンポーネントを整理します。 2-1. Alloy(OpenTelemetry Collector) Grafana Alloy は OpenTelemetry ベースのコレクターです。KTC では各システムの AWS サービスのログやClaude Code からメトリクス・ログを収集する入口として機能しています。 2-2. Prometheus Kubernetes 上のメトリクスを scrape する OSS の監視ツールです。KTC では scrape したメトリクスを Thanos Receiver への remote write で転送するリレー役を担っています。メトリクスの長期保存は Thanos に委ねる構成です。 2-3. Thanos Prometheus をスケールアウト・長期保存対応させるための OSS です。KTC では以下のコンポーネントで構成しています。 コンポーネント 役割 Routing Receiver remote write の受口。hashring に従って Ingesting Receiver へルーティング Ingesting Receiver 実際の TSDB への書き込み。S3 へ flush Store Gateway S3 のブロックをクエリ可能にする読み取りゲートウェイ Query 実際にクエリーを実行するコンポーネント。 Query Frontend クエリのキャッシュ・スプリットを担当。Grafana からの入口 Compactor S3 のブロックを定期的に圧縮・ダウンサンプリング マルチテナント構成を採用しており、書き込み時は通常 THANOS-TENANT HTTP ヘッダーでテナントを指定します。 2-4. Grafana Grafana は Thanos をデータソースとして接続し、ダッシュボードの描画と Recording Rule の管理を担います。KTC ではシステム単位でマルチテナント化した Grafana インスタンスを各チームに払い出しており、今回 Claude Code コスト可視化用に専用インスタンスを立ち上げました。 3. 課題とゴール 「じゃあ session_id を消せば終わりでは?」と思いますよね。でもそれが簡単に言えない理由がありました。 社内で AI 活用を進めるには、まず 測れること が前提になります。誰がどれくらい使っているのか、どのモデルにトークンが寄っているのか、1 セッションあたりの規模感はどうか。こういう粒度がないと「活用が進んでいるのか」を語れない。 特に session_id 単位の分析は、後から「効果的な使い方をしているチームの傾向を見たい」みたいな分析に効いてきます。つまり 生データとしての session_id は手元に残したい 。でも ダッシュボードのクエリでそのまま頑張るのは無理 。この二つを両立させるのが今回のお題でした。 ゴールはこう整理できます。 生データ( session_id 付き)は Thanos に貯め続ける ダッシュボードは「集約済みの軽いメトリクス」を見る 利用者がセルフサービスで見たい情報を見れるように調整ができること 4. 案比較 カーディナリティへの対象策はいくつかあります。今回は 3 案を並べて比較しました。 案 やること メリット 採用/不採用の理由 案1: Alloy でラベルドロップ 収集段階で session_id を捨てる 一番手前で止められて確実 生データから session_id が消え、後の分析が永久にできなくなるため、今回は不採用 案2: Thanos Ruler サーバ側の Recording Rule で集約 集約結果が Thanos に永続化される ルールの Apply が Platform 側でのオペレーションのみになる運用を実施しているため、利用者とPlatformでのやりとりが増えて運用負荷が高いため、今回は不採用 案3: Grafana Managed Recording Rule Grafana 側で集約ルールを定義し書き戻す 利用者が UI で自分でルールを作れる 生データを残しつつ、ルールの主権を利用者に渡せるため、 今回は 採用 判断の軸は 2 つでした。 生データを失わないか ( session_id を残せるか) ルールを誰が育てられるか (利用者が回せるか) 案1 は軸1で不採用。収集段階で捨てるとデータ分析に利用できません。案2 は軸2で不採用。Thanos Ruler はサーバ側のルールファイルを Platform が管理する世界なので、「ちょっとこの集約を試したい」が毎回 Platform への依頼になってしまう。これは Platform チームのボトルネック化も招きます。 残った案3 が、両方の軸をクリアしました。 5. Remote Write 先問題を 2 段階で解いた 案3 を進める上でも課題がありました。Grafana Managed Recording Rule は「集約した結果を どこかに書き戻す 」必要があります。メトリクスの管理に使っているThanosの場合はここがうまく設定できませんでした。 5-1. 詰まり1: Datasource に Thanos Receiver を直接指定できない Thanosにおいて、メトリクスの書き込み先は Thanos Receiver であるので、 Grafana の Datasource で Thanos Receiver を指定することを考えました。 ところが Datasource として登録できませんでした。理由はこうです。 Grafana の Datasource は基本的に Query(読み取り)系 に対して作るもの Thanos Receiver は Write 専用 で、Query を受け付けられない だから Receiver は Datasource として成立しない ここで再検討した結果、既に別用途で使用していた Prometheus でした。Prometheus は Query も Write(remote write 受信)も両方できる 。そこで Prometheus を Grafana の Datasource として指定することにしました。 Grafana の Datasource → Prometheus(Query を受け付けらるので Datasource になれる) Recording Rule の書き戻し → Prometheus の /api/v1/write で受ける(※) Prometheus が remote write で Thanos Receiver へ流す という形にしました。Prometheus が「Query 窓口」と「Write の中継」を兼ねることで、Datasource 問題が解けました。 ※ Prometheusはデフォルト設定では、Queryを受け付けられないので --web.enable-remote-write-receiver フラグを有効にし、外部から /api/v1/write でメトリクスを受け付けられる** にしています。 5-2. 詰まり2: マルチテナントへの書き込み(HTTP ヘッダーを操作できない) 次の壁はマルチテナントでした。Thanos Receiver はテナントを分けて受け取れますが、その振り分けは通常 HTTP ヘッダー( THANOS-TENANT ) で指定します。 ところが Grafana Managed Recording Rule の設定項目には、 書き戻し時の HTTP ヘッダーを差し込む設定がありません 。ルールの式と書き戻し先は指定できても、ヘッダーは触れない。AI 活用用のテナントへ正しく振り分けたいのに、その手段が塞がれている状態でした。 解決には Thanos Receiver 側の機能を使いました。 thanos receive \ --receive.split-tenant-label-name="tenant_id" --receive.split-tenant-label-name を指定すると、Receiver は HTTP ヘッダーではなくメトリクスのラベル値 を見てテナントを振り分けてくれます。つまり、 Recording Rule の集約結果に THANOS-TENANT="ai-usage" のようなラベルを付けておく Receiver がそのラベルを読んで AI 活用用のテナントへ流し込む ヘッダーが操作できないなら、振り分けの判断材料をラベルに寄せる。発想を「ヘッダーで指定する」から「ラベルで宣言する」に切り替えたのがポイントでした。 5-3. 全体アーキテクチャ 2 つの詰まりを解いた結果、データの流れはこうなりました。まず全体像を俯瞰します。 ポイントをまとめると: Prometheus が二役 :Grafana からの write を受け取る --web.enable-remote-write-receiver と、Thanos への転送( remote_write )を同時に担う テナント振り分けはラベルベース :Routing Receiver の --receive.split-tenant-label-name=THANOS-TENANT により、Recording Rule で付けた THANOS-TENANT ラベルの値でテナントが決まる。HTTP ヘッダーを操作できない制約をラベルで回避した 6. Before / After:タイムアウトが消えた 検証段階での効果はシンプルです。 観点 Before After ダッシュボード表示 タイムアウト / 描画されない 集約済みメトリクスを描画できる Thanos Query への負荷 高カーデ系列をメモリ展開して OOM 級 集約済みの軽い系列を読むだけ session_id 単位の生データ 残る 残る(Thanos に蓄積継続) 集約ルールの主権 (案2なら Platform) 利用者が UI で保持・編集 ダッシュボードが普通に開く、という当たり前を取り戻しつつ、生データも分析の主権も手放さずに済んだ、というのが今回のゴールでした。 7. まとめ 今回やったことを、次に同じ状況に出会った人が調査・検討できる粒度で残しておきます。 無限増殖するラベル( session_id 等)はカーディナリティ爆発の主犯 。 生データを収集段階で捨てる前に「後の分析で使うか」を問う 。Alloy でのドロップは確実だが取り返しがつかない Thanos Receiver は Write 専用なので Datasource にできない 。Query も Write もできる Prometheus を中継に挟む Grafana Managed Recording Rule は書き戻し時に HTTP ヘッダーを操作できない 。テナント振り分けは --receive.split-tenant-label-name でラベルベースに寄せる ここまで読んでいただきありがとうございました!