Kiro から AgentCore Gateway 経由で S3 Vectors を操作してみた — MCP × Lambda × IAM 認証

記事タイトルとURLをコピーする

Kiro から AgentCore Gateway 経由で S3 Vectors を操作してみた — MCP × Lambda × IAM 認証

はじめに

まず完成形をお見せします。Kiro のチャットで「修学旅行で東京を巡る青春映画」と聞くと、裏側で S3 Vectors にベクトル検索をかけて、こんな結果が返ってきます。

{
    "query": "修学旅行で東京を巡る青春映画",
    "results": [
        {
            "key": "jp5",
            "distance": 0.4935,
            "metadata": {
                "year": 2024,
                "genre": "youth",
                "director": "熊切和嘉",
                "source_text": "ゼンブ・オブ・トーキョー: 女子高生たちが待ちに待った修学旅行で東京へ..."
            }
        },
        {
            "key": "jp3",
            "distance": 0.7185,
            "metadata": {
                "genre": "anime",
                "source_text": "君の名は。: 東京の少年と田舎の少女が夢の中で入れ替わり..."
            }
        }
    ]
}

キーワード一致ではなく、意味的な類似度で検索しているため、「修学旅行」「東京」「青春」という文脈を理解して「ゼンブ・オブ・トーキョー」が distance=0.4935 でトップに返ってきています。これは事前に S3 Vectors に格納しておいた映画の説明文をベクトル化したデータに対して検索をかけた結果です。

S3 Vectors は 2025年7月に GA になったベクトルストアで、S3 のマネージドなインフラ上でベクトルの格納・検索ができます。OpenSearch や Aurora pgvector のようなクラスタ管理が不要で、API を叩くだけで使えるのが特徴です。

  • メリット: サーバーレスで運用不要、S3 の耐久性、従量課金(格納量 + リクエスト数)、メタデータフィルタ対応
  • デメリット: 最大 100 件/クエリの返却上限、インデックスあたり数百 QPS の上限、コールドクエリで 1 秒未満のレイテンシ(リアルタイム検索には不向きな場合がある)

大規模な検索基盤というよりは、今回のように AI ツールのバックエンドとして少〜中規模のベクトルデータを手軽に扱いたいケースにフィットします。

料金比較: S3 Vectors vs OpenSearch Serverless vs Aurora pgvector

S3 Vectors の最大の強みはコストです。他のベクトルストアと比較してみます。

試算条件

今回のような小〜中規模のユースケースを想定します。

  • ベクトル数: 10,000 件(1024 次元、float32)
  • ベクトルサイズ: 4 KB(ベクトルデータ)+ 1 KB(メタデータ)+ 0.17 KB(キー)= 約 5.17 KB/件
  • 合計ストレージ: 約 50 MB
  • クエリ: 月 10,000 回
  • PUT: 月 1,000 件の追加・更新

S3 Vectors(料金ページより算出)

項目 計算 月額
ストレージ 0.05 GB × $0.06/GB $0.003
PUT 0.005 GB × $0.20/GB $0.001
クエリ API 10,000 回 × $2.50/100万回 $0.025
クエリデータ処理 5.17 KB × 10,000件 × $0.004/TB × 10,000回 $0.002
合計 約 $0.03/月

OpenSearch Serverless

構成 最低 OCU 月額($0.24/OCU/時間 × 720時間)
本番(2AZ冗長) 4 OCU(indexing 2 + search 2) 約 $691/月
開発(dev/test) 1 OCU(indexing 0.5 + search 0.5) 約 $175/月

Aurora Serverless v2 + pgvector

構成 最低 ACU 月額($0.12/ACU/時間 × 720時間)
最小構成(0.5 ACU) 0.5 ACU 約 $43/月
ストレージ(50 MB) 約 $0.005/月
合計 約 $43/月

比較まとめ

サービス 月額目安 特徴
S3 Vectors 約 $0.03 完全従量課金。使わなければほぼ $0
Aurora Serverless v2 + pgvector 約 $43〜 SQL が使える。0 ACU までスケールダウン可能だが、起動に数十秒かかる
OpenSearch Serverless 約 $175〜 全文検索 + ベクトル検索。最低 OCU が常時課金される

S3 Vectors は完全従量課金のため、今回のような小規模データ + 低頻度クエリでは月 $1 未満に収まります。OpenSearch Serverless は開発構成でも月 $175〜かかるため、「ちょっと試してみたい」用途には割高です。Aurora は 0 ACU スケールダウンが可能ですが、コールドスタートのレイテンシが気になる場面があります。

一方、大規模データ(数百万〜数億件)や高頻度クエリ(数千 QPS)が必要な場合は、OpenSearch や Aurora の方がスループット・機能面で有利です。用途に応じて使い分けましょう。

この仕組みを、AgentCore Gateway × Lambda × S3 Vectors で構築していきます。

前回の記事では Amazon S3 Vectors を使ったベクトル検索と Bedrock Knowledge Base との統合を試しました。今回はその S3 Vectors を、AI コーディングアシスタント Kiro から直接操作できるようにします。

具体的には、以下のパイプラインを構築します。

