AgentCore GatewayのAgent Targetを試してみた

AgentCore GatewayのAgent Targetを試してみた

AgentCore GatewayのAgent Targetも触って試してみました!Gatewayでできることがすごく増えていますね・・・!!
2026.07.08

はじめに

こんにちは、ドラム式洗濯機が気になるコンサル部の神野(じんの)です。

直近の2本の記事でAgentCore Gatewayの新機能を紹介してきました。

https://dev.classmethod.jp/articles/agentcore-gateway-inference-target/

https://dev.classmethod.jp/articles/agentcore-policy-guardrails-gateway/

1本目ではLLMプロバイダーをGatewayの背後に置くInference Target、2本目ではCedarポリシーによるGuardrailsを紹介しましたが、その中で予告していたAgent Targetを今回は深掘りします!

Agent Targetを使うと、AgentCore Runtime上のエージェントそのものをGatewayの背後に置けるようになります。下記のようなイメージですね。

CleanShot 2026-07-07 at 09.05.54@2x

今回はせっかくなので、A2Aプロトコルのエージェント2体を1つのGatewayに紐づけて、ローカルのエージェントからGateway経由でリモートの専門エージェント2体に問い合わせを委譲するマルチエージェント構成を構築してみます。

Agent Targetの挙動

まずAgent Targetがどういう位置づけなのかを整理しておきます。

現在のGatewayのターゲットは3つのカテゴリに分類されています。

カテゴリ 動作モード ターゲットの例
MCPターゲット 集約モード。全ターゲットのツールを1つの仮想MCPサーバーに統合 Lambda、OpenAPI、Smithy、MCPサーバーなど
HTTPターゲット 直接転送モード。集約もプロトコル変換もせずそのままプロキシ AgentCore Runtimeエージェント、A2Aサービスなど
Inferenceターゲット モデルベースルーティング。modelの値でプロバイダーを振り分け Bedrock、OpenAI、Anthropic、カスタムプロバイダーなど

なお、コンソールのターゲット追加画面ではMCP / Inference / Agent / Customの4択に見えますが、AgentとCustomはどちらもAPI上はHTTPターゲット(それぞれagentcoreRuntime型とpassthrough型)です。

Agent TargetはこのHTTPターゲットに属します。公式ドキュメントには下記のように記載があります。

The gateway sends traffic directly to the runtime agent without aggregation or protocol translation.

https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-target-http-runtime.html

つまりGatewayは無変換のプロキシとして振る舞います。MCPターゲットのようにtools/listを統合したりはせず、クライアントはパスベースルーティングで各ターゲットを個別に呼び分けます。

エンドポイントの構造
https://{GatewayのID}.gateway.bedrock-agentcore.{リージョン}.amazonaws.com/{ターゲット名}/invocations

RuntimeのARNを指すだけのシンプルな構造で、GatewayがRuntimeのエンドポイントを内部で解決してくれます。

Agent Targetの設定構造
{
  "http": {
    "agentcoreRuntime": {
      "arn": "arn:aws:bedrock-agentcore:us-east-1:<アカウントID>:runtime/RUNTIME_ID",
      "qualifier": "DEFAULT"
    }
  }
}

Runtime側のプロトコルを問いません。RuntimeはHTTP / A2Aなどのプロトコルをサポートしていますが、Agent TargetにA2A専用の設定はなく、A2AのJSON-RPCもそのまま素通しされます。前回のGuardrailsで使ったポリシーエンジン用のスキーマも、MCPまたはA2AプロトコルのRuntimeならデフォルトスキーマが自動適用されると明記されています。

For runtime agents that use MCP or A2A protocols, a default schema is applied automatically and you don't need to provide one.

逆にHTTPプロトコルのRuntimeでGuardrailsを使う場合はスキーマの指定が必要、とされています。ただこのスキーマ周りは、実際に試すとドキュメント通りにいかない部分がありました。補足2で深掘りします。

エージェントをGatewayの背後に置いて何が嬉しいのかというと、公式ドキュメントでは下記が挙げられています。

  • 複数のRuntimeエージェントへのアクセスを1つのGatewayエンドポイントに集約できる
  • Gatewayの認証・オブザーバビリティ・ポリシー(前回のGuardrails!)をエージェントの外側で適用できる
  • パスベースルーティングで複数エージェントを呼び分けられる
  • Gateway経由のトラフィックを使ったA/Bテストやエージェント最適化に使える

エージェントのGatewayとして機能する立ち位置ですね。

細かい特性としては、SSEストリーミングに対応していること、リクエスト・レスポンスを加工するインターセプターLambdaはバッファーモードのみ対応(ストリーミングでは未対応)なことも押さえておくと良さそうです。

今回作る構成

