0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「コストを調べて」と話しかけるだけ — Bedrock AgentCore で AWS コスト調査エージェントを作る

0
Posted at

はじめに

Amazon Bedrock AgentCore を使って、「コストを調べて」と話しかけるだけで Cost Explorer を自律的に掘り下げてくれるエージェントを作りました。

これまで AI エージェントをクラウドで動かそうとすると、Lambda でエージェントループを自前実装するか、Bedrock Agents のマネージドフローに乗せるかの二択でした。
AgentCore は第三の選択肢で、好きなフレームワーク(LangGraph / Strands / OpenAI 等)をそのままクラウドにデプロイできます。

この記事では AgentCore の概要から実装・デプロイ・動作確認までを一気通貫で解説します。

AgentCore は 2025年7月にプレビュー公開、2025年10月13日に GA しました。
本記事の検証は 2026年5月初旬(CLI v0.13.0)時点のものです。
参考: Amazon Bedrock AgentCore is now generally available

AgentCore とは

AgentCore は「AI エージェントをホスティングするためのマネージドプラットフォーム」です。

エージェントループ(認識 → 推論 → 行動 → 観察を繰り返すサイクル)を自前で実装する場合、Lambda でループ制御を書く必要がありました。AgentCore は Runtime がホスティングされているため、フレームワークをそのまま動かせます。

今回作るもの

「コストを調べて」と話しかけると、Cost Explorer を自律的に掘り下げて報告してくれる
Python エージェントを AgentCore CLI でクラウドにデプロイする

AgentCore が便利なユースケース

「複数のサービスをまたいで、長時間・自律的に動く必要があるもの」が向いています。

向いている 向いていない
複数サービスをまたぐ 単一 API を呼ぶだけ
15 分以上かかる可能性がある 数秒で完結する
「調査 → 判断 → 次のアクション」を繰り返す 決まった手順を実行するだけ
操作ログの監査が必要 監査不要な社内ツール

単一 API を呼ぶだけなら Lambda + Bedrock Converse API で十分です。AgentCore は「自律的にループを回す」ユースケースで真価を発揮します。

構成要素(3 つ)

AgentCore は以下の 3 つのコンポーネントで構成されています。

コンポーネント 役割
AgentCore Runtime エージェント本体のホスティング環境。microVM でセッション分離。フレームワーク非依存
AgentCore Gateway 既存の API / Lambda / MCP サーバーを MCP 互換ツールに変換し、エージェントから統一的に呼べるようにする
AgentCore CLI (@aws/agentcore) CDK ベースのデプロイツール。agentcore createagentcore devagentcore deploy の流れで完結

Strands Agents とは

AWS が 2025 年 5 月にオープンソースで公開した Python フレームワークです。「LLM にツール一覧を渡して、LLM が必要なツールを選んで呼び出す」ループを書きやすくしたものです。

from strands import Agent, tool

@tool
def get_cost_by_service(start_date: str, end_date: str) -> str:
    """指定期間のサービス別コストを取得する。コスト増加の原因調査の最初のステップで使う。

    Args:
        start_date: 開始日(YYYY-MM-DD 形式)
        end_date: 終了日(YYYY-MM-DD 形式)
    """
    # boto3 で Cost Explorer を呼ぶ
    ...

@tool デコレータを付けるだけでツール登録完了です。あとは LLM が「このツールを呼ぼう」と判断したら自動で実行されます。

なぜ Strands を選んだか

フレームワーク 判定 理由
Strands Agents ◎ 採用 AgentCore CLI のデフォルトテンプレートが Strands。AWS ネイティブ統合が強い。@tool デコレータだけでツール定義が完結する
LangGraph × グラフベースのワークフロー定義が必要。今回のような単純なツール呼び出しエージェントには過剰
CrewAI × マルチエージェント協調が主眼。単一エージェントのユースケースには不向き。CLI の正式サポート対象(Strands / LangGraph / Google ADK / OpenAI Agents SDK)から外れつつある

AgentCore Runtime とは

Strands で書いたエージェントコードを「クラウドで常時動かせる環境」に乗せるものです。Lambda との主な違いは以下の通りです。

観点 AgentCore Runtime Lambda
最大実行時間 8 時間(maxLifetime のデフォルト値かつ上限) 15 分
セッション状態 microVM 内で保持。セッションはインスタンスを超えて永続可能 呼び出しのたびにリセット
課金モデル 消費ベース(LLM レスポンス待ち中は CPU 課金なし) リクエスト数 + 実行時間
フレームワーク 任意(Strands / LangGraph / CrewAI 等) 自前実装