flowchart LR
    A[Kiro] --> B[AgentCore Gateway] --> C[Lambda] --> D[S3 Vectors]

AgentCore Gateway は Amazon Bedrock AgentCore が提供するマネージド MCP エンドポイントで、Lambda 関数や外部 API を MCP ツールとして公開できます。これにより、Kiro のチャットから「宇宙の映画を検索して」と言うだけで、裏側で Lambda が S3 Vectors にベクトル検索をかけて結果を返す、という流れが実現できます。

今回は1つの Lambda に以下の3つの MCP ツールを実装します。

ツール名 機能 主な引数
query_s3_vectors 自然言語で類似検索 query(検索テキスト)、top_kgenre_filter
put_s3_vectors テキストをベクトル化して格納 keytextmetadata
list_s3_vectors 格納済みベクトルの一覧取得 なし

構成図

flowchart LR
    A[Kiro] -- "MCP\n(SigV4)" --> B[AgentCore Gateway]
    B -- "IAM Role" --> C[Lambda]
    C -- "Bedrock API" --> D[Titan Embeddings V2]
    C -- "S3 Vectors API" --> E[S3 Vector Bucket]

Kiro から S3 Vectors までのデータの流れです。

  1. Kiro が mcp-proxy-for-aws を使って Gateway に MCP リクエストを送信(IAM 認証、SigV4 署名)
  2. Gateway がツール名に基づいて Lambda 関数を呼び出し
  3. Lambda が Bedrock でクエリテキストをベクトル化し、S3 Vectors で類似検索を実行
  4. 結果が Gateway → Kiro に返される
flowchart LR
    G[AgentCore Gateway] --> T["S3VectorsTarget\n(Lambda)"]
    T --> Q[query_s3_vectors]
    T --> P[put_s3_vectors]
    T --> L[list_s3_vectors]

Gateway 内部のツール構成です。Gateway にはターゲット(S3VectorsTarget)を1つ登録し、その中に3つのツールを定義します。3つのツールはすべて同じ Lambda 関数にルーティングされ、Lambda 側でツール名に応じた処理を分岐します。

前提条件

  • AWS CLI v2
  • Python 3.12(Lambda ランタイム)
  • Kiro(MCP クライアント対応の AI IDE)
  • S3 Vectors のベクトルバケット・インデックスが作成済み(前回の記事参照)
  • uvx コマンドが使える(uv パッケージマネージャ)

1. Lambda 関数の作成

S3 Vectors を操作する Lambda 関数を作成します。この Lambda は AgentCore Gateway から MCP ツールとして呼び出されます。

Gateway から Lambda への event 構造

ここで重要なポイントがあります。AgentCore Gateway は、ツール名に基づいて Lambda をルーティングし、ツールの引数(arguments)をそのまま Lambda の event として渡します。

つまり、MCP クライアントが query_s3_vectors ツールを {"query": "space movies", "top_k": 3} という引数で呼び出した場合、Lambda が受け取る event は以下のようになります。

{"query": "space movies", "top_k": 3}

tool_namearguments でラップされるわけではありません。Gateway がツール名でのルーティングを担当してくれるので、Lambda 側はツール名の分岐が不要です。

では1つの Lambda で複数ツールを処理する場合、どうやってツール名を判別するのでしょうか。Gateway は Lambda の context.client_context.custom にメタデータを渡してくれます。その中の bedrockAgentCoreToolName にツール名が TargetName___tool_name の形式で入っています。___(アンダースコア3つ)で分割すれば元のツール名が取得できます。

Lambda コード

コードを機能ごとに分けて見ていきます。

クライアントの初期化とハンドラ登録

import json
import logging
import boto3

logger = logging.getLogger()
logger.setLevel(logging.INFO)

REGION = "us-west-2"
VECTOR_BUCKET = "test-vector-bucket-kiro"
VECTOR_INDEX = "test-vector-index"
EMBED_MODEL = "amazon.titan-embed-text-v2:0"

bedrock = boto3.client("bedrock-runtime", region_name=REGION)
s3vectors = boto3.client("s3vectors", region_name=REGION)

DELIMITER = "___"
HANDLERS = {}


def handler(name):
    def decorator(fn):
        HANDLERS[name] = fn
        return fn
    return decorator

Lambda のモジュール読み込み時に Bedrock と S3 Vectors のクライアントを初期化します。Lambda はコンテナが再利用される間これらのクライアントを使い回すため、ハンドラ関数の外で作成するのがベストプラクティスです。

DELIMITER = "___" は、Gateway がツール名を TargetName___tool_name 形式で渡してくるため、元のツール名を取り出すための区切り文字です(次のエントリポイントで使います)。

HANDLERS dict と @handler デコレータは、ツール名と処理関数の対応を登録する仕組みです。仕組みを簡単に説明すると:

# @handler("query_s3_vectors") を付けると…
@handler("query_s3_vectors")
def handle_query(args):
    ...

# 内部的にはこうなる
HANDLERS["query_s3_vectors"] = handle_query

