
AgentCore Runtime に Claude Agent SDK をデプロイして S3 上のドキュメントを検索してみた
はじめに
こんにちは、スーパーマーケット好きのコンサル部の神野(じんの)です。
前回、Bedrock AgentCore Runtime に追加された Amazon S3 Files をネイティブマウントする機能の検証をしました。
前回はめちゃくちゃシンプルなHTTPサーバーを実装しただけだったので、この仕組みを使って、エージェントを作ってみたくなりました。
そこで今まで触ってこなかったClaude Agent SDKを AgentCore Runtime のコンテナで動かして、S3 に置いたドキュメント群から必要な情報を探し当てる、簡易的なファイル検索エージェントを作ってみます!
今回作るもの
S3 バケットの agent プレフィックスをドキュメント置き場にして、そこに AgentCore Runtime や S3 Files のリファレンス、VPC チェックリスト、サンプルコードなどを配置します。
エージェントには「〇〇についての情報はどこ?」と自然言語で聞いて問い合わせを行います。エージェント側は Grepツールでファイルを絞って、Readツールで中身を読んで、要点だけ日本語で返してくれる、というイメージです。
LLM は Amazon Bedrock 経由の Claude Haiku 4.5、ファイルシステムは S3 Files マウント、エージェントは Read / Write / Bash / Grep / Glob といった組み込みツールを駆使します!
前提
| 項目 | 値 |
|---|---|
| リージョン | us-east-1 |
| Python | 3.12 |
| Python パッケージ管理 | uv |
| claude-agent-sdk | 0.1.76 |
| bedrock-agentcore | 1.9.0 |
| boto3 / botocore | 1.43.5 |
| Bedrock のモデル | us.anthropic.claude-haiku-4-5-20251001-v1:0 |
| Docker | buildx で linux/arm64 ビルドできること |
| 前提インフラ | 前回記事の CFn スタック(VPC / S3 Files / ロール) |
本記事で使用するコードの全量は下記リポジトリにあります。必要に応じてご参照ください。
実装
ここからはコンテナの実装に入っていきます! ECR に push する Python ベースのコンテナです。
CFn スタック(前回 + bedrock:InvokeModel 追加)
前回記事の CFn スタックをベースに、AgentCoreのロールにbedrock:InvokeModel のポリシーを1つ追加しています。
s3files-stack.yaml(CFn テンプレート全体)
AWSTemplateFormatVersion: "2010-09-09"
Description: VPC + S3 Files (FileSystem + MountTargets + AccessPoint) for AgentCore Runtime verification
Parameters:
Suffix:
Type: String
Default: fsverify
Resources:
# ---- VPC ----
Vpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.20.0.0/16
EnableDnsSupport: true
EnableDnsHostnames: true
Tags: [{Key: Name, Value: !Sub "${Suffix}-vpc"}]
Igw:
Type: AWS::EC2::InternetGateway
Properties:
Tags: [{Key: Name, Value: !Sub "${Suffix}-igw"}]
IgwAttach:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref Vpc
InternetGatewayId: !Ref Igw
# ---- Private subnets (AgentCore Runtime ENI + S3 Files Mount Target) ----
SubnetA:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref Vpc
CidrBlock: 10.20.1.0/24
AvailabilityZone: !Select [0, !GetAZs ""]
MapPublicIpOnLaunch: false
Tags: [{Key: Name, Value: !Sub "${Suffix}-private-a"}]
SubnetB:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref Vpc
CidrBlock: 10.20.2.0/24
AvailabilityZone: !Select [1, !GetAZs ""]
MapPublicIpOnLaunch: false
Tags: [{Key: Name, Value: !Sub "${Suffix}-private-b"}]
# ---- Public subnets (for NAT Gateway) ----
PublicSubnetA:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref Vpc
CidrBlock: 10.20.101.0/24
AvailabilityZone: !Select [0, !GetAZs ""]
MapPublicIpOnLaunch: true
Tags: [{Key: Name, Value: !Sub "${Suffix}-public-a"}]
PublicSubnetB:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref Vpc
CidrBlock: 10.20.102.0/24
AvailabilityZone: !Select [1, !GetAZs ""]
MapPublicIpOnLaunch: true
Tags: [{Key: Name, Value: !Sub "${Suffix}-public-b"}]
# ---- NAT Gateway (in PublicSubnetA) ----
EipNat:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
Tags: [{Key: Name, Value: !Sub "${Suffix}-eip-nat"}]
NatGateway:
Type: AWS::EC2::NatGateway
DependsOn: IgwAttach
Properties:
AllocationId: !GetAtt EipNat.AllocationId
SubnetId: !Ref PublicSubnetA
Tags: [{Key: Name, Value: !Sub "${Suffix}-nat"}]
# ---- Public route table ----
PublicRt:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref Vpc
Tags: [{Key: Name, Value: !Sub "${Suffix}-public-rt"}]
PublicRtIgwRoute:
Type: AWS::EC2::Route
DependsOn: IgwAttach
Properties:
RouteTableId: !Ref PublicRt
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref Igw
PublicRtAssocA:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnetA
RouteTableId: !Ref PublicRt
PublicRtAssocB:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnetB
RouteTableId: !Ref PublicRt
# ---- Private route table (NAT + S3 Gateway Endpoint) ----
Rt:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref Vpc
Tags: [{Key: Name, Value: !Sub "${Suffix}-private-rt"}]
RtDefault:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref Rt
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NatGateway
RtAssocA:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref SubnetA
RouteTableId: !Ref Rt
RtAssocB:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref SubnetB
RouteTableId: !Ref Rt
# ---- Security Group (self-referenced NFS 2049) ----
Sg:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: !Sub "${Suffix} S3 Files NFS"
VpcId: !Ref Vpc
Tags: [{Key: Name, Value: !Sub "${Suffix}-sg"}]
SgIngressNfsSelf:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref Sg
IpProtocol: tcp
FromPort: 2049
ToPort: 2049
SourceSecurityGroupId: !Ref Sg
# ---- S3 Gateway VPC Endpoint (free, used for ECR layer pulls) ----
EndpointS3:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcId: !Ref Vpc
ServiceName: !Sub "com.amazonaws.${AWS::Region}.s3"
VpcEndpointType: Gateway
RouteTableIds: [!Ref Rt]
# ---- S3 bucket (versioning + SSE-S3) ----
Bucket:
Type: AWS::S3::Bucket
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
BucketName: !Sub "${Suffix}-${AWS::AccountId}-${AWS::Region}"
VersioningConfiguration:
Status: Enabled
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
# ---- IAM role assumed by S3 Files service ----
S3FilesServiceRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub "${Suffix}-s3files-service-role"
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal: {Service: elasticfilesystem.amazonaws.com}
Action: sts:AssumeRole
Condition:
StringEquals: {aws:SourceAccount: !Ref AWS::AccountId}
ArnLike: {aws:SourceArn: !Sub "arn:aws:s3files:${AWS::Region}:${AWS::AccountId}:file-system/*"}
Policies:
- PolicyName: s3files-bucket-access
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: S3BucketPermissions
Effect: Allow
Action: [s3:ListBucket, s3:ListBucketVersions]
Resource: !GetAtt Bucket.Arn
Condition:
StringEquals: {aws:ResourceAccount: !Ref AWS::AccountId}
- Sid: S3ObjectPermissions
Effect: Allow
Action:
- s3:AbortMultipartUpload
- s3:DeleteObject*
- s3:GetObject*
- s3:List*
- s3:PutObject*
Resource: !Sub "${Bucket.Arn}/*"
Condition:
StringEquals: {aws:ResourceAccount: !Ref AWS::AccountId}
- Sid: EventBridgeManage
Effect: Allow
Action: [events:DeleteRule, events:DisableRule, events:EnableRule, events:PutRule, events:PutTargets, events:RemoveTargets]
Condition:
StringEquals: {events:ManagedBy: elasticfilesystem.amazonaws.com}
Resource: arn:aws:events:*:*:rule/DO-NOT-DELETE-S3-Files*
- Sid: EventBridgeRead
Effect: Allow
Action: [events:DescribeRule, events:ListRuleNamesByTarget, events:ListRules, events:ListTargetsByRule]
Resource: arn:aws:events:*:*:rule/*
# ---- S3 Files: FileSystem ----
FileSystem:
Type: AWS::S3Files::FileSystem
DependsOn: S3FilesServiceRole
Properties:
Bucket: !GetAtt Bucket.Arn
RoleArn: !GetAtt S3FilesServiceRole.Arn
AcceptBucketWarning: true
Tags:
- Key: Name
Value: !Sub "${Suffix}-fs"
# ---- S3 Files: Mount Targets (one per private subnet) ----
MountTargetA:
Type: AWS::S3Files::MountTarget
Properties:
FileSystemId: !Ref FileSystem
SubnetId: !Ref SubnetA
SecurityGroups: [!Ref Sg]
MountTargetB:
Type: AWS::S3Files::MountTarget
Properties:
FileSystemId: !Ref FileSystem
SubnetId: !Ref SubnetB
SecurityGroups: [!Ref Sg]
# ---- S3 Files: Access Point ----
AccessPoint:
Type: AWS::S3Files::AccessPoint
Properties:
FileSystemId: !Ref FileSystem
PosixUser:
Uid: 1000
Gid: 1000
RootDirectory:
Path: /agent
CreationPermissions:
OwnerUid: 1000
OwnerGid: 1000
Permissions: "0755"
Tags:
- Key: Name
Value: !Sub "${Suffix}-ap"
# ---- AgentCore Runtime role ----
RuntimeRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub "${Suffix}-runtime-role"
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal: {Service: bedrock-agentcore.amazonaws.com}
Action: sts:AssumeRole
Condition:
StringEquals: {aws:SourceAccount: !Ref AWS::AccountId}
ArnLike: {aws:SourceArn: !Sub "arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:*"}
Policies:
- PolicyName: ecr-pull
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action: [ecr:BatchGetImage, ecr:GetDownloadUrlForLayer, ecr:GetAuthorizationToken]
Resource: "*"
- PolicyName: cloudwatch-logs
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action: [logs:CreateLogGroup, logs:CreateLogStream, logs:PutLogEvents, logs:DescribeLogStreams]
Resource: !Sub "arn:aws:logs:*:${AWS::AccountId}:log-group:/aws/bedrock-agentcore/*"
- PolicyName: workload-identity
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- bedrock-agentcore:GetWorkloadAccessToken
- bedrock-agentcore:GetWorkloadAccessTokenForJWT
- bedrock-agentcore:GetWorkloadAccessTokenForUserId
Resource: "*"
- PolicyName: s3files-client
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action: [s3files:*, elasticfilesystem:*]
Resource: "*"
- PolicyName: bedrock-invoke
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- bedrock:InvokeModel
- bedrock:InvokeModelWithResponseStream
Resource:
- !Sub "arn:aws:bedrock:*::foundation-model/anthropic.claude-*"
- !Sub "arn:aws:bedrock:*:${AWS::AccountId}:inference-profile/*anthropic.claude-*"
Outputs:
VpcId: {Value: !Ref Vpc}
SubnetIds: {Value: !Sub "${SubnetA},${SubnetB}"}
SecurityGroupId: {Value: !Ref Sg}
BucketName: {Value: !Ref Bucket}
FileSystemId: {Value: !Ref FileSystem}
AccessPointArn: {Value: !GetAtt AccessPoint.AccessPointArn}
RuntimeRoleArn: {Value: !GetAtt RuntimeRole.Arn}
リージョナル推論プロファイル(us.anthropic.claude-...)にも対応するため、Resource には inference-profile の ARN も含めています。
main.py(Claude Agent SDK + BedrockAgentCoreApp)
続いて、エージェントの本体を実装していきます。HTTP サーバ部分は bedrock-agentcore パッケージの BedrockAgentCoreApp に任せます。
処理もシンプルでユーザーからのプロンプトを受け付けて処理を実施するだけです。
import asyncio
import os
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from claude_agent_sdk import (
AssistantMessage,
ClaudeAgentOptions,
ResultMessage,
TextBlock,
ToolUseBlock,
query,
)
MOUNT = os.environ.get("MOUNT_PATH", "/mnt/data")
app = BedrockAgentCoreApp()
async def run_agent(prompt: str) -> dict:
options = ClaudeAgentOptions(
cwd=MOUNT,
allowed_tools=["Read", "Write", "Bash", "Grep", "Glob"],
permission_mode="acceptEdits",
max_turns=8,
env={
"CLAUDE_CODE_USE_BEDROCK": "1",
"AWS_REGION": "us-east-1",
},
model="us.anthropic.claude-haiku-4-5-20251001-v1:0",
)
text_chunks: list[str] = []
tool_calls: list[dict] = []
result_summary: dict = {}
async for msg in query(prompt=prompt, options=options):
if isinstance(msg, AssistantMessage):
for block in msg.content:
if isinstance(block, TextBlock):
text_chunks.append(block.text)
elif isinstance(block, ToolUseBlock):
tool_calls.append({"name": block.name, "input": block.input})
elif isinstance(msg, ResultMessage):
result_summary = {
"duration_ms": msg.duration_ms,
"num_turns": msg.num_turns,
"stop_reason": msg.stop_reason,
"total_cost_usd": msg.total_cost_usd,
}
return {
"answer": "".join(text_chunks),
"tool_calls": tool_calls,
"result": result_summary,
}
@app.entrypoint
def invoke(payload):
prompt = payload.get("prompt", "")
return asyncio.run(run_agent(prompt))
if __name__ == "__main__":
app.run()
ClaudeAgentOptions の cwd を /mnt/data にすることで、エージェントはマウントされた S3 Files をホームディレクトリのように扱えます。
allowed_tools に列挙したツールは事前承認されます。また、permission_mode="acceptEdits" により、ファイル編集や mkdir / rm / mv などのファイルシステム操作も自動承認されます。
パラメータで CLAUDE_CODE_USE_BEDROCK=1 を渡しているので、Bedrock 経由で Claude を呼んでくれます。AWS の認証情報はコンテナのサービスロール(SigV4)からそのまま引き継がれるので、API キーやベアラートークンの管理は不要です。
Dockerfile
次に Dockerfile を書いていきます。claude-agent-sdk は内部で Claude Code CLI を起動するため、CLI が依存している Node.js も入れておく必要があります。
FROM --platform=linux/arm64 public.ecr.aws/docker/library/python:3.12-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
curl \
nodejs \
npm \
&& rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir \
'claude-agent-sdk>=0.1.76' \
'bedrock-agentcore>=1.9.0'
# S3 Files の access point posixUser に合わせて uid 1000 で動かす
RUN groupadd -g 1000 agent && useradd -m -u 1000 -g agent agent
USER 1000:1000
WORKDIR /home/agent
ENV HOME=/home/agent
COPY --chown=agent:agent main.py /home/agent/main.py
EXPOSE 8080
CMD ["python", "/home/agent/main.py"]
uid 1000 の agent ユーザーで動かしているのは、S3 Files アクセスポイントの posixUser(Uid: 1000, Gid: 1000)と揃えるためです。
デプロイ
ECR へのイメージ push、AgentCore Runtime の作成、S3 へのナレッジアップロードまでをまとめた deploy.sh を用意しました。
./deploy.sh
内部では以下を順番に実行しています。
- CFn デプロイ(VPC / S3 Files / IAM ロール)
- Docker イメージのビルド & ECR push
- AgentCore Runtime の作成
- S3 バケットの
agent/プレフィックスにナレッジファイルをアップロード invoke_agent.pyの RUNTIME_ARN を自動更新
詳細はリポジトリの deploy.sh を参照してください。
デプロイには7-8分かかりました。
動作確認
ここからはデプロイしたエージェントを呼び出して、ファイル検索エージェントとしてちゃんと動くか試してみます!
呼び出しクライアント
まず、ランタイムを呼び出すクライアントスクリプトを用意しておきます。レスポンスをストリームで受け取って、チャンクが届くたびに出力する構成にしてみました。
import boto3
import json
import sys
import time
import uuid
REGION = "us-east-1"
RUNTIME_ARN = "arn:aws:bedrock-agentcore:us-east-1:<AccountId>:runtime/fsverify_cas_agent-XXXXXXXXXX"
def invoke(prompt: str) -> None:
client = boto3.client("bedrock-agentcore", region_name=REGION)
session_id = ("cas-" + uuid.uuid4().hex + uuid.uuid4().hex)[:60]
print(f"session: {session_id}")
print(f"prompt: {prompt}\n")
t0 = time.time()
resp = client.invoke_agent_runtime(
agentRuntimeArn=RUNTIME_ARN,
runtimeSessionId=session_id,
payload=json.dumps({"prompt": prompt}).encode(),
)
stream = resp["response"]
buf = b""
for chunk in stream.iter_chunks():
buf += chunk
elapsed = time.time() - t0
result = json.loads(buf.decode("utf-8"))
# ツール呼び出しの過程を表示
for i, tc in enumerate(result.get("tool_calls", []), 1):
name = tc["name"]
inp = tc.get("input", {})
summary = inp.get("pattern") or inp.get("file_path") or inp.get("command") or ""
print(f" [{i}] {name}({summary})")
print()
print(result.get("answer", ""))
print()
r = result.get("result", {})
print(f"--- {r.get('num_turns', '?')} turns | {elapsed:.1f}s | ${r.get('total_cost_usd', 0):.4f} ---")
if __name__ == "__main__":
prompt = sys.argv[1] if len(sys.argv) > 1 else (
"/mnt/data 配下のファイル一覧を教えてください。"
)
invoke(prompt)
レスポンスからツール呼び出しの過程を取り出して、どのツールが何の引数で呼ばれたかを表示しています。
ファイルシステムマウントの種類を聞く
まずはドキュメントを横断して概念を整理してもらいます。
uv run python invoke_agent.py "AgentCore Runtime のファイルシステムマウントの種類を教えて"
[1] Grep(mount|filesystem|sessionStorage|s3FilesAccessPoint)
[2] Read(/mnt/data/agentcore-runtime-overview.md)
[3] Read(/mnt/data/s3-files-guide.md)
[4] Read(/mnt/data/vpc-networking-checklist.md)
AgentCore Runtime のファイルシステムマウント機能は3種類あります。
| 種類 | スコープ | VPC要件 |
|------|----------|---------|
| sessionStorage | セッション単位で隔離 | 不要 |
| s3FilesAccessPoint | ランタイム全体で共有 | 必須 |
| efsAccessPoint | ランタイム全体で共有 | 必須 |
--- 2 turns | 51.0s | $0.0823 ---
おお、ちゃんと動いていますね!! Grep で関連キーワードを含むファイルを絞り込み、agentcore-runtime-overview.md / s3-files-guide.md / vpc-networking-checklist.md の3ファイルを Read して横断的に情報を集めてくれています。
Python コードからツール定義を探す
次はコードの中身を読み解いてもらいます。
uv run python invoke_agent.py "/mnt/data にある Python コードを探して、どんなツールが定義されているか教えて"
[1] Glob(/mnt/data/**/*.py)
[2] Read(/mnt/data/sample-strands-agent.py)
/mnt/data/sample-strands-agent.py に以下の2つのツールが定義されています:
1. get_weather(city: str) - 指定された都市の天気を取得(東京/大阪/札幌/福岡に対応)
2. calculate(expression: str) - 数式を計算
Strands Agents フレームワーク(AWS 推奨)を使用しており、
AgentCore Runtime にデプロイ可能なエージェントとなっています。
--- 3 turns | 25.3s | $0.0137 ---
Glob でPythonファイルを探して、見つかったファイルを Read してコードの中身を分析する、という流れを自律的に組み立ててくれていますね。
おわりに
今回はファイル検索デモまででしたが、自分でClaude Codeのようなものが作れて面白いですね。
ファイルベースで探索するAIエージェントもこれから色々と作れそうです。Strands Agentsでも試してみたいですね!
本記事が少しでも参考になりましたら幸いです。最後までご覧いただきありがとうございました!