コスト調査エージェントの場合、「調査 → 仮説 → 深掘り」と何度もツールを呼び続けるので、15 分制限の Lambda では詰まります。

AgentCore Runtime の課金は消費ベースです。エージェントが LLM のレスポンスを待っている間(I/O wait)は CPU 課金が発生しないため、長時間セッションでもコストが抑えられます。
参考: Host agent or tools with Amazon Bedrock AgentCore Runtime

AgentCore Gateway とは

既存の API や Lambda 関数を MCP 互換ツールに変換し、エージェントから統一的に呼び出せるようにするサービスです。

エージェント
  ↓ MCP プロトコルで統一
AgentCore Gateway
  ├── Lambda ツール
  ├── MCP サーバー
  ├── OpenAPI の REST API
  └── Smithy モデル

Gateway が必要になるのは「外部の MCP サーバーや REST API をツールとして使いたい」場合です。今回のコスト調査エージェントはツールが全部 boto3 の呼び出しで完結するため、Gateway は不要です。

設計ポイント

  • ツールの粒度 — 1 ツール 1 API 呼び出しにします。LLM が組み合わせを判断するので、ツールは小さく保つのが原則です
  • ツールの説明文が設計の核心@tool の docstring が LLM への指示になります。「いつ使うか」「何を返すか」を明確に書かないと LLM が誤った判断をします
  • IAM 設計は従来通り — エージェントに渡す IAM ロールは最小権限です。LLM が予期しないツール呼び出しをする可能性があるため、むしろ従来より厳密に絞る必要があります

前提条件(ローカル環境)

  • Node.js 20 以上(CLI が npm パッケージのため)
  • Python 3.10 以上(エージェントコードが Python)
  • AWS CLI 設定済み
  • IAM 権限(AgentCore API 呼び出し + CDK bootstrap ロールの assume)

参考: Get started with Amazon Bedrock AgentCore

手順

1. CLI インストール

npm install -g @aws/agentcore
agentcore --version  # 0.13.0

2. プロジェクト作成(対話式ウィザード)

agentcore create

対話式ウィザードでプロジェクト名・フレームワーク・モデルなどを選択していきます。今回の選択は以下の通りです。

項目 選択 理由
エージェントタイプ Agent ツール呼び出しを行うため
デプロイ先 Runtime クラウドで常時稼働させたいため
プロトコル HTTP 単純なリクエスト/レスポンスで十分なため
フレームワーク Strands AWS ネイティブ統合が強く、CLI デフォルトテンプレートのため
モデル Claude Sonnet(後で Nova Lite に変更) デフォルト選択。コスト削減のため後から変更
メモリ なし セッション間で状態を引き継ぐ必要がないため
ウィザードのスクリーンショット(13枚)

agentcore-プロジェクト名の入力.png

agentcore-エージェントの導入可否.png

agentcore-エージェント名の入力.png

agentcore-エージェントタイプの選択.png

agentcore-言語選択.png

agentcore-デプロイ先の選択.png

agentcore-プロトコルの選択.png

agentcore-フレームワークの選択.png

agentcore-モデルの選択.png

agentcore-メモリの選択.png

agentcore-追加設定.png

agentcore-確認画面.png

agentcore-デプロイ完了画面.png

生成されるディレクトリ構成:

CostInvestigator/
├── agentcore/
│   ├── agentcore.json      # CLI が管理するリソース定義
│   ├── aws-targets.json    # デプロイ先アカウント・リージョン
│   └── cdk/                # CLI が自動生成・管理(手動編集不要)
└── app/
    └── MyAgent/
        ├── main.py         # エージェントロジック(Strands)
        ├── model/load.py   # モデル定義
        └── pyproject.toml  # Python 依存関係

3. エージェントコードの実装

生成直後の main.py はサンプルツール(add_numbers)のみです。以下の変更を加えます。

model/load.py — モデルを Nova Lite に変更

- return BedrockModel(model_id="global.anthropic.claude-sonnet-4-5-20250929-v1:0")
+ return BedrockModel(model_id="amazon.nova-lite-v1:0")

main.py — ツールを実装

実装するツールは 2 本です。

ツール 用途
get_cost_by_service サービス別コスト取得(調査の起点)
get_cost_by_day 特定サービスの日別コスト推移(スパイク日の特定)
import json
from datetime import datetime, timezone

import boto3
from strands import Agent, tool
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from model.load import load_model

app = BedrockAgentCoreApp()
log = app.logger


