はじめに
2026年5月、Amazon Bedrock AgentCore RuntimeにAmazon S3 Filesをネイティブマウントできる機能が追加された。
Amazon S3 Filesは2026年4月にGAとなったサービスで、S3バケットをNFSv4.2でマウントできる仕組みだ。
これまでAmazon Bedrock AgentCore RuntimeからAmazon S3 Filesを使うには、Dockerイメージにamazon-efs-utilsを仕込んでエントリーポイントスクリプトでマウントするなど、自前で工夫する必要があった。今回のアップデートにより、Amazon Bedrock AgentCore Runtimeが自動でマウント処理を行ってくれるようになった。
本記事では、このAmazon S3 FilesマウントをTerraformで自動構築する方法を紹介する。
なお、Amazon Bedrock AgentCore Runtimeそのものの構築は以下の記事を参考にしていただきたい。
下記の記事はTerraformのAWS Providerが公式にリリースされる前のものだが、主に必要になるリソースは大きく変わらない。
Amazon S3 FilesとAgentCore Runtimeの関係
まず、構成を整理しておこう。
Amazon S3 FilesをAmazon Bedrock AgentCore Runtimeにマウントするには、以下のリソースが必要になる。
- Amazon S3バケット(データの実体)
- Amazon S3 Filesファイルシステム(バケットに紐付くファイルシステム)
- Amazon S3 Filesマウントターゲット(VPC内のAZごとに作成するNFSエンドポイント)
- Amazon S3 Filesアクセスポイント(POSIXユーザーとルートディレクトリを指定するエントリーポイント)
- VPC・サブネット・セキュリティグループ
Amazon Bedrock AgentCore Runtimeのfilesystem_configurationsにアクセスポイントのARNとマウントパスを指定することで、ランタイムの起動時に自動でNFSマウントが行われる。
Amazon S3 FilesはVPCが必須のため、Amazon Bedrock AgentCore Runtimeもnetwork_mode = "VPC"で構築する必要がある。また、同じVPC上に存在する必要がある。
Amazon S3 Filesの構築
Amazon S3バケット
まずはデータの実体となるAmazon S3バケットを作成する。
resource "aws_s3_bucket" "example" {
bucket = local.s3_bucket_name
}
resource "aws_s3_bucket_ownership_controls" "example" {
bucket = aws_s3_bucket.example.id
rule {
object_ownership = "BucketOwnerEnforced"
}
}
resource "aws_s3_bucket_public_access_block" "example" {
bucket = aws_s3_bucket.example.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
Amazon S3 Filesファイルシステム
次に、Amazon S3バケットに紐付くAmazon S3 Filesファイルシステムを作成する。
resource "aws_s3files_file_system" "example" {
bucket_arn = aws_s3_bucket.example.arn
role_arn = aws_iam_role.s3files.arn
}
Amazon S3 Filesのサービスロール
resource "aws_iam_role" "s3files" {
name = local.iam_role_s3files_name
assume_role_policy = data.aws_iam_policy_document.s3files_assume.json
}
data "aws_iam_policy_document" "s3files_assume" {
statement {
effect = "Allow"
principals {
type = "Service"
identifiers = ["s3files.amazonaws.com"]
}
actions = ["sts:AssumeRole"]
}
}
resource "aws_iam_role_policy" "s3files_custom" {
name = local.iam_policy_s3files_name
role = aws_iam_role.s3files.id
policy = data.aws_iam_policy_document.s3files_custom.json
}
data "aws_iam_policy_document" "s3files_custom" {
statement {
effect = "Allow"
actions = [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
"s3:ListBucket",
"s3:GetBucketLocation",
]
resources = [
aws_s3_bucket.example.arn,
"${aws_s3_bucket.example.arn}/*",
]
}
}
Amazon S3 Filesマウントターゲット
VPC内のAZごとにマウントターゲットを作成する。Amazon Bedrock AgentCore Runtimeのサブネットと同じAZに作成する必要がある。
resource "aws_s3files_mount_target" "example" {
file_system_id = aws_s3files_file_system.example.file_system_id
subnet_id = aws_subnet.example.id
security_groups = [aws_security_group.s3files_mount_target.id]
}
Amazon S3 Filesマウントターゲット用のセキュリティグループ
resource "aws_security_group" "s3files_mount_target" {
name = local.sg_s3files_mount_target_name
vpc_id = aws_vpc.example.id
ingress {
from_port = 2049
to_port = 2049
protocol = "tcp"
security_groups = [aws_security_group.bedrock_agentcore_runtime.id]
}
}
ポイントは、Amazon Bedrock AgentCore RuntimeのセキュリティグループからAmazon S3 Filesのマウントターゲットに対してTCPポート2049(NFS)のアウトバウンドを許可し、マウントターゲット側でそのインバウンドを受け入れる部分だ。
Amazon S3 Filesアクセスポイント
アクセスポイントでは、POSIXユーザー(UID/GID)とルートディレクトリを指定する。
posix_userのUID/GIDは、Amazon Bedrock AgentCore Runtimeのコンテナ上で動作するユーザーと合わせる必要がある。非rootコンテナの場合は1000:1000が一般的だ。UID/GIDが合っていないと、マウントは成功してもファイルの書き込みでPermission deniedになる。
resource "aws_s3files_access_point" "example" {
name = local.s3files_access_point_name
file_system_id = aws_s3files_file_system.example.file_system_id
posix_user {
uid = 1000
gid = 1000
}
root_directory {
path = "/data"
creation_info {
owner_uid = 1000
owner_gid = 1000
permissions = "0755"
}
}
}
Amazon Bedrock AgentCore Runtimeの構築
IAM
Amazon Bedrock AgentCore RuntimeにアタッチするIAMロールに、Amazon S3 Filesのマウント権限を追加する。
今回、ファイルに対してReadもWriteも行うアプリの前提であるため以下の内容になっているが、書き込みが不要な場合はs3files:ClientWriteを省略することで読み取り専用にできる。
s3files:GetAccessPointおよびs3files:ListMountTargetsのステートメントは、公式のドキュメントには記載されていないが、Amazon Bedrock AgentCore Runtimeの実行ロールにこれが付与されていないと、そもそもAmazon Bedrock AgentCore Runtimeの作成/変更時にAccess Deniedになってエラーになる。
data "aws_iam_policy_document" "bedrock_agentcore_runtime_custom" {
# (Amazon ECR, Amazon CloudWatch Logsなどの権限は省略)
statement {
effect = "Allow"
actions = [
"s3files:ClientMount",
"s3files:ClientWrite",
]
resources = [
aws_s3files_file_system.example.file_system_arn,
]
condition {
test = "ArnEquals"
variable = "s3files:AccessPointArn"
values = [
aws_s3files_access_point.example.access_point_arn,
]
}
}
statement {
effect = "Allow"
actions = [
"s3files:GetAccessPoint",
]
resources = [
aws_s3files_access_point.example.arn,
]
}
statement {
effect = "Allow"
actions = [
"s3files:ListMountTargets",
]
resources = [
aws_s3files_file_system.example.arn,
]
}
}
Amazon Bedrock AgentCore Runtime
Amazon Bedrock AgentCore Runtimeについては、filesystem_configurationsに先ほど作成したAmazon S3 Filesアクセスポイントを指定する。
前述した通り、Amazon S3 FilesはVPCが必須のため、network_mode = "VPC"を指定し、サブネットはAmazon S3 Filesのマウントターゲットのサブネットと合わせる。
filesystem_configurations.mount_pathは/mnt配下の1階層のみ指定可能なので、間違えないよう注意。
※もちろん、ファイルはもっと深い階層に作れる
resource "aws_bedrockagentcore_agent_runtime" "example" {
# (基本的な設定は省略)
network_configuration {
network_mode = "VPC"
network_mode_config {
subnets = [aws_subnet.example.id]
security_groups = [aws_security_group.bedrock_agentcore_runtime.id]
}
}
filesystem_configurations {
s3_files_access_point {
access_point_arn = aws_s3files_access_point.example.access_point_arn
mount_path = "/mnt/s3files"
}
}
environment_variables = {
AWS_REGION = data.aws_region.current.region
}
}
エージェントアプリの実装
エージェントは、Strands AgentsのPython版で作成し、Tool Useで呼び出せるように構成する。
インタフェースとなる関数に、@toolのデコレータを設定しよう。
Read/Writeどちらもできるようなツール定義をしよう。相対パスをもらい、/mnt/s3files配下の指定ファイルを読み出すか、テキストを書き込むかを行う。
import logging
from pathlib import Path
from typing import Any, Literal
from strands import tool
BASE_PATH = Path("/mnt/s3files")
MAX_SIZE_BYTES = 10 * 1024 * 1024 # 10 MiB
TOOL_NAME = "s3_files_access"
def _error_response(message: str) -> dict[str, Any]:
"""Build a standardized error response dictionary.
Args:
message: Human-readable error message describing the failure cause.
Returns:
A dictionary with `ok=False`, `tool=TOOL_NAME`, and `error=message`.
"""
return {"ok": False, "tool": TOOL_NAME, "error": message}
def _validate_path(relative_path: str) -> tuple[Path | None, str | None]:
"""Validate and normalize a relative path against ``BASE_PATH``.
Performs the following checks in order:
1. Reject inputs containing a null byte (``"\\x00"``).
2. Reject empty or whitespace-only inputs.
3. Join with ``BASE_PATH`` and resolve symlinks via ``Path.resolve(strict=False)``
so the check works for both existing and to-be-created paths.
4. Containment check using ``Path.is_relative_to`` against the resolved
``BASE_PATH`` to prevent escapes (``../``, absolute paths, symlink-based
escapes).
Args:
relative_path: User-supplied path relative to ``BASE_PATH``.
Returns:
A tuple ``(path, error)`` where exactly one element is ``None``:
* ``(Path, None)`` on success with the normalized absolute path.
* ``(None, str)`` on failure with a human-readable error message.
"""
if "\x00" in relative_path:
return None, "invalid input: null byte in path"
if not relative_path.strip():
return None, "invalid input: empty or whitespace-only path"
candidate = (BASE_PATH / relative_path).resolve(strict=False)
base_resolved = BASE_PATH.resolve(strict=False)
if not candidate.is_relative_to(base_resolved):
return None, "access denied: path escapes base directory"
return candidate, None
def _read_file(absolute_path: Path) -> dict[str, Any]:
"""Read a UTF-8 text file at ``absolute_path`` and return a structured response.
Validation order matches the design's error-handling priority: existence,
file-type, size limit, then I/O. Size is checked via ``Path.stat().st_size``
so that oversized files are rejected without ever calling ``read_text``.
Args:
absolute_path: Normalized absolute path to read. The caller is
responsible for ensuring the path is contained within ``BASE_PATH``
via ``_validate_path`` before invoking this helper.
Returns:
On success: ``{"ok": True, "tool": TOOL_NAME, "content": <text>}``.
On failure: an error response built by ``_error_response`` describing
one of: file not found, not a regular file, file too large, UTF-8
decode failure, or generic I/O error.
"""
try:
if not absolute_path.exists():
return _error_response(f"file not found: {absolute_path}")
if absolute_path.is_dir():
return _error_response(f"not a regular file: {absolute_path}")
size = absolute_path.stat().st_size
if size > MAX_SIZE_BYTES:
return _error_response(f"file too large: {size} bytes (max 10MB)")
content = absolute_path.read_text(encoding="utf-8")
except FileNotFoundError:
return _error_response(f"file not found: {absolute_path}")
except UnicodeDecodeError as exc:
return _error_response(f"cannot decode file as UTF-8: {exc.reason}")
except OSError as exc:
return _error_response(f"IO error: {exc}")
return {"ok": True, "tool": TOOL_NAME, "content": content}
def _write_file(absolute_path: Path, content: str) -> dict[str, Any]:
"""Write UTF-8 text ``content`` to ``absolute_path`` and return a structured response.
The size limit is enforced on the UTF-8 encoded byte length (not character
count) so that multibyte payloads are bounded predictably. When the size
exceeds ``MAX_SIZE_BYTES`` the function returns an error response without
performing any filesystem I/O. Missing parent directories are created via
``mkdir(parents=True, exist_ok=True)`` and existing files are overwritten.
Args:
absolute_path: Normalized absolute path to write. The caller is
responsible for ensuring the path is contained within ``BASE_PATH``
via ``_validate_path`` before invoking this helper.
content: UTF-8 text content to write. An empty string yields a
zero-byte file.
Returns:
On success: ``{"ok": True, "tool": TOOL_NAME, "path": str(absolute_path)}``.
On failure: an error response built by ``_error_response`` describing
either a content-too-large rejection or a generic I/O error.
"""
size = len(content.encode("utf-8"))
if size > MAX_SIZE_BYTES:
return _error_response(f"content too large: {size} bytes (max 10MB)")
try:
absolute_path.parent.mkdir(parents=True, exist_ok=True)
absolute_path.write_text(content, encoding="utf-8")
except OSError as exc:
return _error_response(f"IO error: {exc}")
return {"ok": True, "tool": TOOL_NAME, "path": str(absolute_path)}
@tool
def s3_files_access(
operation: Literal["read", "write"],
relative_path: str,
content: str = "",
) -> dict[str, Any]:
"""Read from or write to a UTF-8 text file under the S3 Files mount point (/mnt/s3files).
The tool exposes a single entry point for both reading and writing text files
located beneath the fixed base path ``/mnt/s3files``. Path inputs are
validated and normalized so they cannot escape the base directory via
``../``, absolute paths, or symlinks. Content is handled as UTF-8 text only,
with a 10MB size limit applied to both reads and writes.
Args:
operation: Operation kind. ``"read"`` reads an existing file as UTF-8
text. ``"write"`` writes UTF-8 text content to the target path,
creating any missing parent directories and overwriting an existing
file. Any other value yields an error response.
relative_path: Path relative to ``/mnt/s3files``. Must not be empty,
whitespace-only, contain a null byte, or resolve outside the base
directory.
content: UTF-8 text content to write. Used only when ``operation`` is
``"write"``; ignored for ``"read"``. An empty string writes a
zero-byte file.
Returns:
A ``dict[str, Any]`` describing the result.
On read success::
{"ok": True, "tool": "s3_files_access", "content": <str>}
On write success::
{"ok": True, "tool": "s3_files_access", "path": <absolute path str>}
On failure::
{"ok": False, "tool": "s3_files_access", "error": <message str>}
"""
if operation not in ("read", "write"):
result = _error_response("invalid operation: must be 'read' or 'write'")
console.log(f"s3_files_access: {operation} failed reason={result['error']}")
return result
absolute_path, error = _validate_path(relative_path)
if error is not None or absolute_path is None:
result = _error_response(error or "invalid input: path validation failed")
console.log(f"s3_files_access: {operation} failed reason={result['error']}")
return result
if operation == "read":
result = _read_file(absolute_path)
else:
result = _write_file(absolute_path, content)
if result.get("ok"):
console.log(f"s3_files_access: {operation} ok path={absolute_path}")
else:
console.log(f"s3_files_access: {operation} failed reason={result.get('error')}")
return result
いざ、動かす!
terraform applyでAmazon S3 Files関連リソースを作成し、Amazon Bedrock AgentCore RuntimeやIAMの周辺リソースを修正したら、作ったtoolをエージェントに組み込んで(Strands AgentsのAgent.toolsに設定して)、実行してみよう。
Amazon S3バケットにも、バッチリ作成したファイルオブジェクトが存在している!
これで、Amazon S3 FilesをマウントしたAmazon Bedrock AgentCore Runtimeが動かせるようになった!

