電通総研のブログ - TECH PLAY

TECH PLAY

電通総研

電通総研 の技術ブログ

全867件

はいどーもー! コーポレート本部の宮澤響です! (部署が変わりました!) 本記事では、昨年に引き続き、大学入学共通テストの「情報Ⅰ」の試験問題を解いてみた個人的な感想をお伝えします! なお、昨年の記事は こちら ! 問題・正解 全体の感想 問題ごとの感想 大問1 問1 問2 問3 問4 大問2 A B 大問3 大問4 おわりに 問題・正解 本記事の対象は、令和8年度の本試験問題となります。 問題および正解は、以下に公開されています。 問題PDF (遷移先: 毎日新聞 デジタルWebサイト) 正解PDF (遷移先: 独立行政法人 大学入試センター Webサイト) 全体の感想 情報を読み取る能力や活用する能力が問われている印象なのは昨年と同様ですが、昨年よりも時間設定がかなりシビアになったと感じました。 私の実力不足や、冊子かディスプレイかの環境差異によるところもあるかもしれませんが、60分間ではかなりギリギリで、見直しの時間が全くとれないレベル(解き終わった時点で既に59分経過程度)でした。 そのため、個々の問題を解くために必要な知識レベルはそれほど上昇していないにもかかわらず、時間内に全ての問題を解くために求められる読解・思考スピードが大きく上昇したことで、試験としては難化したのではないかと感じました。 問題ごとの感想 大問1 問1 ITパスポート試験 や 基本情報技術者試験 で出題されていそうな雰囲気がありますよね。 問2 私は8×8マスの図案を全て紙に描き殴りましたが、もっとスマートな解き方もありそうです。 問3 生年の初期値が2000になっているサービス、増えてきましたよね。 私の年齢では、数年前までは、生年を選択する際には下に(年++方向に)スクロールすることが当たり前だったんですが、最近は上に(年--方向に)スクロールすることが増えてきて、私も年を取ったんだなぁと感じたりします。 問4 こちらも ITパスポート試験 や 基本情報技術者試験 で出題されていそうな雰囲気ですが、aについてはこれまであまり考えたことがなかったため、解いていて自分でも「なるほどな」と思いました。 大問2 A ほのかに情報処理安全確保支援士試験の風味を感じました。 ちょうど先月、コンビニで マイナン バーカードを利用して住民票の写しを取得したばかりだったので、個人的にはタ イムリ ーな問題設定でした。 B キャラクター、ちょっと可愛い。笑 2箇所ある演算選択の解答がどちらも単なるAND演算だと、少し不安になりますよね。 大問3 昨年の大問3でも別の部が文化祭に向けて頑張っていましたよね。(文化祭はいいぞおじさん「文化祭はいいぞ」) 【エ】〜【オ】で開始時刻、終了時刻、待ち時間のそれぞれの求め方を改めて日本語で整理しているところが、とても丁寧な誘導だなと思いました。 ここに立ち返れば【カ】〜【コ】も解けるようになっていますし。 大問4 問3-aの 任意の二つの観測点を比較して、緯度が高い観測点の方が、400度開花差の値か600度開花差の値の少なくとも一方が必ず大きい。 という選択肢は、サッと目で追うだけでは理解できず、心の中で「なんて???」とコメントしながら3回くらい読み直しました。 これがあの「全部聞き取れたのに!」のときの気持ちなんですかね。笑 おわりに 本記事では、令和8年度大学入学共通テストの「情報Ⅰ」の本試験問題を解いてみた感想をお伝えしました。 IT業界のみなさまはもちろんですが、それ以外の業界のみなさまや学生のみなさまも、お時間のあるときに解いてみてはいかがでしょうか。 最後までお読みいただき、本当にありがとうございました! 私たちは共に働いていただける仲間を募集しています! みなさまのご応募、お待ちしています! 株式会社電通総研 新卒採用サイト 株式会社電通総研 キャリア採用サイト 執筆: @miyazawa.hibiki レビュー: @miyazaki.hirotoshi ( Shodo で執筆されました )
電通 総研 クロス イノベーション 本部の山下です。2025年11月-12月にかけて開催されたKiroの Hackathon イベントであるKiroweenに参加しましたので、そのレポートをお送りします。 このイベントはKiroを使ってアプリケーションを開発することを目的とした ハッカソン イベントです。 作るもののテーマがハロウィンをモチーフにしたイベントになっています。 参加要件など 以下のような参加要件になっていました。 実際の詳細は 公式サイト をご覧ください。 基本的にKiroを使ってアプリケーション開発をすればよいのですが、テーマが指定されているのが特徴です。 Resurrection: お気に入りの技術を復活させる Frankenstein: 複数の技術を組み合わせてアプリを作る Skeleton Crew: ス ケルト ンを作成し、それから複数のアプリを作る Costume Contest: 洗練された不気味なデザインのアプリを作る といったテーマのようです(日本語訳は筆者による)。 自分はResurrectionを選びました。参加するにあたりテーマ選定にかなり悩んだのですが、知人から自分が普段 Common Lisp を使っていて、それは十分に古い技術なのではという指摘を受けて、確かにその通りだなということで決めました。 作ったもの Kabotanというアプリケーションを実装しました。Kabotanは Common Lisp を使って作った、HTMXとLLMを組み合わせたアプリケーションです。ハロウィンにちなんだ機能を提供していて、質問に答えたり、ハロウィンに関する文章を生成したりすることができます。 なぜ Common Lisp を採用したかというと、古い技術と見なされており、テーマのResurrectionにも合っているためです。一方で自分は普段それなりに Common Lisp を使っているので少しでも Common Lisp の良さを知ってもらえたらと思い選びました。 Kabotanは以下のURLで公開しています。 https://github.com/dentsusoken/kabotan/ Kabotanは以下のような アーキテクチャ になっています。 フロントエンド: HTMX + Tailwind CSS バックエンド: Common Lisp (clack + hunchentoot) LLM: llama.cppを利用したローカルモデル(gpt- oss -120bなどを想定) モダンなアプリケーションではフロントエンドにReactやVue.jsなどの JavaScript フレームワーク を使うことが多いですが、今回はシステムの大部分を Common Lisp で実装したかったため、HTMXを採用しました。 フロントエンドにHTMXを使うことでフロントエンドの JavaScript コードを最小限に抑え、アプリケーションの大部分を Common Lisp で実装することができました。 実際の画面の例を以下に示します。 特に各 コンポーネント 間のやり取りではServer Sent Event(SSE)を利用して、LLMからの応答をリアルタイムに受け取れるようにしています。これにより、ユーザはLLMが応答を生成している間も進捗を確認でき、より インタラクティブ な体験が可能となっています。 個人的には Common Lisp でも現代的なアプリケーションの実装は十分に可能ということを示せたのではないかと思います。 ちなみに、 Common Lisp を含む Lisp 系の言語は括弧が多いことで有名です。慣れるとS式は読みやすいのですがなれないと苦労するかもしれません。例えばKabotanのindex.htmlを返す部分は以下のようなコードになっています。 ( defun serve-index ( env ) "Serve the main index.html page. The Lack session middleware automatically handles session cookies, so we don't need to manually set them here." ( declare ( ignore env )) ( let (( html ( uiop:read-file-string "public/index.html" :external-format :utf-8 ))) `( 200 ( :content-type "text/html; charset=utf-8" ) ( ,html ) ) )) Lisp 系言語ではこのS式と呼ばれる (関数名 引数1 引数2 ... 引数N) というような記法でプログラム自体を記述します。このデータもプログラム本体も全てこのS式で表現することで、非常に強力なマクロを作れたりするのが特徴となっています。 実装するうえで苦労したところ Common Lisp をKiroで利用するにあたって苦労した点、工夫した点がいくつかありました。 Common Lisp をKiroで利用するための整備 まず、 Common Lisp をKiroが利用できるようにするための整備です。例えば、 Common Lisp には標準でデバッガが実装されており、エラー発生時などには自動的にデバッガが起動します。 Common Lisp で広く使われている開発環境のSLIMEではこれを便利に利用することができます。しかし、この機能はKiroなどのAIにとっては対話的な操作が必要になってしまいAIの操作を阻害してしまいます。 また、ASDF(Another System Definition Facility)という Common Lisp の デファクトスタンダード なビルド管理システムがあります。これも事前に定義を行っておきひな形のアプリケーションが動作するような状態まで整備を行いました。その上で、makeを利用して常にデバッガを起動しないオプションを付けて起動するようにし、Kiroからもmake経由で実行するような形にしました。 最終的には以下のような Makefile のエントリとなりました。 --disable-debugger を実行時に引数で渡し、ASDFを使ってKabotanをビルド、実行する形になっています( ql:quickload がASDFを内部で呼ぶ仕組みになっています)。 ROS = ros LISP_IMPL = sbcl SYSTEM = kabotan TEST_SYSTEM = kabotan-test run: $(ROS) -L $(LISP_IMPL) run -- \ --disable-debugger \ --eval '(ql:quickload :$(SYSTEM))' \ --eval '(uiop:quit (kabotan:main))' Server Sent Eventへの対応 Kiroでのアプリケーション開発において、Server Sent Event(SSE)に対応させるのに苦労しました。SSEはサーバからクライアントへリアルタイムにデータを送信するための技術であり、LLMの応答をリアルタイムに受け取るために必要でした。 ブラウザ-Kabotan間のSSE対応 Common Lisp のWebフレームワークであるclackやhunchentootは直接このSSEをサポートしておらず、独自に実装する必要がありました。これはclackのソケットを直接操作する機能を利用して、SSEに対応させることができました。 Kabotan-llama.cpp間のSSE対応 llma.cppのサーバにとってKabotanはSSEのクライアントとして振る舞う必要があります。 これも Common Lisp のHTTPクライアントライブラリのdexadorを利用して独自に実装する必要がありました。dexadorは通信に利用しているソケットを扱うことができ、これを操作することでSSEに対応させることができました。 その他苦労した点 HTMX周りはKiroに色々指示を出さないとうまく対応できないことがあり苦労しました。HTMXはフロントエンドの JavaScript コードを減らすことができる利点がありますが、Kiroにその利点を理解してもらうのが難しい場合があり、何も指示を行わないとフロントエンドの JavaScript でほとんどの実装を行ってしまい、HTMXの利点がない構成になってしまうことがありました。 またLLMを利用するアプリケーションはテストに時間がかかってしまいます。 そしてKiroはコマンドの応答待ち時間が最大で20分になっていますが、稀にこれを超えてしまうことがありました。こうなってしまうと、Kiroはテストを途中で打ち切ったり問題がないのに問題があると判定して編集作業を行おうとしたり、逆に問題があるのに問題ないと判断してしまったりすることがあり、開発効率が低下することがありました。 Kiroの使い方について Hackathon 全体を通じてどのようにKiroを活用したのかについても紹介します。 Kiroを使ううえで重要だと感じたポイントは以下のとおりです。 Spec、Steeringの活用 Hookの活用 テストの工夫 特に、SpecとSteeringの使い分けは重要だと感じました。 Specという名前を見るとSpec側に詳細な仕様を書くべきだと考えがちですが、実際にはSteering側に詳細な仕様を書く方が効果的でした。例えば、 アーキテクチャ に関する指示、設計上の選択といったものはSteeringに記載し、実装が進むにつれて状況が変わるたびにSteeringはプロジェクトの実際の状況を表すように更新する必要がありました。 そして、Specは実際の小さな作業を行うために必要な最小限の仕様に留めておく方が効果的でした。基本的な動作の概要を伝えて、Design.mdを作成してもらい、Task.mdを生成してもらうようにしました。つまり、Specは スクラム 開発などでいうところの「ユーザーストーリー」に近い役割を果たし、Steeringが「詳細な要件定義書」や「設計書」に近い役割を果たす形です。 これらを前提に置き、詳細な設計などはVibe CodingでKiroと相談しながら進め随時Steeringを更新したり、簡単なバグ修正などは直接修正したりして進めました。一定規模を超える作業になりそうな場合はSpecを作成して対応してもらい、 リファクタリング などの作業もSpecとして作成して随時実施するようにしました。 以下は開発時のKiroの画面の様子です。Agent Steeringに色々設計上を指定しておき、作業ごとにSpecを作り開発していきました。 また、Hookも積極的に活用しました。Hookを使うことで、Kiroが生成したコードに対して自動的に追加の処理を行うことができます。Kabotanでは lisp ファイルが更新されたときに自動的にテストが実行されるようにHookを設定しました。Hookは便利なのですが、TaskとしてKiroが実行してしまうためHookを実行している間新しいタスクの着手が出来ないという欠点もあります。つまり、タスクが完了したとKiroが報告してくるので次のタスクを実行しようとするが、Hookが動作している間は新しいタスクに着手できないということです。しかもKiroは現在実行中のタスクを一望するインタフェースが分かりづらい位置にあるので最初は苦労しました。 以下のUIで実行中のタスクなどが確認できます。クリックして初めて詳細が分かるようになっています。常に表示されていると便利なのですが今後是非改善してほしいですね。 テストの書き方も簡単な 単体テスト であればKiroに生成してもらうようにして、実際の動作を確認するような総合テストについては細かく指示を出してKiroに生成してもらうようにしました。総合テストでは受け入れのためのテストを作るような指示を出し、それをこまめに実行するような運用を行いました。これは最終的な動作だけはちゃんと確認したいという意図でした。 Kiroが良くなっていた点 Kiroが発表されてから時間が経過しており、その間にKiro自体も改善されていました。今回の Hackathon を通じて特に良くなっていたと感じた点は以下のとおりです。 利用できるモデルが増え、特にClaude Sonnet 4.5が利用できるようになりました。これにより、生成されるコードの品質が向上しています。また、利用中にKiroがGA(General Availability)になりQ Developer CLI がKiro CLI になったという変化もありました。これに合わせてアカウント管理などもKiro側で行うことが可能になり、より使いやすくなっていました。特に上限に達した場合にも追加で課金を行うことで利用が可能になるのはとても便利になった点です。Q Developerを試していたころは上限に達すると利用できなくなってしまい、開発が中断されてしまうことがありました。新規アカウントをその都度発行するという手段もあるのですが、会社のアカウントで利用している場合は難しい場合もあるので、追加課金で対応できるのは便利です。 また、プロパティベースのテストが生成できるようになりました。以前は 単体テスト などの具体的な値を使ったテストが中心でしたが、今回はプロパティベースのテストを生成するように指示を出すことで、より広範囲な動作確認が可能になります。受入テストなどでは特に有効だと感じました。 まとめ Kiroの Hackathon イベントであるKiroweenに参加し、 Common Lisp を使ったHTMX+LLMアプリケーションであるKabotanを開発しました。Kiroを活用することで、効率的に開発を進めることができ、 Common Lisp でも近代的なアプリケーションの実装が可能であることを示せたと感じています。 またKiroは言語の限定なく利用できるということが 公式ドキュメント で記載されています。 Common Lisp でも問題なく対応出来ました。採用する機会が少ない言語も含めて色々な言語でアプリケーション開発可能であることも確認できました。 Kiro自体も改善されており、より使いやすくなっていました。今後もKiroを活用して様々なアプリケーション開発に挑戦していきたいと考えています。 以上、Kiroween参加レポートでした。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @yamashita.tsuyoshi レビュー: Ishizawa Kento (@kent) ( Shodo で執筆されました )
こんにちは、クロスイノベーション本部の大岡叡です。 2025年12月19日(金)にNECソリューションイノベータ株式会社、キャップジェミニ株式会社と合同で新卒1年目のAWS勉強会を開催しました。この記事では、その勉強会の背景や内容をご紹介します。 背景 目的 内容 気づき・参加者の反応 開催してみての気づき 参加者の反応 まとめ・今後の展望 背景 とある外部のAWS勉強会にて、NECソリューションイノベータ株式会社の上田賢哉様、キャップジェミニ株式会社の遊佐康平様と出会いました。お二人とも新卒1年目です。 その勉強会の休憩時間に私から「3社合同で1年目だけのAWS勉強会をやってみませんか」と提案したところ、その場でお二人とも賛同してくれました。正直なところ、その時は話を合わせてくれているだけかなと思っていたのですが、翌日にお二人から「勉強会をやりましょう」というメールをいただいたので、これは本当に実現させようと思い、開催に至りました。 私が勉強会を提案した理由は、近い年齢の方々との勉強会が有意義だと考えているからです。 私は社内で同期8人と隔週で勉強会を実施しています。同期同士だと遠慮なく疑問をぶつけ合えますし、近い年齢だからこそ得られる刺激もあります。この社内勉強会が良い場になっているので、会社の枠を超えてやってみたらもっと面白いんじゃないか?と思い、提案しました。 目的 勉強会の一番の目的は、 3社の若手エンジニアの技術力・プレゼン力の向上 です。勉強会の形式は、ハンズオンやブックリーディングではなく、登壇者が発表して参加者が聞くというオーソドックスなスタイルを採用しました。 また、他社の同期から刺激を受けたり、同業他社の同期とのつながりを作れることもこの勉強会で期待している効果です。 内容 まずは勉強会の基本情報です。 日時:12/19(金)19:00 - 21:00 場所:電通総研 品川本社 形態:対面のみ 参加者:16名 NECソリューションイノベータ株式会社:3名 キャップジェミニ株式会社:6名 弊社:7名 続いて、勉強会のアジェンダです。発表者は全部で9名で、各社から3名ずつ登壇しました。 時間 タイトル 発表者 19:00-19:05 開会 - 19:05-19:15 1. 初めてAWS触ってみた 〜テスト環境サーバを定時起動/停止できるようにしてみた〜 水谷元紀(電通総研) 19:15-19:25 2. 増えたのは技術ではなく選択肢 溝上木綿(NECソリューションイノベータ) 19:25-19:35 3. 権限管理の落とし穴 〜Allowにしたのにアクセスできない?〜 遊佐康平(キャップジェミニ) 19:40-19:50 4. 11月の個人的に気になったアップデートを調査・検証してみた 大岡叡(電通総研) 19:50-20:00 5. CloudWatch Agentから始める初めてのAWSログ監視 上田賢哉(NECソリューションイノベータ) 20:00-20:10 6. 実質2日でAWS SAAに合格した話 〜おすすめはしない勉強法〜 渡邊篤弥(キャップジェミニ) 20:15-20:25 7. Amazon Route 53を使って名前解決とルーティングを学ぶ 伊藤梨子(電通総研) 20:25-20:35 8. S3署名付きURLは発行できた!…でもAPI GatewayでCORSエラーにハマった話 蒲弘大(NECソリューションイノベータ) 20:35-20:45 9. Glueジョブ設計で知っておくべき同時実行・DPU制限 千葉理緒(キャップジェミニ) 20:45-21:00 閉会 - ※ 敬称略 ※ 19:35-19:40と20:10-20:15は休憩時間としました。 ※ 休憩時間が短かったため21:00-21:10でネットワーキングの時間を設けました。 ※ 開会、閉会、司会は私が務めました。 以下、各発表の内容を簡単に紹介します。 1. 初めてAWS触ってみた 〜テスト環境サーバを定時起動/停止できるようにしてみた〜 EventBridgeとLambdaを使ってEC2の定時起動・停止を実装し、Teamsへの通知もWebhookで実現。実装中につまずいたポイントを分かりやすく、かつ面白く紹介してくれました。 2. 増えたのは技術ではなく選択肢 AWSを勉強して実際のプロジェクトに入り、選択肢が増えたと感じた話。アーキテクチャに正解はなく、色々な構成が考えられるという気づきを共有してくれました。re:Inventで発表された新サービスの紹介もありました。 3. 権限管理の落とし穴 〜Allowにしたのにアクセスできない?〜 IAMでS3へのアクセスを許可したはずなのに、バケット間でファイルをコピーできない…原因はバケットポリシーでした。パブリックアクセスの設定や、セキュリティ事故につながるバケットポリシーの例をクイズ形式で紹介してくれました。 4. 11月の個人的に気になったアップデートを調査・検証してみた 11月に気になって触れていなかったアップデートを調査・検証した話。aws loginコマンド、リージョナルNAT Gateway、M2MのALBでのJWT検証の3つについて話しました。 5. CloudWatch Agentから始める初めてのAWSログ監視 EC2からCloudWatch Logsにログを送信する手順について解説。EC2にCloudWatch Agentをインストールしたのにログが送信できなかったトラブルとその解決についても話してくれました。 6. 実質2日でAWS SAAに合格した話 ChatGPTを活用したチェックリスト勉強法を紹介。試験結果から各分野の習熟度を振り返って分析していたのが印象的でした。 7. Amazon Route 53を使って名前解決とルーティングを学ぶ DNSの基礎から丁寧に解説。Route 53、ALB、EC2の構成でアーキテクチャを組み、実際に名前解決を試した内容を紹介してくれました。 8. S3署名付きURLは発行できた!…でもAPI GatewayでCORSエラーにハマった話 地図上にお気に入りの場所と画像を紐づけて共有できるアプリを、API Gateway・Lambda・S3・RDSを使った構成で構築。その過程で遭遇したCORSエラーの解決について話してくれました。 9. Glueジョブ設計で知っておくべき同時実行・DPU制限 AWS Glueの概要からジョブ・ワーカー・DPUの説明、アカウントごとの制限やジョブの同時実行制限について解説。DPUの性能を上げるか、ワーカー数を増やすかといった設計上の考慮点も紹介してくれました。 気づき・参加者の反応 開催してみての気づき 勉強会を開催してみて、いくつか気づいたことがあったのでご紹介します。 1. 年末は人を集めにくい 年末の時期ということもあり、「行きたいけど予定が合わない!」と連絡をくれた方が多くいました。また、体調不良や忘年会が重なり、直前で不参加になった方もいました。 2. アジェンダはゆとりをもって組むべき 今回は2時間で9名が登壇するタイトなスケジュールでした。21:00〜21:10のネットワーキングの時間以外はあまり交流ができなかったので、次回は休憩時間を少し長めにとったり、ネットワーキングの時間をしっかり設けたりして参加者同士の交流を増やしたいと考えています。 3. 学び続けるモチベーションになる 社外の同期が勉強している姿を見ると、自分も負けないようにもっと頑張ろうという気持ちになります。近い年齢の人同士が互いに良い刺激を与え合えるという点がこの勉強会の価値だと感じました。 参加者の反応 弊社参加者からは以下のような感想をもらいました。嬉しい反応ばかりでした! 「登壇できて良い経験になりました!」 「他の会社の人たちと話せて良い刺激になりました。」 「AWSの勉強を始めようと思いました!」 まとめ・今後の展望 NECソリューションイノベータ株式会社、キャップジェミニ株式会社の1年目の方々とAWS勉強会を実施したお話を紹介しました。 今回の勉強会が参加者から好評だったため、第2回の開催を検討中です。上田様、遊佐様と振り返りを行い、次の勉強会をより良いものにしたいと考えています。 最後までお読みいただきありがとうございました! 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @ooka.toru レビュー: @miyazawa.hibiki ( Shodo で執筆されました )
はじめに 金融IT本部 2年目の坂江 克斗です。 業務にて ドメイン ベースでのアウトバウンド 通信制 限を考えるタイミングがあったため、本記事を書きました。 DNS に関する基本的な内容は こちらの記事 に、アウトバウンドセキュリティの概要に関しては こちらの記事 に記載しているので、気になる方はぜひ参照してみてください。 はじめに 概要 ドメインベースのアウトバウンド通信制御の検証 前提 Amazon Route 53 Resolver DNS Firewallの検証 Terraformの実装 デプロイ後の検証 AWS Network Firewallの検証 Terraformの実装 デプロイ後の検証 AWS Network Firewall Proxyの検証 デプロイ後の検証 まとめ おわりに 概要 以下の表に AWS におけるアウトバウンドセキュリティサービスの一覧を示します。 レイヤ 制御観点 AWS サービス 料金 特徴 DNS (名前解決) ドメイン 名 Amazon Route 53 Resolver DNS Firewall 低( DNS クエリ) 早期に遮断可能。ローカルでの名前解決やIP直打ち、独自 DNS 使用等の Route 53 Resolverを経由しない通信は防御不可 L3–4 IP / Port Security Groups / NACL 無料 ネットワークセキュリティのベース L3–7 IP / Port, TCP ヘッダ / HTTP ヘッダ / TLS ヘッダ AWS Network Firewall , AWS Gateway Load Balancer + サードパーティ ソフト 高(AZ毎の常時稼働エンドポイント+処理量) 柔軟な制御が可能で、マネージドにも運用可能。 ドメイン 制御において、Host ヘッダのスプーフィングは防御不可、SNIスプーフィングには対応可能※1。 DNS + L3–7 IP / Port, HTTP ヘッダ / TLS ヘッダ AWS Network Firewall Proxy 未発表 マネージドな フォワ ードプロキシサービス(通信主体は Proxy)。現状はプレビュー公開中でボディ解析は未提供 ※1 Network Firewall の TLS インスペクション機能を利用することで対策可能 今回は業務要件で挙がった ドメイン ベースのアウトバウンド制御手法に関して、 Amazon Route 53 Resolver DNS Firewall ・ AWS Network Firewall ・ AWS Network Firewall Proxy で比較検証を行います。 ドメイン ベースのアウトバウンド 通信制 御の検証 前提 検証のためローカルで実装します。 EC2をプライベートサブネットに配置し、NAT ゲートウェイ 経由での AWS サービス・外部サービスへのアクセスを想定します。 Allow List により検証します。 SSM接続用の AWS ドメイン と本テックブログ tech.dentsusoken.com へのアクセスのみ許可し、他 ドメイン をすべて拒否。 本検証では、公開されている Web サイトに対して、 通信制 御の挙動を確認する目的で少数のリクエストを送信しており、サービスの可用性や機密性に影響を与える行為は行っていません。 本記事では、検証のシンプルさや負荷の低さ(数リクエスト程度)からテックブログ用 ドメイン による検証を行いました。 しかし、本来はテスト手法に問題があり予期せず過大な負荷をかけてしまった場合など、 規約違反 となる可能性があるため、 IANA(現在はICANNの一部)が管理するテスト用ドメイン ( example.com 等)を使用し検証を行うことが最も適切となります。 Amazon Route 53 Resolver DNS Firewall の検証 Terraformの実装 以下に示すシンプルな構成で検証します。 初めに VPC およびEC2の定義をします。 terraform { required_version = "~> 1.14.0" required_providers { aws = { version = "6.23.0" source = "hashicorp/aws" } } } provider "aws" { region = "ap-northeast-1" } data "aws_region" "current" {} data "aws_partition" "current" {} data "aws_caller_identity" "current" {} ########################################################################### ## VPC resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" enable_dns_hostnames = true enable_dns_support = true tags = { Project = "example" } } resource "aws_internet_gateway" "igw" { vpc_id = aws_vpc.main.id tags = { Project = "example" } } ## Subnet resource "aws_subnet" "public_a" { vpc_id = aws_vpc.main.id cidr_block = "10.0.1.0/24" availability_zone = "ap-northeast-1a" map_public_ip_on_launch = true tags = { Project = "example" } } resource "aws_subnet" "private_a" { vpc_id = aws_vpc.main.id cidr_block = "10.0.2.0/24" availability_zone = "ap-northeast-1a" map_public_ip_on_launch = false tags = { Project = "example" } } ## Root Table resource "aws_route_table" "public_a" { vpc_id = aws_vpc.main.id route { cidr_block = "0.0.0.0/0" gateway_id = aws_internet_gateway.igw.id } tags = { Project = "example" } } resource "aws_route_table" "private_a" { vpc_id = aws_vpc.main.id route { cidr_block = "0.0.0.0/0" nat_gateway_id = aws_nat_gateway.ngw.id } tags = { Project = "example" } } resource "aws_route_table_association" "public_a" { subnet_id = aws_subnet.public_a.id route_table_id = aws_route_table.public_a.id } resource "aws_route_table_association" "private_a" { subnet_id = aws_subnet.private_a.id route_table_id = aws_route_table.private_a.id } ## NAT resource "aws_eip" "ngw" { } resource "aws_nat_gateway" "ngw" { depends_on = [ aws_internet_gateway.igw ] allocation_id = aws_eip.ngw.id subnet_id = aws_subnet.public_a.id tags = { Project = "example" } } ########################################################################### ## Instance data "aws_ami" "amazon_linux_2023" { most_recent = true owners = [ "amazon" ] filter { name = "name" values = [ "al2023-ami-*-x86_64" ] } } resource "aws_instance" "amazon_linux" { ami = data.aws_ami.amazon_linux_2023.id instance_type = "t2.micro" subnet_id = aws_subnet.private_a.id iam_instance_profile = aws_iam_instance_profile.ec2.name vpc_security_group_ids = [ aws_security_group.ec2.id, ] tags = { Project = "example" } } ## Security Group resource "aws_security_group" "ec2" { name = "ec2-sg" vpc_id = aws_vpc.main.id egress { from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = [ "0.0.0.0/0" ] } egress { from_port = 80 to_port = 80 protocol = "tcp" cidr_blocks = [ "0.0.0.0/0" ] } } ## IAM resource "aws_iam_instance_profile" "ec2" { name = "ec2-profile" role = aws_iam_role.ec2.name } resource "aws_iam_role" "ec2" { name = "ec2-role" assume_role_policy = jsonencode ( { Version = "2012-10-17" Statement = [{ Effect = "Allow" Principal = { Service = "ec2.amazonaws.com" } Action = "sts:AssumeRole" }] } ) } resource "aws_iam_role_policy_attachment" "ssm" { role = aws_iam_role.ec2.name policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" } 次に、Route53 Resolverのログを定義します。 Route53 Resolverは VPC 単位で適用されるため、ログの紐付けも VPC 単位となります。 ## Resolver Log resource "aws_cloudwatch_log_group" "example" { name = "/aws/route53/resolver-firewall/example" tags = { Project = "example" } } ### https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_resolver_query_log_config resource "aws_route53_resolver_query_log_config" "example" { name = "example" destination_arn = aws_cloudwatch_log_group.example.arn tags = { Project = "example" } } ### https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_resolver_query_log_config_association resource "aws_route53_resolver_query_log_config_association" "example" { resolver_query_log_config_id = aws_route53_resolver_query_log_config.example.id resource_id = aws_vpc.main.id } Route53 Resolver Firewall を定義します。 使用するリソースは以下の5つです。Route53 Resolverログと同様に、 VPC 単位での紐付けを行います。 aws _route53_resolver_ firewall _config ファイアウォール の基本設定 aws _route53_resolver_ firewall _rule_group 複数の ファイアウォール ルールを紐付けるルールグループ aws _route53_resolver_ firewall _rule_group_association VPC とルールグループを紐付け aws _route53_resolver_ firewall _rule (Allow List / Deny List方式で変動) ドメイン リストを基に許可・拒否・アラートのアクションを設定したルール aws _route53_resolver_ firewall _domain_list (Allow List / Deny List方式で変動) フィルタリングに使用する ドメイン のリスト ## Firewall Config ### https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_resolver_firewall_config resource "aws_route53_resolver_firewall_config" "main" { resource_id = aws_vpc.main.id firewall_fail_open = "DISABLED" } ## Firewall Rule Group ### https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_resolver_firewall_rule_group resource "aws_route53_resolver_firewall_rule_group" "example" { name = "example" tags = { Project = "example" } } ## Firewall Rule Group Association with VPC ### https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_resolver_firewall_rule_group_association resource "aws_route53_resolver_firewall_rule_group_association" "example" { name = "example" vpc_id = aws_vpc.main.id firewall_rule_group_id = aws_route53_resolver_firewall_rule_group.example.id priority = 101 # 100は予約済み } Allow List では、広範囲の ドメイン に対するBlockを定義後に、Allowする設定を追加することで実装することが出来ます。 今回は、SSM接続用の AWS ドメイン と本テックブログ tech.dentsusoken.com へのアクセスのみを許可し、その他の ドメイン をすべて拒否するように設定します。 Allow Listの設定負荷を減らすため firewall_domain_redirection_action = "TRUST_REDIRECTION_DOMAIN" とすることで、初めに解決しに行った ドメイン を信頼し CNAME 後の検証はしない設定にしています。 ## Firewall Domain List ### https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_resolver_firewall_domain_list resource "aws_route53_resolver_firewall_domain_list" "block_example" { name = "block-example" domains = [ "*." ] tags = { Project = "example" } } resource "aws_route53_resolver_firewall_domain_list" "allow_example" { name = "allow-example" domains = [ "tech.dentsusoken.com" , "*.amazonaws.com" , "*.cloudfront.net" ] tags = { Project = "example" } } ## Firewall Rule (Associated with Firewall Rule Group) ### https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_resolver_firewall_rule resource "aws_route53_resolver_firewall_rule" "block_example" { name = "block-example" action = "BLOCK" block_response = "NXDOMAIN" firewall_domain_list_id = aws_route53_resolver_firewall_domain_list.block_example.id firewall_rule_group_id = aws_route53_resolver_firewall_rule_group.example.id firewall_domain_redirection_action = "INSPECT_REDIRECTION_DOMAIN" priority = 200 } resource "aws_route53_resolver_firewall_rule" "allow_example" { name = "allow-example" action = "ALLOW" firewall_domain_list_id = aws_route53_resolver_firewall_domain_list.allow_example.id firewall_rule_group_id = aws_route53_resolver_firewall_rule_group.example.id firewall_domain_redirection_action = "TRUST_REDIRECTION_DOMAIN" priority = 100 } デプロイ後の検証 terraform apply の実行後、エラーなく3分程度で完了しました。 Allow List において、digコマンドによる名前解決を見ると以下の結果となります。 想定通り tech.dentsusoken.com の名前解決は成功し、 www.dentsusoken.com の名前解決は拒否されていることが確認できました。 sh-5.2$ dig tech.dentsusoken.com ; <<>> DiG 9.18.33 <<>> tech.dentsusoken.com ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 62350 ;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 4096 ;; QUESTION SECTION: ;tech.dentsusoken.com. IN A ;; ANSWER SECTION: tech.dentsusoken.com. 300 IN CNAME hatenablog.com. hatenablog.com. 54 IN A 35.75.255.9 hatenablog.com. 54 IN A 54.199.90.60 ;; Query time: 0 msec ;; SERVER: 10.0.0.2#53(10.0.0.2) (UDP) ;; WHEN: Wed Dec 17 12:52:28 UTC 2025 ;; MSG SIZE rcvd: 106 sh-5.2$ dig www.dentsusoken.com ; <<>> DiG 9.18.33 <<>> www.dentsusoken.com ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 17641 ;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 4096 ;; QUESTION SECTION: ;www.dentsusoken.com. IN A ;; Query time: 0 msec ;; SERVER: 10.0.0.2#53(10.0.0.2) (UDP) ;; WHEN: Wed Dec 17 12:52:40 UTC 2025 ;; MSG SIZE rcvd: 48 ローカルでの名前解決として、digコマンドで取得した IPアドレス を基にhostsの書き換えによる ドメイン アクセスを検証します。 Route 53 Resolverを経由せずに名前解決が可能になったことで、拒否されるはずの www.dentsusoken.com への通信が可能になっていることが確認できました。 sh-5.2$ curl https://www.dentsusoken.com curl: (6) Could not resolve host: www.dentsusoken.com sh-5.2$ sudo vi /etc/hosts sh-5.2$ sudo cat /etc/hosts 127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4 ::1 localhost6 localhost6.localdomain6 3.173.219.57 www.dentsusoken.com sh-5.2$ curl https://www.dentsusoken.com <!DOCTYPE html> (中略) <link rel="canonical" href="https://www.dentsusoken.com/" /> <meta name="description" content="株式会社電通総研の公式ホームページです。2024年1月1日に社名をISID(電通国際情報サービス)から変更しました。 お客様の業務課題に対応するソリューションや導入事例のほか、企業情報、IR情報、採用情報等をご紹介しています。" /> <meta property="og:site_name" content="電通総研" /> (中略) </html> AWS Network Firewall の検証 Terraformの実装 以下に示すシンプルな構成で検証します。 初めに VPC およびEC2の定義をします。 VPC に関して、Network Firewall 用のサブネットが存在すること、EC2とNAT ゲートウェイ の通信において Firewall Endpoint を経由するルーティングを設定することに注意が必要です。 terraform { required_version = "~> 1.14.0" required_providers { aws = { version = "6.23.0" source = "hashicorp/aws" } } } provider "aws" { region = "ap-northeast-1" } data "aws_region" "current" {} data "aws_partition" "current" {} data "aws_caller_identity" "current" {} ########################################################################### ## VPC resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" enable_dns_hostnames = true enable_dns_support = true tags = { Project = "example" } } resource "aws_internet_gateway" "igw" { vpc_id = aws_vpc.main.id tags = { Project = "example" } } ## Subnet resource "aws_subnet" "public_a" { vpc_id = aws_vpc.main.id cidr_block = "10.0.1.0/24" availability_zone = "ap-northeast-1a" map_public_ip_on_launch = true tags = { Project = "example" } } resource "aws_subnet" "private_nfw_a" { vpc_id = aws_vpc.main.id cidr_block = "10.0.2.0/24" availability_zone = "ap-northeast-1a" map_public_ip_on_launch = false tags = { Project = "example" } } resource "aws_subnet" "private_a" { vpc_id = aws_vpc.main.id cidr_block = "10.0.3.0/24" availability_zone = "ap-northeast-1a" map_public_ip_on_launch = false tags = { Project = "example" } } ## Root Table resource "aws_route_table" "public_a" { vpc_id = aws_vpc.main.id route { cidr_block = "0.0.0.0/0" gateway_id = aws_internet_gateway.igw.id } route { cidr_block = "10.0.3.0/24" vpc_endpoint_id = tolist ( tolist ( tolist (aws_networkfirewall_firewall.example.firewall_status) [ 0 ] .sync_states) [ 0 ] .attachment) [ 0 ] .endpoint_id } tags = { Project = "example" } } resource "aws_route_table_association" "public_a" { subnet_id = aws_subnet.public_a.id route_table_id = aws_route_table.public_a.id } resource "aws_route_table" "private_nfw_a" { vpc_id = aws_vpc.main.id route { cidr_block = "0.0.0.0/0" nat_gateway_id = aws_nat_gateway.ngw.id } tags = { Project = "example" } } resource "aws_route_table_association" "private_nfw_a" { subnet_id = aws_subnet.private_nfw_a.id route_table_id = aws_route_table.private_nfw_a.id } resource "aws_route_table" "private_a" { vpc_id = aws_vpc.main.id route { cidr_block = "0.0.0.0/0" vpc_endpoint_id = tolist ( tolist ( tolist (aws_networkfirewall_firewall.example.firewall_status) [ 0 ] .sync_states) [ 0 ] .attachment) [ 0 ] .endpoint_id } tags = { Project = "example" } } resource "aws_route_table_association" "private_a" { subnet_id = aws_subnet.private_a.id route_table_id = aws_route_table.private_a.id } ## NAT resource "aws_eip" "ngw" { } resource "aws_nat_gateway" "ngw" { depends_on = [ aws_internet_gateway.igw ] allocation_id = aws_eip.ngw.id subnet_id = aws_subnet.public_a.id tags = { Project = "example" } } ########################################################################### ## Instance data "aws_ami" "amazon_linux_2023" { most_recent = true owners = [ "amazon" ] filter { name = "name" values = [ "al2023-ami-*-x86_64" ] } } resource "aws_instance" "amazon_linux" { depends_on = [ aws_networkfirewall_firewall.example ] ami = data.aws_ami.amazon_linux_2023.id instance_type = "t2.micro" subnet_id = aws_subnet.private_a.id iam_instance_profile = aws_iam_instance_profile.ec2.name vpc_security_group_ids = [ aws_security_group.ec2.id, ] tags = { Project = "example" } } ## Security Group resource "aws_security_group" "ec2" { name = "ec2-sg" vpc_id = aws_vpc.main.id egress { from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = [ "0.0.0.0/0" ] } egress { from_port = 80 to_port = 80 protocol = "tcp" cidr_blocks = [ "0.0.0.0/0" ] } } ## IAM resource "aws_iam_instance_profile" "ec2" { name = "ec2-profile" role = aws_iam_role.ec2.name } resource "aws_iam_role" "ec2" { name = "ec2-role" assume_role_policy = jsonencode ( { Version = "2012-10-17" Statement = [{ Effect = "Allow" Principal = { Service = "ec2.amazonaws.com" } Action = "sts:AssumeRole" }] } ) } resource "aws_iam_role_policy_attachment" "ssm" { role = aws_iam_role.ec2.name policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" } Network Firewall の定義をします。 今回は他 VPC への共有もなく、シンプルな構成となるため必要なリソースは以下の4種類となります。 Firewall はサブネット単位で紐付けます。 また、 aws_networkfirewall_logging_configuration のログタイプをALERTに設定することで、ALERTまたは DROP ルールにマッチした通信のみログに出力されるようになります。 aws _networkfirewall_ firewall Network Firewall の実体となるリソース。 VPC やサブネットへの関連付けを行い、ポリシーやログ設定を適用するための起点 aws _networkfirewall_logging_configuration ログ設定 aws _networkfirewall_ firewall _policy (Allow List / Deny List 方式で変動) 1つ以上のルールグループを紐付け、適用する順序や優先度を設定可能なポリシー aws _networkfirewall_rule_group (Allow List / Deny List 方式で変動) 1つ以上のステートレス/ステートフルルールを設定可能なルールグループ ## Network Firewall (+ Firewall Endpoint) ### https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/networkfirewall_firewall#enabled_analysis_types-1 resource "aws_networkfirewall_firewall" "example" { name = "example" firewall_policy_arn = aws_networkfirewall_firewall_policy.example.arn vpc_id = aws_vpc.main.id enabled_analysis_types = [] # HTTP、HTTPS通信の解析をしてレポートを出力、ドメインリスト作成に活用可能 firewall_policy_change_protection = false # 検証のため subnet_change_protection = false # 検証のため subnet_mapping { subnet_id = aws_subnet.private_nfw_a.id } tags = { Project = "example" } } ## Log Config resource "aws_cloudwatch_log_group" "nfw" { name = "/aws/vpc/network-firewall/example" tags = { Project = "example" } } ### https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/networkfirewall_logging_configuration resource "aws_networkfirewall_logging_configuration" "example" { firewall_arn = aws_networkfirewall_firewall.example.arn logging_configuration { log_destination_config { log_destination = { logGroup = aws_cloudwatch_log_group.nfw.name } log_destination_type = "CloudWatchLogs" log_type = "ALERT" # FLOWの場合はPassルールもログに出力 } } } Allow List の場合は、明示的にSTRICT_ORDERかつ drop :establishedにすることでデフォルトがDenyになることから、以下の実装となります。 この時、SSM接続のために AWS サービス用の ドメイン 名を許可しておく必要があります。 ## Firewall Rule Group ### https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/networkfirewall_rule_group resource "aws_networkfirewall_rule_group" "example" { capacity = 100 # ルール毎のキャパシティに合わせて手動設定必要 name = "example" type = "STATEFUL" rule_group { rules_source { rules_source_list { generated_rules_type = "ALLOWLIST" target_types = [ "HTTP_HOST" , "TLS_SNI" ] targets = [ "tech.dentsusoken.com" , ".amazonaws.com" , ".cloudfront.net" ] } } stateful_rule_options { rule_order = "STRICT_ORDER" } } tags = { Project = "example" } } ## Network Firewall Policy ### https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/networkfirewall_firewall_policy#stateful-rule-group-reference resource "aws_networkfirewall_firewall_policy" "example" { name = "firewall-policy-example" firewall_policy { stateless_default_actions = [ "aws:forward_to_sfe" ] stateless_fragment_default_actions = [ "aws:forward_to_sfe" ] stateful_engine_options { rule_order = "STRICT_ORDER" } stateful_default_actions = [ "aws:drop_established" ] stateful_rule_group_reference { priority = 1 resource_arn = aws_networkfirewall_rule_group.example.arn } } tags = { Project = "example" } } デプロイ後の検証 terraform apply の実行後、エラーなく6分程度で完了しました。 Allow List において、 curl コマンドによるサイトへのアクセスを実行すると、以下の結果となります。 想定通り tech.dentsusoken.com へのアクセスは許可され、 www.dentsusoken.com へのアクセスはブロックされていることが確認できました。 sh-5.2$ curl https://tech.dentsusoken.com <!DOCTYPE html> <html lang="ja" data-admin-domain="//blog.hatena.ne.jp" data-admin-origin="https://blog.hatena.ne.jp" data-author="dentsusoken" data-avail-langs="ja en" data-blog="isid.hatenablog.com" data-blog-host="isid.hatenablog.com" data-blog-is-public="1" data-blog-name="電通総研 テックブログ" data-blog-owner="dentsusoken" data-blog-show-ads="" data-blog-show-sleeping-ads="" data-blog-uri="https://tech.dentsusoken.com/" (中略) </html> sh-5.2$ curl --max-time 10 https://www.dentsusoken.com curl: (28) Connection timed out after 10002 milliseconds DNS Firewall と同様に、hostsファイルの書き換えによる ドメイン アクセスも検証してみると、当然ですが DNS 解決の有無に関係なく、最終的なパケットのHost・SNIヘッダでブロックしていることが確認できました。 sh-5.2$ sudo vi /etc/hosts sh-5.2$ sudo cat /etc/hosts 127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4 ::1 localhost6 localhost6.localdomain6 3.173.219.15 www.dentsusoken.com sh-5.2$ curl --max-time 10 https://www.dentsusoken.com curl: (28) Connection timed out after 10002 milliseconds 次に以下の2パターンでスプーフィングを検証しました。 Hostヘッダ詐称: curl http://www.dentsusoken.com -H "Host: tech.dentsusoken.com" SNIヘッダ詐称: curl https://tech.dentsusoken.com --resolve tech.dentsusoken.com:443:3.173.219.15 -H "Host: www.dentsusoken.com" -k いずれのケースでも、Allow List の判定上は許可され、ブロックされない(=フィルタを通過する)ことを確認しました。 一方で、Host ヘッダや SNI の不整合により、HTTP/ TLS の処理上は正しく応答できず、期待したページ表示には至りませんでした。 以上より、スプーフィングによって「許可判定そのもの」を通過できることが分かりました(ただし、通信の成立や正しいページ表示まで保証されるわけではありません)。 sh-5.2$ curl http://www.dentsusoken.com -H "Host: tech.dentsusoken.com" <html> <head><title>301 Moved Permanently</title></head> <body> <center><h1>301 Moved Permanently</h1></center> <hr><center>CloudFront</center> </body> </html> sh-5.2$ curl https://tech.dentsusoken.com --resolve tech.dentsusoken.com:443:3.173.219.15 -H "Host: www.dentsusoken.com" -k <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> (中略) <H1>421 ERROR</H1> <H2>The request could not be satisfied.</H2> <HR noshade size="1px"> The distribution does not match the certificate for which the HTTPS connection was established with. We can't connect to the server for this app or website at this time. There might be too much traffic or a configuration error. Try again later, or contact the app or website owner. (中略) </BODY></HTML> 今回はCloudFrontを使用したWebサイトであることから、CloudFrontが HTTPS 接続時に実装している ドメインフロンティング対策(SNI・Host・証明書・AWSアカウントの整合性検証) が動作した形となります。 この仕組みにより、本検証ではHostヘッダとSNIヘッダの不整合、またはHTTPのHostヘッダが TLS ハンドシェイク時に提示された証明書に含まれていないことが検知され、CloudFront側で正しく防御されました。 本記事の主題であるアウトバウンドセキュリティの観点とは直接関係しませんが、サービスを公開する際には、CloudFront や ALB などのエッジサーバ、リバースプロキシによる前面での防御を組み合わせることが有効であることが分かります。 AWS Network Firewall Proxyの検証 本記事では記事のボリュームを考慮しデプロイの手順は割愛します。 こちらの記事 でNetwork Firewall Proxyのデプロイ手順を含め詳細を記載していますのでご参照お願いします。 以下に示すシンプルな構成( Securing Egress Architectures with Network Firewall Proxy より引用)を作成します。 (プレビュー中のサービスでありTerraform Providerでは未提供のリソースのため、手動での設定も必要となります。) ルールの設定値としては以下になります。 Pre- DNS : ドメイン tech.dentsusoken.com ・ *.amazonaws.com をAllow。デフォルトアクションはDeny。 Pre-Request:ルールなし。デフォルトアクションはAllow。 Post-Response:ルールなし。デフォルトアクションはAllow。 デプロイ後の検証 Allow List において、 curl コマンドによるサイトへのアクセスを実行すると、以下の結果となります。 想定通り tech.dentsusoken.com へのアクセスは許可され、 www.dentsusoken.com へのアクセスはブロックされていることが確認できました。 sh-5.2$ export https_proxy="https://0a776294cb5ae3886.proxy.nfw.us-east-2.amazonaws.com:443" sh-5.2$ export http_proxy="https://0a776294cb5ae3886.proxy.nfw.us-east-2.amazonaws.com:443" sh-5.2$ curl https://tech.dentsusoken.com <!DOCTYPE html> <html lang="ja" data-admin-domain="//blog.hatena.ne.jp" data-admin-origin="https://blog.hatena.ne.jp" data-author="dentsusoken" data-avail-langs="ja en" data-blog="isid.hatenablog.com" data-blog-host="isid.hatenablog.com" data-blog-is-public="1" data-blog-name="電通総研 テックブログ" data-blog-owner="dentsusoken" data-blog-show-ads="" data-blog-show-sleeping-ads="" data-blog-uri="https://tech.dentsusoken.com/" (中略) </html> sh-5.2$ curl https://www.dentsusoken.com curl: (56) CONNECT tunnel failed, response 403 sh-5.2$ DNS Firewall と同様に、hostsファイルの書き換えによる ドメイン アクセスも検証してみると、当然ですが DNS 解決の有無に関係なく、最終的なパケットのHost・SNIヘッダでブロックしていることが確認できました。 sh-5.2$ sudo vi /etc/hosts sh-5.2$ sudo cat /etc/hosts 127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4 ::1 localhost6 localhost6.localdomain6 3.173.219.15 www.dentsusoken.com sh-5.2$ curl https://www.dentsusoken.com curl: (56) CONNECT tunnel failed, response 403 sh-5.2$ 次に以下の2パターンでスプーフィングを検証しました。 Hostヘッダ詐称: curl http://www.dentsusoken.com -H "Host: tech.dentsusoken.com" SNIヘッダ詐称: curl https://tech.dentsusoken.com --resolve tech.dentsusoken.com:443:3.173.219.15 -H "Host: www.dentsusoken.com" -k Hostヘッダ詐称の場合は、Allow List の判定にマッチせずに拒否されたことが確認できました。 SNIヘッダ詐称の場合は、そもそも名前解決がプロキシ側で発生するため実質的に curl https://tech.dentsusoken.com -H "Host: www.dentsusoken.com" -k と同じ挙動となります。そのため、 tech.dentsusoken.com への名前解決が発生しながら、Hostヘッダが www.dentsusoken.com になっていることで409エラーを返したと考えられます。 sh-5.2$ curl -v http://www.dentsusoken.com -H "Host: tech.dentsusoken.com" * Uses proxy env variable http_proxy == 'https://0a776294cb5ae3886.proxy.nfw.us-east-2.amazonaws.com:443' * Host 0a776294cb5ae3886.proxy.nfw.us-east-2.amazonaws.com:443 was resolved. * IPv6: (none) * IPv4: 10.0.1.29 * Trying 10.0.1.29:443... * TLSv1.3 (OUT), TLS handshake, Client hello (1): (中略) * TLSv1.3 (IN), TLS handshake, Newsession Ticket (4): * Request completely sent off < HTTP/1.1 403 Forbidden (中略) Forbidden * Connection #0 to host 0a776294cb5ae3886.proxy.nfw.us-east-2.amazonaws.com left intact sh-5.2$ curl -v https://tech.dentsusoken.com --resolve tech.dentsusoken.com:443:3.173.219.15 -H "Host: www.dentsusoken.com" -k * Added tech.dentsusoken.com:443:3.173.219.15 to DNS cache * Uses proxy env variable https_proxy == 'https://0a776294cb5ae3886.proxy.nfw.us-east-2.amazonaws.com:443' * Host 0a776294cb5ae3886.proxy.nfw.us-east-2.amazonaws.com:443 was resolved. * IPv6: (none) * IPv4: 10.0.1.29 * Trying 10.0.1.29:443... * ALPN: curl offers http/1.1 * TLSv1.3 (OUT), TLS handshake, Client hello (1): (中略) < HTTP/1.1 409 Conflict < Connection: close < HTTP/1.1 400 Bad Request Connection: close * TLSv1.3 (IN), TLS alert, close notify (256): * shutting down connection #0 * TLSv1.3 (OUT), TLS alert, close notify (256): * TLSv1.3 (IN), TLS alert, close notify (256): * TLSv1.3 (OUT), TLS alert, close notify (256): sh-5.2$ ただし、本検証のように Network Firewall Proxy を単体で利用している場合、 --noproxy オプション等を使用しプロキシを経由しない通信をした場合には、Network Firewall Proxyのルールを無視して通信しに行くことが可能です。 そのため、Network Firewall Proxy単体ではなく他の ファイアウォール 系サービスとの組み合わせが重要となります。 まとめ ドメイン ベースのアウトバウンド 通信制 御は、サブネット単位での制御要件がない限り、 コストと防御効果(スプーフィングにも対応可能)のバランスに優れる Route 53 Resolver DNS Firewall を主軸に据える構成が推奨 だと考えられます。 そのうえで、hosts ファイルの書き換え、IP 直打ち、独自 DNS の利用などにより Route 53 Resolver を経由しない通信が成立し得る点を踏まえ、 Network Firewall や Network Firewall Proxyを併用して補完的にカバー することが重要です。 おわりに 本記事では、 AWS における具体的な ドメイン ベース 通信制 御を検証してみました。 実運用においては、 マネージドルールや推奨される Suricata ルールの導入、ログの活用による継続的な改善 が重要となります。 今後も、実際の運用や検証を通じて理解を深めていきたいと思います。 余談ですが、Network Firewall のapply後に削除することを忘れてしまい8日間放置してしまいました。 シングルAZ構成で Firewall エンドポイントは1つだけでしたが、それでも エンドポイント料金 だけで約76$(12,000円程)のコストが発生してしまいました。 apply 後の削除忘れには改めて注意が必要ですね。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @sakae.katsuto レビュー: Ishizawa Kento (@kent) ( Shodo で執筆されました )
.mermaid { background-color: #ffffff !important; } import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs'; mermaid.initialize({ startOnLoad: true }); はじめに こんにちは。 エンタープライズ 第三本部 データマネジメントユニット マーケティング IT部の藤澤です。 先日、 API プラットフォームのリーディング企業であるKong株式会社様のパートナー向け認定資格プログラム「 Partner Delivery Specialist 」に参加し、無事に 電通 総研第一号の認定を取得することができました。 この資格の認定には、対面で行われた2日間のワークショップへの参加以外にも、事前のオンライン学習、Kong Gateway Associate資格の取得が必要でかなりハードでした。 ワークショップではインフラ周りの知識もかなり必要とされたのですが、普段はアプリケーション開発を中心に行っているため、苦労する場面が多々ありました。 しかし今回のプログラムを通じて、普段の業務であまり触れることがなかったインフラ周りの知識やAPIOpsの思想などについて体系的に学ぶことができ、非常に有意義な経験となりました。 本記事では、Kongの概要に少し触れた後、ワークショップでも取り扱った、Kong Gateway で Kubernetes のマイクロサービスをプロキシする方法について共有させていただきます。 なお本記事の内容は、すべて2025年12月時点のものですのでご了承ください。 対象読者 Kongを触ったことがない方、 Kubernetes について クラウド サービスの試験などで名前は知っているが実際に使ったことがない方。 私自身がそうだったので、詰まった点、学びになった点や、実際のニーズに対してどのように活用できるのかについて詳しく書きました。 技術的な内容については、普段Kongや Kubernetes を使っている方にとっては簡単すぎるかもしれません。 はじめに 対象読者 Kongの概要 題材 EKSクラスターとサンプルバックエンドAPIをセットアップする コマンドインストール Kubernetesクラスターの作成 Bookinfoのデプロイ Kong のインフラをセットアップする CPのセットアップ DPのデプロイとCP・DP間連携 Kong Gatewayを設定する Gateway Serviceの作成 Routeの作成 動作確認 片付け おわりに Kongの概要 Kongは、マイクロサービス アーキテクチャ における API ゲートウェイ として広く採用されているプラットフォームです(日本ではデジタル庁から推奨 API ゲートウェイ に認定されています)。 API 開発ライフサイクルの管理機能、セキュリティ、認証、レート制限、ロギングなどの API ゲートウェイ 機能を統合的に提供し、複雑な API 運用を効率化します。 Kongエコシステムの代表的な コンポーネント は以下の3つです。 Kong Insomnia : API の設計・開発・テストを効率化するための強力なクライアントツールです。Postmanの競合にあたり、デザインファーストな開発を支援します。 Kong Gateway : 外部からのリクエストを受け付ける、世界で最も利用されている オープンソース の API ゲートウェイ です。高速な通信処理と豊富な プラグイン 拡張が特徴です。 Kong Mesh : マイクロサービス間の複雑な通信(内部通信)を制御・可視化するためのサービスメッシュです。サービス間の暗号化や トラフィック 制御を担います。 出典: Kong Inc. 公式サイト より引用 そして、これらの コンポーネント を統合的に管理・運用できる枠組みを SaaS として提供している Kong Konnect という製品があります。 Konnectを利用することで、Insomniaで設計した API スペックの自動公開(開発者ポータル)、 Gateway /Meshの統合監視、チーム間でのコラボレーション機能などが提供され、 API ライフサイクル全体の効率化を実現できます。 本ブログでは、上記のうち主に Kong Gateway と Kong Konnect を使います。 Kong Gateway の アーキテクチャ には、主に以下の3種類があります。 DB lessモード : Kong Gateway の設定情報を、各ノードごとに YAML 形式の設定ファイルで管理する軽量なモードです。 出典: Kong Inc. 公式サイト より引用 Traditionalモード : PostgreSQL などのデータベースを使用して設定を管理するモードで、大規模な環境や複数のノードを運用する際に適しています。 出典: Kong Inc. 公式サイト より引用 Hybridモード : データベースと接続して設定を管理する専門のコン トロール プレーン(CP)と実際に トラフィック をさばく専門のデータプレーン(DP)を分離し、管理を一元化しながら トラフィック 処理を分散できる構成です。スケーラビリティとセキュリティの両面で大きなメリットが得られます。 出典: Kong Inc. 公式サイト より引用 本記事では、 Hybrid アーキテクチャ を採用し、CPはKonnectで管理します 。 この場合、CPのノード作成、DBとの接続などの手順は不要で、 API の開発者が気にする必要はありません。各 API サーバとの通信が可能な場所にDPのノードを配置し、Konnectとの通信を確立すればOKです。 API ゲートウェイ のポリシーを変更する際は、Konnectのエンドポイントに対して設定を行えば、自動的に各DPに設定が反映されます。 これにより、オンプレミス・ クラウド のハイブリッド構成やマルチ クラウド 構成を柔軟に構築することが可能となり、ビジネス要件に応じた最適なインフラ設計を実現できます。 出典: Kong Inc. 公式サイト より引用 Kongそのものの説明はここまでにしておきます。詳しくは 公式ドキュメント などを参照してください。 題材 サン プルバック エンド API として、Istioの Bookinfo というマイクロサービスを使用します。 Bookinfoは4つのマイクロサービスで構成されており、そのうち productpage サービスの /api/v1/products 以下のエンドポイントはBackend For Frontend(BFF)として実装されています。 今回は Kubernetes クラスタ ーにBookinfoとKong Gateway をデプロイして、BookinfoのBFFに対してプロキシしてみます。 具体的な案件のシナリオとしては、レガシーなモノリシックアプリケーションからマイクロサービス アーキテクチャ へ段階的に移行していく際に、まずは4つのマイクロサービスを Kubernetes に切り出した、というものが想定されます。 本ブログでは1つのサービスにしかプロキシしませんが、今後他のマイクロサービスへのプロキシ機能も順次追加していくことを考えると、公開する各サービスに認証認可やログ記録などの共通処理を重複して実装するよりも、共通基盤で一元管理する方が保守性が高く、セキュリティの一貫性も保ちやすいです。 これが、まさにKong Gateway の得意とするところです。 ここからは以下の手順で実装します。 EKS クラスタ ーとサン プルバック エンド API をセットアップする Kong のインフラをセットアップする Kong Gateway を設定する EKS クラスタ ーとサン プルバック エンド API をセットアップする 今回は、 AWS のElastic Kubernetes Service(EKS)上に Kubernetes クラスタ ーを立てます。 ローカルでDocker Desktop, Kind, minikubeあたりを使ってもいいのですが、Podのオートスケーリングなどをやりたかったので、設定が簡単なEKSにしました。 ※ 1日あたり$7程度かかるので、作業が終わったら クラスタ ーを削除することをおすすめします。 コマンドインストール まず、以下のコマンドをローカルにインストールします。 コマンド 説明 kubectl Kubernetes クラスタ ーを操作するための コマンドライン ツール helm Kubernetes のパッケージマネージャー k9s Kubernetes クラスタ ーの状態をターミナル上で視覚的に確認・操作できるツール aws AWS のサービスを操作する コマンドライン ツール eksctl Amazon EKS クラスタ ーを簡単に作成・管理できる コマンドライン ツール hey HTTP負荷テストツール Mac の場合、以下のコマンドでインストールできます。 brew install kubernetes-cli helm k9s awscli eksctl hey aws コマンドを使う際、IAMユーザーのクレデンシャルが必要なので、あらかじめ AWS Management ConsoleでIAMユーザーを作成してポリシーをアタッチし、アクセスキーとシークレットアクセスキーを発行しておきます。 ポリシーは検証用なので AdministratorAccess にしてしまいました。 以下のコマンドで、クレデンシャルなどを登録します。 aws configure 実行すると、4つの質問が順番に表示されるので入力してEnterを押します。 AWS Access Key ID [ None ] : < 取得したアクセスキーIDを貼り付け > AWS Secret Access Key [ None ] : < 取得したシークレットキーを貼り付け > Default region name [ None ] : ap-northeast-1 Default output format [ None ] : json 以下のコマンドで、設定したクレデンシャルが紐づくIAMユーザーが出力されればOKです。 aws sts get-caller-identity Kubernetes クラスタ ーの作成 それでは、EKSに クラスタ ーを作成します。 Management Consoleから作成すると、EC2や VPC などの設定が大変なので、 eksctl コマンドで一気に作ります。 15分程度待ちます。 ※ eksctl を使っておくと、 クラスタ ーを削除するときにも関連リソースが全部消えるので、こちらを推奨します。 eksctl create cluster \ --name kong-techblog \ --region ap-northeast-1 \ --version 1 . 34 \ --nodegroup-name kong-techblog-spot \ --node-type t3.medium \ --nodes 3 \ --spot \ --managed オプションについては、以下のとおりです。 オプションの説明 オプション 説明 name クラスタ ー名なので、何でもOKです。 region 東京リージョンにしました。 version Kubernetes クラスタ ーのバージョンです。 nodegroup-name ノードグループ名なので、何でもOKです。 node-type t2.mircoでやったら、pod数制限にかかり起動しませんでした。 nodes ノード数です。 spot スポット インスタンス を使います。本番ではNGですが、検証用なので安いほうがいいと思います。 managed Managed Instance Groupを使います。 実行が終わったら、Management ConsoleでEKS クラスタ ーが正常に作成されたことを確認します。 ※東京リージョンが選択されていることを確認してください。 kong-techblog クラスタ ーが作成されています。 作成された クラスタ ーをクリックし、詳細画面を開いて[コンピューティング]タブを開きます。 Nodeは1つの計算リソースを表す Kubernetes の概念です。 今回で言うと、EC2 インスタンス が1つのNodeに相当します。 先ほど eksctl コマンドで クラスタ ーを作成する際に node-type=t3.medium と nodes=3 オプションを設定したので、EC2にt3.mediumの インスタンス が3つ立ち上げられています。 次に[リソース]タブを開くと、デフォルトで「リソースタイプ > ワークロード > ポッド」が選択されています。 ここに表示されているのは、コンテナをグループ化して管理するための最小単位となるPodという Kubernetes の概念です。 Podは複数のコンテナからなり、いずれかのNode上で実行され、Pod単位でスケーリングやヘルスチェックなどが行われます。 後でBookinfoやKong Gateway をデプロイすると、Podとしてデプロイされます。 その他の Kubernetes のリソースについての説明は省略します。詳しくは他のブログや公式ドキュメントを参照してください。 Bookinfoのデプロイ さて、 クラスタ ーがデプロイできたので、次にBookinfoをデプロイします。 デプロイに関する定義は、 yaml ファイルに記述します(Bookinfo公式の yaml ファイルを一部改変したものです)。 bookinfo.yaml ################################################################################################## # Namespace ################################################################################################## apiVersion : v1 kind : Namespace metadata : name : bookinfo --- ################################################################################################## # Details service ################################################################################################## apiVersion : v1 kind : Service metadata : name : details namespace : bookinfo labels : app : details service : details spec : ports : - port : 9080 name : http selector : app : details --- apiVersion : apps/v1 kind : Deployment metadata : name : details-v1 namespace : bookinfo labels : app : details version : v1 spec : replicas : 1 selector : matchLabels : app : details version : v1 template : metadata : labels : app : details version : v1 spec : containers : - name : details image : docker.io/istio/examples-bookinfo-details-v1:1.20.1 imagePullPolicy : IfNotPresent ports : - containerPort : 9080 securityContext : runAsUser : 1000 --- ################################################################################################## # Ratings service ################################################################################################## apiVersion : v1 kind : Service metadata : name : ratings namespace : bookinfo labels : app : ratings service : ratings spec : ports : - port : 9080 name : http selector : app : ratings --- apiVersion : apps/v1 kind : Deployment metadata : name : ratings-v1 namespace : bookinfo labels : app : ratings version : v1 spec : replicas : 1 selector : matchLabels : app : ratings version : v1 template : metadata : labels : app : ratings version : v1 spec : containers : - name : ratings image : docker.io/istio/examples-bookinfo-ratings-v1:1.20.1 imagePullPolicy : IfNotPresent ports : - containerPort : 9080 securityContext : runAsUser : 1000 --- ################################################################################################## # Reviews service ################################################################################################## apiVersion : v1 kind : Service metadata : name : reviews namespace : bookinfo labels : app : reviews service : reviews spec : ports : - port : 9080 name : http selector : app : reviews --- apiVersion : apps/v1 kind : Deployment metadata : name : reviews-v1 namespace : bookinfo labels : app : reviews version : v1 spec : replicas : 1 selector : matchLabels : app : reviews version : v1 template : metadata : labels : app : reviews version : v1 spec : containers : - name : reviews image : docker.io/istio/examples-bookinfo-reviews-v1:1.20.1 imagePullPolicy : IfNotPresent env : - name : LOG_DIR value : "/tmp/logs" ports : - containerPort : 9080 volumeMounts : - name : tmp mountPath : /tmp - name : wlp-output mountPath : /opt/ibm/wlp/output securityContext : runAsUser : 1000 volumes : - name : wlp-output emptyDir : {} - name : tmp emptyDir : {} --- apiVersion : apps/v1 kind : Deployment metadata : name : reviews-v2 namespace : bookinfo labels : app : reviews version : v2 spec : replicas : 1 selector : matchLabels : app : reviews version : v2 template : metadata : labels : app : reviews version : v2 spec : containers : - name : reviews image : docker.io/istio/examples-bookinfo-reviews-v2:1.20.1 imagePullPolicy : IfNotPresent env : - name : LOG_DIR value : "/tmp/logs" ports : - containerPort : 9080 volumeMounts : - name : tmp mountPath : /tmp - name : wlp-output mountPath : /opt/ibm/wlp/output securityContext : runAsUser : 1000 volumes : - name : wlp-output emptyDir : {} - name : tmp emptyDir : {} --- apiVersion : apps/v1 kind : Deployment metadata : name : reviews-v3 namespace : bookinfo labels : app : reviews version : v3 spec : replicas : 1 selector : matchLabels : app : reviews version : v3 template : metadata : labels : app : reviews version : v3 spec : containers : - name : reviews image : docker.io/istio/examples-bookinfo-reviews-v3:1.20.1 imagePullPolicy : IfNotPresent env : - name : LOG_DIR value : "/tmp/logs" ports : - containerPort : 9080 volumeMounts : - name : tmp mountPath : /tmp - name : wlp-output mountPath : /opt/ibm/wlp/output securityContext : runAsUser : 1000 volumes : - name : wlp-output emptyDir : {} - name : tmp emptyDir : {} --- ################################################################################################## # Productpage services ################################################################################################## apiVersion : v1 kind : Service metadata : name : productpage namespace : bookinfo labels : app : productpage service : productpage spec : # type: LoadBalancer ports : - port : 9080 name : http # - port: 80 # targetPort: 9080 # name: http selector : app : productpage --- apiVersion : apps/v1 kind : Deployment metadata : name : productpage-v1 namespace : bookinfo labels : app : productpage version : v1 spec : replicas : 1 selector : matchLabels : app : productpage version : v1 template : metadata : annotations : prometheus.io/scrape : "true" prometheus.io/port : "9080" prometheus.io/path : "/metrics" labels : app : productpage version : v1 spec : containers : - name : productpage image : docker.io/istio/examples-bookinfo-productpage-v1:1.20.1 imagePullPolicy : IfNotPresent ports : - containerPort : 9080 volumeMounts : - name : tmp mountPath : /tmp securityContext : runAsUser : 1000 volumes : - name : tmp emptyDir : {} --- デプロイするには、この内容を ./bookinfo.yaml に保存して、以下のコマンドを実行します。 kubectl apply -f ./bookinfo.yaml 30秒ほど経ったら、以下のコマンドでPodの状態を確認します。 kubectl get pods -n bookinfo 以下のように、6つのPodが READY: 1/1 , STATUS: RUNNING となっていることが確認できます。 READY: 0/1 や STATUS: ContainerCreating などとなっている場合は、もう少し待機します。 STATUS: Pending の場合は、Podに対してNodeのリソースが不足している可能性があるので、使用するEC2 インスタンス タイプを見直すか、ノード数を増やすことを検討します。 まだ productpage は クラスタ ー外部からアクセスできるようにしていないので、 /api/v1/products にアクセスするには、 クラスタ ー内部からリクエストを送る必要があります。 kubectl run curl-test --image = curlimages/curl --rm -it --restart = Never -- \ curl http://productpage.bookinfo:9080/api/v1/products このコマンドでは、一時的に curl-test というPodを立てて、そこから curl コマンドを叩いて、終わったらPodを削除しています。 このようにレスポンスが返って来ればOKです。 これでEKS クラスタ ーとサン プルバック エンド API (Bookinfo)のデプロイは完了です! Kong のインフラをセットアップする 前置きが長かったですが、ここから本題の Kong の話になります。 CPのセットアップ まず、 Kong Konnect にサインアップします。 ※(2025年12月現在)サインアップ後30日間は、基本的な機能を無料で使用することができます。 サインアップ後、ログイン直後にはこのような画面が表示されます。 左のメニューで、[ API Gateway ]をクリックして、右上の[New gateway ]で新しく ゲートウェイ を作成します。 ポップアップで、[Self-Managed Hybrid]を選択して、 ゲートウェイ の名前を決め、右下の[Next Step]をクリックします。 クリックすると画面遷移します。これで CP の作成が完了しました。 DPのデプロイとCP・DP間連携 次に、EKSにDPをデプロイし、CPと連携させる設定を行います。 この画面に遷移しているはずなので、左側のメニューの[Data Plane Nodes]をクリックします。 [Configure data plane]をクリックします。 ポップアップで、 Gateway Versionは Self-Managed Gateway 3.12 を、Platformは Kubernetes を選択し、下にスクロールします。 「Advanced Kubernetes Setup」が表示されるので、基本的にはその手順に従って設定を進めます。 以下のコマンドで、Kong Gateway 関連のPodをデプロイする kong Namespaceを作成し、Helm リポジトリ にKong Gateway を登録します(手順2 Set up Helm)。 kubectl create namespace kong helm repo add kong https://charts.konghq.com helm repo update この時点では、まだKong Gateway のインストールは行われていません。Helmチャートという、 Kubernetes のデプロイ定義の雛形のようなものをダウンロードして登録しただけです。 Helmのコマンドは、初回実行時少し時間がかかります。 次に、CPとDPがmTLSにより安全に接続できるよう、CPから認証情報を払い出します(手順3 Generate certificates)。 [Generate certificate]ボタンをクリックすると「Cluster Certificate」と「Certificate Key」が表示されるので、それぞれ新しく ./tls.crt と ./tls.key ファイルを作成し、コピペして保存します。 以下のコマンドで、 Kubernetes クラスタ ーにSecretリソースを作成します。 kubectl create secret tls kong-cluster-cert -n kong --cert = ./tls.crt --key = ./tls.key 続いて、Helmチャート(雛形)の変数部分を定義した values.yaml ファイルを作成します(手順4 Configuration parameters)。 後で詳しく説明しますが、Kong Gateway のPodのオートスケールの確認のため、 Konnectに表示されているものから少し変更しています。 以下の設定をコピペし、 env.cluster_control_plane 、 env.cluster_server_name 、 env.cluster_telemetry_endpoint 、 env.cluster_telemetry_server_name をKonnectが自動生成したvalues. yaml の設定と同じものに書き換えて、 ./values.yaml に保存します。 values.yaml image : repository : kong/kong-gateway tag : "3.12" secretVolumes : - kong-cluster-cert admin : enabled : false env : role : data_plane database : "off" cluster_mtls : pki cluster_control_plane : <Konnectが自動生成したvalues.yamlの設定と同じにする> cluster_dp_labels : "type:docker-kubernetesOS" cluster_server_name : <Konnectが自動生成したvalues.yamlの設定と同じにする> cluster_telemetry_endpoint : <Konnectが自動生成したvalues.yamlの設定と同じにする> cluster_telemetry_server_name : <Konnectが自動生成したvalues.yamlの設定と同じにする> cluster_cert : /etc/secrets/kong-cluster-cert/tls.crt cluster_cert_key : /etc/secrets/kong-cluster-cert/tls.key lua_ssl_trusted_certificate : system konnect_mode : "on" vitals : "off" nginx_worker_processes : "1" upstream_keepalive_max_requests : "100000" nginx_http_keepalive_requests : "100000" proxy_access_log : "off" dns_stale_ttl : "3600" router_flavor : expressions ingressController : enabled : false installCRDs : false resources : requests : cpu : 500m memory : "512Mi" limits : cpu : 2 memory : "2Gi" autoscaling : enabled : true minReplicas : 1 maxReplicas : 5 metrics : - type : Resource resource : name : cpu target : type : Utilization averageUtilization : 10 behavior : scaleDown : stabilizationWindowSeconds : 30 manager : enabled : false その後、以下のコマンドを実行し、Kong Gateway をEKSにデプロイします。 helm install my-kong kong/kong -n kong --skip-crds --values ./values.yaml このコマンドは、Kong公式のHelmチャート( Kubernetes のデプロイ定義の雛形)に対して、その変数部分を定義した values.yaml を埋め込み、 Kubernetes のデプロイ定義を完成させて、それをもとに Kubernetes クラスタ へのデプロイを実行します。 うまくいくと、ポップアップの一番下が「Data Plane Node has been found」に変わり、右下の[Done]ボタンが押せるようになります。 これでKongのインフラのセットアップは完了です! ここまでで構築してきたインフラの構成図は以下のとおりです。 %%{init: {'theme':'base', 'themeVariables': {'fontSize':'18px'}}}%% graph LR Users["🌐<br/><b>ユーザー</b>"] Developer["👨‍💻<br/><b>開発者</b>"] subgraph Konnect["<b>Kong Konnect (SaaS)</b>"] CP["<b>Control Plane</b><br/>設定管理"] end subgraph AWS["<b>AWS (ap-northeast-1)</b>"] NLB["<b>Network Load Balancer</b>"] subgraph EKS["<b>EKS Cluster: kong-techblog-cluster</b>"] subgraph NS_Kong["<b>Namespace: kong</b>"] KongPods["<b>Kong Gateway Pods</b><br/>Data Plane<br/>HPA: 1-5 replicas"] end subgraph NS_Bookinfo["<b>Namespace: bookinfo</b>"] ProductPage["<b>productpage</b><br/>:9080"] Details["<b>details</b><br/>:9080"] Reviews["<b>reviews</b><br/>:9080"] Ratings["<b>ratings</b><br/>:9080"] ProductPage --> Details ProductPage --> Reviews Reviews --> Ratings end end end Developer -.->|"Admin API<br/>(ルート/サービス/プラグイン)"| CP Users -->|HTTP| NLB NLB --> KongPods KongPods --> ProductPage KongPods -.->|mTLS 認証<br/>設定同期| CP style Konnect fill:#e1f5ff,stroke:#333,stroke-width:3px style AWS fill:#fff5e1,stroke:#333,stroke-width:3px style EKS fill:#ffffff,stroke:#333,stroke-width:2px style NS_Kong fill:#ffe1f5,stroke:#333,stroke-width:2px style NS_Bookinfo fill:#e1ffe1,stroke:#333,stroke-width:2px style KongPods fill:#ffd700,stroke:#333,stroke-width:2px style ProductPage fill:#99ff99,stroke:#333,stroke-width:2px style Details fill:#99ff99,stroke:#333,stroke-width:2px style Reviews fill:#99ff99,stroke:#333,stroke-width:2px style Ratings fill:#99ff99,stroke:#333,stroke-width:2px style NLB fill:#ff9999,stroke:#333,stroke-width:2px style CP fill:#99ccff,stroke:#333,stroke-width:2px style Users fill:#ffffff,stroke:#333,stroke-width:2px style Developer fill:#ffccff,stroke:#333,stroke-width:2px 自動的にNetwork Load Balancerが追加されていますが、これはKongのHelmチャートで、Kong Gateway のDPをデプロイすると自動的に作成されるように設定されています。EKS環境で実行すると、EC2にLoad Balancerが自動的にプロビジョニングされ、インターネットからの トラフィック を受け付けられるようになります。 またKong Gateway のPodのオートスケール(Horizontal Pod Autoscaler:HPA)についてですが、先ほどの values.yaml に以下の記述を追加していました。 resources : requests : cpu : 500m memory : "512Mi" limits : cpu : 2 memory : "2Gi" autoscaling : enabled : true minReplicas : 1 maxReplicas : 5 metrics : - type : Resource resource : name : cpu target : type : Utilization averageUtilization : 10 behavior : scaleDown : stabilizationWindowSeconds : 30 これは、初めはKong Gateway Podを1つだけデプロイしておき、PodのCPU使用率が10%を超えると、最大5個までPodをスケールアウトせよ、という命令です(あとでHPAを試すために、各Podで使用できる最大のCPUリソースと 閾値 となる利用率は低めに設定しています)。 また、30秒間CPU使用率が10%を下回った状態が続くと、そのPodは自動的にスケールインされます。 Kong Gateway を設定する CPで ゲートウェイ の設定に変更を加えるには、Konnectで ゲートウェイ ごとに払い出されるAdmin API というエンドポイントを使用します。 Konnectの GUI 上で設定を変更すると、裏側でAdmin API が叩かれます。 CPとDPはmTLSで接続され、CPで ゲートウェイ の設定に変更を加えると、順次DPにも設定が反映されていきます。 ちなみにこの仕組みではDP( Kubernetes )からCP(Konnect)に向けてポーリングされるので、 Kubernetes へのインバウンド トラフィック を許可する必要がないセキュアな設計になっています。 GUI から設定して、インターネット上からBookinfoを叩けるようにします。 Gateway Serviceの作成 左側のメニューから[ Gateway Services]を選択します。 [New gateway service]をクリックします。 Full URL にはKong Gateway DPから見た、BookinfoのアプリケーションエンドポイントURLを入力します。 今回の場合は、 http://productpage.bookinfo:9080/api/v1 を入力します( ドメイン の部分は、 <Service名>.<Namespace名> とすれば、 クラスタ ー内の DNS Podが名前解決してServiceの IPアドレス を引いてきてくれます)。 Name は適当に bookinfo-v1 として、[Save]をクリックします。 なぜ http://productpage.bookinfo:9080 にしないかというと、後でRouteの Path に誤って / を登録してしまうと、 /api/v1 以下以外の /productpage や /login エンドポイントなどにアクセスできるようになってしまうためです。確実に /api/v1 以下だけにアクセスを限定するために、このようにしました。 Routeの作成 次に、遷移先のページでRouteを設定します。 左のメニューの[Routes]ではなく、画面中央に並んでいる[Routes]タブをクリックし 、[New Route]を選択します。 ※左のメニューの[Routes]からだと、別途作成したRouteを、先ほど作成した Gateway Serviceに紐づける作業が必要になります。 設定画面で、 Name を products 、Route ConfigurationはBasicで、 Path に /products を入力します。 Strip Path からチェックを外して[Save]をクリックします。 Routeは、条件に当てはまるリクエストをDPが受けたときに、関連付けてある Gateway Serviceにリクエストをプロキシします。 今回は Path に /products を設定しているので、 DPに対して /products から始まるパスでリクエストが来た場合 に、関連付けた bookinfo-v1 Gateway Serviceにプロキシされます。 Strip Path にチェックをしていると、このときマッチした部分を削除してから、 Gateway Serviceにプロキシします。 Gateway Serviceは、設定したURL productpage.bookinfo:9080/api/v1 の 末尾にRouteから送られてきたパスをくっつけて 、バックエンドに転送します。 以下に、どのようなパスのDPへのリクエストに対して、どのようなパスでDPがバックエンド(Bookinfo)にプロキシするかを示します。 DPへのリクエストのパス Strip Path: true の場合 Strip Path: false の場合 /products productpage.bookinfo:9080/api/v1 ( 404 Not Found ) productpage.bookinfo:9080/api/v1/products (200 OK) /products/1 productpage.bookinfo:9080/api/v1/1 ( 404 Not Found ) productpage.bookinfo:9080/api/v1/products/1 (200 OK) /products/1/ratings productpage.bookinfo:9080/api/v1/1/ratings ( 404 Not Found ) productpage.bookinfo:9080/api/v1/products/1/ratings (200 OK) /api/v1/products/1/ratings Serviceにプロキシされない( 404 Not Found ) Serviceにプロキシされない( 404 Not Found ) DPへのリクエストのパスにも /api/v1 を含めたい場合は、いくつか方法が考えられますが、例えばRouteで Strip Path: true として、 Path に /api/v1 を設定すると、Serviceはそのままで動きます。 動作確認 これでインターネット上からKong Gateway 経由で、Bookinfoを叩けるようになりました。 先ほどの ロードバランサー のURLを取得し、アクセスしてみます。 このコマンドでKong Gateway が使用しているLoadBalancerの EXTERNAL-IP を確認し、コピーしておきます(以下、 your-domain.ap-northeast-1.elb.amazonaws.com とします)。 kubectl get svc -n kong ブラウザや curl で http://your-domain.ap-northeast-1.elb.amazonaws.com/products にアクセスし、以下のように返ってくれば成功です! たくさんリクエストを投げてKong Gateway DPのCPU負荷を上げ、HPAが動作するかどうか見てみます。 k9s コマンドで、Podの状態をリアルタイムで監視できます。 k9s -n kong 今は1Podしか起動していません。 別のターミナルを開き、以下のコマンドを実行し、負荷をかけます。 hey -z 120s -c 100 http://your-domain.ap-northeast-1.elb.amazonaws.com/products しばらくすると、k9s側でPodの状態が更新されていることを確認できます。 5Podまでスケールアウトしました。 hey コマンドの実行が完了してから30秒ほど経つと、スケールインが始まります。 再び1Podに戻りました。 片付け EKS クラスタ ーを削除します。EKS クラスタ ーを作成したときに eksctl を使っていれば、関連リソースが一括ですべて削除されます。 15分ほど待ちます。 eksctl delete cluster --name kong-techblog 念のため、Management ConsoleからNAT ゲートウェイ 、 ロードバランサー 、EC2 インスタンス 、EKS クラスタ ーが削除されていることを確認しておいてください。 これらの課金が大半なので、削除されていれば高額な請求が来ることはありません。 おわりに 本記事では、Kong Gateway で Kubernetes のマイクロサービスをプロキシする方法について解説しました。 今回は AWS 上の Kubernetes 環境の1つのサービスにしかプロキシしませんでしたが、プロキシ先のサービスが増えたり、マイクロサービスのインフラ環境がハイブリッドやマルチ クラウド になったりしても、大きな変更なくスケーラブルに対応できることがKong Gateway の強みです。 また、本記事では書ききれませんでしたが、 yaml ファイルによる宣言的定義をもとにAdmin API を実行してくれるdecK コマンドライン や、 Kubernetes 設定ファイルから自動的にKong Gateway の設定を構築するKong Ingress Controller、 API 利用者に向けてOpen API Specを公開するポータルを作れるDeveloper portal などをはじめとして、APIOpsの構築をサポートするツールが多数提供されています。 API のサイロ化や、品質のばらつきといった課題をお持ちの方は、ぜひKongの導入を検討されてみてはいかがでしょうか? 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: 藤澤 大世 (@ftuajii) レビュー: @kumakura.koki ( Shodo で執筆されました )
1. 自己紹介 金融IT本部 融資ソリューション2部末永です。 入社20年目、42歳です。これまで金融機関向けスクラッチ開発、PKG導入、ユーザ支援ツール導入、保守などに携わってきました。 担当業務領域としては、銀行市場 リスク管理 領域・融資領域・決済領域、生命保険会社ALM領域などを担当してきました。 2. 融資ソリューション2部の取り組み 私の所属する融資ソリューション2部では、金融機関様向けに自社パッケージであるBANK・Rのノウハウを活用した、融資支援システムのスクラッチ開発を行っています。 ITコンサル、新規 システム開発 、エンハンス、保守、などフェーズによって関わり方がありますが、要件定義で合意した仕様のシステムをお客様に納品する活動を行っています。 3. これまでの取り組み、改善していきたい課題 これまで自分が担当してきたお客様の多くは、弊社が長くお付き合いしているお客さまで十分に深耕できている先が多かった印象があります。お客様の困りごとや提案訴求ポイントを把握しやすい距離感であるメリットを活かした提案を行ってきました。 一方で、お客様が気づいていない課題などに直面するケースや深耕しきれていないお客様へのアプローチをしてビジネスを拡大するには、自身の対顧 ヒアリ ング・深堀、提案能力のさらなる向上が必要と考えています。 具体的には、新規のお客様に対してBPR業務コンサルからご支援をして新規システムの提案をするケースにおいては、お客様の困りごと・優先順位の整理、訴求ポイント、 電通 総研の優位性の検討、お客様向けの説明能力などが必要となってくると考えています。 4. 電通 総研の部署横断の新たな取り組みへの参加 2024年1月から社名が 電通 総研にかわり、「SI× コンサルティング × シンクタンク 」として新たな価値提供を目指しています。 私自身も視座や能力を改めて開発する必要性を感じていた中、組織横断選抜のワークショップ企画があり参加してきました。 【企画概要 ~某コンサルファーム講師を招いてのワークショップ形式~ 】  テーマ:脱“受託型 SIer ”思考研修 ⁻課題解決型コミュニケーションの実践 期間:半日×4セット(全4回) 研修での学び ・  ヒアリ ング設計の在り方。(課題仮説を持ったうえで、お客様の困りごと・方向性整理を併走、推進する) ・ 課題の定義にあたって視座をあげ、ストーリーを構築すること。 ・ 我々オリジナルの訴求ポイントの追求。 ・ 上記を共有・推進、訴求するために必要な会議設計。 【ポイント・特徴】 顧客訴求・顧客を動かすための思考ト レーニン グ:  従来の SIer 思考からのステップアップを狙う。 (1) SIer 思考:相手の困りごと → 自分たちができることは何か? (2)コンサル思考:相手の困りごと → 相手に必要なことは何か? → それをできる方法は? 部署横断参加: 年次、顧客、扱う商品(SI、PKG、製品開発、コンサル)など、背景が違うメンバの参加でやり方の吸収や気づきを得ていく。 ワーク中心の研修: 講師から最小限の考え方・ツール説明を聞いて、後はひたすらグループワーク。仮実践を通して、ノウハウ・気づきを創出する。 5. 研修概要・取り組みのご紹介 演習風景 「我々オリジナルの訴求ポイントの追求」  ワーク課題設定 「顧客課題 ヒアリ ング後、顧客にとっての提供価値・TOBE像を考え、 電通 総研が選ばれるシナリオを検討する」  演習を通しての学び 現在地点の共有 顧客から出る困りごとは氷山の一角。顧客の困りごと・現在地点を整理するための問いかけ自体が、顧客への提供価値になる。 ゴールの共有 WHY 電通 総研?を磨くために、TOBE像の解像度を上げること、顧客から見た優先度の整理が重要。 顧客にない発想の引き出しによる価値提供 顧客担当部では思いつかない指摘の検討、第 三者 だからできる発想が価値を生む。 ・同じ課題は他部署にもあるのでは? フォーカスされている調達以外のフロー上にも別の問題があるのでは? ・情報システム部(横串管理部署)の視点での指摘や、マネジメント視点での優先・懸念ポイントは何か?  他社優位性を見出すためのコミットメント 製品価値や実績だけでは他社優位性が見いだせないケースは多々ある。 スコープ外への対応やコミットメント姿勢など自社が最も顧客訴求できる点を検討したうえで、付加価値をどこに置くか、検討・調整していくことが価値を生む。 演習風景 「会議設計の検討」 ワーク課題設定 「顧客向けBPR提案において、初回会議~提案受注までのシナリオを検討する」 演習を通しての学び 全体計画の整理 そもそも売る側の立場として、案件受注を目的に置き受注に向けた経路設計から始める。 顧客への依頼事項の整理 後回しにしがちだが、とても重要。顧客から見た前提やWHY 電通 総研を輝かすための素材 ヒアリ ングを意識すること。 各回のストーリー・ポイントの整理 打合せ回数、期間はどの程度? それぞれの回でどう進めるかを検討する。 例:①(課題共有、TOBE検討ネタの提供、ミニワークショップ)→ ②(顧客持ち帰り事項の共有、TOBE検討の議論) → ③(TOBE像優先順位の議論)→ ④(提案) 6. まとめ 対顧 ヒアリ ング・深堀、提案能力向上に向けては、日々の活動の中での声がけや気づき・検討が重要と思っていますが、研修を通して視点や考え方、 マインドセット を体感して、実務での学びを大きくするための活動をご紹介させていただきました。 「顧客課題の深堀・訴求活動」と「品質の良いシステムを作り上げる活動」は、それぞれの分野のエキスパートによってなされることがありますが、私はそれぞれが業種・職位を超えた視点を持って、提案・開発を進めていくことが重要と思っています。 BPRコンサル・上流提案というと一部のメンバが携わる活動と思われるかもしれませんが、営業・ コンサルタント ・PM・SEがそれぞれの視点で考え、互いに議論を重ねていくことで、付加価値・顧客体験価値が上がっていくものと思い日々活動しています。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @suenaga.asuka レビュー: @miyazawa.hibiki ( Shodo で執筆されました )
こんにちは。クロス イノベーション 本部エンジニアリングテクノロ ジー センターの宮原です。 本記事は 電通総研 Advent Calendar 2025 の19日目の記事になります。12/1から始まっている 電通 総研 Advent Calendarもいよいよ最終日となります。執筆されたみなさん、お疲れ様でした! さて、本記事では AWS re:Invent 2025で発表された新機能の一つである AWS Lambda Durable Functionsの機能をご紹介できればと思います。 AWS re:Invent 2025に関連する記事は宮崎さんの『 AWS re: Invent2025で登場した3つのFrontier Agentsの1つである「AWS DevOps Agent」を触りつつ、概要について整理してみる。』 や『 AWS re:Invent2025 Keynote現地速報 』もありますので、あわせてご覧いただけると嬉しいです。 速報の記事でも記載したように今年の AWS re:InventではAI関連のアップデートが多くあった一方で AWS Lambdaなどのコンピューティング関連のアップデートは少なかった印象です。 そんな中で AWS Lambda Durable Functionsの機能がリリースされています! 本記事では注目の新機能 AWS Lambda Durable Functionsの機能についてサンプルコードを共有しながら、ご紹介できればと思います。 AWS Lambda Durable Functionsとは 製品ドキュメントのページでは AWS Lambda Durable Functionsについて以下の記載があります。 AWS Lambda の既存のプログラミングモデル内で、Lambda Durable Functions を使って複数ステップのアプリケーションや AI ワークフローの構築を簡単にします。Durable Functions は、進行状況を自動的にチェックポイントし、長時間実行されるタスクの間に最大1年間実行を一時停止し、障害から回復します。追加のインフラを管理したり、独自の状態管理やエラー処理コードを書く必要はなく、より早く イノベーション を進めることができます。 この文面だけだとわかりにくいのですが、以下が重要だと認識しています。 AWS Lambdaの中でチェックポイントを設けることができる 実行待ち時間を指定し、チェックポイントから処理を再開できる 例えば、EC2を起動して、一定時間待機し、その上でEC2 インスタンス を停止するなどのオペレーションが AWS Lambda Durable Functionsを利用することで可能になります。 Lambdaといえば最大実行時間が15分という制約がありますが、 AWS Lambda Durable Functions内で指定した待ち時間は実行時間には含まれず、課金も行われません。 また、複数ステップの処理の中でチェックポイントを設け、特定のステップで失敗した際に、失敗したステップからリトライ、再開することも可能です。 AWS Lambda Durable Functionsのサンプルコード ドキュメントを読むだけではわかりにくい部分もあったため実際にサンプルコードを書きながら理解を深めてみました。 今回最初に実行した AWS Lambda Durable Functionsのサンプルコードは以下になります。 import { type DurableContext, withDurableExecution } from "@aws/durable-execution-sdk-js"; export const handler = withDurableExecution ( async ( _event : unknown , context : DurableContext ): Promise < void > => { await context.step( "step1" , async () => console .log( "step 1 executed" )); await context.wait( { seconds : 5 } ); await context.step( "step2" , async () => console .log( "step 2 executed" )); } ); console.logを実行し、5秒待ち、再度console.logを実行するだけのシンプルなものになっています。Lambdaのテスト機能経由で関数を実行したところDurable execution detailsというリンクが表示されました。クリックすると永続オペレーションとイベント履歴が確認できる以下のような画面が表示されます。 こちらの画面からstep1、待機、step2の処理が成功していることを確認できます。 次にサンプルコードについてです。 AWS Lambda Durable Functionsを実行する際には別途@ aws /durable-execution- sdk -jsをインストールし、通常のLambda関数のハンドラをwithDurableExecution関数でラップします。 context.step関数を利用し、チェックポイントを作成しながら、処理を記述します。 context.wait関数を利用すると指定した実行時間だけ処理を待つことができます。 冒頭にも記載の通り、この待ち時間はLambdaの実行時間には含まれず、課金の対象外になります。 処理を失敗させてみる 今度は処理の途中でエラーが発生した際にどのような挙動になるかを確かめてみます。 以下のコードをLambda関数にデプロイし、テストを実行しました。 このコードでは2番目のステップで乱数を生成し、数字が偶数の場合に例外を送出するようにしています。 import { type DurableContext, withDurableExecution } from "@aws/durable-execution-sdk-js"; export const handler = withDurableExecution ( async ( _event : unknown , context : DurableContext ): Promise < void > => { await context.step( "step1" , async () => { console .log( "step 1 executed" ); } ); await context.step( "step2" , async () => { const num = Math . floor ( Math . random () * 100 ); if (num % 2 === 0 ) { throw new Error ( "Random failure occurred" ); } console .log( "step 2 executed" ); } ); await context.step( "step3" , async () => { console .log( "step 3 executed" ); } ); } ); エラーが発生した場合の永続オペレーションとイベント履歴は以下のようになっていました。 step2で処理が失敗し、リトライが自動で実行され、step2から処理が再開されていることがわかります。このように AWS Lambda Durable Functionsを利用することでエラー発生時に処理が自動リトライされ、失敗したステップから処理を再開することが可能になっています。 また、リトライロジックの制御は、context.step関数の第3引数にオプションとして渡すことが可能です。試行回数やエラーのクラスを受け取り、値に応じてリトライロジックを細かく制御できます。以下はリトライロジックを制御する場合のサンプルコードになります。 await context.step( "step2" , async () => { const num = Math . floor ( Math . random () * 100 ); if (num % 2 === 0 ) { throw new Error ( "Random failure occurred" ); } console .log( "step 2 executed" ); } , { retryStrategy ( _error : Error , attemptCount : number ) { if ( 3 < attemptCount) { return { shouldRetry : false } ; } return { shouldRetry : true } ; } , } , ); 実践例 今回は、この AWS Lambda Durable Functionsの機能を利用して、EC2 インスタンス を起動して、20分経過後に、再度EC2 インスタンス を停止する スクリプト を実装して、動かしてみました。EC2 インスタンス の計画停止を想定した簡単な スクリプト になっています。 こちらも待ち時間を含めるとLambdaの15分の実行時間上限を超えていますが、問題なく動作しています。 このような状態を持った スクリプト は、 AWS Step Functionsなどを利用して実行することが多かったと思いますが、 AWS Lambda Durable Functionsを利用することで AWS Lambdaのロジックとして実装できるようになったのは嬉しいポイントだと思います。 import { type DurableContext, withDurableExecution } from "@aws/durable-execution-sdk-js"; import { DescribeInstanceStatusCommand, EC2Client, StartInstancesCommand, StopInstancesCommand } from "@aws-sdk/client-ec2"; const ec2 = new EC2Client( { region : "ap-northeast-1" } ); const instanceId = process .env.EC2_INSTANCE_ID; if (!instanceId) { throw new Error ( "EC2_INSTANCE_ID environment variable is not set." ); } export const handler = withDurableExecution( async ( _event : unknown , context : DurableContext ): Promise < void > => { await context.step( "startInstance" , async () => { console .log( `Starting instance: ${ instanceId } ` ); const startResp = await ec2. send ( new StartInstancesCommand( { InstanceIds : [ instanceId ] } )); console .log( "Start response:" , JSON . stringify (startResp.StartingInstances, null , 2 )); } ); await context.wait( { minutes : 20 } ); await context.step( "stopInstance" , async () => { console .log( `Stopping instance: ${ instanceId } ` ); const statusResp = await ec2. send ( new DescribeInstanceStatusCommand( { InstanceIds : [ instanceId ] , IncludeAllInstances : true } )); if (statusResp.InstanceStatuses?. [ 0 ] .InstanceState?.Name !== "running" ) { console .log( `Instance ${ instanceId } is not in running state` ); return ; } const stopResp = await ec2. send ( new StopInstancesCommand( { InstanceIds : [ instanceId ] } )); console .log( "Stop response:" , JSON . stringify (stopResp.StoppingInstances, null , 2 )); } ); } ); 所感 AWS Lambda Durable Functionsを利用してみた感想です。 context.step関数でラップすることで処理が自動的にリトライされるようになり、エラーが発生しても特定のステップからリトライ、再開されるようになります。 通常のLambda関数のようなロジックでもリトライを想定し、 AWS Lambda Durable Functionsの形式で実装していくことで AWS Lambdaの処理がより安定すると思います。 また、context.wait関数を利用して、Lambdaの実行上限を超えて、一定時間待ち、処理を再開するようなロジックが書けるようになったのも大きいと思います。 今回はcontext.wait関数を呼び出しましたが、context.createCallback関数やcontext.waitForCallback関数なども用意されており、callbackが呼び出されるまで実行を停止するようなロジックも書けるようです。 AIエージェントのHuman-In-the-Loopもこちらの機能を使うことで簡単に実装できそうです。 終わりに 今回は AWS Lambda Durable Functionsの新機能を試してみました。待ち時間や処理の再開などの機能が追加され、 AWS Lambdaの利用範囲がまた拡大したのではないでしょうか。 私自身も AWS Lambda Durable Functionsにマッチする実際の ユースケース を考えていきたいと思いました。 最後までお読みいただきありがとうございました! 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @miyahara.hikaru レビュー: @miyazawa.hibiki ( Shodo で執筆されました )
Merry Christmas! エンタープライズ 第一本部 業務変革ソリューション2部の倉内です! 本記事は 電通総研 Advent Calendar 2025 18日目の記事です! 17日目である昨日の記事は、クロス イノベーション 本部 大岡叡さんの「 【AWS資格】新卒1年目がAWS資格を全冠したので振り返る 」でした! 新卒1年目で AWS 全12資格を取得するまでの背景や、挑戦を通じて得られたこと・つらかったことを振り返った記事です。 新卒1年目で全冠…1つ上の先輩として負けていられないなと思いつつ、ただただ尊敬です。ぜひご一読ください! さて、私は現在新卒2年目で、普段はエンジニアとして金融領域のソリューション開発に携わっています。 タイトルの通り、実は来年から働きながら大学院に通うことにしました。 今回はそこで学ぼうとしている「システム×デザイン思考」という考え方について書いてみます。なぜこれを学ぼうと思ったのか、そしてこの考え方がITエンジニアにとってどう役立ちそうか、自分なりに整理してみました。 なぜ大学院に行くのか 「難しい問題」と「複雑な問題」 「システム思考」とは何か 「デザイン思考」とは何か 2つを組み合わせる エンジニアとの相性 これから学ぶこと おわりに なぜ大学院に行くのか 学部時代から、いつか大学院で研究したいという気持ちはありました。ただ、当時は「研究よりも社会実装に近いところでやりたい」という思いがあり、まずは就職する道を選びました。 では、なぜ今なのか。きっかけは「地方創生」への興味でした。 地方出身の人間として、学生時代から地域の課題に関心があり、社会人になってからもその思いは変わりませんでした。ただ、地方創生について調べたり、関わっている人の話を聞いたりする中で、壁にぶつかりました。 地方創生が複雑な問題であることは、誰もが知っていることだと思います。私自身もそれは理解していたつもりでした。しかし、「複雑だ」とわかっていることと、「その複雑さにどう向き合えばいいか」がわかっていることは、まったく別の話でした。 人口減少、産業の衰退、若者の流出、行政と民間の連携……。これらが互いに影響し合っていることは頭では理解していました。でも、「だから何をすればいいのか」となると、途端にわからなくなる。一つの課題に手を打とうとすると、別のところに影響が出る。関係者によって「何が問題か」の認識も違う。 エンジニアとして技術を学んできましたが、技術だけではこの複雑さを扱えないと感じました。 「複雑な問題を扱うための方法論が必要だ」 そう思って調べる中で出会ったのが「システム×デザイン思考」という考え方でした。 「難しい問題」と「複雑な問題」 この考え方を学ぶ中で、最初に腑に落ちたのが「難しい問題」と「複雑な問題」の違いです。 難しい問題というのは、たとえば アルゴリズム の実装やパフォーマンスチューニングのようなものです。専門知識が必要で、時間もかかりますが、正解があります。手順を踏めば解けます。再現性もあります。 一方、複雑な問題というのは、正解がありません。要素が多く、それぞれが影響し合っていて、一つを変えると別のところに影響が出る。しかも、関わる人によって「何が問題か」の認識が違っていたりします。 地方創生はまさにこれです。「若者が出ていくのが問題だ」という人もいれば、「仕事がないのが問題だ」という人もいる。「そもそも人口が減っても別にいいのでは」という意見もある。何を解決すべきかすら、合意を取るのが難しい。 ITの世界でも、似たような状況はあります。DX推進や レガシーシステム の刷新、組織をまたいだプロジェクトなどは、技術的な正解を出しても、それだけではうまくいかないことが多いと聞きます。 私たちエンジニアは、難しい問題を解く訓練を受けています。でも、複雑な問題には、別のアプローチが必要らしい。そのことに気づいたのが、最初の発見でした。 「システム思考」とは何か では、複雑な問題にはどう向き合えばいいのか。その一つの答えが「システム思考」です。 システム思考とは、物事を個別の要素としてではなく、全体として捉える考え方です。要素と要素がどのように影響し合っているか、その関係性に注目します。 たとえば、地方の人口減少を考えてみます。 若者が都市部に出ていく。地元の働き手が減る。企業が撤退する。さらに仕事がなくなる。ますます若者が出ていく。 これは悪循環です。どこか一箇所を切り取って対処しても、構造が変わらなければ同じことが繰り返されます。「若者に残ってもらおう」と呼びかけても、仕事がなければ残れない。「企業を誘致しよう」としても、働き手がいなければ来てくれない。 システム思考では、こうした因果関係を図にして可視化します。これを「因果ループ図」と呼びます。図にすることで、どこに手を打てば全体が良くなるか、いわゆる「 レバレッジ ポイント」が見えてきます。 たとえば、上の例で言えば、「リモートワーク可能な仕事を増やす」というのは一つの レバレッジ ポイントかもしれません。都市部の企業に勤めながら地方に住めるなら、「仕事がないから出ていく」という構造を変えられる可能性があります。 もちろん、これが正解かどうかはわかりません。でも、構造を可視化することで、「どこに手を打つと効果がありそうか」を議論できるようになります。 【図1:地方の人口減少の悪循環を示す因果ループ図の例】 「デザイン思考」とは何か 一方、システム思考だけでは足りない部分もあります。 構造を分析することはできても、「そもそも何が問題なのか」を発見するのは得意ではありません。また、論理的に正しい解決策を導いても、それが人の心に響くとは限りません。 そこで出てくるのが、デザイン思考です。 デザイン思考は、人間を中心に置いた問題解決のアプローチです。まず現場に行き、人の話を聞き、観察する。言葉にならない困りごとや、本人も気づいていない 潜在的 なニーズを探る。そこから問題を定義し、解決策を考え、素早く形にして試す。 地方創生の文脈で言えば、「住民が本当は何に困っているのか」「行政の人は何を考えているのか」を、データや報告書ではなく、直接会って聞くことから始める。そういうアプローチです。 デザイン思考でよく使われるプロセスとして、「共感→問題定義→ア イデア 創出→プロトタイプ→テスト」という5つのステップがあります。いきなり解決策を考えるのではなく、まず相手の立場に立って共感することから始めるのが特徴です。 エンジニアの感覚からすると、少しふわっとした印象を受けるかもしれません。私も最初はそうでした。 でも、考えてみると、要件定義書に書かれていることが本当にユーザーの求めていることなのか、疑問に思った経験は誰にでもあるのではないでしょうか。 2つを組み合わせる システム思考とデザイン思考。 この2つは、一見すると異なるアプローチに見えます。片方は構造を分析し、もう片方は人間に寄り添う。片方は全体を俯瞰し、もう片方は個人に深く入り込む。 でも、複雑な問題を解くには、両方が必要です。 デザイン思考で現場の声を集め、問題を発見・定義する。システム思考でその問題の構造を可視化し、どこに手を打つべきかを考える。 再びデザイン思考で解決策を発想し、プロトタイプを作る。そしてシステム思考で、その解決策が全体にどんな影響を与えるかを検証する。 この行き来が重要だと言われています。 システム思考の提唱者の一人であるピーター・センゲは、著書『学習する組織』の中で、全体(wholes)・相互関係・変化のパターンに目を向ける視点(システム思考)を組織学習の土台として示しています。実践面については、別著『The Dance of Change』の中で、現場に近い人々の小さな試行と対話の重要性を論じています。 またデザイン思考の公式フレームでは、問題の理解・定義を飛ばしてア イデア に走ると“誤った問題”を解きやすいと明確に示されています。 どちらか一方だけでは不十分で、両者を行き来することで、複雑な問題に対処できるようになる。これがシステム×デザイン思考の基本的な考え方です。 【図2:システム思考とデザイン思考の行き来】 たとえば、地方創生で「移住促進」を考えるとします。 まず、デザイン思考で移住を検討している人や、実際に移住した人に話を聞く。すると、「仕事の不安」だけでなく、「子どもの教育」「医療へのアクセス」「地域コミュニティへの馴染みやすさ」など、さまざまな要素が出てくる。 次に、システム思考でそれらの要素の関係性を整理する。「教育環境が整うと、子育て世代が移住しやすくなる」「子育て世代が増えると、地域の活気が出る」「活気が出ると、さらに移住者が増える」といった好循環が見えてくるかもしれない。 そして、再びデザイン思考で「では、教育環境を整えるために何ができるか」を発想し、小さく試してみる。 正直なところ、私はまだこれを実践できているとは言えません。でも、この枠組みを知っているだけで、物事の見え方が少し変わってきた気がします。 エンジニアとの相性 この考え方を学んでいて思うのは、エンジニアは意外とこの思考法と相性がいいのではないか、ということです。 まず、システム思考について。私たちは日常的に、システムを設計しています。要素を分解し、関係性を定義し、全体として動くように組み立てる。 コンポーネント 間の依存関係を意識し、一箇所を変えたときの影響範囲を考える。これはまさにシステム思考そのものです。 また、デザイン思考について。私たちは「動くもの」を作れます。ア イデア を形にして試すことができる。これはデザイン思考で言うプロトタイピングです。紙に書いた企画書ではなく、実際に触れるものを作って見せられるのは、エンジニアの大きな強みだと思います。 日々の業務でも、この考え方は使えそうです。 たとえば、プロジェクトで問題が起きたとき。「誰が悪い」という犯人探しではなく、「どういう構造がこの問題を生んでいるのか」と考える。因果ループ図を描いてみると、個人の努力ではどうにもならない構造的な問題が見えてくるかもしれません。 あるいは、新しい機能を開発するとき。要件定義書をそのまま実装するのではなく、「この機能を使う人は、本当は何がしたいのか」を考える。可能であれば、実際に使う人に話を聞いてみる。 つまり、必要なスキルの土台はすでに持っている。足りないのは、それを技術以外の領域にも適用するという発想なのかもしれません。 これから学ぶこと 来年から大学院で システムエンジニア リング、デザインプロジェクト、システム ダイナミクス などを学ぶ予定です。座学だけでなく、実際のプロジェクトを通じて実践する機会もあると聞いています。 特に楽しみなのは、異なるバックグラウンドを持つ人たちと一緒に学べることです。エンジニアだけでなく、デザイナー、 コンサルタント 、行政職員、医療従事者など、さまざまな分野の社会人が集まると聞いています。 複雑な問題に取り組むには、多様な視点が必要です。自分とは違う考え方に触れることで、視野が広がることを期待しています。 おわりに 本記事では、私が大学院で学ぼうとしている「システム×デザイン思考」について、なぜ興味を持ったのかという背景も含めて書いてみました。 まだ学び始めたばかりで、偉そうなことは言えませんが、「技術だけでは解決できない問題が数多くある」ということに気づけたのは、自分にとって大きな一歩でした。 来年からは実際に大学院で学びながら、この思考法を実務にどう活かせるか試行錯誤していく予定です。学んだことや気づいたことは、本ブログで共有していければと思います。 この記事が、同じようなもやもやを感じているエンジニアの方にとって、何かのきっかけになれば幸いです。 さて、 電通 総研 Advent Calendar 2025もいよいよ明日が最終日! 明日の記事は、クロス イノベーション 本部 宮原光さんの「 AWS re:Invent2025 Keynote 現地速報」です! アドベントカレンダー のトリを飾るにふさわしい、現地からの熱気が伝わってくる記事になること間違いなしです!お楽しみに! 最後までお読みいただき、ありがとうございました。 それでは、よいクリスマスを。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @kurauchi.tatsuya レビュー: @miyazaki.hirotoshi ( Shodo で執筆されました )
はじめに クロス イノベーション 本部、新卒1年目の大岡叡です。 本記事は 電通 総研 Advent Calendar 2025 17日目の記事です。 12月上旬に12個目の AWS 資格を取得しました。これにより、 AWS 資格全種類を取得したことになり、 2026 Japan All AWS Certifications Engineers クライテリア を満たしました。 本記事では新卒1年目で全冠を目指した背景、目指して・達成して得られたこと、つらかったことを振り返っていきたいと思います。なお、資格取得のスケジュールや具体的な勉強方法については、すでに多くの良記事がありますので本記事では割愛します。 はじめに 全冠を目指そうと思った背景 一言で言うと 詳しく 全冠を目指して・達成して良かったこと 1. 「できるやつ」と思われる → 期待に応えようと成長できる 2. 上司の説明がすぐ理解できる → スムーズな業務遂行 3. 周囲から褒めてもらえる → 仕事ももっと頑張ろうと思える つらかったこと まとめ・今後の展望 全冠を目指そうと思った背景 一言で言うと 全冠を達成すると得られる All AWS Certifications Engineer(以下、All Cert)という称号に魅力を感じたため目指しました。 詳しく 就職活動で出会った方から、 AWS Jr. Championsという表彰プログラムがあるという話を聞いたのが最初のきっかけでした。 AWS Jr. Championsとは、APN参加企業に所属する若手向けの表彰プログラムです。以下が公式の説明です。 AWS Partner Network (APN) 参加企業に所属し、現在社会人歴 1~3 年目で突出した AWS 活動実績がある若手エンジニアを「Japan AWS Jr. Champions」として表彰します。これは、 AWS を積極的に学び、自らアクションを起こし、周囲に影響を与えている APN 若手エンジニアを選出しコミュニティを形成する、日本独自の表彰プログラムです。 ( AWS Japan APN ブログ より引用) この AWS Jr. Championsを調べていく中で、All AWS Certifications Engineerという別の表彰があることを知りました。All Certは AWS の全資格を取得すると表彰されるプログラムです。以下が公式の説明です。 「Japan All AWS Certifications Engineers」とは、 AWS Partner Network (APN) に参加している会社に所属し、下記クライテリアの「 AWS 認定資格を全て保持している」 AWS エンジニアの皆様を対象にした表彰プログラムです。 ( AWS Japan APN ブログ より引用) さらに調べていくと、All Certを達成すると以下のようなメリットがあることが分かりました。 AWS の公式サイトに会社名と名前が掲載される 例: 2025 Japan All AWS Certifications Engineers の発表 会社のプレスリリースに名前が掲載される 例: 電通総研グループ社員9名がAWSの「2025 Japan AWS Top Engineers」「2025 Japan AWS All Certifications Engineers」「2025 Japan AWS Jr. Champions」に選出 (当たり前だが) AWS の知識を体系的かつ幅広く身に付けることができる 特に1と2のように対外的な実績として示せる点に魅力を感じました。 そして、1年目で達成できればより強い実績になると考え、全冠取得を決意しました。 全冠を目指して・達成して良かったこと AWS 公式サイトや会社のプレスリリースに掲載されるのは来年になるので、ここではそれ以外に実感している良かったことを紹介します。 1. 「できるやつ」と思われる → 期待に応えようと成長できる 同期に対して全冠を目指していることや、複数の資格を持っていることを公言していたので、新人の システム開発 研修で AWS を使ってインフラ構築する際に、同期からたくさんの質問を受けました。 質問の中には資格勉強で得た知識から回答できるものもあれば、わからないものもたくさんありました。質問してくれた人の期待に応えられるように、わからない質問が来てもわかるまで調べて回答するようにしました。その過程で調査力と実践的な知識が身についたと感じています。 2. 上司の説明がすぐ理解できる → スムーズな業務遂行 業務上、上司との会話に AWS の用語が当たり前に登場しますが、資格勉強をしていたおかげで苦労せずに理解できています。その結果、業務におけるコミュニケーションが円滑に進み、スムーズに仕事ができていると感じています。 3. 周囲から褒めてもらえる → 仕事ももっと頑張ろうと思える 全冠を達成したことを部署のTeamsチャネルで報告したところ、部長や仕事で関わっている上司に加えて、普段話していない方からも「すごいね!」「おめでとう!」という言葉をいただきました。頑張って良かったと思うと同時に、仕事でも期待に応えられるようにもっと頑張ろうと思えました。また、部署の温かさも感じられて嬉しかったです。 つらかったこと 「1年目でAll Cert」のために資格取得を決意したので、資格勉強の一番の目的を「合格」に置いていました。そのうえで、最短距離で合格するには問題演習を繰り返すことだと考えていました。(全冠した現在でもその考えは変わりません。)そのため、問題演習の中で「実際に触って試したい」と思う場面があっても、ぐっとこらえてひたすら問題を解き続けていました。このように好奇心を抑えながら勉強を続けるのは、なかなか苦しいものでした。 加えて、資格勉強は1人で取り組むため、孤独感もつきまといます。私の場合は、同期を誘って一緒に勉強するようにしていたのでなんとかやっていけました。 とはいえ、資格勉強自体がつらかったわけではありません。たくさんの知識を吸収できる楽しさがありましたし、All Certという目標に向かって努力すること自体にやりがいがありました。また、実務で得た知識が問われる問題に出会えたときは、小さな喜びも感じられました。 まとめ・今後の展望 新卒1年目で AWS 資格全冠を達成した経験を振り返りました。 次は AWS Jr. Championsを目指します。 ここまでお読みいただきありがとうございました。 この記事がAll Certを目指すきっかけになれば幸いです。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @ooka.toru レビュー: @miyazawa.hibiki ( Shodo で執筆されました )
こんにちは! グループ経営ソリューション事業部 アドバンストテクノロ ジー 部の佐藤です。 突然ですが、日々の技術学習で、皆さんはどのように AI を活用しているでしょうか? Copilot Chat と対話的に疑問を紐解いていく方もいれば、Genspark のような AI 検索エンジン 派の方、最近だと ChatGPT Atlas といった AI ブラウザを駆使する方もいるでしょう。 このように、新しい知識を調べ、学習するために AI を活用することはもはや当たり前になりつつあります。 しかしながら、こうした「インプット」のノウハウは、AI 駆動開発のように何かを作り出す「アウトプット」の話題と比べると、語られる機会が少ない印象を受けます。 そこで、 電通総研 Advent Calendar 2025 、16 日目の今日は、技術知識の学習における「 メンタルモデル 」の重要性を押さえつつ、自分だけのナレッジベースを AIと共創 することでメンタルモデルの構築を効率化する実践的な学習スキームを紹介したいと思います。 技術知識を学ぶ理由 意思決定への道標 RTKB - Reviewable and Traceable Knowledge Base - ドメインと品質の定義 TDL(Test-Driven Learning) 抽象と具体の分離 問題空間の明確化 トレーサビリティの確保 RTKB の学習フロー さいごに:AI 時代における意思決定の重要性 技術知識を学ぶ理由 学習スキームの話に入る前に、少し立ち止まって「そもそも論」を考えてみたいと思います。 そもそも、私たちはなぜ技術知識を学ぶのでしょうか? 多くのエンジニアに共通する理由の一つとして 「興味や関心といった"知識欲"を満たすため」 があるかと思います。 これは、エンジニアにとって最も純粋かつ強力な、尊重されるべき動機です。エンジニアとして長く働き続けるための、必須要件とすら言えるかもしれません。 しかし、ビジネスの観点からは、これ だけ では不十分です。 所属する組織、ひいては社会に対し価値を提供するために「知識をどう使うのか」という視座から、技術知識を学ぶ本質的な理由を考えなければなりません。 本稿では、それを 「意思決定の質を高めるため」 と捉えてみたいと思います。 意思決定への道標 ソフトウェア開発は、意思決定の連続です。 アーキテクチャ の選定から変数の 命名 に至るまで、あらゆる活動は意思決定により駆動されます。 語弊を恐れずに言えば、あらゆる知識は、この「意思決定」に寄与することで、初めてビジネス上の価値を与えられます。 「技術知識は意思決定の根拠ではなく、 課題解決の手段 ではないか」 と思う方もいるかもしれません。 確かに、多くの技術知識の本質は手段です。しかし、知識が手段としての価値を発揮するか否かは、意思決定に依存しています。 例えば、Strategy パターンを 知って いたとしても、「この コンポーネント は Strategy パターンで実装する」という意思決定が出来なければ、その知識は一行のコードも生み出すことはありません。 つまり、いかなる知識も、 意思決定の土俵に上げられない限りは宝の持ち腐れ となってしまいます。 では、意思決定において適切な知識を動員し、質の高い決断を下すにはどうすれば良いでしょうか。 そのためのカギとなるのが、 「メンタルモデル」 です。 メンタルモデルについては、弊社のアーキテクトである 米久 保さんの過去記事でも解説されています。 tech.dentsusoken.com メンタルモデルの構築には、知識を点として暗記するのではなく、対象が持つ多面性を捉え、それが「何を解決するのか」「いつ適用できるのか」といった様々なコンテキスト上で概念化を行うことが重要です。 そのようにして構築された個々のモデルは、やがてアナロ ジー や対比などの認知作用によって相互に関連付けられ、言わば 「認知の地図」 を描き始めます。 この地図を心に広げることができれば、知識は 私たちを意思決定へと導いてくれる、確かな道標 に変貌するのです。 次章では、この「認知の地図」を描く際のパレットとなる自分だけのナレッジベースを、AIと共創する学習スキームを紹介します。 RTKB - Reviewable and Traceable Knowledge Base - 技術知識を AI で調べるとき、最も手軽で一般的なアプローチは、Copilot Chat などの AI チャットとの対話的な学習だと思います。 しかし、チャット上での対話は基本的に 使い捨てのフロー情報 であり、チャット履歴は知識の墓場になりがちです。 RTKB は、そんな漫然としたチャットから脱却し、AI からの回答を Reviewable かつ Traceable な 形式知 として管理することで学習効果を高めるアプローチです。 AI チャットに「聞くだけ」の学習に対する課題感から、筆者自身が実際に試行錯誤しつつ実践してきたアプローチを、本稿執筆にあたりブラッシュアップし、スキームとして定義しました。 大仰な名前を付けてはみましたが、 仕組みはいたってシンプル です。 まずは、ローカル環境にナレッジベースのルート ディレクト リを作成し、Cursor などの AI コーディングエディタで ワークスペース として開きます。 あとは、エージェントに学びたい知識の技術ドキュメントを作成してもらい、Git で構成管理するだけです。 これだけであれば、ただのドキュメントのフォルダ管理ですが、RTKB の要はプロジェクトに配置する AGENTS.md にあります。 このファイルに、 メンタルモデル構築に適したドキュメントの仕様 を厳格に定義することで、AI チャットにただ「聞くだけ」では得られない、学習効果の高い出力を引き出します。 ここからは実際の AGENTS.md を一部引用しつつ、ポイントを解説します。 ※ AGENTS.md の全文や、実際に出力されたサンプルドキュメントは GitHub リポジトリ にて公開していますので、あわせてご確認ください ドメイン と品質の定義 AGENTS.md では、まずこのナレッジベースが扱う「技術的概念」の ドメイン を定義しています。 ## Glossary - 技術的概念: - Computer Science のコンテクストにおける汎ゆる概念を示す - 技法・原理・原則・プロトコル・パターン・プラクティス・仕様・パラダイム などを含む ここで「技術的概念」という言葉をあえて広義に定めることで、RTKB は特定の要素技術から抽象度の高い概念まで幅広く適用可能な汎用スキームとして機能します。 また、 NON-NEGOTIABLE な規約として以下を明記することで、ハルシネーションを可能な限り抑制し、学習教材としての信頼性向上を狙います。 ## 全ての文書において NON-NEGOTIABLE な遵守事項 - 技術的概念について記述する際は、**必ず厳密な技術用語を利用**する - 厳密とは、その言葉が対象の技術領域において広く一般的に使われる正確な言葉であるという syntactic な側面と同時に、意味的にも定義通りであるという semantic な側面の両方を満たす事を指す - 技術的概念に関して不明確な点がある場合に、推察や推論、憶測による根拠のない断定を行ってはならない。全ての文章は、事実に基づき記述される必要がある TDL (Test-Driven Learning) TDD のように、学習もテストから始めましょう。 ## Comprehension Check 記載時の遵守事項 - `Comprehension Check` 章には、主要な技術的概念、技術文書の重要な論点/要点の理解度を問う簡潔なテストを箇条書きで記載する - 正誤を問う形式や選択肢問題ではなく、自由回答方式の設問とする - 技術文書の主題ならびに主要な技術的概念について、それ自体(WHAT)ではなく、それに関する WHAT FOR / WHY / WHEN の理解度を問う設問が望ましい - 重要な技術的概念と同じ問題空間を対象とした、異なる技術的概念が存在する場合、それとの違いを問う設問を含める RTKB では、ドキュメントの冒頭に必ず Comprehension Check を設けています。 これにより、ドキュメントが Reviewable (=復習可能)になると同時に、読者は「何を理解しなければならないか」を明確化したうえで学習を開始することができます。 なお、各設問では意思決定に向けて重要な観点である "WHY" や "WHEN" を 自由回答方式 で問うことを推奨しています。 この回答を通して、対象の概念を「自分の言葉」で説明するという営みは、メンタルモデルの構築にとって重要なファクターとなります。 ただドキュメントを読み返すのではなく、 復習として Comprehension Check に取り組むことで、より効果的にメンタルモデルを構築することができます。 抽象と具体の分離 アーキテクト思考:メンタルモデル構築の重要性 でも説明されているとおり、メンタルモデルの構築には「抽象と具体の往来」が効果的です。 RTKB は、 概念の解説(=抽象)と、その適用事例の解説(=具体)が必ずドキュメントに含まれる ようにス ケルト ンを定義しています。 ## {WHAT/WHAT FOR/WHY/WHERE the CONCEPT IS FROM (the ORIGIN):主題と関わる技術的概念の解説} - 文章形式での記述を優先し、必要に応じて項への分割、箇条書き・表を用いる ## {HOW/USAGE:技術的概念の使い方/機序/運営・運用/活動/メカニズム/アプローチ} - 文章形式での記述を優先し、必要に応じて箇条書き・表を用いる また、それらを異なる章に分離することで段落内の抽象度の揺らぎを回避し、概念理解から適用への思考フローと読書体験の摩擦を低減します。 問題空間の明確化 多くの技術的概念は、何らかの課題を解決する手段として機能します。 RTKB では、技術的概念が対象とする 問題空間 についても説明することを定めています。 また、ある概念の適用事例と併せて、その概念を「適用しない場合」を対比的に解説することで、問題空間をより鮮明に浮かび上がらせるよう図ります。 ## 全ての文書において NON-NEGOTIABLE な遵守事項 - 技術的概念は、何らかの問題空間上の課題に対する解決空間で機能する。技術文書には、必ずこの問題空間上の課題の詳説と、それを理解するための関連する技術的概念まで含めて説明されなければならない ## {HOW/USAGE:技術的概念の使い方/機序/運営・運用/活動/メカニズム/アプローチ} - 理解の促進のため、その技術的概念{を使わない場合/が存在しなかった時代}における具体例を初めに提示する ### {HOW/USAGE WITHOUT the CONCEPT:技術的概念 {を使わない/が存在しなかった時代} のアプローチ} <!-- optional/if applicable:主題の技術的概念が存在しなければ解決不可能な課題である場合、本章は省略可能 --> - コードによる実装例や、具体的な事例を示すことで、主題の技術的概念が存在しない場合にどのような問題があるかを詳説する これにより、その技術的概念が「なぜ必要とされるのか」「いつ適用すべきか」といった 実践的なコンテキストにおける理解 が深まります。 加えて、その概念が対象とする問題空間を把握することにより、その空間を対象とする他の技術的概念とのアナロジカルな関連付け、すなわち 「認知の地図」の広がりも期待 されます。 トレーサビリティの確保 上述の通り、 AI とのチャット履歴は使い捨ての「フロー情報」であり、ナレッジベースに蓄積するストック情報とは一般的には相容れません。 しかし少し見方を変えると、 AI への質問履歴は理解に至るまでの軌跡 そのものであり、捨て去るには惜しい情報とも思えてきます。 そこで RTKB では、質問履歴をチャットに埋もれさせず Traceable に文書化するための規約も定めています。 ## 質問回答時の遵守事項 既存の技術文書に関して、ユーザーからの質問を受領した場合は、以下の方針に従う。 ### WHAT IS THIS:技術的概念の定義に関する質問 - 技術的概念そのものの定義を確認する質問の場合は、`Glossary` 章に追記する ### FOLLOW UP QUESTIONS:方法や理由、目的、意図を問う質問 - 技術文書に記載された内容に関する、理解を深めるための follow up questions に対しては、`Dialogue for Understanding` に質問の要約と回答を追記する - 質問が、`Dialogue for Understanding` の回答に対する追加の質問の場合は、新たな"Q"節は設けず、既存の回答に追記する RTKB の学習フロー ここまで解説してきた AGENTS.md により作成される技術ドキュメントを軸に、RTKB では以下のフローで学習を行います。 学習トピックの バックログ 管理 仕事やプライベートで出会った「ちゃんと説明できないな」「詳しく理解したいな」といったトピックを、任意のツールで バックログ 管理しておきます マイナーな技術や、複数のバージョンが存在するライブラリにおける特定バージョンの API の使い方など、AI が誤りやすいトピックについては、RTKB の対象外として管理することを推奨します 問いかけ 学習トピックについて「分からないことが分からない」状態であれば、学ぶべき技術的概念を対話的に定めます 自身の理解の程度を示しつつ対話することで、ドキュメントが扱うスコープの最適化が期待されます ドキュメント作成依頼 学習したい対象が明らかになったら、ドキュメントの作成をエージェントに依頼します 実装例で使用する言語の指定や、 AGENTS.md 記載ルールの範囲内における詳細度などの調整は可能です TDL 冒頭の Comprehension Check を確認し、学習のゴールをイメージしたうえで、読み始めます 回答例は ▶ Answers を押下すると展開表示されます 深堀り 読み進めていくなかで、疑問点があればエージェントに質問します エージェントは、ユーザーからの質問に合わせてドキュメントを適切に追記/修正してくれます Review 空き時間を使って Comprehension Check による復習を実施します ナレッジベースを GitHub 上で管理していれば、アプリを使って 移動中などの隙間時間で復習が捗る のでお勧めです さいごに:AI 時代における意思決定の重要性 冒頭に記載したとおり、ソフトウェア開発は意思決定の連続です。 これをもう少し解像度を上げて見ると、日常の活動は、意思決定に先行する「方針検討」と、意思決定に基づく「遂行」から構成されると言えるでしょう。 今日の AI は、詳細設計に基づくコーディングのような具体的な「遂行」作業だけでなく、抽象度が高く複雑な「方針検討」タスクでさえも、その高度な推論能力により高い精度でこなせるようになりつつあります。 意思決定の前後が急激に「時短」されたことにより、私たちが ある期間に下さなければならない意思決定の数は、数年前とは比べ物にならない程増加 しています。 そして、この 「意思決定の時間密度」はこれからも高まり続けていく と考えられます。 だからこそ、来る AI 時代において、より迅速かつ的確な決断を下せるよう、私たちはこれからも様々な技術を学び続け、「認知の地図」を拡張していく必要があるのではないでしょうか。 その一つのアプローチとして、本稿で紹介した RTKB が少しでも皆さんのご参考となれば幸いです。 * 本稿に掲載された画像は AI により生成されました 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @satorin レビュー: @miyazawa.hibiki ( Shodo で執筆されました )
こんにちは、グループ経営ソリューション事業部の 米久 保です。本記事は 電通総研 Advent Calendar 2025 15日目の記事です。 アーキテクト思考とは この記事において、 アーキテクト思考 とは ITアーキテクト が仕事をするうえで身につけておくべき思考方法、ツールや フレームワーク 、その利用方法を総称するものとします。その有用性はアーキテクトの枠を超えて、他の職種にも大いに役立つものです。 アーキテクト思考と呼べるものには、今回取り上げる「雲」「 弁証法 」の他にも、「 メンタルモデルの構築 」「システム思考」「推論」「 トレードオフ 分析」「分割統治」などさまざまなものがあります。 対立の解消は課題解決の鍵 アーキテクトの仕事とは、上位目的を達成するための全体構想を描き、実現への道筋を計画し、その実行を主導することです。その過程では大小さまざまな問題が発生し、それらを乗り越えてゴールへ向かうために、日々課題解決が求められます。 課題解決が一筋縄ではいかないのは、相互に対立する要件や事象がしばしば存在するためです。AとBを両立させることが困難な、「あちらを立てればこちらが立たず」という状況です。英語でいうとTrade-off( トレードオフ )ですね。 このようなとき、双方のメリットとデメリットを挙げて比較評価したうえで、選択という意思決定を下すのが トレードオフ 分析です。アーキテクトとして、 トレードオフ 分析を適切に行う技術を身につけることの重要性は言うまでもありません。が、 トレードオフ 分析に入る前に、一呼吸入れて クリティカル・シンキング で問いかけてみましょう。 本当にその対立は、二者択一で選択しないといけないのでしょうか? 対立を乗り越える思考法 私たちは、対立している(と思われる)二つの主張を目にしたとき、どちらかを選ばなければならない、すなわち二律背反の関係であると考えがちです。思い込み、あるいは思考のバイアスを克服するためには、それに適した思考ツールを活用するのがよいでしょう。 この記事では「雲」と「 弁証法 」をご紹介します。 雲 「雲」は、書籍『ザ・ゴール』で有名な エリヤフ・ゴールドラット 博士の生み出した TOC 思考プロセスの道具の一つです。「 クラウド 」「対立解消図」と呼ばれることもあります。雲では、対立関係を図に描くことで、その解消の糸口を見出します。 具体例で考えましょう。ソフトウェア 開発プロセス において、レビューのリードタイムや 工数 、レビューアの負荷はしばしば問題となります。 開発者 「レビューがなかなか返ってこない」「指摘事項の対応に時間がかかる」「レビューをもっと簡素化してほしい」 レビューア 「品質担保のためにしっかりと厳格なレビューをしなくては」「指摘すべき点が多くてどうしても時間がかかる」 対立関係にある、「レビューを厳格化したい」という主張(D)と「レビューを簡素にしたい」という主張(D')を起点に雲を描くと、次の図のようになります。 Dからの矢印の先には、Dを必要条件(つまり上位の目的)とする項目を描く(B) 同様にD'を必要条件とする項目を描く(C) BとCの両方を必要条件とする、共通の目標を描く(A) 雲は、対立関係は解消できるはずというスタンスを取ります。どうやって解消するのでしょうか? まずは、DとD'が暗黙の前提としている事柄や仮定を洗い出します。 レビューを厳格化(D) 厳格なレビューを行うには時間がかかる レビューを簡素化(D') レビューを簡素化すればリリースサイクルの短縮が可能 これらの前提、仮定を疑ってみます。 厳格なレビューには必ず時間がかかるのだろうか。時間を削減する方法があるのでは? レビューを簡素化して品質の悪いソフトウェアをリリースしたら、問合せ対応や障害対応に時間を要して全体としてはリリースサイクルが長くなるのでは? 以上を踏まえて、たとえば以下の施策を打つことで、厳格なレビューを行い、なおかつリードタイムを短くしよう、というのが雲による対立解消のアプローチです。 静的解析ツールや、AIレビューを導入する Pull Requestのサイズを小さくする 弁証法 弁証法 の歴史は古く、 古代ギリシャ の哲学者 ソクラテス が対話を通じて真理に近づく方法として用いた問答法は、 弁証法 の一種とされています。現代で使われる 弁証法 の基礎論理を確立したのはドイツの哲学者 ヘーゲル (1770年~ 1831年 )です。 ヘーゲル の 弁証法 は以下のプロセスを取ります。 命題(テーゼ:正)の提示 それと矛盾する、または否定する命題(アンチテーゼ:反)の提示 それらを本質的に統合した命題( ジンテーゼ :合)の考案 先と同じ例を使って考えてみましょう。 テーゼ(正) レビューを厳格に行う アンチテーゼ(反) レビューを簡素化する これらを統合した、高次の解決策を考えます。そのためには、それぞれのテーゼの本質は何かを考えることが必要です。 テーゼ(正) レビューを厳格に行うのはなぜか? どのような価値観に基づくのか? 品質が重要である ソフトウェアの一貫性が大事である 良いソフトウェアを作る人、組織を育てていきたい アンチテーゼ(反) レビューを簡素化するのはなぜか? どのような価値観に基づくのか? 顧客に素早く価値を届ける必要がある フィードバックループ を短縮して継続的に学習し改善すべき それぞれが大事にしている価値や目指す方向性を踏まえて、 目的そのものから問い直す のが 弁証法 的アプローチです。 ジンテーゼ (合) レビューの目的を再定義する:学習する組織としてのレビュー 具体的な施策としては、たとえば以下のようなア イデア が考えられるでしょう。 対面でレビューを行い、ペア設計/ ペアプロ で設計の改善や リファクタリング を行う 対面レビュー実施時のトラン スクリプト やgit差分データを生成AIに要約させ、レビュー ガイドライン を更新する レビュー ガイドライン を活用して生成AIによる事前レビューを実施する まとめ この記事では、アーキテクト思考として「雲」と「 弁証法 」をご紹介しました。これらは、二律背反のトラップから逃れ、対立を解消したり、対立を乗り越えたりする方法として利用できる思考ツールです。 日々の業務でうまく活用することができれば、強力な武器となることでしょう。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @tyonekubo ( Shodo で執筆されました )
はじめに こんにちは、グループ経営ソリューション事業部の永井です。 私は、2017年から aiuola(あいうぉーら) と Ci*X(さいくろす) という自社プロダクトの開発を担当しています。 aiuola および Ci*X のファーストリリースは 2018年 10月。それ以降は、エンハンスバージョンアップを重ね、製品の機能を強化してきました。 エンハンス開発の要件定義~設計フェーズでは、 Google の Design Docs を参考にした独自のドキュメントを用いて設計をしています。 この記事では、その具体的な内容と運用方法についてご紹介します。 ※ 本記事は 電通総研 Advent Calendar 2025 14日目 の記事です。 aiuola 、 Ci*X の開発スタイル まず、 aiuola 、 Ci*X チームのエンハンス開発の流れを簡単に説明します。 aiuola 、 Ci*X は、半年ごとに新しいバージョンをリリースしています。開発期間 6ヶ月は、大まかに、 要件定義~設計:1~2ヶ月 実装:2~2.5ヶ月 テスト:1~1.5ヶ月 出荷準備:0.5~1ヶ月 とフェーズが分かれています。 全体としてはウォータフォール型のように見えますが、 アジャイル の手法を取り入れ、2週間ごとのスプリントを回しながら開発を進めています。 (詳しくは、以前の記事もご覧ください! → エンタープライズアプリケーションプラットフォーム「aiuola」におけるアジャイル開発 ) エンハンス開発で対応する案件には、お客様からの要望や、社内で企画した新機能、不具合の修正などが含まれます。 それらの案件は、開発チームの バックログ に登録されます。そして優先度付けを行ったうえで、 6ヶ月 の開発期間に収まるよう対応案件を決定します。 Design Docs とは Design Docs は、直訳すると「設計書」ですが、ソフトウェア開発界隈では、 Google の Design Docs を指すことが多いでしょう。詳細はリンク先のブログ投稿を参照していただきたいのですが、ここではその内容を簡単に説明します。 Design Docs の目的は、ソフトウェアの目的(どのような課題を解決するか)を明確にし、その実現方法と トレードオフ について議論することで、チーム内での認識の統一を図ることです。また、あとから、「なぜこのような意思決定をしたのだろう?」と疑問に思ったとき、過去の Design Docs を参照することでその経緯をたどることも可能となります。 Design Docs には決まったフォーマットはありませんが、一般的に以下のような項目が含まれます。 コンテキスト:このドキュメントを読むにあたり必要となる知識 目標とやらないこと:ソフトウェアや機能が何を目的としたものか、どんな課題を解決するか。逆に、対象外とする課題は何か 設計:システム アーキテクチャ やデータモデル、 API 設計、制約など 代替案:検討した他の設計案と、評価(採用しなかった理由、 トレードオフ など) その他の関心ごと:開発の進め方やテストの手法、セキュリティ対策、パフォーマンス最適化、運用・保守など 作成したドキュメントは、レビュープロセスを経て実装フェーズに進みますが、実装中に設計の見直しが必要になった場合は、ドキュメントを修正し、軌道修正をしながら開発を進めていくことができます。 aiuola 、 Ci*X の Design Docs ここでは、私たちが使っている Design Docs のフォーマットや作り方、レビュープロセスについて、ご紹介します。 (ちなみに、チーム内では「Design Docs」ではなく、「検討資料」と呼んでいます。なぜ「検討資料」にしたのかは忘れてしまいました 笑) Design Docs のフォーマット aiuola 、 Ci*X では、エンハンス開発を行う際、案件ごとに Design Docs を作成することとしています。 Design Docs のフォーマットは、試行錯誤の結果、以下の項目を含めることになっています。項目とその書き方を以下にまとめました。 (実物とは少し異なりますが、大まかな構成は同じです) 概要 要望の背景・解決したい課題 このエンハンス案件によって、どのような課題を解決するかを明らかにします。お客様のご要望はいったん受け止めますが、 aiuola 、 Ci*X はパッケージ製品なので、製品としてどのような対応をするのがベストかを慎重に検討する必要があります。 要件 このエンハンス案件の要件を定義します。必須な要件(Must)、可能なら実現したい要件(Want)をまとめるとともに、実現しないことも(あれば)記載します。 仕様検討 前提知識 このドキュメントを読む人が知っておくべき用語や、既存機能の簡単な説明を記載します。 他社製品の調査 市場に既に存在する製品に、今回の要件を満たせる類似機能が存在する場合、どのような方法で実現しているのか、公開情報を調査します。先行している製品を参考に、よりよい仕様を実装することで、競争力を高めます。 法律・制度の調査 エンタープライズ アプリケーションは、法律や制度によって仕様が自ずと決まってしまう場合があります。法律や制度を正確に理解し、それを遵守した実装を行うことが求められます。 業務の検討 エンタープライズ アプリケーションの場合、業務プロセス内にシステムが登場することを想定した設計を行う必要があります。 仕様案 アプリケーションの仕様案をまとめます。ここでは、いわゆる外部設計に相当する内容を記載します。複数の仕様案がある場合、それらを比較し、このエンハンスバージョンで最適な案を選定します。 制約 例えば、エンハンスバージョンアップ後に 後方互換 がなくなる箇所など、ユーザーに影響のある制約事項があれば、まとめておきます(なるべく、そういった制約が生まれないような仕様案を模索します)。 技術調査 新しい要素技術を導入するなど、実装に入る前に調査・検証が必要となる場合は、その内容と結果を記載します。 設計 この章には、いわゆる内部設計に相当する内容を記載します。 DB設計、 アーキテクチャ 設計、画面設計、バッチ設計、 API 設計、非機能要件の設計など、必要となる項目を洗い出し、記載します。 改修を行わないが、当該案件の影響を受けるため リグレッション テストを実施しておいた方がよい機能も明記しておきます。 その他 外部サービスとの連携で必要な調整など 社外のサービスと連携するような案件の場合、先方との調整が発生することもあるため、そういった情報はまとめておきます。 他チームとの連携で必要な調整など 例えば、インフラ構成の変更が必要な案件など、社内の他のチームにも作業を依頼しなければならない場合、その内容をまとめておきます。 リリース時・リリース後の注意事項 本番環境へのリリース作業中、特別に実施する必要がある作業があれば、その内容を記載しておきます。また、リリース後に利用者側で必ずやらなければならない作業がある場合も、その内容を記載しておきます。 仕様相談やレビューの記録 ドメイン エキスパートやテッ クリード と行う仕様の相談ミーティングや、レビュー時のやり取りを記録に残しておきます。この項があることによって、どうしてこのような仕様になっているのか、あとから追いかけることができます。 私たちは、 Confluence というツールで Design Docs を管理しており、小規模な案件であれば1ページで書ききってしまうのですが、大規模な案件の場合、項目ごとにページを分割したり、階層化したりして、読みやすいドキュメントになるよう工夫しています。 また、上記項目はすべて必須というわけではなく、案件内容によって必要な箇所のみ記載することにしています。 Design Docs のレビュー Design Docs を作成したら、 ドメイン エキスパートやテッ クリード によるレビューを実施します。 案件の規模や内容によっては、すべてを書き終えてレビューを依頼するのではなく、少人数での仕様相談会や途中段階でのレビューを実施することもあります。 仕様相談会やレビュー時のやり取りは、意思決定の根拠として重要であるため、 Design Docs の 仕様相談やレビューの記録 の項に記録しておきます。 実装中にも修正を加えていく レビュープロセスを通過し、いよいよ実装、となりますが、まだ Design Docs の役目は終わりません。 実装中に、設計の見直しが行われたり、新たな影響範囲が見つかったりすることはよくあります。 その場合、 Design Docs を修正し、新たな方針のもと実装を進めていくこととします。 テストと Design Docs テスト担当者は、開発者の作成した Design Docs を参照し、テスト仕様書を作成します。 Design Docs に、影響機能が網羅的に記載されていることで、テスト範囲が明確になります。 なお、実装中も Design Docs の修正は続いているため、実装完了後、テスト実施に入る前に、必ずテスト仕様書にも Design Docs 最終版の内容を反映させることとしています。 Design Docs と仕様ドキュメント Design Docs は、エンハンス案件の開発時に利用するもので、作り捨てのドキュメントです。 完成したアプリケーションの仕様は、 別途バージョン管理している、仕様ドキュメント(画面定義書、バッチ仕様書、 API 仕様書 など)への反映を行います。 過去の Design Docs の管理 ここまで来てようやく Design Docs はその役目を終えることとなります。 過去の Design Docs は、 当該エンハンスバージョンの Design Docs 置き場に格納されます。 おわりに 今回は、私たち aiuola 、 Ci*X チームで利用している Design Docs についてご紹介しました。 元の Google の考え方を参考にしつつ、チームにフィットするようカスタマイズした結果、現時点ではこのような形になっていますが、チームの状況によってこれからもどんどん改善されてよりよいものになっていくと思います。 本記事が、皆様の設計ライフの一助となれば幸いです。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 執筆: @nagai.satoshi レビュー: @miyazawa.hibiki ( Shodo で執筆されました )
はじめに エンタープライズ 第一本部の佐藤です。 本記事は 電通 総研 Advent Calendar 2025 13日目の記事です。 さくらのクラウド がアプリケーションの実 行基 盤となる「AppRun」等の正式なサービスを2025年12月9日から開始しています。 この記事は、AppRunの使用を通じて国産 クラウド の現在地を把握してみるのが本旨となります。 はじめに AppRun 概要 コンテナレジストリのデモ 概要 イメージの作成 レジストリの作成 ユーザーの作成 イメ―ジのpush AppRunのデモ 起動の設定 起動確認 デプロイ戦略 今回作成したリソースの料金 感想 AppRun 概要 「AppRun」は、コンテナイメージをもとにアプリケーションを自動デプロイし、スケーリングや運用管理をシームレスに実行できるマネージドサービスです。 引用元: https://www.sakura.ad.jp/corporate/information/newsreleases/2025/12/09/1968222395/ このAppRunのデモの前に さくらのクラウド 上にイメージをpushしておく必要があるので、以下の手順を実施しています。 コンテナ レジストリ のデモ 概要 「コンテナ レジストリ 」はDockerなどのコンテナエンジンが扱うイメージファイルを保管する レジストリ 機能を さくらのクラウド 上で提供するサービスです。 引用元: https://manual.sakura.ad.jp/cloud/appliance/container-registry/index.html イメージの作成 デモで使用するコンテナのイメージを作成します。 今回試すサーバーはgoで以下のように記述しました。 package main import ( "fmt" "net/http" "log" ) func main() { http.HandleFunc( "/" , func (w http.ResponseWriter, r *http.Request){ log.Printf( "%s %sのリクエストがありました" , r.Method, r.URL.Path) fmt.Fprintf(w, "こんにちは!さくらのクラウド!" ) }) http.HandleFunc( "/health" , func (w http.ResponseWriter, r *http.Request){ log.Printf( "%s からヘルスチェック" , r.RemoteAddr) w.WriteHeader(http.StatusOK) fmt.Fprintf(w, "OK" ) }) log.Println( "Starting server on : 30080" ) err := http.ListenAndServe( ":30080" , nil ) if err != nil { log.Fatal(err) } } イメージをビルドし、 server-demo と名前を付けています。 レジストリ の作成 次にこのイメージをpushする レジストリ を作成します。 手順 通りに作成します。 以下のような画面で作成しました。 📝(余談)アイコンは自分で作成できるので マニュアル を参考に手書きの実家の犬アイコンを自作しました。 ユーザーの作成 以下の画像のユーザーのタブで新規に作成しました。 ユーザー作成時に指定したユーザー名とパスワードでDockerのログインを実施します。 イメ―ジのpush 次にdockerコマンドで server-demo イメージにタグを付与します。 docker tag server-demo:latest <レジストリ名>/server-demo:latest レジストリ へのログインを実施します。 コマンドは以下を実施します。 docker login [コンテナレジストリ名] Username: [ユーザ名] Password: [パスワード] タグ付けした server-demo イメージをpushします。 docker push <レジストリ名>/server-demo:latest このイメージが GUI のコンテナ レジストリ から確認できるのかなと思いましたが、私は見つけられませんでした。 AppRunのデモ 起動の設定 アプリケーションの作成画面で共有型を選択し、起動します。 以下のようにアプリケーションの設定をしました。 コンテナの設定は以下のとおりです。 IPアドレス の指定等も項目でありますが、家のネットワークは IPアドレス が固定できないのでスキップしました。 ログの送信はfluentbit, opentelemetry-collectorを使用するとありましたが、標準/エラー出力はそのままログに記録されるので今回はこれを有効にして確認することにしました。 メトリクスの送信はPrometheus, opentelemetry-collectorが使用できるのですが、検証のための実装が手間だったので省きました。 ログやメトリクスを収集するモニタリングスイートの詳細は https://manual.sakura.ad.jp/cloud/appliance/monitoring-suite/index.html を参照してください。 この設定でアプリケーションを作成し、起動を確認してみます。 起動確認 画像のように起動を確認することができました。 ブラウザで公開URLにアクセスしたところレスポンスも表示されました。 ちなみにログでは以下のようになっています。 10秒間隔でヘルスチェックをしていますが、2025-12-15 00:27:11を最後にこれが止まっており、 トラフィック がないので最初に設定したゼロスケールが実行されていることが確認できます。 この状態で再度URLにアクセスするとレスポンスが返るまで体感で5~6秒かかりました。 デプロイ戦略 アプリケーションの構成を変更してデプロイから、新たなイメージをデプロイします。 新たなイメージは以下の記述を変えただけです。 fmt.Fprintf(w, "こんにちは!さくらのクラウド!v2" ) バージョン情報を確認するとv2が増えました。 この時点ですでに変更済みですが、操作の項目から トラフィック 管理を押下し、50%で分配するようにしました。 2回に1回は以下のようになりました。 今回作成したリソースの料金 サービス 料金 補足 コンテナ レジストリ 月額220円 ストレージ5GiB/1 レジストリ (超過分は1GiBごと22円) AppRun(共有型/0.5vCPU × 1GiB メモリ) 約5円/1時間 - モニタリングスイート(ログ) 月額110円 ユーザ領域 ストレージ1GiB/1件 参照: https://cloud.sakura.ad.jp/products/container-registry/ https://www.sakura.ad.jp/corporate/information/newsreleases/2025/12/09/1968222395/ https://cloud.sakura.ad.jp/news/2025/11/11/3service_formalization/ 感想 ベータ版の時から結構良く見られた感想ですが、設定項目が少なくて簡単でした。 ドキュメントが日本語話者が書いた日本語なのも理解しやすさに貢献していると思います。 設定項目が少ないメリットの一方で、それは柔軟性・拡張性との トレードオフ なのでここは手放しに素晴らしいとは言えないですね。 IaC化も Terraform for さくらのクラウド(v2) で、できます。 個人的にはゼロスケールしてくれるのがいいなと感じており、個人レベルのサービス立ち上げ時に トラフィック がないのに ランニングコスト だけがかかる問題を解消できそうです。 今回の検証で興味が出てきたので、体系的にサービスの概要をつかめるように さくらのクラウド検定 を受けてみようかなと思います。 受験費用が一般 11,000円(税込)で「 さくらのクラウド 」クーポン 50,000円分(一般・学生共通)がもらえるのがいいですね。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @sato.yu レビュー: @miyazawa.hibiki ( Shodo で執筆されました )
こんにちは、 電通 総研のグループ経営ソリューション事業部でエンジニアをしている大浦です。 本記事は 電通総研 Advent Calendar 2025 12日目の記事です。 昨日の記事は、上野さんによる TanStack AI × Amazon Bedrockで作るAIチャットボット でした。 フロントエンドを起点に AI を組み立てる アーキテクチャ の考え方が興味深く面白かったです。 今回は、 モノリス で作られたアプリから、1つの機能をマイクロサービスとして切り出す判断についてお話します。 1 はじめに 1.1 注意点 2 そもそもマイクロサービスを切り出すべきか 2.1 モノリスで得られるPros 2.1.1 安定性 2.1.2 デプロイ容易性 2.1.3 実装容易性 2.2 モノリスのCons 2.2.1 認知負荷の増加 2.2.2 リードタイムの増加、リリースサイクルの長期化 2.2.3 柔軟なスケールが実施できない 2.2.4 ライブラリの衝突 3 切り出すと判断したら何を切り出すか 3.1 切り出す価値が高い機能 3.1.1 リリースサイクルや設定変更の周期が製品本体とは違う 3.1.2 機能固有の依存を持つ 3.1.3 機能単位でスケールアウトなどの構成を取りたい 3.1.4 特定の環境でのみ利用される機能 3.2 切り出しやすい機能 4 どのように切り出すか 4.1 モノリスで実現した場合 4.1.1 通知サービスのインターフェース 4.1.2 通知サービスの実行クラス 4.1.3 build.gradleの依存 4.2 マイクロサービスによる切り出しを行った場合 4.2.1 (補足) マイクロサービスの実装言語としてGoを選んだ理由 4.2.1.1 Goは単一バイナリとして配布できる 4.2.1.2 高並行な I/O 処理を素直に書ける 4.2.1.3 起動が速く、リソース特性が読みやすい 4.2.2 モノリス側(Spring) 4.2.3 切り出したマイクロサービス側 4.3 この例についての補足 (本番運用を考えた場合) 5 まとめ 1 はじめに SaaS アプリケーションの構成として、マイクロサービス化を進めているというサービスについて目にする機会が増えました。 あわせて「最初からマイクロサービスで作るのではなく、 モノリス で作り始めて必要になったらマイクロサービス化しましょう」という意見を聞くことも多いかと思います。 Martin Fowlerも以下のように述べています。 「成功したマイクロサービスのほとんどは、最初からマイクロサービスだったわけではない。 まずモノリスとして作られ、それが肥大化して“割らざるを得なくなった”結果として生まれている。」 「逆に、最初からマイクロサービスとして作られたシステムで、うまくいった例はほとんど聞かない。 私の知る限り、そうした多くは深刻な問題を抱えることになっている。」 ※原文は以下の2点(著者意訳) 1.Almost all the successful microservice stories have started with a monolith that got too big and was broken up 2.Almost all the cases where I've heard of a system that was built as a microservice system from scratch, it has ended up in serious trouble. ref) https://martinfowler.com/bliki/MonolithFirst.html マイクロサービスに分割断面を見出し「分割する」という話は書籍やネットで見つけることができます。 ( ドメイン 境界で切りましょう。チーム構成で切りましょう。など) 本記事では、大きな分割断面を見つけるのではなく、 モノリス をマイクロサービス化するための第一歩として 「機能を切り出す」ということにフォーカスした話をします。 ビジネスの拡大に合わせて成長してきた モノリス アプリを前提に以下の3点を扱います。 そもそもマイクロサービスを切り出すべきか 切り出すと判断したら何を切り出すか どのように切り出すか 1.1 注意点 以降は筆者が今までに モノリス から機能を切り出す or 切り出さない判断をしてきた中で考えてきたことを元に記載しています。 正解を提示するよりは、判断のための問いや観点を共有することを目的としています。 もし、誰かにとって議論のきっかけになればとても嬉しいです。 2 そもそもマイクロサービスを切り出すべきか 「プロジェクト開始早々にマイクロサービスにするな。」という意見が広く納得されている背景として、 モノリス ならではの Pros/Cons があります。 マイクロサービス化は、「現状の モノリス から得られるProsを失ってでも、Consを解消する価値がある」と合意出来ている場合に実施を検討します。 2.1 モノリス で得られるPros 2.1.1 安定性 エンタープライズ アプリでは基本的に、ACID特性を持つ トランザクション が重要です。 モノリス 構成(=多くの場合単一のDBで構成)である限り、 トランザクション の整合性をシンプルに保てます。 もし機能を 疎結合 にするために複数のDBにまたがった トランザクション を構成する場合には、分散 トランザクション や結果整合性など、より複雑な判断が必要です。 2.1.2 デプロイ容易性 モノリス は通常デプロイ単位が1つであり、実行や停止、スケールをまとめて実施できます。ログも集めやすいです。 特にオンプレミス環境での動作が求められる場合には、デプロイ単位が1つであることで、運用における各種手順を容易にできるなどのメリットもあります。 2.1.3 実装容易性 モノリス は基本的に、 コンパイル 時にアプリ全体が含まれます。 Java などの静的型付け言語では、型やインターフェースの不一致を実行前に コンパイル エラーとして検知できます。 2.2 モノリス のCons 2.2.1 認知負荷の増加 製品のコードベースが大きくなるにつれて、1つの機能変更の影響範囲が広がりやすくなります。 これは、コードベースの認知負荷の増加に繋がります。 2.2.2 リードタイムの増加、リリースサイクルの長期化 変更時の影響範囲の広がりは、修正時のビルド時間、検証事項の増加、変更時の 回帰テスト 範囲の増加に繋がります。 この結果、リードタイムが伸び、変更が慎重に実施されることでリリースサイクルが長くなる、という問題に繋がります。 これは、新機能の知見を持ったチームがPoCを実施する場合の足かせになります。 2.2.3 柔軟なスケールが実施できない 単一ファイルとしてデプロイする以上、特定の機能だけスケールさせる。といったことはできません。 例えば、特定の時期にだけ重い処理がある場合でも、アプリケーション全体をまとめてスケールアップさせる必要があります。 また、単一デプロイ単位で大きくなるとメモリフットプリントが増加し、スケール時に重要な起動時間の増加にもつながります。 2.2.4 ライブラリの衝突 外部ライブラリなどの依存追加により、ライブラリの衝突などがおこるようになる可能性が高まります。 3 切り出すと判断したら何を切り出すか では、製品が育ってきて モノリス の辛みが顕在化してきた場合に、どの機能を切り出すかを考えてみます。 切り出しは多くの場合、少なくとも以下の2軸を検討する必要があります。 切り出す価値があるか 切り出しやすいか 以下に、切り出す価値が高いと考える機能の性質を記載します。 切り出す価値が、切り出しやすさやマイクロサービス化の辛みを引き受けてでも実現する価値があるかを検討し、切り出しポイントを決めましょう。 3.1 切り出す価値が高い機能 3.1.1 リリースサイクルや設定変更の周期が製品本体とは違う 競争優位性を得るために迅速な フィードバックループ を回すことが求められる機能は、切り出して本体と独立してリリースできるとビジネス上の効果が高いです。 「○○機能チーム」など特定のチームが担当している機能の場合には、他チームとのコミュニケーションパスを限定することで生産性を高めることにつながることもあります。 3.1.2 機能固有の依存を持つ サードパーティー SDK が必要である機能や、外部サービスへの依存が強い機能を切り出すと、製品本体の依存関係を単 純化 できます。 依存ライブラリが増えると、ライブラリ衝突のような問題に突き当たることがあり、依存が増えるごとにリスクは高まります。 また、もしライブラリの 脆弱性 などが出た場合にも影響範囲の局所化や修正の反映が迅速になります。 ライブラリ依存を増やさないようにするための手段としての切り出しも価値があると考えています。 3.1.3 機能単位でスケールアウトなどの構成を取りたい BI処理など、データの増加に伴いリソース使用量や時間の増加が大きい機能は、切り出すと個別のワークロードを見たスケール判断ができます。 また、月末だけ高負荷がかかるなど ピーキー な振る舞いを持つ機能は、メインのアプリはそのままでスケールアップ/スケールアウトが実現できます。 3.1.4 特定の環境でのみ利用される機能 アドオンやオプション機能など、利用有無が顧客や環境によって異なる機能を切り出せると、製品本体品質の維持につながります。 本体から分離することで、認知負荷やテスト範囲を抑えられます。 もし モノリス の中で実現する場合、アドオンやオプションの変更が製品本体の再テストやバージョンアップの足かせとなることがあります。 3.2 切り出しやすい機能 切り出しやすさについても記載します。 ただ、切り出しやすさよりも切り出す価値の方が重要です。 切り出す価値が高い機能であっても、以下のような性質がある場合には切り出しコストに見合わないことが多く、切り出しを行わない根拠になると考えます。 データの所有権が分離できない 厳密な同期 トランザクション が必須 境界仕様が頻繁に変わる 4 どのように切り出すか ここでは、具体例として Spring Framework で作成されている経費申請アプリケーションの通知機能を切り出します。 Slack, Teams, LINE, メールと、複数の通知先をもつケースを想定しています。 最初に留意点を記載します。 以下の通知は、マイクロサービス分割の中でも比較的易しい題材となります。 = 状態をほぼ持たず、結果整合性を許容しやすく、失敗しても業務停止になりにくい だからこそ、 モノリス からの最初の切り出しを試すうえで有効と考えます。 また、本番運用をするうえで考慮が必要なポイントについては、「4.3 この例についての補足 (本番運用を考えた場合)」として後述しています。 4.1 モノリス で実現した場合 まずは、Spring Bootで作られた モノリス アプリです。 マイクロサービスとして切り出す前の状態として提示しています。 ※今後の説明に重要な部分のみ記載しています。 ※通常のSpringアプリの範囲なので全ては説明していません。 もし動くコードを見たい場合は、以下のコードと共に生成AIに聞くことで生成してくれると思います。 4.1.1 通知サービスのインターフェース Java においては以下のようなインターフェースがあります。 public enum NotificationChannel { SLACK, TEAMS, LINE, EMAIL } public record NotificationRequest( NotificationChannel channel, String to, // SlackのチャンネルID / メールアドレス / LINEのuserId など String subject, // 表題 String message // 本文 ) {} public interface NotificationService { void send(NotificationRequest request); } 4.1.2 通知サービスの実行クラス NotificationServiceを実装したクラスNotificationServiceImplは以下です。 ※大規模 エンタープライズ アプリケーションであればStrategyパターンを用いて、各通知先ごとにクラスを作る構成にすることが多いと考えますが、ここでは簡単のため、全ての実処理をNotificationServiceImpl内に記載しています。 // package jp.co.example.notification.infrastructure; @Service public class NotificationServiceImpl implements NotificationService { @Override public void send(NotificationRequest request) { switch (request.channel()) { case SLACK -> sendSlack(request); case TEAMS -> sendTeams(request); case LINE -> sendLine(request); case EMAIL -> sendEmail(request); default -> throw new IllegalArgumentException( "Unsupported channel: " + request.channel()); } } private void sendSlack(NotificationRequest request) { // Slack用の通知処理 } private void sendTeams(NotificationRequest request) { // Teams用の通知処理 } private void sendLine(NotificationRequest request) { // Line用の通知処理 } private void sendEmail(NotificationRequest request) { // Eメール用の通知処理 } } 4.1.3 build.gradleの依存 各種通知処理に SDK を利用する場合、build.gradleやpom. xml にSlackやLineなどの SDK への依存が追加されます。 この通知のためにしか使わない多くの SDK が追加され、Jar Hellの危険が増します。 4.2 マイクロサービスによる切り出しを行った場合 ここでは、実際の通知処理をGoで書かれたマイクロサービスに切り出してみます。 4.2.1 (補足) マイクロサービスの実装言語としてGoを選んだ理由 マイクロサービス側の実装言語は、必ずしも Go である必要はありません。 Java 、Kotlin、Node.js、 Python など、要件を満たす選択肢は複数あります。 チームの技術スタックを統一したい場合や、重厚な ドメイン ロジックを持つ機能を切り出す場合には、 Java を選ぶことも十分に妥当と考えます。 本記事の例でGoを選んでいる理由は、 「通知機能を切り出したマイクロサービス」という文脈において、運用・構成のコストが低い と判断しているためです。 具体例をいくつか挙げます。 4.2.1.1 Goは単一バイナリとして配布できる Goでビルドしたアプリケーションは、単一の実行バイナリとして配布できます。 JVM やランタイム、依存ライブラリを別途考慮する必要がなく、以下のような運用上のメリットがあります。 デプロイ、プロセス常駐のための運用手順が単純 実行環境の差異によるトラブルが起きにくい 4.2.1.2 高並行な I/O 処理を素直に書ける 通知処理は、多くの場合(CPUバウンドではなく) I/O バウンドです。 Webhookや外部 API 呼び出しを多数並列に処理するケースでは、goroutineによる軽量スレッドが実装と保守の両面で扱いやすいと感じています。 (また主観ですが、async/awaitや.then()などを駆使するよりも個人的にgoroutineの方が読みやすいです)。 4.2.1.3 起動が速く、リソース特性が読みやすい Goアプリケーションは起動が速く、メモリ使用量も比較的予測しやすいです。 これは、以下のような場面で効いてきます。 オートスケール時の立ち上がり 障害復旧時の再起動 4.2.2 モノリス 側(Spring) まず、 Java 側は通知を呼び出すだけにします。 = 通知処理を切り出したマイクロサービスに委譲する。 @Service public class NotificationServiceImpl implements NotificationService { private final RestTemplate restTemplate; private final String baseUrl = "http://localhost:8081" ; // goプロセスのURL, 本来は設定から読み込む @Override public void send(NotificationRequest request) { try { String url = baseUrl + "/api/v1/notify" ; restTemplate.postForLocation(url, request); log.info( "Notification delegated: channel={}, to={}" , request.channel(), request.to()); } catch (RestClientException e) { log.error( "Notification delegation failed: channel={}, to={}" , request.channel(), request.to(), e); // 恐らくここは設計判断が必要、ログだけ出して握り潰す?業務エラーにする? throw new RuntimeException( "Failed to delegate notification" , e); } } } 通知を依頼するためのHTTPリクエストを投げるだけになっています。 実際に通知をする処理を委譲したことで、通知先毎の各種 SDK はアプリ本体側では不要になりました。 実際に送信されるリクエストは以下のような構造になる想定です 例) Slackへの承認依頼Post { " channel ": " SLACK ", " to ": " U0456ABCD ", " subject ": " 経費申請の承認依頼 ", " message ": " 【承認依頼】 \n 申請者: 山田太郎 \n 申請番号: EXP-2025-00123 \n 金額: 12,480円 \n 申請日: 2025/02/10 \n\n 以下のリンクから内容を確認し、承認または差戻しをお願いします。 \n https://expense.example.com/approval/EXP-2025-00123 " } 4.2.3 切り出したマイクロサービス側 Go側では、通知リクエストから適切な通知先に通知を送ります。 package main import ( "encoding/json" "log" "net/http" ) // 通知のリクエスト構造 type NotificationRequest struct { Channel string `json:"channel"` To string `json:"to"` Subject string `json:"subject"` Message string `json:"message"` } func main() { http.HandleFunc( "/api/v1/notify" , NotifyHandler) log.Println( "Notification Service running on :8081" ) if err := http.ListenAndServe( ":8081" , nil ); err != nil { log.Fatal(err) } } func NotifyHandler(w http.ResponseWriter, r *http.Request) { var req NotificationRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { log.Println( "Invalid request:" , err) http.Error(w, "invalid request" , http.StatusBadRequest) return } // 即レスポンスを返す(Spring側を待たせない) w.WriteHeader(http.StatusAccepted) // 通知を非同期で処理(goroutine利用) go func (req NotificationRequest) { switch req.Channel { case "SLACK" : sendSlack(req) case "TEAMS" : sendTeams(req) case "LINE" : sendLine(req) case "EMAIL" : sendEmail(req) default : log.Printf( "[goroutine] unsupported channel: %s \n " , req.Channel) return } log.Printf( "[goroutine] completed notification: channel=%s to=%s \n " , req.Channel, req.To) }(req) } func sendSlack(req NotificationRequest) { // Slack SDK or Webhook を呼ぶ処理 log.Printf( "[Slack] sending to %s \n message: \n %s \n " , req.To, req.Message) } func sendTeams(req NotificationRequest) { // Teams SDK or Webhook を呼ぶ処理 log.Printf( "[Teams] sending to %s \n message: \n %s \n " , req.To, req.Message) } func sendLine(req NotificationRequest) { // Line SDK or Webhook を呼ぶ処理 log.Printf( "[LINE] sending to %s \n message: \n %s \n " , req.To, req.Message) } func sendEmail(req NotificationRequest) { // Eメールを送信する処理 log.Printf( "[Email] sending to %s \n subject=%s \n message: \n %s \n " , req.To, req.Subject, req.Message) } 4.3 この例についての補足 (本番運用を考えた場合) 前述のGo実装は説明用の最小構成です。 もしこのまま本番に出してしまうと、以下のような問題が起きます。 goroutine実行中にプロセスが落ちた場合、 モノリス 側では成功したように見えるが実際には通知されない。(アプリ側のみ正常終了など) 二重送信が発生しても止められない。(通知処理完了後にアプリ側の処理が異常終了、マイクロサービス側のみ正常終了など) 障害時に「どの申請の通知が失敗したか」追えない。 上記に対応しようとする場合は、以下のような対応が検討対象になります。 相関IDの追加 申請番号など、 モノリス 側との一連の処理を紐づける情報を加える。 リトライ + バックオフ + デッドレターキュー(DLQ)の追加 通知先サービスが一時的に落ちたときのために再処理を可能にする仕組みを加える。 マイクロサービス側のプロセスが落ちても適切に動作するように、マイクロサービス側でDBを持つなど。 べき等キーの追加 申請書番号 + イベント種別 + 通知先 などを覚えておき、通知済みの場合は通知しない。 メトリクスとアラートの設定 通知の成功率や遅延、DLQ件数 などを監視対象にする。 このように、割と簡単な例でも本番で運用する場合には考慮すべきポイントが数多くあります。 この辺りは、 モノリス のConsとマイクロサービスのProsだけを見ている場合に見落とすことがあります。 できる限り、切り出した場合の大変さについても切り出しの検討段階で考慮したいですね。 5 まとめ 今回は、 モノリス で作られたアプリから、1つの機能をマイクロサービスとして切り出す判断について書いてきました。 そもそもマイクロサービスを切り出すべきか → マイクロサービス化する場合は、 モノリス のPros / Consを意識する。 → 特に、 モノリス のProsを考える。 切り出すと判断したら何を切り出すか → 切り出すことで得られる価値が高い機能を切り出す。 → 価値が高い機能であっても、切り出しやすさを含め トレードオフ を見る。 どのように切り出すか → 第一歩は、状態をほぼ持たず、結果整合性を許容しやすく、失敗しても業務停止になりにくい機能を対象にする。 → 本番運用において、マイクロサービス化で問題となる特別な考慮ポイントについて十分に検討する。 最後に、ここまではおおむねアーキテクトの観点でのみ切り出す判断を書いてきています。 実際のプロダクト開発においては、失うProsが運用に直撃します。 そのため、切り出し判断はアーキテクトだけでなく、保守/運用・SRE・QA・プロダクトチームなどを巻き込んで合意することになると思います。 今後も、製品の成功のために出来ることを考え周囲と議論しながら、製品開発を推進していきたいと考えています。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @oura.osamu レビュー: @nagamatsu.yuji ( Shodo で執筆されました )
こんにちは。 電通 総研の金融IT本部(兼XI本部)の上野です。 本記事は 電通総研 Advent Calendar 2025 11日目の記事です。 最近一番嬉しかったことと言えば、 AWS のコンソール検索で「ECR」と入力すると一発と入力するとコンテナ レジストリ へ飛べるようになったことです。 「ECSからの遷移」や「container registry」と検索していたのは私だけではないはずです。 さて、本日は最近登場したTanStackエコシステムの一つ、 TanStack AI を利用してチャットボットを作ってみました。様々なエコシステムで知られるTanStackですが、AIアプリケーション開発の分野に参入していました。 本記事では、TanStack AI の特徴や Vercel AI SDK との違いを解説し、 Amazon Bedrock と組み合わせてファイル添付対応のチャットボットを構築する方法を紹介します。 TanStack AIとは 主な特徴 TanStack AI が生まれた背景 TanStack AI vs Vercel AI SDK TanStack AI を選ぶべきケース Vercel AI SDK を選ぶべきケース TanStack AI の実装例 クライアント実装例 サーバー実装例 Vercel AI SDKの利用例(比較用) クライアント側の実装例 サーバー側の実装例 Amazon Bedrock とは Amazon Bedrock の特徴 Amazon Bedrock を使うメリット 他のクラウド AI サービスとの比較 利用可能なモデル 主な機能比較 選定の指針 TanStack AI × Amazon Bedrockとの接続 サーバー側の実装例 Amazon Bedrock アダプタークラス Express サーバー クライアント側の実装例 カスタム Connection Adapter useChat フックの使用 実装にあたってのポイント(1)StreamChunk の形式 実装にあたってのポイント(2)Extended Thinking(思考プロセス)のサポート ファイル添付のサポート デモ まとめ TanStack AIとは TanStack AI は、複数の AI プロバイダーに対応した統一インターフェースを提供する オープンソース SDK です。 TanStack AI Alpha: Your AI, Your Way において「AI ツールのスイスを目指す」と述べており、「特定のベンダーやプラットフォームに依存しない中立的な設計を意識して作られた」という意味だと理解しています。 TanStack AI takes a different approach. We're building the Switzerland of AI tooling. An honest, open source set of libraries (across multiple languages) that works with your existing stack instead of replacing it. 主な特徴 ベンダーロックインなし: OpenAI、Anthropic、 Google Gemini、Ollama など複数のプロバイダーに対応 完全な型安全性: TypeScript でエンドツーエンドの型安全を実現 フレームワーク 非依存:React、Solid、Vanilla JS など任意の環境で利用可能 マルチモーダル対応:テキスト、画像、音声、動画、ドキュメントをサポート MIT ライセンス: 完全な オープンソース TanStack AI が生まれた背景 TanStack の創設者 Tanner Linsley 氏は、既存の AI SDK (Vercel AI SDK ) に対する懸念から TanStack AI を開発しました。 TanStack AI は、これらの課題を解決する「中立的な」 SDK として設計されています。Cloudflare や Netlify がスポンサーとして参加しているのも、オープンで移植可能なツールが彼らのプラットフォームにも恩恵をもたらすからだと考えています。 TanStack AI vs Vercel AI SDK 項目 TanStack AI Vercel AI SDK 哲学 中立・ オープンソース Next.js/Vercel エコシステム統合 プロバイダー数 4(OpenAI, Anthropic, Gemini, Ollama) 多数 ストリーミング制御 ステート管理して明示的に管理 HTTP ストリーム生成までを強くサポート 成熟度 アルファ版(2025年後半〜) 安定版 デプロイ先 どこでも( AWS , Cloudflare, Netlify 等) どこでも、ただしVercel 最適化 現時点では圧倒的にVercel AI SDK が高機能ですが、Vercel提供の SDK はどうしてもプラットフォーム依存部分が出てきます。 標準 OSS として利用可能ではありますが、Next.jsと密接に統合されており、Vercelデプロイ時に最適化されています。例えばVercel Functionsの タイムアウト 指定などもセットで利用可能になっている等、Vercelデプロイしない場合は不要な機能がいくつかあります。 TanStack AI を選ぶべきケース Vercel 以外の環境( AWS 、Cloudflare 等)にデプロイする 将来的にプロバイダーを切り替える可能性がある TanStack エコシステムを使用している Vercel AI SDK を選ぶべきケース Next.js + Vercel で開発している 今すぐ本番環境で使いたい(TanStack AI はまだアルファ版) 多数のプロバイダーに対応したい 豊富なドキュメントと事例が必要 TanStack AI の実装例 クライアント実装例 import { useChat } from '@tanstack/ai-react' import { fetchServerSentEvents } from '@tanstack/ai-react/connection' function Chat () { const { messages , sendMessage , isLoading } = useChat( { connection : fetchServerSentEvents( '/api/chat' ), } ) const handleSubmit = ( e : React.FormEvent ) => { e. preventDefault () const input = e. currentTarget . querySelector ( 'input' ) if (input?.value) { sendMessage(input.value) input.value = '' } } return ( < div > < div className = "messages" > { messages. map (( msg ) => ( < div key = { msg. id } className = { `message ${ msg.role } ` } > { msg.content } </ div > )) } </ div > < form onSubmit = { handleSubmit } > < input type = "text" placeholder = "メッセージを入力..." /> < button type = "submit" disabled = { isLoading } > 送信 </ button > </ form > </ div > ) } サーバー実装例 import { chat, toStreamResponse } from '@tanstack/ai' import { openai } from '@tanstack/ai-openai' export async function POST ( request : Request ) { const { messages } = await request.json() const response = await chat( { provider : openai( { apiKey : process .env.OPENAI_API_KEY } ), model : 'gpt-4o' , messages , } ) return toStreamResponse(response) } Vercel AI SDK の利用例(比較用) TanStack AIとの差分 submit時の sendMessage(input.value) 相当の処理は、 useChat フックがよしなにやってくれる。 クライアント側の実装例 import { useChat } from 'ai/react' export function Chat () { const { messages , input , handleInputChange , handleSubmit , isLoading , } = useChat( { api : '/api/chat' , } ) return ( < div > < div className = "messages" > { messages. map (( msg ) => ( < div key = { msg. id } className = { `message ${ msg.role } ` } > { msg.content } </ div > )) } </ div > < form onSubmit = { handleSubmit } > < input type = "text" placeholder = "メッセージを入力..." value = { input } onChange = { handleInputChange } /> < button type = "submit" disabled = { isLoading } > 送信 </ button > </ form > </ div > ) } サーバー側の実装例 import { streamText } from 'ai' import { openai } from '@ai-sdk/openai' export async function POST(req: Request) { const { messages } = await req.json() const result = streamText({ model: openai('gpt-4o'), messages, }) // ストリーミングレスポンスをそのまま返す return result.toDataStreamResponse() Amazon Bedrock とは Amazon Bedrockは、 AWS が提供するフルマネージド AI サービスです。Anthropic Claude、Meta Llama、 Amazon Titanなど、複数の基盤モデルを統一された API で利用できます。 Amazon Bedrock の特徴 マルチモデル : 複数ベンダーのモデルを同一 API で利用 IAM 統合 : AWS の認証・認可基盤を活用 VPC 対応 : PrivateLink でセキュアな接続 リージョン : 東京(ap-northeast-1)で利用可能 従量課金 : AWS 請求に統合、予測可能なコスト管理 Amazon Bedrock を使うメリット AWS の IAM による認証・認可を利用したい VPC PrivateLink でセキュアな接続をしたい AWS の請求に統合してコスト管理をしたい CloudTrail で アクセスログ を取得したい 他の クラウド AI サービスとの比較 マルチモデル AI サービスは各 クラウド ベンダーが提供しています。 AWS の Bedrock、 Google Cloud の Vertex AI、 Microsoft の Azure AI Foundry(旧 Azure AI Studio)を比較してみます。 ※名称や提供モデル・機能は逐次変化するため、公式の情報を参照してください。 利用可能なモデル プロバイダー Amazon Bedrock Vertex AI Azure AI Foundry Anthropic Claude ✅ Claude 4, 3.5, 3 系 ✅ Claude 3.5, 3 系 ❌ OpenAI GPT ❌ ❌ ✅ GPT-4o, GPT-4, GPT-3.5 Google Gemini ❌ ✅ Gemini 2.0, 1.5 ❌ Meta Llama ✅ Llama 3系 ✅ Llama 3系 ✅ Llama 3系 Mistral ✅ Mistral Large, Small ✅ Mistral ✅ Mistral Large 自社モデル Amazon Nova, Triton Gemini Phi-4, Phi-3 主な機能比較 機能 Amazon Bedrock Vertex AI Azure AI Foundry 統一 API Converse API Gemini API / Model Garden Azure OpenAI + Azure AI Inference ファインチューニング 可 可 可 RAG 機能 Knowledge Bases Vertex AI Agent Builder(統合中?Vertex AI Search) Azure AI Search 連携 エージェント Bedrock Agents Vertex AI Agent Builder Azure AI Foundry Agent Service ガードレール Guardrails for Bedrock Responsible AIツール群 Content Safety ※ファインチューニングはモデルによって可不可があります。 選定の指針 現時点ではモデルベースで利用基盤を選ぶことになるかと思います。 実際は各サービスのモデルカタログを見ながら、要件にあったモデルが使えるかどうか、対象のリージョンで提供されているかどうかを加味して比較する必要があります。 Claude を使いたい : Amazon Bedrock または Vertex AI OpenAI GPT を使いたい :Azure AI Foundry Gemini を使いたい :Vertex AI 今回は使い慣れた AWS 環境で Claude を利用したかったため、 Amazon Bedrock を選択しました。 TanStack AI × Amazon Bedrockとの接続 TanStack AI と Amazon Bedrock を接続する方法を説明する前に、TanStack AI の 2 層構造+呼び出すAIプロバイダーで、合計3つの役割に分かれていることを理解する必要があります。 クライアント層 useChat + Connection Adapter (SSE/HTTP/WebSocket) SSE ストリームのリクエストとレスポンスハンドリング fetchServerSentEvents 、 fetchHttpStream など サーバー層 chat() + Provider Adapter (OpenAI/Anthropic/etc) @tanstack/ai-openai 、 @tanstack/ai-anthropic など AI プロバイダー OpenAI / Anthropic / Google Gemini / Ollama サポートされていない場合は自前で実装が必要 TanStack AI は現在アルファ版であり、公式にサポートされているAIプロバイダーは以下の 4 つです: OpenAI ( @tanstack/ai-openai ) Anthropic ( @tanstack/ai-anthropic ) Google Gemini ( @tanstack/ai-gemini ) Ollama ( @tanstack/ai-ollama ) Amazon Bedrock 用のアダプターは公式には提供されていません 。Anthropic の Claude を使いたい場合、 @tanstack/ai-anthropic で直接 Anthropic API に接続することは可能ですが、 Amazon Bedrock 経由で利用したい場合はカスタムアダプターを実装する必要があります。 TanStack AIは公式にカスタム可能と謳っているため、本来クライアント層/サーバー層ともに Bedrockのアダプターを作成して接続したかったのですが、TanStack AIが未だα版であり、公開情報も少ないためにサーバサイドは自前で接続処理を設けます。 今回の実装方針は以下のとおりです。 サーバー側 : Amazon Bedrock SDK を直接使用し、TanStack AI の StreamChunk 形式で通信する クライアント側 :TanStack AI の useChat フック + カスタム Connection Adapter でレスポンスを受け取る この方法により、TanStack AI の useChat フックの恩恵(メッセージ管理、ローディング状態、型安全性)を受けながら、 Amazon Bedrock と接続できます。 サーバー側の実装例 Amazon Bedrock アダプタークラス AWS SDK を使用して Amazon Bedrock の Converse API を呼び出し、TanStack AI の StreamChunk 形式でストリームを返します。 import { BedrockRuntimeClient, ConverseStreamCommand, } from '@aws-sdk/client-bedrock-runtime' export interface StreamChunk { type : 'content' | 'thinking' | 'done' | 'error' id ?: string model ?: string timestamp ?: number delta ?: string // 増分トークン content ?: string // 累積コンテンツ role ?: string // 'assistant' finishReason ?: string error ?: string } // Adapterと言っているが、TanStack AIとのアダプタではなく単にawsのsdkから接続するBedrockへのAdapter export class BedrockAdapter { private client : BedrockRuntimeClient private modelId : string async * chatStream ( messages , options ): AsyncGenerator < StreamChunk > { const command = new ConverseStreamCommand( { modelId : this .modelId, messages : this .formatMessages(messages), additionalModelRequestFields : options.enableThinking ? { thinking : { type : 'enabled' , budget_tokens : 5000 } } : undefined , } ) const response = await this .client. send (command) for await ( const event of response.stream!) { if (event.contentBlockDelta?.delta) { const delta = event.contentBlockDelta.delta if ( 'thinking' in delta) { yield { type : 'thinking' , delta : delta.thinking, ... } } else if ( 'text' in delta) { yield { type : 'content' , delta : delta.text, role : 'assistant' , ... } } } if (event.messageStop) { yield { type : 'done' , finishReason : 'end_turn' } } } } } Express サーバー Bedrock アダプターを使用し、SSE 形式でクライアントに送信します。 import express from 'express' import { bedrock } from './adapters/bedrock-adapter.js' const app = express() const adapter = bedrock( { region : process .env.AWS_REGION || 'us-east-1' , modelId : process .env.BEDROCK_MODEL_ID || 'claude-sonnet-4' , } ) app.post( '/api/chat' , async ( req , res ) => { const { messages } = req. body // SSE ヘッダー設定 res.setHeader( 'Content-Type' , 'text/event-stream' ) res.setHeader( 'Cache-Control' , 'no-cache' ) res.setHeader( 'Connection' , 'keep-alive' ) const stream = adapter.chatStream(messages, { systemPrompt : 'Hello World' , enableThinking : true , } ) for await ( const chunk of stream) { res. write ( `data: ${ JSON . stringify (chunk) } \n\n ` ) if (chunk. type === 'done' || chunk. type === 'error' ) break } res.end() } ) クライアント側の実装例 カスタム Connection Adapter TanStack AI の ConnectionAdapter インターフェースを実装し、サーバーからのレスポンスを処理します。 import { useChat, type ConnectionAdapter } from '@tanstack/ai-react' import type { UIMessage, ModelMessage, StreamChunk } from '@tanstack/ai' // カスタム Connection Adapter function createBedrockConnection(): ConnectionAdapter { return { async *connect( messages: UIMessage[] | ModelMessage[], _data?: Record < string , unknown > , abortSignal?: AbortSignal ): AsyncIterable < StreamChunk > { // メッセージを Bedrock 形式に変換 const chatMessages = messages.map (( msg ) => ( { role : msg.role, content : extractTextContent(msg), } )) const response = await fetch ( '/api/chat' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body : JSON . stringify ( { messages : chatMessages } ), signal : abortSignal, } ) const reader = response. body ?.getReader() const decoder = new TextDecoder () let buffer = '' while ( true ) { const { done , value } = await reader!.read() if (done) break buffer += decoder. decode (value, { stream : true } ) const lines = buffer. split ( ' \n ' ) buffer = lines. pop () || '' for ( const line of lines) { if (line. startsWith ( 'data: ' )) { const data = JSON . parse (line. slice ( 6 )) as StreamChunk yield data } } } } , } } useChat フックの使用 カスタム Connection Adapter を useChat に渡して使用します。 function App () { const connection = useMemo(() => createBedrockConnection(), [] ) // TanStack AI の useChat フック const { messages , sendMessage , isLoading } = useChat( { connection , } ) return ( < div > { messages. map (( message ) => ( < div key = { message. id } className = { message.role } > { message.parts. map (( part , idx ) => { if (part. type === 'thinking' ) { return < div key = { idx } className = "thinking" > { part.content } </ div > } if (part. type === 'text' ) { return < div key = { idx } > { part.content } </ div > } return null } ) } </ div > )) } < form onSubmit = { ( e ) => { e. preventDefault () sendMessage(input) } } > < input type = "text" /> < button disabled = { isLoading } > 送信 </ button > </ form > </ div > ) } 実装にあたってのポイント(1)StreamChunk の形式 TanStack AI の useChat が正しく動作するためには、サーバーから返す SSE が以下の StreamChunk 形式に準拠している必要があります: // ContentStreamChunk { type : 'content' , delta: '新しい' , content: '新しい' , role: 'assistant' , id, model, timestamp } // ThinkingStreamChunk { type : 'thinking' , delta: '考え中...' , content: '考え中...' , id, model, timestamp } // DoneStreamChunk { type : 'done' , finishReason: 'end_turn' , id, model, timestamp } // ErrorStreamChunk { type : 'error' , error: 'エラーメッセージ' } この形式に準拠していれば、 useChat は自動的にメッセージを UIMessage に変換し、 thinking は ThinkingPart として、 content は TextPart として管理されます。 実装にあたってのポイント(2)Extended Thinking(思考プロセス)のサポート Claude Sonnet 4 / Opus 4 では Extended Thinking 機能が利用可能です。この機能を有効にすると、Claude が回答を生成する前の「思考過程」をストリーミングで取得できます。 // Thinking 有効時のレスポンス例 { type : 'thinking' , delta: 'ユーザーは...' , content: 'ユーザーは...' } { type : 'thinking' , delta: 'について質問して' , content: 'ユーザーは...について質問して' } { type : 'content' , delta: 'こんにちは!' , content: 'こんにちは!' } { type : 'content' , delta: 'ご質問に' , content: 'こんにちは!ご質問に' } { type : 'done' , finishReason: 'end_turn' } UI 側で thinking タイプのチャンクを別途表示することで、AI の思考過程を可視化できます。 ファイル添付のサポート これだけだと面白くないので、ファイル添付をサポートしてみます。 こちらもサーバーサイドが TanStack AI の場合はカスタムが難しそうですが、今回はサーバについてはBedrockの SDK を利用しているだけなので、クライアント側でファイル情報を管理する変更がほとんどです。 なお、Bedrock では Claude 系のモデルの場合以下の形式をサポートしています。 カテゴリ 形式 画像 JPEG , PNG , GIF, WebP ドキュメント PDF, CSV , DOC, DOCX, XLS, XLSX, HTML, TXT, MD 1. 添付ファイルの管理 TanStack AI の sendMessage はテキストのみを受け取るため、添付ファイルは別途管理します。 // 添付ファイルの型定義 interface Attachment { type : 'image' | 'document' mediaType : string // 'image/png', 'application/pdf' など data : string // Base64 エンコード済みデータ name : string } function App () { const [ pendingAttachments , setPendingAttachments ] = useState< Attachment []>( [] ) const attachmentsRef = useRef< Attachment []>( [] ) // ... } 2. ref を使った添付ファイルの受け渡し // カスタム Connection Adapter(添付ファイル対応) function createBedrockConnection ( getAttachments : () => Attachment []): ConnectionAdapter { return { async * connect ( messages , _data , abortSignal ) { const chatMessages = messages. map (( msg , index ) => ( { role : msg.role, content : extractTextContent(msg), // 最後のユーザーメッセージにのみ添付ファイルを付与 ...(msg.role === 'user' && index === messages. length - 1 ? { attachments : getAttachments() } : {} ), } )) // fetch でサーバーに送信... } , } } // 使用側 const connection = useMemo( () => createBedrockConnection(() => attachmentsRef. current ), [] ) 3. 送信時のタイミング制御 sendMessage を呼ぶ 前に attachmentsRef で管理するファイルを送信します。 const handleSendMessage = async () => { // 送信前に attachmentsRef を設定(connect 関数から参照される) attachmentsRef. current = [ ...pendingAttachments ] // UI 表示用に保存 setAttachments(( prev ) => [ ...prev, ...pendingAttachments ] ) // 入力をクリア setInput( '' ) setPendingAttachments( [] ) // メッセージ送信(この時点で connect が呼ばれ、attachmentsRef を参照) await sendMessage(messageText) // 送信完了後にクリア attachmentsRef. current = [] } 4. サーバ側での添付 サーバー側では、添付ファイルを追加する formatMessages関数を設け、 Converse API に渡す前にメッセージを整形します。 private formatMessages(messages: ChatMessage[]): BedrockMessage[] { return messages. map (( msg ) => { const content: ContentBlock [] = [] // テキストコンテンツ if (msg.content) { content. push ( { text : msg.content } ) } // 添付ファイル if (msg.attachments) { for ( const attachment of msg.attachments) { if (attachment. type === 'image' ) { content. push ( { image : { format : this .getImageFormat(attachment.mediaType), source : { bytes : Buffer .from(attachment.data, 'base64' ) } , } , } ) } else if (attachment. type === 'document' ) { content. push ( { document : { format : this .getDocumentFormat(attachment.mediaType), name : sanitizeDocumentName(attachment. name ), source : { bytes : Buffer .from(attachment.data, 'base64' ) } , } , } ) } } } return { role : msg.role, content } } ) } デモ まとめ TanStack AI と Amazon Bedrock を組み合わせることで、以下のメリットが得られます TanStack AI による型安全性、React フック、ストリーミング制御 Bedrock の エンタープライズ 機能(IAM 認証、 VPC 統合、ガバナンス) カスタムアダプターの実装は手間がかかりますが、TanStack AI の設計思想(AsyncGenerator パターン、StreamChunk 型)に従えば比較的シンプルに実装できます。 TanStack AI はまだアルファ版ですが、将来的には公式で Bedrock アダプターがサポートされる可能性もあります。それまでは、今回紹介したようなカスタムアダプターで対応できます。 ただし結論として、現時点で Bedrock等公式に接続が提供されていないAI基盤を利用する場合はVercel AI SDK を利用したほうが簡潔な構成にできます。 TanStack AIの一番の課題として、接続先のアダプタを自前で構える必要がある点が現時点でかなりの ボトルネック となります。 しかし、Vercel エコシステムにロックインされてしまわないためにも、TanStack AI の今後の進歩には期待しています。 以上、拝読ありがとうございました。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 執筆: @kamino.shunichiro レビュー: Ishizawa Kento (@kent) ( Shodo で執筆されました )
はじめに こんにちは。 エンタープライズ 第一本部・テクノロ ジー ビジネスユニット・テクノロ ジー ビジネス2部の多田圭佑です。 掲題の構成でシステムを構築するにあたり、基本的な構成であるにもかかわらず思わぬところでつまずいてしまったので、将来同じような問題に遭遇した方向けに書いています。 5分程度でサクッと読めますので、お付き合いいただければ幸いです。 本記事は 電通総研 Advent Calendar 2025 10日目の記事です。 つまずいたこと2つ 先に私がつまずいたことの2つを述べます。 CloudFront経由だと画面遷移でエラーになる 画像が表示されない システム構成 以下のような、 AWS では一般的な構成です。 (Internet) -> CloudFront -> ALB -> ECS ECS上にReact Router v7(SSR。ビルドツールはVite)を使用したシステムが稼働しており、それをCloudFront経由で公開しています。 CloudFrontはこのシステム以外でも使用しており、特定のパスパターンに合致した場合のみこのシステムにルーティングするよう設定しています。 1. CloudFront経由だと画面遷移でエラーになる 事象 前提としてCloudFrontでのパスパターンは後段で維持されます。 例えば、パスパターンを「aaa/bbb」としたときは以下のように転送されます。 ( HTTPS の終端はALBで、バックエンド(ECS)との通信はプライベートネットワーク内のHTTPです) AWS サービス URL CloudFront https://example.cloudfront.net/aaa/bbb ALB https://hogehoge.region.elb.amazonaws.com/aaa/bbb ECS http://localhost:8888/aaa/bbb 逆に言えば、CloudFrontを経由した場合は http://localhost:8888/aaa にはアクセスできません。 当初、私がルーティングを実装した際は以下のように routes.ts を設定しました。 こうすることで /aaa/bbb をベースとしてその配下のパスにアクセスできます。 export default [ route( '/healthcheck' , 'routes/healthcheck.tsx' ), // healthcheck用 ...prefix( 'aaa/bbb' , [ index( 'routes/index.tsx' )) route( '/ccc' , 'routes/ccc.ts' ), route( '/ddd' , 'routes/ddd.ts' ), ] ), ] satisfies RouteConfig; ただ、CloudFront経由だとインデックスへのアクセスは問題ないのですが、そこからの画面遷移に失敗していました。 ローカルやALB経由だと問題ありませんでした。 原因 React Router v7のルート・コードスプリット( 遅延ルート )では、クライアントは起動時に /__manifest というエンドポイントから「ルートID → チャンクURL」のマップを取得します。 今回はパスパターン経由で公開しているため /__manifest にアクセスしようとするとパスパターンに合致しないため404となってしまい、その結果チャンクのURLが分からずに動的 import が失敗していました。 ローカルやALB経由だとパスパターンの制約がないため問題がありませんでした。 対応 React Router v7では、アプリケーションのベースパスを指定する basename という設定があります。 今回の構成では、CloudFrontから /aaa/bbb というパスパターン経由でこのWebアプリケーションにアクセスしているため、この basename を設定することで問題を解決しました。 具体的には、 react-router.config.ts に以下のように設定を追加しました。 import type { Config } from "@react-router/dev/config" ; export default { ssr : true , basename : "/aaa/bbb" , // その他の設定... } satisfies Config; routes.ts は以下のようにシンプルになりました。 export default [ route( '/healthcheck' , 'routes/healthcheck.tsx' ), // healthcheck用。/aaa/bbb/healthcheck route( '/ccc' , 'routes/ccc.ts' ), route( '/ddd' , 'routes/ddd.ts' ), ] satisfies RouteConfig; この設定により、React Routerは /__manifest へのアクセスを /aaa/bbb/__manifest として解決するようになり、CloudFront 経由でも正しく マニフェスト ファイルを取得できるようになりました。 また、リンクやナビゲーションも自動的に /aaa/bbb を基準としたパスで動作するようになるため、ルーティング全体が正常に機能するようになりました。 2. 画像が表示されない 事象 ルーティングの問題は解決しましたが、今度は画像が表示されない問題が発生しました。 /images/icon.svg を取得しようとして404となっていました。 原因 原因を調査したところ、画像などの静的アセットファイルはReact Routerのルーティングとは別に扱われるため、 basename の設定だけでは対応できないことがわかりました。 静的ファイルは通常 public ディレクト リに配置されますが、これらのファイルへのパスも /aaa/bbb の プレフィックス を考慮する必要があります。しかし、ビルド時に生成される静的アセットへの参照パスは、 basename の設定では反映されません。 解決方法 この問題を解決するために、以下の対応を行いました。 1. Vite の base 設定を追加 vite.config.ts に base オプションを追加して、静的アセットのベースパスを明示的に設定しました。 ( react-router.config.ts とは若干異なるため注意です) import { defineConfig } from "vite" ; import { reactRouter } from "@react-router/dev/vite" ; export default defineConfig( { base : "/aaa/bbb" , plugins : [ reactRouter() ] , // その他の設定... } ); この設定により、ビルド時に生成される静的アセットへの参照パスに /aaa/bbb の プレフィックス が自動的に付与されるようになります。 2. 画像パスの設定 コンポーネント 内で画像を参照する際は、以下のように画像を直接インポートすることで、Vite が自動的に適切なパスに変換してくれます。 import iconSvg from "/images/icon.svg" ; function MyComponent () { return < img src = {iconSvg} alt = "Icon" />; // <img src="/aaa/bbb/images/icon.svg" /> } 以下のように 環境変数 を使用して文字列ベースで解決する方法もあります。こちらは動的に画像パスを生成する必要がある場合に有効です。 function MyComponent () { return < img src = { ` $ { import . meta . env . BASE_URL} / images / icon.svg `} alt="Icon" />; } まとめ CloudFrontのパスパターンを使用している場合は basename の設定が必要でした。この設定により /__manifest がprefix付きとなり、 マニフェスト ファイルの取得やルーティングが正常に動作します。 また、静的ファイルの参照についても同様に basename を考慮した設定が必要になります。 これから同様の構成でシステムを構築される方の参考になれば幸いです。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @tada.keisuke レビュー: @kinjo.ryuki ( Shodo で執筆されました )
はじめに こんにちは。クロス イノベーション 本部 クラウド イノベーション センター3年目の宮崎です。 AWS re:Invent 2025 で発表された AWS DevOps Agent を触ってみたので、概要を記載したいと思います。 AWS re:Invent 2025については、同じくクロス イノベーション 本部エンジニアリングテクノロ ジー センターの宮原さんが、速報記事を書いているので、是非下記の記事もご覧ください! AWS re:Invent2025 Keynote現地速報 また、 AWS re:Inventがそもそもどのようなものか把握したい方は、過去の私の記事もご覧いただけると幸いです。 AWS re:Invent 2024とAWS GameDayについて 本記事は 電通総研 Advent Calendar 2025 9日目の記事です! 記事の目的 新しいエージェントについての紹介 AWS DevOps Agentの機能やセットアップの流れの紹介 AWS DevOps Agentの位置づけ まず前提として、 AWS は re:Invent 2025で「Frontier Agents」という新しいコンセプトを掲げ、次の3つの新しいエージェントサービスを発表しました。 Kiro autonomous agent AWS Security Agent AWS DevOps Agent Frontier Agentsは、 AWS 公式のページでは「介入を必要とせずに何時間も動作する、自律的で大規模にスケーラブルなAIエージェント」という、新しいクラスのエージェントと定義しています。 従来の 「人間をサポートする AI アシスタント」から一歩進み、「チームのメンバーとして成果を出すことを期待されるデジタルワーカー」的な位置づけのサービス群だと捉えています。 引用:自律的で大規模に拡張可能なAIエージェント - Frontier agents – AWS フロンティアエージェントは、目標達成のために独立して動作し、同時実行タスクに対応するために大規模にスケールし、介入なしに数時間または数日間持続的に動作する自律システムです。個々のタスクを支援する従来のAIアシスタントとは異なり、フロンティアエージェントはチームの延長として機能し、多様なユースケースにわたって包括的な成果をもたらします。 この中で、AWS DevOps AgentFrontier agent – AWS DevOps Agent – AWSはシステムの障害対応(インシデントレスポンス)を加速し、将来の問題を未然に防ぐための運用エージェントで、具体的には、チケットなどをトリガーに自動でエージェントが起動し、メトリクス、ログ、最近のデプロイ履歴(GitHub)、トレース情報などを自動で分析し根本原因を特定・ユーザーに通知を行います。 また、システムの構成要素とその相互関係、デプロイ履歴を含むマップを自動構築し、影響範囲を正確に把握することが可能になります。 AWS DevOps Agent は、その中でも運用・インシデント対応を担う役割を持つ Frontier Agent です。 監視ツールやログを横断して自動で調査・分析し、インシデントの原因特定と復旧案、さらに再発防止策まで提案してくれる AWS の生成AIエージェントサービスとなっています。 インシデントの対応において、様々な情報を人間が行き来して疲弊するのではなく、人間は判断に注力できる未来はすぐそこに来ていると感じています。 インシデント対応における、以下のような課題を人間の介入無しに解決できることを狙っていると思われます。 課題 インシデント調査に時間がかかる CloudWatch / Datadog / New Relic / その他監視ツール / デプロイ履歴 / Runbook などを人がいちいち行き来して調べるのは大変。 ⇒ AWS DevOps Agentがこれらをまとめて見て、根本原因候補を自動で絞り込む。 夜間・休日オンコール負荷が高い問題 24/7 で人を張り付けるのはしんどいしコストも高い。 ⇒ AWS DevOps Agentが常時起動のオンコール要員として自動で調査開始し、対応案まで出してくれるので、 MTTR (復旧時間)を短縮しつつ負荷も軽減。 再発防止・改善の振り返りが十分にできていない インシデント後に振り返りや今後に向けた改善等をきちんとやる時間がない。 ⇒ AWS DevOps Agentが過去インシデントを横串で分析し、監視や構成・デプロイの改善案を提案してくれる。 AWS DevOps Agentの機能 参考: https://docs.aws.amazon.com/ja_jp/devopsagent/latest/userguide/what-is.html 上記の公式ドキュメントベースでご説明します: 1. 常時稼働の自動インシデント対応 アラートやチケットをトリガーに、自動で調査開始し、メトリクス、ログ、トレース、デプロイ履歴、コード、Runbook などを突き合わせ、原因候補と影響範囲を特定することができます。具体的な緩和策や復旧手順、 ロールバック 案までプランとして提示することが可能です。 2. チャットベースの インタラクティブ 調査 専用の DevOps Agent Web アプリ(Agent Space)から、視覚的に調査の状況や、人間が追加チャットでどこまで調査したか、他に影響はないか等も確認ができます。また、別チャットにて追加の調査や指摘もすることが可能です。 3. トポロジ学習とマルチツール連携 AWS リソースや各サービスの トポロジ(関係性)を自動で学習します。 CloudWatch、Datadog、New Relic、Dynatrace、Splunk などのオブザーバビリティツール、監視設定と連携し横断的に相関をとることも可能なようです。 4. 予防・再発防止のための改善提案 調査後に改善の提案を確認できます。また、過去インシデントのパターンを分析し、改善案を自動で提示することが可能です。 AWS DevOps Agentの管理・認証 今回、実際の検証を実施した際はIAM認証(既存のマネジメントコンソールのセッション)にて初期設定を実施しました。一方で、少し経つと AWS DevOps Agent Spaceウェブアプリのセッションが切れてしまいました。そこで、 AWS の公式ドキュメントを確認すると、IAM認証でのアクセスは30分までに制限されていました。 そのため、本番環境等において複数人に展開する場合はIAM Identity Centerが推奨されているようです。外部プロバイダーとの連携により集中管理することが可能になり、最大12時間のセッションが提供されます。 Setting Up IAM Identity Center Authentication AWS DevOps Agentのセットアップ手順 ※現状パブリックプレビュー期間中のため、 AWS DevOps Agent自体の使用料は発生しませんが、他のサービスへのクエリや API 呼び出し時に当該サービスから料金が掛かる場合は使用料金が発生します。 引用:Public preview pricing and limits マネジメントコンソールの検索欄に、 AWS DevOpsを入力すると出てきました。 現在はプレビュー版になっておりリージョンが制限されているので、 バージニア 北部に遷移します。 サービスページから「Begin setup」を押下し、エージェントスペースを作成します。 Agent Spaceの名前を設定し、画面に従って設定をします。 問題なく作成後、Agent Spaceの画面に遷移すれば完了です。 機能検証前の実施内容 AWS DevOps Agentを試す前に、調査対象となる環境、および仮トラブルを AWS DevOps Agentの公式ドキュメントの内容に基づき準備しました。 環境準備の詳細手順は以下のリンクからご覧ください。 Creating a test environment 内容は、準備されているIaCコードを用いてCloud FormationでEC2やLambdaの環境をデプロイ後、用意されたトラブルに対して AWS DevOps Agentを利用し、代表機能を見ていけるというものになっています。 AWS DevOps Agentを試したいが、対象のシステム環境がない方 が試す場合に打って付けです。 ※実際のアプリケーション等の検証環境がある方はそちらで検証した方が、実際の業務で何がどこまでできそうか把握しやすいと思います。 実際の調査の流れ 作成したAgent Spacesから画面右上の「Operator access 」を押下します。 すると別タブで AWS DevOps Agentの画面に遷移します。erator access から作成したエージェントに質問をします。 質問内容 以下を項目ごとに入力します。 (実際のプロンプトは対象の環境やエラーの状況によって変えてください。 今回は AWS DevOps Agentの公式ドキュメントの内容に基づき準備した場合のプロンプトになっています。) ※今後、日本語対応されるまでは調査したい内容を日本語から英語へ翻訳する1アクションが必要になりそうです。 Investigation details: Investigate high CPU utilization on test EC2 instance and Lambda error rate spikes generated by test stacks. Investigation starting point: Start from CloudWatch alarms AWS -AIDevOps-EC2-CPU-Test and AWS -AIDevOps-Lambda-Error-Test which are currently in ALARM. 内容を確認後「Start Investigation」を押下し、調査を開始します。 タイムライン形式で何の情報をインプットに、どのようなステップで調査をしているか、最終的に何を根本原因と判断したかまで等が把握できます。 視覚的で内容も把握しやすいものとなっています。 調査結果が約5分~10分ほどで出ます。 詳細を確認すると根本原因について記載しています。 実際のインシデントが起こった流れについても、詳細に記載されていました。 また、調査は並行で走らせることができました。 ※現状、同時に実行できるインシデント解決調査タスクの数は 3 までとなっています。 実際の本番利用においては、エラーに関連するリソースの指定や環境の指定により調査対象をプロンプトによって指定する等で、異なる視点からの調査を並行して実施することもできそうです。 まとめ AWS DevOps Agentは設定が簡単で、実際のエラーの 調査内容/理由 を時系列に沿ってUIで分かりやすく、詳細に解説してくれました。調査は並行で走らせることができ、根本原因が約5分~10分で直ぐに把握が可能です。 AWS DevOps Agentはインシデントの調査・根本原因の特定はもちろん、ポストモーテム(システムにトラブルや障害が発生したあとに行う事後分析)に非常に有効だと思いました。 そして何より、「チームのメンバーとして成果を出すことを期待されるデジタルワーカー」的な位置づけのサービスとして AWS DevOps Agentで一次調査を任せることができるレベルだと言うことが分かりました。 現状、二次対応で実際のリソースの設定変更などについてはKiro CLI (旧 Amazon Q Developer CLI )で AWS CLI の認証情報との組み合わせにより設定変更や環境参照なども可能になっています。 日本語対応やリージョンの拡大を期待しつつ、 AWS DevOps Agentがインシデント対応のプロセスにおいてどの部分まで担っていけるのか、 AWS の他サービス(Kiro CLI , Amazon Q Develper等)やAI機能が備わってきているオブザーバビリティ・インシデント対応周りの SaaS サービスとの棲み分け等について、引き続き Watch して確認して行ければと思っています。 参考記事 AWS公式資料:Frontier agents AWS公式資料:AWS DevOps Agent helps you accelerate incident response and improve system reliability (preview) AWS公式資料:AWS DevOps Agent (Preview) AWS公式ドキュメント:AWS DevOps Agent パブリックプレビューの価格と制限 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @miyazaki.hirotoshi レビュー: @handa.kenta ( Shodo で執筆されました )
はい、こんにちはー!コーポレート本部サイバーセキュリティ推進部の福山です。 本記事は 電通総研 Advent Calendar 2025 10日目の記事です。 今回は、当社もThird Sponsorとして協賛させていただいた、国内最大級の国際的なセキュリティカンファレンスである 「CODE BLUE」 に初めて参加してきたので、そのレポートになります。 CODE BLUEをあまり知らない方向けに、簡単ではありますが紹介いたします。 CODE BLUEとは Contests/Workshops Hacking Active Directory インターポールAIクエスト TRAPA Cyber Range Exercise 最後に CODE BLUEとは CODE BLUEは、世界トップクラスの情報セキュリティ専門家が集まり、最先端の技術や脅威動向に関する講演・情報交換が行われる、国際的なセキュリティカンファレンスです(会場は ベルサール高田馬場 で2日間開催)。 海外の著名な研究者や国内のトップエンジニアによるセッションが用意されており、最終日には登壇者および参加者同士で交流するPartyも設けられています。 また、実践的なスキルを磨けるコンテストおよびワークショップが用意されており、実際に手を動かして学べる点も、CODE BLUEの大きな魅力だと感じました。 今回はコンテストおよびワークショップに焦点を当てて紹介します。 Contests/Workshops 今回は3つのコンテストおよびワークショップに参加させていただきました。どれも非常に学びの深い内容でした! Hacking Active Directory このワークショップでは、攻撃者目線で Active Directory (AD)環境に侵入し、特権昇格や横展開を行う手法をハンズオン形式で体験できます。 また、攻撃に対する緩和策についても触れられていました。 ※こちらのワークショップは人数制限があり、当日現地予約という形式になっていました。 初日の午前中に2日間全ての枠が埋まるほどの人気ぶりでした。 参加者は Webブラウザ から Apache Guacamole環境にアクセスし、Kali Linux を操作します。 (※図は IBM X-Force Red提供資料より引用) 外部公開された脆弱なWebサーバが侵入の起点というシナリオで、大まかに以下のフェーズで内容が構成されていました。 フェーズ 技法 使用ツール 主な目的 初期侵入 (Initial Access ) LLMNR / NBT-NS Poisoning responder, hashcat ネットワーク上の認証ハッシュを窃取し、平文パスワードを取得。 内部偵察 (Reconnaissance) SMB Recon & AD偵察 NetExec, BloodHound 共有フォルダの探索、ADの構成や攻撃経路の可視化。 権限昇格 (Privilege Escalation) Kerberoasting Impacket GetUserSPNs, hashcat SPNを持つアカウントのサービスチケットを収集・復号し、サービスアカウントのパスワードを取得。 攻撃者がどのようにAD環境で足場を築き、権限を広げていくのかを実体験できるようになっています。 また、攻撃者視点を学んだ後、それに対する具体的な緩和策が解説されました。 攻撃技法 緩和策 LLMNR / NBT-NS Poisoning グループポリシーでLLMNRを無効化し、 ネットワークアダプタ 設定でNBT-NSを無効化する。 Kerberoasting サービスアカウントのパスワードを30文字以上の複雑なものにする。 RC4 暗号化を無効化し、より強固な暗号化方式を使用する。 ※対策が難しい場合は、 Windows イベントID 4768(Kerberos認証チケット要求)や4769(サービスチケット要求)を監視し、 RC4 -HMACの使用や短時間での複数のSPN要求を検知する。 「攻撃者がどのようにしてADを狙うのか」「この緩和策がなぜ必要なのか」といった点が深く腹落ちするワークショップでした。 インターポールAIクエスト こちらは過去に 国際刑事警察機構 (インターポール)向けに提供されたCTFをアレンジしたものとなっており、AIとサイバー犯罪がテーマとなっていました。 基調講演でも取り上げられた「AIとセキュリティ」に焦点が当てられているだけに、興味をそそる内容だったと思います。 会場の Wi-Fi 経由で、2日間どこからでもアクセスできました。 AIクエスト世界の被害者(生成AI)に、チャットベースで被害状況などを ヒアリ ングしながらサイバー犯罪を読み解いていく、対話型のCTFです。 被害者のお孫さんとの家族写真が暗号化されて見られなくなってしまったという相談から始まり、1問目からTorへのアクセスを要求されるという、非常に興味深い問題設定でした。 もちろん、実際の生成AIサービスのように聞けばなんでも教えてくれるノリではなく、質問をクリアするとヒントを1個聞く権限が付与されるといったルールになっていました。 最後まで到達できず厳しい結果に終わりましたが、自力での解決を強いられるところに、現実のサイバー捜査の厳しさ・情報収集の難しさを実感できました。 TRAPA Cyber Range Exercise こちらはTRAPA が提供するサイバーレンジを利用した、ブルーチーム向けの実践的な演習に参加しました。 APTグループに攻撃を受けた仮想環境で、ログ解析、インシデント対応、被害拡大の防止措置を行うという、非常にリアルなシミュレーションとなっています。 こちらも会場の Wi-Fi 経由で、2日間どこからでもアクセスできました。 侵害を受けた環境はオンプレ構成となっており、インターネットとの間に DMZ があり、外部からのリクエストによって Proxyサーバ が侵入の起点になるといったシナリオでした。 SOCルームというダッシュボードで攻撃事象の確認やインシデントの調査結果を記録し(正解したらポイントを獲得)、 ログ調査にはSplunkを使って、 ファイアウォール ログや Windows ホストモニターなどを活用して演習を進めていきました。 複数の資産にまたがるログと時間制限の中では、ログを読むスピードが求められます。 こちらも最後まで到達できず厳しい結果に終わりましたが、調査に行き詰ったときにはTRAPA提供のAIに聞くとアド バイス がもらえたので面白かったです。 最後に 簡単ですが、Contests/Workshopsの紹介でした。 改めて自身の実力不足を痛感しつつも、初めてのCODE BLUEは結果として想像以上に楽しく、まるで海外カンファレンスに参加しているような刺激を受けました。 運営の皆様に心から感謝します。 本記事では紹介しませんでしたが、各セッションについても普段は聞けない興味深い内容が多く、大きな学びになりました。 ワークショップやコンテストについては開催する側も面白そうだなと思ったので、次回以降チャンスがあればチャレンジしてみたいです。 それでは、来年のCODE BLUE 2026でお会いしましょう! 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @fukuyama.kenta ( Shodo で執筆されました )
はじめに はじめまして。グループ経営ソリューション事業部の三谷です。 この記事では、 電通 総研で監視ツールとして利用しているDatadogについてどのようなサービスなのか基本的な情報と、導入手順をまとめた記事になります。 Datadogとは Datadog は、 クラウド やコンテナ環境向けの「監視・可観測性プラットフォーム」です。 サーバ・コンテナ・ ミドルウェア ・アプリケーション・ クラウド サービスからデータを集めて、1 つの画面で横断的に可視化・分析できる SaaS 型のサービスです。 Datadog が扱う主なデータは次の 4 種類に整理できます。 メトリクス CPU 使用率、メモリ使用量、ディスク I/O、リクエスト数、レイテンシなどの時系列データ。 ログ アプリケーションログ、 アクセスログ 、システムログ、 ミドルウェア ログなど。 トレース ( APM ) 1リクエストがどのサービス・コンテナを通過したか、その経路と各 区間 のレイテンシ。 つまり、アプリケーションの動作をトレースして、どこで遅延やエラーが発生しているかをDatadogから調査が可能です。 イベント デプロイ、スケールアウト/イン、障害、設定変更などの「出来事」を表す情報。 私は今まで、監視は主にCloudWatchを利用していました。CloudWatchだと、 AWS アカウントを跨いだ一元監視が難しく「ほかの AWS アカウントではどうなっているんだっけ・・・」と調査のために比較したいとき、 AWS アカウントを切り替えながら確認が必要で非常に手間がかかっていました。 Datadogではその問題は発生せず、複数の AWS アカウントのデータを一目で確認することができます。 Datadogでなにができるか(どのように監視するか) Datadog による監視の全体像は、大きく分けて4つの段階があります。 1. データを集める Agent による収集 監視対象のサーバやコンテナにDatadog Agentを導入すると、標準で次の情報を収集できます。 OS レベルのメトリクス CPU、メモリ、ディスク、ネットワーク I/O など コンテナメトリクス コンテナごとの CPU / メモリ / ネットワーク利用状況 ミドルウェア メトリクス nginx, Redis, PostgreSQL , MySQL などの各種インテグレーションを通じたメトリクス ログ・トレースの収集 Datadog はログ・トレースも一元的に扱えます。 ログの収集の例として下記の構成があります。 Datadog Agentでのログ収集 CloudWatch Logsや FireLens + Fluent Bit でのログ収集 それを Datadog に転送して保存・検索・解析 2. タグで関連づける Datadog では、すべてのメトリクス・ログ・トレース・イベントをタグで管理しています。 タグ設計が監視の品質を左右する、重要な設計要素になります。 たとえば、以下の調査を行いたい場合、Datadogだけで調査が完結します。 「ECSの特定タスクで500エラーが増えたタイミングで、CPU・メモリ利用率はどうだったか」 「その直前にデプロイやスケールイベントは発生していなかったか」 「どのリリースバージョンからエラー率が上がっているか」 代表的なタグ(例): 環境別 env:prod , env:staging , env:dev サービス別 service:my-api , service:batch-worker クラスタ ー別 cluster:my-ecs-cluster 3. 可視化・分析する 収集したデータはDatadogのコンソール上でダッシュボードを作成することが可能です。 ダッシュボードでの管理例 ECS / Fargate 向けの標準ダッシュボードを作る クラスタ 全体の CPU / メモリ使用率 サービスごとの runningタスク数 カスタムダッシュボード 本番環境 env:prod に特化したダッシュボードを作る リクエスト数 エラー率 CPU / メモリ使用率 4. モニター(アラート)で通知する 可視化して満足していても意味がないので、 閾値 や条件を定義して必要な通知を行う必要があります。Datadogではモニターと呼ぶ機能がこれに該当します。 代表的なモニターの例(ECS タスクの場合) ECS サービスのタスク数監視 条件例: running_tasks < desired_tasks が 5 分以上継続したらアラート エラー率監視 条件例: service:my-api AND env:prod の 5xx 発生率が一定以上でアラート 通知先 各種ツールへの通知が可能です。なおJiraと連携することで、Datadogにてアラート発生時にJiraのチケットを自動起票することも可能です。 https://www.atlassian.com/ja/devops/observability-tutorials/jira-datadog-integration 社内でJira自動起票について少し検討したことがありました。便利ではあるものの、長時間アラートが出た場合連続して同じチケットが起票されるのでは等、導入における考慮すべき点が多く、導入は一旦ステイとなりました。 また、オンコールについてもDatadog On-Callというサービスが存在しており一元管理することが可能です。 https://docs.datadoghq.com/ja/service_management/on-call/ ECSサービスの監視を実際にやってみる Datadogの基本的な機能が分かったところで、今回はDatadog Agentを導入してECSサービスの監視を行いたいと思います。 前提条件 AWS アカウントを持っていること Datadogアカウントを作成済みであること Datadogの API キーを取得済みであること Datadog AgentとDatadogが通信可能であること Datadogは収集するデータによって使用するポートが異なります。今回利用するAgentは TCP /443を使用します。 詳細はDatadogのドキュメントをご確認ください。 https://docs.datadoghq.com/ja/agent/configuration/network/ AWS での作業 タスク定義の作成 まず、ECSで使用するタスク定義を作成します。 今回のタスク定義では、以下の2つのコンテナを設定しました。 Datadog Agentコンテナ イメージ URI : public.ecr.aws/datadog/agent:latest Apache コンテナ イメージ URI : public.ecr.aws/docker/library/httpd:latest ※今回はECRのパブリックイメージを使用しました。 Datadogでの監視を有効化するため、Datadog Agentコンテナに以下の 環境変数 を設定します。 DD_API_KEY : Datadogの API キー DD_SITE : Datadogのサイト(例: datadoghq.com ) ECS_FARGATE : true クラスタ ーの作成 次に、ECS クラスタ ーを作成します。 任意の クラスタ ー名を入力し、その他デフォルトの設定で作成します。 サービスの作成 作成した クラスタ ー上でサービスを起動します。 サービス作成時の設定項目: タスク定義ファミリー : 先ほど作成したタスク定義を選択 環境 : 「起動タイプ」を選択 ネットワーキング : VPC を選択 サブネットを選択 適切なセキュリティグループを選択 (添付画像は入力前のものです) その他の項目はデフォルトのまま作成 サービスが正常に開始すると上図のようになります。 Datadogコンソールでの確認 監視を開始するためにDatadog側で必要な作業はほぼありません。 API キーさえ用意できれば、Agent導入後すぐに監視が始まります。 AWS マネジメントコンソールからサービス起動は確認できたので、Datadogからも確認してみます。 DatadogコンソールにてInfrastructure→Containers→ECS Explorer から起動したサービスが確認できます。 監視対象のサービスが見えました。CPU/メモリも取得できていそうです。 これで基本的な監視設定が完了しました。 APM など高度な監視には詳細な設定が必要ですが、基本的な監視であれば簡単に導入して監視を始められることがわかりました。 さいごに 今回はDatadogの仕組みについての基本的機能の概要と簡単なサービス監視までを実施しました。 Datadogは、最初の14日間はトライアル期間であり、すべての機能が無料で使用できます。 皆様もよいDatadogライフをお過ごしください。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @mitani.kyoka レビュー: @kinjo.ryuki ( Shodo で執筆されました )
はじめに こんにちは、XI本部エンジニアリングテクノロ ジー センターの徳山広士です。 この記事では、データ基盤/データ分析基盤におけるデータパイプラインのAI駆動開発の手法を提案します。 「 ODCS (Open Data Contract Standard) 」フォーマットの"Data Contract"を仕様書として生成AIへ与えてデータパイプラインのコードを生成させる手法です。 Data Contractは、データの仕様(テーブル構造、カラム定義、データ型など)と品質要件を YAML 形式で定義する標準フォーマットです。 当記事の主な内容は、Data Contractやローカル開発環境などの基礎的な説明から当件のAI駆動開発方法、コード生成に成功した検証結果についてです。 想定読者: データエンジニア、データマネジメント業務担当者 当記事で得られる知見: データパイプラインのAI駆動開発の勘所 前提知識: データパイプライン(ETL/ ELT 処理)の基礎知識 一般的に労力と時間のかかるデータマネジメント・サイクルをAI駆動開発によって加速できればと考えて、データエンジニアリングのAI駆動開発手法を探求し、この記事にまとめました。 データパイプラインのAI駆動開発実現の課題 生成AIに何を伝えるべきか アプリケーションのAI駆動開発においては、 ドメイン や ユースケース 、機能、制約などを生成AIに説明することでコード生成するアプローチを見聞きします。では、 データエンジニアリングにおいては何を説明すれば良いのか?どのように指示を出せば包括的に開発してくれるのか? これが最初に直面した課題でした。 自然言語 のプロンプトで目的の SQL クエリについて説明する方法も考えましたが、複雑なロジックの表現が困難であり、細かい要件が伝わらず、プロンプトの再現性・再利用性にも疑問がありました。 アプローチ: データ アーキテクチャ とデータモデルを伝える この課題に対して、データエンジニアリングの観点から「データ アーキテクチャ 」と「データモデル」の2つの軸で情報を整理し、データ基盤/データ分析基盤の全体像と開発方法を生成AIに伝えるアプローチを採用しました。 データ アーキテクチャ の説明 生成AIが全体像を理解するための情報: データレイヤー構造と各レイヤーの責務: raw, staging, core, martなど データストアの構成: データレイク, データウェアハウス, データマートなど 各データストアのデータフォルダ構成: "raw/システム名/YYYYMMDD"など データ処理アプローチ: ELT など インフラストラクチャ構成: 各データストアの物理実装で利用の SaaS など データパイプライン構成技術: dagster , dbt core など これらの内容を プロジェクトドキュメント(DESIGN.md等) として整備しました。 データモデルの説明 生成AIがデータパイプラインで実現すべき対象の情報: データの構成: テーブル, カラム, リレーションシップなど データ仕様: データ型, 主キー等の制約, 変換ロジックなど データ品質仕様: 満たすべき基準, テストルールなど これらを Data Contract(ODCS形式) としてドキュメント整備しました。 この2つを組み合わせることで、生成AIはデータパイプラインの実行環境全体の設計意図を理解し、データ基盤/データ分析基盤に適した具体的な実装を行えるようになりました。 データパイプライン作成依頼 生成AIへの開発依頼は、データパイプラインの要件を ADR (Architecture Decision Record) に記述し、その ADR で定義されたデータパイプラインを関連するData Contractにもとづいて開発するように生成AIへ依頼する方法を採用しました。 ADR に少なくとも以下の内容を書くようにしました。 データパイプラインの作成対象のデータ名 データ提供の背景や目的, データ利用の ユースケース ADR の記載内容の参考イメージ タイトル: P001 ECビジネスデータパイプライン構造 ステータス: Accepted コンテキスト: Eコマースビジネスパイプラインのデータ統合対象のデータを決める必要がある。 決定事項: Eコマースビジネスパイプラインは、web_salesとweb_returnsを対象とした構成にする。 検証内容の詳細 実際の検証環境や要件、作成対象のデータなどを説明します。 検証では TPC-DS (*1)のデータを使用し、生成AIには Claude Code(Sonnet 4.5, Anthropic) を使用しました。 web_salesテーブルの更新用のソースデータを変換してweb_salesへ統合するデータパイプラインの SQL クエリを生成します。 ※1 TPC -DS は Transaction Processing Performance Council ( TPC ) の商標です。 本記事は TPC ベンチマーク 結果の公表を目的としたものではありません。 ローカル開発環境 データパイプラインのデプロイ先は AWS やAzure、 Google Cloudなどの SaaS ですが、AI駆動開発を効率化するためにデプロイ先を模したローカル完結型の開発環境を整備しました。 データレイク兼データウェアハウス: DuckDB データ モデリング : dbt core オーケストレータ: dagster データ品質テスト: Soda Core DuckDBで構成 : SaaS への外部通信を排除し、生成AIの生産性を最大化。 SaaS で巨大になりがちなデータレイクやデータウェアハウスをコンパクトなDuckDBで代用しました。 コードベースのツール選定 : 生成AIがコードを直接参照・実装できるよう、全てコードベースのツールを採用。 GUI のETLツールは、生成AIとの連携で独自 SDK / API 経由が必要となり生産性の低下が懸念されるため使用しませんでした。 ツールによるインフラストラクチャの抽象化 : デプロイ先の SaaS とは物理的な環境構成が異なりますが、dbt coreなどのソフトウェアが抽象化し環境差分を吸収してくれます。 データ アーキテクチャ 概要 データパイプラインのデプロイ先のデータ分析基盤のデータ アーキテクチャ を図にしたものです。論理構成の主要部分のみを抜粋し、簡潔な内容を掲載しております。 データレイヤー: "raw", "staging", "core", "mart" の4層 データストア: データレイクとデータウェアハウス データ処理アプローチ: ELT データパイプライン構成技術: Dagster, dbt core, Soda Core インフラストラクチャ: AWS とAzure, Google Cloudの3大 クラウド サービスでそれぞれ実施、詳細は割愛 データモデル web sales ※一部のカラムは割愛 ディメンショナル モデリング されたスター スキーマ 構造となっており、複数の サロゲートキー を持っています。データ構造は把握しやすいもののETL/ ELT 処理の観点では10個以上のテーブル結合や中間テーブルを介したテーブル結合、一部カラムの演算処理が必要な複雑な構造になっています。 作成対象のデータパイプライン rawレイヤー 生データのデータファイルが外部テーブル (External table) としてデータウェアハウスに既にテーブル化されており、そのテーブルを参照すれば良いため、処理の実装は不要。 stagingレイヤー rawレイヤーのデータをもとにマスターデータとの結合によるデータ取得や演算処理などの ビジネスロジック をdbtの SQL クエリモデルとして実装 SELECT対象のカラム数: 35個 テーブル結合数: 12個 coreレイヤー stagingレイヤーのデータをもとに増分処理でデータ更新する処理をdbtの SQL クエリモデルとして実装 SELECT対象のカラム数: 38個 テーブル結合数: 0個 実装ルール SELECT文でカラムの型キャストを明記 SELECT文でカラムの名称をAS文で指定 CTE (Common Table Expression) でソースデータ定義 coreレイヤーでは増分処理を実装 生成AIへの依頼 前述の「 アプローチ: データアーキテクチャとデータモデルを伝える 」に記載のドキュメントに加えて、開発 ガイドライン も整備し開発依頼を行いました。 生成AIへ提供した情報 プロジェクトドキュメント Data Contract 開発 ガイドライン (開発規約や トラブルシューティング など) ADR (Architecture Decision Record) プロンプト 実際のプロンプト 以下のファイルを読み込んで、当プロジェクトについて理解してください。 - ./docs/DESIGN.md - ./docs/DATAMODEL_GENERATION_GUIDELINES.md ADRで定義されたデータパイプラインを関連する`Data Contract`にもとづいて作成します。 今回は、ADRの"./docs/adr/pipelines/P001-ecommerce-business-pipeline-structure.md"で決定したデータパイプラインを作成してください。 実際のData Contractのサンプル "ws_item_sk"という主要なカラムのData Contractを一部抜粋したものです。より詳しい内容と解説は 詳細解説 にて後述します。 - name : ws_item_sk businessName : ウェブ販売商品サロゲートキー logicalType : string physicalType : STRING primaryKey : true primaryKeyPosition : 1 transformSourceObjects : - core_tpcds.item.i_item_sk transformLogic : SELECT i_item_sk FROM s_web_order JOIN s_web_order_lineitem ON (word_order_id = wlin_order_id) LEFT JOIN item ON (wlin_item_id = i_item_id AND i_rec_end_date IS NULL ) relationships : - type : foreignKey to : - core_tpcds.item.i_item_sk quality : - type : library rule : duplicateCount name : "主キー重複チェック(ws_item_sk)" dimension : uniqueness mustBe : 0 severity : error businessImpact : "重複レコードによりファクトテーブルのデータ整合性が崩れ、売上集計に誤差が生じる" 生成されたコード stagingレイヤー 12テーブルを結合し、35カラムを生成する SQL クエリです。Data Contractの transformLogic で定義した変換ロジックが正確に反映されています。 with s_web_order as ( select * from {{ source( ' raw_tpcds ' , ' s_web_order_1 ' ) }} ), s_web_order_lineitem as ( select * from {{ source( ' raw_tpcds ' , ' s_web_order_lineitem_1 ' ) }} ) SELECT d1.d_date_sk::STRING AS ws_sold_date_sk, t_time_sk::STRING AS ws_sold_time_sk, d2.d_date_sk::STRING AS ws_ship_date_sk, i_item_sk::STRING AS ws_item_sk, c1.c_customer_sk::STRING AS ws_bill_customer_sk, c1.c_current_cdemo_sk::STRING AS ws_bill_cdemo_sk, c1.c_current_hdemo_sk::STRING AS ws_bill_hdemo_sk, c1.c_current_addr_sk::STRING AS ws_bill_addr_sk, c2.c_customer_sk::STRING AS ws_ship_customer_sk, c2.c_current_cdemo_sk::STRING AS ws_ship_cdemo_sk, c2.c_current_hdemo_sk::STRING AS ws_ship_hdemo_sk, c2.c_current_addr_sk::STRING AS ws_ship_addr_sk, wp_web_page_sk::STRING AS ws_web_page_sk, web_site_sk::STRING AS ws_web_site_sk, sm_ship_mode_sk::STRING AS ws_ship_mode_sk, w_warehouse_sk::STRING AS ws_warehouse_sk, p_promo_sk::STRING AS ws_promo_sk, word_order_id::STRING AS ws_order_number, wlin_quantity:: INTEGER AS ws_quantity, i_wholesale_cost:: DECIMAL ( 7 , 2 ) AS ws_wholesale_cost, i_current_price:: DECIMAL ( 7 , 2 ) AS ws_list_price, wlin_sales_price:: DECIMAL ( 7 , 2 ) AS ws_sales_price, ((i_current_price:: DECIMAL ( 7 , 2 ) - wlin_sales_price:: DECIMAL ( 7 , 2 )) * wlin_quantity:: INTEGER ):: DECIMAL ( 7 , 2 ) AS ws_ext_discount_amt, (wlin_sales_price:: DECIMAL ( 7 , 2 ) * wlin_quantity:: INTEGER ):: DECIMAL ( 7 , 2 ) AS ws_ext_sales_price, (i_wholesale_cost:: DECIMAL ( 7 , 2 ) * wlin_quantity:: INTEGER ):: DECIMAL ( 7 , 2 ) AS ws_ext_wholesale_cost, (i_current_price:: DECIMAL ( 7 , 2 ) * wlin_quantity:: INTEGER ):: DECIMAL ( 7 , 2 ) AS ws_ext_list_price, (i_current_price:: DECIMAL ( 7 , 2 ) * web_tax_percentage:: DECIMAL ( 7 , 2 )):: DECIMAL ( 7 , 2 ) AS ws_ext_tax, wlin_coupon_amt:: DECIMAL ( 7 , 2 ) AS ws_coupon_amt, (wlin_ship_cost:: DECIMAL ( 7 , 2 ) * wlin_quantity:: INTEGER ):: DECIMAL ( 7 , 2 ) AS ws_ext_ship_cost, (wlin_sales_price:: DECIMAL ( 7 , 2 ) * wlin_quantity:: INTEGER - wlin_coupon_amt:: DECIMAL ( 7 , 2 )):: DECIMAL ( 7 , 2 ) AS ws_net_paid, (((wlin_sales_price:: DECIMAL ( 7 , 2 ) * wlin_quantity:: INTEGER ) - wlin_coupon_amt:: DECIMAL ( 7 , 2 )) * ( 1 + web_tax_percentage:: DECIMAL ( 7 , 2 ))):: DECIMAL ( 7 , 2 ) AS ws_net_paid_inc_tax, (((wlin_sales_price:: DECIMAL ( 7 , 2 ) * wlin_quantity:: INTEGER ) - wlin_coupon_amt:: DECIMAL ( 7 , 2 )) - (wlin_quantity:: INTEGER * i_wholesale_cost:: DECIMAL ( 7 , 2 ))):: DECIMAL ( 7 , 2 ) AS ws_net_paid_inc_ship, ((wlin_sales_price:: DECIMAL ( 7 , 2 ) * wlin_quantity:: INTEGER ) - wlin_coupon_amt:: DECIMAL ( 7 , 2 ) + (wlin_ship_cost:: DECIMAL ( 7 , 2 ) * wlin_quantity:: INTEGER ) + i_current_price:: DECIMAL ( 7 , 2 ) * web_tax_percentage:: DECIMAL ( 7 , 2 )):: DECIMAL ( 7 , 2 ) AS ws_net_paid_inc_ship_tax, (((wlin_sales_price:: DECIMAL ( 7 , 2 ) * wlin_quantity:: INTEGER ) - wlin_coupon_amt:: DECIMAL ( 7 , 2 )) - (i_wholesale_cost:: DECIMAL ( 7 , 2 ) * wlin_quantity:: INTEGER )):: DECIMAL ( 7 , 2 ) AS ws_net_profit, s_web_order.processing_date::STRING AS processing_date, current_timestamp :: TIMESTAMP AS ingested_at FROM s_web_order LEFT JOIN {{ ref( ' date_dim ' ) }} d1 ON ( cast (word_order_date AS date ) = d1.d_date) LEFT JOIN {{ ref( ' time_dim ' ) }} ON (word_order_time = t_time) LEFT JOIN {{ ref( ' customer ' ) }} c1 ON (word_bill_customer_id = c1.c_customer_id) LEFT JOIN {{ ref( ' customer ' ) }} c2 ON (word_ship_customer_id = c2.c_customer_id) LEFT JOIN {{ ref( ' web_site ' ) }} ON (word_web_site_id = web_site_id AND web_rec_end_date IS NULL ) LEFT JOIN {{ ref( ' ship_mode ' ) }} ON (word_ship_mode_id = sm_ship_mode_id) JOIN s_web_order_lineitem ON (word_order_id = wlin_order_id) LEFT JOIN {{ ref( ' date_dim ' ) }} d2 ON ( cast (wlin_ship_date AS date ) = d2.d_date) LEFT JOIN {{ ref( ' item ' ) }} ON (wlin_item_id = i_item_id AND i_rec_end_date IS NULL ) LEFT JOIN {{ ref( ' web_page ' ) }} ON (wlin_web_page_id = wp_web_page_id AND wp_rec_end_date IS NULL ) LEFT JOIN {{ ref( ' warehouse ' ) }} ON (wlin_warehouse_id = w_warehouse_id) LEFT JOIN {{ ref( ' promotion ' ) }} ON (wlin_promotion_id = p_promo_id) coreレイヤー stagingレイヤーのデータを増分処理でcoreレイヤーに取り込む SQL クエリです。dbtのincrementalモデルを使用しています。 {{ config( materialized= ' incremental ' , unique_key=[ ' ws_item_sk ' , ' ws_order_number ' ], on_schema_change= ' fail ' , incremental_strategy= ' merge ' if target. type != ' duckdb ' else ' delete+insert ' ) }} -- Incrementalモデル:staging層からデータを取得 select ws_sold_date_sk::STRING as ws_sold_date_sk, ws_sold_time_sk::STRING as ws_sold_time_sk, ws_ship_date_sk::STRING as ws_ship_date_sk, ws_item_sk::STRING as ws_item_sk, ws_bill_customer_sk::STRING as ws_bill_customer_sk, ws_bill_cdemo_sk::STRING as ws_bill_cdemo_sk, ws_bill_hdemo_sk::STRING as ws_bill_hdemo_sk, ws_bill_addr_sk::STRING as ws_bill_addr_sk, ws_ship_customer_sk::STRING as ws_ship_customer_sk, ws_ship_cdemo_sk::STRING as ws_ship_cdemo_sk, ws_ship_hdemo_sk::STRING as ws_ship_hdemo_sk, ws_ship_addr_sk::STRING as ws_ship_addr_sk, ws_web_page_sk::STRING as ws_web_page_sk, ws_web_site_sk::STRING as ws_web_site_sk, ws_ship_mode_sk::STRING as ws_ship_mode_sk, ws_warehouse_sk::STRING as ws_warehouse_sk, ws_promo_sk::STRING as ws_promo_sk, ws_order_number::STRING as ws_order_number, ws_quantity:: INTEGER as ws_quantity, ws_wholesale_cost:: DECIMAL ( 7 , 2 ) as ws_wholesale_cost, ws_list_price:: DECIMAL ( 7 , 2 ) as ws_list_price, ws_sales_price:: DECIMAL ( 7 , 2 ) as ws_sales_price, ws_ext_discount_amt:: DECIMAL ( 7 , 2 ) as ws_ext_discount_amt, ws_ext_sales_price:: DECIMAL ( 7 , 2 ) as ws_ext_sales_price, ws_ext_wholesale_cost:: DECIMAL ( 7 , 2 ) as ws_ext_wholesale_cost, ws_ext_list_price:: DECIMAL ( 7 , 2 ) as ws_ext_list_price, ws_ext_tax:: DECIMAL ( 7 , 2 ) as ws_ext_tax, ws_coupon_amt:: DECIMAL ( 7 , 2 ) as ws_coupon_amt, ws_ext_ship_cost:: DECIMAL ( 7 , 2 ) as ws_ext_ship_cost, ws_net_paid:: DECIMAL ( 7 , 2 ) as ws_net_paid, ws_net_paid_inc_tax:: DECIMAL ( 7 , 2 ) as ws_net_paid_inc_tax, ws_net_paid_inc_ship:: DECIMAL ( 7 , 2 ) as ws_net_paid_inc_ship, ws_net_paid_inc_ship_tax:: DECIMAL ( 7 , 2 ) as ws_net_paid_inc_ship_tax, ws_net_profit:: DECIMAL ( 7 , 2 ) as ws_net_profit, processing_date::STRING as processing_date, ingested_at:: TIMESTAMP as ingested_at, current_timestamp :: TIMESTAMP as inserted_at, current_timestamp :: TIMESTAMP as updated_at from {{ ref( ' web_sales_view ' ) }} {% if is_incremental() %} -- incrementalの場合、processing_dateでフィルタリング where processing_date = ' {{ var("processing_date", "20240101") }} ' {% endif %} 詳細解説 Data Contractとは? Data Contractは、データの提供者と利用者の間でデータ仕様やデータ品質要件を合意するための標準フォーマットです。 フォーマットの既定の項目を使ってデータ仕様やデータ品質要件を定義することができます。 既定の項目がサポートしているものは、テーブル スキーマ や各種の論理名、データの粒度などの メタデータ に加えて、サンプルデータ、データ品質ルールなど多岐に渡ります。 当検証では「 ODCS (Open Data Contract Standard) 」という YAML 形式のフォーマットを使用します。ODCSは Linux Foundation傘下のBitolプロジェクトがサポートする代表的な仕様です。 当記事では、Data Contractを生成AIへ与えるコンテキストとして活用します。Data Contractの作成自体は設計プロセスで生成AIに行わせることも可能です(別記事で説明予定)。 今回使用のData Contractの主要項目 主に使用した項目と各項目の簡単な説明を記載します。 name: カラム物理名 businessName: カラム論理名 logicalType: 論理データ型 physicalType: 物理データ型 transformSourceObjects: カラムのデータを作成する際に必要なソースデータに関する情報 transformLogic: カラムのデータを作成する際に必要なデータ変換ロジック relationships: カラムのリレーションシップ情報 quality: カラムのデータ品質要件 実際のData Contract 以下、代表的な2つのカラムのData Contractを抜粋します。 主キー兼外部キーの例: ws_item_sk こちらは前述のData Contractの詳述です。 複合主キーの一部であり外部キーでもあるカラムです。3テーブルの結合と、3種類のデータ品質テストが定義されています。 - name : ws_item_sk businessName : ウェブ販売商品サロゲートキー logicalType : string physicalType : STRING primaryKey : true primaryKeyPosition : 1 transformSourceObjects : - core_tpcds.item.i_item_sk transformLogic : SELECT i_item_sk FROM s_web_order JOIN s_web_order_lineitem ON (word_order_id = wlin_order_id) LEFT JOIN item ON (wlin_item_id = i_item_id AND i_rec_end_date IS NULL ) relationships : - type : foreignKey to : - core_tpcds.item.i_item_sk customProperties : - target_contract_id : "core_tpcds.item" - target_contract_path : "./core_tpcds/item.yaml" - target_property : "i_item_sk" quality : - type : library rule : duplicateCount name : "主キー重複チェック(ws_item_sk)" dimension : uniqueness mustBe : 0 severity : error businessImpact : "重複レコードによりファクトテーブルのデータ整合性が崩れ、売上集計に誤差が生じる" - type : library rule : nullCount name : "主キーNULLチェック(ws_item_sk)" dimension : completeness mustBe : 0 severity : error businessImpact : "NULL値が存在すると商品別売上分析が不可能になる" - type : sql name : "商品マスタ参照整合性チェック" dimension : consistency query : | SELECT COUNT(*) FROM ${object} ws LEFT JOIN item i ON ws.ws_item_sk = i.i_item_sk WHERE ws.ws_item_sk IS NOT NULL AND i.i_item_sk IS NULL mustBe : 0 severity : error businessImpact : "参照整合性が崩れると商品マスタとの結合で欠損が発生し、商品情報を取得できない売上データが生じる" 複雑な計算ロジックの例: ws_net_profit 純利益は複数のソースから計算され、計算整合性と平均値の妥当性の両方がチェックされます。 - name : ws_net_profit businessName : ウェブ純利益 logicalType : number physicalType : DECIMAL(7, 2) transformSourceObjects : - raw.s_web_order_lineitem.wlin_sales_price - raw.s_web_order_lineitem.wlin_quantity - raw.s_web_order_lineitem.wlin_coupon_amt - core_tpcds.item.i_wholesale_cost transformLogic : SELECT (((wlin_sales_price::DECIMAL(7,2) * wlin_quantity::INTEGER) - wlin_coupon_amt::DECIMAL(7,2)) - (i_wholesale_cost::DECIMAL(7,2) * wlin_quantity::INTEGER))::DECIMAL(7,2) FROM s_web_order JOIN s_web_order_lineitem ON (word_order_id = wlin_order_id) LEFT JOIN item ON (wlin_item_id = i_item_id AND i_rec_end_date IS NULL ) quality : - type : sql name : "純利益計算整合性チェック" dimension : accuracy query : | SELECT COUNT(*) FROM ${object} WHERE ws_net_profit IS NOT NULL AND ws_net_paid IS NOT NULL AND ws_ext_wholesale_cost IS NOT NULL AND ABS(ws_net_profit - (ws_net_paid - ws_ext_wholesale_cost)) > 0.01 mustBe : 0 severity : error businessImpact : "計算式が不正確な場合、利益分析の信頼性を損なう" - type : sql name : "純利益平均値チェック" dimension : accuracy query : | SELECT AVG(ws_net_profit) FROM ${object} WHERE ws_net_profit IS NOT NULL mustBeBetween : [ -100, 1000 ] severity : warning businessImpact : "平均利益が異常値の場合、価格設定や原価データに問題がある可能性" transformSourceObjectsとtransformLogicの重要性 当検証で特に試行錯誤したのが、この2つのプロパティの記述でした。 当初の仮説 : 各プロパティでデータ仕様を記述し、relationshipsプロパティでデータ間の関係性を記述すれば、生成AIがJOIN処理やカラム毎の計算ロジック、変換ロジックを類推できるのではないか 実際の課題 : データ仕様やリレーションシップ(データモデル上の関係性)とETL/ ELT 処理(データ変換ロジック)の間には隔たりがある 例えば、 ws_item_sk カラムは item テーブルと外部キー関係にありますが、実際のデータ取得には s_web_order → s_web_order_lineitem → item という3テーブルの結合が必要です。 これは生成AIに分かってもらえそうで分かってもらえませんでした。 s_web_order と item の2つのテーブルを無理に結合しようとしてしまいますし、チャットで説明して改善しても後で忘れて問題が再発し、忘れないように記録してもらっても個別のケースの実装方法として記録されて他ケースへの応用が懸念されました。 他に、生成AIは サロゲートキー どうしでテーブル結合したり、主キーで結合するように指示して改善しても今度は肝心の サロゲートキー を取得してくれなかったりしました。 また、カラムの演算処理は間違った内容が実装されるか全く実装されないかでした。 そのため、以下の2つのプロパティを使うようにしました: transformSourceObjects : データの出所(ソーステーブル・カラム)を明示 transformLogic : 具体的な変換ロジック(JOIN条件、フィルタ、計算式など)を記述 これにより、生成AIは「何を参照して、どう変換するか」を正確に理解し、意図通りの SQL を生成できるようになりました。 transformSourceObjectsとtransformLogicの作成方法 transformSourceObjectsプロパティとtransformLogicプロパティの作成自体をデータモデルの設計過程で生成AIと共に行います。 もし、データソース側のシステムで業務知識 (例. 純利益の計算式) が何かドキュメントなどに整理されてあれば、その内容をもとに生成AIが両プロパティを定義できる可能性があります。 また、例えばディメンショナル モデリング であれば、データ粒度 (Grain) の定義やConformance Matrixなどを機械判読可能なファイル形式で生成AIと共に作成し、それら成果物をソースとして生成AIが両プロパティを作成できる可能性があります。 relationshipsプロパティは ODCS v3.1.0 にリリース予定 ODCSの現在の最新公式バージョンはv3.0.2ですが、このバージョンには relationshipsプロパティが存在しません 。 ODCS v3.1.0でrelationshipsプロパティが実装される予定で、当検証ではODCSの v3.1.0 を先行的に使用しています。 v3.1.0での追加予定: ODCS v3.1.0 RFC0013 その他のプロパティの作成方法 nameプロパティやphysicalTypeプロパティなどの基礎的なプロパティの作成は、データソースのDBの スキーマ 情報や生データのデータファイルをソースとして生成AIに与えることで半自動生成もしくは自動生成が可能です。 Excel などの固有ソフトウェアのファイル形式の場合は、機械判読可能なオープンなファイル形式への変換が必要です。 さいごに 検証の振り返り 本記事では、Data Contract(ODCS形式)を活用したデータパイプラインのAI駆動開発手法を検証しました。 成果として得られたこと : 12テーブル結合、35カラムの複雑な SQL クエリを生成AIが正確に生成 Data Contractに定義した型キャスト、計算ロジック、JOIN条件が意図通りに実装 増分処理やdbtのincremental設定も適切に生成 成功要因 : データ アーキテクチャ とデータモデルの2軸でコンテキストを整理 データ項目やデータ型、制約などの スキーマ 情報に加えて、 transformSourceObjects と transformLogic でデータリネージを明示 ローカル完結型の開発環境で生成AIの試行錯誤を高速化 所感 データエンジニアリングのAI駆動開発に向けたドキュメント整備方法の1つを見出すことができて良かったと思います。 データ基盤/データ分析基盤を概念的に分解し、既存の専門用語に当てはめて構造化して説明することで生成AIが体系的かつ詳細に理解するようになることが実感できました。 また、特定の製品・サービスに依存せずに進められましたので、再利用性が比較的に高いと考えており、今後の応用として関連の製品・サービスと連携しDataOpsやAIOpsへ昇華できればと期待しています。 データマネジメントのより広い範囲への活用の可能性 本記事ではデータパイプラインのコード生成に焦点を当てましたが、Data Contract自体の生成やデータ品質テストの自動生成など、より広い範囲への活用も考えられます。実際にData Contractの quality プロパティを応用したデータ品質テストコードの生成を検証しており、その内容は別途記事化の予定です。 また、データの意味的な定義やデータ統合に関する マッピング 情報などを整備してData Contractを拡張することで、例えばデータマネジメント全般における機械判読可能なドキュメントとしての活用やAI伴走型の分析データモデル開発への応用も検討しています。 データエンジニアとして、データマネジメントにおけるData Contractと生成AIの活用可能性については、今後も継続的に検証を進めていく予定です。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @shikarashika レビュー: @yamada.y ( Shodo で執筆されました )