def build_system_prompt() -> str:
    today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
    return f"""
You are an AWS cost investigation agent. Today's date is {today}.
When asked about costs, follow this process:
1. Start by getting cost breakdown by service for the relevant period
2. Identify the top services by cost
3. Drill down into suspicious services using available tools
4. Provide a clear summary of findings and likely causes

Always respond in Japanese regardless of the input language.
"""


@tool
def get_cost_by_service(start_date: str, end_date: str) -> str:
    """Get AWS cost breakdown by service for a given period. Use this as the first step in any cost investigation.

    Args:
        start_date: Start date in YYYY-MM-DD format
        end_date: End date in YYYY-MM-DD format (exclusive)
    """
    client = boto3.client("ce", region_name="us-east-1")
    response = client.get_cost_and_usage(
        TimePeriod={"Start": start_date, "End": end_date},
        Granularity="MONTHLY",
        Metrics=["UnblendedCost"],
        GroupBy=[{"Type": "DIMENSION", "Key": "SERVICE"}],
    )
    results = []
    for result in response["ResultsByTime"]:
        for group in sorted(
            result["Groups"],
            key=lambda x: float(x["Metrics"]["UnblendedCost"]["Amount"]),
            reverse=True,
        ):
            amount = float(group["Metrics"]["UnblendedCost"]["Amount"])
            if amount > 0:
                results.append({"service": group["Keys"][0], "cost_usd": round(amount, 4)})
    return json.dumps(results, ensure_ascii=False)


@tool
def get_cost_by_day(service: str, start_date: str, end_date: str) -> str:
    """Get daily cost trend for a specific AWS service. Use this to identify which days had cost spikes.

    Args:
        service: AWS service name (e.g. 'Amazon EC2', 'Amazon S3')
        start_date: Start date in YYYY-MM-DD format
        end_date: End date in YYYY-MM-DD format (exclusive)
    """
    client = boto3.client("ce", region_name="us-east-1")
    response = client.get_cost_and_usage(
        TimePeriod={"Start": start_date, "End": end_date},
        Granularity="DAILY",
        Metrics=["UnblendedCost"],
        Filter={"Dimensions": {"Key": "SERVICE", "Values": [service]}},
    )
    results = [
        {
            "date": r["TimePeriod"]["Start"],
            "cost_usd": round(float(r["Total"]["UnblendedCost"]["Amount"]), 4),
        }
        for r in response["ResultsByTime"]
    ]
    return json.dumps(results, ensure_ascii=False)


_agent = None


def get_or_create_agent():
    global _agent
    if _agent is None:
        _agent = Agent(
            model=load_model(),
            system_prompt=build_system_prompt(),
            tools=[get_cost_by_service, get_cost_by_day],
        )
    return _agent


@app.entrypoint
async def invoke(payload, context):
    log.info("Invoking Agent.....")
    agent = get_or_create_agent()
    stream = agent.stream_async(payload.get("prompt"))
    async for event in stream:
        if "data" in event and isinstance(event["data"], str):
            yield event["data"]


if __name__ == "__main__":
    app.run()

4. ローカルテスト

WSL 環境では xdg-open が存在しないため --no-browser で起動し、curl で直接叩きます。

AWS_PROFILE=<PROFILE> agentcore dev --no-browser

別ターミナルから動作確認:

curl -s -X POST http://localhost:8080/invocations \
  -H "Content-Type: application/json" \
  -d '{"prompt": "Hello"}'
data: "Hello! How can I help you today?"

5. デプロイ

AWS_PROFILE=<PROFILE> agentcore deploy

初回は CDK bootstrap が走るため数分かかります。IAM ロール・AgentCore Runtime エンドポイントが自動作成されます。

