AgentCore Runtime に Claude Agent SDK をデプロイして S3 上のドキュメントを検索してみた

AgentCore Runtime に Claude Agent SDK をデプロイして S3 上のドキュメントを検索してみた

2026.05.08

はじめに

こんにちは、スーパーマーケット好きのコンサル部の神野(じんの)です。

前回、Bedrock AgentCore Runtime に追加された Amazon S3 Files をネイティブマウントする機能の検証をしました。

https://dev.classmethod.jp/articles/bedrock-agentcore-runtime-s3-files-native-mount/

前回はめちゃくちゃシンプルなHTTPサーバーを実装しただけだったので、この仕組みを使って、エージェントを作ってみたくなりました。

そこで今まで触ってこなかったClaude Agent SDKを AgentCore Runtime のコンテナで動かして、S3 に置いたドキュメント群から必要な情報を探し当てる、簡易的なファイル検索エージェントを作ってみます!

https://code.claude.com/docs/en/agent-sdk/overview

今回作るもの

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 / ロール)

本記事で使用するコードの全量は下記リポジトリにあります。必要に応じてご参照ください。

https://github.com/yuu551/agentcore-filesystem-demo

実装

ここからはコンテナの実装に入っていきます! ECR に push する Python ベースのコンテナです。

CFn スタック(前回 + bedrock:InvokeModel 追加)

前回記事の CFn スタックをベースに、AgentCoreのロールにbedrock:InvokeModel のポリシーを1つ追加しています。

s3files-stack.yaml(CFn テンプレート全体)
s3files-stack.yaml
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 に任せます。
処理もシンプルでユーザーからのプロンプトを受け付けて処理を実施するだけです。

main.py
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()

ClaudeAgentOptionscwd/mnt/data にすることで、エージェントはマウントされた S3 Files をホームディレクトリのように扱えます。

allowed_tools に列挙したツールは事前承認されます。また、permission_mode="acceptEdits" により、ファイル編集や mkdir / rm / mv などのファイルシステム操作も自動承認されます。

https://code.claude.com/docs/en/agent-sdk/permissions

パラメータで CLAUDE_CODE_USE_BEDROCK=1 を渡しているので、Bedrock 経由で Claude を呼んでくれます。AWS の認証情報はコンテナのサービスロール(SigV4)からそのまま引き継がれるので、API キーやベアラートークンの管理は不要です。

Dockerfile

次に Dockerfile を書いていきます。claude-agent-sdk は内部で Claude Code CLI を起動するため、CLI が依存している Node.js も入れておく必要があります。

Dockerfile
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 アクセスポイントの posixUserUid: 1000, Gid: 1000)と揃えるためです。

デプロイ

ECR へのイメージ push、AgentCore Runtime の作成、S3 へのナレッジアップロードまでをまとめた deploy.sh を用意しました。

デプロイ
./deploy.sh

内部では以下を順番に実行しています。

  1. CFn デプロイ(VPC / S3 Files / IAM ロール)
  2. Docker イメージのビルド & ECR push
  3. AgentCore Runtime の作成
  4. S3 バケットの agent/ プレフィックスにナレッジファイルをアップロード
  5. invoke_agent.py の RUNTIME_ARN を自動更新

詳細はリポジトリの deploy.sh を参照してください。

https://github.com/yuu551/agentcore-filesystem-demo/blob/main/deploy.sh

デプロイには7-8分かかりました。

動作確認

ここからはデプロイしたエージェントを呼び出して、ファイル検索エージェントとしてちゃんと動くか試してみます!

呼び出しクライアント

まず、ランタイムを呼び出すクライアントスクリプトを用意しておきます。レスポンスをストリームで受け取って、チャンクが届くたびに出力する構成にしてみました。

invoke_agent.py
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でも試してみたいですね!

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


生成AI活用はクラスメソッドにお任せ

過去に支援してきた生成AIの支援実績100+を元にホワイトペーパーを作成しました。御社が抱えている課題のうち、どれが解決できて、どのようなサービスが受けられるのか?4つのフェーズに分けてまとめています。どうぞお気軽にご覧ください。

生成AI資料イメージ

無料でダウンロードする

この記事をシェアする

関連記事