ECサイトのカスタマーサポートを題材に、下記の構成を作ります。

  • ローカル:一次受付(トリアージ)エージェント。問い合わせを分解して専門エージェントに委譲し、結果を統合して回答する
  • リモート1:OrderAgent(注文・配送担当)。注文DBを照会するツールを持つ
  • リモート2:TechAgent(テクニカルサポート担当)。製品ナレッジを検索するツールを持つ

リモートの2体はA2AプロトコルでRuntimeにデプロイし、1つのGatewayにAgent Targetとしてぶら下げます。ローカルのエージェントはGateway経由でしかリモートエージェントと会話しません。

「注文した掃除機がまだ届かない。あと届いたらすぐ使いたいからWi-Fi設定も教えて」のような複合的な問い合わせを、2体の専門エージェントへの委譲で解決する流れです。

CleanShot 2026-07-07 at 09.10.11@2x

前提

  • AWSアカウントと認証設定済みのAWS CLI(バージニア北部リージョンを使用)
  • CDKブートストラップ済みの環境(us-east-1)
  • AgentCore CLI(今回は1.0.0-preview.16を使用)
  • uv

AgentCore CLIは下記でインストールできます。

実行コマンド
npm install -g @aws/agentcore
agentcore --version

構築

プロジェクトを作成する

まずは空のプロジェクトを作成します。エージェントは後から2体追加するので--no-agentを付けています。

実行コマンド
agentcore create --name AgentTargetDemo --no-agent
cd AgentTargetDemo

A2Aエージェントを2体追加する

専門エージェント2体を追加します。ポイントは--protocol A2Aです。

実行コマンド
agentcore add agent --name OrderAgent --language Python --framework Strands \
  --model-provider Bedrock --memory none --protocol A2A

agentcore add agent --name TechAgent --language Python --framework Strands \
  --model-provider Bedrock --memory none --protocol A2A

生成されたテンプレートを見ると、A2Aプロトコルの場合はStrandsのエージェントをStrandsA2AExecutorでラップして、serve_a2aでA2Aサーバーとして起動する構成になっています。RuntimeのA2Aプロトコルはポート9000のルートパスでA2Aサーバーが動くことを期待する仕様で、その辺りはserve_a2aが面倒を見てくれます。

OrderAgentの実装がこちらです。テンプレートのツールとシステムプロンプトを差し替えて、注文DBのモックを照会するエージェントにしています。

app/OrderAgent/main.py
from strands import Agent, tool
from strands.multiagent.a2a.executor import StrandsA2AExecutor
from bedrock_agentcore.runtime import serve_a2a
from model.load import load_model

# 注文DBのモック
ORDERS = {
    "ORD-1001": {"item": "スマートスピーカー Echo Mini", "status": "配達済み", "delivered_at": "2026-07-01"},
    "ORD-1002": {"item": "ロボット掃除機 CleanBot X", "status": "配送中", "estimated_delivery": "2026-07-09", "carrier": "クラメソ急便", "tracking_id": "CM-4471-8829"},
    "ORD-1003": {"item": "ワイヤレスイヤホン AirSound Pro", "status": "配送遅延", "delay_reason": "配送センターの混雑", "estimated_delivery": "2026-07-12"},
}

@tool
def lookup_order(order_id: str) -> str:
    """注文IDから注文情報(商品名・配送状況・お届け予定日など)を照会する。"""
    order = ORDERS.get(order_id.upper())
    if not order:
        return f"注文 {order_id} が見つかりません。注文IDをご確認ください。"
    return str(order)

SYSTEM_PROMPT = """
あなたはECサイトの注文・配送担当エージェントです。
注文状況や配送に関する問い合わせに、lookup_orderツールで注文DBを照会して簡潔に回答してください。
担当外の質問(製品の使い方など)には答えず、担当外である旨を返してください。
"""

agent = Agent(
    name="OrderAgent",
    description="ECサイトの注文・配送担当。注文IDをもとに配送状況やお届け予定日を照会して回答します。",
    model=load_model(),
    system_prompt=SYSTEM_PROMPT,
    tools=[lookup_order],
)

if __name__ == "__main__":
    serve_a2a(StrandsA2AExecutor(agent))

Agentのnameとdescriptionは、A2Aのエージェント発見の仕組みであるAgent Card(後述)にそのまま記載されるので、他のエージェントが読んで委譲判断できる内容を書いておくのが良いと思います。

TechAgentも同じ構造で、製品ナレッジのモックを検索するツールを持たせています。

app/TechAgent/main.py(コード全体)
app/TechAgent/main.py
from strands import Agent, tool
from strands.multiagent.a2a.executor import StrandsA2AExecutor
from bedrock_agentcore.runtime import serve_a2a
from model.load import load_model

