はじめに
7/16にパブリックプレビューが発表されたAmazon Bedrock AgentCoreは、AWS Lambdaのような感覚でサーバレスにAIエージェントをホストできるPaaS的なマネージドサービスだ。今後のAIエージェント開発における重要な要素になるだろう。
一方で、
- AWSの基礎的な構築技術
- 認証に関する基礎的な知識
- Dockerコンテナに対する理解
- AIエージェント開発のノウハウ
と、全部盛りでの知識が求められるため、これをすぐに自力で使いこなすにはハードルが高いと思われる人も多いだろう。
Bedrock AgentCore Starter Toolkitというお手軽構築ツールもあるが、やはり、既に運営している自動化基盤がある場合はそこに取り込んで自動構築をしたいだろう。
ということで、TerraformでAmazon Bedrock AgentCoreを自動構築できるようにする。
とはいえ、まだプレビュー版のローンチ直後でAWS Providerで非対応のため、無理矢理通す方法となるためご了承いただきたい。
自動構築はできるが、terraform destroy
で一部残ってしまうリソースがあることをご留意いただきたい。
全体構成
今回作成するプロジェクトの全体構成は以下の通りだ。
script/agents
がAmazon Bedrock AgentCore上で動作するAIエージェント、script/interface
がそのAIエージェントを実行するためのスクリプトだ。
bedrock-agentcore-example
├── script
│ ├── agents
│ │ ├── .bedrock_agentcore.yaml
│ │ ├── .dockerignore
│ │ ├── Dockerfile
│ │ ├── main.py
│ │ └── requirements.txt
│ └── interface
│ ├── .env
│ ├── main.py
│ └── requirements.txt
└── terraform
├── 01_main.tf
├── 02_variable.tf
├── 11_iam.tf
├── 12_ecr.tf
├── 13_cloudwatch_logs.tf
├── 21_1_agentcore.tf
├── 21_2_agentcore.py
└── template_file
└── script
├── agents
│ ├── .bedrock_agentcore.yaml.tmpl
│ └── Dockerfile.tmpl
└── interface
└── .env.tmpl
Terraformの構成全体の定義
Terraformで作成するリソース名は以下のファイルで定義している。
リージョンは東京リージョンでも良いが、LLMのモデルのスロットリングに悩まされるのも嫌なので、バージニアリージョンを指定している。当然ながら、Amazon Bedrock AgentCoreはプレビューでプロダクション利用はできないため、将来的にプロダクション利用する際は、利用する組織の規約に従いリージョンを決定していただきたい。
provider "aws" {
region = "us-east-1"
default_tags {
tags = {
Name = var.default_tag_name
}
}
}
data "aws_region" "current" {}
data "aws_caller_identity" "self" {}
variable "prefix" {
default = "プロジェクト識別のための名前"
}
variable "default_tag_name" {
type = string
default = "任意のタグ名(タグ付与可能な全リソースに付与される)"
}
locals {
iam_role_name = "${var.prefix}-role"
iam_policy_name = "${var.prefix}-policy"
ecr_repository_name = "${var.prefix}-repository"
bac_runtime_name = replace("${var.prefix}-runtime", "-", "_")
}
事前準備しておくリソース
IAM
IAMについては、以下のドキュメントを参考に作成する。
最後の"pricing:*"
をAllowするステートメントのみ、ドキュメントの内容に追加している。
これは、今回作るAIエージェントに組み込むAWS Pricing MCP Serverに必要な権限だ。
※今回、せっかくなので、MCP Serverまで組み込んだAIエージェントを作っている。
resource "aws_iam_role" "example" {
name = local.iam_role_name
assume_role_policy = data.aws_iam_policy_document.assume.json
}
data "aws_iam_policy_document" "assume" {
statement {
effect = "Allow"
principals {
type = "Service"
identifiers = [
"bedrock-agentcore.amazonaws.com",
]
}
actions = [
"sts:AssumeRole",
]
condition {
test = "StringEquals"
variable = "aws:SourceAccount"
values = [
data.aws_caller_identity.self.id,
]
}
condition {
test = "ArnLike"
variable = "AWS:SourceArn"
values = [
"arn:aws:bedrock-agentcore:${data.aws_region.current.region}:${data.aws_caller_identity.self.id}:*",
]
}
}
}
resource "aws_iam_role_policy" "custom" {
name = local.iam_policy_name
role = aws_iam_role.example.id
policy = data.aws_iam_policy_document.custom.json
}
data "aws_iam_policy_document" "custom" {
statement {
effect = "Allow"
actions = [
"ecr:BatchGetImage",
"ecr:GetDownloadUrlForLayer",
]
resources = [
"arn:aws:ecr:${data.aws_region.current.region}:${data.aws_caller_identity.self.id}:repository/*",
]
}
statement {
effect = "Allow"
actions = [
"ecr:GetAuthorizationToken",
]
resources = [
"*",
]
}
statement {
effect = "Allow"
actions = [
"logs:DescribeLogStreams",
"logs:CreateLogGroup",
]
resources = [
"arn:aws:logs:${data.aws_region.current.region}:${data.aws_caller_identity.self.id}:log-group:/aws/bedrock-agentcore/runtimes/*",
]
}
statement {
effect = "Allow"
actions = [
"logs:DescribeLogGroups",
]
resources = [
"arn:aws:logs:${data.aws_region.current.region}:${data.aws_caller_identity.self.id}:log-group:*",
]
}
statement {
effect = "Allow"
actions = [
"logs:CreateLogStream",
"logs:PutLogEvents",
]
resources = [
"arn:aws:logs:${data.aws_region.current.region}:${data.aws_caller_identity.self.id}:log-group:/aws/bedrock-agentcore/runtimes/*:log-stream:*",
]
}
statement {
effect = "Allow"
actions = [
"xray:PutTraceSegments",
"xray:PutTelemetryRecords",
"xray:GetSamplingRules",
"xray:GetSamplingTargets",
]
resources = [
"*",
]
}
statement {
effect = "Allow"
actions = [
"cloudwatch:PutMetricData",
]
resources = [
"*",
]
condition {
test = "StringEquals"
variable = "cloudwatch:namespace"
values = [
"bedrock-agentcore",
]
}
}
statement {
effect = "Allow"
actions = [
"bedrock-agentcore:GetWorkloadAccessToken",
"bedrock-agentcore:GetWorkloadAccessTokenForJWT",
"bedrock-agentcore:GetWorkloadAccessTokenForUserId",
]
resources = [
"arn:aws:bedrock-agentcore:${data.aws_region.current.region}:${data.aws_caller_identity.self.id}:workload-identity-directory/default",
# "arn:aws:bedrock-agentcore:${data.aws_region.current.region}:${data.aws_caller_identity.self.id}:workload-identity-directory/default/workload-identity/agentName-*",
"arn:aws:bedrock-agentcore:${data.aws_region.current.region}:${data.aws_caller_identity.self.id}:workload-identity-directory/default/workload-identity/*",
]
}
statement {
effect = "Allow"
actions = [
"bedrock:InvokeModel",
"bedrock:InvokeModelWithResponseStream",
]
resources = [
"arn:aws:bedrock:*::foundation-model/*",
"arn:aws:bedrock:${data.aws_region.current.region}:${data.aws_caller_identity.self.id}:*",
]
}
statement {
effect = "Allow"
actions = [
"pricing:*",
]
resources = [
"*",
]
}
}
Amazon ECR
Amazon ECRの作成は特段難しくないが、今回、アプリケーションの更新時に自動で再Pushするようにしたいため、terraform_data.image_push.triggers_replace
で、AIエージェントのメインモジュールと、Dockerfileを更新したときにトリガーするようにしている。
また、docker build
のところが通常と異なっているが、これは、Amazon Bedrock AgentCoreがARM64プロセッサのコンテナでしか動作しないため、ビルド設定を変更している。このビルド方法であれば、AMDのプロセッサでもARM用のコンテナがビルドできる。
Dockerfileの内容については後述。
resource "aws_ecr_repository" "example" {
name = local.ecr_repository_name
image_tag_mutability = "MUTABLE"
force_delete = true
}
data "aws_ecr_authorization_token" "token" {}
resource "terraform_data" "image_push" {
triggers_replace = {
template_hash = sha256(join("", [filesha256("../script/agents/main.py"), filesha256("./template_file/script/agents/Dockerfile.tmpl")]))
}
provisioner "local-exec" {
command = <<-EOF
docker buildx build --platform linux/arm64 ../script/agents -t ${aws_ecr_repository.example.repository_url}:latest; \
docker login -u AWS -p ${data.aws_ecr_authorization_token.token.password} ${data.aws_ecr_authorization_token.token.proxy_endpoint}; \
docker push ${aws_ecr_repository.example.repository_url}:latest
EOF
}
}
Amazon Bedrock AgentCoreの作成
さて、いよいよ本丸のリソースを作っていく。
externalデータソース
今回は、Terraformのexternalデータソースを利用する。
terraform_dataデータソースでCLIで実行しようとすると、2回目以降の実行でリソースが既に存在するためにエラーになるケースがあるので、スクリプト内で修正リソース作成前に参照してチェックをしよう。
ポイントは、スクリプトの正常終了時に標準出力でagent_runtime_id
とagent_runtime_arn
を出力しておくことだ。こうすることで、後で別のリソースで払い出された値を参照することが可能にになる。
data "external" "bedrock_agentcore" {
depends_on = [
terraform_data.image_push,
]
program = ["python3", "./21_2_agentcore.py"]
query = {
aws_region = data.aws_region.current.region
agent_runtime_name = local.bac_runtime_name
agent_runtime_artifact = "${aws_ecr_repository.example.repository_url}:latest"
network_mode = "PUBLIC"
role_arn = aws_iam_role.example.arn
}
}
import boto3
import botocore
import json
import sys
import time
def main():
params = json.load(sys.stdin)
aws_region = params["aws_region"]
agent_runtime_name = params["agent_runtime_name"]
agent_runtime_artifact = params["agent_runtime_artifact"]
network_mode = params["network_mode"]
role_arn = params["role_arn"]
bac = boto3.client("bedrock-agentcore-control", region_name=aws_region)
try:
paginator = bac.get_paginator("list_agent_runtimes")
for page in paginator.paginate():
for runtime in page.get("agentRuntimes", []):
if runtime.get("agentRuntimeName") == agent_runtime_name:
print(
json.dumps(
{
"agent_runtime_id": runtime["agentRuntimeId"],
"agent_runtime_arn": runtime["agentRuntimeArn"],
}
)
)
sys.exit(0)
except botocore.exceptions.ClientError as e:
sys.stderr.write(str(e))
print(json.dumps({"error": "list_agent_runtimes() failed."}))
sys.exit(1)
try:
create_result = bac.create_agent_runtime(
agentRuntimeName=agent_runtime_name,
agentRuntimeArtifact={
"containerConfiguration": {"containerUri": agent_runtime_artifact}
},
networkConfiguration={"networkMode": network_mode},
roleArn=role_arn,
)
except botocore.exceptions.ClientError as e:
sys.stderr.write(
json.dumps({"error": f"create_agent_runtime() failed {str(e)}"})
)
sys.exit(1)
try:
while True:
get_result = bac.get_agent_runtime(
agentRuntimeId=create_result["agentRuntimeId"]
)
if get_result["status"] == "READY":
break
elif get_result["status"] in ("CREATE_FAILED", "UPDATE_FAILED"):
raise RuntimeError(f"invalid status {get_result["status"]}.")
time.sleep(1)
except botocore.exceptions.ClientError as e:
sys.stderr.write(json.dumps({"error": f"get_agent_runtime() failed {str(e)}"}))
sys.exit(1)
except RuntimeError as e:
sys.stderr.write(json.dumps({"error": str(e)}))
sys.exit(1)
print(
json.dumps(
{
"agent_runtime_id": create_result["agentRuntimeId"],
"agent_runtime_arn": create_result["agentRuntimeArn"],
}
)
)
if __name__ == "__main__":
main()
エージェントのDockerビルドに必要なファイルの作成
エージェントのアプリケーションをビルドするにあたり必要なファイルを作っていく。
なお、これらのファイルはBedrock AgentCore Starter Toolkitのagentcore configure
でも作成可能だが、デフォルトで作成されるDockerfileはリビルドに時間がかかる等、少し使い勝手が悪い部分なので、この作成方法を推奨する。
Dockerfile
Dockerfileは以下のように作成する。
今回、MCPを使いたいため、デフォルトで作成されるDockerfileに加えてuvのインストールを追加している。
また、COPYの位置を変更して、リビルドの高速化をしている。
resource "local_file" "agents_dockerfile" {
filename = "../script/agents/Dockerfile"
content = templatefile("${path.module}/template_file/script/agents/Dockerfile.tmpl", {
aws_region = data.aws_region.current.region
})
}
FROM public.ecr.aws/docker/library/python:3.12-slim
WORKDIR /app
# Install system dependencies if needed
# Copy entire project (respecting .dockerignore)
COPY ./requirements.txt ./requirements.txt
# Install dependencies
RUN apt update
RUN apt install -y curl
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
RUN cp -p /root/.local/bin/uv /usr/local/bin/uv
RUN cp -p /root/.local/bin/uvx /usr/local/bin/uvx
# Install from requirements file
RUN python -m pip install --no-cache-dir -r requirements.txt
# Set AWS region environment variable
ENV AWS_REGION=${aws_region}
ENV AWS_DEFAULT_REGION=${aws_region}
# Signal that this is running in Docker for host binding logic
ENV DOCKER_CONTAINER=1
RUN python -m pip install "aws_opentelemetry_distro_genai_beta>=0.1.2"
# Copy entire project (respecting .dockerignore)
COPY . .
# Create non-root user
# RUN useradd -m -u 1000 bedrock_agentcore
# USER bedrock_agentcore
EXPOSE 8080
# Use the full module path
CMD ["opentelemetry-instrument", "python", "-m", "main"]
.bedrock_agentcore.yaml
.bedrock_agentcore.yamlは、Bedrock AgentCore Starter Toolkitのための設定ファイルだ。
Amazon Bedrock AgentCoreのエージェント作成時にagents.main.oauth_configuration
に関連する内容を変更した場合等は修正が必要なので、適宜中身は見直していただきたい。
resource "local_file" "agents_dot_bedrock_agentcore" {
filename = "../script/agents/.bedrock_agentcore.yaml"
content = templatefile("${path.module}/template_file/script/agents/.bedrock_agentcore.yaml.tmpl", {
aws_region = data.aws_region.current.region
aws_account_id = data.aws_caller_identity.self.id
execution_role_arn = aws_iam_role.example.arn
ecr_repository_uri = aws_ecr_repository.example.repository_url
agent_id = data.external.bedrock_agentcore.result.agent_runtime_id
agent_arn = data.external.bedrock_agentcore.result.agent_runtime_arn
uuid = uuid()
})
}
default_agent: main
agents:
main:
name: main
entrypoint: main.py
platform: linux/arm64
container_runtime: docker
aws:
execution_role: ${execution_role_arn}
account: '${aws_account_id}'
region: ${aws_region}
ecr_repository: ${ecr_repository_uri}
ecr_auto_create: false
network_configuration:
network_mode: PUBLIC
protocol_configuration:
server_protocol: HTTP
observability:
enabled: true
bedrock_agentcore:
agent_id: ${agent_id}
agent_arn: ${agent_arn}
agent_session_id: ${uuid}
authorizer_configuration: null
oauth_configuration: null
エージェント実行スクリプトの.env
エージェント実行のスクリプトでは、作成したAmazon Bedrock AgentCoreのAgentIdが必要になるため、以下のように.envを作ってあげることで、都度環境変数を設定しなくてもいい感じに呼び出せるようになる。
resource "local_file" "interface_dotenv" {
filename = "../script/interface/.env"
content = templatefile("${path.module}/template_file/script/interface/.env.tmpl", {
aws_region = data.aws_region.current.region
agent_runtime_arn = data.external.bedrock_agentcore.result.agent_runtime_arn
})
}
AWS_REGION='${aws_region}'
AGENT_RUNTIME_ARN='${agent_runtime_arn}'
エージェントのメインスクリプト
エージェントのメインスクリプトは以下の通りだ。
ここは好きに書き換えてもらって問題ない。
import boto3
import logging
import os
import sys
import uuid
from bedrock_agentcore.runtime import BedrockAgentCoreApp, PingStatus
from mcp import stdio_client, StdioServerParameters
from strands import Agent
from strands.models import BedrockModel
from strands.tools.mcp import MCPClient
# Initialize General
session_id = str(uuid.uuid4())
tools = []
# Initialize Logger
logging.basicConfig(
level=logging.INFO,
format="%(levelname)s: %(message)s",
handlers=[logging.StreamHandler(sys.stdout)],
)
logger = logging.getLogger("Agent Application Log")
# Initialize Amazon Bedrock
session = boto3.Session()
bedrock_model = BedrockModel(
model_id="us.anthropic.claude-sonnet-4-20250514-v1:0",
boto_session=session,
)
# Initialize Bedrock AgentCore
app = BedrockAgentCoreApp()
# Initialize MCP
stdio_mcp_client = MCPClient(
lambda: stdio_client(
StdioServerParameters(
command="uvx",
args=["awslabs.aws-pricing-mcp-server@latest"],
env={**os.environ},
)
)
)
try:
stdio_mcp_client.start()
tools = tools + stdio_mcp_client.list_tools_sync()
except Exception as e:
logger.error(f"stdio_mcp_client.start() error: {e}", exc_info=True)
# Main
@app.entrypoint
def main(payload):
logger.info(f"payload: {payload}")
"""Process user input and return a response"""
agent = Agent(
model=bedrock_model,
system_prompt="""
You are AI assistant. You are an exceptional AWS financial professional.
Find the optimal AWS instance based on the input information and return the monthly AWS usage fee to the user. Use these capabilities:
+ To check AWS usage fees, use aws-pricing-mcp-server.
+ If your inquiry is in Japanese, please provide the amount in Japanese yen in addition to USD.
""",
tools=tools,
)
try:
user_message = payload.get("prompt", "こんにちは")
logger.info(f"user_message: {user_message}")
result = agent(user_message)
except Exception as e:
logger.error(f"agent() error: {e}", exc_info=True)
result.massage = f"agent() error: {e}"
logger.info(f"result: {result.message}")
stdio_mcp_client.stop(None, None, None)
return ("result", result.message)
if __name__ == "__main__":
app.run()
boto3
bedrock-agentcore
mcp
strands-agents
strands-agents-tools
後処理
できる限り自動作成リソースをTerraformの管理下に入れておこう。
Amazon CloudWatch Logs
AWS Lambdaと同じように事前にロググループを作っておけばマネージドサービスが勝手に乗っかってくれるかと思いきや、Amazon Bedrock AgentCoreはロググループにAgentIdが設定され、AgentIdはランダムなサフィックスの文字列が入るため、予め作ることができなかった。
初回はterraform apply -var initial_apply=true
でimportを空振りさせ、一度Amazon Bedrock AgentCoreを作った後に該当のvarを外してimportをしよう。
variable "initial_apply" {
type = bool
default = false
}
locals {
import_indicator = var.initial_apply ? {} : {
"example" = "dummy"
}
}
import {
for_each = local.import_indicator
to = aws_cloudwatch_log_group.bedrock_agentcore[each.key]
id = "/aws/bedrock-agentcore/runtimes/${data.external.bedrock_agentcore.result.agent_runtime_id}-DEFAULT"
}
resource "aws_cloudwatch_log_group" "bedrock_agentcore" {
for_each = local.import_indicator
name = "/aws/bedrock-agentcore/runtimes/${data.external.bedrock_agentcore.result.agent_runtime_id}-DEFAULT"
retention_in_days = 3
}
いざ、実行する!
さて、上記の準備が全部終わったらterraform apply --var initial_apply=true
して、Amazon CloudWatch Logsのロググループが作られているのを確認したら再度terraform apply
してimportを完了させてから以下のことをやってみよう。
Bedrock AgentCore Starter Toolkitを使う場合
script/agents
配下に設定済みの.bedrock_agentcore.yamlが格納されているはずなので、
$ agentcore invoke '{"prompt": "ECS Fargate で vCPUが1.0、メモリが8GBの月額料金を知りたい"}'
と呼び出してみよう。response
で結果が得られるはずだ。ただし、残念ながら日本語はエンコードされてしまって視認性が悪い状態で出力される。
Pythonのスクリプトを自前で書く
以下のスクリプトを作成する。中身は別段難しいことはないので詳細の解説は割愛する。
アプリケーションが、ペイロードのトップにあるprompt
を取得するため、それに合わせたリクエストをなげるようにしている。
import boto3
import json
import os
import sys
import uuid
from dotenv import load_dotenv
load_dotenv()
session_id = str(uuid.uuid4())
agent_core_client = boto3.client(
"bedrock-agentcore",
region_name=os.environ["AWS_REGION"],
)
payload = json.dumps({"prompt": sys.argv[1]})
response = agent_core_client.invoke_agent_runtime(
agentRuntimeArn=os.environ["AGENT_RUNTIME_ARN"],
runtimeSessionId=session_id,
qualifier="DEFAULT",
payload=payload,
)
response_body = response["response"].read()
response_data = json.loads(response_body)
print("Agent Response:", response_data[1]["content"][0]["text"])
boto3
python-dotenv
このアプリのあるディレクトリで、
$ python3 main.py "ECS Fargate で vCPUが1.0、メモリが8GBの月額料金を知りたい"
とすることで、MCPから得た情報をもとにした以下のような料金情報が取得できたはずだ。
Agent Response: ECS Fargateの料金情報が取得できました。vCPU 1.0、メモリ 8GBの月額料金を計算してお伝えします。
## ECS Fargateの月額料金(vCPU 1.0、メモリ 8GB)
### 🌐 US East (N. Virginia) リージョンの場合:
**標準(x86)プラットフォーム:**
- **vCPU料金**: $0.04048/時間
- **メモリ料金**: $0.004445/GB/時間
**月額料金計算(24時間×30日 = 720時間):**
- vCPU: $0.04048 × 1.0 × 720時間 = **$29.15**
- メモリ: $0.004445 × 8GB × 720時間 = **$25.60**
- **合計: $54.75 USD/月**
**ARM(Graviton2)プラットフォーム(約20%安い):**
- vCPU: $0.03238 × 1.0 × 720時間 = **$23.31**
- メモリ: $0.003560 × 8GB × 720時間 = **$20.51**
- **合計: $43.82 USD/月**
---
### 🏯 Asia Pacific (Tokyo) リージョンの場合:
**標準(x86)プラットフォーム:**
- **vCPU料金**: $0.05056/時間
- **メモリ料金**: $0.005530/GB/時間
**月額料金計算:**
- vCPU: $0.05056 × 1.0 × 720時間 = **$36.40**
- メモリ: $0.005530 × 8GB × 720時間 = **$31.85**
- **合計: $68.25 USD/月**
**ARM(Graviton2)プラットフォーム:**
- vCPU: $0.04045 × 1.0 × 720時間 = **$29.12**
- メモリ: $0.004420 × 8GB × 720時間 = **$25.42**
- **合計: $54.54 USD/月**
---
### 💴 日本円換算(1USD = 155円で計算):
**東京リージョンの場合:**
- **標準プラットフォーム**: $68.25 × 155円 = **約10,579円/月**
- **ARMプラットフォーム**: $54.54 × 155円 = **約8,454円/月**
**バージニア北部リージョンの場合:**
- **標準プラットフォーム**: $54.75 × 155円 = **約8,486円/月**
- **ARMプラットフォーム**: $43.82 × 155円 = **約6,792円/月**
---
### 💡 おすすめ:
1. **コスト重視**: バージニア北部リージョン + ARM(Graviton2)= **約6,792円/月**
2. **国内利用**: 東京リージョン + ARM(Graviton2)= **約8,454円/月**
ARMプラットフォーム(Graviton2)は約20%安く、多くのアプリケーションで互換性があるため、コスト削減には非常に有効です。
これで、簡単にAmazon Bedrock AgentCoreをデプロイできるようになった!