2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Amazon S3 FilesをマウントしたAmazon Bedrock AgentCore RuntimeをTerraformで自動構築する

2
Last updated at Posted at 2026-05-23

はじめに

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に設定して)、実行してみよう。

image.png

Amazon S3バケットにも、バッチリ作成したファイルオブジェクトが存在している!

image.png

これで、Amazon S3 FilesをマウントしたAmazon Bedrock AgentCore Runtimeが動かせるようになった!

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?