# 製品ナレッジベースのモック
KNOWLEDGE_BASE = {
    "CleanBot X": [
        "初期設定: 1) 本体を充電ドックに設置 2) スマホアプリ CleanBot Home をインストール 3) アプリの指示に従い2.4GHz帯のWi-Fiに接続(5GHz帯は非対応)",
        "エラーE03: 車輪に異物が絡まっている状態。電源を切り、車輪周りを清掃してください。",
        "吸引力が弱い場合: ダストボックスとフィルターを清掃。フィルターは3ヶ月ごとの交換を推奨。",
    ],
    "AirSound Pro": [
        "ペアリング: ケースのボタンを3秒長押しでペアリングモードに入ります。",
        "片耳しか聞こえない場合: 両方をケースに戻し10秒待ってから再接続してください。",
    ],
    "Echo Mini": [
        "セットアップ: Alexaアプリから「デバイスの追加」を選択してください。",
    ],
}

@tool
def search_product_knowledge(product_name: str) -> str:
    """製品名からトラブルシューティング・設定手順などのナレッジを検索する。"""
    for name, articles in KNOWLEDGE_BASE.items():
        if name.lower() in product_name.lower() or product_name.lower() in name.lower():
            return "\n".join(articles)
    return f"製品 {product_name} のナレッジが見つかりません。対応製品: {', '.join(KNOWLEDGE_BASE.keys())}"

SYSTEM_PROMPT = """
あなたはECサイトのテクニカルサポート担当エージェントです。
製品の設定方法やトラブルシューティングの問い合わせに、search_product_knowledgeツールでナレッジを検索して簡潔に回答してください。
担当外の質問(注文状況・配送など)には答えず、担当外である旨を返してください。
"""

agent = Agent(
    name="TechAgent",
    description="ECサイトのテクニカルサポート担当。製品の初期設定・トラブルシューティングをナレッジ検索で解決します。",
    model=load_model(),
    system_prompt=SYSTEM_PROMPT,
    tools=[search_product_knowledge],
)

if __name__ == "__main__":
    serve_a2a(StrandsA2AExecutor(agent))

GatewayとAgent Targetを追加する

Gatewayを作成し、2体のRuntimeを指すAgent Targetを追加します。CLIでのターゲットタイプはhttp-runtimeです。

実行コマンド
agentcore add gateway --name AgentGateway --protocol-type None --authorizer-type AWS_IAM

agentcore add gateway-target --name order-support --gateway AgentGateway \
  --type http-runtime --runtime OrderAgent

agentcore add gateway-target --name tech-support --gateway AgentGateway \
  --type http-runtime --runtime TechAgent

ここで1つ注意点があります。Agent Targetはprotocol-typeがMCPのGatewayには追加できません。Noneを指定して作成しましょう。ターゲット名(order-support / tech-support)はそのままルーティングパスになります。

デプロイする

実行コマンド
agentcore deploy -y
実行結果(抜粋)
✓ Deployed to 'default' (stack: AgentCore-AgentTargetDemo-default)
Outputs:
  GatewayAgentGatewayUrlOutput: https://agenttargetdemo-agentgateway-xxxx.gateway.bedrock-agentcore.us-east-1.amazonaws.com
  GatewayTargetOrderSupportIdOutput: V2RNRURGGW
  GatewayTargetTechSupportIdOutput: 35A8UJKZCK
  ApplicationAgentOrderAgentRuntimeArnOutput: arn:aws:bedrock-agentcore:us-east-1:<アカウントID>:runtime/AgentTargetDemo_OrderAgent-xxxx
  ApplicationAgentTechAgentRuntimeArnOutput: arn:aws:bedrock-agentcore:us-east-1:<アカウントID>:runtime/AgentTargetDemo_TechAgent-xxxx

CDK経由でRuntime 2体・Gateway・ターゲット2つが一気にデプロイされます。手元では5分ほどで完了しました。

動作確認

Agent Cardを取得する(と権限エラー)

A2Aにはエージェント発見の仕組みとしてAgent Cardがあります。エージェントの名前・説明・スキル・エンドポイントURLを記載したJSONで、A2Aサーバーの/.well-known/agent-card.jsonで公開されます。

Gateway経由では、ターゲットのパス配下の下記URLで取得できます。

実行コマンド
uvx awscurl --service bedrock-agentcore --region us-east-1 \
  "https://<GatewayのID>.gateway.bedrock-agentcore.us-east-1.amazonaws.com/order-support/invocations/.well-known/agent-card.json"

が、いきなりエラーが返ってきました。

実行結果
{"message":"User: arn:aws:sts::<アカウントID>:assumed-role/AgentCore-AgentTargetDemo-McpGatewayAgentGatewayRol-xxxx/gateway-session-xxxx is not authorized to perform: bedrock-agentcore:GetAgentCard on resource: arn:aws:bedrock-agentcore:us-east-1:<アカウントID>:runtime/AgentTargetDemo_OrderAgent-xxxx because no identity-based policy allows the bedrock-agentcore:GetAgentCard action"}