handler("query_s3_vectors") が返す decorator 関数が、渡された関数(handle_query)を HANDLERS dict に登録して返します。Python のデコレータ構文(@)を使うことで、関数定義と同時にツール名の紐づけが完了します。この後のツール関数(handle_query 等)で実際に使っていきます。

埋め込み生成

def get_embedding(text: str) -> list:
    resp = bedrock.invoke_model(
        modelId=EMBED_MODEL,
        body=json.dumps({"inputText": text}),
    )
    return json.loads(resp["body"].read())["embedding"]

前回の記事と同じく、Bedrock の Titan Text Embeddings V2 でテキストを 1024 次元のベクトルに変換します。query_s3_vectors(検索クエリのベクトル化)と put_s3_vectors(格納テキストのベクトル化)の両方で使用します。

エントリポイント(ツール名のルーティング)

def lambda_handler(event, context):
    logger.info("EVENT: %s", json.dumps(event, default=str))

    # context からツール名を取得("TargetName___tool_name" → "tool_name")
    original = context.client_context.custom.get("bedrockAgentCoreToolName", "")
    tool_name = original.split(DELIMITER)[-1] if DELIMITER in original else original
    logger.info("TOOL: %s (original: %s)", tool_name, original)

    fn = HANDLERS.get(tool_name)
    if not fn:
        return {"error": f"Unknown tool: {tool_name}"}
    return fn(event)

Gateway から呼び出されると、event にはツールの引数がそのまま入り、context.client_context.custom にメタデータが入ります。bedrockAgentCoreToolName の値は S3VectorsTarget___query_s3_vectors のような形式なので、___ で分割して末尾の query_s3_vectors を取り出し、HANDLERS dict から対応する関数を呼び出します。

handle_query: 類似検索

@handler("query_s3_vectors")
def handle_query(args):
    query_text = args.get("query", "")
    if not query_text:
        return {"error": "query is required"}

    top_k = min(args.get("top_k", 5), 100)
    params = {
        "vectorBucketName": VECTOR_BUCKET,
        "indexName": VECTOR_INDEX,
        "queryVector": {"float32": get_embedding(query_text)},
        "topK": top_k,
        "returnDistance": True,
        "returnMetadata": True,
    }
    if genre := args.get("genre_filter"):
        params["filter"] = {"genre": genre}

    result = s3vectors.query_vectors(**params)
    return {
        "query": query_text,
        "results": [
            {
                "key": v["key"],
                "distance": round(v.get("distance", 0), 4),
                "metadata": v.get("metadata", {}),
            }
            for v in result["vectors"]
        ],
    }

自然言語のクエリテキストを get_embedding でベクトル化し、query_vectors で類似検索を実行します。topK は S3 Vectors の上限 100 を超えないようにクランプしています。genre_filter が指定された場合はメタデータフィルタを追加し、特定ジャンルのベクトルだけを対象に検索します。

レスポンスには各ベクトルの keydistance(cosine distance、小さいほど類似)、metadata を含めて返します。

handle_put: ベクトルの格納

@handler("put_s3_vectors")
def handle_put(args):
    key, text = args.get("key", ""), args.get("text", "")
    if not key or not text:
        return {"error": "key and text are required"}

    metadata = args.get("metadata", {})
    s3vectors.put_vectors(
        vectorBucketName=VECTOR_BUCKET,
        indexName=VECTOR_INDEX,
        vectors=[{
            "key": key,
            "data": {"float32": get_embedding(text)},
            "metadata": {**metadata, "source_text": text},
        }],
    )
    return {"status": "success", "key": key}

テキストをベクトル化して S3 Vectors に格納します。呼び出し側が指定した metadata(ジャンルなど)に加えて、source_text として元のテキストも自動的にメタデータに保存します。これにより、検索結果で「何のテキストから生成されたベクトルか」を確認できます。

同じ key で再度呼び出すと上書きされます。

handle_list: ベクトル一覧

@handler("list_s3_vectors")
def handle_list(_args):
    result = s3vectors.list_vectors(
        vectorBucketName=VECTOR_BUCKET,
        indexName=VECTOR_INDEX,
    )
    vectors = [{"key": v["key"]} for v in result.get("vectors", [])]
    return {"vectors": vectors, "count": len(vectors)}

インデックス内のベクトルキー一覧と件数を返します。引数は不要ですが、Gateway は常に event を渡すため _args で受け取っています。現在格納されているデータの確認用です。

コード全文

上記をまとめた全文です。

"""S3 Vectors クエリ Lambda 関数
AgentCore Gateway 経由で MCP ツールとして呼び出される。
"""

import json
import logging
import boto3

logger = logging.getLogger()
logger.setLevel(logging.INFO)

REGION = "us-west-2"
VECTOR_BUCKET = "test-vector-bucket-kiro"
VECTOR_INDEX = "test-vector-index"
EMBED_MODEL = "amazon.titan-embed-text-v2:0"

bedrock = boto3.client("bedrock-runtime", region_name=REGION)
s3vectors = boto3.client("s3vectors", region_name=REGION)

DELIMITER = "___"
HANDLERS = {}


def handler(name):
    def decorator(fn):
        HANDLERS[name] = fn
        return fn
    return decorator