デプロイ後に自動作成される IAM ロールのポリシーは以下の通りです(Bedrock / CloudWatch Logs のみ)。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "bedrock:InvokeModel",
                "bedrock:InvokeModelWithResponseStream"
            ],
            "Resource": [
                "arn:aws:bedrock:*:<ACCOUNTID>:inference-profile/*",
                "arn:aws:bedrock:*::foundation-model/*"
            ],
            "Effect": "Allow"
        },
        {
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:ap-northeast-1:<ACCOUNTID>:log-group:/aws/bedrock-agentcore/runtimes/*",
            "Effect": "Allow"
        }
    ]
}

Cost Explorer 権限は含まれていないため、手動で追加する必要があります。本来は agentcore/cdk/lib/cdk-stack.ts でロールに権限を定義するのが正しいですが、今回はお試しのため手動で attach しました。

aws iam attach-role-policy \
  --role-name <RUNTIME_ROLE_NAME> \
  --policy-arn arn:aws:iam::aws:policy/AWSBillingReadOnlyAccess \
  --profile <PROFILE>

6. 動作確認

AWS_PROFILE=<PROFILE> agentcore invoke \
  --prompt "AWSコストをサービス別に調べて。期間は2026-04-01から2026-04-30"

エージェントが以下を自律的に実行します:

  1. get_cost_by_service でサービス別コストを取得
  2. 上位サービス(CloudWatch / Amazon Q)を自分で判断して get_cost_by_day で深掘り
  3. スパイク日(4/20)を特定してサマリーを生成

実行結果(抜粋):

コストが高い順にサービスを並べると、以下のようになります:
1. AmazonCloudWatch: $19.621
2. Amazon Q: $18.3667
3. Amazon Elastic Load Balancing: $15.3173
...

AmazonCloudWatch の変動を見ると、特に 2026-04-20 に $2.5392 という高いコストが記録されています。
この変動は、新しいメトリクスやアラームが作成されたり、既存のリソースの使用量が急増したりすることが考えられます。

Session: a9ee6072-424f-4d9f-8157-3fa1b30c8b26
To resume: agentcore invoke --session-id a9ee6072-424f-4d9f-8157-3fa1b30c8b26

指示していないのに get_cost_by_day を自分で呼んで深掘りしているのがポイントです。 エージェントは「どのツールをどの順番で呼ぶか」を自律的に判断します。docstring に「コスト増加の原因調査の最初のステップで使う」「スパイク日の特定に使う」と書いたことで、LLM が適切な順序でツールを選択しています。

7. リソース削除

AgentCore CLI でリソースを削除します。

agentcore remove all
agentcore deploy

remove allagentcore/agentcore.json の設定をリセットし、続く deploy で AWS 上のリソース(CloudFormation スタック)が削除されます。

詰まったポイント

agentcore dev がブラウザに到達しない(WSL)

agentcore dev 起動時に Error: spawn xdg-open ENOENT が出てブラウザが開きません。WSL 環境では xdg-open が存在しないためです。

対処: --no-browser フラグで起動し、curl で直接叩きます。

AWS 認証情報が見つからない

agentcore dev 起動後に curl を叩くと NoCredentialsError が発生します。agentcore dev 自体に AWS_PROFILE を渡す必要があります。

AWS_PROFILE=<PROFILE> agentcore dev --no-browser

モデルが「未来の日付」と判断してツールを呼ばない

Nova Lite の学習データカットオフが 2024 年以前のため、2025 年・2026 年を「未来」と判断してツール呼び出しを拒否します。

対処: システムプロンプトに現在日付を動的に埋め込みます。

def build_system_prompt() -> str:
    today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
    return f"Today's date is {today}. ..."

これは Nova Lite 固有の問題です。Claude 系モデルでは発生しません。コストを抑えるために Nova Lite を選んだトレードオフとして認識しておく必要があります。また、英語のシステムプロンプトに Always respond in Japanese と書く方式は、Nova Lite では指示追従が弱く無視されることがあります。確実に日本語で返したい場合はシステムプロンプト自体を日本語で書くことも検討してください。

agentcore invoke がローカルではなくクラウドに向く

agentcore dev でローカル起動中でも agentcore invoke はクラウドのデプロイ済みエンドポイントに向きます。ローカルテストは curl で http://localhost:8080/invocations を直接叩きます。

クラウドの実行ロールに Cost Explorer 権限がない

agentcore deploy で自動作成される実行ロールには Cost Explorer 権限が含まれていません。本来は agentcore/cdk/lib/cdk-stack.ts でロールに権限を追加するのが正しいですが、今回はお試しのため手動で AWSBillingReadOnlyAccess を attach しました。

まとめ

  • agentcore createagentcore devagentcore deploy の 3 ステップで CDK を自分で書かずにデプロイできます
  • CDK インフラは CLI が自動管理するため、自分で書くのはエージェントロジック(main.py)のみです
  • ツールの docstring が LLM へのツール説明になります。「いつ使うか」を明記することが設計の核心です
  • エージェントは「どのツールをどの順番で呼ぶか」を自律的に判断します。今回は指示していないのに get_cost_by_day を自分で呼んで深掘りしました
  • IAM 権限はデプロイ後に不足が判明することがあります。本番では CDK でロールに権限を定義しておくべきです

AgentCore は「フレームワークを選ばない」「インフラを意識しない」という点で、エージェント開発の敷居を大きく下げてくれるサービスだと感じました。一方で、IAM 権限の管理やモデル固有の癖(日付問題など)は従来通り自分で対処する必要があるので、マネージドだからといって全部お任せにはできません。

参考

0
1
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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?