エラーの主語がGatewayの実行ロールになっています。Gatewayは自身の実行ロールでRuntimeを呼び出すのですが、CLIが自動生成する実行ロールにはInvokeAgentRuntimeの権限しか付いておらず、Agent Cardの取得に必要なbedrock-agentcore:GetAgentCardがありません。1本目の記事のBedrock Mantleのときと同じパターンですね・・・自動生成ロールは最小構成なので、機能を使う分だけ足していく必要があります。

Gateway実行ロールに下記のインラインポリシーを追加します。Runtime本体だけでなくエンドポイント(runtime-endpoint)のARNにも必要な点に注意です。最初Runtime本体のARNだけ許可したところ、今度はruntime/xxx/runtime-endpoint/DEFAULTに対する同じエラーが返ってきました。

Gateway実行ロールに追加するポリシー
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": "bedrock-agentcore:GetAgentCard",
    "Resource": [
      "arn:aws:bedrock-agentcore:us-east-1:<アカウントID>:runtime/AgentTargetDemo_OrderAgent-xxxx",
      "arn:aws:bedrock-agentcore:us-east-1:<アカウントID>:runtime/AgentTargetDemo_OrderAgent-xxxx/runtime-endpoint/*",
      "arn:aws:bedrock-agentcore:us-east-1:<アカウントID>:runtime/AgentTargetDemo_TechAgent-xxxx",
      "arn:aws:bedrock-agentcore:us-east-1:<アカウントID>:runtime/AgentTargetDemo_TechAgent-xxxx/runtime-endpoint/*"
    ]
  }]
}

追加後にもう一度実行すると、無事Agent Cardが返ってきました!

実行結果
{
  "capabilities": {"streaming": true},
  "defaultInputModes": ["text"],
  "defaultOutputModes": ["text"],
  "description": "ECサイトの注文・配送担当。注文IDをもとに配送状況やお届け予定日を照会して回答します。",
  "name": "OrderAgent",
  "preferredTransport": "JSONRPC",
  "protocolVersion": "0.3.0",
  "skills": [{"description": "ECサイトの注文・配送担当。注文IDをもとに配送状況やお届け予定日を照会して回答します。", "id": "main", "name": "OrderAgent", "tags": ["main"]}],
  "url": "https://bedrock-agentcore.us-east-1.amazonaws.com/runtimes/arn%3Aaws%3A.../invocations",
  "version": "0.1.0"
}

コードに書いたnameとdescriptionがそのまま載っていますね。ただ、このurlフィールド・・・GatewayではなくRuntime自体のURLを指しています。実はこれが後で罠になるのですが、いったん置いておいて先に進みます。

A2AメッセージをGateway経由で送る

A2AはJSON-RPCで通信するので、message/sendメソッドをターゲットのパスにPOSTしてみます。

実行コマンド
uvx awscurl --service bedrock-agentcore --region us-east-1 -X POST \
  "https://<GatewayのID>.gateway.bedrock-agentcore.us-east-1.amazonaws.com/order-support/invocations" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": "req-001",
    "method": "message/send",
    "params": {
      "message": {
        "role": "user",
        "parts": [{"kind": "text", "text": "注文ORD-1002はいつ届きますか?"}],
        "messageId": "11111111-1111-1111-1111-111111111111"
      }
    }
  }'
実行結果(抜粋)
{
  "id": "req-001",
  "jsonrpc": "2.0",
  "result": {
    "artifacts": [{
      "name": "agent_response",
      "parts": [{"kind": "text", "text": "注文ORD-1002(ロボット掃除機 CleanBot X)は現在配送中です。\n\nお届け予定日:2026年7月9日\n配送業者:クラメソ急便\n追跡番号:CM-4471-8829\n..."}]
    }],
    "kind": "task",
    "status": {"state": "completed"}
  }
}

お、Gateway経由でOrderAgentがツールで注文DBを照会して回答してくれていますね!A2AのJSON-RPCがそのまま返却されていることが確認できました。

ローカルエージェントからオーケストレーションする

いよいよ本題です。ローカルの一次受付(トリアージ)エージェントから、Gateway経由でリモートの2体に委譲します。

StrandsにはリモートA2AエージェントのクライアントラッパーとしてA2AAgentクラスが用意されていて、Agent Cardの解決からJSON-RPCの組み立てまで面倒を見てくれます。client_configにhttpxクライアントを渡せるので、記事1本目と同じくmcp-proxy-for-awsのSigV4HTTPXAuthを差し込めばGatewayのIAM認証を実行可能となります。

https://strandsagents.com/docs/user-guide/concepts/multi-agent/agent-to-agent/