def get_embedding(text: str) -> list:
    resp = bedrock.invoke_model(
        modelId=EMBED_MODEL,
        body=json.dumps({"inputText": text}),
    )
    return json.loads(resp["body"].read())["embedding"]


def lambda_handler(event, context):
    logger.info("EVENT: %s", json.dumps(event, default=str))

    # context からツール名を取得("TargetName___tool_name" → "tool_name")
    original = context.client_context.custom.get("bedrockAgentCoreToolName", "")
    tool_name = original.split(DELIMITER)[-1] if DELIMITER in original else original
    logger.info("TOOL: %s (original: %s)", tool_name, original)

    fn = HANDLERS.get(tool_name)
    if not fn:
        return {"error": f"Unknown tool: {tool_name}"}
    return fn(event)


@handler("query_s3_vectors")
def handle_query(args):
    """自然言語クエリで S3 Vectors を類似検索"""
    query_text = args.get("query", "")
    if not query_text:
        return {"error": "query is required"}

    top_k = min(args.get("top_k", 5), 100)
    params = {
        "vectorBucketName": VECTOR_BUCKET,
        "indexName": VECTOR_INDEX,
        "queryVector": {"float32": get_embedding(query_text)},
        "topK": top_k,
        "returnDistance": True,
        "returnMetadata": True,
    }
    if genre := args.get("genre_filter"):
        params["filter"] = {"genre": genre}

    result = s3vectors.query_vectors(**params)
    return {
        "query": query_text,
        "results": [
            {
                "key": v["key"],
                "distance": round(v.get("distance", 0), 4),
                "metadata": v.get("metadata", {}),
            }
            for v in result["vectors"]
        ],
    }


@handler("put_s3_vectors")
def handle_put(args):
    """テキストを埋め込み生成して S3 Vectors に格納"""
    key, text = args.get("key", ""), args.get("text", "")
    if not key or not text:
        return {"error": "key and text are required"}

    metadata = args.get("metadata", {})
    s3vectors.put_vectors(
        vectorBucketName=VECTOR_BUCKET,
        indexName=VECTOR_INDEX,
        vectors=[{
            "key": key,
            "data": {"float32": get_embedding(text)},
            "metadata": {**metadata, "source_text": text},
        }],
    )
    return {"status": "success", "key": key}


@handler("list_s3_vectors")
def handle_list(_args):
    """S3 Vectors インデックス内のベクトル一覧を取得"""
    result = s3vectors.list_vectors(
        vectorBucketName=VECTOR_BUCKET,
        indexName=VECTOR_INDEX,
    )
    vectors = [{"key": v["key"]} for v in result.get("vectors", [])]
    return {"vectors": vectors, "count": len(vectors)}

補足:

  • handle_put は1件ずつの挿入ですが、MCP ツールとしての用途(対話的に少量追加)ではこれで十分です。大量のベクトルを投入する場合は、PutVectors の最大バッチサイズ(500件/リクエスト)を活用した別途バッチ処理を用意するのが推奨です(後述の「S3 Vectors ベストプラクティス」参照)

Lambda のデプロイ

IAM ロールの作成

Lambda が Bedrock(埋め込み生成)と S3 Vectors(ベクトル操作)にアクセスするためのロールを作成します。

信頼ポリシー:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "Service": "lambda.amazonaws.com" },
      "Action": "sts:AssumeRole"
    }
  ]
}

権限ポリシー:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "BedrockInvokeModel",
      "Effect": "Allow",
      "Action": "bedrock:InvokeModel",
      "Resource": "arn:aws:bedrock:us-west-2::foundation-model/amazon.titan-embed-text-v2:0"
    },
    {
      "Sid": "S3VectorsAccess",
      "Effect": "Allow",
      "Action": [
        "s3vectors:PutVectors", "s3vectors:GetVectors",
        "s3vectors:DeleteVectors", "s3vectors:QueryVectors",
        "s3vectors:ListVectors", "s3vectors:GetIndex",
        "s3vectors:GetVectorBucket"
      ],
      "Resource": [
        "arn:aws:s3vectors:us-west-2:<アカウントID>:bucket/<ベクトルバケット名>",
        "arn:aws:s3vectors:us-west-2:<アカウントID>:bucket/<ベクトルバケット名>/index/*"
      ]
    },
    {
      "Sid": "CloudWatchLogs",
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:us-west-2:<アカウントID>:*"
    }
  ]
}
aws iam create-role \
  --role-name Lambda-S3Vectors-Role \
  --assume-role-policy-document file://lambda_s3vectors/trust_policy.json

aws iam put-role-policy \
  --role-name Lambda-S3Vectors-Role \
  --policy-name S3VectorsLambdaPolicy \
  --policy-document file://lambda_s3vectors/permissions_policy.json

Lambda 関数の作成

zip -j function.zip lambda_s3vectors/lambda_function.py

aws lambda create-function \
  --function-name s3vectors-query \
  --runtime python3.12 \
  --role arn:aws:iam::<アカウントID>:role/Lambda-S3Vectors-Role \
  --handler lambda_function.lambda_handler \
  --zip-file fileb://function.zip \
  --timeout 30 \
  --memory-size 256 \
  --region us-west-2

