TECH PLAY

電通総研

電通総研 の技術ブログ

822

はじめに 最近、 AWS の AI Practitioner を取得した金融IT本部 2年目の坂江 克斗です。 今回は、XI 本部の佐藤悠さんに協力していただきながら執筆した記事となります。 最近、「AI エージェント」という言葉をよく耳にするようになりましたが、正直なところ、私自身はエージェントが何をしているのか全く分かっていませんでした。 そこで今回、AI初心者の自分なりに理解するため、 MCP ベースのエージェントを 専用のライブラリを使わずに自作 し、その仕組みを理解してみることにしました。 本記事では、AI 初心者の方向けに、以下のイメージを掴んでもらえればと思います。 AIエージェントが「どのような仕組み」なのか MCP ホスト / MCP クライアント / MCP サーバの役割分担 はじめに 学習フロー エージェントとは ざっくりAIを考える AIの弱み RAG(Retrieval-Augmented Generation) Function calling Model Context Protocol(MCP) AIエージェント エージェントの作成(MCPベース) 前提 実装方針 通信内容の整理 JSON-RPC MCP 今回想定するMCPリクエスト・レスポンス initialize notifications/initialized tools/list tools/call 検証 まとめ おわりに 学習フロー エージェントの概要を理解し、実装するにあたり大まかに以下の流れで進めていきました。 佐藤さんから MCP やエージェントに関する概要を聞き、ソースと併せて理解を深める(1日目) MCP の公式ドキュメントを確認し、機能を絞ってエージェント( MCP ホスト / MCP クライアント / MCP サーバ / LLMとの連携)を実装する(1-2日目) 佐藤さんが執筆した MCP に関する記事も、あわせてご覧ください。 【初心者向け】2年目エンジニアが実践したMCPサーバー構築ガイド2025 【初心者向け】2年目エンジニアが解説するMCPクライアント構築ガイド エージェントとは ざっくりAIを考える 私自身、モデル構造や学習 アルゴリズム を理解しているわけではないので、ここでは最低限持っておきたい概念レベルに絞って整理します。 AI とは、人間の知能的な振る舞いを模倣することを目的とした概念であり、現在一般ユーザーが日常的に触れている AI の多くは、LLM(Large Language Model)を中心としたシステムになっています。 LLM の内部構造を詳細に追い始めると一気に難しくなりますが、本質的には非常に多くのパラメータを持つ巨大な関数、つまり、 「入力データを与えると、出力データが返ってくる」 ただそれだけの存在です。( ニューラルネットワーク では「層」という概念を用いてパラメータを調整していますが、外から見ると最終的には数式で表現できる関数として扱えます) 重要なのは、 LLM 自身が自動的に外部 API を呼び出したり、何かを実行しているわけではない という点です。 ただし、その関数が持つパラメータ量と複雑さが人間の想像をはるかに超えているため、結果として「考えているように見える」出力が得られている、というわけです。 AIの弱み AI が関数である以上、当然ながら限界も存在します。 それは、学習時(関数の各パラメータの調整時)に与えられていない情報については、単体の LLM だけでは原則として扱うことができません。 そのため開発者としては、「AI が知らない情報を、外部のデータを取得して回答に含めてほしい」と考えるようになります。 この課題を解決するために登場した仕組みが、RAG(Retrieval-Augmented Generation:検索拡張生成)です。 RAG(Retrieval-Augmented Generation) RAG(Retrieval-Augmented Generation) は、「AIが、質問に関連する情報を参照しながら、回答を生成しているように見せる」仕組みです。 この仕組みでは、従来の Web 検索や 検索エンジン の裏側でも使われてきた ベクトル検索 が利用されています。 ベクトル検索の細かい仕組みや実装は難しいですが、ざっくりとWeb検索をした際に関連サイトが取得できるイメージを持っておけば問題ありません。 大まかな実装イメージは、次のようなシンプルな流れになります。 まず、ユーザがプロンプト(質問文など)を入力すると、その内容をもとにベクトル検索を行い、 あらかじめ用意しておいたデータベースの中から 、プロンプトに関連する情報を取得します。(検索に適したプロンプトに変換するために、事前に LLM を利用する方式も存在します) 次に、その取得した情報をプロンプトとあわせて LLM に渡すことで、より文脈に沿った、精度の高い回答を生成できるようになります。 一方で、RAG はあくまで 「検索を拡張する仕組み」 であり、関連する情報を用いて回答を生成することはできますが、 API を呼び出したり何らかの処理やタスクを実行したりすることはできません。 そこで登場したのが Function Calling です。 Function calling Function calling は、OpenAI が発表した仕組みで、「AI が外部システムと連携し、学習範囲外のデータ取得や API 実行などの処理を行えるように見せる」ための方法です。 ただし、最初に述べたとおり、AI 自体は API を呼び出すことはできません。そのため、 API を実際に呼び出す処理はアプリケーション側で実装する必要があります。 イメージとしては以下の図に示す、クライアント(アプリ)・サーバ(LLM)間での処理フローとなります。 初めのLLMへのリクエストにおいて、LLMに使用すべきツール(機能)を選択させ、その機能をクライアント側で実行したのちに、結果とともに再度LLMに回答をもらう流れとなります。仕組み自体は、「 API 呼び出しをうまく抽象化しているだけ」とも言えますが、この発想を形にした点が非常に面白いと感じました。 一方で、Function callingには弱点もあります。それは、ツール呼び出しの実装がクライアント側に閉じてしまうという点です。つまり、実装が各アプリごとに依存する課題を抱えています。 この課題を解決するために登場したのが MCP (Model Context Protocol) です。 Model Context Protocol( MCP ) Model Context Protocol(MCP) は Claude を開発している Anthropic 社が定義した プロトコル です。 Function calling と同様に、「AI が外部システムと連携して、処理・回答しているように見せる」という点は同じですが、ツール呼び出しの方法そのものを標準化したことが大きな特徴です。 アーキテクチャ は以下のようになります。先ほどのクライアントとなるアプリが、 MCP ホストと MCP サーバに分割された形になります。 Function Callingに比べて少しリソースが増えました。 全体の処理フローは以下のようになり、 MCP サーバがツールの実行を抽象化していることが分かります。 この構成の強みは、 MCP ホスト / MCP クライアント / MCP サーバという役割を標準化したことにあります。 MCP ホスト:1つ以上の MCP クライアントを管理し、LLMと対話しながら処理をする アプリケーション全体 。 MCP クライアント: MCP ホスト内部に存在し、 MCP プロトコル に基づいて MCP サーバと通信を行う コンポーネント 。( MCP ホストから MCP サーバへの呼び出しを抽象化) MCP サーバ:実際に 外部サービスとの連携を行う コンポーネント で、 MCP プロトコル に基づいて MCP クライアントと通信する 。 例えば、サービス提供者が MCP サーバを用意しておけば、利用者はその実装の詳細を意識せず、他の MCP サーバと同じ感覚で利用できます。 HTTP が存在しなかった世界では、サービスごとに独自の プロトコル で API を実装する必要がありました。実際には、HTTP という共通 プロトコル があるからこそ、私たちは意識せずに様々な Web サービスを利用できています。 MCP も、それと同じ文脈でサービスの提供者・使用者の目線で考えると分かりやすいかと思います。 AIエージェント Function Calling や MCP を用いることで、LLM が本来単体では実行できないタスクについても、外部サービス(ツール)による処理の実行や、外部サービスにより取得したデータを文脈として取り込みながら回答を生成できるようになります。 このように、 外部のサービスやデータと連携した処理を基に振る舞う仕組み全体を、一般に AI エージェント と呼びます。 ただし、実態としてはWeb アプリケーションとしてのクライアント・サーバ構成で動いているという点を理解しておくことが重要です。 エージェントの作成( MCP ベース) 前提 ということで、実際に MCP をベースにしたエージェントを実装してみます。 本記事の目的は、 MCP の推奨構成をそのまま再現することではなく、各 コンポーネント がどんな順序で、どんなデータをやり取りしているかを理解することです。 そのため、 公式ドキュメント で扱われているいくつかの要素を省略し、以下の方針で実装を行いました。 目標 エージェントに「( アメリ カの都市名)の直近の天気」を必要とする質問を尋ねると、実際の天気予報 API を基にした回答を返してくれること 実装方針 MCP のバージョン 2025-11-25( Model Context Protocol 、 GitHub ) 機能面 MCP サーバ Prompts / Resources は省略して、Tools のみを実装し、外部 API (天気予報 API )の呼び出しを主に実装 ローカルでHTTPサーバとして起動 MCP クライアント Roots / Sampling / Elicitation は省略し、 MCP サーバに対する Tools の呼び出しを主に実装 MCP ホストにクラスとして内包される MCPホスト UIとして CLI でのstdin / stdoutを使用(ユーザとの入出力は コマンドライン ベース) ローカルで起動 通信面 HTTP ベースでの実装 SSEやセッション管理(stateful)を省略し、ステートレスに実装 認証は実装しない 使用技術 LLM:Claude MCP ホスト / MCP クライアント / MCP サーバー: JavaScript (TypeScript)での実装 実装コードは GitHub で公開しています。詳細が気になる方は mcp-sample を参照してください( mcp 系のライブラリを使用せず自作したこともあり、設計・実装ともに粗い点はご容赦ください)。 実装方針 MCP の公式ドキュメントをただ読むだけで、そのままコードに落とし込むのは難しく感じたため、今回は以下の手順で実装をします。 先に発生するすべてのHTTP通信を書き出し 各通信それぞれの HTTPヘッダ と ボディ( JSON -RPC) を整理 通信内容から逆算して実装。 通信内容の整理 JSON -RPC MCP では JSON-RPC 2.0 がメッセージ形式として採用されています。 以下が JSON -RPC の基本的な形式です。 リクエスト { " jsonrpc ": " 2.0 ", " id ": " number ", " method ": " string ", " params ": { object } } レスポンス { " jsonrpc ": " 2.0 ", " id ": " number ", " result ": { object } } レスポンス(エラー) { " jsonrpc ": " 2.0 ", " id ": " number ", " error ": { " code ": " number ", // -32768 ~ -32000 are reserved, " message ": " string ", " data ": " primitive or structed value " } } 今回はこの JSON -RPC を HTTP リクエストのボディに乗せて送受信する形で実装します。 つまり、HTTP はあくまで通信経路として利用し、実際の処理内容やメソッドの表現は JSON -RPC によって行う構成になります。 TCP の上でHTTPが成り立つように、HTTPのボディの上で JSON -RPC形式によるやり取りを行うイメージです MCP 本来の MCP の通信では、 公式ドキュメントのシーケンス図 に記載されているとおり、Server-Sent Events(SSE)やセッション管理を含む構成が想定されています。 しかし、今回の構成では理解を優先するため、SSE やセッション管理は割愛し、公式ドキュメントの構成を簡略化した以下の形で MCP によるやり取りを行います。 今回はセッションを持たないステートレスな HTTP 通信として実装しているため、通信の終了フェーズを明示的に実装する必要はありません。 今回想定する MCP リクエスト・レスポンス 通信の全体像が把握できたところで、公式ドキュメントに記載の ライフサイクル章 や トランスポート章 を参考にし、各フェーズにおいてどのようなデータが送受信されるのかを整理します。 initialize リクエスト ヘッダ Content-Type: application/json Accept: application/json - ボディ { " jsonrpc ": " 2.0 ", " id ": 1 , " method ": " initialize ", " params ": { " protocolVersion ": " 2025-11-25 ", " capabilities ": {} , " clientInfo ": { " name ": " ExampleClient ", " version ": " 1.0.0 ", " description ": " An example MCP client application " } } } レスポンス ステータス: 200 ボディ { " jsonrpc ": " 2.0 ", " id ": 1 , " result ": { " protocolVersion ": " 2025-11-25 ", " capabilities ": { " tools ": { " listChanged ": false } } , " serverInfo ": { " name ": " ExampleServer ", " version ": " 1.0.0 ", " description ": " An example MCP server providing tools and resources " } } } notifications/initialized リクエスト ヘッダ Content-Type: application/json Accept: application/json MCP-Protocol-Version: 2025-11-25 - ボディ { " jsonrpc ": " 2.0 ", " method ": " notifications/initialized " } レスポンス ステータス: 202 ボディ:空 tools/list リクエスト ヘッダ Content-Type: application/json Accept: application/json MCP-Protocol-Version: 2025-11-25 - ボディ { " jsonrpc ": " 2.0 ", " id ": 2 , " method ": " tools/list ", " params ": {} } レスポンス ステータス: 200 ボディ { " jsonrpc ": " 2.0 ", " id ": 2 , " result ": { " tools ": [ { " name ": " get_weather ", " description ": " Get current weather information for a location ", " inputSchema ": { " type ": " object ", " properties ": { " latitude ": { " type ": " number " } , " longitude ": { " type ": " number " } } , " required ": [ " latitude ", " longitude " ] } } ] } } tools/call リクエスト ヘッダ Content-Type: application/json Accept: application/json MCP-Protocol-Version: 2025-11-25 - ボディ { " jsonrpc ": " 2.0 ", " id ": 3 , " method ": " tools/call ", " params ": { " name ": " get_weather ", " arguments ": { " longitude ": " -94.0 ", " latitude ": " 40.71 " } } } レスポンス ステータス: 200 ボディ { " jsonrpc ": " 2.0 ", " id ": 3 , " result ": { " content ": [ { " type ": " text ", " text ": " Current weather in New York: \n Temperature: 72°F \n Conditions: Partly cloudy " } ] } } このようにデータの流れをイメージできたため、あとはこの流れ通りに動作するよう MCP ホスト / MCP クライアント / MCP サーバ を実装していきました。 その結果が、先ほど紹介した リポジトリ の状態となります。 実装を進める中で、特に詰まった点は以下の2点です。 LLMが JSON を返却する際、どうしてもコードブロックを含めてしまう点 システムプロ ンプトによる制御では完全に回避できなかったため、文字列を整形する処理を実装し対処。 「直接天気を調べて」とは指示せず、「天気をもとにアド バイス してほしい」といった依頼をした場合に、LLMがツールを選択せず、「その情報を調べてもよいですか?」と確認してくるケースがあった点 「回答に必要な処理が存在する場合は、必ずツール呼び出しのみを返すこと」を明示したプロンプトで対処。 検証 本章では、前章で整理した通信設計が、実際の実装でもその通りに動作しているかを確認します。 初めにローカル環境で MCP サーバーを起動します。 PS C:\Users\katsu\Documents\github\mcp-sample\server> npm run start > mcp-server@1.0.0 start > node dist/index.js MCP server listening on http://127.0.0.1:8080/mcp 次に、同じくローカル環境で MCP ホストを起動します( MCP ホストはプロセス内部で MCP クライアントクラスの インスタンス を生成・管理しています)。 起動後はユーザーの入力を促すメッセージが出るので、依頼文(ユーザプロンプト)を入力します。 PS C:\Users\katsu\Documents\github\mcp-sample\host> npm run start > mcp-host@1.0.0 start > node --env-file=.env dist/index.js ################################################################## ------------ initialize request ------------ Content-Type: application/json Accept: application/json { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-11-25", "capabilities": {}, "clientInfo": { "name": "weather", "version": "0.1.0", "description": "Weather MCP Client with Stateless and Streamable HTTP and non SSE" } } } ------------ initialize response ------------ status: 200 { "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2025-11-25", "capabilities": { "tools": { "listChanged": false } }, "serverInfo": { "name": "weather", "version": "0.1.0", "description": "Sample MCP Server with Stateless and Streamable HTTP and non SSE" } } } ################################################################## ------------ notifications/initialized request ------------ Content-Type: application/json Accept: application/json MCP-Protocol-Version: 2025-11-25 { "jsonrpc": "2.0", "method": "notifications/initialized" } ------------ notifications/initialized response (no body) ------------ status: 202 ################################################################## ------------ tools/list request ------------ Content-Type: application/json Accept: application/json MCP-Protocol-Version: 2025-11-25 { "jsonrpc": "2.0", "id": 2, "method": "tools/list" } ------------ tools/list response ------------ status: 200 { "jsonrpc": "2.0", "id": 2, "result": { "tools": [ { "name": "get_weather", "description": "Get weather forecast for US coordinates using NWS API.", "inputSchema": { "type": "object", "properties": { "latitude": { "type": "number" }, "longitude": { "type": "number" } }, "required": [ "latitude", "longitude" ] } } ] } } ################################################################## # 質問してみましょう 明日夜のニューヨークの天気を基に、服装のアドバイスをしてください。今は半袖短パンを想定しています。 ################################################################## ------------ LLM request ------------ Content-Type: application/json x-api-key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX anthropic-version: 2023-06-01 { "model": "claude-sonnet-4-5", "max_tokens": 1000, "system": "\nYou are a tool-routing assistant.\n\nABSOLUTE RULES:\n- You MUST respond in Japanese only.\n- Output MUST be a single valid JSON object.\n- Do NOT use Markdown.\n- Do NOT use code fences or triple backticks.\n- The first character MUST be \"{\" and the last character MUST be \"}\".\n- Do NOT include any text outside the JSON object.\n", "messages": [ { "role": "user", "content": "\nYou will be given:\n1) A list of available tools (JSON)\n2) A user request\n\nDecide whether a tool is needed.\n\nRules:\n- Use a tool only if it materially improves correctness or usefulness.\n- If multiple tools could work, pick exactly ONE.\n- Never invent tools or parameters.\n- If required parameters are missing, ask ONE concise follow-up question.\n- If answering accurately to user request requires external or real-world data, you MUST select the appropriate tool and make a tool_call.\n\nOutput format (JSON only):\n\nTool call:\n{\n \"type\": \"tool_call\",\n \"tool\": \"<tool_name>\",\n \"arguments\": { },\n \"reason\": \"<short reason>\"\n}\n\nDirect answer:\n{\n \"type\": \"direct_answer\",\n \"answer\": \"<answer>\",\n \"reason\": \"<short reason>\"\n}\n\nTools (JSON):\n[\n {\n \"name\": \"get_weather\",\n \"description\": \"Get weather forecast for US coordinates using NWS API.\",\n \"inputSchema\": {\n \"type\": \"object\",\n \"properties\": {\n \"latitude\": {\n \"type\": \"number\"\n },\n \"longitude\": {\n \"type\": \"number\"\n }\n },\n \"required\": [\n \"latitude\",\n \"longitude\"\n ]\n }\n }\n]\n\nUser request:\n明日夜のニューヨークの天気を基に、服装のアドバイスをしてください。今は半袖短パンを想定しています。\n" } ] } ------------ LLM response ------------ status : 200 { "model": "claude-sonnet-4-5-20250929", "id": "msg_0144BrLAY9HUWMcHmgopBxpp", "type": "message", "role": "assistant", "content": [ { "type": "text", "text": "```json\n{\n \"type\": \"tool_call\",\n \"tool\": \"get_weather\",\n \"arguments\": {\n \"latitude\": 40.7128,\n \"longitude\": -74.0060\n },\n \"reason\": \"ニューヨークの明日夜の天気予報を取得して、適切な服装アドバイスを提供するため\"\n}\n```" } ], "stop_reason": "end_turn", "stop_sequence": null, "usage": { "input_tokens": 481, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "cache_creation": { "ephemeral_5m_input_tokens": 0, "ephemeral_1h_input_tokens": 0 }, "output_tokens": 104, "service_tier": "standard" } } ################################################################## ------------ tools/call request ------------ Content-Type: application/json Accept: application/json MCP-Protocol-Version: 2025-11-25 { "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "get_weather", "arguments": { "latitude": 40.7128, "longitude": -74.006 } } } ------------ tools/call response ------------ status: 200 { "jsonrpc": "2.0", "id": 3, "result": { "content": [ { "type": "text", "text": "Forecast for 40.7128, -74.006:\n\nTonight:\nTemperature: 13°F\nWind: 14 to 22 mph N\nMostly Cloudy\n---\nSunday:\nTemperature: 25°F\nWind: 17 to 22 mph N\nMostly Cloudy\n---\nSunday Night:\nTemperature: 16°F\nWind: 14 to 17 mph NW\nMostly Cloudy\n---\nMonday:\nTemperature: 32°F\nWind: 14 mph NW\nSunny\n---\nMonday Night:\nTemperature: 16°F\nWind: 8 to 12 mph NW\nMostly Clear\n---\nTuesday:\nTemperature: 31°F\nWind: 9 mph W\nSunny\n---\nTuesday Night:\nTemperature: 22°F\nWind: 6 to 10 mph W\nPartly Cloudy\n---\nWednesday:\nTemperature: 30°F\nWind: 9 mph NW\nMostly Sunny\n---\nWednesday Night:\nTemperature: 17°F\nWind: 10 mph NW\nMostly Clear\n---\nThursday:\nTemperature: 29°F\nWind: 10 mph NW\nSunny\n---\nThursday Night:\nTemperature: 15°F\nWind: 10 mph NW\nPartly Cloudy\n---\nFriday:\nTemperature: 30°F\nWind: 8 to 12 mph W\nPartly Sunny then Slight Chance Light Snow\n---\nFriday Night:\nTemperature: 18°F\nWind: 12 to 15 mph W\nChance Light Snow\n---\nSaturday:\nTemperature: 22°F\nWind: 16 to 21 mph NW\nChance Light Snow then Mostly Sunny\n---" } ] } } ################################################################## ------------ LLM request ------------ Content-Type: application/json x-api-key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX anthropic-version: 2023-06-01 { "model": "claude-sonnet-4-5", "max_tokens": 1000, "system": "\nYou are a Japanese assistant.\n\nABSOLUTE RULES:\n- You MUST respond in Japanese only.\n", "messages": [ { "role": "user", "content": "\nYou will be given:\n1) A list of API result\n2) A user request\n\nAnswer user request with API result.\nDo not mention that you are referring to the API results.\n\nAPI Result (JSON):\n{\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"Forecast for 40.7128, -74.006:\\n\\nTonight:\\nTemperature: 13°F\\nWind: 14 to 22 mph N\\nMostly Cloudy\\n---\\nSunday:\\nTemperature: 25°F\\nWind: 17 to 22 mph N\\nMostly Cloudy\\n---\\nSunday Night:\\nTemperature: 16°F\\nWind: 14 to 17 mph NW\\nMostly Cloudy\\n---\\nMonday:\\nTemperature: 32°F\\nWind: 14 mph NW\\nSunny\\n---\\nMonday Night:\\nTemperature: 16°F\\nWind: 8 to 12 mph NW\\nMostly Clear\\n---\\nTuesday:\\nTemperature: 31°F\\nWind: 9 mph W\\nSunny\\n---\\nTuesday Night:\\nTemperature: 22°F\\nWind: 6 to 10 mph W\\nPartly Cloudy\\n---\\nWednesday:\\nTemperature: 30°F\\nWind: 9 mph NW\\nMostly Sunny\\n---\\nWednesday Night:\\nTemperature: 17°F\\nWind: 10 mph NW\\nMostly Clear\\n---\\nThursday:\\nTemperature: 29°F\\nWind: 10 mph NW\\nSunny\\n---\\nThursday Night:\\nTemperature: 15°F\\nWind: 10 mph NW\\nPartly Cloudy\\n---\\nFriday:\\nTemperature: 30°F\\nWind: 8 to 12 mph W\\nPartly Sunny then Slight Chance Light Snow\\n---\\nFriday Night:\\nTemperature: 18°F\\nWind: 12 to 15 mph W\\nChance Light Snow\\n---\\nSaturday:\\nTemperature: 22°F\\nWind: 16 to 21 mph NW\\nChance Light Snow then Mostly Sunny\\n---\"\n }\n ]\n}\n\nUser request:\n明日夜のニューヨークの天気を基に、服装のアドバイスをして ください。今は半袖短パンを想定しています。\n" } ] } ------------ LLM response ------------ status : 200 { "model": "claude-sonnet-4-5-20250929", "id": "msg_011ykwGSo5qRzEHTQpD9UV9G", "type": "message", "role": "assistant", "content": [ { "type": "text", "text": "明日夜のニューヨークの天気は以下の通りです:\n\n**日曜日の夜**\n- 気温:16°F(約-9°C)\n- 風:14〜17 mph 北西 の風\n- 天候:ほぼ曇り\n\n**服装のアドバイス:**\n\n半袖短パンは**絶対に避けてください**。この気温では命に関わる危険性があり ます。\n\n以下の装備が必要です:\n\n1. **厚手の冬用コート**(ダウンジャケットやウールコートなど)\n2. **重ね着**(長袖のイン ナー、セーター、フリースなど複数枚)\n3. **厚手の長ズボン**(ジーンズやウールパンツに加えて、可能であればレギンスなどの下着も)\n4. **防寒アクセサリー**:\n - 帽子(耳まで覆えるもの)\n - 手袋(厚手のもの)\n - マフラー\n - 厚手の靴下\n5. ** 防風性のある上着**(風が強いため)\n\n氷点下の極寒の天候ですので、肌の露出は最小限に抑え、しっかりと防寒対策をしてお出かけく ださい。" } ], "stop_reason": "end_turn", "stop_sequence": null, "usage": { "input_tokens": 713, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "cache_creation": { "ephemeral_5m_input_tokens": 0, "ephemeral_1h_input_tokens": 0 }, "output_tokens": 382, "service_tier": "standard" } } ################################################################## 明日夜のニューヨークの天気は以下の通りです: **日曜日の夜** - 気温:16°F(約-9°C) - 風:14〜17 mph 北西の風 - 天候:ほぼ曇り **服装のアドバイス:** 半袖短パンは**絶対に避けてください**。この気温では命に関わる危険性があります。 以下の装備が必要です: 1. **厚手の冬用コート**(ダウンジャケットやウールコートなど) 2. **重ね着**(長袖のインナー、セーター、フリースなど複数枚) 3. **厚手の長ズボン**(ジーンズやウールパンツに加えて、可能であればレギンスなどの下着も) 4. **防寒アクセサリー**: - 帽子(耳まで覆えるもの) - 手袋(厚手のもの) - マフラー - 厚手の靴下 5. **防風性のある上着**(風が強いため) 氷点下の極寒の天候ですので、肌の露出は最小限に抑え、しっかりと防寒対策をしてお出かけください。 上記の結果から、想定通りの通信が発生し、回答も外部サービスを使用した結果が含まれていることが確認できました。 まとめ 外部のサービスやデータを用いて、あたかもLLM自身が処理を行ってタスクを実施し、回答しているように見せる仕組み全体を、AIエージェントと呼びます。 その中で、外部サービス(ツール)呼び出しの形式を標準化したものがFunction Callingや MCP です。 特に MCP で定義された コンポーネント の役割は以下のように分類できます。 MCP ホスト:1つ以上の MCP クライアントを管理し、LLMと対話しながら処理をする アプリケーション全体 。 MCP クライアント: MCP ホスト内部に存在し、 MCP プロトコル に基づいて MCP サーバと通信を行う コンポーネント 。 MCP サーバ:実際に 外部サービスとの連携を行う コンポーネント で、 MCP プロトコル に基づいて MCP クライアントと通信する 。 実体がWebアプリと同じようなクライアント・サーバ構成である以上、 MCP サーバの配置場所や実行させる処理内容によっては、セキュリティや監査の扱いが非常に重要になります。例えば、ローカルで実行した MCP サーバが意図せず内部ファイルに干渉してしまうケースや、外部に公開した MCP サーバがセキュリティ攻撃にさらされる可能性などが考えられます。 これは、これまでの Web サービスと本質的には同じであるため、適切な アーキテクチャ 設計と セキュリティポリシー の策定が、今後ますます定式化されていく領域だと感じています。 おわりに 佐藤さんの助けもあり、これまで実態がつかめていなかった「AI エージェント」という概念を掴みつつ、実装しながら理解を深められたのはとても面白い体験でした。 このエージェントをさらに抽象化した仕組みとして Strands が登場しているので、次はそちらの実装を検証してみたいと思います。 今後も抽象化のレイヤーはどんどん高まっていくはずなので、流れに取り残されないためにも、AI 関連技術の基礎知識を学びつつ、使用者側としても継続的に研鑽していきます。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @sakae.katsuto レビュー: @yamashita.tsuyoshi ( Shodo で執筆されました )
アバター
こんにちは、 電通 総研 XI本部 サイバーセキュリティテクノロ ジー センターの櫻井です。 本記事では 代表的な クラウド サービスプロバイダ(以降、CSPと略)である AWS ( Amazon Web Services )、 Microsoft Azure、 Google Cloudの最上位アーキテクト資格に関する比較を紹介します。 なお、本記事でご紹介する資格の情報は2026年1月時点のものとなります。 アーキテクト資格の種類 筆者の経歴 チャレンジのおススメ順 各CSP最上位資格で共通で問われる内容 各CSP最上位資格の特色 まとめ 最後に アーキテクト資格の種類 各CSP資格群はFoundational、Associate、Professiona(Expert)lの3段階に分かれており、受験者の対象と試験の大枠としては以下を想定しています。 Foundational 対象: クラウド 初心者やIT未経験者。また、基礎知識を理解したい営業・ マーケティング ・プロジェクトマネジメントサイドの担当者 試験の大枠: クラウド 自体の理解、大枠のサービス概要、 クラウド 利用料計算の理解 Associate 対象:ソリューション設計に1年以上の実務経験のあるITエンジニア 試験の大枠:設計に用いるソリューションの基本理解 Professional(Expert) 対象:ソリューション設計に理解のある、実務経験のあるITエンジニア 試験の大枠:設計時のソリューションベストプラクティスの理解、適切なサービス組み合わせの理解 それぞれの資格群の名称(略称)に関しては以下の表のとおりです。 Grade AWS Azure Google Foundational Cloud Practitioner(CLF) Azure Fundamentals(AZ-900) Cloud Digital Leader(CDL) Associate Solutions Architect Associate(SAA) Azure Administrator(AZ-104) Associate CloudEngineer(ACE) Professional(Expert) Solutions Architect Professional(SAP) Azure SolutionsArchitect Expert(AZ-305) Professional Cloud Architect (PCA) 以降は、Professional、Expert資格にフォーカスして説明を行います。 筆者の経歴 (テンプレにはなりますが、)オンプレ& クラウド 、ネットワーク&セキュリティのごちゃまぜエンジニアです。担務領域に関しては特にこだわりなくなんとなく興味を持った資格は取得してます。(※ My credly ) 電通 総研ではサイバーセキュリティテクノロ ジー センターで主にセキュリティサービス設計、運用やマルチ クラウド のセキュリティ設計レビューに従事しています。 また、筆者の各CSPの経験歴としては以下となります。 AWS 7年程度、メインは クラウド インフラ設計ガッツリです。オンプレ接続のために用いる クラウド リソース管理や運用/監視、リソースデプロイの自動化がメインタスクでした。 電通 総研に入社後はアプリまで含めた AWS 全般のセキュリティ設計レビューを行っています。 Microsoft Azure 5年程度、メインは クラウド マイグレーション に関わるインフラ設計、ID( Microsoft Entra)の設計・運用でした。 電通 総研に入社後は Microsoft defender for cloudをはじめとしたセキュリティサービスの調査や社内向けサービス設計に従事しています。 Google Cloud 3年程度、以前はBigQueryやApp Engineを触っていた程度です。 電通 総研に入社後は Google Cloudログサービスの調査・検証の実施、Firebaseを利用したWebサイト構築のセキュリティ設計レビューにも参加しています。 チャレンジのおススメ順 最初に、3種すべてを取得するならばの前提で筆者の考える難易度を記載します。各試験で問われる範囲と出題形式、傾向、学習コンテンツを踏まえて、以下の順が望ましいと考えています。 Google Cloud(Professional Cloud Architect) AWS (Solutions Architect Professional) Microsoft Azure(Azure Solutions Architect Expert) はい、見事に筆者のチャレンジ順とは逆です。 単一CSPでガッツリ設計をやっている方は専門の分野を突き詰めるのが望ましいのであまり難易度比較に意味はありませんが、基本は下に行くほど難しい試験である(事前準備が必要である)と筆者は考えています。 各CSP最上位資格で共通で問われる内容 最上位資格の試験について、共通で必要とされる内容を簡単に整理します。 対象CSPのサービス全般に対する理解① 対象のサービス種類(試験に含まれるサービスの範囲) CSPの試験ガイドで具体的に対象と記載されているサービスについてすべてが対象となります。 例として、 AWS (Solutions Architect Professional)であれば 試験ガイド の「範囲内の AWS のサービスと機能」となっているサービスはすべて対象です。 各試験によってどこまでを対象とするかは異なりますが、ITサービス構築のために クラウド インフラを利用するだけではなじみのないAIやデータ分析のサービスも含まれますので、普段利用していないサービスでも用途等は理解しておく必要があります。 対象CSPのサービス全般に対する理解② サービスの単位 「 AWS における VPC はリージョナルサービスである」、「 Google Cloudにおける VPC はグロー バルサ ービスである」という比較に代表されるように クラウド 設計におけるサービスがどこまでの範囲で展開可能かを明確に覚えておきましょう。 対象としているCSPにフォーカスしてきちんと学習しましょう。実務目線でも設計や提案の際に理解できていないと致命的です。 例えば、複数リージョンに渡る負荷分散の設計方法は各CSP毎に大きく異なる等ですね。 (この場合、グローバルロードバランシングの種類をちゃんと理解しているか?になります) 設計の理解① 設定パラメータ 利用ケースが多いサービス(コンピュート、コンテナ、アプリケーション統合)については細かい設定が問われる問題が出題されます。この点に関しては明確に実務経験があった方が望ましいといえます。 対象サービスのパラメータを設定するのに、どういった方法があるのかも含めて押さえておくとよいです。 1.「環境を作りなおす必要があるのか?」もしくは「既存の環境のままで設定変更が可能か?」 2.「マネジメントコンソールから設定できるか?」もしくは「 API や CLI からのみ設定できるのか?」 設計の理解② ソリューションの選定、ソリューションの組み合わせ比較 出題内容としては、具体的なソリューションを一意に特定するケースに加えて、必要な要件に対して複数のソリューションの組み合わせを比較した上で最も適したものを選択するケースも存在します。 いわゆるベストプラクティスを選択する必要がありますので、 AWS の場合は Black Belt の様な公式のソリューションガイドを参考に学習するとよいです。 筆者の経験でも、顧客の特殊要件を踏まえ意図的にベストプラクティスを選択しなかった経験もありますし、当時と比較して新しいサービスがリリースされている可能性もありますので、最低限は確認しましょう。 各CSP最上位資格の特色 それぞれの試験の概要と筆者の試験自体に関する印象、筆者の主観的分析を紹介します。 AWS Solutions Architect Professional(SAP) 問題数:75問、時間:180分 試験後の印象 とにかく問題数が多い。そして問題文、選択肢の文章が比較的長いので持続的な集中力を求められる。 AWS のProfessional、Security分野の試験では共通している点かと思います。 主観的分析 AWS に関してはCSPがリリースしているサービスの種類が他の2社と比べて多いです。(200個以上) 時間をかけて理解する必要があり、初学者は数に圧倒されると思います。 問題そのものは取り組みやすいですが、文章量の理解に時間を要するため練習問題等で慣れておく必要があります。 SAPを含め、 AWS 資格のコンテンツは数多く存在するためこの点は学習者にプラスです。 Azure Solutions Architect Expert(AZ-305) 問題数: 不定 (概ね50問前後)、時間:120分 試験後の印象 設計の組み合わせやサービスの詳細など細かい部分まで回答を求められる。(理由は後述) 試験への慣れが必要。3パートに分かれており、2パートは選択を終了したら戻ることができない。 本試験はダイレクトに「 クラウド インフラ設計」の理解を求められる。 Microsoft Webページ の記載の通り、複数系統に分かれているためです。 主観的分析 CSP3種試験の中では難易度は最も高いと思います。 設計にフォーカスしているため、サービス単体を理解しているだけではNGなケースが多いです。 Microsoft AzureのAssociate以上の試験に関しては Microsoft Learn でコンテンツを検索しながら回答が可能です。 その点に起因するかはわかりませんが、事前の学習では手が回らないような調べないとほぼ回答できないような細かいサービス詳細に関する問題も一部出題されます。(この要件を満たすライセンスの種類は?というような問題) 購入した問題集の設問で構わないのでこんな感じで検索したら出てきそうというのを Microsoft Learnで事前に確認しておくとよいです。関係のないコンテンツも検索結果に含まれてしまい、思いのほか抽出が難しいです。 40問程度の大問パート以外にシナリオ問題とケース問題で合計3パート以上で構成され、パート間で戻ることはできないため、時間配分を間違わないようにしましょう。(筆者はAZ-305以外の試験で勘違いし、ドボンしています) 市販の学習用コンテンツは最低限は存在します。 Microsoft Azureは Microsoft Learn以外にも公式の トレーニングコース が充実しているため、集中的に学習したいものは登録しましょう。 開催日程が決まっていますが、 Microsoft Virtual Training Days で無料で受講できるハンズオンや講習も存在するので確認しましょう。 Google Cloud Professional Cloud Architect(PCA) 問題数: 不定 (50問以上、60問未満)、時間:120分 試験後の印象 一定以上の知識は必要だが、複雑な問題はあまり出題されない。 サービスの種類と設計をバランスよく理解しておくことが求められる。 数問で構成されるケース問題が複数出題される。全く関係ない導入文も含まれるので選択をミスらないようにしましょう。 試験中に問題言語を英語に切り替えられない。筆者だけかもしれませんが、細かいオプション説明を英字で確認しようと切り替えても日本語のままでした。 主観的分析 個人的な忖度ではないですが、試験自体は一番取り組みやすいと感じました。試験時間と問題文、文章量がマッチしていると思います。 ACEとは別にProfessionalとなっているので簡単ではないですが、適度な難易度の問題が適量出題されるイメージです。 難点は学習コンテンツが少ないことかと思います。PCAに限っては市販の書籍は見当たりませんでした。 Udemyのコンテンツで日本語対応のもの(※1、※2)がありますのでこちらをお勧めします。 筆者の感覚では既にACEを取得済みであれば、Udemyのコンテンツを繰り返すだけでも合格することは十分可能だと思います。(自信がない方はラボ演習をしましょう) 所属する会社によって変わりますが、 Google Cloudに関する招待制ト レーニン グプログラムも存在します。無料でラボコンテンツが一定期間利用できるプログラムです。 ※1 https://www.udemy.com/course/google-cloud-professional-cloud-architectpca/ ※2 https://www.udemy.com/course/google-cloud-professional-cloud-architect-i/ まとめ 以上を踏まえて、それぞれの試験を試験自体の難易度(筆者の感覚)、試験への慣れの必要性、学習コンテンツの豊富さで整理したいと思います。(勉強時間は各個人によるので含めません) 評価観点 AWS (SAP) Azure(AZ-305) Google (PCA) 試験自体の難易度(筆者感覚) 普通 (サービスの数は多いが基本的なサービス組み合わせ、設計等を理解できていれば問題なし。) 難しい (ひとえに難しい。細かいところまで出題される。) 比較的取り組みやすい (サービスの種類を覚え、組み合わせや設計が理解できていれば問題なし。) 試験への慣れの必要性 ある程度必要 (問題数、文章量への慣れが必要。) 慣れが必要 (最終的にExpertとなるためにはAZ-104の合格も併せて必要なので先に受験し訓練しましょう。) ほぼ不要 (初チャレンジでもケース問題の切り替えに注意すれば問題はないと思います。) 学習コンテンツの豊富さ 豊富 (ググっても、 Amazon で検索しても、Udemyでも大量にあります。むしろ精査が難しいレベル。) 普通 (発売されているものは少ないですが、良書が多いので一度は目を通しましょう。) 少ない (市販書籍がないのでUdemy等を活用しましょう。) 表からチャレンジ順の理由がご理解いただけたかと思いますが、AZ-305に関しては、事前に準備しないといけないもの(知識も当然だが、問題形式や回答方法の予習)が多く、3種横並びで見た場合で最初にチャレンジするのは難易度が高いという評価です。ただ、 クラウド インフラ設計という点からは逸脱している内容ではないので、学んでおくことでプラスになる内容であると思います。 最後に 読了いただきありがとうございました。 筆者は事前の積み重ねもあって3種のArchitect最上位資格を約半年間で取得できました。しかし、エンジニア(もしくは コンサルタント )としてのゴールは資格を取得することではありません。 基本は クラウド への理解を深めるためにCSPの提供するラボ等を活用し、じっくり学習することをお勧めします。 今後も クラウド 、セキュリティに関する投稿を行っていきますのでよろしくお願いします。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @sakurai.ryo レビュー: @kobayashi.hinami ( Shodo で執筆されました )
アバター
はじめに 金融IT本部 2年目の坂江 克斗です。 業務にてアウトバウンドセキュリティを考えるタイミングがあったため、本記事を書きました。 初学者の視点で疑問に感じる部分も含め、アウトバウンドセキュリティ全体の基本的な概念を解説できればと思います。 はじめに アウトバウンドセキュリティの概要 AWSのアウトバウンドセキュリティサービス Route53 Resolver DNS Firewall Network Firewall Network Firewall Proxy アウトバウンドセキュリティの設計方針 Deny List (拒否リスト) Strict Allow List (厳格な許可リスト) Generous Allow List (寛容な許可リスト) まとめ おわりに アウトバウンドセキュリティの概要 Webアプリケーション開発においては、 SQLインジェクション や XSS 、DDoS など、インバウンド通信に対するセキュリティを意識する機会が多い一方で、アウトバウンド通信のセキュリティについては、あまり意識されないことが多いように感じます。 しかし、 AWS re:Inforce 2025 - Outbound network controls made easy でも述べられているとおり、アウトバウンドセキュリティは 防御(Prevention) と 可視性(Visibility) の両面において、近年ますます重要になっています。 防御の観点では、以下のケースが挙げられます。 マルウェア に感染したサーバから外部の悪意あるエンドポイントへの通信を防止。 ソフトウェア サプライチェーン において、意図しない参照先や汚染された OSS へのアクセスを制御。 直接的な通信遮断に至らない場合であっても、 不正アクセス や異常な通信パターンを検知する機会の増加。 可視性の観点では、以下のケースが挙げられます。 NAT Gateway と VPC エンドポイントの使い分けのような設定ミスの検知 SOC(Security Operations Center)における分析・調査のためのセキュリティログや分析材料の充実。 よって、防御と可視性の両面において、アウトバウンドセキュリティが重要であることが分かります。 アウトバウンドセキュリティは大枠として、 DNS ベース、パケットヘッダ(3層-7層)ベース、ボディベースでのフィルタリング が中心となり、組み合わせることで多層防御を形成します。(ただし、ボディの解析は処理負荷があるため、セキュリティと性能のバランスを考慮した設定が必要となると思われます) AWS では以下の表に示すアウトバウンドセキュリティ対策が挙げられ、中でもマネージドに提供されているものとして Route 53 Resolver DNS Firewall ( DNS ベース) および Network Firewall (L3 - L7パケットヘッダベース) 、 AWS Network Firewall Proxy (L3 - L7パケットヘッダベース) が存在します。 レイヤ 制御観点 AWS サービス 料金 特徴 DNS (名前解決) ドメイン 名 Route53 Resolver DNS Firewall 低( DNS クエリ) DNS クエリを制御。ローカルでの名前解決やIP直打ち、独自 DNS 使用の通信は防御不可。 L3–4 IP / Port SG / NACL 無料 ファイアウォール 。ネットワークセキュリティのベースとなるフィルタリング。 L3–7 IP / Port, TCP ヘッダ / HTTP ヘッダ / TLS ヘッダ Network Firewall , GatewayLoadBalancer + サードパーティ ソフト 高(AZ毎の常時稼働エンドポイント+処理量) 柔軟な制御が可能で、マネージドにも運用可能。 DNS + L3–7 IP / Port, HTTP ヘッダ / TLS ヘッダ AWS Network Firewall Proxy 調査中 マネージドな フォワ ードプロキシサービス。現状はパブリックプレビュー中で無料使用可能。 AWS のアウトバウンドセキュリティサービス Route53 Resolver DNS Firewall Route 53 Resolver は、 VPC のCIDRブロックにおける +2 の 予約済み IPアドレス で提供されるマネージドリゾルバ です。例えば、10.0.0.0/16の VPC では、10.0.0.2がRoute 53 Resolver の IP アドレスとして割り当てられます。この IPアドレス 自体は VPC 内で共通ですが、実際に DNS クエリを処理するリゾルバのエンドポイントはAZごとに配置されており、高可用性を考慮した構成となっています VPC 内のアプリケーションからこの Route 53 Resolver を経由して送信される アウトバウンド DNS クエリに対して VPC 単位でフィルタリング を行うサービスが、Route 53 Resolver Firewall となります。 ドメイン リストを基に ALLOW / BLOCK / ALERT を設定することで、 特定の ドメイン 群に対する通信を許可・拒否、またはアラートとして検知 することができます。 また、BLOCK の場合にはカスタム DNS レスポンスを設定することで、CNAME を用いて特定の ドメイン への名前解決に誘導することも可能です。 さらに、CNAME による変換後の ドメイン についても検証対象とするかを設定できるため、Allow List / Deny List のいずれにおいても柔軟な構成が可能です。 CNAME による変換後の ドメイン 検証 Deny List 構成では、CNAME 変換後の ドメイン までを検証対象とすることで、悪質な ドメイン への名前解決を防止できます。 Allow List 構成において CNAME 変換後の ドメイン まで全て許可対象とする場合、管理すべき ドメイン 数が増加し、運用負荷が高くなる傾向があります。そのため、運用方針によっては CNAME 変換後の検証を行わない設定とし、管理コストを抑える選択肢も考えられます。 Network Firewall AWS Network Firewall は、 AWS マネージドの ファイアウォール および IDS/IPS サービスであり、インバウンド / アウトバウンドに対して サブネット単位で設定したルール(ステートレスルール / ステートフルルール)に基づいて、パケットを ALLOW / DROP / ALERT に分類し制御 します。 ( 1つのNetwork Firewallを複数のVPCで共有できる機能 も公開されています) ステートレスルールは、全てのパケットを個別に評価して判定を行います。一方、ステートフルルール(Suricata互換)では、 TCP や TLS 、HTTP などの一連の通信(コネクション)を単位として制御することが可能です。 例えば、ステートレスルールでは TCP の 3 ウェイハンドシェイクを構成する各パケットをそれぞれ独立してフィルタリングしますが、ステートフルルールでは TCP の通信全体をひとまとまりとして扱い、コネクションの状態(established か否か)を追跡しながらフィルタリングを行います。 Network Firewall では、上図に示すようにルートテーブルの設定によって、 検査対象となる通信の往路・復路の両方を同一の Firewall Endpoint に通過させる ことで、ステートフルな 通信制 御を実現しています。( 非対称ルーティングの回避 ) これは、各 Firewall Endpoint が通信状態を個別に管理しており、 TCP や HTTP などのハンドシェイクにおいてリクエストとレスポンスの両方を確認することで、通信が正しく確立されたものであるかを判断する必要があるためと考えられます。 今回は比較的シンプルな構成を例に紹介しましたが、 Network Firewallのデプロイモデル には複数のパターンが存在します。 実際の設計においては、通信要件や可用性、運用負荷を考慮したうえで、適切なモデルを選択することが重要です。 Network Firewall と Gateway Load Balancer(GWLB) Network Firewall は、内部的に Gateway Load Balancer と連携して動作するマネージドサービスです。 Gateway Load Balancer は、GENEVE プロトコル を用いて サードパーティ 製のインスペクションサービスへ トラフィック をトンネリングする仕組みを提供するもので、インバウンド / アウトバウンド のセキュリティ対策に利用できます。 一方で、Network Firewall を利用することで、こうしたインスペクション機能そのものを AWS マネージドサービスとして構成できるため、構築・運用負荷を大きく下げることができます。 Network Firewall Proxy 2025年11月末にマネージドな フォワ ードプロキシサービスである AWS Network Firewall Proxy が発表されました。現在はパブリックプレビュー中のため、 オハイオ (us-west-2)リージョン限定で利用可能となっています。 厳密にはNetwork Firewall の機能の一部に当たりますが、役割の明確化のために分けて説明いたします。 AWS Network Firewall Proxy はいわゆるWebプロキシのように、 HTTP / HTTPS 通信専用のマネージドな フォワ ードプロキシ であり、Route 53 Resolver DNS Firewall や Network Firewall と同様にアウトバウンドセキュリティに適した AWS サービスとなります。 以下の構成( Architecture overview より引用)により使用可能であり、NAT ゲートウェイ と統合して動作するものとなっています。 本サービスでは、以下の図( Securing Egress Architectures with Network Firewall Proxy より引用)に示すように 名前解決前(Pre- DNS )、名前解決後の接続先サーバへのHTTPリクエスト(Pre-request)、HTTPレスポンス(Post-response)の3段階 で評価を行い、名前解決に使用する ドメイン 名からL3-L7(IP / Port, HTTP / TLS ヘッダ) までの情報を評価に使用可能となります。 詳細な挙動や設計上の注意点については、 こちらの記事 で紹介しておりますので、是非ご参照ください。 アウトバウンドセキュリティの設計方針 セキュリティ全体としては、セキュリティ効果と運用負荷のバランスを踏まえ、以下の 3 つの方針が重要となります。 多層防御を活用する 導入コストやリスクが低く、効果の高い対策から着手する 最初から完璧を目指さず、運用を通じて継続的に改善する アウトバウンドセキュリティにおける具体的な設計方針として、 AWS re:Inforce 2025 - Outbound network controls made easy で紹介されている考え方に基づき、 Deny List / Strict Allow List / Generous Allow List の 3 つの方針を紹介します。 Deny List (拒否リスト) 明示的に拒否する対象のみを定義する方式であり、以下のようなメリットがあります。 導入が容易 AWS ではマネージドな拒否リストを利用可能 拒否された通信はログとして記録されるため、通信内容の分析や調査が可能(なお、ALERT を挟んだ Allow List 構成においても、同様に情報を取得可能) 一方で、悪質な宛先は無数に存在するため、全てを網羅的に拒否リストへ登録することは現実的ではありません。 そのため、未知の通信先が許可されてしまう可能性が残り、 Allow List と比較するとリスク低減効果は限定的 である点がデメリットとなります。 2025年に公開された AWS Network Firewall with Active Threat Defense では、 AWS がグローバルに展開する独自の ハニーポット から収集した脅威情報を活用し、既知の悪意あるインフラに対する拒否ルールを動的に更新することが可能となっています。 これにより、従来の静的な Deny List が抱えていた運用負荷や追従性の課題を一定程度緩和できます。 また、 AWS Network Firewall の 地理的IPフィルタリング も、ルール管理を簡素化する観点から、同様に運用負荷の軽減に寄与する機能と言えます。 Strict Allow List (厳格な許可リスト) 明示的に許可する対象のみを定義する方式であり、以下のようなメリットがあります。 リスク低減効果が最も高い 通信先の追加・変更・削除時に、設計レビューや承認といった定められた運用手順を強制しやすい 一方、デメリットとしては以下の内容が挙げられます。 導入・運用コストが高い (アプリが依存する OSS 、 OSS 自体のアクセス先等、管理対象が多数) 運用コストの高さから、正当な通信の許可が抜けてしまい拒否するリスクがある 許可すべき通信が限定的なサービスにおいては非常に有効な手段となりますが、多様な外部サービスや API と連携する必要があるシステムに対しては導入コストも高く相性が悪い方式となります。 Generous Allow List (寛容な許可リスト) Allow List のリスク低減と Deny List の運用コストの低さを組み合わせた 、最も推奨される方針となります。 AWS Network Firewall においては、主に以下の方針で Deny List および Allow List を構成します。 Deny List AWS マネージドルール 既知の悪意のある宛先を拒否 高リスクな地理的 IP 高リスクな TLD 高リスクなポート IP 直指定による通信 ポートと プロトコル が一致しない通信 Generous Allow-List 信頼できる ドメイン リスト、以下例 *.gov (日本向けなら *.go.jp )、 *.edu 、us-*.amazonaws.com .<自社 ドメイン >、 .<信頼する他社 ドメイン > Generous Allow List では、初期段階では厳格に絞り込みすぎず、 比較的寛容な範囲で ドメイン リストを設定 します。 その上で、運用開始後に取得したログを基に継続的な評価と見直しを行い、Allow List / Deny List の精度を段階的に高めていくことが重要です。 このような運用を支援する手段として、 Reporting on network traffic in Network Firewall の機能を利用することで、 過去の通信ログから ドメイン 単位のアクセス状況を分析・可視化 し、ルール設計や改善に活用することができます。 まとめ アウトバウンド対策は DNS / L3–7(IP / Port / TCP / HTTP / TLS ヘッダ)/ ボディ のレイヤで整理すると理解しやすい。 AWS のマネージドな主要選択肢として、 Route 53 Resolver DNS Firewall / Network Firewall / Network Firewall Proxy が挙げられる。 次回以降の記事で詳細検証しますが、要件に応じた AWS サービスの主軸の使い分けは以下のイメージを持っています。 ドメイン フィルタ:Route 53 Resolver DNS Firewall + Network Firewall or Network Firewall Proxy UDP / TCP を含む評価:Network Firewall HTTP / HTTPS のみの評価:Network Firewall Proxy ただし、あくまでプロキシであるため、ベースのネットワークセキュリティの担保が必要。 Generous Allow List を基に、まずは低コストで効果の高い対策から導入し、ログを活用しながら継続的に改善していくことが現実的である。 おわりに 本記事では、 AWS におけるアウトバウンドセキュリティ全体の概要を整理しました。 今回は基礎的な内容を中心に取り上げましたが、今後は実際の設計・検証・運用を通じて、より実践的な理解を深めていきたいと思います。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @sakae.katsuto レビュー: @nagamatsu.yuji ( Shodo で執筆されました )
アバター
金融IT本部入社一年目の河岸歩希です。 この度、社内で実施されている 伴走型研修 に参加し、 AWS Certified Solutions Architect – Professional (以下SAP) を受験いたしました。本記事では、その学習過程で得た知見や経験を、僭越ながら受験体験記としてまとめさせていただきます。 これからSAP認定の取得を目指される方や、 AWS 業務未経験だけどSAPに興味をお持ちの方にとって、本記事が少しでも参考になれば幸いです。 前提 想定読者 著者情報 試験概要と伴走型研修について 試験概要(SAP-C02) 伴走型研修とは 伴走型研修の感想 結果と勉強時間 学習リソース AWS Solution Architect Professional (AWS-SAP-C02) 演習テスト Google AI Studio 問題を解いていて思ったこと 当日の立ち回り おわりに 前提 想定読者 これからSAPを受ける予定のある方 AWS 業務未経験だけど AWS の資格取得に興味がある方 資格勉強が好きな方 著者情報 学部卒の新卒1年目 大学では情報系の学部に所属 ( クラウド 技術には触れたことがないためほぼ未経験者) 会社に入ってから(4月) AWS を学び始める CLF(5月)とSAA(7月)をそれぞれ取得済み 業務では AWS を触っていない(触りたい) 試験概要と伴走型研修について 試験概要(SAP-C02) 試験の公式ガイドにも記載がありますが、この AWS Certified Solutions Architect – Professional認定は、 AWS を用いたソリューションの設計における2年以上の実務経験を持つ方を主な対象として設計されています。 私のような社会人1年目がこの資格を取得できたなら、それは 2年分の業務経験に相当する知識 や設計能力が身についた一つの証となり、自身のキャリアにおける大きな自信へと繋がるのではないかと考えております。 SAP認定の学習を始めるにあたり、皆様に最初におすすめしたいのは、まず公式の「試験ガイド」に必ず目を通し、それから学習計画を立てることです。 その理由は、試験範囲が他の認定と比較しても 非常に広範かつ多岐にわたる ためです。やみくもに学習を始めてしまうと、重要な分野に時間を割けなかったり、逆に試験の比重が低い部分に時間を使いすぎてしまったりする可能性があります。 以下は、その試験ガイドから引用した、出題範囲の主要な分野です。これらの分野に、それぞれどの程度の比重が置かれているかを把握することが、効率的な学習の第一歩となります。 本試験のコンテンツ分野と重み設定は、以下のとおりです。 コンテンツ分野 1: 複雑な組織に対応するソリューションの設計 (採点対象コンテンツの 26%) コンテンツ分野 2: 新しいソリューションのための設計 (採点対象コンテンツの 29%) コンテンツ分野 3: 既存のソリューションの継続的な改善 (採点対象コンテンツの 25%) コンテンツ分野 4: ワークロードの移行とモダナイゼーションの加速 (採点対象コンテンツの 20%) どの分野もまんべんなく出題されるため、偏りなく学習することが重要です。 試験時間と設問数、合格点に関しても、SAAより高く設定されています。 試験時間:180分 設問数:75 (うち10問が採点対象外) 合格点:750点 詳細は 試験ガイド を参照していただければと思います。 伴走型研修とは 当社では半期に一度、 AWS やAzureなどの資格取得に興味のある方を対象に伴走型研修が行われております。(要申し込み) 内容としては、外部講師との1on1(スキルや状況に応じた学習計画のアド バイス )、毎週30分の勉強会(試験問題の解説や情報共有)、専用Teamsでの相談や質疑応答などを通して、資格取得まで包括的なサポートを受けることができます。 怠惰な私にとってはピッタリの研修だと思い応募しました。 伴走型研修の感想 特によかったと思うのは、外部講師との1on1です。 9月からスタートして、11月末までの3か月間のうち、ほぼ隔週で1on1の時間を設けていただきました。 (スケジュールに関しても、柔軟に対応していただけました) この定期的な1on1を通じて、学習の進捗と次回の面談までの目標を設定するのですが、この目標設定が継続的な学習のモチベーション維持に大きく寄与したと思います。 また、勉強に際してのアド バイス もいただけたため、効率的に学習を進めることができました。 結果と勉強時間 結果↓ 無事SAPに合格することができました。 意外かもしれませんが、 試験で最も壁を感じたのは、SAPよりもアソシエイト(SAA)でした 。 もちろん、出題される問題の運にも左右されるため、一概には言えないことですが。 ご参考までに、私が各試験を初めて受けた際の 「難易度感」 を表すと、以下のようになります。 SAA >>> SAP >>>>> CLF = 基本情報技術者試験 こうして振り返ってみると、CLFからSAAへの難易度の上昇がいかに大きかったかを改めて感じます。 SAPの学習では、SAAで得た個々のサービスの知識を、より深く、実践的なシナリオに当てはめていく意識が重要だと感じました。 そのようなアプローチで学習を続けることが、合格への確かな一歩になるのではないかと思います。 学習リソース 主にUdemyとGoogleAIStudioを用いて学習を行いました。 AWS Solution Architect Professional ( AWS -SAP-C02) 演習テスト https://www.udemy.com/share/10bBe7/ 75問構成の模擬試験が5セット収録されているUdemyの問題集です。 この教材を選んだ理由は、問題の最終更新日が他の教材と比較して最も新しかったからです。 AWS のサービスは仕様変更が頻繁に行われるため、その最新の変更に対応して問題内容を随時更新してくれるコースを選ぶ ことが、非常に重要だと考えています。 難易度はかなり高めでした。 私自身、学習時間の都合で2周目には手が回らず、結果的に1周しかできませんでしたが、平均正答率は6割程度でした。 若干解説が薄い部分もありますが、この問題集だけで試験範囲は網羅できるので安心です。 Google AI Studio https://aistudio.google.com/prompts/new_chat もちろん、ChatGPTなど他のAIサービスでも全く問題ありません。 私が Google AI Studioをおすすめする理由は、無料で最新モデル(※2025年12月時点)が利用でき、一度に扱える情報量( トーク ン数)が多い点です。これにより、模擬試験の疑問点や不明点を余すことなく質問できるため、非常に効率的でした。 模擬試験を解き、解説を読んでも腹落ちしない部分があれば、すぐにこの Google AI Studioを開き、納得できるまで対話を繰り返す、というスタイルで理解を深めていきました。 問題を解いていて思ったこと 問題を解いていてふと思ったことがあります。それは 要件によって正解が変わる ということです。 当たり前のことのように聞こえるかもしれませんが、180分という時間の中で75問もの長文問題と向き合い続けると、どれだけ準備をしていても、どうしても集中力が途切れてしまう瞬間が訪れます。 そして、そういうときに限って、問題文の最も重要な 「制約条件」や「優先順位」 の部分をつい読み飛ばしてしまい、「選択肢のAもBも、技術的にはどちらも可能に見える…」という、判断に迷う状況に陥りがちです。 例えば、「モノリシックなWebアプリケーションを、サーバー管理から解放されるマネージドサービスへ移行したい」という典型的なシナリオを考えてみましょう。 この場合、頭の中にはいくつかの有力な候補が浮かびます。(実際の試験では、与えられた選択肢の中から判断します) 候補1: AWS Fargate ( Amazon ECS on Fargate) 候補2: AWS Lambda どちらもサーバーレスのマネージドサービスですが、どちらが「正解」になるかは、問題文に添えられた 「要件としては~」 といった一文にかかっています。 ケース1:要件が「コードの変更を最小限に抑え、迅速に移行したい」場合 この場合、正解は AWS Fargate になるでしょう。 モノリシックな既存アプリケーションをLambdaが要求するような小さな関数に分割するのは、大規模な再設計を伴うため、迅速な移行とは言えません。一方、コンテナ化してFargateに乗せる場合であれば、比較的少ないコード変更で移行を完了させることが可能です。 ケース2:要件が「長期的な運用コストを最適化し、スケーラビリティを最大化したい」場合 この場合、正解は AWS Lambda になる可能性が高まります。 アプリケーションを関数にまで分解する初期の移行コストは高くとも、完全な従量課金やイベント駆動のスケーラビリティといった、 クラウド ネイティブのメリットを最大限に活かせる長期的な視点を、この要件は重視しているからです。 2択までに絞れたが、何がまちがっているのかわからないようなケースに陥ったときは、一度冷静になって問題文を読み返し、 「コスト」 「スピード」 「運用負荷」 といったキーワードと、各選択肢を照らし合わせてみてください。 ユーザーが最も重視している要件に応えている方が正解です。 当日の立ち回り お手洗いと身分証は忘れずに。 私は身分証を忘れて家に取りに帰りました。 皆様にはぜひ落ち着いて持ち物を確認してから出発していただきたいです。 また、試験は長丁場になるので、合間に休憩を入れることをおすすめします。 おわりに 今回、伴走型研修に参加させていただいたことで、 AWS 未経験だった私でも、最後まで諦めることなく資格学習と向き合うことができたと実感しています。 一人では難しいモチベーションの維持に悩んでいる方にとって、このような学習環境は一度検討する価値が大いにあるかと思います。 SAPは確かに難関資格ですが、今回の経験を通じて、たとえ実務経験がなくても、コツコツと演習を重ねれば必ず合格に手が届く試験だと思いました。 本記事が、これから挑戦される一人でも多くの方の参考となり、合格への一助となれば幸いです。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @kawagishi.ibuki レビュー: @miyazawa.hibiki ( Shodo で執筆されました )
アバター
はい、こんにちはー!クロス イノベーション 本部 サイバーセキュリティテクノロ ジー センターの福山です。 2025年もReact2Shellなどを筆頭に多くの 脆弱性 情報が公表されました。 公開された 脆弱性 情報の傾向を1年単位で振り返ってみたいと思い、各種サイトの API を用いて収集し、分析してみました。 収集対象のデータ 2025年 分析結果 深刻度レベル別(2024/2025年) 深刻度レベル別(2025年月別) NVDステータス別 攻撃コード公開済み脆弱性の割合(PoC/MSF) 悪用確認済み脆弱性の割合(CISA KEV)/製品内訳(上位5製品) RCE可能な脆弱性の割合/悪用確認済みネットワーク機器内訳 CWE別件数(上位10位) CWE別リスク分布 CVE別/2025年最も危険な脆弱性4選 CVE別/今後悪用が広まる可能性が高い脆弱性4選 まとめ 収集対象のデータ 2025年に公表された約5万件のCVEを含む各種データを、 スクリプト で収集しました。 取得した情報の内容や抽出条件については以下を参照してください。 取得情報の一覧を表示 取得情報 概要 取得方法 参照元 CVE-ID 脆弱性 の一意の識別子。NIST(米国国立標準技術研究所)が運営するNVDに登録されたCVE番号を使用 2025年に公表されたCVE-IDを全件取得 NVD( https://nvd.nist.gov/vuln ) CVSS Base Score 基本評価基準に基づいて、 脆弱性 そのものの深刻度を数値(0.0〜10.0)で評価したスコア 対象CVE-IDにおける、CVSS v3.1(※1)のスコアを取得。取得優先度はNIST評価(Primary)>CNA(※2)評価(Secondary)で、両方未評価の場合は「N/A」とする。 NVD( https://nvd.nist.gov/vuln ) CVSS 深刻度レベル CVSS Base Scoreを基に分類した深刻度レベル(Low / Medium / High / Critical) 対象CVE-IDにおける、CVSS Base Scoreに基づく深刻度レベルを取得。取得優先度はNIST評価(Primary)>CNA評価(Secondary)とする。 NVD( https://nvd.nist.gov/vuln ) NVDステータス NVDにおける当該 脆弱性 の分析状況(例:Undergoing Analysis、Analyzed など) 対象CVE-IDにおける、NVDステータスを取得 NVD( https://nvd.nist.gov/vuln ) CWE-ID 共通 脆弱性 タイプ(Common Weakness Enumeration)の識別子 対象CVE-IDにおける、CWE-IDを取得。取得優先度はNIST評価(Primary)>CNA評価(Secondary)とする。 NVD( https://nvd.nist.gov/vuln ) RCE リモートから任意のコード実行が可能かどうか 筆者独自のフィルター。対象CVE-IDにおいて、Descriptionに大文字・小文字に関わらず「remote code execution」「execute arbitrary code」「arbitrary code execution」のいずれかのキーワードが含まれ、かつAttack Vector が「NETWORK」であるか判定 NVD( https://nvd.nist.gov/vuln ) CISA KEV 実際に悪用されているかどうか CISA のKEV(Known Exploited Vulnerabilities Catalog)への登録有無、ベンダー名、製品名を収集 CISA KEV( https://www.cisa.gov/known-exploited-vulnerabilities-catalog ) PoC PoC(概念実証コード)が公開されているかどうか 筆者独自のフィルター。PoC-in- GitHub のデータを基に判定(※ GitHub に存在するPoCのみ参照)。具体的にはZIPファイルを取得したうえで、全ファイルパスを走査し、CVE-IDが含まれる場合は抽出。 PoC-in- GitHub ( https://github.com/nomi-sec/PoC-in-GitHub ) MSF Metasploit Frameworkに当該 脆弱性 のモジュールが組み込まれているかどうか 筆者独自のフィルター。Metasploit Frameworkに対象CVE-IDを示すファイルが存在するかチェック。具体的にはZIPファイルを取得したうえでmodules/配下のファイルパスを走査し、CVE-ID形式の文字列があれば抽出。 Metasploit Framework( https://github.com/rapid7/metasploit-framework ) EPSS Score FIRSTが提供するEPSS(Exploit Prediction Scoring System)。今後30日以内に悪用される確率の予測値(0〜1) 対象CVE-IDにおける、EPSS Scoreを取得 FIRST EPSS( https://www.first.org/epss/ ) (※1)CVSS v3.1の上位バージョンであるCVSS v4.0が2023年11月に公開されたが、2025年に公表されたCVE全体の約25%しか評価されておらずNVDによる評価が進まないため、広く普及していない状況である。本記事ではCVSS v3.1をベースに解説を進める。 (※2)CNA=CVE Numbering Authorityの略称で、MITRE社から認定を受けて採番を行う組織のこと。 2025年 分析結果 ⚠️本集計は2026年1月中旬時点のデータに基づきます。 2025年に公表されたCVEの総数は 49,972 件でした。 2024年は40,704件であったため、 前年比で約23%増 、 9,000件近くの増加 となっています。 以降、分析結果の詳細です。 深刻度レベル別(2024/2025年) 2025年に公表された全CVEにおける、深刻度レベル別の件数です。 2024年と比較して 全体的に増加傾向 となっています。 2025年分は直近での集計ということもあり2024年分と比較すると差が出ていますが、それでも 未評価のCVEが前年比約4倍 と目立ちます。 NVDとしては、CNAによる評価の迅速化であったり、CWEの紐づけをLLMで自動化するなどの効率化(※)を図っているようですが、それでも対応が追いついていないことがわかります。 (※)参考: Vulnerability Root Cause Mapping with CWE 深刻度レベル別(2025年月別) 2025年に公表された全CVEにおける、月別/深刻度レベル別の件数です。 直近で公表されたCVEに未評価が多いのは当然ですが、12月は 未評価が1,500件 を超えています。 NVDではKEVに登録があるものを優先して評価(※)しているようですが、直近の 脆弱性 には対応が追いついていない状況が見て取れます。 今は未評価でも、分析が進むにつれてCriticalレベルと判明する 脆弱性 が出てくる可能性はありそうです。 (※)参考: The National Vulnerability Database (NVD) – Where It Is and Where It’s Going NVDステータス別 左が2024年、右が2025年に公表された全CVEにおける、NVD分析状況の割合を100%積み上げ棒グラフで示したものです。 (各ステータスの意味は下表を参照してください) 2024年分はAnalyzed(分析完了)と Modified(更新)を合わせて約80%の分析が完了しています。 一方、2025年分は分析完了の割合は前年並みを維持しているものの、 Awaiting Analysis(分析待ち)が約40% と大きく目立ちます。 ここについては今後分析が進んでいくと思われますが、現状では情報の確定に時間を要していることが伺えます。 また、 Rejected(却下)されたCVEが前年比で2倍以上 に増加している点も注目すべき変化です。 背景には、特定のライブラリに起因する 脆弱性 が製品ごとに乱立した際のCVEの統合や、昨今話題となっているAIツールを用いた大量かつ低品質な 脆弱性 報告の増加(※)といった、NVDを取り巻く環境の変化が影響していると考えられます。 (※)参考: NVD's HUGE Backlog: Vulnerability Crisis Explained ステータス名 意味 解説 Received 受理 NVD登録初期状態 Awaiting Analysis 分析待ち CVEの情報は公開されているが、NVDによるCVSSスコアやCWEの紐付けなどの詳細分析を待っている状態 Undergoing Analysis 分析進行中 CPE (情報システムを構成する、ハードウェア・ソフトウェアなどの識別子)やCWEの付与を行っている最中の状態 Analyzed 分析完了 NVDによる詳細分析がすべて完了した状態 Modified 修正 分析完了後に、新しい情報が追加・更新された状態 Deferred 延期 分析の優先順位が下げられた状態 Rejected 却下 脆弱性 ではないと判明した、あるいは既存のCVEと重複していた等の理由で、CVE-ID自体が取り消された状態 攻撃コード公開済み 脆弱性 の割合(PoC/MSF) 2025年に公表された全CVEにおける、PoCが公開されているCVEの割合は 2.47% で、 1,234件 ありました。 GitHub 以外やプライベー トリポジ トリのPoCも含めると、実際にはこの数値より膨らむものと推測されます。 また、上記のうちMetasploit Frameworkでモジュール化されているCVEの割合は 4.04% で、 50件 でした。 攻撃コードがモジュール化されている 脆弱性 は、高度な技術を持たない攻撃者でも簡単に悪用できる可能性があるため、特に注意が必要です。 悪用確認済み 脆弱性 の割合( CISA KEV)/製品内訳(上位5製品) 左の円グラフは2025年に公表された全CVEのうち、 CISA KEVに登録された割合を示しており、全体の 0.33% でした。 件数にするとわずか167件ですが、これらは理論上のリスクではなく、実際に悪用されていることが確認されているため、優先的に対応すべき重大な 脆弱性 として取り扱う必要があります。 右の円グラフは、その中でも悪用報告が多かった製品の内訳です。 Windows が17.37% と最多なのは、多くの企業で共通基盤として使われており、攻撃者にとって最も効率よく広範囲に影響を及ぼせるためと考えられます。 また、上位にはFortinetやIvantiといったネットワーク機器も並んでいます。 VPN などの境界を守る機器は、一度突破されると組織内への侵入や権限奪取に直結するため、OSと同等に執拗な標的となっていると考えられます。 RCE可能な 脆弱性 の割合/悪用確認済みネットワーク機器内訳 2025年に公表された全CVEにおいて、リモートコード実行(RCE)可能と判断されるものは、全体の 3.25% でした。 リモートから悪意のあるコードを実行された場合、情報窃取や攻撃の起点となるリスクが高く、特に注意が必要です。 さらにその中でも悪用確認済みの 脆弱性 に絞ると、 ネットワーク機器の 脆弱性 が3割 を超えています。 なお、これらの 脆弱性 のうち CWE-787(境界外書き込み) に分類される 脆弱性 は約4割と最も多く占めていることもわかりました。 これらの境界機器のRCEは、認証を介さず外部から直接システム権限を奪取できるものが多いため、昨今の ランサムウェア 攻撃において最も警戒すべき侵入経路とされています。 CWE別件数(上位10位) 2025年のCWE別件数の上位10タイプを、2025年(オレンジ)と2024年(グレー)で比較しました。 CWE-79( クロスサイトスクリプティング )が不動の1位となっています。 また、最も件数が増加したのは CWE-74(インジェクション)で1,169件増加 、一方で最も件数が減少したのは CWE-787(境界外書き込み)で843件減少 となりました。 CWE-74については、2025年に暫定策として取り入れられたGap Filling(CNAから提出されたCVSSおよびCWEデータを再検証せずに、一時的に受け入れるもの)による登録促進(※)の影響で、CNA側で付与したCWE-74が精査を待たずに大量に登録されたことが、増加の主な要因ではないかと推測しています。 (※)参考: The National Vulnerability Database (NVD) – Where It Is and Where It’s Going CWE別リスク分布 前項では件数に着目しましたが、ここでは筆者独自の観点で、リスクベースで分析してみます。 下図のバブルチャートでは、バブルの大きさが2025年に公表されたCVE総数を表し、X軸にRCE可能なCVE件数、Y軸に CISA KEVに登録されたCVE件数でプロットしたものです。 その中でもRCE可能な件数が50件以上、KEV登録数が10件以上のCWEに注目し、右上の領域にプロットされたCWEを下表にリストアップしました。 これらの 脆弱性 タイプは、 段階を踏まずインターネット越しから任意のコマンドを実行できる割合が高く、悪用実績も多数報告されている ため、特に注意が必要なカテゴリと言えるでしょう。 一方で最もサイズが大きいバブルはCWE-79ですが、単体でのRCEやKEV登録数は相対的に低めです。 CWE-ID 名称 解説 CWE-787 境界外書き込み メモリ操作時の不備により、本来の範囲を超えてデータを書き込んでしまう 脆弱性 のタイプ CWE-502 信頼できないデータのデシ リアラ イゼーション 外部から受け取ったデータをオブジェクトに復元する際、悪意あるコードを実行可能にする 脆弱性 のタイプ CWE-78 OSコマンドインジェクション OSコマンドの生成時に外部入力のバリデーションを不適切に行うことで、任意のコマンドを実行可能にする 脆弱性 のタイプ CVE別/2025年最も危険な 脆弱性 4選 2025年を通して最も危険な 脆弱性 について、個人の見解に基づいて選抜しました。 選抜の観点としては以下です。 CVSSスコア10(最大値)かつRCE、PoC、MSF、KEVをすべて網羅している 脆弱性 もし自社の環境や稼働しているシステムに下記に該当する 脆弱性 が存在する場合は、一刻も早く対処することをおすすめします。 CVE Published Vendor Product CVSS CWE RCE PoC MSF KEV EPSS CVE-2025-32433 2025-04-16 Erlang Erlang /OTP 10 CWE-306 TRUE TRUE TRUE TRUE 0.43921 CVE-2025-47812 2025-07-10 Wing FTP Server Wing FTP Server 10 CWE-158 TRUE TRUE TRUE TRUE 0.924 CVE-2025-55182 2025-12-03 Meta React Server Components 10 CWE-502 TRUE TRUE TRUE TRUE 0.5512 CVE-2025-37164 2025-12-16 Hewlett Packard Enterprise (HPE) OneView 10 CWE-94 TRUE TRUE TRUE TRUE 0.8131 CVE別/今後悪用が広まる可能性が高い 脆弱性 4選 最後に、2025年に公表されたCVEの中で、今後悪用が広まる可能性が高い 脆弱性 について、個人の見解に基づいて選抜しました。 選抜の観点としては以下です。 CWE別リスク分布で特定したCWEのうち、EPSSスコア0.9以上(直近30日間で悪用される可能性が非常に高い)かつPoC、MSFが公開されている 脆弱性 これらの 脆弱性 も早期に特定し、優先的に対処することをおすすめします。 CVE Published Vendor Product CVSS CWE RCE PoC MSF KEV EPSS CVE-2025-0282 2025-01 Ivanti Connect Secure, Policy Secure, and ZTA Gateways 9 CWE-787 TRUE TRUE TRUE TRUE 0.94105 CVE-2025-24016 2025-02 Wazuh Wazuh Server 9.9 CWE-502 TRUE TRUE TRUE TRUE 0.93801 CVE-2025-24813 2025-03 Apache Tomcat 9.8 CWE-502 TRUE TRUE TRUE TRUE 0.94183 CVE-2025-53770 2025-07 Microsoft SharePoint 9.8 CWE-502 FALSE TRUE TRUE TRUE 0.91188 まとめ 2025年はこれまで以上に 脆弱性 情報の量が増えたと感じていましたが、実際に分析することで日々のニュースを追うだけでは見えてこないその年の傾向や、NVDを取り巻く環境の変化や運用方針の転換が、データに影響を及ぼしていることが見えてきました。 CWEの総数で上位10位に入っていないものの、ひとたび悪用されればRCEに直結しやすく、かつ実際にKEVとして登録されるケースも目立ちました。 特に、ネットワーク機器の深刻な 脆弱性 として分類される傾向が見えたCWE-787と、冒頭でも触れたReact2Shellを含む CWE-502の動向には引き続き注視する必要がありそうです。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @fukuyama.kenta レビュー: @miyazawa.hibiki ( Shodo で執筆されました )
アバター
クロス イノベーション 本部、新卒1年目の大岡叡です。 今回は、1月29日にプレビュー公開された AWS Deployment SOPsを使ってみたので、その内容を報告します。 AWS Deployment SOPsを使ってみた結果、簡単なプロンプトを一度与えるだけで静的Webサイトをデプロイでき、CodePipelineのCI/CDについても簡単に構築することができました。この検証を通じて、非エンジニアがインフラ構築をして簡単なアプリケーションのデプロイを行う日も遠くないのではないかと思ったと同時に、 フルスタ ックアプリケーションも今回のように簡単なプロンプトを一度与えるだけでデプロイできる時代が来るのではないかと、期待が膨らみました。 AWS MCP Serverとは AWS Knowledge Tools AWS API Tools Agent SOP Tools AWS Deployment SOPsとは 検証の前提 検証1:Next.jsアプリケーションのデプロイ 検証内容 実装してくれたCDKのコード Deployment SOPsなしの場合 検証2:CodePipelineのセットアップ 検証内容 実装してくれたCDKのコード まとめ 参考文献 AWS MCP Serverとは AWS Deployment SOPsの説明をするうえで前提となる AWS MCP Server について説明します。 AWS MCP Serverは8つのツールを提供しており、 AWS Knowledge Tools 、 AWS API Tools 、 Agent SOP Tools の3つのカテゴリに分類されています。 AWS Knowledge Tools aws___search_documentation : API リファレンス・ベストプラクティス・サービスガイドを含む、すべての AWS ドキュメントを横断的に検索して関連情報を見つける。 aws___read_documentation : AWS ドキュメントページを取得し、 AIアシスタント が利用しやすいように Markdown 形式に変換する。 aws___recommend : AWS ドキュメントページに関連するトピックや、他のユーザーがよく一緒に閲覧しているコンテンツに基づいて、おすすめのドキュメントページを取得する。 aws___list_regions :リージョンID(例:ap-northeast-1)と名称(例:Asia Pacific (Tokyo))のペアの一覧を取得する。 aws___get_regional_availability :サービス、機能、 SDK API 、CloudFormationリソースについて、 AWS リージョンごとの利用可否情報を確認する。 AWS API Tools aws___call_aws : AWS API の呼び出しを実行する。 aws___suggest_aws_commands :関連する AWS API の説明と構文ヘルプを取得する。AIモデルの学習データに含まれていない可能性がある、新しくリリースされた API にも対応できる。 Agent SOP Tools aws___retrieve_agent_sop :Agent SOPsの検索または特定のSOPsの詳細情報の取得をする。 💡 Agent SOPsとは Agent SOPsとは、Claude Codeのような AIアシスタント が AWS 関連のタスクを行う際の標準作業手順書(Standard Operating Procedures)のことです。例えば、以下のようなSOPが用意されています。 SOP名 説明 create-secrets-using-best-practices ローテーション機能とKMSを備えたSecrets Managerによるシークレット作成 create-budget アラート機能付きの AWS Budgets作成 setup_cloudwatch_alarm_notifications SNS 経由でのCloudWatchアラーム通知セットアップ application-failure-troubleshooting CloudWatchログを分析してアプリケーション障害を デバッグ AWS Deployment SOPsとは AWS Deployment SOPsは、 aws___retrieve_agent_sop のツールが取得するデプロイ用のSOPのことです。 以下の4つのSOPがDeployment SOPsです。 SOP名 説明 deploy-webapp アプリケーションがDeployment SOPsでサポートされているものか確認し、適切なDeployment SOPを選択する。 deploy-frontend-app AWS CDKのコードを生成し、アプリケーションをデプロイした後、デプロイしたWebアプリのURLを提供する。 setup-pipeline AWS CodePipelineを使用してパイプラインを作成し、 GitHub に変更がプッシュされると自動的にアプリケーションの検証とデプロイを行う。 document-deployment デプロイに関するドキュメントを生成し、進捗を管理する。 Deployment SOPsがサポートしているアプリケーションタイプは以下のとおりです。 SPA:React, Vue, Angular, SvelteKit SSG:Next.js (static export), Nuxt, Gatsby , Hugo, Jekyll, Docusaurus, Astro, Eleventy Static websites そして、Deployment SOPsは2つの ユースケース 「 AWS へのアプリケーションのデプロイ」と「CodePipelineのセットアップ」をサポートしています。今回はこの2つの ユースケース を実際に試してみました。 検証の前提 今回はコーディングエージェントとしてClaude Codeを使用しました。 Skills等のカスタマイズ系のものは使用しないようにしました。 AWS CLI (認証情報設定済み)やGit CLI などの必要なツールはインストール済みです。 プロジェクトのルートで、以下のような .mcp.json を配置しました。 { " mcpServers ": { " aws-mcp ": { " command ": " uvx ", " args ": [ " mcp-proxy-for-aws@latest ", " https://aws-mcp.us-east-1.api.aws/mcp " ] } } } あらかじめ以下のようなTODOアプリケーションをNext.jsの静的エクスポートで実装しました。これはプロジェクトのルート ディレクト リにtodo-appという ディレクト リ名で配置しました。 検証1:Next.jsアプリケーションのデプロイ Claude Codeの挙動ベースで検証内容を報告します。結果としては、簡単なプロンプトを一度与えるだけでCloudFront + S3の構成を構築してデプロイを行い、CloudFrontのURLを共有してくれました。 検証内容 最初に「 AWS MCP の AWS Deployment SOPsを用いて、todo-appを AWS にデプロイしてください。」というプロンプトをClaude Codeに投げました(ちなみに、この検証で投げたプロンプトはこれだけです)。すると、 AWS MCP Serverの aws___retrieve_agent_sop を用いてdeploy-webappのSOPを取得してきて、アプリケーションがDeployment SOPsでサポートされているタイプか分析してくれました。 アプリケーションのファイルの読み込みが完了した後、 AWS MCP Serverの aws___retrieve_agent_sop を用いてdeploy-frontend-appのSOPを取得してきてくれました。そして、Next.jsの静的エクスポートはデプロイ可能ということで、デプロイ開始宣言をしてくれました。 続いて、 DEPLOYMENT_PLAN.md を生成してくれました。これ以降はこのプランに従って自律的に実装を進めてくれました。 --- sop_name: deploy-frontend-app, setup-pipeline repo_name: aws-deployment-sops-test app_name: TodoApp app_type: Frontend Application (Next.js Static Export) branch: main created: 2026-02-01 last_updated: 2026-02-01 --- # Deployment Plan: TodoApp Coding Agents should follow this Deployment Plan, and validate previous progress if picking up the Deployment in a new coding session. **IMPORTANT**: Update this plan after EACH step completes. Mark the step `[x]` and update `last_updated` timestamp. ## Phase 1: Gather Context and Configure - [x] Step 0: Inform User of Execution Flow - [x] Step 1: Create Deployment Plan - [] Step 2: Create Deploy Branch - [] Step 3: Detect Build Configuration - [] Step 4: Validate Prerequisites - [] Step 5: Revisit Deployment Plan ## Phase 2: Build CDK Infrastructure - [] Step 6: Initialize CDK Foundation - [] Step 7: Generate CDK Stack - [] Step 8: Create Deployment Script - [] Step 9: Validate CDK Synth ## Phase 3: Deploy and Validate - [] Step 10: Execute CDK Deployment - [] Step 11: Validate CloudFormation Stack ## Phase 4: Update Documentation - [] Step 12: Finalize Deployment Plan - [] Step 13: Update README.md そして、最終的にCloudFrontのURLを提供してくれました。このURLをクリックしたら実際にデプロイされたアプリケーションが正常に動作していることを確認できました。 実装してくれたCDKのコード 実際にClaude Codeが生成してくれたCDKのコードを以下に示します。CloudFront(Distribution, CloudFront Function)、S3(Webサイト用)、S3(CloudFront アクセスログ 用)、S3(S3 アクセスログ 用)などがセキュリティのベストプラクティスに基づいて実装されています。 import * as cdk from "aws-cdk-lib" ; import * as cloudfront from "aws-cdk-lib/aws-cloudfront" ; import * as s3deploy from "aws-cdk-lib/aws-s3-deployment" ; import { CloudFrontToS3 } from "@aws-solutions-constructs/aws-cloudfront-s3" ; import { Construct } from "constructs" ; export interface FrontendStackProps extends cdk.StackProps { environment : string ; buildOutputPath : string ; } export class FrontendStack extends cdk.Stack { public readonly distributionDomainName : string ; public readonly bucketName : string ; constructor ( scope : Construct , id : string , props : FrontendStackProps ) { super (scope, id, props); const { environment , buildOutputPath } = props; const isProd = environment === "prod" ; const removalPolicy = isProd ? cdk.RemovalPolicy.RETAIN : cdk.RemovalPolicy.DESTROY; // CSP via CloudFront Function - permissive policy for 3rd-party CDNs/APIs const cspFunction = new cloudfront.Function( this , "CspFunction" , { runtime : cloudfront.FunctionRuntime.JS_2_0, code : cloudfront.FunctionCode.fromInline( ` function handler(event) { var response = event.response; response.headers['content-security-policy'] = { value: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https: data: blob:; style-src 'self' 'unsafe-inline' https:; font-src 'self' https: data:; img-src 'self' https: data: blob:; connect-src 'self' https: wss:; frame-src 'self' https:; media-src 'self' https: blob:; worker-src 'self' https: blob:; object-src 'self' https:; manifest-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" }; return response; } ` ), comment : "Adds Content-Security-Policy header" , } ); // Extension rewrite function for Next.js static export (trailingSlash: false) // Rewrites /path to /path.html const extensionRewriteFunction = new cloudfront.Function( this , "ExtensionRewriteFunction" , { runtime : cloudfront.FunctionRuntime.JS_2_0, code : cloudfront.FunctionCode.fromInline( ` function handler(event) { var request = event.request; var uri = request.uri; if (!uri.includes('.') && uri !== '/') { request.uri = uri + '.html'; } return request; } ` ), comment : "Rewrites /path to /path.html for Next.js static export" , } ); const cloudfrontToS3 = new CloudFrontToS3( this , "CFToS3" , { bucketProps : { removalPolicy , autoDeleteObjects : !isProd, versioned : false , enforceSSL : true , } , loggingBucketProps : { removalPolicy , autoDeleteObjects : !isProd, lifecycleRules : [{ id : "DeleteOldLogs" , enabled : true , expiration : isProd ? cdk.Duration.days( 3650 ) : cdk.Duration.days( 7 ) }] , enforceSSL : true , } , cloudFrontLoggingBucketProps : { removalPolicy , autoDeleteObjects : !isProd, lifecycleRules : [{ id : "DeleteOldLogs" , enabled : true , expiration : isProd ? cdk.Duration.days( 3650 ) : cdk.Duration.days( 7 ) }] , enforceSSL : true , } , insertHttpSecurityHeaders : false , cloudFrontDistributionProps : { comment : ` ${ id } - ${ environment } ` , defaultRootObject : "index.html" , defaultBehavior : { responseHeadersPolicy : cloudfront.ResponseHeadersPolicy.SECURITY_HEADERS, functionAssociations : [ { function : cspFunction, eventType : cloudfront.FunctionEventType.VIEWER_RESPONSE, } , { function : extensionRewriteFunction, eventType : cloudfront.FunctionEventType.VIEWER_REQUEST, } , ] , } , priceClass : cloudfront.PriceClass.PRICE_CLASS_100, enableIpv6 : true , httpVersion : cloudfront.HttpVersion.HTTP2_AND_3, minimumProtocolVersion : cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021, } , } ); const websiteBucket = cloudfrontToS3.s3Bucket!; const distribution = cloudfrontToS3.cloudFrontWebDistribution; new s3deploy.BucketDeployment( this , "DeployWebsite" , { sources : [ s3deploy.Source.asset(buildOutputPath) ] , destinationBucket : websiteBucket, distribution , distributionPaths : [ "/*" ] , prune : true , memoryLimit : 512 , } ); this .distributionDomainName = distribution.distributionDomainName; this .bucketName = websiteBucket.bucketName; // Outputs new cdk.CfnOutput( this , "WebsiteURL" , { value : `https:// ${ distribution.distributionDomainName } ` , description : "CloudFront distribution URL" , exportName : ` ${ id } -WebsiteURL` , } ); new cdk.CfnOutput( this , "BucketName" , { value : websiteBucket.bucketName, description : "S3 bucket name" , exportName : ` ${ id } -BucketName` , } ); new cdk.CfnOutput( this , "DistributionId" , { value : distribution.distributionId, description : "CloudFront distribution ID" , exportName : ` ${ id } -DistributionId` , } ); new cdk.CfnOutput( this , "DistributionDomainName" , { value : distribution.distributionDomainName, description : "CloudFront domain name" , exportName : ` ${ id } -DistributionDomain` , } ); if (cloudfrontToS3.s3LoggingBucket) { new cdk.CfnOutput( this , "S3LogBucketName" , { value : cloudfrontToS3.s3LoggingBucket.bucketName, description : "Bucket for S3 access logs" , exportName : ` ${ id } -S3LogBucket` , } ); } if (cloudfrontToS3.cloudFrontLoggingBucket) { new cdk.CfnOutput( this , "CloudFrontLogBucketName" , { value : cloudfrontToS3.cloudFrontLoggingBucket.bucketName, description : "Bucket for CloudFront access logs" , exportName : ` ${ id } -CloudFrontLogBucket` , } ); } cdk.Tags.of( this ). add ( "Stack" , "Frontend" ); cdk.Tags.of( this ). add ( "aws-mcp:deploy:sop" , "deploy-frontend-app" ); cdk.Tags.of(cloudfrontToS3). add ( "Stack" , "Frontend" ); cdk.Tags.of(cloudfrontToS3). add ( "aws-mcp:deploy:sop" , "deploy-frontend-app" ); } } 以上より、「 AWS MCP の AWS Deployment SOPsを用いて、todo-appを AWS にデプロイしてください。」と一言お願いをすれば、それなりの構成でインフラを構築してくれて、アプリケーションをデプロイできることを確認できました。これなら非エンジニアの方でも簡単かつ安全にアプリケーションをデプロイできるのではないでしょうか。 Deployment SOPsなしの場合 Deployment SOPsを使用せずにClaude Codeに「todo-appを AWS にデプロイしてください。」とお願いしたらどうなるか試してみました。ここでは非エンジニアがデプロイする想定で、条件などを一切与えずにお願いしました。こちらも検証1と同様、Claudeの指示に基本従う方針で進めました。 最初にdeploy方法の選択を迫られました。Recommendedの AWS Amplifyを選択しました。 デプロイを進めてくれて、最終的にAmplifyのURLを表示してくれました。 しかし、このURLにアクセスしたところ、以下のようなWebページでした。 Amplifyへのデプロイ手順はCloudFront + S3構成と比べて複雑ではないものの、単に「デプロイして」と依頼するだけでは、現状のClaude Codeでは自律的にタスクを完遂することが難しいようです。 検証2:CodePipelineのセットアップ Claude Codeの挙動ベースに検証内容を報告します。CodePipelineが構築され、 GitHub にプッシュするだけで変更が自動的にデプロイされる仕組みが整いました。 検証内容 最初に「 AWS MCP の AWS Deployment SOPsを用いて、CodePipelineをセットアップしてください。」というプロンプトをClaude Codeに投げました。すると、 AWS MCP Serverの aws___retrieve_agent_sop を用いてsetup-pipelineのSOPを取得してきて、CodePipelineのセットアップを開始してくれました。 次に以下のような確認をされました。lintエラーの修正だけお願いしておきました。 検証1と同様にDEPLOYMENT_PLAN.mdを更新しながらCDKのコード実装など色々やってくれた後に、以下の認証をお願いされました。これは手順通り対応しました。 その後、パイプラインのデプロイなど色々やってくれてタスクが完了しました。 CodePipelineの動作を確認するために以下のようにh1の中身に変更を加えて、deploy-to- aws ブランチにプッシュしたところ、CodePipelineがトリガーされてビルド→デプロイが実行され、Webページに変更内容が反映されました。 実装してくれたCDKのコード 実際にClaude Codeが生成してくれたCDKのコードを以下に示します。CodePipeline、CodeBuildプロジェクト、S3 バケット ( アーティファクト 用)が構築されます。 import * as cdk from "aws-cdk-lib" ; import * as codebuild from "aws-cdk-lib/aws-codebuild" ; import * as codepipeline from "aws-cdk-lib/aws-codepipeline" ; import * as pipelines from "aws-cdk-lib/pipelines" ; import { Construct } from "constructs" ; import { FrontendStack } from "./frontend-stack" ; export interface PipelineStackProps extends cdk.StackProps { codeConnectionArn : string ; repositoryName : string ; branchName : string ; } export class PipelineStack extends cdk.Stack { public readonly pipeline : pipelines.CodePipeline ; constructor ( scope : Construct , id : string , props : PipelineStackProps ) { super (scope, id, props); const source = pipelines.CodePipelineSource. connection ( props.repositoryName, props.branchName, { connectionArn : props.codeConnectionArn, triggerOnPush : true , } ); const synth = new pipelines.ShellStep( "Synth" , { input : source, commands : [ // Install dependencies "(cd todo-app && npm install)" , "(cd infra && npm install)" , // Quality checks "(cd todo-app && npm run lint)" , // Secret scanning "npx -y @secretlint/quick-start '**/*'" , // Build frontend "(cd todo-app && npm run build)" , // CDK synth "cd infra" , "npx tsc" , `npx -y cdk synth --context codeConnectionArn=" ${ props.codeConnectionArn } " --context repositoryName=" ${ props.repositoryName } " --context branchName=" ${ props.branchName } "` , ] , primaryOutputDirectory : "infra/cdk.out" } ); this .pipeline = new pipelines.CodePipeline( this , "Pipeline" , { pipelineName : "TodoAppPipeline" , selfMutation : true , pipelineType : codepipeline.PipelineType.V2, synth , synthCodeBuildDefaults : { buildEnvironment : { computeType : codebuild.ComputeType.MEDIUM, buildImage : codebuild.LinuxBuildImage.STANDARD_7_0 } , partialBuildSpec : codebuild.BuildSpec.fromObject( { version : "0.2" , phases : { install : { "runtime-versions" : { nodejs : "latest" , } , } , } , } ), } , } ); // Deploy stage for production const deployStage = new cdk.Stage( this , "Deploy" , { env : { account : this .account, region : this . region } , } ); new FrontendStack(deployStage, "TodoAppFrontend-prod" , { stackName : "TodoAppFrontend-prod" , environment : "prod" , buildOutputPath : "../todo-app/out" , } ); this .pipeline.addStage(deployStage); // Build pipeline to enable property access this .pipeline.buildPipeline(); cdk.Tags.of( this ). add ( "Stack" , "Pipeline" ); cdk.Tags.of( this ). add ( "aws-mcp:deploy:sop" , "setup-pipeline" ); new cdk.CfnOutput( this , "PipelineName" , { value : "TodoAppPipeline" , description : "CodePipeline Name" , } ); new cdk.CfnOutput( this , "PipelineArn" , { value : this .pipeline.pipeline.pipelineArn, description : "CodePipeline ARN" , } ); } } まとめ AWS Deployment SOPsを用いて、静的エクスポートしたNext.jsアプリケーションをデプロイしたり、CodePipelineのセットアップを行いました。特に問題なくAIがすべての作業を完了してくれたことは驚きでした。近い将来、非エンジニアでもデプロイを行える日は遠くないと感じました。さらに、 フルスタ ックアプリケーションも簡単なプロンプトを一度与えるだけで構築できる時代が来るのではないかと、期待が膨らみました。 最後までお読みいただきありがとうございました! 参考文献 https://docs.aws.amazon.com/aws-mcp/latest/userguide/what-is-mcp-server.html https://docs.aws.amazon.com/aws-mcp/latest/userguide/getting-started-aws-mcp-server.html https://docs.aws.amazon.com/aws-mcp/latest/userguide/understanding-mcp-server-tools.html https://docs.aws.amazon.com/aws-mcp/latest/userguide/agent-sops.html https://docs.aws.amazon.com/aws-mcp/latest/userguide/agent-sops-deployment.html https://aws.amazon.com/jp/about-aws/whats-new/2025/01/aws-announces-deployment-agent-sops-in-aws-mcp-server-preview/ 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @ooka.toru レビュー: @miyazawa.hibiki ( Shodo で執筆されました )
アバター
はじめに 金融IT本部 2年目の坂江 克斗です。 今回はECR リポジトリ 作成テンプレート機能について、2025年12月に プッシュ時の自動リポジトリ作成 がサポート開始となりましたので、新機能を含めたテンプレート機能全体を検証し紹介します。 ベースの概念から含めて丁寧に解説できればと思います。 はじめに コンテナの概要 コンテナとは コンテナに関する基本用語 Amazon ECR の概要 リポジトリ作成テンプレートの概要 プルスルーキャッシュの概要 レプリケーションの概要 リポジトリ作成テンプレートの検証 前提 実装 検証 プルスルーキャッシュ レプリケーション プッシュ時の自動作成(Create on push) まとめ おわりに (補足)Dive deep into Amazon ECR より内部実装の紹介 内部アーキテクチャ 動作イメージ紹介 コンテナの概要 コンテナの概要に関して、ざっくりと説明します。 コンテナとは コンテナ技術とは、 Linux カーネル が持つ機能(namespaceやcgroup)を利用して、1つのOS上に論理的に隔離された実行環境を作成する技術 を指します。この隔離された実行環境そのものをコンテナと呼びます。 コンテナ内で動作しているのは 仮想マシン とは異なり、 ホストOS上で動作する通常のプロセス(プログラム単位) です。 プロセスが属する namespace によって、プロセス・ ファイルシステム ・ネットワーク・ Linux ユーザやグループの権限等を分離・隔離し、cgroupによってコンテナのプロセスが使用するリソース(CPU/メモリ)を制限することで、 1つのOS上に存在しながら、独立した OS のように振る舞う環境が実現されています。 コンテナに関するリソースの管理や操作を行う OSS を、 コンテナランタイム と呼びます。 このうち、 Linux カーネル の機能を直接利用してコンテナの作成・起動・削除といった処理を実行するものを 低レベルのコンテナランタイム (runc、gVisor、kata-containers)と呼びます。 一方で、イメージの管理やコンテナのライフサイクル管理を担い、コンテナの作成・削除といったリクエストを低レベルランタイムに委譲するものを 高レベルのコンテナランタイム (containerdやCRI-O)と呼びます。 コンテナに関する基本用語 コンテナを扱ううえで、以下の用語が頻出します。 用語 説明 イメージ(Image) コンテナを実行するために必要な ファイルシステム と メタデータ をまとめたもの。 レジストリ (Registry) コンテナイメージを保管・配布するためのサービス。 API による認証・認可やイメージ管理、保管・配布基盤などを含めた サービス全体 を指す。 リポジトリ (Repository) レジストリ 内に作成される イメージの論理的な格納単位 。 通常はサービスやアプリケーション単位で作成し使用される。 タグ(Tag) イメージに付与されるバージョンや識別子として使用。 現在、コンテナに関する形式は Open Container Initiative(OCI)規格 により定義されています。 OCI では、コンテナを実行するための 低レベルなコンテナランタイム に加え、 イメージ および レジストリ の仕様について標準化が行われています。 この標準化により、Docker に限らずさまざまなコンテナランタイムや レジストリ (Docker Hub、ECR、 GitHub )で互換性が保たれており、私たちは内部実装の違いを意識することなくコンテナイメージを利用することができます。 Amazon ECR の概要 Amazon Elastic Container Registry(ECR)は、 AWS が提供する マネージドなコンテナ レジストリ サービス です。 ECR には、 AWS アカウント毎に作成され、IAM によるアクセス制御が可能な プライベート レジストリ と、イメージの取得(pull)を誰でも行うことができる パブリック レジストリ の2 種類が存在します。 実際にアプリケーションをデプロイする際には、プライベート レジストリ を使用してアプリケーションイメージをセキュアに管理・運用するケースが一般的です。一方で、パブリック レジストリ はベースイメージの取得(踏み台サーバ用の Amazon Linux イメージなど)といった、公開されているイメージを参照する用途で利用されることが多いです。 ECR には、 レジストリ としての基本的機能だけでなく、 イメージの 脆弱性 スキャン 、 ライフサイクル管理 、 プルスルーキャッシュ 、 レプリケーション そして リポジトリ 作成テンプレート など、コンテナイメージ管理を効率化するためのさまざまな機能が用意されています。 本記事では、ECR が提供する機能のうち リポジトリ 作成テンプレート機能 に焦点を当て、その概要と活用方法について紹介します。 ECRに関してもっと詳しく知りたい方向けに、本記事の最後にECRの内部 アーキテクチャ や技術的な仕組みを解説した章を追加しましたので、ぜひご参照ください。 リポジトリ 作成テンプレートの概要 名前の通り、本機能は ユーザの代わりに Amazon ECR 自身が リポジトリ を作成する際に、その リポジトリ に適用される設定をテンプレートとして定義 できる機能です。 従来、ECR において リポジトリ が自動的に作成されるタイミングは、 プルスルーキャッシュ や レプリケーション 機能を有効化した場合に限られていました。 しかし、2025年12月のアップデートにより、 リポジトリ 作成テンプレート機能の拡張として push 時に リポジトリ を自動作成する機能 が追加されました。 具体的にテンプレートに設定する項目は、以下となります。 適用対象 Create on push 、 Pull through cache 、 Replication のいずれか、または複数を指定。 リポジトリ 名のプレフィクス。 プレフィクスを指定せず、 他の リポジトリ 作成テンプレートに該当しない全 リポジトリ を対象とすることも可能。 イメージタグ設定 Mutable (タグの上書き可能)もしくは Immutable (タグの上書き不可)を指定。 上記設定から除外するタグを指定するための、フィルター用プレフィクス。 暗号化設定 AES-256 もしくはKMSを指定。 KMSの場合は、 AWS マネージドキーもしくはカスタ マーマネ ージドキー(CMK)を指定。 リポジトリ 権限 リポジトリ 単位で適用するIAMリソースポリシーを設定。 ライフサイクルポリシー リポジトリ AWS タグ イメージタグではなく、ECR リポジトリ 自体に付与したい AWS リソース用のタグを指定。 リポジトリ 作成ロール 未定義の場合、 サービスにリンクされたロール を自動的に使用。 ただし、上記のロールは ecr:CreateRepository 権限しかないため、ケースごとに以下の権限を追加付与したカスタムロールの作成が必要。( AWSが提案するポリシー ) KMS キーを使用する場合:対象の KMS キーに対する権限 リソースタグを付与する場合: ecr:TagResource 権限 プルスルーキャッシュの概要 プルスルーキャッシュ は、特定の プライベートECR リポジトリ をキャッシュとして 、オリジンとなるパブリック レジストリ や別のプライベート ECRからECR自身がコンテナイメージを取得し、必要に応じてプライベート ECR リポジトリ を自動作成したうえで、そのイメージを保存する機能です。 pull元のECSタスク等は、 プライベートECRに対してイメージをpullするだけ で、シームレスに利用できるようになります。 この仕組みにより、インターネットへの直接通信ができない ECS タスクであっても、追加で NAT Gateway をデプロイするなどネットワーク要件に影響を与える対応を行うことなく、ECR を経由した 外部のパブリックイメージのセキュアな取得 が可能となります。 注意点としては、イメージをキャッシュしてから再度イメージの最新をアップストリーム レジストリ に確認する周期が、 少なくとも24時間に1回 となっています。そのため、頻繁に更新されるイメージの最新を同期して使用したい場合には少し不便かもしれません。 プルスルーキャッシュの設定において特に重要な項目は、以下の 2 点です。 アップストリーム レジストリ キャッシュとして利用するプライベートECRに対する、オリジンとして使用する(上位の) レジストリ を指します。 パブリックECR、プライベートECR、Docker Hub、Quay.io、 Kubernetes 、 GitHub Container Registry、 Microsoft Azure Container Registry、GitLab Container Registryのいずれかを指定。 リポジトリ 名プレフィクス ECRへのイメージpullリクエストが発生した際に、キャッシュ リポジトリ 作成をトリガーする リポジトリ 名のプレフィクス。 ECR自身が、他アカウントのプライベートECRにアクセス、もしくはキャッシュ用の リポジトリ を作成することから、IAM権限が必要となりますが、 サービスにリンクされたロール が自動で付与されるため、ユーザ側での管理は必要ありません。 しかし、 pull元のECSタスク等に対しては ecr:CreateRepository 、 ecr:BatchImportUpstreamImage 権限を明示的に付与する必要があります。 直感的には、ECR側に付与されたロールがこれらの権限を持つイメージですが、実際にはイメージを利用する側(ECS タスク等)のロール単位でプルスルーキャッシュの利用可否を制御できる設計となっています。 レプリケーション の概要 レプリケーション は、 特定のプライベートECR リポジトリ に pushされたイメージ を、別のリージョンや AWS アカウントに存在する プライベートECR リポジトリ へ自動的に複製(レプリケート) する機能です。 レプリケーション 先に リポジトリ が存在しない場合は、自動的に リポジトリ が作成されます。 レプリケーション の設定において特に重要な項目は、以下の 2 点です。 送信先 タイプ クロスアカウント、 クロスリー ジョンのいずれか、または両方を指定。 レプリケーション 先のアカウントIDやリージョンを指定。 リポジトリ 名プレフィクス レプリケーション の元となるECR リポジトリ 名のプレフィクス。 プレフィクスに一致するECR リポジトリ にイメージが追加された際に、 レプリケーション がトリガーされます。 プルスルーキャッシュ同様に、ECR自身に付与するIAM権限が必要となりますが、 サービスにリンクされたロール が自動で付与されるため、ユーザ側での管理は必要ありません。 ただし、クロスアカウントの場合には、 レプリケーション 元のアカウントが レプリケーション 先にイメージを複製するために、 レプリケーション先のレジストリ側に適切な権限設定 が必要となります。 リポジトリ 作成テンプレートの検証 前提 検証のためローカルで実装します。 以下の構成を使用します。 プライベートサブネットのみデプロイし、ECSタスクからは VPC エンドポイントを経由してECRにアクセス。 クライアントは AWS CLI およびdocker CLI を使用して、ECRにアクセス。 以下の機能を設定します。 プルスルーキャッシュ example-cache/ というプレフィクスのECR リポジトリ に対して、パブリックECR( public.ecr.aws )をアップストリーム レジストリ として設定。 ECSタスクはこの リポジトリ にpullをリクエストします。 レプリケーション example-replication/ というプレフィクスのECR リポジトリ に対して、 ap-northeast-1 から us-east-1 への レプリケーション を設定。 リポジトリ 作成テンプレート プッシュ時 example-create-on-push/ というプレフィクスのECR リポジトリ に対して、タグなしイメージを 14日後 に削除するライフサイクルを設定。 プルスルーキャッシュ example-cache/ というプレフィクスのECR リポジトリ に対して、タグなしイメージを 140日後 に削除するライフサイクルを設定。 レプリケーション example-replication/ というプレフィクスのECR リポジトリ に対して、タグなしイメージを 365日後 に削除するライフサイクルを設定。 ※ ただし、 レプリケーション 先の リポジトリ は us-east-1 リージョンに作成されるため、本テンプレートのみ us-east-1 リージョンに作成。 実装 基本設定および VPC などのネットワークに関する定義をします。 ECSから VPC エンドポイント経由でECRにアクセスするため、最低限3つの VPC エンドポイント(または到達可能なエンドポイント)が必要となります。構成図に示したとおり、その内訳は以下となります。 s3 イメージレイヤーの実体の保存場所。 dkr.ecr docker pull 等、OCI準拠の レジストリ アクセスを行うためのエンドポイント。 レジストリ のホスト名が <アカウントID>.dkr.ecr.<リージョン名>.amazonaws.com という形式であることからも分かるように、このエンドポイントを 名前解決可能な状態 で利用する必要があります。 api .ecr ECR用の AWS API を使用する際のエンドポイント。 terraform { required_version = "~> 1.14.0" required_providers { aws = { version = "6.28.0" source = "hashicorp/aws" } } } provider "aws" { region = local.regions.primary } locals { account_id = "<アカウントID>" regions = { primary = "ap-northeast-1" secondary = "us-east-1" } availability_zones = { primary = [ "$ { local.regions.primary } a" , "$ { local.regions.primary } c" , "$ { local.regions.primary } d" ] } } data "aws_caller_identity" "current" {} ########################################################################################### # VPC ########################################################################################### ## VPC resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" enable_dns_hostnames = true enable_dns_support = true tags = { Project = "example" } } ## Subnet resource "aws_subnet" "private_a" { vpc_id = aws_vpc.main.id cidr_block = "10.0.2.0/24" availability_zone = local.availability_zones.primary [ 0 ] map_public_ip_on_launch = false tags = { Project = "example" } } ## Route Table resource "aws_route_table" "private_a" { vpc_id = aws_vpc.main.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 } ## VPC Endopoint ### https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_endpoint.html resource "aws_vpc_endpoint" "s3" { vpc_id = aws_vpc.main.id service_name = "com.amazonaws.$ { local.regions.primary } .s3" vpc_endpoint_type = "Gateway" route_table_ids = [ aws_route_table.private_a.id, ] tags = { Project = "example" } } resource "aws_vpc_endpoint" "ecr_dkr" { vpc_id = aws_vpc.main.id service_name = "com.amazonaws.$ { local.regions.primary } .ecr.dkr" vpc_endpoint_type = "Interface" subnet_ids = [ aws_subnet.private_a.id ] security_group_ids = [ aws_security_group.vpce.id, ] private_dns_enabled = true tags = { Project = "example" } } resource "aws_vpc_endpoint" "ecr_api" { vpc_id = aws_vpc.main.id service_name = "com.amazonaws.$ { local.regions.primary } .ecr.api" vpc_endpoint_type = "Interface" subnet_ids = [ aws_subnet.private_a.id ] security_group_ids = [ aws_security_group.vpce.id, ] private_dns_enabled = true tags = { Project = "example" } } ## Security Group resource "aws_security_group" "vpce" { name = "vpce-sg" vpc_id = aws_vpc.main.id tags = { Project = "example" } } resource "aws_vpc_security_group_ingress_rule" "ingress_from_ecs" { security_group_id = aws_security_group.vpce.id from_port = 443 to_port = 443 ip_protocol = "tcp" referenced_security_group_id = aws_security_group.ecs.id } ECSの定義を行います。以下の項目に注意してください。 変数の定義は後程記載しますが、 public.ecr.aws/nginx/nginx:tag のイメージをプルスルーキャッシュにより取得。 イメージ URI としては <プライベートECRのホスト名>/<プルスルーキャッシュ条件のプレフィクス名>/nginx/nginx:tag を指定。 ECSの実行ロールに、プルスルーキャッシュ利用に必要な権限を付与。 ecr:CreateRepository 、 ecr:BatchImportUpstreamImage ECSからS3へ通信できるように、ECSのセキュリティグループのルールを設定。 ゲートウェイ エンドポイントを利用して AWS サービスへアクセスする場合、名前解決の結果は VPC のCIDRではなく、 AWS サービスごとに割り当てられた専用の IPアドレス となります。 そのため、セキュリティグループではS3用の IPアドレス レンジ( マネージドプレフィックスリスト )に対して明示的に通信を許可する必要があります。 ## Cluster resource "aws_ecs_cluster" "example" { name = "example-cluster" tags = { Project = "example" } } ## Service resource "aws_ecs_service" "example" { name = "example-service" cluster = aws_ecs_cluster.example.name launch_type = "FARGATE" desired_count = 1 task_definition = aws_ecs_task_definition.nginx.arn network_configuration { subnets = [ aws_subnet.private_a.id ] security_groups = [ aws_security_group.ecs.id ] } tags = { Project = "example" } } ## Task resource "aws_ecs_task_definition" "nginx" { family = "example-nginx" requires_compatibilities = [ "FARGATE" ] cpu = 256 memory = 512 network_mode = "awsvpc" execution_role_arn = aws_iam_role.ecs_exec.arn task_role_arn = aws_iam_role.ecs.arn container_definitions = <<EOL [ { "name": "nginx", "image": "$ { local.cached_image_uri } ", "memory": 512, "cpu": 256, "essential": true, "portMappings": [ { "containerPort": 80, "hostPort": 80, "protocol": "tcp" } ] } ] EOL tags = { Project = "example" } } ## Security Group resource "aws_security_group" "ecs" { name = "ecs-sg" vpc_id = aws_vpc.main.id tags = { Project = "example" } } resource "aws_vpc_security_group_egress_rule" "egress_to_vpce" { security_group_id = aws_security_group.ecs.id from_port = 443 to_port = 443 ip_protocol = "tcp" referenced_security_group_id = aws_security_group.vpce.id } data "aws_ec2_managed_prefix_list" "s3" { name = "com.amazonaws.$ { local.regions.primary } .s3" } resource "aws_vpc_security_group_egress_rule" "egress_to_s3_ip" { security_group_id = aws_security_group.ecs.id from_port = 443 to_port = 443 ip_protocol = "tcp" prefix_list_id = data.aws_ec2_managed_prefix_list.s3.id } ## IAM resource "aws_iam_role" "ecs" { name = "ecs-role" assume_role_policy = jsonencode ( { Version = "2012-10-17" Statement = [{ Effect = "Allow" Principal = { Service = "ecs-tasks.amazonaws.com" } Action = "sts:AssumeRole" }] } ) } resource "aws_iam_role" "ecs_exec" { name = "ecs-exec-role" assume_role_policy = jsonencode ( { Version = "2012-10-17" Statement = [{ Effect = "Allow" Principal = { Service = "ecs-tasks.amazonaws.com" } Action = "sts:AssumeRole" }] } ) } resource "aws_iam_policy" "ecr_cache" { name = "ecr-cache" policy = jsonencode ( { Version = "2012-10-17" Statement = [ { Sid = "AllowEcrPullThroughCache" Effect = "Allow" Action = [ "ecr:CreateRepository" , "ecr:BatchImportUpstreamImage" ] Resource = "*" } ] } ) } resource "aws_iam_role_policy_attachment" "ecr_cache" { role = aws_iam_role.ecs_exec.name policy_arn = aws_iam_policy.ecr_cache.arn } resource "aws_iam_role_policy_attachment" "ecs_exec" { role = aws_iam_role.ecs_exec.name policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" } プルスルーキャッシュおよび レプリケーション を定義します。 プルスルーキャッシュ用の リポジトリ 名プレフィクス: example-cache レプリケーション 用の リポジトリ 名プレフィクス: example-replication また、イメージ URI などについては、可能な限り共通で再利用可能な式として変数定義していますが、値としては以下に示すようなシンプルなものになっています。 ECSが本来取得したいアップストリーム レジストリ のイメージ URI origin_image_uri : public.ecr.aws/nginx/nginx:1.29-alpine アップストリーム レジストリ のホスト名 origin_registry_host : public.ecr.aws アップストリーム レジストリ のイメージパス origin_image_path : nginx/nginx:1.29-alpine プライベートECRのホスト名 cached_registry_host : <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com ECSタスクが参照するプライベートECR リポジトリ (キャッシュイメージの保存先) cached_image_uri : <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/example-cache/nginx/nginx:1.29-alpine locals { ## pull_through_cache origin_image_uri = "public.ecr.aws/nginx/nginx:1.29-alpine" origin_registry_host = split ( "/" , local.origin_image_uri) [ 0 ] origin_image_path = replace (local.origin_image_uri, "$ { local.origin_registry_host } /" , "" ) pull_through_cache_prefix = "example-cache" cached_registry_host = "$ { local.account_id } .dkr.ecr.$ { local.regions.primary } .amazonaws.com" cached_image_uri = "$ { local.cached_registry_host } /$ { local.pull_through_cache_prefix } /$ { local.origin_image_path } " ## Replication replication_filter_prefix = "example-replication" ## create on push create_on_push_prefix = "example-create-on-push" } ## pull through cache ## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_pull_through_cache_rule resource "aws_ecr_pull_through_cache_rule" "example" { ecr_repository_prefix = local.pull_through_cache_prefix upstream_registry_url = local.origin_registry_host } ## replication ## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_replication_configuration resource "aws_ecr_repository" "example_replication" { name = "example-replication/nginx/nginx" image_tag_mutability = "MUTABLE" tags = { Project = "example" } } resource "aws_ecr_replication_configuration" "example" { replication_configuration { rule { destination { region = local.regions.secondary registry_id = local.account_id } repository_filter { filter = local.replication_filter_prefix filter_type = "PREFIX_MATCH" } } } } リポジトリ 作成テンプレートを以下の設定で定義します。 プッシュ時 リポジトリ 名プレフィクス: example-create-on-push/ タグなしイメージを 14日後 に削除するライフサイクルを設定。 プルスルーキャッシュ リポジトリ 名プレフィクス: example-cache/ タグなしイメージを 140日後 に削除するライフサイクルを設定。 レプリケーション リポジトリ 名プレフィクス: example-replication/ タグなしイメージを 365日後 に削除するライフサイクルを設定。 ※ ただし、 レプリケーション 先の リポジトリ は us-east-1 リージョンに作成されるため、本テンプレートのみ us-east-1 リージョンに作成。 また、今回はリソースタグの付与を行うことから、 サービスにリンクされたロール の権限では不足となります。そのため、カスタムロールを作成し必要な権限を設定します。 ## repository creation template ## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_repository_creation_template resource "aws_iam_role" "ecr_creation_template" { name = "ecr-creation-template" assume_role_policy = jsonencode ( { Version = "2012-10-17" Statement = [{ Effect = "Allow" Principal = { Service = "ecr.amazonaws.com" } Action = "sts:AssumeRole" }] } ) } resource "aws_iam_policy" "ecr_creation_template" { name = "ecr-creation-template" policy = jsonencode ( { Version = "2012-10-17" Statement = [ { Sid = "AllowEcrCreateRepo" Effect = "Allow" Action = [ "ecr:CreateRepository" , "ecr:ReplicateImage" , "ecr:TagResource" ] Resource = "*" } ] } ) } resource "aws_iam_role_policy_attachment" "ecr_creation_template" { role = aws_iam_role.ecr_creation_template.name policy_arn = aws_iam_policy.ecr_creation_template.arn } resource "aws_ecr_repository_creation_template" "example_create_on_push" { prefix = local.create_on_push_prefix description = "An example-create-on-push template" image_tag_mutability = "MUTABLE" custom_role_arn = aws_iam_role.ecr_creation_template.arn applied_for = [ "CREATE_ON_PUSH" , ] encryption_configuration { encryption_type = "AES256" } lifecycle_policy = <<EOT { "rules": [ { "rulePriority": 1, "description": "Expire images older than 14 days", "selection": { "tagStatus": "untagged", "countType": "sinceImagePushed", "countUnit": "days", "countNumber": 14 }, "action": { "type": "expire" } } ] } EOT resource_tags = { Project = "example" } } resource "aws_ecr_repository_creation_template" "example_pull_through_cache" { prefix = local.pull_through_cache_prefix description = "An example-pull-through-cache template" image_tag_mutability = "MUTABLE" custom_role_arn = aws_iam_role.ecr_creation_template.arn applied_for = [ "PULL_THROUGH_CACHE" , ] encryption_configuration { encryption_type = "AES256" } lifecycle_policy = <<EOT { "rules": [ { "rulePriority": 1, "description": "Expire images older than 140 days", "selection": { "tagStatus": "untagged", "countType": "sinceImagePushed", "countUnit": "days", "countNumber": 140 }, "action": { "type": "expire" } } ] } EOT resource_tags = { Project = "example" } } resource "aws_ecr_repository_creation_template" "example_replication" { region = local.regions.secondary prefix = local.replication_filter_prefix description = "An example-replication template" image_tag_mutability = "MUTABLE" custom_role_arn = aws_iam_role.ecr_creation_template.arn applied_for = [ "REPLICATION" , ] encryption_configuration { encryption_type = "AES256" } lifecycle_policy = <<EOT { "rules": [ { "rulePriority": 1, "description": "Expire images older than 365 days", "selection": { "tagStatus": "untagged", "countType": "sinceImagePushed", "countUnit": "days", "countNumber": 365 }, "action": { "type": "expire" } } ] } EOT resource_tags = { Project = "example" } } 検証 terraform apply によりデプロイした後に、テンプレート毎の挙動を確認します。 プルスルーキャッシュ ECSタスクが参照するECR リポジトリ はTerraformで未定義でしたが、ECSが正常にデプロイされました。 プルスルーキャッシュにより、ECSタスクが参照する リポジトリ が自動作成され、中にはnginxのイメージが追加されていることが確認できました。 リポジトリ のライフサイクルに関しても、想定通りテンプレートに設定した値になっていることを確認できました。 レプリケーション ローカルから <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/example-replication/nginx/nginx リポジトリ にイメージをプッシュします。 katsu@maru MINGW64 ~/OneDrive/Documents/アプリ開発/AWS検証系 $ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com Login Succeeded katsu@maru MINGW64 ~/OneDrive/Documents/アプリ開発/AWS検証系 $ docker push <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/example-replication/nginx/nginx:1.29-test The push refers to repository [<アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/example-replication/nginx/nginx] 25f453064fd3: Pushed 0abf9e567266: Pushed e096540205d5: Pushed 567f84da6fbd: Pushed 1074353eec0d: Pushed 33f95a0f3229: Pushed da7c973d8b92: Pushed 085c5e5aaa8e: Pushed 1.29-test: digest: sha256:4e91fa982779ee440fa905e1727c977b6ea28c929f9d424bd8e030951298124e size: 1989 i Info → Not all multiplatform-content is present and only the available single-platform image was pushed sha256:648c87cd4f9a37a104d194db573d1afe8fa6b2632e618257c0f09e26a80d5110 -> sha256:4e91fa982779ee440fa905e1727c977b6ea28c929f9d424bd8e030951298124e プッシュした結果、 us-east-1 リージョンに想定通り リポジトリ が作成され、イメージもレプリケートされていることが確認できました。 また、他のECR リポジトリ は作成されておらず、指定のプレフィクスを持つ リポジトリ のイメージのみレプリケートされていました。 リポジトリ のライフサイクルに関しても、想定通りテンプレートに設定した値になっていることを確認できました。 プッシュ時の自動作成(Create on push) ローカルから、以下2パターンの存在しないECR リポジトリ にイメージをプッシュします。 テンプレート対象外: <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/example/nginx/nginx テンプレート対象内: <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/example-create-on-push/nginx/nginx katsu@maru MINGW64 ~/OneDrive/Documents/アプリ開発/AWS検証系 $ docker push <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/example/nginx/nginx:1.29-alpine The push refers to repository [<アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/example/nginx/nginx] 0abf9e567266: Unavailable 567f84da6fbd: Unavailable 1074353eec0d: Unavailable 25f453064fd3: Unavailable e096540205d5: Unavailable da7c973d8b92: Unavailable 33f95a0f3229: Unavailable 085c5e5aaa8e: Unavailable unexpected status from POST request to https://<アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/v2/example/nginx/nginx/blobs/uploads/?mount=sha256:25f453064fd3e8a9754b6e51b86c637e13203cbfc748fcf73f3c8b2d10816ae3&from=example/nginx: 404 Not Found katsu@maru MINGW64 ~/OneDrive/Documents/アプリ開発/AWS検証系 $ docker push <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/example-create-on-push/nginx/nginx:1.29-test The push refers to repository [<アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/example-create-on-push/nginx/nginx] 1074353eec0d: Pushed 085c5e5aaa8e: Pushed da7c973d8b92: Pushed 567f84da6fbd: Pushed 0abf9e567266: Pushed e096540205d5: Pushed 25f453064fd3: Pushed 33f95a0f3229: Pushed 1.29-test: digest: sha256:4e91fa982779ee440fa905e1727c977b6ea28c929f9d424bd8e030951298124e size: 1989 i Info → Not all multiplatform-content is present and only the available single-platform image was pushed sha256:648c87cd4f9a37a104d194db573d1afe8fa6b2632e618257c0f09e26a80d5110 -> sha256:4e91fa982779ee440fa905e1727c977b6ea28c929f9d424bd8e030951298124e プッシュした結果、想定通りテンプレートが対象とするプレフィクスを持つ リポジトリ のみpush処理が成功しました。 また、想定通り リポジトリ が作成されていることも確認できました。 リポジトリ のライフサイクルに関しても、想定通りテンプレートに設定した値になっていることを確認できました。 まとめ リポジトリ 作成テンプレートは、 特定の自動作成 ユースケース に限定 されるものの、共通のポリシーや設定を強制できるという点で、運用上のメリットが大きい機能です。 一方で、今回新たに追加された リポジトリ の自動作成機能については、指定したプレフィクスさえ満たせば リポジトリ が自動的に作成されてしまうため、入力ミスなどによって意図しない リポジトリ が作成される可能性があります。 また、自動作成された リポジトリ は、明示的にimportしない限りTerraformなどのIaC管理下には置かれないため、実リソースとコードの乖離が発生しやすい点も、運用面でのデメリットと感じました。 そのため、本機能を利用する際は、 リポジトリ 名の 命名規則 や運用ルールを事前に明確化したうえで導入することが重要だと考えます。 おわりに 本記事では、コンテナおよびECRの概要を整理したうえで、 リポジトリ 作成テンプレートの概要と挙動について、実際に検証を行いました。 特定の ユースケース に向けた機能であるため、私自身はしばらく使用する予定はありませんが、本記事がこれから利用を検討されている方の参考になれば幸いです。 個人的な感覚ではありますが、普段私たちが利用している IT技術 の多くは、数多くの抽象化や技術の積み重ねによって、 シンプルで使いやすいインターフェース として提供されています。そのため、実際に何が行われているのかを具体的にイメージしづらい部分も少なくありません。 こうした背景にある実装や仕組みを把握することは、日々の業務に直接活かされない場面も多いかもしれませんが、理解の実感が結果として技術に対する自信や判断力につながっていくと考えています。 そのため、今後の記事においても使用方法だけでなく、その背後にある技術や仕組みについても共有していきたいと考えています。 (補足)Dive deep into Amazon ECR より内部実装の紹介 Deep Dive 系の記事は AWS サービスごとに別途作成する予定ですが、今回はせっかくなので AWS re:Invent 2023 - Dive deep into Amazon ECR (CON405) を参考にしつつ、 Amazon ECR に限定してその内部仕様について少しだけ見ていきたいと思います。 内部 アーキテクチャ AWS の各サービスも実体としてはアプリケーションであり、 AWS API の仕様に基づいたリクエストを処理するFrontend Service (EC2)が存在します。その背後では、ストレージや名前解決などに関わる複数の AWS サービスが連携して動作しています。 ECRも例外ではなく、以下に示す構成となっております。背後では、S3やDynamoDBなどのストレージサービスも使用しています。 (これらの S3 や DynamoDB 自体も同様に、Frontend Service を起点とした構成になっていると考えられます) ただし、 レジストリ サービスとして利用されるという特性上、コンテナイメージの操作に関するアクセス時には AWS API ではなく、OCI 規格に基づく push / pull リクエストが使用されます。 そのため ECR では、これらの OCI リクエストを受け付けるための専用の Proxy Service がデプロイされている点が、他の AWS サービスとは少し異なる特徴となっています。 あくまで推測にはなりますが、Lambda や S3 などの AWS サービスも、 AWS API とは形式の異なるURLベースのアクセスが可能であることから、ECRと同様に中継層(Proxyに相当する コンポーネント )が存在している可能性があると考えています。 動作イメージ紹介 すべてを説明すると分量が多くなってしまうため、本記事では イメージの pull 時の挙動 に絞って説明します。 push / pull 時の仕様に関しても OCI で標準化されており、 The OpenContainers Distribution Spec で説明されています。 pullの仕様としては、以下2つのリクエストに分類されます。 <レジストリエンドポイント>/v2/<リポジトリ名>/manifests/<タグまたはイメージダイジェスト> への メタデータ (manifest)の取得 manifestは JSON 形式のデータであり、イメージレイヤごとのダイジェスト( ハッシュ値 )が記載されています。 <レジストリエンドポイント>/v2/<リポジトリ名>/blobs/<ダイジェスト> への blob(ダイジェストで管理される論理的なコンテンツ)の取得 ただし、2 つ目の blob の取得については、少なくとも Amazon ECR ではイメージレイヤそのものではなく 保存先の URL を返却しつつリダイレクトさせる方式 が採用されています。 blob はあくまでダイジェストで管理される論理的なコンテンツとして、 The OpenContainers Distribution Spec ではイメージレイヤなのか保存先URLなのかを明示的に指定しているわけではありません。 そのため、具体的に blob が返すものとして何が存在するかは 実装依存 となっており、 レジストリ の仕様やバージョンによって異なる可能性があります。 実際に ECR で pull が発生した際の、 AWS サービス内部での処理フローは以下に示すような流れとなります。 OCI 準拠の pull リクエストは Proxy Serviceによって中継され、 AWS API のエンドポイントとして動作する Frontend Service が、イメージ管理に関する主要な処理を担っています。 また、manifest やイメージレイヤの実体は S3 に保存されており、DynamoDBはあくまで参照情報のみ保存していることがわかります。 ここまでの内容から、ECR等の AWS サービスが抽象的なサービスではなく、複数の AWS サービスが連携して動作する 実体のあるアプリケーションとして運用されている という点がイメージできたのではないでしょうか。 個人的に、サービスにリンクされたロールなど、 AWS サービス自体が主体となって IAM 権限を基に操作するという感覚が、これまであまり掴めていませんでしたが、内部 アーキテクチャ におけるFrontend Service の実体や役割を知ったことで、その挙動がよりしっくりと理解できるようになりました。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @sakae.katsuto レビュー: @kobayashi.hinami ( Shodo で執筆されました )
アバター
こんにちは。スマート ソサエティ センター 行政デジタル部所属の宇佐美です。 今回は、Vertex AIのGeminiバッチ予測機能を使って、大量のテキストデータを一括で処理する方法をご紹介します。 バッチ予測とは Vertex AIのバッチ予測は、大量のプロンプトを非同期で一括処理できる機能です。リアルタイム推論と比較して以下のメリットがあります。 コスト効率 : リアルタイム推論と比較して安価な料金で利用可能。特にGemini 2.5 Flash はリアルタイム推論のおよそ半額で利用することが出来ます。 高 スループット : 1回のジョブで数十万件のリクエストを処理可能 シンプルなワークフロー : 個別リクエストの管理が不要 データの入出力先としてBigQueryまたはCloud Storageを使用できます。今回はBigQueryを使用します。 BigQueryについて BigQueryは Google Cloudが提供するフルマネージドのデータウェアハウスです。大規模データの分析に特化しており、 ペタバイト 規模のデータに対しても SQL で高速にクエリを実行できます。 BigQueryのデータ構造は以下の階層になっています。 デー タセット : テーブルやビューをまとめるコンテナ。リージョンを指定して作成する テーブル : 実際のデータを格納する場所。 スキーマ (カラム定義)を持つ 今回使用するデー タセット 今回は JaGovFaqs-22k という日本の官公庁のFAQデー タセット を使用します。各レコードには質問と回答が含まれており、今回はそれらを要約対象とします。 前提条件 Google Cloudプロジェクトが作成済みであること 必要な API が有効化されていること(Vertex AI API 、BigQuery API ) 適切なIAMロールが付与されていること 実装手順 1. 必要なライブラリのインストール pip install google-cloud-bigquery google-genai datasets 2. BigQuery スキーマ の作成 まず、BigQueryのデー タセット と、入力用データを管理するためのテーブル( faq_input )を定義します。 from google.cloud import bigquery PROJECT_ID = "your-project-id" LOCATION = "us-central1" client = bigquery.Client(project=PROJECT_ID) # データセット作成 dataset = bigquery.Dataset(f "{PROJECT_ID}.batch_demo" ) dataset.location = LOCATION client.create_dataset(dataset, exists_ok= True ) # テーブル作成 schema = [ bigquery.SchemaField( "Id" , "INTEGER" , mode= "REQUIRED" ), bigquery.SchemaField( "Question" , "STRING" ), bigquery.SchemaField( "Answer" , "STRING" ), bigquery.SchemaField( "copyright" , "STRING" ), bigquery.SchemaField( "url" , "STRING" ), bigquery.SchemaField( "request" , "STRING" , mode= "REQUIRED" ), ] table = bigquery.Table(f "{PROJECT_ID}.batch_demo.faq_input" , schema=schema) client.create_table(table, exists_ok= True ) ポイント : request カラムは、 GenerateContentRequest の構造に準拠する必要があります。 コードを実行すると、以下のとおりデー タセット とテーブルが作成されます。 3. データ登録 Hugging Faceからデー タセット を取得し、バッチ予測用のフォーマットに変換してBigQueryにロードします。 せっかくなので、 構造化出力 でレスポンスのフォーマットを指定してみましょう。 import json import pandas as pd from datasets import load_dataset response_schema = { "type" : "OBJECT" , "properties" : { "summary" : { "type" : "STRING" , "description" : "質問と回答の要約" }, }, } hf_dataset = load_dataset( "matsuxr/JaGovFaqs-22k" ) rows = [{ "Id" : i, "Question" : item[ "Question" ], "Answer" : item[ "Answer" ], "copyright" : item[ "copyright" ], "url" : item[ "url" ], "request" : json.dumps({ "contents" : [{ "role" : "user" , "parts" : [{ "text" : f "以下の質問と回答を140文字以内で要約してください。 \n\n ## 質問 \n {item['Question']} \n\n ## 回答 \n {item['Answer']}" }]}], "generationConfig" : { "responseMimeType" : "application/json" , "responseSchema" : response_schema}, }, ensure_ascii= False ) } for i, item in enumerate (hf_dataset[ "train" ])] df = pd.DataFrame(rows) job = client.load_table_from_dataframe(df, f "{PROJECT_ID}.batch_demo.faq_input" ) job.result() 余談ですが、日本語のような非ASCIIコードの文字は出力時にエスケープされてしまうため、 ensure_ascii=False でエスケープしないように設定しています。 4. バッチ予測タスクの作成 次に、バッチ予測ジョブを作成して実行します。 モデルは速度と品質、利用料金のバランスに優れているGemini 2.5 Flash を利用します。 出力先には別のテーブル(faq_output)を指定します。 from google import genai from google.genai.types import CreateBatchJobConfig genai_client = genai.Client( vertexai= True , project=PROJECT_ID, location=LOCATION ) job = genai_client.batches.create( model= "gemini-2.5-flash" , src=f "bq://{PROJECT_ID}.batch_demo.faq_input" , config=CreateBatchJobConfig(dest=f "bq://{PROJECT_ID}.batch_demo.faq_output" ), ) 5. 結果の確認 Vertex AI → バッチ推論から結果を確認してみましょう。正常に終了していれば、ステータスが「完了」になっています。 22794件のデータに対して15分弱で処理が完了しています。 先ほど作成したfaq_outputテーブルも確認してみます。 response.candidates[0].content.parts[0].text の中身が実際に出力された回答です。 指定されたフォーマット通りに出力されています。 { " text ":" { \n \" summary \" : \" 会計検査院は、国や地方公共団体の公共工事に対し、会計経理面だけでなく設計・積算・施工まで実地で検査します。現場では資料確認や担当者への聴取に加え、非破壊検査装置でコンクリート強度や鉄筋を確認し、必要に応じて破壊検査も実施。不適切な事態の是正を図っています。 \"\n } " } まとめ ここまでの処理をまとめて一つのサンプルコードにします。 import json import pandas as pd from datasets import load_dataset from google import genai from google.cloud import bigquery from google.genai.types import CreateBatchJobConfig, HttpOptions PROJECT_ID = "your-project-id" LOCATION = "us-central1" # BigQueryセットアップ client = bigquery.Client(project=PROJECT_ID) # データセット作成 dataset = bigquery.Dataset(f "{PROJECT_ID}.batch_demo" ) dataset.location = LOCATION client.create_dataset(dataset, exists_ok= True ) # テーブル作成 schema = [ bigquery.SchemaField( "Id" , "INTEGER" , mode= "REQUIRED" ), bigquery.SchemaField( "Question" , "STRING" ), bigquery.SchemaField( "Answer" , "STRING" ), bigquery.SchemaField( "copyright" , "STRING" ), bigquery.SchemaField( "url" , "STRING" ), bigquery.SchemaField( "request" , "STRING" , mode= "REQUIRED" ), ] table = bigquery.Table(f "{PROJECT_ID}.batch_demo.faq_input" , schema=schema) client.create_table(table, exists_ok= True ) # データ準備 response_schema = { "type" : "OBJECT" , "properties" : { "summary" : { "type" : "STRING" , "description" : "質問と回答の要約" }, }, } hf_dataset = load_dataset( "matsuxr/JaGovFaqs-22k" ) rows = [{ "Id" : i, "Question" : item[ "Question" ], "Answer" : item[ "Answer" ], "copyright" : item[ "copyright" ], "url" : item[ "url" ], "request" : json.dumps({ "contents" : [{ "role" : "user" , "parts" : [{ "text" : f "以下の質問と回答を140文字以内で要約してください。 \n\n ## 質問 \n {item['Question']} \n\n ## 回答 \n {item['Answer']}" }]}], "generationConfig" : { "responseMimeType" : "application/json" , "responseSchema" : response_schema}, }, ensure_ascii= False ) } for i, item in enumerate (hf_dataset[ "train" ])] df = pd.DataFrame(rows) job = client.load_table_from_dataframe(df, f "{PROJECT_ID}.batch_demo.faq_input" ) job.result() # バッチ予測実行 genai_client = genai.Client( vertexai= True , project=PROJECT_ID, location=LOCATION, http_options=HttpOptions(api_version= "v1" ) ) job = genai_client.batches.create( model= "gemini-2.5-flash" , src=f "bq://{PROJECT_ID}.batch_demo.faq_input" , config=CreateBatchJobConfig(dest=f "bq://{PROJECT_ID}.batch_demo.faq_output" ), ) 注意点 リージョンの制約 バッチ予測ジョブとBigQueryデー タセット は同じリージョンに配置する必要があります。マルチリージョンデー タセット (US、 EU など)はサポートされていないため、 us-central1 のような特定のリージョンを指定してください。 最後に Vertex AIのGeminiバッチ予測を使用することで、大量のデータを効率的に処理できます。 データ分析、コンテンツ生成、文書分類など、即時性を必要としない大規模なLLM処理タスクには、バッチ予測の活用をぜひご検討ください。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @usami.tsubasa レビュー: @miyazawa.hibiki ( Shodo で執筆されました )
アバター
1.自己紹介 2.はじめに 3.VLAとは? 元となるChatGPTなどのAI VLAはアクションを出力 4.今回の検証の狙い 5. 検証環境 環境: ハードウェア: 6.検証内容:SmolVLAによる複数タスクの実行 SmolVLAとは 目指したこと 設定したタスク 7.検証結果 できたこと 検証から見えた課題 8.まとめ 1.自己紹介 HS本部Open Innovationラボ(通称イノラボ)の奥野です。 もともとは製造業で精密機器や家電のメカ設計・機能/制御設計・企画などに携わっていました。 2018年に 電通 総研に入社し、現在は様々な先端技術のR&Dに取り組むイノラボで、ロボット等の研究開発に取り組んでいます。 2.はじめに 最近ニュースで ヒューマノイド 、フィジカルAIといった言葉を目にすることが増えました。年明けに開催されたCES2026でも Hyundai Motor Groupのグループ企業であるBoston Dynamicsが研究開発を続けていた Atlas の商用化に向けた戦略が発表され盛り上がっていました。 イノラボでは長くロボットに関する研究開発に取り組んでおり、特に 最近はフィジカルAIの技術検証を進めております。 今回、その取り組みとして、フィジカルAIの根幹技術であるVLA( Vision Language Action)モデルでアームロボットを動かす検証を行った内容を執筆します。 細かい実行コードなどは記載せず、本記事ではVLAを試してみて何ができるのか・どんな課題がありそうかといったことをお伝えしたいと思います。 なお今回の検証では Hugging FaceのLeRobotとSO-ARM101を組み合わせた環境 で、VLAモデルの事後学習からロボット制御までを行っています。 3.VLAとは? 元となるChatGPTなどのAI 多くの人が使うようになったChatGPTはLLM(Large Language Model)でテキスト入力からテキストを出力したり、 Vision を組み込んだVLM( Vision -Language Model)で画像内容を理解し説明するなど視覚と言語を統合したモデルが組み込まれています。 VLAはアクションを出力 VLAは画像とテキストの入力から、Action(ロボットの制御)を出力するモデルとなります。 LLMやVLMで発展してきたTransformerやDiffusion modelといった技術が拡張され、ロボット制御である アクションを出力するよう発展したものがVLA です。 これまでシステムに閉じていたAIが、現実世界に直接関与できるようになったという点で インパク トのある技術進化だと言えます。 以下は Google が2023年に発表し注目を集めたVLAモデル RT-2 から引用した図です。 モデル内部に視覚と言語を統合したVLMを内包し、その推論結果から直接ロボットを制御するActionを生成することで汎化性を実現しています。 4.今回の検証の狙い 今回の検証目的は大きく3つです。本記事では狙い1と2についての結果を述べます。 狙い3についてはSmolVLAと Physical Intelligenceのπ0.5 を比較しましたので、別の記事で改めて書きたいと思います。 狙い1 : VLAの実行環境を構築して理解する 狙い2 : VLAでアームロボットを動かして精度感や課題を確認する 狙い3 : モデルによる差異を確認する 5. 検証環境 環境: マシンスペック Windows 11 Pro AMD Ryzen 9 9950X3D NVIDIA RTX5090 ソフトウェア : Lerobot v0.41 最初はv0.33の環境で検証していましたが、途中でv0.41がリリースされ環境を変更しました。 このバージョンアップでLerobot Datasetがv3.0となり、 Dataset Tools が使えるようになりました。 ただしv0.33で記録済みのデー タセット をv0.41環境(Lerobot Dataset v3.0)で使うためには、データ変換が必要となる点に注意です。 ハードウェア: ロボット : SO-ARM101 カメラ : USBカメラ3台(空間に2台、ロボットリストに1台) 以下は構築した実環境の様子です。 以下は3つの各カメラの様子です。 6.検証内容:SmolVLAによる複数タスクの実行 SmolVLAとは 今回の検証で使用した SmolVLA はHugging Faceが開発した軽量なVLAモデルです。他のVLAがパラメータ数Bなどと巨大であるのに対して、 SmolVLAはパラメータ数が450Mと小型化され、軽量な環境でも扱いやすいモデル です。 今回の環境でも問題なくファインチューニングから実行までできました。 目指したこと 今回の検証の前に ACT(Action Chunking with Transformer) による模倣学習でアームロボットにタスクを実行させる検証を実施していました。 ACTでも単独タスクであればある程度できるようになりますが、複数のタスクを連続して行うような複雑なものになると上手く学習させることができない結果でした。そこで今回はもう少し難しいことを目指してトライしました。 単独タスクが高い精度で実行できる 1つのモデルで複数の単独タスクが実行できる 複数の単独タスクを連続実行できる 設定したタスク 以下4つのアイテムをそれぞれ黄色のカゴに入れる、ピック&プレースのタスクを4種類設定しました。 それぞれの初期位置は固定として、目印をテーブル上につけた環境としました。 英文がSmolVLAの入力指示文です。 紫色のボール:Pick up only purple ball and put in yellow basket 黄色のボール:Pick up only yellow ball and put in yellow basket 赤色のキューブ:Pick up only red cube and put in yellow basket 青色のキューブ:Pick up only blue cube and put it in the basket 7.検証結果 できたこと 以下は今回の検証でチューニングしたSmolVLAモデルで、4つのタスクを連続実行している様子です。当初目指していた3つの目標は概ね達成することができました。 この動画では 4つのタスクを連続実行 していますが、 1つのモデルでそれぞれの単独タスクのみを実行することも実現 できています。 またそれぞれの単独タスクは 8~9割と高い成功率でタスクを実行することができました 。 検証から見えた課題 データ取得の大変さ 最終的な今回のモデルでは合計550回、1時間以上の学習データでファインチューニングしています。データ取得は人間による繰り返し作業となるため、 精神的にも肉体的にも非常にハード 。 SmolVLAにおいてはタスクを成功させるためには起きうる 条件全ての学習データが必要な傾向 で、条件に対する汎化性を持たせようとすると指数的に学習コストが高まります。例えば初期状態が常に4つ全アイテムがある状態だけのデータで学習すると、1つでもアイテムがない状態で実行した場合にタスクは失敗(動かないなど)となります。 モデル学習時間・コスト 軽量なSmolVLAでも今回の環境では1つのモデルの学習に10時間以上かかりました。今回は学習データの変化に対する結果を試行錯誤しながら検証を進めたため、この 学習時間が大きな ボトルネック でした。 一方で クラウド 環境でH100を使った場合6時間程度まで削減できることが確認できましたが、時間削減のために ハイスペックな GPU を使うと相応の利用コスト がかかります。やはりフィジカルAIには潤沢な GPU インフラが必要で、それだけコストもかかることが分かりました。 モデルの限界感 位置変動への汎化性は低いと感じました。学習データを増やすことである程度精度を高められそうではありますが、かなりのデータ量が必要になると想定されます。 紫と黄色のボール位置を交換するといった条件を混同しそうな変化に対しては、それぞれの条件データをどれだけ入れても成功できませんでした。 AIではそのままロボットを制御してしまうため、アーム先端がずっと振動する、急激な軌道変動が頻繁に起きる、先端が机に激突するといったことが多発しました。結果として筆者の環境では2ヶ月程度でモータ故障が発生しています。ロボットの最終的な制御部分にはAIをそのまま適応させるのではなく、ロボット制御を考慮したフィルタリングなどの工夫が必要となりそうです。 8.まとめ 検証を通して軽量なSmolVLAでも 一定のタスク実行精度や複数タスクの適応性があることを確認 できました。一方で多くの課題や限界感も見えた結果となりました。 VLAはフィジカルAIの重要な技術で急速に発展しています。日々新しいロボット・モデルが発表されており、期待が高まっていますが、それぞれ できることもあれば出来ないこともあり、落ち着いて見極めることが重要 と考えます。 実業務で使っていくためには解決すべき課題や、ソフト・ハード両面でシステム的な落とし所を設計する必要がありそうです。 しかし今回見えた課題については、様々な解決策の研究が進んでいたり米中を中心に巨額な投資がされており、ChatGPTの精度が急激に高まったようにフィジカルAIも周辺技術含め急激に進化する日が近いかもしれません。 新しい動向をキャッチしながら、引き続き検証を続けていきたいと思います。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @okuno_takahiro レビュー: @azeta.takuya ( Shodo で執筆されました )
アバター
検証環境 題材としたアプリ 主な機能 技術スタック SDDとは 1. cc-sddでSDDを試す 良かった点 課題に感じた点 2. spec-workflow-mcpを試してみる 実際に使ってみた流れ 良かった点 課題に感じた点 3. SDDで実装品質をどこまで引き上げられるか 3.1 バックエンドのリファクタリング Steeringの改善 生成されたタスク リファクタリングの結果 3.2 UI/UX課題の改善を要件定義からAIに任せる 発見した課題 AIに要件定義から任せる 改善結果 おわりに こんにちは、クロス イノベーション 本部の 北涼 太です。 最近、コーディングエージェントと協力して開発する手法が主流になりつつあると思います。 ただ、ざっくりした指示だと思ったような実装にならないことも多いですよね。 そんな中で気になっていたのが、SDD(Spec-Driven Development)という開発スタイルです。 「仕様を先に固めてから実装する」アプローチで、AIとの協業と相性が良いとされています。 今回はこのSDDを使って、実際にWebアプリを開発してみました。 今回の検証で分かったのは、 SDDは強力だが、仕様レビューの負荷が想像以上に高い ということでした。 そこで、レビュー体験を改善するSDDツールへの切り替えも試しています。 この記事では、以下の3点についてお伝えします。 CLI ベースのSDDツール(cc-sdd)を試して感じた課題 GUI でレビューできるツール(spec-workflow- mcp )への切り替え SDDで実装品質をどこまで引き上げられるか 検証環境 項目 内容 SDDツール cc-sdd → spec-workflow- mcp AIエージェント Codex(gpt-5.2-codex) 題材としたアプリ 今回は「 RSS ニュースをユーザーの趣味嗜好、フィードバックに合わせてスコアリングするWebアプリ」を題材にしました。 アプリ自体が目的ではないので、シンプル(かつギリギリ役に立ちそう)なアプリとしています。 主な機能 RSS URLとユーザーの趣味嗜好を入力 記事ごとにGood/Bad評価をつけられる 各ニュース記事についてAIエージェントに質問可能 ニュース取得ボタンで収集とスコアリングを実行 成果物のイメージは以下です。 ・メイン画面 ・AIエージェントとのチャット画面 技術スタック Frontend : TypeScript + React / Vite + TailwindCSS Backend : Python + FastAPI Infra : AWS CDK + API Gateway + Lambda SDDとは 本題に入る前に、SDDについて簡単に説明します。 SDDとは、要求・設計・タスクを先に固めてから実装に進むことで、曖昧さを減らす開発スタイルです。 今回は、以下のような手順を採用して開発を進めました。 フェーズ 内容 Steering プロダクト/技術/構成の前提をそろえる(product.md / tech.md / structure.md) Requirements ユーザーストーリーと受け入れ基準を定義する Design UI・ API ・データモデルなどの設計を詰める Tasks 実装タスクに分解し、順序と依存を整理する Implementation タスクを順番に実装する(必要に応じて設計へ戻る) コーディングエージェントに開発を任せられる粒度まで仕様を先に固める分、 レビュー対象となるドキュメントの量が多くなりやすい のが特徴です。 1. cc-sddでSDDを試す まずは CLI ベースのSDDツールであるcc-sdd( リポジトリ : https://github.com/gotalab/cc-sdd )を試してみました。 cc-sddは、Kiro 1 の設計思想( AWS では、 AI-DLC と呼んでいます)を他のAIエージェント上で再現するための CLI ツールです。成果物は Markdown で出力されます。今回はCodex CLI と組み合わせて運用しました。 ツールの仕組みについては詳しい記事がたくさん出ていますので、ここでは実際に使ってみた所感を中心に書きます。 良かった点 設計漏れが起きにくい : 不完全な設計書を渡しても、LLMが不足分を指摘してくれる 仕様を充実させるほど安定する : 前提条件を詳しく書くほど、出力のブレが減る 課題に感じた点 「仕様として十分か」の判断が難しい : どこまで設計すればAIに渡す前提として足りているのか、慣れが必要 AIの柔軟さが損なわれる可能性 : スクラッチで書く場合、最初から仕様を詰め切れないことも多い。中途半端に詳しい仕様を渡すと、かえってAIの判断を縛ってしまう Markdown のレビューが大変 : 自動生成された詳細な仕様を読み通すのに時間がかかる 特に負担だったのが、 Markdown ベースのレビュー でした。 仕様として十分な設計をAIが生成してくれるため、ドキュメント量が多くなるのは仕方ないかもしれません。しかし、指摘箇所を日本語で記述するコストが想像以上にかかりました。 たとえば「2.4章の〇〇について、××と書いているが、△△と書いてほしい」のような書き方をしなければならず、レビューのたびにこの作業が発生します。 そこで、SDDの流れは維持しつつ、仕様レビューを GUI で完結できる「spec-workflow- mcp 」に切り替えてみました。 2. spec-workflow- mcp を試してみる spec-workflow- mcp ( リポジトリ : https://github.com/Pimzino/spec-workflow-mcp )は、SDDワークフローを MCP (Model Context Protocol)経由で進めるための仕組みです。成果物は Markdown で管理しつつ、 レビューや承認はWebの GUI 上で完結 できます。 実際に使ってみた流れ 基本的な流れ(requirements → design → tasks → implementation)はcc-sddと同じです。 また、コーディングエージェントとのやり取りも、他のSDDツールと同様、 CLI から行います。 他の CLI ツールと異なるのは、 MCP サーバーとしてローカルにWebフロントエンドが立ち上がることです。 Webからはプロジェクトの様々な情報が確認できるのですが、特に注目すべきなのは、コーディングエージェントの作業を GUI 上でレビューできる機能です。 まず、requirementsなどの各ワークフローが完了すると、Web上に承認依頼が届きます。 承認依頼を開くと、 Markdown が レンダリング された状態で仕様を確認できます。 気になる箇所があれば、範囲選択をしてそのままコメントを残せます。 修正依頼を出すと、AIが仕様を直してくれます。変更点はDiffで確認できます。 requirements → design → tasks と順番に承認していき、最後にタスク開始を依頼すると、カンバンボードで進捗が自動更新されていきます。 良かった点 行範囲に対してコメントを残せる : 指摘箇所を日本語で説明する必要がなく、直接該当部分にコメントできる 通知が分かりやすい : 視覚化されるべき情報が適切に視覚化されている 課題に感じた点 Webと CLI の行き来が若干面倒 : 純粋な CLI ツールと比較して、どうしても操作回数は多くなる 3. SDDで実装品質をどこまで引き上げられるか ここからは、SDDを使って実装品質をどこまでコン トロール できるかを検証します。 3.1 バックエンドの リファクタリング 今回の検証では、要件定義に重きを置き、実装方針はほとんどAIに任せていました。 Steeringでクリーン アーキテクチャ に準じることをルールとして明記していたのですが、実際に生成された実装は以下のような構造でした。 backend/ ├── rss_news_agent/ │ ├── __init__.py │ ├── adapters.py │ ├── api.py │ ├── errors.py │ ├── llm.py │ ├── logging_service.py │ ├── models.py │ ├── repositories.py │ ├── services.py │ └── settings.py ├── tests/ ├── app.py ├── lambda_handler.py ├── pyproject.toml ├── requirements.txt └── uv.lock たとえば repositories.py を見ると、本来Provider層に書くべき実装が混在していました。 class DynamoDbClient (Protocol): def put_item ( self, TableName: str , Item: dict [ str , dict [ str , str ]], **kwargs: object , ) -> dict [ str , object ]: ... def delete_item ( self, TableName: str , Key: dict [ str , dict [ str , str ]], ConditionExpression: str , ) -> dict [ str , object ]: ... また llm.py を見ると、LLMに関連する様々なデータ型やクラスが一つのファイルに詰め込まれていました。 class LlmFeedbackSummaryDto (BaseModel): good_ids: list [ str ] = Field(default_factory= list ) bad_ids: list [ str ] = Field(default_factory= list ) class BedrockRuntimeClientAdapter : def __init__ (self, model_id: str , region: str | None = None ) -> None : self._client = boto3.client( "bedrock-runtime" , region_name=region) self._model_id = model_id これは良い実装とは言いにくいと思います。そこで、SDDを使って リファクタリング を依頼してみました。 Steeringの改善 方針として、Steering(今回は structure.md )のコード規約を詳しく追記し、具体的な実装はAIに任せます。 structure.mdの変更(抜粋) ## Directory Organization project-root/ ├── backend/ # FastAPIバックエンド -│ ├── rss_news_agent/ # ドメインロジック(API/サービス/リポジトリ) +│ ├── rss_news_agent/ # ドメインロジック(Clean Architecture) +│ │ ├── handler/ # ルーティング/入力変換 +│ │ ├── model/ # ドメイン/DTOモデル +│ │ ├── repository/ # 永続化インターフェイスと実装 +│ │ ├── service/ # ビジネスロジック +│ │ ├── provider/ # 外部依存(DB/HTTP等)の生成 +│ │ └── usecase/ # エンドポイント単位のユースケース │ ├── tests/ # pytestテスト │ ├── app.py # 本番/デプロイ用エントリ │ ├── local_app.py # ローカル開発用エントリ │ └── lambda_handler.py # Lambdaハンドラ ## Code Organization Principles 5. **クリーンアーキテクチャ指向**: バックエンドは層分離を意識し、依存性注入(DI)を徹底する +6. **責務の抽象化**: 外部依存はproviderに閉じ、ビジネスロジックはservice/usecaseに集約する +7. **境界の明確化**: 抽象(Port/Protocol等)は内側のレイヤーに置き、実装は外側に限定する ## Module Boundaries * **Repositories vs Services**: 永続化ロジックとビジネスロジックを分離 +* **Endpoint vs Usecase**: 原則として1エンドポイントに対して1usecaseを用意する +* **Provider vs Service**: providerは外部アクセスに限定し、ビジネスルールはserviceに置く +* **Port vs Implementation**: 抽象は内側、実装は外側に配置する 生成されたタスク この変更を反映すると、以下のようなタスクが生成されました。 - [-] 1. 現状のAPI/サービス/リポジトリの依存関係を整理して移行方針を確定する - [-] 2. model層を新設し、ドメインモデル/DTO/入力出力スキーマを整理する - [-] 3. repository層を新設し、永続化インターフェイスと実装を整理する - [-] 4. service層を新設し、ドメインロジックを整理する - [-] 5. usecase層を新設し、1エンドポイント=1usecaseの実装を追加する - [-] 6. handler層を新設し、ルーティングと入出力変換を移行する - [-] 7. provider層を新設し、外部依存の生成を集約する - [-] 8. 既存エントリポイントの接続を更新する リファクタリング の結果 実装を進めてもらった結果、各レイヤーがしっかり整理された構造になりました。 backend/ ├── rss_news_agent/ │ ├── handler/ │ ├── model/ │ ├── provider/ │ ├── repository/ │ │ ├── __init__.py │ │ ├── article_feedback_repository.py │ │ ├── rss_source_repository.py │ │ └── user_profile_repository.py │ ├── service/ │ ├── usecase/ │ │ ├── __init__.py │ │ ├── add_rss_source.py │ │ ├── delete_rss_source.py │ │ ├── fetch_articles.py │ │ ├── list_rss_sources.py │ │ ├── save_article_feedback.py │ │ ├── save_user_profile.py │ │ ├── send_article_chat.py │ │ └── update_rss_source.py │ └── __init__.py ├── tests/ ├── app.py ├── lambda_handler.py ├── pyproject.toml ├── requirements.txt └── uv.lock 実装内容も改善されました。たとえば user_profile_service.py では、純粋な ビジネスロジック だけが実装されています。 class UserProfileService : def __init__ ( self, repository: UserProfileRepository, clock: Callable[[], datetime] | None = None , ) -> None : self._repository = repository self._clock = clock or datetime.utcnow def save_personal_text (self, personal_text: str , user_id: str = "default" ) -> UserProfile: profile = UserProfile( user_id=user_id, personal_text=personal_text, updated_at=self._clock(), ) return self._repository.save(profile) def get_personal_text (self, user_id: str = "default" ) -> str | None : profile = self._repository.get(user_id) if profile is None : return None return profile.personal_text この検証から、 Steeringの質がコーディングエージェントの出力品質に直結する ことが確認できました。 3.2 UI/UX課題の改善を要件定義からAIに任せる 基本的なアプリケーションは構築できましたが、実際に触ってみると使いにくい点がいくつかありました。 発見した課題 1. RSS ソースの編集ボタンが効かない 「Edit」ボタンを押しても何も起きません。これは使いにくい点というより実装漏れですね。 2. パーソナル情報の保存状態が分からない ユーザーの趣味嗜好を入力する部分で、「SAVE」を押すと入力した文字が消えます。保存できたのか分かりません。 「SAVE」を押すと、バックエンドには保存処理が飛んでいるものの、画面上では文字が消えただけに見えます。 3. チャットの応答待ちが分かりづらい 各記事に対してAIエージェントと会話できる機能がありますが、返答を待っている間のインジケータが分かりにくいです。 AIに要件定義から任せる これらの課題を具体的には指摘せず、「UI/UX上の課題を自分で見つけて改善方針を仕様化せよ」と指示を出してみました。 以下は、生成された requirements.md の抜粋です。 ## Requirements ### Requirement 1: RSSソース管理の編集体験強化 (中略) ### Requirement 2: パーソナルシグナルの保存状態の可視化 (中略) ### Requirement 3: 記事一覧の可読性とスキャン性向上 (中略) ### Requirement 4: 収集アクションと進行状況の明確化 (中略) ### Requirement 5: チャット導線の発見性と文脈維持 (中略) WHEN チャット送信中 THEN システム SHALL 送信中の状態を明確に示し、二重送信を防ぐ。 こちらが指摘しなくても、明らかに使いにくい部分は要件として言及されていました。それ以外の項目も、改善案として納得できる内容でした。 改善結果 このまま機能設計 → タスク設計 → 実装と進めました。実装段階では期待どおりでない部分もあったため、都度フィードバックを与えています。 1. RSS ソースの編集 UIにはまだ改善の余地がありますが、編集できるようになりました。 2. パーソナル情報の保存状態 保存されたことが分かりやすくなりました。 3. チャットの応答待ち インジケータが表示されるようになり、分かりやすくなりました。 この検証から、 明らかに問題がある挙動については、AIに要件定義から任せても一定の改善が可能 なことが分かりました。 ただし、今回の検証アプリは コンポーネント 数が少なく、比較的シンプルな構成でした。より複雑なアプリケーションでは、同じようにうまくいくとは限りません。 おわりに 今回SDDを試してみて、個人的に印象に残ったことを書いておきます。 レビューの負荷は想像以上だった Markdown で生成された仕様を読んで、日本語で指摘を書いて…という作業は、耐えがたいものがありました。spec-workflow- mcp に切り替えてからは、行を選択してコメントするだけで済むようになり、だいぶ楽になったと思います。SDDを続けるなら、レビュー体験は重要だと感じました。 Steeringは可能な限り詳細に書いたほうが良い 当たり前かもしれませんがSteeringはプロジェクトの根本となる部分であり、品質を担保する意味で非常に重要です。 「クリーン アーキテクチャ で」と書くだけでは不十分で、 ディレクト リ構成や各レイヤーの責務まで書いてあげると、期待に近い実装が出てきました。ここは試行錯誤しながら調整していく部分かなと思います。 AIに要件定義から任せるのは、ものによる 明らかな問題点は拾ってくれましたが、細かいUI/UXの調整は結局人間がフィードバックを重ねる必要がありました。 まだ手探りな部分も多いですが、SDDとAIエージェントの組み合わせには可能性を感じています。 同じようにSDDを試している方の参考になれば幸いです。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @kita.ryota レビュー: @yamashita.tsuyoshi ( Shodo で執筆されました ) Kiroとは、 AWS が開発したAIエージェント型 IDE で、SDDのアプローチを採用しています。 ↩
アバター
XI本部 クラウド イノベーション センター所属、2年目の米田です。 別ブログにて Amazon ECR と FutureVulsを用いたコンテナイメージ運用について紹介させていただきましたが、実際に使っていく中で、 Amazon ECR には想像以上に多くの便利な機能が備わっていることに気づきました。本記事では、その中でも 日々の運用を効率化したり、セキュリティや管理性を高めたりするうえで役立つ ECR の機能について、実体験を交えながらいくつかご紹介します。 これから Amazon ECR に触れていく方や、すでに利用しているものの「なんとなく使っている」という方にとっても、本記事が少しでも参考になれば幸いです。 まずは復習 Amazon ECR の主な機能 タグフィルタ アーカイブストレージクラス プルタイム更新の除外 まとめ 参考 まずは復習 以前のブログでは、代表的なコンテナ オーケストレーション サービスである Amazon Elastic Kubernetes Service( Amazon EKS) と Amazon Elastic Container Service( Amazon ECS) の 2 つのサービスについてご紹介しました。これらのサービスは、コンテナのデプロイやスケーリング、可用性の確保など、アプリケーション実 行基 盤として重要な役割を担っています。 また、それらのサービスへアプリケーションをデプロイするための コンテナイメージの管理先 として、 Amazon Elastic Container Registry( Amazon ECR) についても取り上げました。 Amazon ECR は、主に Docker コンテナイメージを管理するためのフルマネージドな レジストリ サービスであり、 Amazon ECS、 Amazon EKS、 AWS Lambda(コンテナイメージ形式での実行)など、 AWS の主要なコンテナ関連サービスと高い互換性を持っています。そのため、 AWS 上でコンテナを利用する際の標準的なイメージ管理基盤として利用されるケースも多く、セキュリティ、可用性、運用性の観点からも安心して利用できるサービスとなっています。 Amazon ECR の主な機能 Amazon ECR には、単なるコンテナイメージの保管場所にとどまらず、セキュリティや運用効率を高めるためのさまざまな機能が備わっています。例えば、イメージのスキャンによる 脆弱性 検出、ライフサイクルポリシーによる不要なイメージの自動削除、IAM と連携した細かなアクセス制御など、実運用において役立つ機能が充実しています。 私自身の理解を整理する意味も込めて、本記事では面白いと感じた機能について、実際の利用シーンも想定しながらご紹介します。 タグフィルタ Amazon ECRでコンテナイメージを管理する際、イメージタグの上書き可否を制御できることをご存知でしょうか。ECRのイメージタグミュータビリティは、同じタグ名で異なるイメージを上書きできるかどうかを制御する機能です。 MUTABLE(ミュータブル) - デフォルトの設定 同じタグで何度でも上書き可能 例えば latestタグ を更新し続けるような運用に適している IMMUTABLE(イミュータブル) 一度プッシュしたタグは二度と上書きできない セキュリティとトレーサビリティを重視する運用に最適 IMMUTABLE_WITH_EXCLUSION(除外設定付きイミュータブル) - 比較的新しい機能 基本的にイミュータブルだが、特定のタグパターンだけミュータブルにできる 柔軟性とセキュリティのバランスが取れた設定 例えば、latest タグを使ってコンテナイメージを運用しているプロジェクトでは、latest を継続的に更新するために イメージをミュータブル(上書き可能) に設定しているケースが多く見られます(いわゆるlatest運用)。確かにこの設定にしておくと、同じタグ名であれば常に最新のイメージを参照できるという利便性はありますが、意図せず古いバージョンが上書きされてしまったり、本番環境と開発環境でイメージ内容が一致しなくなるといったリスクが高まるという危険性も伴います。 このような運用による問題を避けるために、 Amazon ECR では タグフィルタ機能 が登場しました。タグフィルタを利用することで、特定のタグ名や形式に対してのみ更新を許可したり、逆に更新を制限することができます。例えば、latest タグはミュータブルのままにしておきつつ、その他の重要なタグはイミュータブルに固定する、といった運用が可能になります。 タグフィルタ機能は コンソール上から簡単に設定できるだけでなく、Terraform の AWS プロバイダーでもすでにサポートされています。インフラをコードとして管理している場合でも、Terraform で柔軟にルールを定義・再利用できるため、安定したタグ運用を実現しやすくなっています。 resource "aws_ecr_repository" "this" { name = "blog-app" image_tag_mutability = "IMMUTABLE_WITH_EXCLUSION" image_tag_mutability_exclusion_filter { filter = "latest" filter_type = "WILDCARD" } } アーカイブ ストレージクラス Amazon ECRを使い続けていると、過去のバージョンのコンテナイメージが蓄積され、ストレー ジコス トが気になってきます。削除するには コンプライアンス 上の保持が必要だったり、いざというときの ロールバック 用に残しておきたかったりと、なかなか削除できないイメージも多いはずです。 そんな課題を解決するために、 AWS は2024年に アーカイブ ストレージクラス(Archive Storage Class)機能をリリースしました。この機能を使うと、アクセス頻度の低いコンテナイメージを通常ストレージの約3割削減されたコストで保管できます。厳密には、最初の150TBまでは標準ストレージと同じコスト($0.10)なのですが、それ以上の アーカイブ がある場合はディスカウントされるようです(以下リファレンスですが、日本語版では情報が掲載されておりませんでしたのでご注意ください)。 https://aws.amazon.com/jp/ecr/pricing/ 項目 標準ストレージ アーカイブ ストレージ 月額コスト(ap-northeast-1) $0.10/GB $0.07/GB(約3割減) アクセス時間 即座 復元が必要(数時間) 最小保存期間 なし 90日間 使用シーン 頻繁にpull/deploy 長期保管・ コンプライアンス 復元コスト なし pull時に復元料金が発生 過去バージョンの長期保管 ロールバック 用の古いバージョン メンテナンス終了した古いバージョン などで用意しておくべきイメージに対して、本機能を駆使することが多いようです。 アーカイブ は手動で実施することもできますし、もちろん CLI やライフサイクルポリシーを使ってイメージを遷移させることもできます。以下はそれぞれの設定例です。 コンソール CLI aws ecr put-image-lifecycle-policy \ --repository-name blog-app \ --image-ids imageTag=v1. 0 . 0 ライフサイクルポリシー resource "aws_ecr_lifecycle_policy" "this" { repository = aws_ecr_repository.this.name policy = jsonencode ( { rules = [ { rulePriority = 1 description = "90日以上前のイメージをアーカイブ" selection = { tagStatus = "any" countType = "sinceImagePushed" countUnit = "days" countNumber = 90 } action = { type = "archive" } } , ] } ) } 簡単に設定できそうですね。 アーカイブ するとプルすることはできなくなり、エラーが返ってきます。 $ docker pull <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/<リポジトリ名>@tag Error response from daemon: unknown: The requested image is in an inaccessible state. Please restore if needed プルする(使用する)ことはできませんが、 describe-images や list-images などをつかった詳細表示は可能です。 こうなると、めったに使わなさそうなイメージはすべて アーカイブ してしまった方が良い気もしますが、復元には一定のコストがかかります。また復元には20分ほど時間がかかるようです(とはいえ自分が試してみたときは数分で復元されてました)。何時間もかかるようなものではないようなので、障害復旧とかでもプロジェクトの要件次第では利用できるかもしれません。 # 復元リクエスト aws ecr restore-archived-images \ --repository-name blog-app \ --image-ids imageTag=v1.0.0 アーカイブ 後は削除も可能ですが、 アーカイブ されたイメージの最小ストレージ期間は 90 日間なようで、ライフサイクルポリシーで削除するには最低でも90日を設定する必要があるようです。もし90日未満のイメージを削除したい場合は、 batch-delete-image API を使った手動削除 になるので、もし短期間のみの アーカイブ を望むのであれば アーカイブ 設定は不適切になります。 ちなみにですが、2025年8月よりECRの リポジトリ あたり 100,000 のイメージまでがサポートされているようです。 https://aws.amazon.com/jp/about-aws/whats-new/2025/08/amazon-ecr-supports-100000-images/ このサポートが保証するイメージ数に、 アーカイブ したイメージが含まれるのかはわかりませんでした...。 コストだけでなく、キャパシティも最適化されると良いですね。 プルタイム更新の除外 Amazon ECRでは、コンテナイメージがプルされるたびに「最終プル時刻(LastRecordedPullTime)」が更新されます。この情報は、sinceImagePulledを使ったライフサイクルポリシーで「実際に使われていないイメージを自動削除する」という賢い運用に活用できます。 そんな中で、この時刻の更新を実施しないようにする方法も存在します。それが プルタイム更新の除外機能(Pull Time Update Exclusion) です。特定のIAMロールからのプルをLastRecordedPullTimeの更新対象から除外することで、より正確なライフサイクル管理が可能になります。 最近のアップデート機能のため、現時点ではまだ詳細な情報は多くありませんが、例えばセキュリティスキャナー(Snyk、Trivy、CrowdStrike など)や CI/CD パイプライン上のテスト処理が頻繁にコンテナイメージを pull すると、実際には本番環境で利用されていないイメージであっても「最近使用された」と判定されてしまうといった課題が想定されます。 その結果、本来であれば削除対象となるはずの古いイメージが、ECR のライフサイクルポリシーによって削除されず、不要なイメージが溜まり続けてしまう可能性があります。 こうした課題に対して、今回制御した制御機能を活用することで、スキャンやテスト用途の pull を「実運用の利用」とは区別して扱えるようになり、ライフサイクルポリシーをより正確に機能させることができます。 まとめ 今回は、 Amazon ECR における運用をより安全かつ効率的にするための機能として、 ミュータブル・イミュータブルのタグ制御(タグフィルタ)、 アーカイブ 機能、 プルタイムの更新除外機能 の 3 つを紹介しました。 これらの機能を活用することで、 タグの誤更新によるリスクを抑えられる 利用頻度の低いイメージを適切に保管・整理できる ライフサイクルポリシーをより意図通りに運用できる といったようなメリットが得られます。 ECR は単なるイメージ保管庫ではなく、工夫次第で運用負荷やコスト、セキュリティリスクの低減にも大きく貢献してくれるサービスだと感じました。 紹介できなかった機能もあるので第二弾としてまた掲載できればと思っております。 本記事が、これから ECR を本格的に活用していく方の参考になれば幸いです。 参考 https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/archive-image.html https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/pull-time-update-exclusions.html https://aws.amazon.com/jp/ecr/pricing/ https://aws.amazon.com/jp/ecr/ 執筆: @yoneda.kosuke レビュー: @taguchi.kazutoshi ( Shodo で執筆されました )
アバター
こんにちは! エンタープライズ 第三本部 マーケティング IT部の母壁です。 この度Lookerが MCP に対応し、外部のLLM(生成AIエージェント)からのLookerのデータ探索および活用が可能となりました。この記事ではLooker MCP の導入による利便性と、データ分析・ダッシュボード作成自動化における効果について検証した内容をご紹介します。 まず初めに、 MCP およびLookerの基礎的な知識をご説明します。 MCPとは MCP Toolbox Lookerについて 【検証】Gemini CLIからLooker MCPを使ってみる 1. MCP Toolboxのインストール 2. Gemini CLIのインストール 3. MCPクライアントの構成 4. ダッシュボードの自動生成 5. プロンプトの調整 おわりに:実用性はあるのか? MCP とは MCP (Model Context Protocol)は、 生成AIと外部データソースとの連携を標準化するためのオープンスタンダードな 通信プロトコル です 。 簡単にいうと今まで各社がそれぞれの方法で開発していたLLMと外部ツールの接続方式を統一したら色々と便利だよね、ということです。 MCP を公開したAnthropic社は「AIのためのUSB-Cのような存在」と表現しています。(今や iPhone や Android が同じType-Cコネクタになったように、LLMと外部ツール間の統一された接続規格として機能するということですね!) MCP が オープンソース 化されてから続々と対応するツールやサービスが登場しており、今後さらなる広がりが予想されます。 MCP Toolbox MCP Toolbox とは、AIアプリケーションとデータベースなどの連携を効率化するための Google が提供する オープンソース ツール群です 。 AIエージェントが企業データに安全にアクセスし、 自然言語 でのデータ検索や分析といったタスクを効率的に実行するための基盤を提供します。このToolboxの一部として、2025年7月に「Looker MCP Server」が提供開始されました 。 Lookerについて Looker は Google が提供するビジネスインテリジェンス(BI)ツールです。単なるデータの可視化にとどまらず、ガバナンスの効いた統合的なデータプラットフォームとしての側面も持ち合わせていますが、今回はデータソースから収集、加工、分析したデータをダッシュボード上で可視化できるツールとしてご紹介します。 【検証】Gemini CLI からLooker MCP を使ってみる ここから本題となりますが、 Looker MCP を使用した生成AIによるダッシュボードの自動生成 についてご紹介します。 本来Lookerでダッシュボードを作成するには GUI 上でデータを探索し、可視化したタイル(グラフなど)を一つずつ作成・配置をする必要があります。これを生成AIによって自動化できたら便利そうですよね、、? そこでLooker MCP の登場によりダッシュボード自動生成の機能がどこまで実用的なものなのか検証してみました。 以下、こちらの 公式ドキュメント を参考に作業を行いました。 1. MCP Toolboxのインストール まず MCP Toolboxの最新バージョンをダウンロードします。 curl -O https://storage.googleapis.com/genai-toolbox/v0.14.0/darwin/arm64/toolbox バイナリを実行可能にします。 chmod +x toolbox 最後にバージョンを確認しておきましょう。(検証時はV0.12で実施しています) ./toolbox --version 2. Gemini CLI のインストール 今回はローカル環境からGemini CLI を使用して MCP サーバーを構成しました。 Gemini CLI の利用は、 npx コマンドを使って一時的に実行することも可能ですが、 settings.json の設定など繰り返し利用する想定なので、今回は npm でグローバルにインストールしています。 npm install -g @google/gemini-cli 3. MCP クライアントの構成 続いて MCP クライアントを構成します。 以下のように settings.json ファイルを定義することで、Geminiが MCP クライアントとして機能し、起動時に MCP サーバーを立ち上げてくれます。この時ファイルは .gemini フォルダ直下に配置する必要がある点に注意してください。 各要素を少し解説します。 looker-toolbox :Looker MCP Serverを使用する場合は指定します。 command :インストールした MCP Toolboxのパスを指定します。 env :Looker インスタンス に接続する MCP クライアントを構成するための 環境変数 です。 LOOKER_BASE_URL :接続するLookerのURLを指定します。 LOOKER_CLIENT_ID :LookerにログインするユーザーのIDです。 LOOKER_CLIENT_SECRET :Lookerにログインするユーザーのシークレットです。 設定は以上です!ローカル環境という点もありますが、想定よりも簡単に設定が完了しました。 ではいよいよプロンプトを投げてダッシュボードを作ってもらいます。 4. ダッシュボードの自動生成 Gemini CLI を起動してみます。 MCP クライアントが正常に構成されていれば「Using: 1 MCP server」と表記されます。 ダッシュボードを表示する元データとして、BigQueryのパブリックデー タセット に用意されている ECサイト の売上データを使用しました。 事前準備としてBigQueryからLookerにデータを取り込み、LookMLで エクスプローラ ー等の定義を行ったうえで作業しています。 またLookMLでは以下のようにメジャーを記述し、日本語のラベルを追加することでLLMが探索をしやすくなるように メタデータ を補填する工夫を行いました。メジャー名に略称を使用している場合や、専門的な単語がある場合に、日本語のラベルを定義することで日本語のプロンプトに対して探索の精度が上がることは事前に確認しています。 この状態でLLMにダッシュボード生成を依頼してみましょう。 まずは事前に用意した エクスプローラ ー「hahakabe02」を指定して、ダッシュボードの中身については特に指定せずに生成を依頼してみます。 hahakabe02を使用して、2023年のセールスダッシュボードを作成してください。 依頼をするとまずはログインするユーザーの参照できる範囲でどのデータを探索するか逐一確認されます。参照するデータを選択することでダッシュボードに不要なデータを弾くことができます。 LLMが使用できるツールには例えば以下のようなものがあります。 get_models :LookML モデルの一覧を取得 get_explores : エクスプローラ ーの一覧を取得 get_dimensions :指定された Explore のディメンションの一覧を取得 get_measures :指定された Explore のメジャーの一覧を取得 query :クエリを実行してデータを返却 make_dashboard :ダッシュボードを作成しURLを返却 add_dashboard_element :ダッシュボードにタイルを追加 一連の探索が終わるとクエリを実行してダッシュボードに表示するタイルを作成してくれます。できあがったダッシュボードを確認してみましょう。 月ごとの売上の推移や 都道 府県別の売上など、お題に合うように指標を決めてタイルを作成してくれています。 一方で各タイルの集計条件を細かく確認してみると、例えば総売上の指標については発注情報のうち、「購入日時」のディメンションではなく「製造日時」のディメンションでフィルタリングしているようでした。このような集計条件の指定はプロンプトで指定してあげるほうが良さそうです。 5. プロンプトの調整 改めてプロンプトを調整してダッシュボード生成を依頼してみます。タイルの種類、参照するメジャー、フィルターの条件など各タイルに指定してみます。 Explore「hahakabe02」を使用して、新しいダッシュボード「2024年セールスダッシュボード demo」を作成してください。 以下のビジュアライゼーションをダッシュボードに追加してください: 総売上 タイプ:単一値 メジャー:[総売上] 日付フィルタ:[購入日時]が2024年である 値の形式:$#,##0 平均注文金額 タイプ:単一値 メジャー:[平均注文金額] 日付フィルタ:[購入日時]が2024年である 値の形式:$#,##0 ... できあがったダッシュボードを見てみるとフィルターが正常に適用され、2024年の購入データのみが表示されていることが確認できました!またグラフの形式や使用するメジャーを指定することである程度望んでいた表示になっていました。 ただし「値の形式」で表示桁数を整数値にするようにプロンプトで指示しましたが、LLMがそこを設定することは現状できないようでした。やはり作成されたグラフが正しいデータを参照しているのかの確認や、最終的な見た目の仕上げにはまだ人の手が必要です。 おわりに:実用性はあるのか? MCP はデータ統合・分析基盤やLLMの外部システム連携において、急速的に浸透しつつあります。Lookerが MCP に対応したことで実務で使える場面も増えてくると大いに期待できます。 1. ダッシュボード作成の初動を効率化 先述したように最終的なダッシュボードの正確性の確認や表示スタイルのチューニングなどには人力のコストがある程度必要なものの、作業の初動を早められることは確実だと感じました。また対象のデータに精通していない人でもAIとの会話でデータの探索、グラフの作成ができるため、人員調達のハードルも下がることが期待されます。 2. より高度な インサイト の導出 従来のLookerの組み込み会話分析機能では、通常1つの エクスプローラ ーの範囲内でデータをクエリしますが、複数の エクスプローラ ーを探索しデータを統合することでより柔軟で高度なデータ分析やレポート作成ができるようになると思います。また MCP サーバーを複数立てることでLookerだけでなく CRM やGoogleDriveなど外部サービスと連携することも可能です。例えば今回のように顧客データや売上だけでなく、営業活動のデータやウェブサイト行動のデータなどを集約し、横断的な分析と インサイト の提示を得ることも可能だと思われます。 このようなメリットが期待できる一方で、改めてLookMLにおけるデータの定義を充実させることが会話分析の精度向上に不可欠であることを実感しました。 最後までお読みいただきありがとうございました。 本記事ではLooker MCP を中心にご紹介をしましたが、弊社ではLookerの導入実績も豊富にありますので、Lookerの導入を検討されている、あるいはLookerの次の活用フェーズをお考えの際にはぜひご相談ください! 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @hahakabe.kiichi レビュー: @kinjo.ryuki ( Shodo で執筆されました )
アバター
XI本部 クラウド イノベーション センター所属、2年目の米田です。 この度、2026 Japan All AWS Certifications Engineers クライテリア の応募基準を満たしました。約1年間(厳密には1年1か月)という比較的短期間で全冠でき、資格取得のための学習でかなり クラウド への理解が深まったと感じております。 今回は、 クラウド 初心者(ひいてはIT初心者)が、どのようにして AWS 資格を全冠得を進めたかなどを紹介できればと思います! これから AWS 資格に挑戦する人や、「何から取ればいいかわからない」「モチベーションが続かない」という人の参考になれば嬉しいです。 (同様に新卒1年目で条件を満たした大岡さんのブログも合わせてご覧ください!1年目で全冠を達成されたのはすごいですね、、!全冠を目指されたきっかけや思いが綴られておりますのでぜひ。) https://tech.dentsusoken.com/entry/2025/12/23/%E3%80%90AWS%E8%B3%87%E6%A0%BC%E3%80%91%E6%96%B0%E5%8D%921%E5%B9%B4%E7%9B%AE%E3%81%8CAWS%E8%B3%87%E6%A0%BC%E3%82%92%E5%85%A8%E5%86%A0%E3%81%97%E3%81%9F%E3%81%AE%E3%81%A7%E6%8C%AF%E3%82%8A%E8%BF%94 はじめに 他の方のブログでも紹介していただいていますが、そもそも 「2026 Japan All AWS Certifications Engineers」 とは、 AWS Partner Network(APN)に参加している会社に所属し、2026年度のクライテリアで定義された AWS 認定資格をすべて保持している AWS エンジニアを対象とした 表彰プログラム です。 2026年度のクライテリアでは、以下の AWS 認定資格を すべて有効な状態で保持していること が条件です。 資格は申し込み時点で有効であり、2026年4月30日までに有効期限が切れないことが応募要件となっています(うっかり失効しそう)。 レベル 必須資格 Foundational AWS Certified Cloud Practitioner Foundational AWS Certified AI Practitioner Associate AWS Certified Solutions Architect – Associate Associate AWS Certified SysOps Administrator – Associate または AWS Certified CloudOps Engineer – Associate Associate AWS Certified Developer – Associate Associate AWS Certified Data Engineer – Associate Associate AWS Certified Machine Learning Engineer – Associate Professional AWS Certified Solutions Architect – Professional Professional AWS Certified DevOps Engineer – Professional Specialty AWS Certified Security – Specialty Specialty AWS Certified Machine Learning – Specialty または AWS Certified Generative AI Developer – Professional Specialty AWS Certified Advanced Networking – Specialty なお「SysOps Administrator – Associate」か「CloudOps Engineer – Associate」のどちらか1つの取得でもOKです。 AWS Certified Machine Learning – Specialty は 2026年3月31日をもって受験終了予定 ですが、取得していない場合は AWS Certified Generative AI Developer – Professional を取得することでクライテリアを満たすことができます。 https://aws.amazon.com/jp/blogs/psa/2026-japan-all-aws-certifications-engineers-criteria/ 受験してみた感想ですが、SysOps Administrator – Associate と CloudOps Engineer – Associate にそこまで大きな差は感じられませんでした。最近アップデートがあった資格なだけあって身構えていましたが、これまで対象範囲でなかったECSやEKS、そしてCDKについて簡単に対策していけば、十分に合格できるかと思います。 ロードマップ 気になる取得までのロードマップですが、私は以下のような間隔と順番で資格取得いたしました。 資格 レベル 取得時期 AWS Certified Cloud Practitioner Foundational 2024年12月 AWS Certified Solutions Architect – Associate Associate 2025年1月 AWS Certified Solutions Architect – Professional Professional 2025年3月 AWS Certified Security – Specialty Specialty 2025年5月 AWS Certified Advanced Networking – Specialty Specialty 2025年7月 AWS Certified AI Practitioner Foundational 2025年8月 AWS Certified Machine Learning – Specialty Specialty 2025年9月 AWS Certified Machine Learning Engineer – Associate Associate 2025年10月 AWS Certified Data Engineer – Associate Associate 2025年11月 AWS Certified Developer – Associate Associate 2025年12月 AWS Certified DevOps Engineer – Professional Professional 2026年1月 AWS Certified CloudOps Engineer – Associate Associate 2026年1月 こちらも感想になりますが、やはり AWS に慣れてない最初は「受かりそう!」と思えるまで時間がかかりました。(特にSAP合格までは聞き慣れないサービスばかりで本当に苦労しました……)。 ただ、学習を続けていくうちに少しずつ理解が積み重なり、実際に受験してみると 「意外と受かるな」という感覚も持てるようになりました。 特に後半は、 AWS のサービス全体像や設計思想への理解が深まってきたことで、問題文を読んだ瞬間に「何を問われているのか」が掴める場面が増え、試験への 心理的 なハードルもかなり下がったように思います。 ただ落ちると 2週間は再受験できない ので、適当に受けるのではなく、しっかりと準備をしてから挑むことも大事ですね。 進め方 資格取得までの進め方についてですが、基本的には多くの方のブログで紹介されているとおり、オンライン問題集や教材を中心に学習していました。 これはあくまで個人的な学習スタイルですが、Udemyなどの動画講座を「視聴する」よりも、自分で実際に手を動かして調べたり、検証したりする方が圧倒的に理解が深まると感じました。実際に AWS 環境を触りながらサービスの挙動を確認し、設定を試して失敗し、なぜそうなるのかを調べる、みたいなプロセスを繰り返すことで、知識が単なる暗記ではなく、実感を伴った理解として身についていったと思います。 ただ一方で、自分の知らないサービスを知るきっかけを得る、 AWS のベストプラクティスを体系的に学ぶ、という意味では、動画講座や解説コンテンツをチェックする方法も非常に有効だと感じています。インプット(講座)とアウトプット(実践)をバランスよく組み合わせることが、結果的に一番の近道かと。 AIの活用 分からない内容が出てきた際にネット検索で調査するのも良いですが、個人的には近年発展したAIに頼ってみるのも有効だと思います。 2025年になっていろいろ出てきた AWS の MCP サーバについてご存じでしょうか。 MCP (Model Context Protocol)は、LLMアプリケーションと AWS サービスやリソースを接続するための標準化された プロトコル です。こちらは ユースケース に応じて AWS 環境の構築・運用・ トラブルシューティング など、様々なシーンで活用できる強力なツールになります。普段は開発などで使用しますが、たまに資格勉強でも利用していました。 https://awslabs.github.io/mcp/servers/ 利用方法についてはいろいろブログで記載されているので割愛しますが、例えば以下のような MCP サーバは使ってみても良いかなと思います。 AWS Documentation MCP Server AWS 公式ドキュメントをリアルタイムで検索・参照してくれる MCP サーバーです。 常に最新の情報をもとに回答してくれるため、特に最近アップデートされた資格や新サービス周りを調べる際に非常に有効だと感じました。 AWS IaC MCP Server CloudFormation や AWS CDK、Terraform などの Infrastructure as Code(IaC) に関する設計・記述を支援してくれる MCP サーバーです。 リソース定義の書き方を調べたり、テンプレートのレビューをしたりする用途に向いており、「この構成をコードでどう書けばよいか?」といった疑問を解消するのに役立ちます。実務でIaCを触っている人はもちろん、SAP や DOP など設計系試験の理解を深める用途でも使えると感じました。 AWS Pricing MCP Server リアルタイムの AWS 料金情報にアクセスし、コスト分析を行ってくれる MCP サーバーです。 個人的には使用頻度はそこまで高くありませんでしたが、 AWS 資格の試験では「コスト最適化」の観点でベストプラクティスを問われることが多いため、ケースによっては十分活用できるツールだと思い、紹介しています。 AWS の公式サイトやブログなどを調べて理解を深めてもらっても良いですが、一旦概要を知りたい際や、調べ方がわからない場合には上記 MCP サーバなどを活用して一旦AIに投げてしまっても良いですね! まとめ 改めて、 AWS 資格を全冠し、「2026 Japan All AWS Certifications Engineers」の応募クライテリアを満たすことができました。 もちろん資格取得そのものも目標のひとつでしたが、それ以上に、学習を通して クラウド 全体への理解が深まり、自信を持って AWS に向き合えるようになったことが何より大きな成果だったと感じています。 なお、All Certs を達成すると、 AWS Summit Japan で表彰されるようで、年によっては副賞として景品がもらえることもあるようです。 クラウド に少しでも興味がある方であれば、ぜひ一度、 AWS 資格へのチャレンジ、そして全冠を目標にしてみてほしいと思います。 (来年も全冠維持します...) 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @yoneda.kosuke レビュー: @miyazawa.hibiki ( Shodo で執筆されました )
アバター
XI本部 クラウド イノベーション センター所属、2年目の米田です。 今回は、 AWS サービス上でのコンテナイメージの運用と、それをより効率的に実現するFutureVulsについて、基礎的な部分を中心に共有させていただきます。 本ブログは、 10月下旬にあった社内向けの勉強会 にて発表させていただいた内容を含んだものになっており、少し振り返りながら改めてまとめさせていただきました。 勉強会当日は100名以上の参加者となり、そのような場での発表は私自身とても良い経験でした。勉強会にご参加いただいた方、コメントいただいた方、取りまとめていただいた方、そしてフューチャー社の皆様、改めてありがとうございました。 はじめに コンテナ制御 コンテナイメージ管理 リポジトリの種別 自動継続的スキャン 脆弱性管理 自動トリアージ FutureVuls AI 未然防止 まとめ 参考 はじめに 普段、皆さんはどのような方法でアプリケーションを開発・実行されていますでしょうか。 クラウド 上でアプリケーションを起動する方法にはさまざまな選択肢があります。 仮想マシン 上で直接実行する方法、PaaS を利用する方法、サーバレス アーキテクチャ など、用途や規模に応じて複数のアプローチが存在します。 その中でも、近年特に多く利用されているのが コンテナサービス です。 コンテナは、アプリケーションとその実行に必要なライブラリや設定をまとめて扱えるため、開発環境と本番環境の差異が生まれにくく、デプロイやスケーリングを容易に行えるという特徴があります。また、移行性の高さも大きなメリットかと思います。別環境への移行や、新しい環境の構築時などで効率的に作業を進めることができ、個人的にはこのあたりは非常に有用だと感じています。 AWS ではコンテナを扱うのに特化したさまざまなサービスが用意されており、それぞれの特性を理解して適切に使い分けることが重要です。ここではその代表的なものを取り上げます。 コンテナ制御 AWS でコンテナ オーケストレーション を検討する際、代表的な選択肢として Amazon EKS と Amazon ECS の 2 つのサービスが挙げられます。 Amazon EKS は、 Kubernetes をマネージドサービスとして提供するもので、 Kubernetes のエコシステムや標準的な運用手法をそのまま利用できる点が特徴です。一方、 Amazon ECS は AWS 独自のコンテナ オーケストレーション サービスであり、 Kubernetes を直接意識することなく、 AWS に最適化された形でコンテナを管理・実行することができます。そのため、ECS は比較的シンプルな構成でコンテナを扱えるという利点があります。 また、これらのコンテナサービスでは、コンテナをどこで起動するか という点も重要な要素になります。 AWS では主に、 Amazon EC2 や AWS Fargate といった起動方式が利用されます。 Amazon EC2 を利用する場合は、 仮想マシン 上でコンテナを実行するため インスタンス のスペックや台数を自分で管理する必要がありますが、その分柔軟な構成が可能です。一方、 AWS Fargate はサーバレスな起動方式であり、 インスタンス 管理を意識することなく、必要なリソースを指定するだけでコンテナを実行できます。 以下で、EC2 と AWS Fargate について簡単に整理してみました。 項目 Amazon EC2 AWS Fargate 起動方式 仮想マシン 上でコンテナを実行 サーバレスでコンテナを実行 インスタンス 管理 必要 不要 OS管理 必要 不要 スケーリング 自前設定 自動 課金方式 インスタンス 単位 vCPU / メモリ使用量 自由度 高い ある程度制約あり また、ご存じの方も多いかもしれませんが、 AWS Lambda でもコンテナイメージを利用してアプリケーションを実行することができます。(コンテナLambdaと呼ぶこととします) AWS Lambda は FaaS(Function as a Service)に分類されるサービスで、従来は ZIP ファイル形式でアプリケーションコードをデプロイする方法が一般的でした。この方式は手軽である一方、デプロイ可能なファイルサイズに制限(最大250MB)があり、ライブラリや依存関係が多いアプリケーションでは制約になることがあります。 一方で、Lambda では コンテナイメージを用いたデプロイ にも対応しており、この場合は10 GB までのサイズのイメージをデプロイすることができます。さらにLambda web adapterの登場により、従来は ECS や EC2 上で動かすことが一般的だった Web アプリケーションを、ほぼそのままの構成で Lambda 上に載せることが可能になりました。さらにコンテナサービスの幅が広がりました。 コンテナイメージ管理 上記のように、 AWS にはコンテナを制御・起動するためのさまざまなサービスが存在しますが、それらを支える重要な コンポーネント として Amazon Elastic Container Registry(ECR) が用意されています。 Amazon ECR は、主にDocker コンテナイメージを管理するためのマネージド レジストリ サービスであり、 Amazon ECS、 Amazon EKS、 AWS Lambda(コンテナイメージ) など、 AWS の主要なコンテナ関連サービスと高い互換性を持っています。これにより、同じコンテナイメージを複数のサービス間で再利用しながら、安全かつ一元的に管理することが可能です。 そんな Amazon ECR には、いろいろ面白い機能がありそうですので、また別の記事で詳しく紹介させていただきます。今回は一部分のみ抜粋して紹介します。 リポジトリ の種別 Amazon Elastic Container Registry(ECR)には、プライベー トリポジ トリ と パブリック リポジトリ の 2 種類が用意されています。どちらもコンテナイメージを管理するためのサービスですが、用途や設計思想には明確な違いがあります。 以下自分の理解です。 プライベー トリポジ トリ AWS アカウント内でのみ利用できるコンテナイメージの保管場所で、社内向けアプリケーションや、外部に公開したくない独自のアプリケーションイメージを扱う場合には、プライベー トリポジ トリが適しています。 パブリック リポジトリ インターネット上に公開されたコンテナイメージを配布するための仕組み。 AWS アカウントを持っていないユーザーでもイメージを取得できるため、 OSS の配布やサンプルアプリケーションの公開などに向いています。 パブリック リポジトリ であれば基本的に認証なしでイメージ取得が可能ですが、プライベー トリポジ トリでは認証が必要になります。実際に認証なしでプライベー トリポジ トリへpullを試みると、以下のように拒否されてしまいます。 $ docker pull <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/<対象リポジトリ>:tag Error response from daemon: pull access denied for <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/<対象リポジトリ>, repository does not exist or may require 'docker login': denied: Your authorization token has expired. Reauthenticate and try again. 認証してトライしてみます。 # 認証 aws ecr get-login-password --region ap-northeast-1 \ | docker login \ --username AWS \ --password-stdin <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com $ docker pull <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/<対象リポジトリ>:tag tag: Pulling from <対象リポジトリ> 11b72ab0c7e9: Pull complete 940530de9b36: Pull complete 711f628928ef: Pull complete 75131756b8ac: Pull complete ... 今度はうまく接続できてそうです。 CI/CDで認証ステージを組み込んであげれば、自動的にECRへログイン→ECRへのイメージプッシュが行えます。 自動継続的スキャン コンテナは軽量で再利用しやすいという利点がある一方で、ベースイメージやライブラリをそのまま使い回すことが多く、知らないうちに 脆弱性 を含んだ状態で運用してしまうケースも少なくありません。そのため、コンテナイメージに含まれる 脆弱性 をどのように管理するかは、運用において避けて通れない課題となっています。 特に、複数のサービスで同じコンテナイメージを利用している場合、1 つの 脆弱性 が複数のシステムに影響を及ぼす可能性があります。こうしたリスクに対応するためには、単発のチェックに留まらず、継続的に 脆弱性 を把握し、適切に対応していく仕組みを整えることが重要です。 そこで AWS ではコンテナイメージの 脆弱性 を管理するための仕組みとして、プライベート レジストリ を対象に 基本スキャン と 拡張スキャン の 2 種類の 脆弱性 スキャン方式が用意されています。 基本スキャンでは、イメージのプッシュ時またはオンデマンドでのスキャンが可能であり、OSに含まれるパッケージの既知の 脆弱性 を検出することができます。こちらは基本的に無料で使用することができ、スキャン結果についてはECRのコンソール上から確認することができます。 スキャンには共通 脆弱性 識別子 (CVE) データベースを使用する2種類の設定があったようですが、Clairと呼ばれる オープンソース の 脆弱性 データベースを用いたスキャンは廃止され、現在は AWS ネイティブスキャンという方式がGAとなっている状態でした。利用するためにはオプトインが必要なようですね。 もう一つのスキャン方法は、拡張スキャンと呼ばれる方式です。こちらは有料になりますが、 Amazon Inspector と統合され、より詳細な 脆弱性 検出が可能になります。また、スキャン対象もOSだけでなく、 プログラミング言語 のパッケージやライブラリにも対応するため、より網羅的に 脆弱性 管理が可能となります。オンデマンドではなく継続的なスキャンがサポートされる点も良いですね。 Inspector統合なこともあり、検出結果は以下画像のようにInspectorのダッシュボードから確認できます。大まかに 脆弱性 が検知されたかも確認できますし、画像にはありませんがスキャン結果詳細を確認することももちろん確認可能です。 拡張スキャンによる料金は、東京リージョンで以下のようになっているようです。 スキャンタイミング コスト プッシュ時 対象イメージにつき $0.11 継続スキャン スキャンごとに $0.01 こうしてみると、そこまで高額なわけでもないので性能を考えると拡張スキャンを採用しておくのが良いのかなと思います。 脆弱性 管理 Amazon Inspector のみを利用した 脆弱性 管理も有効な選択肢ですが、コンテナイメージの数が増えてきたり、複数チーム・複数サービスで同時に運用している場合、「検知したあと、どう管理・運用するか」 という点も重要になってくると思います。 こうした中、 脆弱性 管理ツールの「FutureVuls」を用いることでECR上のイメージの 脆弱性 管理をより効率化することができます。 詳細な設定方法については公式ドキュメントを参照いただければと思いますが、おおまかな流れは以下になります。 FutureVuls で AWS 連携の設定を実施 FutureVulsで 脆弱性 管理対象とする ECR リポジトリ を選択 以降、 AWS 側で実行されるスキャン結果を定期的に FutureVuls に取り込み、 脆弱性 ・タスクとして管理 (公式) help.vuls.biz 手順としては非常に簡単で、個人的にはUIも直感的でわかりやすいなと思っています。 FutureVuls にはECRイメージの 脆弱性 管理を支えるいくつかの機能があり、深ぼっていきたいところですが、ここでは重要な機能と、大変便利な機能を一つずつピックアップし紹介します。 自動 トリアージ FutureVuls の大きな特徴の一つが、SSVC(Stakeholder-Specific Vulnerability Categorization) という フレームワーク に基づいた 脆弱性 管理を実現できる点です。FutureVuls ではこの フレームワーク を用いてリスクを評価し、優先順位付けを行う、自動 トリアージ 機能を搭載しており、「out of cycle(重大な検出)」や「Immediate(緊急性の高い検出)」などと自動分類することで、セキュリティチームが比較的重要な脅威に迅速に対応できるようになります。 さらに 脆弱性 対応のタスク管理もFutureVulsにて実施可能で、対応ステータス等の把握が容易になります。 SSVCについて詳しく知りたい方はFutureVuls様のこちらの記事をご参照ください: www.vuls.biz FutureVuls AI FutureVuls には、 脆弱性 管理をさらに効率化するための機能としてFutureVuls AI が提供されています。 FutureVuls AI を簡単に紹介すると、 検出された 脆弱性 に対して分かりやすいサマリを提示し、対応判断を支援してくれる AI アシスタント です。単に 脆弱性 を検知するだけでなく、「この 脆弱性 は何が問題で、どこに影響があるのか」を把握するまでの時間を大きく短縮してくれます。 脆弱性 対応を行う際には、CVE 情報や各種アドバイザリを確認することが一般的ですが、これらの情報は 英語で記載されており、数ページに及ぶことも少なくありません。そのため、内容を正しく理解するまでに想像以上の時間がかかるケースもあります。 FutureVuls AI は、こうした 大量かつ専門的な情報を要約し、ポイントを整理して提示してくれるため、 脆弱性 の種類 影響範囲の有無 対応の必要性や優先度の判断材料 といった情報を、短時間で把握することが可能になります。 未然防止 AWS Inspector や FutureVuls を活用することで、 脆弱性 を効率的に検知・管理することは可能ですが、理想を言えば 「 脆弱性 が検知されない状態」を目指したい ところです。つまり、問題が発生してから対応するのではなく、そもそも 脆弱性 を含まないイメージを作るための仕組みづくりが重要になります。 その取り組みの一例として、ECR にコンテナイメージを push する前の段階で、依存関係のアップデートを自動で検知・提案してくれるツールを導入する方法があります。具体的には、 GitHub 上で利用できる Renovate や Dependabot などの自動化ツールが代表的です。 これらのツールを導入すると、アプリケーションや Dockerfile で使用しているライブラリ、ベースイメージの新しいバージョンが公開された際に、変更内容を反映した Pull Request を自動で作成してくれます。開発者はその PR をレビューしてマージするだけでよく、手作業でバージョンチェックを行う必要がありません。 さらに、利用するコンテナイメージのベースとして Distroless イメージを採用することで、セキュリティリスクをより一層抑えることができます。Distroless イメージは、OS やシェルといった不要な コンポーネント を含まず、アプリケーションの実行に必要な最小限のライブラリのみで構成されたイメージです。そのため、攻撃対象となり得るパッケージの数を減らすことができ、結果として 脆弱性 が検知される可能性そのものを低減できます。 このように、 依存関係の自動アップデートによる「事前対策」 Distroless イメージの採用による「攻撃対象の最小化」 といった取り組みを組み合わせることで、コンテナイメージに含まれる 脆弱性 を大幅に削減し、より安全で持続可能なコンテナ運用を実現することが可能になります。 まとめ 今回は、コンテナイメージの管理方法として AWS が提供する Amazon Elastic Container Registry(ECR) を中心にご紹介しました。 ECS や EKS、Lambda など複数のコンテナ関連サービスと親和性が高く、イメージを一元的に管理できる点は、ECR の大きな強みと言えます。 また、実運用における 脆弱性 管理の観点では、FutureVuls を活用することで、検知から対応までをより効率的に実施できることをご紹介しました。 単にスキャン結果を確認するだけでなく、優先度に基づいた対応判断や、チケット管理・共有まで含めた運用を行うことで、継続的な 脆弱性 管理体制を構築しやすくなります。 勉強会の中でもコメントをいただきましたが、自動化できる部分は積極的に自動化し、可能な限り手動での設定や対応を減らしていくことは、今後ますます重要になっていくと感じています。今後は外部サービスとのさらなる連携も検討されているとのことで、より実践的な運用が実現できる点に期待が高まります。 参考 https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/image-scanning.html https://aws.amazon.com/jp/inspector/pricing/ https://help.vuls.biz/manual/scan/image/ https://docs.aws.amazon.com/whitepapers/latest/overview-deployment-options/amazon-elastic-kubernetes-service.html https://aws.amazon.com/jp/ecr/ 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @yoneda.kosuke レビュー: @taguchi.kazutoshi ( Shodo で執筆されました )
アバター
はじめに 金融IT本部 2年目の坂江 克斗です。 今回は11月にプレビューが公開された AWS Network Firewall Proxy の検証結果について紹介します。 プロキシの位置づけから丁寧に解説できればと思います。 本記事は 2025年12月時点(プレビュー) の仕様に基づきます。正式リリース時に仕様が変更される場合もあるため、参考情報としてご覧ください。 はじめに プロキシの概要 よくある疑問 ざっくりと説明 プロキシの位置づけ AWS Network Firewall Proxyの概要 アーキテクチャ 動作イメージ AWS Network Firewall Proxyに関わるリソースの詳細 ルールグループ・ルール プロキシ設定・プロキシ AWS Network Firewall Proxy の検証 前提 Terraformの実装 手動設定 ルールグループ・ルールの作成 プロキシ設定の作成 プロキシの作成 VPCエンドポイントのセキュリティグループ修正 EC2へのプロキシ設定追加 動作検証 まとめ おわりに プロキシの概要 よくある疑問 社内で開発をしていると、「モジュールがインストールできない」「このポートで通信したいのに、なぜか拒否される」といった場面に遭遇することがあります。 こうしたときに周囲に相談すると、「それ、プロキシの問題じゃない?」と言われることも少なくありません。 ひとまず「なるほど」と頷きつつ対処をしてみますが、そもそもプロキシとは何をしているものなのでしょうか? ざっくりと説明 自分の中で整理した結果、プロキシとは 「通信の一元的な制御と監査・接続元情報の隠蔽(・キャッシュによる高速化)を目的とした通信中継用サーバ」 と考えています。 よく使用されるプロキシ(Webプロキシ)を基に作成したイメージは以下となります。 指定の プロトコル (HTTP/ HTTPS )の通信時、 クライアントは名前解決をせずプロキシにリクエストを送信 します。その後、プロキシによって DNS の名前解決を行い、プロキシサーバとクライアント・プロキシサーバと接続先のサーバでそれぞれ 個別に通信が確立 し、中継が行われる仕組みとなっています。 上記の説明だけではわかりづらい部分もあるため、混同しやすい他の用語と比較しながらその特徴をつかんでいきます。 プロキシの位置づけ 個人的に混同しやすいと感じたのは、NATやNAPT機器、パケットインスペクションになります。 例えば、 AWS においてNAT Gateway を使用した場合は、アウトバウンド通信をする際にパケットのIPやポートを書き換えることで、内部のネットワーク空間つまり接続元のIP情報を隠蔽する役割があるといえますし、Network Firewall を使用すればルートテーブルの設定をすることで通信を統一的に制御・監査することが出来ます。 では、プロキシとこれらのサービスの違いはどこにあるのでしょうか。 結論としては、目的とそれに伴い対応するレイヤー、中継の性質が異なります。 以下の表に各サービスの情報を記載いたします。 用語 レイヤー 目的 中継レベル 対応する AWS サービス NAT, NAPT L3-4(IP / Port) アウトバウンド通信の成立、それに伴うプライベートIPの隠蔽(NAPTの場合、 グローバルIP の使用効率向上) アウトバウンド用。パケットのIPやポート情報を書き換えるのみで、クライアントと接続先サーバは 任意の4層以上の プロトコル で直接セッションを確立 しやり取り。 NAT ゲートウェイ パケットインスペクション L3-7(IP / Port, UDP / TCP 対応) 通信内容に基づくセキュリティ制御・検知 インバウンド/アウトバウンド用。通信内容を書き換えず、 通過する全パケットを透過的に検査・制御。 AWS Network Firewall , AWS Gateway Load Balancer + サードパーティ ソフト リバースプロキシ DNS + L3-7(IP / Port, HTTP / TLS 対応) 外部公開サービスの集約・防御・負荷分散 インバウンド用。パケットの書き換えではなく、プロキシが通信主体となり、クライアントとプロキシ・プロキシと接続先サーバとの間で 個別の通信(指定の4層以上の プロトコル )を確立。 Amazon CloudFront , Elastic Load Balancing(ELB), Amazon API Gateway フォワ ードプロキシ DNS + L3-7(IP / Port, HTTP / TLS 対応) アウトバウンド通信の一元的な制御・監査 アウトバウンド用。パケットの書き換えではなく、プロキシが通信主体となり、クライアントとプロキシ・プロキシと接続先サーバとの間で 個別の通信(指定の4層以上の プロトコル )を確立。 ※ ただし、クライアントの設定によりプロキシを回避可能。 AWS Network Firewall Proxy ※ 表を簡易にするため、SOCKS などの L4 プロキシ(主に TCP コネクションの中継のみで、アプリケーションレイヤの制御をしないもの)は本表では対象外としました。 表に記載しましたが、プロキシはインバウンド/アウトバウンドの制御によってそれぞれリバースプロキシ/ フォワ ードプロキシとも呼ばれます。 このように、NAT・パケットインスペクション・プロキシは、それぞれ得意とするレイヤーや適用範囲が異なります。 また中継の性質として、 L4以上のセッションを個別に確立する点がプロキシの大きな特徴である と整理できます。 AWS re:Invent 2025 - AWS Network Firewall Proxy (NET216) ではクライアントと接続先サーバそれぞれ個別に通信を確立するプロキシを明示的なプロキシ(Explicit Proxy)と表現し、パケットインスペクションのような透過的な制御ツールを透過的なプロキシ(Transparent Proxy)と表現していました。 一方で、業務上「プロキシ」と呼ばれるものは、通信の中継主体となる 明示的なプロキシ( フォワ ードプロキシ/リバースプロキシ)を指すケースがほとんどである印象を受けます。 さらに近年では、Network Firewall 自体にも、 TLSインスペクション という、プロキシのように TLS 通信を一度終端して内容を検査する機能が追加されており、用語と実装の対応関係が直感的に分かりづらくなっていると感じます。 AWS Network Firewall Proxyの概要 AWS Network Firewall Proxy はいわゆるWebプロキシと同様に、 HTTP / HTTPS 通信専用のマネージドな フォワ ードプロキシサービス であり、Route 53 Resolver DNS Firewall や Network Firewall と同様にアウトバウンドセキュリティを構成するための AWS サービスの一つです。 現在プレビュー提供中であり、料金は無料となっていますが、利用可能なリージョンは オハイオ リージョン(us-east-2)のみに限定されています。 アーキテクチャ 以下の構成( Securing Egress Architectures with Network Firewall Proxy より引用)に示すように NAT ゲートウェイ と統合されたプロキシが作成され、クライアントは専用の インターフェース VPC エンドポイント( AWS PrivateLink)を経由してプロキシへアクセス する構成となっています。 プロキシの作成時、プロキシを紐づけたNAT ゲートウェイ と同じサブネットに、 VPC エンドポイントが自動的に1つ作成されます。 任意で他のサブネットや他の VPC に VPC エンドポイントを作成可能なため、マルチ VPC 構成においても以下の図( Securing Egress Architectures with Network Firewall Proxy より引用)に示すように VPC エンドポイント経由での使用が可能となります。 (もちろん、PrivateLinkを用いたアクセスではなく、Cloud WANやTransit Gateway を使用する構成も可能です。) 一方、 AWS Network Firewall Proxy が登場する以前に想定されていたユーザマネージドなプロキシ構成では、小規模なサービスであれば単体の EC2 で実装したプロキシでも対応可能ですが、可用性や性能を考慮する場合には Auto Scaling 付きの EC2 を配置し、NLB によって負荷分散を行う必要があり、運用負荷の高い構成となりがちでした。 今回マネージドなプロキシが提供されたことで、少なくともプロキシ基盤そのものに関するインフラ管理の負荷は大きく軽減されたと感じます。 動作イメージ 本サービスでは、以下の図( Securing Egress Architectures with Network Firewall Proxy より引用)に示すように 名前解決前(Pre- DNS )、名前解決後の接続先サーバへのHTTPリクエスト(Pre-request)、HTTPレスポンス(Post-response)の3段階 で評価を行います。 また、名前解決に使用する ドメイン 名からL3-L7(IP / Port, HTTP / TLS ヘッダ) までの情報を評価に使用可能です。 ただし、上記の評価フローは TLS インターセプト ( Squid における SSL Bump)と呼ばれる方式を有効化した場合の挙動を示しています。 TLS インターセプト では、クライアントと接続先サーバ間で直接 TLS セッションを確立するのではなく、 クライアントとプロキシ、プロキシと接続先サーバの間でそれぞれ独立した TLS セッションを確立 します。 TLS インターセプト の実体はシンプルです。外部公開するアプリケーションが、信頼された CA により発行された証明書を用いて TLS 通信を行うのと同様に、Network Firewall Proxy はプライベート CA を使用し、初めて通信が発生した宛先 ドメイン に対して証明書を動的に発行します。 クライアント側は上記のプライベート CA を信頼している(CAの公開鍵を持つ)ことで、クライアントとプロキシ間の TLS 通信が成立します。 詳細な設定手順については後述します。 一方で、以下の図( Securing Egress Architectures with Network Firewall Proxy より引用)に示すように、 TLS インターセプト を無効化した場合には、Network Firewall Proxy を経由して TCP セッションは個別に確立 されるものの、 TLS セッションおよび HTTP セッションは クライアントと接続先サーバ間でエンドツーエンドに確立 されます。 そのため、 TLS インターセプト を無効化した場合、 TLS 通信の完全性と機密性をエンドツーエンドに維持することができますが、評価に利用できる情報は ドメイン 名、IP、ポート、SNI ヘッダなどの暗号化されていない情報 に限定されます。 AWS Network Firewall Proxyに関わるリソースの詳細 Network Firewall Proxyで使用するリソースの構造は以下の図に示すようになっています。 各リソースの詳細に関して説明します。 ルールグループ・ルール ルールには、それぞれ 評価フェーズ(Pre- DNS / Pre-request / Post-response)・評価条件・Allow/Deny/Alertアクション を設定します。(ルールにマッチしなかった通信は、後述するプロキシ設定のデフォルトアクションに基づき評価されます。) 一方で、ルールグループはルールを複数(最大1000個)設定できる単なる箱としてのリソースとなります。 ルールの評価条件として使用できる条件キーを以下に示します。 下線付きの条件キー は、 TLS インターセプト が無効化の状態でも使用可能な値となります(HTTPヘッダの値は TLS により暗号化されるため)。 リクエスト request:SourceAccount :送信元の AWS アカウントID request:SourceVpc :送信元のVPCID request:SourceVpce :送信元の VPC エンドポイントID request:Time request:SourceIp request:DestinationIp request:SourcePort request:DestinationPort request:Protocol request:DestinationDomain : DNS クエリ or SNI request:Http: Uri request:Http:Method request:Http:UserAgent request:Http:ContentType request:Http:Header/{CustomHeaderName} レスポンス response:Http:StatusCode response:Http:ContentType response:Http:Header/{CustomHeaderName} プロキシ設定・プロキシ プロキシ設定はそれ自体が AWS リソースとして作成されるものであり、これをプロキシの作成時に紐付けます。 プロキシ設定の作成時には、 複数のルールグループを紐付けつつ、デフォルトアクション(ルールにマッチしなかった通信へのアクション)をAllow/Deny/Alertで設定 します。 プロキシの作成時には、 プロキシ設定やNAT ゲートウェイ 、評価フェーズ毎のログ出力先、 TLS インターセプト を使用する場合はプライベートCA を設定します。 実際に通信を評価するプロセスを、プロキシ設定の例を用いて以下に示します。 ルールグループやルールの優先順と評価フェーズの順序が存在する点に注意が必要です。 AWS Network Firewall Proxy の検証 前提 検証のためローカルで実装します。ただし、Network Firewall Proxy用のTerraform Providerが未実装のため、一部手動で設定します。 概要で紹介した以下の構成( Securing Egress Architectures with Network Firewall Proxy より引用)を作成します。 EC2をプライベートサブネットに配置し、NAT ゲートウェイ +Network Firewall Proxy経由での AWS サービス・外部サービスへのアクセスを想定。 Network Firewall Proxyの設定 Pre- DNS 、Pre-request、Post-Responseの3段階それぞれの評価を設定し、動作を確認します。(デフォルトアクションは全て Allow) Pre- DNS : ドメイン tech.dentsusoken.com をDeny。 Pre-Request: URI */files/* をDeny(StringLike)。 Post-Response:レスポンスステータス 404 をDeny。 TLS インターセプト を有効化します。 本検証では、公開されている Web サイトに対して、 通信制 御の挙動を確認する目的で少数のリクエストを送信しており、サービスの可用性や機密性に影響を与える行為は行っていません。 本記事では、検証のシンプルさや負荷の低さ(数リクエスト程度)からテックブログ用 ドメイン による検証を行いました。 しかし、本来はテスト手法に問題があり予期せず過大な負荷をかけてしまった場合など、 規約違反 となる可能性があるため、 IANA(現在はICANNの一部)が管理するテスト用ドメイン ( example.com 等)を使用し検証を行うことが最も適切となります。 Terraformの実装 VPC 、NAT ゲートウェイ 、検証用のEC2を以下のように定義します。 ただし、 TLS インターセプト の有効化に伴いクライアントEC2側には、プロキシの動的な証明書作成に使用するプライベートCAの親、つまり ルートCAへの信頼( トラストアンカー ) を設定する必要があります。 そのため、後述するプライベートCAの作成時に生成される ルート証明書 を、user_dataを使用して インスタンス 起動時に自動的にトラストストアに設定されるようにShellコマンドを記載しています。 これまで皆さんが google.com や yahoo.co.jp などにアクセスする際、サイトの証明書が信頼できるCAにより発行されたものとして認識され、意識せずとも TLS 接続が成立していたのは、 OS(PC)にあらかじめ設定されたトラストストア内に、主要な公開CAの証明書が含まれていたためです。 今回も同様に、プロキシが利用するプライベートCAの証明書をトラストストアへ登録することで、クライアントはプロキシが生成した証明書を信頼できるようになります。 terraform { required_version = "~> 1.14.0" required_providers { aws = { version = "6.23.0" source = "hashicorp/aws" } } } provider "aws" { region = local.regions.primary } locals { account_id = "677276073034" regions = { primary = "us-east-2" } availability_zones = { primary = [ "$ { local.regions.primary } a" , "$ { local.regions.primary } b" , "$ { local.regions.primary } c" ] } } data "aws_caller_identity" "current" {} ########################################################################################### # VPC ########################################################################################### ## 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 = local.availability_zones.primary [ 0 ] 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 = local.availability_zones.primary [ 1 ] 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" } } ########################################################################################### # EC2 ########################################################################################### ## 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, ] user_data = <<-EOF #!/bin/bash cat > /etc/pki/ca-trust/source/anchors/nfw-proxy-root-ca.pem <<PEM-EOF $ { aws_acmpca_certificate.root.certificate } PEM -EOF update-ca-trust EOF 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" } TLS インターセプト 設定用のプライベートCAおよびログ出力先として使用するCloudWatchロググループを定義します。 ただし、プライベートCAに関しては以下の方針で作成します。 ベストプラクティス に基づきルートCAと中間CAを作成し、プロキシ作成時には中間CAを指定する。 ただし、プロキシは中間CAから直接証明書を発行するのではなく、 プロキシ内部で中間CAから派生させた「Proxy専用の下位CA(PathLen0)」を自動生成し、以降の動的 サーバ証明書 はこのProxy専用CAから発行 する仕様と考えられる。 そのため、中間CAには下位CAを1段作成できる設定( SubordinateCACertificate_PathLen1/V1 )を適用する。 また、プロキシが中間CAを利用してProxy専用下位CA(派生不可)を作成できるよう、 AWS RAM による共有( AWSRAMSubordinateCACertificatePathLen0IssuanceCertificateAuthority )を設定する。 使用する鍵 アルゴリズム や署名 アルゴリズム については、 AWS コンソールから作成した場合のデフォルト値(KeyAlgorithm: RSA _2048、SigningAlgorithm:SHA256WITHRSA)を使用。 リソースポリシーを使用し、プライベートCAへのアクセスは指定のプロキシ(サービス プリンシパル : proxy.network-firewall.amazonaws.com 、プロキシ名: example-proxy )のみに限定( 公式ドキュメント )。 また、プライベートCA用の各Terraformリソースの概要を以下に示します。 aws _acmpca_certificate_authority プライベート CA(ROOT / SUBORDINATE)の本体。 ルートCAの情報をクライアントEC2のトラストストアに登録(トラストアンカー)。 aws _acmpca_certificate CA 証明書(ROOTの場合は自己署名、SUBORDINATEの場合は親CAが署名して発行)。 aws _acmpca_certificate_authority_certificate 発行されたCA証明書をCAにインポートして有効化。 aws _acmpca_permission ACM 専用のプライベートCAに対する権限設定。 ACM による証明書の自動更新向け。 aws _acmpca_policy IAMリソースポリシー。 ## Private Certificate Authority (PCA) - Certificate Authority ### https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/acmpca_certificate_authority resource "aws_acmpca_certificate_authority" "root" { type = "ROOT" usage_mode = "GENERAL_PURPOSE" certificate_authority_configuration { key_algorithm = "RSA_2048" signing_algorithm = "SHA256WITHRSA" subject { common_name = "nfw-proxy-root-ca.internal.root" country = "JP" organization = "Example Org" } } key_storage_security_standard = "FIPS_140_2_LEVEL_3_OR_HIGHER" permanent_deletion_time_in_days = 30 # 削除後復旧可能な日数:7-30日 } resource "aws_acmpca_certificate_authority" "example" { type = "SUBORDINATE" usage_mode = "GENERAL_PURPOSE" certificate_authority_configuration { key_algorithm = "RSA_2048" signing_algorithm = "SHA256WITHRSA" subject { common_name = "nfw-proxy-root-ca.internal.middle" country = "JP" organization = "Example Org" } } key_storage_security_standard = "FIPS_140_2_LEVEL_3_OR_HIGHER" permanent_deletion_time_in_days = 30 # 削除後復旧可能な日数:7-30日 } ## PCA Certificate ### https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/acmpca_certificate resource "aws_acmpca_certificate" "root" { certificate_authority_arn = aws_acmpca_certificate_authority.root.arn certificate_signing_request = aws_acmpca_certificate_authority.root.certificate_signing_request signing_algorithm = "SHA256WITHRSA" # https://docs.aws.amazon.com/ja_jp/privateca/latest/userguide/template-definitions.html#RootCACertificate-V1 template_arn = "arn:aws:acm-pca:::template/RootCACertificate/V1" validity { type = "YEARS" value = 2 # ValidationException: The certificate validity specified exceeds the certificate authority validity.に注意 } } resource "aws_acmpca_certificate" "example" { depends_on = [ aws_acmpca_certificate_authority_certificate.root ] certificate_authority_arn = aws_acmpca_certificate_authority.root.arn certificate_signing_request = aws_acmpca_certificate_authority.example.certificate_signing_request signing_algorithm = "SHA256WITHRSA" # https://docs.aws.amazon.com/ja_jp/privateca/latest/userguide/template-definitions.html#SubordinateCACertificate_PathLen1-V1 template_arn = "arn:aws:acm-pca:::template/SubordinateCACertificate_PathLen1/V1" validity { type = "YEARS" value = 1 } } ## PCA Certificate Authority Certificate ### https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/acmpca_certificate_authority_certificate resource "aws_acmpca_certificate_authority_certificate" "root" { certificate_authority_arn = aws_acmpca_certificate_authority.root.arn certificate = aws_acmpca_certificate.root.certificate certificate_chain = aws_acmpca_certificate.root.certificate_chain } resource "aws_acmpca_certificate_authority_certificate" "example" { certificate_authority_arn = aws_acmpca_certificate_authority.example.arn certificate = aws_acmpca_certificate.example.certificate certificate_chain = aws_acmpca_certificate.example.certificate_chain } ## PCA Permission ### https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/acmpca_permission resource "aws_acmpca_permission" "root" { certificate_authority_arn = aws_acmpca_certificate_authority.root.arn actions = [ "IssueCertificate" , "GetCertificate" , "ListPermissions" ] principal = "acm.amazonaws.com" } resource "aws_acmpca_permission" "example" { certificate_authority_arn = aws_acmpca_certificate_authority.example.arn actions = [ "IssueCertificate" , "GetCertificate" , "ListPermissions" ] principal = "acm.amazonaws.com" } ## PCA Resource Policy ### https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/acmpca_policy resource "aws_acmpca_policy" "example" { resource_arn = aws_acmpca_certificate_authority.example.arn policy = jsonencode ( { "Version" : "2012-10-17" , "Statement" : [ { "Effect" : "Allow" , "Principal" : { "Service" : "proxy.network-firewall.amazonaws.com" } , "Resource" : "$ { aws_acmpca_certificate_authority.example.arn } " , "Action" : [ "acm-pca:GetCertificate" , "acm-pca:DescribeCertificateAuthority" , "acm-pca:GetCertificateAuthorityCertificate" , "acm-pca:ListTags" , "acm-pca:ListPermissions" ] , "Condition" : { "ArnEquals" : { "aws:SourceArn" : "arn:aws:network-firewall:$ { local.regions.primary } :$ { local.account_id } :proxy/example-proxy" } } } , { "Effect" : "Allow" , "Principal" : { "Service" : "proxy.network-firewall.amazonaws.com" } , "Action" : [ "acm-pca:IssueCertificate" ] , "Resource" : "$ { aws_acmpca_certificate_authority.example.arn } " , "Condition" : { "StringEquals" : { "acm-pca:TemplateArn" : "arn:aws:acm-pca:::template/SubordinateCACertificate_PathLen0/V1" } , "ArnEquals" : { "aws:SourceArn" : "arn:aws:network-firewall:$ { local.regions.primary } :$ { local.account_id } :proxy/example-proxy" } } } ] } ) } ## Log resource "aws_cloudwatch_log_group" "example" { name = "/aws/vpc/network-firewall/proxy/example" tags = { Project = "example" } } 手動設定 上記のTerraformリソースをapplyした後に、 AWS コンソールからNetwork Firewall Proxyの設定を行います。 ルールグループ・ルールの作成 以下のルールを含むルールグループを作成します。 Pre- DNS : ドメイン tech.dentsusoken.com をDeny。 Pre-Request: URI */files/* をDeny(StringLike)。 Post-Response:レスポンスステータス 404 をDeny。 VPC コンソールのプロキシルールグループ画面より、「プロキシルールグループを作成」ボタンからルールグループを作成します。 ルールグループの作成時は単一のルールのみ作成可能なため、まずはPre- DNS のルールを作成します。 フェーズとアクションを選択し、「条件を追加」ボタンから条件キーと値を設定します 「作成」ボタンを押して作成を完了します。 「条件を追加」ボタンから、Pre-RequestとPost-Responseの条件を同様に追加します。 結果、以下のルールが設定できれば完了です。 プロキシ設定の作成 VPC コンソールのプロキシ設定画面より、「プロキシ設定を作成」ボタンからプロキシ設定を作成します。 デフォルトアクションとして、Pre- DNS / Pre-request / Post-Response全てにAllowを設定します。 「ルールグループをアタッチ」ボタンから、先ほど作成したルールグループを選択します。 「作成」ボタンを押して作成を完了します。 プロキシの作成 VPC コンソールのプロキシ画面より、「プロキシを作成」ボタンからプロキシを作成します。 以下のパラメータを設定し、「作成」ボタンを押して作成を完了します。 名前: example-proxy (Terraform実装にてプライベートCAのリソースポリシーに指定したプロキシ名) プロキシ設定:本章で作成したプロキシ設定 NAT ゲートウェイ :TerraformによりデプロイしたNAT ゲートウェイ プライベート証明書:Terraformによりデプロイした 中間CA ポート番号(クライアントがプロキシにアクセスする際の、プロキシ側のリスニングするポート番号) HTTPS :443 HTTP:1080 ログ配信 Pre- DNS / Pre-Request / Post-Request :TerraformによりデプロイしたCloudWatchロググループ 作成後のアタッチ処理は 10 分程度で完了します。 ここで設定の不備によりアタッチが失敗した場合、「失敗メッセージ」項目にエラー内容が表示されます。 Internal Error 例)中間CAをこれ以上派生不可な設定(PathLen0)で作成していた場合に、プロキシが内部でProxy専用下位CAを生成できず発生。 Access Denied 例)プライベートCAのリソースベースポリシーが、作成したプロキシからのアクセスを許可していない場合に発生。 VPC エンドポイントのセキュリティグループ修正 先ほどのプロキシ作成に伴い、自動で VPC エンドポイントが作成されています。 しかし、以下に示すように VPC エンドポイントに付与されたセキュリティグループはデフォルト設定であり、クライアントEC2からのアクセスに非対応となっています。 そのため、インバウンドルールにEC2セキュリティグループからのポート443への通信の許可を追加します。 EC2へのプロキシ設定追加 EC2にSSM接続を行い、プロキシを経由するように設定します。 念のため、Terraformで定義したとおり、クライアントEC2に正しくルートCAに対するトラストアンカーが設定されていることを、 trust list コマンド で確認します。 sh-5.2$ trust list | grep nfw-proxy-root-ca -C 2 pkcs11:id=%BE%58%DE%C4%B8%D2%49%D3%05%8F%DF%C6%2F%0E%1C%D7%7B%4A%BF%05;type=cert type: certificate label: nfw-proxy-root-ca.internal.root trust: anchor category: authority sh-5.2$ 次に、作成したプロキシのプライベート DNS 名・ポート番号を使用して、 http_proxy ・ https_proxy 環境変数 を設定します。(プロキシとEC2間の通信を保護するために、HTTPプロキシに対しても HTTPS 用のエンドポイントを指定) これらの 環境変数 を設定することで、 クライアントからのHTTPおよび HTTPS 通信のみ が、指定したプロキシエンドポイント経由で送信されるようになります。 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$ 動作検証 リソースが全てデプロイできたため、検証に入ります。 アクセスを制限していない https://www.dentsusoken.com へのアクセスは、以下のように許可されました。 sh-5.2$ curl https://www.dentsusoken.com <!DOCTYPE html> (中略) <meta property="og:site_name" content="電通総研" /> <meta property="og:type" content="article" /> <meta property="og:url" content="https://www.dentsusoken.com/top" /> (中略) </html> Pre- DNS のDenyルールにマッチする通信 https://tech.dentsusoken.com の場合、以下のように403レスポンスが返ってきました。 sh-5.2$ curl -v https://tech.dentsusoken.com * 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): (中略) < * CONNECT tunnel failed, response 403 * closing connection #0 curl: (56) CONNECT tunnel failed, response 403 CloudWatch ログでは、拒否(Deny)された通信について、 final_rule_name および final_rule_group_name により、マッチしたルールが特定できるログが出力されていることを確認できました。 { " event_timestamp ": 1767152710 , " proxy_name ": " example-proxy ", " client_src_ip ": " 10.0.2.23 ", " final_action ": " deny ", " src_vpc ": " vpc-0ee3a513a9f07e326 ", " dest_domain ": " tech.dentsusoken.com. ", " http_method ": "", " dest_ip ": " <nil> ", " http_status_code ": -1 , " final_rule_name ": " test-dns ", " final_rule_group_name ": " example-rule-group " } Pre-Request の Deny ルールにマッチする通信 https://www.dentsusoken.com/files/ の場合、以下のように403レスポンスが返ってきました。 sh-5.2$ curl -v https://www.dentsusoken.com/files/ * 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): (中略) > * TLSv1.3 (IN), TLS handshake, Newsession Ticket (4): * Request completely sent off < HTTP/1.1 403 Forbidden < 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): 想定通りPre-Request用のルールにマッチしていることが確認できました。 { " event_timestamp ": 1767152715 , " proxy_name ": " example-proxy ", " client_src_ip ": " 10.0.2.23 ", " final_action ": " deny ", " src_vpc ": " vpc-0ee3a513a9f07e326 ", " dest_domain ": " www.dentsusoken.com. ", " http_method ": " GET ", " dest_ip ": " 3.160.22.80 ", " http_status_code ": -1 , " final_rule_name ": " test-uri ", " final_rule_group_name ": " example-rule-group " } Post-Response の Deny ルールにマッチする通信 https://httpbin.org/status/404 ( httpbin )の場合、以下のように403レスポンスが返ってきました。 sh-5.2$ curl -v https://httpbin.org/status/404 * 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): (中略) * TLSv1.3 (IN), TLS handshake, Newsession Ticket (4): * Request completely sent off < HTTP/1.1 403 Forbidden < 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$ 想定通りPost-Response用のルールにマッチしていることが確認できました。 { " event_timestamp ": 1767152721 , " proxy_name ": " example-proxy ", " client_src_ip ": " 10.0.2.23 ", " final_action ": " deny ", " src_vpc ": " vpc-0ee3a513a9f07e326 ", " dest_domain ": " httpbin.org. ", " http_method ": " GET ", " dest_ip ": " 23.21.107.74 ", " http_status_code ": 404 , " final_rule_name ": " test-status ", " final_rule_group_name ": " example-rule-group " } ただし、本検証のように Network Firewall Proxy を単体で利用している場合、 --noproxy オプション等によりクライアント側でプロキシ設定を無効化すると、プロキシ経由の制御を回避して通信が成立し得ます。 そのため、 Network Firewall Proxy 単体での運用というよりも、 ファイアウォール 系サービスやNetwork Firewall を組み合わせることによって、監査面とセキュリティを両立できると感じました。 まとめ Network Firewall Proxy は、Network Firewall や Route 53 Resolver DNS Firewall と並ぶ AWS におけるアウトバウンドセキュリティの選択肢の一つであり、 HTTP/ HTTPS のアウトバウンド制御を、プロキシ型でマネージドに実現 するサービスです。 実際に検証してみると、 アーキテクチャ がシンプルかつマネージドであるため構築・運用の負荷は低い一方、未発表である正式リリース時のコストや、 TLS インターセプト を利用する場合の証明書管理など、運用面で考慮すべき点もあることが分かりました。 他のアウトバウンドセキュリティサービスとの要件に応じた使い分けとしては、 ドメイン ベースの制御には Route 53 Resolver DNS Firewall 、 HTTP/ HTTPS に限定した 通信制 御には Network Firewall Proxy 、 UDP / TCP を含めたより広範な通信評価が必要な場合には Network Firewall を主軸にして組み合わせていく、といった使い分けが考えられます。 ただし、前章で触れた通り、プロキシはクライアント側の設定によって回避可能であるため、 Network Firewall Proxy を単体で利用するのではなく、Network Firewall 等と組み合わせてネットワークレベルで制御することで、監査面とセキュリティを両立する設計が重要 と感じます。 おわりに 本記事では、プロキシの基本的な役割を整理したうえで、 AWS Network Firewall Proxy の概要と挙動について実際に検証を行いました。 正式リリースや今後の更新によって、マネージドの ドメイン リストやIPリストも増えていくととても嬉しいですね。 また、アウトバウンドセキュリティに関して理解できた部分も増えてきましたが、理論として説明できていても感覚的に落とし込めていない部分はまだ多く残っているため、実際の業務経験を通じて検証と運用を重ねながら理解を深めていきたいと思います。 私たちは一緒に働いてくれる仲間を募集しています! 電通総研 キャリア採用サイト 電通総研 新卒採用サイト 執筆: @sakae.katsuto レビュー: @akutsu.masahiro ( Shodo で執筆されました )
アバター
はいどーもー! コーポレート本部の宮澤響です! (部署が変わりました!) 本記事では、昨年に引き続き、大学入学共通テストの「情報Ⅰ」の試験問題を解いてみた個人的な感想をお伝えします! なお、昨年の記事は こちら ! 問題・正解 全体の感想 問題ごとの感想 大問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 勉強会を開催しました。この記事では、その勉強会の背景や内容をご紹介します。 背景 目的 内容 気づき・参加者の反応 開催してみての気づき 参加者の反応 まとめ・今後の展望 背景 とある外部の勉強会にて、 NEC ソリューションイノベータ株式会社の上田賢哉様、キャップ ジェミニ 株式会社の遊佐康平様と出会いました。お二人とも新卒1年目です。 その勉強会の休憩時間に私から「3社合同で1年目だけの AWS 勉強会をやってみませんか」と提案したところ、その場でお二人とも賛同してくれました。正直なところ、その時は話を合わせてくれているだけかなと思っていたのですが、翌日にお二人から「勉強会をやりましょう」というメールをいただいたので、これは本当に実現させようと思い、開催に至りました。 私が勉強会を提案した理由は、近い年齢の方々との勉強会が有意義だと考えているからです。 私は社内で同期8人と隔週で勉強会を実施しています。同期同士だと遠慮なく疑問をぶつけ合えますし、近い年齢だからこそ得られる刺激もあります。この社内勉強会が良い場になっているので、会社の枠を超えてやってみたらもっと面白いんじゃないか?と思い、提案しました。 ( AWS である理由は、お二人と出会った勉強会が AWS 関連の勉強会だったからという単純な理由です。) 目的 勉強会の一番の目的は、 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 で執筆されました )
アバター