このA2AAgentをツールでラップして、トリアージエージェントに持たせます。どのURLにリクエストが飛んだか見えるように、ログ出力のフックも仕込んでいます。なお、A2AAgentのインスタンスを使い回すと同じエージェントへの2回目の委譲がEvent loop is closedで失敗したため、ツール呼び出しごとに作り直す形にしています。

local_orchestrator.py
"""ローカルで動くトリアージ(一次受付)エージェント。

Gateway経由でリモートの専門エージェント2体(OrderAgent / TechAgent)に
A2Aプロトコルで問い合わせを委譲する。
"""

import boto3
import httpx
from a2a.client import ClientConfig
from mcp_proxy_for_aws.sigv4_helper import SigV4HTTPXAuth
from strands import Agent, tool
from strands.agent.a2a_agent import A2AAgent

GATEWAY_URL = "https://<GatewayのID>.gateway.bedrock-agentcore.us-east-1.amazonaws.com"

auth = SigV4HTTPXAuth(
    credentials=boto3.Session().get_credentials(),
    service="bedrock-agentcore",
    region="us-east-1",
)

async def log_request(request):
    print(f"  [HTTP] {request.method} {request.url}")

def make_client() -> httpx.AsyncClient:
    return httpx.AsyncClient(
        auth=auth, timeout=300, event_hooks={"request": [log_request]}
    )

def make_agent(target: str) -> A2AAgent:
    # A2AAgentは呼び出しのたびに新しいイベントループを作るため、インスタンスを
    # 使い回すと2回目の呼び出しがEvent loop is closedで失敗する。
    # ツール呼び出しごとにhttpxクライアントごと作り直す。
    return A2AAgent(
        endpoint=f"{GATEWAY_URL}/{target}/invocations",
        client_config=ClientConfig(httpx_client=make_client(), streaming=False),
    )

@tool
def ask_order_agent(question: str) -> str:
    """注文・配送に関する質問を注文担当エージェントに問い合わせる。注文IDがあれば含めること。"""
    result = make_agent("order-support")(question)
    return str(result.message["content"][0]["text"])

@tool
def ask_tech_agent(question: str) -> str:
    """製品の設定方法・トラブルシューティングをテクニカルサポートエージェントに問い合わせる。製品名を含めること。"""
    result = make_agent("tech-support")(question)
    return str(result.message["content"][0]["text"])

orchestrator = Agent(
    system_prompt=(
        "あなたはECサイトのカスタマーサポート一次受付です。"
        "問い合わせを分解し、注文・配送の質問はask_order_agentに、"
        "製品の使い方・トラブルはask_tech_agentに委譲して、結果を統合して回答してください。"
    ),
    tools=[ask_order_agent, ask_tech_agent],
)

orchestrator(
    "注文ORD-1002で買ったロボット掃除機がまだ届きません。いつ届きますか?"
    "あと、届いたらすぐ使いたいのでWi-Fi接続のやり方も教えてください。"
)

実行してみます。

実行コマンド
uv run --with 'strands-agents[a2a]' --with mcp-proxy-for-aws --with boto3 local_orchestrator.py
実行結果(HTTPログ部分)
Tool #1: ask_order_agent
Tool #2: ask_tech_agent
  [HTTP] GET https://<GatewayのID>.gateway.bedrock-agentcore.us-east-1.amazonaws.com/order-support/invocations/.well-known/agent-card.json
  [HTTP] GET https://<GatewayのID>.gateway.bedrock-agentcore.us-east-1.amazonaws.com/tech-support/invocations/.well-known/agent-card.json
  [HTTP] POST https://bedrock-agentcore.us-east-1.amazonaws.com/runtimes/arn%3Aaws%3A...OrderAgent-xxxx/invocations
  [HTTP] POST https://bedrock-agentcore.us-east-1.amazonaws.com/runtimes/arn%3Aaws%3A...TechAgent-xxxx/invocations

トリアージエージェントが問い合わせを分解して2体に並行で委譲するところまでは期待通り・・・なのですが、HTTPログをよく見てください。Agent Cardの取得(GET)はGateway経由なのに、肝心のメッセージ送信(POST)はbedrock-agentcore.us-east-1.amazonaws.com、つまりRuntime自体のURLに飛んでいます・・・!

Gatewayがバイパスされる

原因は先ほど見たAgent Cardのurlフィールドです。A2Aクライアントはプロトコルの仕様として、カード取得後の通信先をカードに書かれたurlから決めます。そしてRuntime上のserve_a2aは、プラットフォームが注入する環境変数AGENTCORE_RUNTIME_URL(Runtime自体のURL)をそのままカードのurlに書き込むため、A2AAgentのendpointにGatewayを指定しても、メッセージ送信だけGatewayを素通りしてRuntime直行になってしまうわけです。