Lambda ランタイムの Python 3.12 には boto3 がプリインストールされており、S3 Vectors のクライアント(boto3.client("s3vectors"))も含まれているため、追加のレイヤーは不要です。

動作確認

aws lambda invoke \
  --function-name s3vectors-query \
  --cli-binary-format raw-in-base64-out \
  --payload '{"query": "space adventure movies", "top_k": 3}' \
  --region us-west-2 \
  /tmp/output.json

cat /tmp/output.json | python3 -m json.tool
{
    "query": "space adventure movies",
    "results": [
        {
            "key": "v1",
            "distance": 0.7723,
            "metadata": {
                "genre": "scifi",
                "source_text": "Star Wars: A farm boy joins rebels to fight an evil empire in space"
            }
        },
        ...
    ]
}

2. AgentCore Gateway の作成(IAM 認証)

認証方式の選択

AgentCore Gateway は以下の3つの認証方式(Inbound Auth)をサポートしています。

認証方式 概要 ユースケース
JWT(CUSTOM_JWT) Cognito 等の IdP でトークンを発行し、Bearer トークンで認証 マルチエージェント環境でツール単位のアクセス制御が必要な場合
IAM(AWS_IAM) IAM ユーザー/ロールの認証情報で SigV4 署名 シンプルな構成。IAM ロールがあればすぐ使える
なし(NONE) 認証なし 公開 API(本番では非推奨)

今回は IAM 認証を選択しました。理由は以下の通りです。

  • Cognito のセットアップやクライアントシークレットの管理が不要
  • IAM ユーザー/ロールの認証情報がそのまま使える
  • mcp-proxy-for-aws が SigV4 署名を自動で行ってくれる

補足: agentcore CLI の create-mcp-gateway コマンドはデフォルトで Cognito(CUSTOM_JWT)認証の Gateway を作成します。IAM 認証で作成するには、AWS CLI の bedrock-agentcore-control サービスを直接使う必要がありました。

Gateway の作成

aws bedrock-agentcore-control create-gateway \
  --name "S3VectorsGateway" \
  --authorizer-type AWS_IAM \
  --protocol-type MCP \
  --role-arn "arn:aws:iam::<アカウントID>:role/AgentCoreGatewayExecutionRole" \
  --region us-west-2

レスポンス:

{
    "gatewayId": "s3vectorsgateway-xxxxx",
    "gatewayUrl": "https://s3vectorsgateway-xxxxx.gateway.bedrock-agentcore.us-west-2.amazonaws.com/mcp",
    "status": "CREATING",
    "authorizerType": "AWS_IAM"
}

ポイント: Gateway の作成には bedrock-agentcore-control サービスを使います。bedrock-agentcore(ハイフンなし末尾)は Data Plane API で、Gateway の CRUD は含まれていません。

ステータスが READY になるまで待ちます。

aws bedrock-agentcore-control get-gateway \
  --gateway-identifier <Gateway ID> \
  --region us-west-2 \
  --query 'status'

Gateway 実行ロールについて

agentcore CLI で一度 Gateway を作成すると、AgentCoreGatewayExecutionRole という IAM ロールが自動作成されます。このロールには lambda:InvokeFunction 権限が含まれているため、Lambda ターゲットの呼び出しに必要な権限は揃っています。

手動でロールを作成する場合は、以下の権限が必要です。

  • bedrock-agentcore:*(Gateway 操作)
  • lambda:InvokeFunction(Lambda ターゲット呼び出し)

3. Lambda ターゲットの追加

Gateway に Lambda 関数をターゲットとして登録します。ここで MCP ツールのスキーマ(ツール名、説明、パラメータ)を定義します。

ツールスキーマの定義

lambda_target.json を作成します。

{
    "lambdaArn": "arn:aws:lambda:us-west-2:<アカウントID>:function:s3vectors-query",
    "toolSchema": {
        "inlinePayload": [
            {
                "name": "query_s3_vectors",
                "description": "自然言語クエリで S3 Vectors を類似検索します",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "query": {
                            "type": "string",
                            "description": "検索クエリ(自然言語)"
                        },
                        "top_k": {
                            "type": "number",
                            "description": "返す結果の最大数(デフォルト5、最大100)"
                        },
                        "genre_filter": {
                            "type": "string",
                            "description": "ジャンルでフィルタ(例: scifi, family)"
                        }
                    },
                    "required": ["query"]
                }
            },
            {
                "name": "put_s3_vectors",
                "description": "テキストをベクトル埋め込みに変換して S3 Vectors に格納します",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "key": {
                            "type": "string",
                            "description": "ベクトルの一意キー"
                        },
                        "text": {
                            "type": "string",
                            "description": "埋め込み生成するテキスト"
                        },
                        "metadata": {
                            "type": "object",
                            "description": "追加メタデータ"
                        }
                    },
                    "required": ["key", "text"]
                }
            },
            {
                "name": "list_s3_vectors",
                "description": "S3 Vectors インデックス内のベクトル一覧を取得します",
                "inputSchema": {
                    "type": "object",
                    "properties": {}
                }
            }
        ]
    }
}