対処はシンプルで、カードのurlがGatewayを指すようにRuntime側の環境変数を上書きします。serve_a2aはAGENTCORE_RUNTIME_URLの値をそのままカードに使うので、agentcore.jsonのruntimesにenvVarsを追加して、各ターゲットのGateway URLを設定します。

agentcore.json(runtimesの抜粋)
"runtimes": [
  {
    "name": "OrderAgent",
    "protocol": "A2A",
    "envVars": [
      {
        "name": "AGENTCORE_RUNTIME_URL",
        "value": "https://<GatewayのID>.gateway.bedrock-agentcore.us-east-1.amazonaws.com/order-support/invocations"
      }
    ]
  },
  {
    "name": "TechAgent",
    "protocol": "A2A",
    "envVars": [
      {
        "name": "AGENTCORE_RUNTIME_URL",
        "value": "https://<GatewayのID>.gateway.bedrock-agentcore.us-east-1.amazonaws.com/tech-support/invocations"
      }
    ]
  }
]

再デプロイしてAgent Cardを確認すると、urlがGatewayを指すようになりました。

実行コマンド
agentcore deploy -y
Agent Cardのurl(修正後)
https://<GatewayのID>.gateway.bedrock-agentcore.us-east-1.amazonaws.com/order-support/invocations

再実行してみる

改めてオーケストレーターを実行します。

実行結果
Tool #1: ask_order_agent
Tool #2: ask_tech_agent
  [HTTP] GET https://<GatewayのID>.gateway.bedrock-agentcore.us-east-1.amazonaws.com/order-support/invocations/.well-known/agent-card.json
  [HTTP] GET https://<GatewayのID>.gateway.bedrock-agentcore.us-east-1.amazonaws.com/tech-support/invocations/.well-known/agent-card.json
  [HTTP] POST https://<GatewayのID>.gateway.bedrock-agentcore.us-east-1.amazonaws.com/tech-support/invocations
  [HTTP] POST https://<GatewayのID>.gateway.bedrock-agentcore.us-east-1.amazonaws.com/order-support/invocations
両方の情報が揃いました!まとめてご案内いたします

## 配送状況(注文 ORD-1002)

ご注文のロボット掃除機(CleanBot X)は現在配送中です。

| 項目 | 内容 |
|------|------|
| お届け予定日 | 2026年7月9日 |
| 配送業者 | クラメソ急便 |
| 追跡番号 | CM-4471-8829 |

## Wi-Fi接続の設定方法(CleanBot X)

1. 本体を充電ドックに設置する
2. スマホに「CleanBot Home」アプリをインストールする
3. アプリを起動し、画面の指示に従って設定を進める
4. Wi-Fi接続時は2.4GHz帯のネットワークを選択する
   - 5GHz帯には非対応のためご注意ください

到着まであと少しです!ご不明な点があればお気軽にお申し付けください

今度は全リクエストがGateway経由になっていますね!

Gatewayからしか受け付けできないようにする

Agent CardのURLを直しただけでは行儀の良いクライアントがGatewayを通ってくれるだけで、Runtimeを直接呼べる経路自体は残っています。GatewayのポリシーやGuardrailsで統制するなら、直接アクセスを塞ぎたいケースもあるかもしれません。

公式ドキュメントにこのためのパターンが用意されていて、RuntimeにリソースベースポリシーをアタッチしてGateway実行ロール以外のInvokeを明示Denyします。

https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-security-best-practices.html

今回は検証のためOrderAgentのRuntimeだけに適用して動作確認します。実運用でGatewayのみ接続可能とする場合は、Gatewayに紐づける全Runtime(今回の構成ならTechAgentも)に同じリソースベースポリシーを適用してください。

なお、この方法は今回のようなIAM(SigV4)認証のRuntime向けです。OAuth(JWT)認証のRuntimeの場合は、customJWTAuthorizerのallowedWorkloadConfigurationでGatewayのワークロードのみを許可する方法が用意されています。

実行コマンド
aws bedrock-agentcore-control put-resource-policy --region us-east-1 \
  --resource-arn "arn:aws:bedrock-agentcore:us-east-1:<アカウントID>:runtime/AgentTargetDemo_OrderAgent-xxxx" \
  --policy '{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Sid": "AllowOnlyGatewayRole",
        "Effect": "Allow",
        "Principal": {"AWS": "arn:aws:iam::<アカウントID>:role/<Gateway実行ロール名>"},
        "Action": "bedrock-agentcore:InvokeAgentRuntime",
        "Resource": "arn:aws:bedrock-agentcore:us-east-1:<アカウントID>:runtime/AgentTargetDemo_OrderAgent-xxxx"
      },
      {
        "Sid": "DenyOtherPrincipals",
        "Effect": "Deny",
        "Principal": {"AWS": "*"},
        "Action": "bedrock-agentcore:InvokeAgentRuntime",
        "Resource": "arn:aws:bedrock-agentcore:us-east-1:<アカウントID>:runtime/AgentTargetDemo_OrderAgent-xxxx",
        "Condition": {
          "ArnNotEquals": {
            "aws:PrincipalArn": "arn:aws:iam::<アカウントID>:role/<Gateway実行ロール名>"
          }
        }
      }
    ]
  }'

実際に試してみます。

動作確認結果
Runtime直接呼び出し → 403 explicit deny in a resource-based policy
Gateway経由        → HTTP 200(正常に応答)
直接呼び出し時のエラーメッセージ
{"message":"User: arn:aws:sts::<アカウントID>:assumed-role/<自分のロール>/xxx is not authorized to perform: bedrock-agentcore:InvokeAgentRuntime on resource: arn:aws:bedrock-agentcore:us-east-1:<アカウントID>:runtime/AgentTargetDemo_OrderAgent-xxxx with an explicit deny in a resource-based policy"}

これでGateway経由のみでアクセスできるようになりました。Gatewayのみターゲットにアクセスしたい場合はこのパターンが使えそうですね。

後片付け

検証が終わったら下記でリソースを削除できます。リソースベースポリシーはスタック外で付けたので先に削除しておきます。

実行コマンド
aws bedrock-agentcore-control delete-resource-policy --region us-east-1 \
  --resource-arn "arn:aws:bedrock-agentcore:us-east-1:<アカウントID>:runtime/AgentTargetDemo_OrderAgent-xxxx"

agentcore remove all --json
agentcore deploy -y

Gateway実行ロールに手で追加したGetAgentCardのインラインポリシーも、ロールごとスタック削除で消えます。

おわりに

Inference Target・Cedar Guardrailsと合わせて、新しくなったGatewayの機能を3本にわたって触ってきました!

ツールもLLMもエージェントもGatewayの背後に置けて、エージェントが増えていく中で、Gatewayが起点になりそうな将来を想像しました。

本記事が少しでも参考になりましたら幸いです。最後までご覧いただきありがとうございました!

補足1:エージェントレジストリ的な使い方

複数のエージェントが1つのGatewayにぶら下がってパスで呼び分けられるということは、フロント画面にエージェント一覧を出して、ユーザーが選んだエージェントに接続する、といった社内エージェントポータルのような使い方もできそうです。こちらも軽くどう実装するのかイメージを膨らめせてみます。

一覧の組み立ては2ステップです。

  1. ListGatewayTargets APIで、Gatewayにぶら下がっているターゲット(=エージェント)を列挙する
  2. 各ターゲットのAgent CardをGateway経由で取得して、エージェントの名前や説明で一覧を肉付けする
registry_demo.py(抜粋)
cp = boto3.client("bedrock-agentcore-control", region_name="us-east-1")
targets = cp.list_gateway_targets(gatewayIdentifier=GATEWAY_ID)["items"]

with httpx.Client(auth=auth, timeout=30) as client:  # authはSigV4HTTPXAuth
    for t in targets:
        if t["status"] != "READY":
            continue
        card = client.get(
            f"{GATEWAY_URL}/{t['name']}/invocations/.well-known/agent-card.json"
        ).json()
        agent_list.append({
            "id": t["name"],
            "displayName": card["name"],
            "description": card["description"],
            "connectUrl": card["url"],
        })
実行結果
[
  {
    "id": "tech-support",
    "displayName": "TechAgent",
    "description": "ECサイトのテクニカルサポート担当。製品の初期設定・トラブルシューティングをナレッジ検索で解決します。",
    "connectUrl": "https://<GatewayのID>.gateway.bedrock-agentcore.us-east-1.amazonaws.com/tech-support/invocations"
  },
  {
    "id": "order-support",
    "displayName": "OrderAgent",
    "description": "ECサイトの注文・配送担当。注文IDをもとに配送状況やお届け予定日を照会して回答します。",
    "connectUrl": "https://<GatewayのID>.gateway.bedrock-agentcore.us-east-1.amazonaws.com/order-support/invocations"
  }
]

エージェントの表示名・説明文・接続先URLが揃っているので、このJSONをそのまま画面に並べれば、一覧からエージェントを選んで接続するUIが作れそうですね。

なお、一覧の列挙に使うListGatewayTargetsはコントロールプレーンのAPIなので、ブラウザから直接呼ぶのではなく、バックエンド側で実行してキャッシュしておく形が前提になります。MCPターゲットのtools/listのように、データプレーンで全エージェントを一覧できるエンドポイントは今のところなさそうです。

補足2:A2Aターゲットはスキーマを明示するとターゲット単位ポリシーが作れる

特定のIAMロールからorder-supportターゲットだけを呼べなくする、というターゲット単位の出し分けです。アクションには{ターゲット名}___POST:/invocations形式のターゲット単位のアクション名を指定します。