inlinePayload に複数のツールを定義できます。1つの Lambda に3つのツールを紐づけていますが、Gateway がツール名でルーティングしてくれるため、Lambda 側でツール名を意識する必要はありません(前述の通り、arguments がそのまま event として渡されます)。

ターゲットの登録

aws bedrock-agentcore-control create-gateway-target \
  --gateway-identifier <Gateway ID> \
  --name S3VectorsTarget \
  --target-configuration '{
    "mcp": {
      "lambda": '"$(cat lambda_target.json)"'
    }
  }' \
  --credential-provider-configurations '[{"credentialProviderType": "GATEWAY_IAM_ROLE"}]' \
  --region us-west-2

credentialProviderTypeGATEWAY_IAM_ROLE にすると、Gateway の実行ロールの権限で Lambda を呼び出します。


4. Kiro の MCP 設定

mcp-proxy-for-aws

AgentCore Gateway は IAM 認証(SigV4 署名)を要求しますが、MCP プロトコル自体には SigV4 署名の仕組みがありません。ここで mcp-proxy-for-aws が橋渡しをしてくれます。

mcp-proxy-for-aws は、ローカルで stdio MCP サーバーとして動作し、Kiro からの MCP リクエストを受け取って SigV4 署名を付与し、Gateway の Streamable HTTP エンドポイントに転送します。

Kiro -- stdio --> mcp-proxy-for-aws -- SigV4 signed HTTP --> AgentCore Gateway

設定ファイル

.kiro/settings/mcp.json に以下を追加します。

{
  "mcpServers": {
    "s3vectors-gateway": {
      "command": "uvx",
      "args": [
        "mcp-proxy-for-aws@latest",
        "https://<Gateway ID>.gateway.bedrock-agentcore.us-west-2.amazonaws.com/mcp",
        "--service", "bedrock-agentcore",
        "--region", "us-west-2"
      ],
      "env": {
        "AWS_PROFILE": "default"
      },
      "disabled": false,
      "autoApprove": []
    }
  }
}
  • --service bedrock-agentcore: SigV4 署名のサービス名。Gateway の認証に必要
  • --region us-west-2: Gateway のリージョン
  • AWS_PROFILE: 使用する AWS プロファイル。bedrock-agentcore:InvokeGateway 権限を持つ IAM ユーザー/ロールを指定

設定を保存すると、Kiro の MCP サーバーパネルに s3vectors-gateway が表示され、3つのツールが使えるようになります。


5. 動作確認

Kiro のチャットから Gateway 経由で S3 Vectors を操作してみます。

ベクトル一覧の取得

Kiro が list_s3_vectors ツールを呼び出し:

{"vectors": [{"key": "v1"}, {"key": "v3"}, {"key": "v2"}], "count": 3}

データの追加

put_s3_vectors で The Matrix と Toy Story を追加:

{"status": "success", "key": "v4"}
{"status": "success", "key": "v5"}

類似検索

「AI と仮想現実の映画」で query_s3_vectors を実行:

{
    "query": "movies about artificial intelligence and virtual reality",
    "results": [
        {
            "key": "v4",
            "distance": 0.732,
            "metadata": {
                "genre": "scifi",
                "source_text": "The Matrix: A hacker discovers reality is a simulation and joins a rebellion against machines"
            }
        },
        ...
    ]
}

The Matrix が distance=0.732 でトップに。

日本語データの追加と検索

英語の映画データだけでなく、日本語のデータも追加してみます。put_s3_vectors で日本語テキストを投入:

Kiro: 日本のアニメ映画をいくつか追加して

Kiro が put_s3_vectors を4回呼び出し:

{"key": "jp1", "text": "千と千尋の神隠し: 少女が不思議な世界に迷い込み、両親を救うために湯屋で働く", "metadata": {"genre": "anime"}}
{"key": "jp2", "text": "となりのトトロ: 田舎に引っ越した姉妹が森の精霊トトロと出会い、不思議な冒険をする", "metadata": {"genre": "anime"}}
{"key": "jp3", "text": "君の名は。: 東京の少年と田舎の少女が夢の中で入れ替わり、やがて運命的な出会いを果たす", "metadata": {"genre": "anime"}}
{"key": "jp4", "text": "もののけ姫: 呪いを受けた青年が森の神々と人間の争いに巻き込まれ、共存の道を探る", "metadata": {"genre": "anime"}}

さらに実写映画も追加:

{"key": "jp5", "text": "ゼンブ・オブ・トーキョー: 女子高生たちが待ちに待った修学旅行で東京へ。班長のゆりかはスカイツリー、渋谷スクランブル交差点、東京タワーなど定番スポットを巡る計画を立てるが、他のメンバーにはそれぞれ秘密の目的があった。11人の少女たちが東京中を駆け回り、最高の思い出を作る青春映画。", "metadata": {"genre": "youth", "director": "熊切和嘉", "year": 2024}}

Titan Text Embeddings V2 は多言語対応なので、日本語テキストもそのままベクトル化できます。

日本語で検索してみます。「修学旅行で東京を巡る青春映画」で query_s3_vectors を実行:

{
    "query": "修学旅行で東京を巡る青春映画",
    "results": [
        {
            "key": "jp5",
            "distance": 0.4935,
            "metadata": {
                "director": "熊切和嘉",
                "year": 2024,
                "genre": "youth",
                "source_text": "ゼンブ・オブ・トーキョー: 女子高生たちが待ちに待った修学旅行で東京へ..."
            }
        },
        {
            "key": "jp3",
            "distance": 0.7185,
            "metadata": {
                "source_text": "君の名は。: 東京の少年と田舎の少女が夢の中で入れ替わり...",
                "genre": "anime"
            }
        }
    ]
}

ゼンブ・オブ・トーキョーが distance=0.4935 で圧倒的にトップ。「東京」「青春」というキーワードだけでなく、「修学旅行」という意味的な類似性もしっかり捉えています。2位の「君の名は。」は「東京」が共通するため distance=0.7185 で返ってきました。

「感動するアニメ映画」で検索すると:

{
    "query": "感動するアニメ映画",
    "results": [
        {"key": "v3", "distance": 0.8628, "metadata": {"source_text": "Finding Nemo: A father fish..."}},
        {"key": "jp4", "distance": 0.8864, "metadata": {"source_text": "もののけ姫: 呪いを受けた青年が..."}},
        {"key": "jp3", "distance": 0.8893, "metadata": {"source_text": "君の名は。: 東京の少年と..."}},
        {"key": "jp5", "distance": 0.8918, "metadata": {"source_text": "ゼンブ・オブ・トーキョー: ..."}},
        {"key": "jp1", "distance": 0.895, "metadata": {"source_text": "千と千尋の神隠し: ..."}}
    ]
}

日本語のクエリで英語のデータ(Finding Nemo)も含めて横断的に検索できています。Titan Text Embeddings V2 の多言語ベクトル空間が、言語の壁を越えた意味的な類似検索を実現しています。

フィルタ付き検索

「子供向けの楽しい映画」で genre_filter=family を指定:

{
    "query": "fun movies for kids",
    "results": [
        {
            "key": "v5",
            "distance": 0.8331,
            "metadata": {
                "source_text": "Toy Story: Toys come alive when humans are not around and go on adventures together",
                "genre": "family"
            }
        },
        {
            "key": "v3",
            "distance": 0.8878,
            "metadata": {
                "source_text": "Finding Nemo: A father fish searches the ocean to find his lost son",
                "genre": "family"
            }
        }
    ]
}

family ジャンルの2件のみが返され、フィルタも正常に動作しています。


S3 Vectors ベストプラクティス

公式ドキュメントに記載されているベストプラクティスのうち、今回の構成に関連するものをまとめます。

ベクトルの挿入・削除はバッチで

PutVectors / DeleteVectors のスループット上限は、1インデックスあたり 1,000 リクエスト/秒 または 2,500 ベクトル/秒のどちらか先に到達した方です。コスト最適化のため、最大バッチサイズの 500 件/リクエストでまとめて処理することが推奨されています。

To optimize costs, we recommend inserting and deleting vectors in large batches, up to the maximum batch size of 500 vectors per API request.

S3 Vectors best practices

今回の put_s3_vectors ツールは1件ずつの挿入ですが、MCP ツールとして対話的に使う分には問題ありません。初期データの大量投入やバッチ更新が必要な場合は、Lambda とは別にバッチスクリプトを用意して、500件ずつ PutVectors を呼ぶのが効率的です。

# バッチ投入の例(500件ずつ分割)
BATCH_SIZE = 500
for i in range(0, len(all_vectors), BATCH_SIZE):
    batch = all_vectors[i:i + BATCH_SIZE]
    s3vectors.put_vectors(
        vectorBucketName=VECTOR_BUCKET,
        indexName=VECTOR_INDEX,
        vectors=batch,
    )

クエリのスループットとレイテンシ

QueryVectors / GetVectors / ListVectors は、1インデックスあたり数百リクエスト/秒が上限です。超過すると 429 TooManyRequestsException が返るため、リトライ機構(エクスポネンシャルバックオフ)の実装が推奨されています。

Your application can achieve hundreds of QueryVectors, GetVectors, or ListVectors requests per second per S3 vector index. If you exceed the request rates, you might receive a 429 TooManyRequestsException error. We recommend you use a retry mechanism and configure your application to send fewer requests.

S3 Vectors best practices

レイテンシについては、コールドクエリ(しばらくアクセスがなかったインデックスへの初回クエリ)で1秒未満、ウォームクエリ(頻繁にアクセスされているインデックス)で最短 100 ミリ秒とされています。今回の Lambda のタイムアウトを 30 秒に設定しているのは、コールドスタート + コールドクエリの両方が重なるケースを考慮したものです。

インデックスの分割でスケールアウト

クエリ性能を上げたい場合は、ベクトルを複数のインデックスに分散させることが推奨されています。