作成しようとしたポリシー
forbid (
  principal == AgentCore::IamEntity::"arn:aws:sts::<アカウントID>:assumed-role/<ロール名>",
  action == AgentCore::Action::"order-support___POST:/invocations",
  resource == AgentCore::Gateway::"<GatewayのARN>"
);

ところがA2Aプロトコルのターゲットに対しては、これがバリデーションで下記のエラーになります。

ポリシー作成失敗時のstatusReasons(抜粋)
unrecognized action `AgentCore::Action::"order-support___POST:/invocations"`
did you mean `AgentCore::Action::"arn:aws:bedrock-agentcore:us-east-1:<アカウントID>:gateway/agenttargetdemo-agentga...

そんなアクション名は知らない、と言われてしまいます。agentcore CLIのフォームベース生成でも同じアクション名が生成されるため失敗します。HTTPプロトコルのRuntimeターゲットでは同じ形式で問題なく作れるので、A2Aターゲットだけスキーマが連携されていないように見えます。

ターゲット設定のschemaフィールドが怪しい気がしています。ドキュメントではA2Aプロトコルならデフォルトスキーマが自動適用されるとありましたが、どうもこのデフォルトスキーマからはアクションが解決できていないようなので、自前のOpenAPIスキーマで上書きしてみます。

ターゲットにスキーマを明示設定する(抜粋)
cp.update_gateway_target(
    gatewayIdentifier="<GatewayのID>",
    targetId="<ターゲットID>",
    name="order-support",
    targetConfiguration={"http": {"agentcoreRuntime": {
        "arn": "<OrderAgentのRuntime ARN>",
        "qualifier": "DEFAULT",
        # POST /invocations にA2AのJSON-RPCが来ることを表すOpenAPIを明示
        "schema": {"source": {"inlinePayload": json.dumps(openapi_schema)}},
    }}},
    credentialProviderConfigurations=[{"credentialProviderType": "GATEWAY_IAM_ROLE"}],
)
設定したOpenAPIスキーマ全体
{
  "openapi": "3.0.0",
  "info": {"title": "OrderAgent A2A", "version": "1.0.0"},
  "paths": {
    "/invocations": {
      "post": {
        "operationId": "invoke",
        "requestBody": {
          "required": true,
          "content": {"application/json": {"schema": {
            "type": "object",
            "properties": {
              "jsonrpc": {"type": "string"},
              "id": {"type": "string"},
              "method": {"type": "string"},
              "params": {"type": "object", "properties": {
                "message": {"type": "object", "properties": {
                  "role": {"type": "string"},
                  "parts": {"type": "array", "items": {"type": "object", "properties": {
                    "kind": {"type": "string"}, "text": {"type": "string"}}}},
                  "messageId": {"type": "string"}
                }}
              }}
            },
            "required": ["jsonrpc", "method"]
          }}}
        },
        "responses": {"200": {"description": "A2A JSON-RPC response",
          "content": {"application/json": {"schema": {"type": "object"}}}}}
      }
    }
  }
}

スキーマ設定後に冒頭のポリシー(DenyOrderForMyRoleV5という名前で作成)を作り直すと、今度はACTIVEになりました!実行時の挙動も確認します。

動作確認結果(同一ロールからの呼び出し)
order-support → HTTP 403 Request Denied [Policy evaluation denied due to DenyOrderForMyRoleV5]
tech-support  → HTTP 200(通常応答)

A2Aターゲットでもターゲット単位の出し分けができました!
A2Aターゲットにターゲット単位のCedarポリシーを効かせたい場合は、デフォルトスキーマに頼らずターゲットのschemaにPOST /invocationsのOpenAPIを明示する形になりそうです・・・

パスをcontext.input.params.message.partsに向ければ、2本目の記事と同じwhen guardrails構文でエージェントへの入力メッセージにGuardrails(ContentFilter)も適用でき、暴力的なメッセージが403でブロックされるところまで動きました。suppressOutputによる出力側の抑止も、message/send(バッファリング)であれば動作します。ただし2本目の記事で触れたsuppressOutputとストリーミングの相性問題はA2Aターゲットでも同じで、message/streamとの併用はできませんでした。(あくまで私が検証した範囲ではですが・・・)

ちなみにドキュメント上は逆に、HTTPプロトコルのRuntimeこそGuardrails利用にスキーマ指定が必要とされています。ただ実際は、スキーマなしのHTTPプロトコルターゲットにGuardrailポリシーを作成でき、実行時のブロックも動作しました(2本目の記事の構成でも、本記事での再検証でも同様)。スキーマの要否については、ドキュメントと実際の挙動が両方向で食い違っているように見えますがリリースされたばかりのためか不安定で解消されていくとは思います!

この記事をシェアする

関連記事