To improve query performance per vector index, consider configuring your application to divide vectors across multiple vector indexes when possible. For example, if you have multi-tenant workloads and your application queries each tenant independently, consider storing each tenant's vectors in a separate vector index.

S3 Vectors best practices

今回は単一インデックスですが、本番でデータ量やクエリ頻度が増えた場合は、カテゴリやテナントごとにインデックスを分ける設計を検討するとよいでしょう。

非フィルタメタデータキーの活用

フィルタ条件として使わないメタデータ(テキストチャンクや内部管理情報など)は、インデックス作成時に非フィルタメタデータキーとして設定します。

When creating a vector index, configure metadata fields that don't require filtering as non-filterable metadata keys. For example, store text chunks for vector embeddings as non-filterable metadata fields when you need them only for reference.

S3 Vectors best practices

今回の Lambda では source_text をメタデータに格納していますが、これをフィルタ条件に使うことはないため、本番環境では非フィルタメタデータキーに設定するのが望ましいです。

非フィルタメタデータキーはインデックス作成時にしか設定できないため、後から変更するにはインデックスの再作成が必要です。設計段階でどのメタデータキーをフィルタに使うか検討しておくことが重要です。


AgentCore Gateway のスケーラビリティ

今回の構成を本番で使う場合、各コンポーネントのスループット上限を把握しておく必要があります。

コンポーネント デフォルト上限 引き上げ
AgentCore Gateway(tool-call 同時接続) 50 同時接続/Gateway、50 同時接続/アカウント
Gateway 呼び出しタイムアウト 15 分
Lambda 同時実行数 1,000/リージョン 可(数万まで)
S3 Vectors クエリ 数百 RPS/インデックス

Quotas for Amazon Bedrock AgentCore

ボトルネックになるのは Gateway の 50 同時接続です。Lambda(1,000 同時実行)や S3 Vectors(数百 RPS)より先に Gateway が詰まります。

ただし「50 同時接続」は「50 ユーザーまで」という意味ではありません。MCP の tool-call は1回のリクエスト/レスポンスで完結するため、1ユーザーが常時接続を占有するわけではありません。例えば1回の tool-call が平均 1 秒で完了するなら、50 同時接続で理論上 50 RPS を捌けます。実際の利用パターンは「ユーザーがチャットで質問 → エージェントが判断 → tool-call → レスポンス」という流れなので、同時に 50 接続を使い切ることはまずなく、数百人規模でも問題なく利用できるはずです。足りなくなった場合は Service Quotas から引き上げ申請が可能です。

AgentCore Gateway はあくまで「AI エージェントが MCP ツールとして呼び出す」用途に最適化されており、数千 QPS を捌く API バックエンドとしては設計されていません。大量のリクエストを処理する必要がある場合は、API Gateway + Lambda で直接 REST API を構築する方が適切です(API Gateway のデフォルトは 10,000 RPS/リージョン)。


ハマりポイントまとめ

項目 内容
Gateway の認証方式 agentcore CLI のデフォルトは Cognito(CUSTOM_JWT)。IAM 認証にするには aws bedrock-agentcore-control create-gateway --authorizer-type AWS_IAM を使う
Control Plane と Data Plane Gateway の CRUD は bedrock-agentcore-controlbedrock-agentcore は Data Plane(invoke 等)のみ
Lambda の event 構造 Gateway は arguments を event に、ツール名を context.client_context.custom['bedrockAgentCoreToolName'] に渡す。ツール名は TargetName___tool_name 形式
SigV4 署名 MCP プロトコルには SigV4 の仕組みがないため、mcp-proxy-for-aws で署名を付与する必要がある
Gateway 実行ロール agentcore CLI で一度 Gateway を作ると AgentCoreGatewayExecutionRole が自動作成される。lambda:InvokeFunction 権限を含む

クリーンアップ

# Gateway ターゲットの削除
aws bedrock-agentcore-control delete-gateway-target \
  --gateway-identifier <Gateway ID> \
  --target-id <Target ID> \
  --region us-west-2

# Gateway の削除
aws bedrock-agentcore-control delete-gateway \
  --gateway-identifier <Gateway ID> \
  --region us-west-2

# Lambda
aws lambda delete-function --function-name s3vectors-query --region us-west-2

# IAM ロール
aws iam delete-role-policy --role-name Lambda-S3Vectors-Role --policy-name S3VectorsLambdaPolicy
aws iam delete-role --role-name Lambda-S3Vectors-Role

まとめ

AgentCore Gateway を使うことで、Lambda 関数を MCP ツールとして公開し、Kiro から直接呼び出せるようになりました。IAM 認証 + mcp-proxy-for-aws の組み合わせにより、Cognito のセットアップやトークン管理なしでシンプルに接続できます。

Gateway がツール名でのルーティングと引数の受け渡しを担当してくれるため、Lambda 側のコードもシンプルに保てます。S3 Vectors のようなベクトル検索を AI アシスタントのツールとして組み込む際に、AgentCore Gateway は便利な選択肢です。

参考ドキュメント

山本 哲也 (記事一覧)

多分インフラエンジニアです。データ分析に興味あります。

山を走るのが趣味です。今年は 100 マイルレース完走します。