【ハンズオン】Amazon Bedrock AgentCore Runtime instancesをAWS CDKで構築してみよう!

technologies

  • HOME
  • BLOG
  • technologies
  • 【ハンズオン】Amazon Bedrock AgentCore Runtime instancesをAWS CDKで構築してみよう!

目次

  1. はじめに
  2. 今回利用するAWSサービス
  3. 【アーキテクチャ解説】Runtime instancesの構成要素と依存関係
  4. 【ハンズオン】AWS CDKでRuntime instancesを構築してみる
  5. 【考察】コンソールとIaC、microVMとRuntime instances、CDKとTerraform
  6. 【料金】Runtime instancesのコストの考え方
  7. 【注意点】制限事項と利用上の注意
  8. まとめ

1. はじめに

こんにちは!ベンジャミンの松延(まつのぶ)です!

2026年8月6日、Amazon Bedrock AgentCore Runtimeの新しいコンピュートタイプとしてRuntime instancesが一般提供(GA)されました。同日にAWS News Blogでも紹介記事が公開されています。

Amazon Bedrock AgentCore(以下、AgentCore)のRuntimeは、これまでサーバーレスなmicroVM上でエージェントを動かす方式でした。手軽で運用負荷も低い一方、セッションは最大8時間、1つのランタイムにつき1つのエージェント、GPUは非対応という前提があります。
そのため、「数日かけて動き続けるバッチ的な処理をさせたい」「複数のエージェントに同じ作業ディレクトリを共有させたい」「GPUで推論やレンダリングをさせたい」といった要件が出てくると、EC2やECSで実行基盤を自前で組むことになります。

Runtime instancesは、この部分をAWS管理のEC2インスタンスとして提供するコンピュートタイプです。しかもそのEC2インスタンスはご自身のAWSアカウントの中で動きます。
つまり、データも既存のアカウント統制もそのまま自分の手の内に残ったまま、インスタンスの起動・パッチ適用・スケール・破棄はAgentCoreに任せられる、という位置づけです。

そしてもう一つ、この記事の切り口に関わる事実があります。Runtime instancesの土台となるCapacity providerは、作成後に編集できるのがdescriptionだけで、インスタンスタイプやネットワークを変えたいときは作り直しになります。コンソールで手動作成した構成は、変更のたびに「あの時どう設定したっけ」を思い出す作業から始まってしまいます。だからこそ、最初からIaCでコードとして残しておく価値が大きい構成だと考えました。

今回は、AWS CDK(TypeScript)を使って、Capacity provider・Agent runtime・エンドポイントまでを一式コードで構築し、実際にエージェントを呼び出すところまでをシェアしていきたいと思います。

なお、AgentCore自体の概要やManaged Harnessを使った構成については前回記事で扱っているので、あわせて読んでいただけるとうれしいです。

対象読者とこの記事でわかること

本記事は、次のような方を対象としています。

  • AgentCoreで作ったエージェントを本番運用に載せることを検討している、インフラエンジニアやバックエンドエンジニアの方
  • コンソールで作ったAgentCoreの構成を、レビューできる形・環境ごとに複製できる形に持っていきたい方
  • サーバーレスなmicroVMとRuntime instancesの使い分けや、Runtime instancesのコスト構造を知りたい方

そして、この記事を読み終えると、次のことがわかります。

  1. Runtime instancesの構成要素と、microVMとの使い分けの判断軸
  2. AWS CDKのL1 constructで、IAMロールからCapacity provider、Agent runtimeまでを一式構築する具体的な手順
  3. Runtime instancesの課金の考え方と、見落とすとコストや挙動に効いてくる設定(インスタンスの最大生存時間のデフォルト値など)

2. 今回利用するAWSサービス

1. Amazon Bedrock AgentCore Runtimeとは

AgentCore Runtimeは、AIエージェントやツールをホストするための実行環境です。LangGraph・CrewAI・Strands Agentsといったオープンソースのフレームワークや、任意のモデル・プロトコルに対応しており、エージェントのコードをデプロイすれば、呼び出しの受け口・セッション管理・認証・オブザーバビリティといった周辺をAgentCore側が引き受けます。

AgentCore Runtimeには2つのコンピュートタイプがあります。1つがこれまでのサーバーレスなmicroVM、もう1つが今回扱うRuntime instancesです。どちらを選んでも、呼び出しの入口は同じInvokeAgentRuntime APIで、IAMによる認可やオブザーバビリティの仕組みも共通です。エージェントを載せる「土台」だけを付け替えるイメージだと捉えると、わかりやすいかなと思います。

公式ドキュメント:Host agent or tools with Amazon Bedrock AgentCore Runtime

2. Runtime instancesとは

Runtime instancesは、エージェントをAmazon EC2のマネージドインスタンス上で動かすコンピュートタイプです。microVMとの違いを公式ドキュメントの比較表から整理すると、次のようになります。

観点microVMRuntime instances
向いているワークロード短時間で完了する、API駆動の軽量なエージェント長時間・ステートフル、あるいはGPUや複数エージェントの協調が必要なワークロード
管理モデルフルマネージド・サーバーレス、需要に応じてスケールご自身のアカウント内のAWS管理EC2。パッチ適用と更新はAWSが実施
最大セッション時間最大8時間最大14日
OSLinuxコンテナ(arm64)Linux(x86_64およびarm64)
ネットワークPUBLICまたはVPCVPC
1セッションあたりのエージェント数1:11:N
アーティファクトコンテナイメージ、Amazon S3ソースコンテナイメージ、Amazon S3ソース
GPU非対応対応するGPUインスタンスタイプを選択可能。ドライバはAWS側でプロビジョニング
課金消費量ベース、AgentCoreが課金EC2インスタンスはご自身のアカウントで稼働。Savings PlansやODCRを利用可能。

表の「ネットワーク」の行は少し補足が必要です。microVMでもVPCモードを選べますが、その場合にご自身のVPCに作られるのはENI(ネットワークインターフェース)だけで、エージェントが動くmicroVM自体はAWS管理の環境にあります。ENI越しにVPC内のRDSや内部APIへアクセスできる、という位置づけです。

一方、Runtime instancesでは、EC2インスタンスそのものがご自身のアカウントとVPCの中で起動します。「ネットワークだけ自分のVPCに出す」か「コンピュートごと自分のアカウントに置く」かが、この行の違いです。

なお「アーティファクト」の行はどちらも同じです。コンテナイメージ(Amazon ECR)と、コードと依存パッケージをzipにしてS3に置く直接コードデプロイの2方式で、公式の直接コードデプロイ手順(microVM向け)で作ったzipを、そのままRuntime instancesのagent runtimeにも指定できます。本記事のハンズオンもこの方式です。

公式ドキュメント:Configure Amazon Bedrock AgentCore Runtime and tools for VPC

3. Capacity providerとは

Capacity providerは、エージェントが動くEC2インフラを定義するリソースです。OS(アーキテクチャ)、許可するインスタンスタイプ、VPCとサブネット、セキュリティグループ、EBSボリューム、そしてインスタンスのプロビジョニングとアクセスに使うIAMロールを、ひとまとめに宣言します。

Capacity providerは再利用可能なテンプレートとして扱われ、1つのCapacity providerを複数のAgent runtimeに紐づけられます。逆に言うと、同じCapacity providerを共有するAgent runtimeを同じセッションIDで呼び出すと、両方のエージェントが同じEC2インスタンス上に配置され、ファイルシステムを共有して協調できます。これがRuntime instancesならではの動き方です。

作成するとCREATING状態になり、設定の検証が通るとREADYになります。検証に失敗した場合はCREATE_FAILEDです。

公式ドキュメント:Capacity provider – Runtime instances

4. AWS CDKとは

AWS Cloud Development Kit(以下、CDK)は、TypeScriptやPythonなどのプログラミング言語でAWSリソースを定義し、CloudFormationテンプレートに変換してデプロイするIaCツールです。

今回はCDKのL1 construct(Cfnで始まるクラス)を使います。L1 constructはCloudFormationのリソースと1対1で対応しているため、プロパティ名や必須・任意の区別をCloudFormationのリファレンスでそのまま裏取りできます。GA直後のサービスを扱ううえでは、この「一次情報と突き合わせやすい」という性質が効いてきます。

AgentCore関連のリソースはaws-cdk-lib/aws-bedrockagentcoreモジュールに含まれており、今回使うCfnCapacityProviderCfnRuntimeCfnRuntimeEndpointはいずれもここから利用します。

公式ドキュメント:aws-cdk-lib.aws_bedrockagentcore module


3. 【アーキテクチャ解説】Runtime instancesの構成要素と依存関係

本記事で構築する構成は、下記のようになります。

この構成のポイントは、「エージェントが何を実行するか」と「エージェントがどこで実行されるか」が別のリソースに分かれていることです。Agent runtimeがコードとその設定を持ち、Capacity providerがコンピュートを持ちます。両者はCapacityProviderArnで結びつきます。

まずデプロイ時の流れは次のとおりです。CDKのconstruct同士の依存関係もこの順に決まります。

  1. VPC・サブネット・セキュリティグループを用意します。Runtime instancesはネットワーク構成がVPC必須のため、ここが土台になります。
  2. 3種類のIAMロールを作成します。
    • ①AgentCoreが自アカウントにEC2を立てるために引き受けるインフラストラクチャロール
    • ②EC2インスタンスに付与されるインスタンスプロファイル
    • ③エージェントのコード自身が使う実行ロール
  3. Capacity providerを作成します。①のロールARNとネットワーク、インスタンスタイプ、EBSボリュームを指定します。ここでREADYになるまで検証が走ります。
  4. エージェントのコードをzipにしてS3へアップロードします。CDKのアセット機能を使えば、この2手順はcdk deployに含められます。
  5. Agent runtimeを作成します。③の実行ロール、S3上のコード、エントリポイント、そして3で作ったCapacity providerのARNを指定します。このARNを指定することでコンピュートタイプがRuntime instancesになります。
  6. エンドポイントを作成します。呼び出し先のエイリアスにあたるリソースです。

次に、実際にエージェントを呼び出したときの流れです。

  1. ユーザーが、Runtime Endpointに対してruntimeSessionIdを指定し、InvokeAgentRuntimeを実行します。
  2. 指定したruntimeSessionIdに対応するセッションがまだ無ければ、AgentCoreは①のロールを使ってCapacity providerの定義どおりにEC2インスタンスを起動し、インスタンス内でエージェントを起動します。セッションの初回呼び出しはインスタンスのプロビジョニングを含むぶん時間がかかります。
  3. 指定したruntimeSessionIdに対応するセッションがある場合は、稼働しているEC2インスタンスをそのまま再利用します。同一Capacity providerを共有する別Agent runtimeを同一セッションIDで呼び出すと、そのエージェントが同一インスタンス上に追加で起動します。
  4. AgentCoreがリクエストをエージェントへプロキシし、レスポンスをストリーミングで返します。各エージェントは、それぞれのAgent runtimeの実行ロールから導出されたIAM認証情報で動きます。
  5. セッションが停止すると、EC2インスタンスは終了しますが、Volumesで定義した永続ボリュームは残ります。同一セッションIDで再度呼び出すと、新しいインスタンスに同じボリュームが再アタッチされ、前回のデータがそのまま見える状態から再開できます。

4. 【ハンズオン】AWS CDKでRuntime instancesを構築してみる

それでは、実際にCDKでRuntime instancesを構築してみましょう!

今回作るのは、ウェブサイトの「アクセスカウンター」のようなエージェントになります。呼び出されるたびに、EBSボリューム上のファイルに1行追記して「これで何回目か」を数え、モデルの回答と一緒に「何回目の呼び出しか(history_count)」と「どのインスタンスで動いているか(hostname)」を返します。

Runtime instancesがmicroVMと決定的に違うのはステートフルである(セッションを停止・再開しても状態を保持できる)ことで、これがなければわざわざEC2を選ぶ理由がありません。そこで後半では、EC2インスタンスを終了させたあとに同じセッションで呼び直し、hostnameは変わっている(インスタンスは作り直された)のにhistory_countは続いている(EBSが引き継がれた)ことを確認します。

全体の流れは次のとおりです。

  1. CDKプロジェクトを初期化する
  2. ネットワーク(VPC・サブネット・セキュリティグループ)を定義する
  3. 3種類のIAMロールを定義する
  4. Capacity providerを定義する
  5. エージェントコードを用意し、zipにまとめる
  6. Agent runtimeとエンドポイントを定義する
  7. デプロイして構築結果を確認する
  8. エージェントを呼び出し、セッション永続を確認する

前提条件

ハンズオンを実施するにあたっての前提条件は下記になります。なお、macOSにて検証しているため、Windowsを使用されている方は適宜コマンドを読み替えてください。

  • AWSアカウント
    • 本ハンズオンでは、東京リージョン(ap-northeast-1)を使用します。
  • AWS CLI(2.36.18以上)
    • Capacity provider関連のコマンド(create-capacity-providerなど)が含まれるバージョンが必要です。CHANGELOGによると2.36.18で追加されました(手元では2.36.39で動作を確認しています)。あわせて、事前にAWS認証情報を設定しておきます。
  • Node.js(22以上)
  • AWS CDKv2
  • uv
    • エージェントの依存パッケージをarm64向けに取得するために使います。インストール方法は公式ドキュメントを参照してください。

環境変数を設定しておいてください。

export AWS_PROFILE=[プロファイル名]
export AWS_REGION=ap-northeast-1
export ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
echo $ACCOUNT_ID

公式ドキュメント:aws sts get-caller-identity

ステップ1:CDKプロジェクトの初期化

作業用のディレクトリを作成し、TypeScriptのCDKプロジェクトを初期化します。

# 公式ドキュメント:https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/ref-cli-cmd-init.html
mkdir agentcore-instances-cdk && cd agentcore-instances-cdk
npx cdk init app --language typescript

以下のような出力結果が表示されます。

Applying project template app for typescript
# Welcome to your CDK TypeScript project
...
✅ All done!

続いて、CDKのバージョンを確認します。CfnCapacityProviderはGA直後に追加されたconstructなので、aws-cdk-libが古いと存在しません。

npx cdk --version
npm ls aws-cdk-lib

以下のような出力結果が表示されます。

[CDKのバージョン]
agentcore-instances-cdk@0.1.0 /Users/[ユーザー名]/agentcore-instances-cdk
└── aws-cdk-lib@[aws-cdk-libのバージョン]

グローバルにインストール済みの古いcdkコマンドがあると、新しいaws-cdk-libと組み合わせたときにCloud assembly schema version mismatchエラーになります。
その場合はnpm install --save-dev aws-cdk@latestでプロジェクトにCLIを入れ、npx cdkで実行してください。

まだBootstrapしていないアカウント・リージョンの場合は、CDKのBootstrapを実行しておきます。エージェントのzipをアップロードする先のS3バケットもここで用意されます。

# 公式ドキュメント:https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/ref-cli-cmd-bootstrap.html
npx cdk bootstrap aws://$ACCOUNT_ID/$AWS_REGION

公式ドキュメント:AWS CDK Bootstrapping

ステップ2:ネットワーク(VPC・サブネット・セキュリティグループ)の定義

Runtime instancesはネットワーク構成がVPC必須なので、まずVPCから定義します。エージェントはインターネットへの経路を持たないプライベートサブネットに置き、Amazon Bedrock・S3・CloudWatch Logs・AgentCoreへの通信はVPCエンドポイント経由にします。

lib/agentcore-instances-cdk-stack.tsを作成し、constructorの中に次の定義を追加していきます。

ステップ6の最後に、ファイル全体の完成形を載せます

// ネットワーク
const vpc = new ec2.Vpc(this, 'AgentVpc', {
  ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),
  maxAzs: 1,
  natGateways: 0,
  subnetConfiguration: [
    { name: 'private', subnetType: ec2.SubnetType.PRIVATE_ISOLATED, cidrMask: 24 },
  ],
});

const agentSg = new ec2.SecurityGroup(this, 'AgentSecurityGroup', {
  vpc,
  description: 'Security group for AgentCore Runtime instances',
  allowAllOutbound: true,
});

// VPCエンドポイント
vpc.addGatewayEndpoint('S3Endpoint', {
  service: ec2.GatewayVpcEndpointAwsService.S3,
});

const endpointSg = new ec2.SecurityGroup(this, 'EndpointSecurityGroup', {
  vpc,
  description: 'Security group for interface VPC endpoints',
  allowAllOutbound: false,
});
endpointSg.addIngressRule(agentSg, ec2.Port.tcp(443), 'HTTPS from agent instances');

const interfaceServices: Record<string, ec2.IInterfaceVpcEndpointService> = {
  AgentCore: ec2.InterfaceVpcEndpointAwsService.BEDROCK_AGENTCORE,
  BedrockRuntime: ec2.InterfaceVpcEndpointAwsService.BEDROCK_RUNTIME,
  CloudWatchLogs: ec2.InterfaceVpcEndpointAwsService.CLOUDWATCH_LOGS,
};
for (const [id, service] of Object.entries(interfaceServices)) {
  vpc.addInterfaceEndpoint(`${id}Endpoint`, {
    service,
    securityGroups: [endpointSg],
    privateDnsEnabled: true,
  });
}

インフラ目線でのポイントは3つあります。

  1. インバウンドは開けません。
    • 呼び出しはInvokeAgentRuntime API経由でAgentCoreがプロキシするので、エージェント側のセキュリティグループにインバウンドルールは不要です。
    • エンドポイント側のセキュリティグループには、エージェントのセキュリティグループからのHTTPS(443)だけを許可しています。
  2. NAT Gatewayもインターネットゲートウェイを使用しません。
    • AgentCore RuntimeのVPC設定に関する公式ドキュメント(microVMのVPCモード向けの記述ですが、考え方は同じです)では、インターネットに出られないVPCで必須のエンドポイントとしてS3のゲートウェイ型エンドポイントとCloudWatch Logsのエンドポイント(コンテナデプロイの場合はECRも)が挙げられ、AWSサービスへの通信はNAT GatewayではなくVPCエンドポイントを使うことが推奨されています。
    • こちらに対して、エージェントがモデルを呼ぶためのAmazon Bedrock Runtimeと、インスタンスプロファイルに付くAWS管理ポリシーの権限(bedrock-agentcore:PutSystemLogEvents)から必要と判断したAgentCoreのデータプレーンを加えた、計4つのエンドポイントを作成しています。
  3. 検証用に1AZ構成にしています。
    • インターフェース型エンドポイントはAZごとに時間課金されるため、AZを増やすとその分コストが増えます。
    • 本番では公式ドキュメントの推奨どおり2AZ以上のプライベートサブネットにし、エンドポイントも各AZに配置する形になります。

直接コードデプロイの場合、AgentCoreはアップロードしたzipを内部のサービス所有バケット(acr-code-*)に取り込み、そこからインスタンスへ配布します。
S3ゲートウェイ型エンドポイントのポリシーを絞る場合は、自分のバケットだけでなくこのバケットへのs3:GetObject(aws:PrincipalServiceNamebedrock-agentcore.amazonaws.comの場合)も許可する必要があります。今回は検証目的のため、デフォルトのポリシー(全許可)のままにしています。

エージェント側のセキュリティグループはallowAllOutbound: trueにしています。この構成ではインターネットへの経路自体が無いので実害はありませんが、より厳密にするならallowAllOutbound: falseにしてHTTPS(443)のアウトバウンドだけを許可する方法があります(本記事では未検証です)。

公式ドキュメント:aws-cdk-lib.aws_ec2.VpcConfigure Amazon Bedrock AgentCore Runtime and tools for VPCUse interface VPC endpoints (AWS PrivateLink) for AgentCore

ステップ3:3種類のIAMロールの定義

アーキテクチャ解説で触れた3種類のロールを定義します。役割の違いをもう一度整理しておくと、次のとおりです。

#ロール誰が引き受けるか用途AWS管理ポリシー
インフラストラクチャロールbedrock-agentcore.amazonaws.comAgentCoreがご自身のアカウントにEC2・EBS・ENIなどを作成・管理するBedrockAgentCoreRuntimeInstancesOperatorRolePolicy
インスタンスプロファイルec2.amazonaws.comインスタンスからシステムログを送る。エージェントコードの権限ではないBedrockAgentCoreRuntimeInstancesInstanceRolePolicy
実行ロールbedrock-agentcore.amazonaws.comエージェントコード自身がモデル呼び出しなどに使う(自前で定義)

①と②には、Runtime instancesのGAにあわせて追加されたAWS管理ポリシーがあるので、それをそのまま使います。

// ① インフラストラクチャロール(capacity provider operator role)
const infraRole = new iam.Role(this, 'InfraRole', {
  roleName: 'BedrockAgentCoreCapacityProviderOperatorRole-handson',
  assumedBy: new iam.ServicePrincipal('bedrock-agentcore.amazonaws.com'),
  managedPolicies: [
    iam.ManagedPolicy.fromAwsManagedPolicyName(
      'BedrockAgentCoreRuntimeInstancesOperatorRolePolicy',
    ),
  ],
});

// ② インスタンスプロファイル
const instanceRole = new iam.Role(this, 'InstanceRole', {
  roleName: 'AmazonBedrockAgentCoreCapacityProviderDefaultInstanceRole-demo',
  assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
  managedPolicies: [
    iam.ManagedPolicy.fromAwsManagedPolicyName(
      'BedrockAgentCoreRuntimeInstancesInstanceRolePolicy',
    ),
  ],
});
const instanceProfile = new iam.InstanceProfile(this, 'InstanceProfile', {
  role: instanceRole,
});

// ③ 実行ロール
const executionRole = new iam.Role(this, 'ExecutionRole', {
  roleName: 'BedrockAgentCoreRuntimeExecutionRole-handson',
  assumedBy: new iam.ServicePrincipal('bedrock-agentcore.amazonaws.com'),
});
executionRole.addToPolicy(new iam.PolicyStatement({
  sid: 'BedrockModelInvocation',
  actions: ['bedrock:InvokeModel', 'bedrock:InvokeModelWithResponseStream'],
  resources: ['*'],
}));
executionRole.addToPolicy(new iam.PolicyStatement({
  sid: 'CloudWatchLogs',
  actions: [
    'logs:CreateLogGroup',
    'logs:CreateLogStream',
    'logs:PutLogEvents',
    'logs:DescribeLogGroups',
    'logs:DescribeLogStreams',
  ],
  resources: ['*'],
}));

②のロール名だけ -demo なのは、ロール名の上限64文字に収めるためです(-handson だと65文字になり cdk synth でエラーになります)。

③の bedrock:InvokeModel は検証用に * にしています。本番では使用する推論プロファイルと基盤モデルの ARN に絞ってください。

また、本番でオブザーバビリティ(X-Ray・CloudWatchメトリクス)やAgentCore Identityを使う場合は、公式ドキュメントの実行ロール定義(logs:PutResourcePolicyxray:PutTraceSegmentscloudwatch:PutMetricDatabedrock-agentcore:GetWorkloadAccessToken*など)を参照して、必要な権限を足してください。

公式ドキュメント:AWS managed policies for Amazon Bedrock AgentCoreAgentCore Runtime permissions

ステップ4:Capacity providerの定義

いよいよ本題のCapacity providerです。CfnCapacityProviderはL1 constructなので、プロパティ構造はCloudFormationのAWS::BedrockAgentCore::CapacityProviderと同じです。

// Capacity provider
const capacityProvider = new agentcore.CfnCapacityProvider(this, 'CapacityProvider', {
  name: 'handson_capacity_provider',
  description: 'Capacity provider for the Runtime instances hands-on',
  computeConfiguration: {
    ec2Configuration: {
      launchTemplateSource: {
        launchParameters: {
          operatingSystem: 'LINUX_ARM64',
          instanceRequirements: {
            allowedInstanceTypes: ['m7g.medium'],
          },
          instanceProfileArn: instanceProfile.instanceProfileArn,
          monitoring: 'BASIC',
          propagatedTags: {
            Project: 'agentcore-instances-handson',
          },
        },
      },
      vpcConfiguration: {
        subnets: vpc.selectSubnets({
          subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
        }).subnetIds,
        securityGroups: [agentSg.securityGroupId],
      },
      volumes: [
        {
          ebsConfiguration: {
            name: 'workspace',
            sizeGiB: 20,
            volumeType: 'gp3',
            encrypted: true,
          },
        },
      ],
      lifecycleConfiguration: {
        maxLifetime: 1209600,      // 14日(秒)
        idleInstanceTimeout: 900,  // 15分(秒)
      },
    },
  },
  permissionsConfiguration: {
    capacityProviderOperatorRoleArn: infraRole.roleArn,
  },
});

各プロパティの意味を、インフラ設計の観点から整理します。

プロパティ今回の値補足
operatingSystemLINUX_ARM64LINUX_X86_64も選べます。
今回はステップ5でarm64向けのwheelを取得する手順と揃えるためLINUX_ARM64にしています。
allowedInstanceTypes['m7g.medium']最大30個まで列挙できます。
検証用途なので汎用ファミリーの最小サイズにしています。
GPUを使う場合はg5g6などの対応ファミリー(対応ファミリーは公式ドキュメント参照)をここに含めます。
instanceProfileArn②のARN省略すると、AgentCoreがデフォルトのインスタンスロールを作成します。
propagatedTagsProjectタグこのCapacity providerが作るEC2・EBS・ENIすべてに伝播します。
コスト配分タグの運用に直結する設定です。
(今回は未指定)launchParametersにはほかにcapacityReservationSpecification(ODCRを使う場合に指定)・ephemeralVolumeslicenseSpecificationssshKeyNameがあります。
subnets / securityGroupsプライベートサブネット / 作成したSGそれぞれ1〜16個
volumes[].ebsConfigurationworkspaceという名前の20GiB gp3nameはAgent runtime側でマウントするときの参照名です。
暗号化はデフォルトで有効ですが、明示しています。
lifecycleConfiguration.maxLifetime1209600秒(14日)インスタンスの最大生存時間です(コンソール表記は「インスタンスの最大有効期間」)。
到達すると、活動中でもインスタンスが終了します。
セッション(IDと永続ボリューム)は残り、同じセッションIDで呼び出すと新しいインスタンスで再開できます。
省略時のデフォルトは8時間です。
lifecycleConfiguration.idleInstanceTimeout900秒(15分)インスタンス上のすべてのエージェントがアイドルになってからこの秒数で停止します。
停止中はEC2料金がかかりません。
省略時のデフォルトも900秒です。

Runtime instancesはセッションが最大14日ですが、maxLifetimeはセッションではなくインスタンスの寿命で、指定しない場合のデフォルトは8時間です。長時間動かす前提でこの値を省略すると、8時間でインスタンスが終了し、実行中の処理が中断されます(セッション自体は残るので同じIDで再開はできますが、処理は途切れます)。

「最大14日」はセッション自体の上限で、maxLifetimeに指定できる最大値でもあります。さらに、ライフサイクルの設定はagent runtime側にも別にあり(ステップ6のlifecycleConfiguration)、そちらもデフォルトは8時間です。両方を明示して初めて14日動かせます。

IaCで組んでおくと、この種の「暗黙のデフォルト」がコード上に明示されるので、レビューで気づけるのもうれしいところです。

インスタンスタイプについて、公式ドキュメントに非GPUファミリーの対応一覧はありませんが、手元でt4g.smallを指定してCapacity providerを作成したところ、ValidationException: Instance type 't4g.small' is not supported in region 'ap-northeast-1'となり、作成前に弾かれました(Qiita記事でもt3.smallで同様の報告があります)。T系(バースト可能)ファミリーは東京リージョンでは使えないようです。
そのため今回は、より安価なT系ではなく、汎用M系で最小サイズのmedium(1vCPU/4GiB)を持つm7gファミリーのm7g.mediumを選んでいます。

description以外のプロパティは、CloudFormationのUpdate requiresReplacementです。つまり、インスタンスタイプやボリュームを変更してデプロイすると、capacity providerが作り直されます。関連するAgent runtimeがある状態では削除できないため、変更時の手順は考察の章で触れます。

公式ドキュメント:AWS::BedrockAgentCore::CapacityProvideraws-cdk-lib.aws_bedrockagentcore.CfnCapacityProvider

ステップ5:エージェントコードの用意

エージェントのコードを用意します。今回はStrands AgentsとAgentCore Python SDKを使い、@app.entrypointで呼び出しを受け取る形にします。

プロジェクト直下(agentcore-instances-cdk/)にagentディレクトリを作成し、uvでPythonプロジェクトを初期化します。

mkdir agent && cd agent
uv init --python 3.13 --no-workspace
uv add bedrock-agentcore strands-agents

uv initはひな形のmain.py(hello world)を生成するので、これを次の内容で上書きします。

cat <<'EOF' > main.py
import os
from datetime import datetime, timezone
from pathlib import Path

from bedrock_agentcore.runtime import BedrockAgentCoreApp
from strands import Agent
from strands.session.file_session_manager import FileSessionManager

app = BedrockAgentCoreApp()

# capacity providerで定義したEBSボリュームのマウント先
WORKSPACE = Path("/mnt/workspace")
HISTORY = WORKSPACE / "history.log"      # 呼び出し回数のカウンター
SESSIONS = WORKSPACE / "sessions"        # 会話履歴の保存先

@app.entrypoint
def invoke(payload, context):
    prompt = payload.get("prompt", "Hello!")

    # 永続ボリュームに呼び出し履歴を追記する
    WORKSPACE.mkdir(parents=True, exist_ok=True)
    with HISTORY.open("a", encoding="utf-8") as f:
        f.write(f"{datetime.now(timezone.utc).isoformat()} {prompt}\n")
    with HISTORY.open(encoding="utf-8") as f:
        history_count = sum(1 for _ in f)

    # 会話履歴もEBS上に永続化する(セッションIDごとにディレクトリが作られる)
    session = FileSessionManager(session_id=context.session_id, storage_dir=str(SESSIONS))
    # 東京リージョンで利用できる推論プロファイルを明示する
    agent = Agent(model="jp.anthropic.claude-haiku-4-5-20251001-v1:0", session_manager=session)

    result = agent(prompt)

    return {
        "result": str(result),
        "history_count": history_count,
        "hostname": os.uname().nodename,
    }

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

コードのポイントは3つです。

1つ目は、カウンターの実体が/mnt/workspace/history.logというEBS上のファイルであること。インスタンスのメモリやルートボリュームではなく、capacity providerで定義した永続ボリュームに書いています。

2つ目は、会話履歴もEBS上に置いていることです。Strands AgentsのFileSessionManagerにランタイムのセッションID(context.session_id)と/mnt/workspace/sessionsを渡すと、会話履歴とエージェントの状態がそのディレクトリに保存され、次の呼び出しで復元されます。これが無いと、インスタンスが入れ替わった時点でエージェントの記憶はプロセスのメモリごと消えます。

3つ目は、返り値にhostnameを含めていること。これで「今どのインスタンスで動いているか」がひと目でわかります。後のステップで、hostnameが変わってもhistory_countと会話の両方が続く(=ボリュームが引き継がれ、その上の状態をエージェントが使えている)ことを確認します。

モデルは明示的に指定しています。Strands AgentsのAgent()はモデル未指定だとリージョンに応じたデフォルトの推論プロファイルを組み立てますが、ap-で始まるリージョンではapac.anthropic.claude-sonnet-4-6になり(執筆時点のstrands-agents 1.54.0で確認。デフォルトのモデルIDはSDKのバージョンで変わります)、執筆時点の東京リージョンにはこのIDの推論プロファイルが存在しません(jp.global.のみ)。
そのまま呼び出すとValidationExceptionになるため、東京で利用できるjp.anthropic.claude-haiku-4-5-20251001-v1:0を指定しています。

次に、デプロイパッケージ(zip)を作ります。ここが直接コードデプロイのハマりどころで、依存パッケージはzipに同梱する必要があります。しかも、実行環境に合わせたLinux arm64向けのwheelを取得しなければなりません。今回Capacity providerをLINUX_ARM64にしたのは、公式ドキュメントの手順とアーキテクチャを揃えるためでもあります。

uv pip install \
  --python-platform aarch64-manylinux2014 \
  --python-version 3.13 \
  --target=deployment_package \
  --only-binary=:all: \
  -r pyproject.toml

cd deployment_package
zip -qr ../deployment_package.zip .
cd ..
zip -q deployment_package.zip main.py
ls -lh deployment_package.zip
cd ..

以下のような出力結果が表示されます。

-rw-r--r--  1 [ユーザー名]  staff   [サイズ]M [日付] deployment_package.zip

zipのサイズ上限は圧縮時250MB・展開時750MBです。また、AgentCore Runtimeが読み取れるように、ファイルは644・ディレクトリは755のパーミッションになっている必要があります。__pycache__は別アーキテクチャでビルドされたバイトコードが混ざる原因になるので含めないようにします。

公式ドキュメント:Direct code deployment for Pythonuv inituv adduv pip install

ステップ6:Agent runtimeとエンドポイントの定義

最後に、Agent runtimeとエンドポイントを定義します。zipのアップロードはCDKのアセット機能(aws-s3-assets)に任せます。これでcdk deployのたびに、zipがBootstrapで作られたS3バケットへアップロードされ、そのバケット名とキーがAgent runtimeに渡ります。

// エージェントのzipをS3アセットとしてアップロード
const agentAsset = new s3assets.Asset(this, 'AgentAsset', {
  path: path.join(__dirname, '../agent/deployment_package.zip'),
});
agentAsset.grantRead(executionRole);

// Agent runtime(compute type = Runtime instances)
const runtime = new agentcore.CfnRuntime(this, 'Runtime', {
  agentRuntimeName: 'handson_agent',
  description: 'Agent hosted on Runtime instances',
  roleArn: executionRole.roleArn,
  agentRuntimeArtifact: {
    codeConfiguration: {
      code: {
        s3: {
          bucket: agentAsset.s3BucketName,
          prefix: agentAsset.s3ObjectKey,
        },
      },
      entryPoint: ['main.py'],
      runtime: 'PYTHON_3_13',
    },
  },
  capacityProviderConfiguration: {
    capacityProviderArn: capacityProvider.attrArn,
  },
  filesystemConfigurations: [
    {
      capacityProviderVolume: {
        volumeName: 'workspace',
        mountPath: '/mnt/workspace',
      },
    },
  ],
  protocolConfiguration: 'HTTP',
  lifecycleConfiguration: {
    idleRuntimeSessionTimeout: 900,  // 15分(秒)
    maxLifetime: 1209600,            // 14日(秒)。capacity provider側のmaxLifetime以下にする
  },
});
runtime.addResourceDependency(capacityProvider);

// エンドポイント
const endpoint = new agentcore.CfnRuntimeEndpoint(this, 'RuntimeEndpoint', {
  name: 'handson_endpoint',
  agentRuntimeId: runtime.attrAgentRuntimeId,
  // 省略すると作成時のバージョンに固定される。runtimeの更新に追随させる
  agentRuntimeVersion: runtime.attrAgentRuntimeVersion,
});

// 出力
new cdk.CfnOutput(this, 'CapacityProviderArn', { value: capacityProvider.attrArn });
new cdk.CfnOutput(this, 'AgentRuntimeArn', { value: runtime.attrAgentRuntimeArn });
new cdk.CfnOutput(this, 'EndpointName', { value: endpoint.name });

ここでのポイントは4つです。

  1. capacityProviderArnを指定することで、このAgent runtimeのコンピュートタイプがRuntime instancesになります。省略するとmicroVMです。コンピュートタイプは作成後に変更できないので、間違えたら作り直しになります。
  2. capacityProviderVolumeのvolumeNameは、ステップ4のebsConfiguration.nameと一致させます。mountPath/mnt/配下に1階層という制約があります。
  3. addResourceDependency(capacityProvider)で明示的に依存関係を張っています(addDependencyは非推奨になっています)。ARNの参照で暗黙の依存は生まれますが、Capacity providerがREADYになる前にAgent runtimeの作成が走らないよう、意図を明示しておきます。
  4. lifecycleConfigurationはAgent runtime側のライフサイクル設定です。Capacity provider側のmaxLifetimeidleInstanceTimeoutがインスタンスの寿命を決めるのに対し、こちらはセッションの寿命を決めます。省略するとデフォルトの900秒/28,800秒(8時間)が適用されるうえ、maxLifetimeはcapacity provider側のmaxLifetime以下でなければならない制約があります(超えるとValidationExceptionになります)。capacity provider側だけを14日にしても、こちらを省略すると8時間で切れるので、2段構えで明示しておきます。

これで定義は一通り揃いました。CDKファイル全体の完成形は次のとおりです。

cat <<'EOF' > lib/agentcore-instances-cdk-stack.ts
import * as path from 'path';
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as s3assets from 'aws-cdk-lib/aws-s3-assets';
import { aws_bedrockagentcore as agentcore } from 'aws-cdk-lib';

export class AgentcoreInstancesCdkStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // ---------- ネットワーク ----------
    const vpc = new ec2.Vpc(this, 'AgentVpc', {
      ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),
      maxAzs: 1,
      natGateways: 0,
      subnetConfiguration: [
        { name: 'private', subnetType: ec2.SubnetType.PRIVATE_ISOLATED, cidrMask: 24 },
      ],
    });

    const agentSg = new ec2.SecurityGroup(this, 'AgentSecurityGroup', {
      vpc,
      description: 'Security group for AgentCore Runtime instances',
      allowAllOutbound: true,
    });

    // VPCエンドポイント
    vpc.addGatewayEndpoint('S3Endpoint', {
      service: ec2.GatewayVpcEndpointAwsService.S3,
    });

    const endpointSg = new ec2.SecurityGroup(this, 'EndpointSecurityGroup', {
      vpc,
      description: 'Security group for interface VPC endpoints',
      allowAllOutbound: false,
    });
    endpointSg.addIngressRule(agentSg, ec2.Port.tcp(443), 'HTTPS from agent instances');

    const interfaceServices: Record<string, ec2.IInterfaceVpcEndpointService> = {
      AgentCore: ec2.InterfaceVpcEndpointAwsService.BEDROCK_AGENTCORE,
      BedrockRuntime: ec2.InterfaceVpcEndpointAwsService.BEDROCK_RUNTIME,
      CloudWatchLogs: ec2.InterfaceVpcEndpointAwsService.CLOUDWATCH_LOGS,
    };
    for (const [id, service] of Object.entries(interfaceServices)) {
      vpc.addInterfaceEndpoint(`${id}Endpoint`, {
        service,
        securityGroups: [endpointSg],
        privateDnsEnabled: true,
      });
    }

    // ---------- IAMロール ----------
    // ① インフラストラクチャロール(capacity provider operator role)
    const infraRole = new iam.Role(this, 'InfraRole', {
      roleName: 'BedrockAgentCoreCapacityProviderOperatorRole-handson',
      assumedBy: new iam.ServicePrincipal('bedrock-agentcore.amazonaws.com'),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName(
          'BedrockAgentCoreRuntimeInstancesOperatorRolePolicy',
        ),
      ],
    });

    // ② インスタンスプロファイル
    const instanceRole = new iam.Role(this, 'InstanceRole', {
      roleName: 'AmazonBedrockAgentCoreCapacityProviderDefaultInstanceRole-demo',
      assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName(
          'BedrockAgentCoreRuntimeInstancesInstanceRolePolicy',
        ),
      ],
    });
    const instanceProfile = new iam.InstanceProfile(this, 'InstanceProfile', {
      role: instanceRole,
    });

    // ③ 実行ロール
    const executionRole = new iam.Role(this, 'ExecutionRole', {
      roleName: 'BedrockAgentCoreRuntimeExecutionRole-handson',
      assumedBy: new iam.ServicePrincipal('bedrock-agentcore.amazonaws.com'),
    });
    executionRole.addToPolicy(new iam.PolicyStatement({
      sid: 'BedrockModelInvocation',
      actions: ['bedrock:InvokeModel', 'bedrock:InvokeModelWithResponseStream'],
      resources: ['*'],
    }));
    executionRole.addToPolicy(new iam.PolicyStatement({
      sid: 'CloudWatchLogs',
      actions: [
        'logs:CreateLogGroup',
        'logs:CreateLogStream',
        'logs:PutLogEvents',
        'logs:DescribeLogGroups',
        'logs:DescribeLogStreams',
      ],
      resources: ['*'],
    }));

    // ---------- Capacity provider ----------
    const capacityProvider = new agentcore.CfnCapacityProvider(this, 'CapacityProvider', {
      name: 'handson_capacity_provider',
      description: 'Capacity provider for the Runtime instances hands-on',
      computeConfiguration: {
        ec2Configuration: {
          launchTemplateSource: {
            launchParameters: {
              operatingSystem: 'LINUX_ARM64',
              instanceRequirements: {
                allowedInstanceTypes: ['m7g.medium'],
              },
              instanceProfileArn: instanceProfile.instanceProfileArn,
              monitoring: 'BASIC',
              propagatedTags: {
                Project: 'agentcore-instances-handson',
              },
            },
          },
          vpcConfiguration: {
            subnets: vpc.selectSubnets({
              subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
            }).subnetIds,
            securityGroups: [agentSg.securityGroupId],
          },
          volumes: [
            {
              ebsConfiguration: {
                name: 'workspace',
                sizeGiB: 20,
                volumeType: 'gp3',
                encrypted: true,
              },
            },
          ],
          lifecycleConfiguration: {
            maxLifetime: 1209600,      // 14日(秒)
            idleInstanceTimeout: 900,  // 15分(秒)
          },
        },
      },
      permissionsConfiguration: {
        capacityProviderOperatorRoleArn: infraRole.roleArn,
      },
    });

    // ---------- Agent runtime ----------
    const agentAsset = new s3assets.Asset(this, 'AgentAsset', {
      path: path.join(__dirname, '../agent/deployment_package.zip'),
    });
    agentAsset.grantRead(executionRole);

    const runtime = new agentcore.CfnRuntime(this, 'Runtime', {
      agentRuntimeName: 'handson_agent',
      description: 'Agent hosted on Runtime instances',
      roleArn: executionRole.roleArn,
      agentRuntimeArtifact: {
        codeConfiguration: {
          code: {
            s3: {
              bucket: agentAsset.s3BucketName,
              prefix: agentAsset.s3ObjectKey,
            },
          },
          entryPoint: ['main.py'],
          runtime: 'PYTHON_3_13',
        },
      },
      capacityProviderConfiguration: {
        capacityProviderArn: capacityProvider.attrArn,
      },
      filesystemConfigurations: [
        {
          capacityProviderVolume: {
            volumeName: 'workspace',
            mountPath: '/mnt/workspace',
          },
        },
      ],
      protocolConfiguration: 'HTTP',
      lifecycleConfiguration: {
        idleRuntimeSessionTimeout: 900,  // 15分(秒)
        maxLifetime: 1209600,            // 14日(秒)。capacity provider側のmaxLifetime以下にする
      },
    });
    runtime.addResourceDependency(capacityProvider);

    const endpoint = new agentcore.CfnRuntimeEndpoint(this, 'RuntimeEndpoint', {
      name: 'handson_endpoint',
      agentRuntimeId: runtime.attrAgentRuntimeId,
      // 省略すると作成時のバージョンに固定される。runtimeの更新に追随させる
      agentRuntimeVersion: runtime.attrAgentRuntimeVersion,
    });

    // ---------- 出力 ----------
    new cdk.CfnOutput(this, 'CapacityProviderArn', { value: capacityProvider.attrArn });
    new cdk.CfnOutput(this, 'AgentRuntimeArn', { value: runtime.attrAgentRuntimeArn });
    new cdk.CfnOutput(this, 'EndpointName', { value: endpoint.name });
  }
}
EOF

bin/agentcore-instances-cdk.ts側では、デプロイ先のリージョンを明示しておきます。

cat <<'EOF' > bin/agentcore-instances-cdk.ts
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { AgentcoreInstancesCdkStack } from '../lib/agentcore-instances-cdk-stack';

const app = new cdk.App();
new AgentcoreInstancesCdkStack(app, 'AgentcoreInstancesCdkStack', {
  env: {
    account: process.env.CDK_DEFAULT_ACCOUNT,
    region: 'ap-northeast-1',
  },
});
EOF

公式ドキュメント:AWS::BedrockAgentCore::RuntimeAWS::BedrockAgentCore::RuntimeEndpointaws-cdk-lib.aws_s3_assets.AssetConfigure Amazon Bedrock AgentCore lifecycle settings

ステップ7:デプロイと構築結果の確認

まず、CloudFormationテンプレートに変換できることを確認します。

# 公式ドキュメント:https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/ref-cli-cmd-synth.html
npx cdk synth > /dev/null && echo "synth OK"

問題なければデプロイします。VPCとVPCエンドポイントの作成に加えて、Capacity providerの検証(CREATINGREADY)が走るため、数分から10分程度かかります。

# 公式ドキュメント:https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/ref-cli-cmd-deploy.html
npx cdk deploy --require-approval never

以下のような出力結果が表示されます。

✨  Synthesis time: 8.81s

AgentcoreInstancesCdkStack: deploying... [1/1]
AgentcoreInstancesCdkStack: creating CloudFormation changeset...

 ✅  AgentcoreInstancesCdkStack

✨  Deployment time: 217.7s

Outputs:
AgentcoreInstancesCdkStack.AgentRuntimeArn = arn:aws:bedrock-agentcore:ap-northeast-1:[アカウントID]:runtime/handson_agent-sd7gKH2vyT
AgentcoreInstancesCdkStack.CapacityProviderArn = arn:aws:bedrock-agentcore:ap-northeast-1:[アカウントID]:capacity-provider/handson_capacity_provider-xf4X0jRed0
AgentcoreInstancesCdkStack.EndpointName = handson_endpoint

手元の環境では、VPCエンドポイントの作成からCapacity providerの検証、Agent runtimeの作成まで含めて3分半ほどで完了しました。

後で使うので、Agent runtimeのARNを環境変数に入れておきます。

# 公式ドキュメント:https://docs.aws.amazon.com/cli/latest/reference/cloudformation/describe-stacks.html
export AGENT_RUNTIME_ARN=$(aws cloudformation describe-stacks \
  --stack-name AgentcoreInstancesCdkStack \
  --query "Stacks[0].Outputs[?OutputKey=='AgentRuntimeArn'].OutputValue" \
  --output text)

# 公式ドキュメント:https://docs.aws.amazon.com/cli/latest/reference/cloudformation/describe-stacks.html
export CAPACITY_PROVIDER_ARN=$(aws cloudformation describe-stacks \
  --stack-name AgentcoreInstancesCdkStack \
  --query "Stacks[0].Outputs[?OutputKey=='CapacityProviderArn'].OutputValue" \
  --output text)
echo $AGENT_RUNTIME_ARN
echo $CAPACITY_PROVIDER_ARN

Capacity providerがREADYになっていることをAWS CLIで確認します。

get-capacity-provider--capacity-provider-idには、ARNではなく末尾のID(例:handson_capacity_provider-xf4X0jRed0)を渡します。ARNを渡すとAccessDeniedExceptionになります。下記コマンドでは${CAPACITY_PROVIDER_ARN##*/}でARNからIDを切り出しています。

# 公式ドキュメント:https://docs.aws.amazon.com/cli/latest/reference/bedrock-agentcore-control/get-capacity-provider.html
aws bedrock-agentcore-control get-capacity-provider \
  --capacity-provider-id ${CAPACITY_PROVIDER_ARN##*/} \
  --query '{name:name,status:status,statusReason:statusReason}'

以下のような出力結果が表示されます。

{
    "name": "handson_capacity_provider",
    "status": "READY",
    "statusReason": null
}

Agent runtimeも確認しておきましょう。capacityProviderConfigurationにARNが入っていれば、コンピュートタイプがRuntime instancesになっています。

# 公式ドキュメント:https://docs.aws.amazon.com/cli/latest/reference/bedrock-agentcore-control/get-agent-runtime.html
aws bedrock-agentcore-control get-agent-runtime \
  --agent-runtime-id ${AGENT_RUNTIME_ARN##*/} \
  --query '{name:agentRuntimeName,status:status,capacityProvider:capacityProviderConfiguration}'

以下のような出力結果が表示されます。

{
    "name": "handson_agent",
    "status": "READY",
    "capacityProvider": {
        "capacityProviderArn": "arn:aws:bedrock-agentcore:ap-northeast-1:[アカウントID]:capacity-provider/handson_capacity_provider-xf4X0jRed0"
    }
}

AWSマネジメントコンソールでも確認しておきましょう。

AgentCoreコンソールのキャパシティプロバイダー画面では、CDKで定義したOS・インスタンスタイプ・サブネット・2種類のロール・アイドルタイムアウト・最大有効期間・ストレージがそのまま表示されます。なお、APIのREADYはコンソールでは「アクティブ」と表示されます。

Agent runtime側の画面では、コンピューティングタイプが「インスタンス」になっていることがわかります。

ステップ8:エージェントの呼び出しとセッション永続の確認

それでは、エージェントを呼び出してみましょう。uuidgenで生成した値を使います。

export SESSION_ID="handson-session-$(uuidgen | tr 'A-Z' 'a-z')"
echo $SESSION_ID

# 公式ドキュメント:https://docs.aws.amazon.com/cli/latest/reference/bedrock-agentcore/invoke-agent-runtime.html
aws bedrock-agentcore invoke-agent-runtime \
  --agent-runtime-arn $AGENT_RUNTIME_ARN \
  --qualifier handson_endpoint \
  --runtime-session-id $SESSION_ID \
  --payload '{"prompt": "AWS CDKとは何か、1文で教えてください"}' \
  --cli-binary-format raw-in-base64-out \
  --cli-read-timeout 300 \
  response.json && cat response.json

初回の呼び出しは、EC2インスタンスのプロビジョニングを含むため1〜2分程度かかります。以下のような出力結果が表示されます。

{"result": "AWS CDK(Cloud Development Kit)は、TypeScriptやPythonなどのプログラミング言語を使ってAWSインフラストラクチャをコード化し、CloudFormationテンプレートに変換して自動的にAWSリソースをデプロイできるフレームワークです。\n", "history_count": 1, "hostname": "ip-10-0-0-101.ap-northeast-1.compute.internal"}

hostnameがプライベートサブネットのIPアドレス(10.0.0.0/24)に基づく名前になっていることから、エージェントがVPC内のプライベートサブネットで動いていることがわかります。そして、モデルの回答が返ってきていることから、NAT Gatewayのない構成でもVPCエンドポイント経由でモデル呼び出しまで通っていることがわかります。

同じセッションIDでもう一度呼び出してみます。

# 公式ドキュメント:https://docs.aws.amazon.com/cli/latest/reference/bedrock-agentcore/invoke-agent-runtime.html
aws bedrock-agentcore invoke-agent-runtime \
  --agent-runtime-arn $AGENT_RUNTIME_ARN \
  --qualifier handson_endpoint \
  --runtime-session-id $SESSION_ID \
  --payload '{"prompt": "capacity providerとは何か、1文で教えてください"}' \
  --cli-binary-format raw-in-base64-out \
  response.json && cat response.json

以下のような出力結果が表示されます。今度は既存のインスタンスが再利用されるので、応答は早くなります(手元では約3秒でした)。

{"result": "Capacity Providerは、Amazon ECSクラスタの自動スケーリングを管理し、タスクに必要なコンピューティングリソース(EC2インスタンスやFargate)を動的に調整して最適化するAWSの機能です。\n", "history_count": 2, "hostname": "ip-10-0-0-101.ap-northeast-1.compute.internal"}

history_countが2に増え、hostnameは1回目と同じです(プロンプトに文脈を与えていないため、モデルはECSのCapacity providerと解釈しています)。

ここで、EC2側でこのインスタンスを見ておきましょう。AgentCoreが管理するインスタンスはデフォルトではdescribe-instancesの一覧に出ません。表示設定を有効にしたうえで、AgentCoreが作成したAuto Scalingグループ(agentcore-managed-instances-[capacity provider ID])からインスタンスIDを取得します。

# 公式ドキュメント:https://docs.aws.amazon.com/cli/latest/reference/ec2/modify-managed-resource-visibility.html
aws ec2 modify-managed-resource-visibility --default-visibility visible

# 公式ドキュメント:https://docs.aws.amazon.com/cli/latest/reference/autoscaling/describe-auto-scaling-groups.html
export INSTANCE_ID=$(aws autoscaling describe-auto-scaling-groups \
  --query "AutoScalingGroups[?starts_with(AutoScalingGroupName,'agentcore-managed-instances-')].Instances[0].InstanceId" \
  --output text)
echo $INSTANCE_ID

# 公式ドキュメント:https://docs.aws.amazon.com/cli/latest/reference/ec2/describe-instances.html
aws ec2 describe-instances --instance-ids $INSTANCE_ID \
  --query 'Reservations[0].Instances[0].{type:InstanceType,state:State.Name,ip:PrivateIpAddress,arch:Architecture,operator:Operator,volumes:BlockDeviceMappings[].Ebs.VolumeId}'

以下のような出力結果が表示されます。

{
    "type": "m7g.medium",
    "state": "running",
    "ip": "10.0.0.101",
    "arch": "arm64",
    "operator": {
        "Managed": true,
        "Principal": "bedrock-agentcore.amazonaws.com",
        "HiddenByDefault": true
    },
    "volumes": [
        "vol-[ルートボリュームID]",
        "vol-[workspaceボリュームID]"
    ]
}

operatorbedrock-agentcore.amazonaws.comが入っていて、これがAgentCore管理のEC2 managed instanceであることがわかります。後で使うので、workspaceボリューム(/dev/sdf)のIDを控えておきます。

# 公式ドキュメント:https://docs.aws.amazon.com/cli/latest/reference/ec2/describe-instances.html
export WORKSPACE_VOLUME_ID=$(aws ec2 describe-instances --instance-ids $INSTANCE_ID \
  --query "Reservations[0].Instances[0].BlockDeviceMappings[?DeviceName=='/dev/sdf'].Ebs.VolumeId" \
  --output text)
echo $WORKSPACE_VOLUME_ID

ボリュームが2本あるのは、ルートボリュームと、Capacity providerで定義したworkspaceボリュームです。

EC2コンソールで見ると、インスタンス概要に「管理対象」のバッジと「オペレーター」が表示されます。

ストレージタブでは、ルートボリュームとworkspaceボリューム(/dev/sdf、暗号化あり)の2本がアタッチされています。

インスタンスの裏側では、AgentCoreがAuto Scalingグループと起動テンプレートを使っています。起動テンプレートの所有者が①のインフラストラクチャロールになっていて、「AgentCoreがこのロールを引き受けてインスタンスを立てている」ことが見て取れます。

では、セッションを明示的に停止して、インスタンスを終了させてみます。

# 公式ドキュメント:https://docs.aws.amazon.com/cli/latest/reference/bedrock-agentcore/stop-runtime-session.html
aws bedrock-agentcore stop-runtime-session \
  --agent-runtime-arn $AGENT_RUNTIME_ARN \
  --qualifier handson_endpoint \
  --runtime-session-id $SESSION_ID

以下のような出力結果が表示されます。

{
    "runtimeSessionId": "handson-session-[UUID]",
    "statusCode": 200
}

続けて、先ほど取得したインスタンスIDで状態を追いかけてみます。

# 公式ドキュメント:https://docs.aws.amazon.com/cli/latest/reference/ec2/describe-instances.html
while true; do
  echo "$(date +%T) $(aws ec2 describe-instances --instance-ids $INSTANCE_ID \
    --query 'Reservations[0].Instances[0].State.Name' --output text)"
  sleep 10
done

Ctrl+Cで止められます。

手元では、stop-runtime-sessionの後もインスタンスは15分ほどrunningのままで、最後の呼び出しから約15分後にterminatedになりました。
terminatedになると、ルートボリュームは削除されますが、workspaceボリュームはavailable状態で残ります。

手元の環境では、stop-runtime-sessionが200を返した後もEC2インスタンスはすぐには終了しませんでした。実際にインスタンスがstopping → stopped → shutting-down → terminatedと遷移したのは、最後の呼び出しからちょうど15分後、つまりcapacity providerのidleInstanceTimeout(900秒)に達したタイミングでした。
stop-runtime-sessionはセッションの終了をAgentCoreに伝えるAPIで、EC2の回収そのものはlifecycleConfigurationに従って行われる、と理解しておくのがよさそうです。インスタンスが動いている間はEC2料金が発生するので、コストを意識するならidleInstanceTimeoutを短めに設定しておきましょう。

When AgentCore stops the session, it terminates the EC2 instance but retains the volume.
https://docs.aws.amazon.com/ja_jp/bedrock-agentcore/latest/devguide/runtime-instances-how-it-works.html#runtime-instances-persistent-volumes

公式ドキュメントの「Manage your data on Runtime Instances」には、StopRuntimeSessionはセッション内の1つのAgent runtimeを停止するだけでインスタンスは終了せず、インスタンスを終了させるのはidleInstanceTimeoutmaxLifetimeである、という表があり、今回の実測はこれと一致しています(一方でInstancesの概要ページには「セッションを停止するとEC2インスタンスを終了する」という記述もあり、ページ間で表現が揺れています。執筆時点の観測として記します)。
また、Agent runtime側のidleRuntimeSessionTimeout(ステップ6)もデフォルトの900秒なので、どちらのタイムアウトで回収されたかは今回の実測では区別できていません。

# 公式ドキュメント:https://docs.aws.amazon.com/cli/latest/reference/ec2/describe-volumes.html
aws ec2 describe-volumes --volume-ids $WORKSPACE_VOLUME_ID \
  --query 'Volumes[0].{state:State,size:Size,attachments:Attachments}'

以下のような出力結果が表示されます。

{
    "state": "available",
    "size": 20,
    "attachments": []
}

インスタンスのstateterminatedになったのを確認したら、同じセッションIDで再度呼び出します。

# 公式ドキュメント:https://docs.aws.amazon.com/cli/latest/reference/bedrock-agentcore/invoke-agent-runtime.html
aws bedrock-agentcore invoke-agent-runtime \
  --agent-runtime-arn $AGENT_RUNTIME_ARN \
  --qualifier handson_endpoint \
  --runtime-session-id $SESSION_ID \
  --payload '{"prompt": ""セッションを再開しました。これまでに私が質問した内容を、順番に箇条書きで教えてください"}' \
  --cli-binary-format raw-in-base64-out \
  --cli-read-timeout 300 \
  response.json && cat response.json

新しいインスタンスのプロビジョニングが走るので、初回と同じくらい時間がかかります(手元では45秒でした)。以下のような出力結果が表示されます。

{"result": "これまでに質問いただいた内容は以下の通りです:\n\n1. AWS CDKとは何か\n2. Capacity Providerとは何か\n", "history_count": 3, "hostname": "ip-10-0-0-47.ap-northeast-1.compute.internal"}

見るべき点は3つあります。

  1. hostnameip-10-0-0-101からip-10-0-0-47に変わっている:EC2インスタンスは新しく作り直されています。
  2. history_countが停止前の2から続いて3になっている:/mnt/workspace/history.logが残っており、新しいインスタンスに再アタッチされています。
  3. モデルが前のインスタンスでの質問を2つとも思い出している:FileSessionManager/mnt/workspace/sessionsに保存した会話履歴を読み戻し、エージェントの状態ごと引き継がれています。

つまり、EC2インスタンスは新しく作り直されたが、/mnt/workspaceにマウントしたEBSボリュームは前回のものが再アタッチされ、その上に置いた状態をエージェントがそのまま使えているということになります。これがRuntime instancesの「セッションの永続ストレージ」の実体であり、停止と再開を挟んで作業を続けられる理由です。

逆に言うと、ステップ5でFileSessionManagerを使わずにAgent()をプロセス内で持ち回るだけの実装にすると、history_countは続いてもモデルは「1回目の呼び出しです」と答えます(手元でも最初はそうなりました)。
ファイルシステムは永続化されても、プロセスのメモリは永続化されません。エージェントの状態は、EBS上のファイルやAgentCore Memoryのように、インスタンスの外に明示的に置く必要があります。

会話履歴を永続化すると、放っておけば履歴は増え続けます。Strands AgentsのAgent()はデフォルトでSlidingWindowConversationManager(直近40メッセージ)を使うため、モデルに渡す文脈は上限で頭打ちになりますが、FileSessionManagerが保存するファイルは増え続けます。本番では、古い履歴を要約に置き換えるSummarizingConversationManagerや、AgentCore Memoryへの外部化を検討してください。

公式ドキュメント:Strands Agents – Conversation Management

クリーンアップ

検証が終わったら、リソースを削除します。IaCなので基本はcdk destroy一発ですが、削除順には依存関係があります。

  • Capacity providerは、関連するAgent runtimeを先に削除(または関連付け解除)しないと削除できません。CDKはスタック内の依存関係をもとに逆順で削除するので、通常はこの点を意識せずに済みます。
  • Capacity providerを削除すると、そのセッションと永続ストレージ(EBS)もすべて削除されます。残しておきたいデータがあれば、事前にエージェント経由(またはInvokeAgentRuntimeCommand)でS3などへ書き出しておきます。
# 公式ドキュメント:https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/ref-cli-cmd-destroy.html
npx cdk destroy
Are you sure you want to delete: AgentcoreInstancesCdkStack (y/n)? y

以下のような出力結果が表示されます。

AgentcoreInstancesCdkStack: destroying... [1/1]

 ✅  AgentcoreInstancesCdkStack: destroyed

削除後、AgentCoreが作成したEC2関連リソースが残っていないかを確認しておきます。

managed instanceはフィルタ検索に出てこないので、AgentCoreが作ったAuto Scalingグループの有無と、控えておいたボリュームIDで確認します。

# 公式ドキュメント:https://docs.aws.amazon.com/cli/latest/reference/autoscaling/describe-auto-scaling-groups.html
aws autoscaling describe-auto-scaling-groups \
  --query "AutoScalingGroups[?starts_with(AutoScalingGroupName,'agentcore-managed-instances-')].AutoScalingGroupName" \
  --output text

# 公式ドキュメント:https://docs.aws.amazon.com/cli/latest/reference/ec2/describe-volumes.html
aws ec2 describe-volumes --volume-ids $WORKSPACE_VOLUME_ID \
  --query 'Volumes[].{id:VolumeId,state:State}' --output table

Auto Scalingグループが空で、ボリュームがInvalidVolume.NotFoundになっていれば完了です。

なお、CloudWatch Logsのロググループはcdk destroyでは削除されません(Agent runtimeが初回呼び出し時に自動作成するため、スタックの管理外です)。保存量に応じた課金が続くので、不要なら削除しておきます。

# 公式ドキュメント:https://docs.aws.amazon.com/cli/latest/reference/logs/describe-log-groups.html
aws logs describe-log-groups \
  --log-group-name-prefix /aws/bedrock-agentcore/runtimes/handson_agent \
  --query 'logGroups[].logGroupName' --output text | tr '\t' '\n' \
  | xargs -I{} aws logs delete-log-group --log-group-name {}

あわせて、ステップ8でvisibleに変更したmanaged instanceの表示設定も元に戻しておきます。

# 公式ドキュメント:https://docs.aws.amazon.com/cli/latest/reference/ec2/modify-managed-resource-visibility.html
aws ec2 modify-managed-resource-visibility --default-visibility hidden

Bootstrapで作られたS3バケット(cdk-*-assets-*)に、アップロードしたzipが残ります。CDKを今後も使う場合はそのままで問題ありませんが、完全に片付けたい場合はバケット内のオブジェクトを削除してください。


5. 【考察】コンソールとIaC、microVMとRuntime instances、CDKとTerraform

ハンズオンを通して、Runtime instancesを構成する要素と、それらがどう依存し合っているかが見えてきたと思います。ここでは「どう作るか」と「どちらのコンピュートタイプを選ぶか」の2つの軸で、選択肢を整理します。

構築手段の比較:コンソール手動 / CDK / CloudFormation / Terraform

まず「どう作るか」です。Capacity providerをどの手段で定義できるか(執筆時点)を含めて比較すると、次のようになります。

パターン定義の置き場所再現性レビュー性Capacity providerの対応向くケース
コンソール手動画面操作低い。作り直すたびに設定を思い出す必要がある。なし対応初回の動作確認、仕様を把握するための検証。
AWS CDK(L1)TypeScript / Python高い。cdk deployで同じ構成を複製できる。プルリクエストで差分をレビューできる。CfnCapacityProviderあり(安定版)VPC・IAM・S3アセットまで1つのスタックにまとめたい。既存資産がCDK。
CloudFormationYAML / JSON高い差分をレビューできる。AWS::BedrockAgentCore::CapacityProviderありNode.jsのビルド環境を持ち込みたくない。既存資産がCloudFormation。
Terraform(awsccプロバイダ)HCL高い差分をレビューできる。awscc_bedrockagentcore_capacity_providerあり(2026年9月時点のv1.101.0で確認)既存資産がTerraform。Cloud Control API経由でCloudFormationのリソース定義から自動生成されるため、プロパティ構造はCDK L1と同じ。
Terraform(awsプロバイダ)HCL高い差分をレビューできる。未対応(2026年9月時点のv6.64.0で確認。aws_bedrockagentcore_agent_runtimeはあるがcapacity providerに相当するリソースが無い)現時点ではRuntime instancesの構築には使えない。

CDKのL1 constructはCloudFormationと1対1なので、今回のTypeScriptをそのままYAMLに書き換えることもできます。逆に「CDKで書いておいて、必要になったらcdk synthで吐き出したテンプレートをCloudFormationとして運用する」という移行もしやすいです。
VPC・IAM・S3アセットのアップロードまで1つのスタックで完結できる点でCDKを選びましたが、CloudFormationやawsccプロバイダでも同じ構成が組めるので、「チームの既存資産に合わせて選ぶ」のが現実的だと思います。

Capacity providerの変更はどう扱うか

Capacity providerの変更はIaCでは「置換」になるので、その流れを理解しておくことが大事です。

CloudFormation(CDK)では、Update requires: Replacementのプロパティを変更してデプロイすると、次の順で処理されます。

  1. 新しいCapacity providerを作成する
  2. それを参照しているagent runtimeのCapacityProviderArnを新しいARNに更新する(こちらはNo interruption)
  3. 古いCapacity providerを削除する

ここで注意したいのは3です。Capacity providerを削除すると、そのセッションと永続ボリューム(EBS)がすべて削除されます。インスタンスタイプを変えるだけのつもりでも、データが消えます。
実運用では、変更前に必要なデータをS3などご自身が管理する場所へ書き出してから、メンテナンスウィンドウの中で置換する運用になると思います。データを取り出す正攻法は、エージェント自身にコピーさせるか、InvokeAgentRuntimeCommandでセッション内でコマンドを実行してS3などへ書き出す方法です(どちらも実行ロールの権限で動くので、実行ロールに書き込み先への権限を付けておきます)。

公式ドキュメント(Manage your data on Runtime InstancesEncryption at rest for Runtime Instances)は「AgentCoreが作成した永続ボリュームはEBSスナップショットを取れない(管理リソースとして作成・運用するため)」と明記しています。ただし、手元(東京リージョン、執筆時点)では、この永続ボリュームに対してaws ec2 create-snapshotが成功し、completedまで進み、そのスナップショットからcreate-volumeでボリュームを復元できました。公式が非対応としている挙動なので、退避手段として頼らず、上記のS3書き出しを正攻法にすることをおすすめします。

IaCのうれしいところは、この「何が置換されるか」が事前にわかることです。

# 公式ドキュメント:https://docs.aws.amazon.com/cdk/v2/guide/ref-cli-cmd-diff.html
npx cdk diff

置換になるリソースには[~]とともに(requires replacement)が表示されるので、レビューの段階で「これはデータが消える変更だ」と気づけます。

コンピュートタイプの比較:microVMとRuntime instances、どちらを選ぶか

次に「どちらのコンピュートタイプを選ぶか」です。機能の違いは第2章の表で整理したので、ここでは要件からどちらを選ぶかという判断軸に絞ります。

判断軸microVMを選ぶRuntime instancesを選ぶ
実行時間1セッションが8時間以内で完了する。8時間を超える。あるいは停止・再開を挟みながら最大14日まで続けたい。
状態の持ち方呼び出し単位で完結する。セッションストレージ(最大1GB)で足りる。作業ファイル・キャッシュ・チェックポイントをEBSに永続化したい。
エージェント構成1ランタイム1エージェントで十分。複数のエージェントが同じホスト・同じファイルシステムで協調する。
ハードウェア1セッションあたりの上限である2vCPU/8GB(公式クォータ)で足りる。GPUが必要。あるいはメモリやCPUをインスタンスタイプで選びたい。
ネットワークPUBLICで問題ない。VPC内で完結させたい。既存のVPC統制を適用したい。
コスト最適化秒単位の従量課金で、アイドル時のコストを気にしたくない。Savings PlansやODCRを効かせたい。アイドル時は明示的に停止する運用ができる。
運用の手間最小。VPCもIAMロールも1つで済む。VPC・3種類のIAMロール・EBSの設計が必要。

GA時のAWS News Blogでは「軽量なオーケストレーターエージェントをmicroVMで動かし、専門的なワーカーエージェントをRuntime instancesで動かす」という組み合わせも紹介されています。どちらか一方に寄せる必要はなく、同じInvokeAgentRuntime APIで呼べるからこそ、ワークロードごとに土台を選べるのがこの設計の良いところだと感じています。

迷ったらmicroVMから始めるのが無難だと思います。コンピュートタイプは作成後に変更できませんが、Agent runtime自体は作り直しが軽いので、「8時間で足りない」「GPUが要る」といった要件が具体的になった時点でRuntime instancesに移すほうが、最初からVPCとロール設計を抱え込むより負担が小さいです。

公式ドキュメント:Compare compute types – Runtime instances


6. 【料金】Runtime instancesのコストの考え方

Runtime instancesの課金は「EC2の料金 + AgentCoreの管理費」が基本ですが、それ以外にも常時かかるものと、セッションの状態によって変わるものがあります。ハンズオンの構成をベースに整理します。

課金要素の整理

課金要素単価(東京リージョン)課金されるタイミング
EC2インスタンス(m7g.medium)$0.0527/時間(オンデマンド)セッションのインスタンスが起動している間。
プロビジョニング(起動)から停止・終了まで。
料金ページの記載では約1分の最低課金あり。
AgentCore管理費EC2オンデマンド価格の12%(G系GPUファミリーは7.8%)。
m7g.mediumなら約$0.0063/時間。
EC2と同じ時間。
Savings Plans・RI・ODCRの割引は効かず、常にオンデマンド価格を基準に計算。
EBSボリューム(gp3、永続ボリューム)$0.096/GB-月(20GiBなら約$1.92/月)セッションが停止している間も課金。
セッションまたはCapacity providerを削除するまで残る。
VPCエンドポイント(インターフェース型)$0.014/時間/エンドポイント/AZ + $0.01/GB作成してから削除するまで常時。
今回は3本×1AZで約$0.042/時間。
VPCエンドポイント(S3ゲートウェイ型)無料
Amazon Bedrockのモデル呼び出しモデルごとのトークン単価エージェントがモデルを呼んだとき
データ転送標準のEC2データ転送料金

単価は2026年9月時点でAWS Pricing APIおよび各サービスの料金ページから取得した値です。EC2の単価はインスタンスタイプとリージョンで変わるため、実際の見積もりはAWS Pricing Calculatorで確認してください。

概算例:1日8時間・平日だけ動かす場合の月額イメージ

前提を次のように置きます。

  • リージョン:東京(ap-northeast-1)
  • インスタンス:m7g.medium × 1セッション、オンデマンド
  • 稼働:1日8時間 × 20営業日 = 160時間/月。それ以外の時間はidleInstanceTimeoutでインスタンス停止
  • EBS:gp3 20GiB × 1
  • VPCエンドポイント:インターフェース型3本 × 1AZ、常時
  • モデル呼び出しとデータ転送は含めない
項目計算月額
EC2(m7g.medium)160時間 × $0.0527$8.43
AgentCore管理費$8.43 × 12%$1.01
EBS(gp3 20GiB)20 × $0.096$1.92
VPCエンドポイント730時間 × $0.042$30.66
合計(モデル呼び出し除く)約$42.02

この試算で目を引くのは、稼働率が低いうちはEC2よりVPCエンドポイントの固定費のほうが大きいことだと思います。エンドポイントは「使った分」ではなく「置いてある時間」で課金されるためです。
これはRuntime instances固有の話ではなく、プライベートサブネットで完結させる構成一般に言えることなので、実運用では既存の共用VPCのエンドポイントに相乗りする、あるいは複数のCapacity providerで同じVPCを使い回す、といった設計でエンドポイントの固定費を按分するのが現実的です。

逆に24時間365日動かす(730時間/月)場合は、EC2が約$38.47、AgentCore管理費(EC2オンデマンド料金の12%)が約$4.62となり、コストの主役がEC2に移ります。この段階になると、EC2部分にSavings Plansを効かせる価値が出てくると思います。

見落としがちなコスト

  • idleInstanceTimeoutを長くしすぎる:デフォルトは900秒(15分)で、未設定でもアイドル15分で停止します。ただし「すぐ再開したいから」と数時間に延ばすと、その間はアイドルでもEC2料金がかかります。maxLifetime(デフォルト8時間、最大14日)と組み合わせて、要件に応じた値を決めておきましょう。
  • 停止中のセッションのEBS:インスタンスが止まってもEBSは残り、課金され続けます。セッションの削除、またはCapacity providerの削除まで消えません。「止めたから大丈夫」ではない点に注意が必要です。
  • 並列セッション数:1セッション = 1インスタンスなので、同時に10セッション動けばインスタンスも10台です。microVMのようにセッションが増えても勝手に「薄く」なるわけではありません。
  • GPUインスタンス:管理費は7.8%に下がりますが、EC2の単価自体が桁違いです。allowedInstanceTypesにGPUファミリーを含める場合は、idleInstanceTimeoutをより短く設定するなどの対策を検討します。

公式ドキュメント:Amazon Bedrock AgentCore PricingAmazon EC2 On-Demand Pricing


7. 【注意点】制限事項と利用上の注意

最後に、Runtime instancesを使ううえで押さえておきたい制限事項と注意点をまとめます。

主なクォータ

クォータデフォルト値調整可否
Capacity providers per account1,000不可
Agents per capacity provider session(1セッションあたりのエージェント数)20不可
Total agents per account1,000
Active session workloads per account東京リージョンでは2,500
(us-east-1・us-west-2は5,000)
直接コードデプロイのzip上限250MB(圧縮時),
750MB(展開時)
不可
新規セッションの作成レート25TPS

Runtime instancesならではの注意点として、AgentCoreのクォータに加えて、ご自身のアカウントのEC2・EBS・VPC・EC2 Auto Scalingのクォータも消費します。AgentCoreが代理でRunInstancesCreateFleetCreateVolumeAttachNetworkInterfaceなどを呼ぶためです。並列セッションを増やす場合は、対象インスタンスファミリーのvCPU上限やEBSのボリューム数上限も見ておく必要があります。EC2 Auto ScalingのAPIレートクォータはService Quotasコンソールに出ないため、上げる場合はサポートケースが必要です。

公式ドキュメント:Quotas for Amazon Bedrock AgentCore

変更できないもの

  • Capacity providerはdescription以外を変更できません。IaCで変更すると置換(Replacement)になり、古いCapacity providerとともにセッションとEBSも消えます(置換の流れと事前のデータ退避については、第5章「Capacity providerの変更はどう扱うか」を参照してください)。
  • Agent runtimeのコンピュートタイプは変更できません。microVMで作ったものをRuntime instancesに変える(またはその逆)には、agent runtimeを作り直します。
  • Capacity providerは、関連付いているAgent runtimeがある間は削除できません。先にAgent runtimeを削除するか、関連付けを解除します。

インスタンスの最大生存時間はデフォルト8時間

lifecycleConfiguration.maxLifetimeはインスタンスの最大生存時間で、到達すると活動中でもインスタンスが終了します。指定しない場合のデフォルトは28,800秒(8時間)、最大が1,209,600秒(14日)です。

「セッションが最大14日」という説明と混同しやすいのですが、両者は別の概念です。セッションの上限が14日で、到達するとAgentCoreがセッションを停止します(インスタンスは終了し、永続ボリュームは保持されます)。この14日は連続稼働の上限で、停止後も同じセッションIDで呼び出せば永続ボリュームが再アタッチされ、続きから再開できます(再開回数の上限は執筆時点の公式ドキュメントに記載がありません)。

一方、インスタンスの寿命がmaxLifetimeで、到達してもセッションIDと永続ボリュームは残り、同じセッションIDで呼び出せば新しいインスタンスで再開できます。8時間を超えて処理を続ける前提なら、Capacity provider側のmaxLifetimeと、Agent runtime側のlifecycleConfiguration.maxLifetime(こちらもデフォルト8時間で、Capacity provider側以下にする制約があります)の両方を明示しておきます。あわせて、Capacity provider側のidleInstanceTimeoutとAgent runtime側のidleRuntimeSessionTimeout(いずれもデフォルト900秒)も要件に応じて決めておきます。

セキュリティモデル:同じインスタンス上のエージェントは分離されない

Runtime instancesの設計で最も意識しておきたいのがここです。公式ドキュメントのセキュリティモデルから、要点を3つ挙げます。

  1. 同一インスタンス上のエージェント同士に、セキュリティ境界はありません。コンテナで動いていてもプロセスで動いていても、同じファイルシステムを共有します。同じセッションに同居させるエージェントは、相互に信頼できるものに限定します。
  2. 分離の単位はセッション(Capacity provider + セッションIDの組)です。信頼レベルの違うワークロードは、別のセッションに分けます。
  3. AgentCoreはruntimeSessionIdが呼び出し元のものかを検証しません。認可はAgent runtimeのARNに対して行われます。1つのIAMプリンシパルで複数のエンドユーザーを代理呼び出しするマルチテナント構成では、「このユーザーはこのセッションID」というバインドをバックエンド側で実装するのが利用者の責任です。セッションIDをクライアントから受け取ってそのまま渡す設計は避けます。より強い分離が必要なら、ユーザーやテナントごとにIAMプリンシパルを分けると、IAMがセッションのスコープを強制してくれます。

また、実行ロールの認証情報はインスタンス上のすべてのコードから読めるため、Agent runtimeごとの実行ロールは最小権限にしておきます。

なお、「既存のアカウント統制がそのまま効く」と書きましたが、例外が1つあります。公式ドキュメントによると、SCP・アクセス許可境界・VPCの統制はインスタンス上の操作に適用される一方、AgentCoreがリソースの削除・クリーンアップに使うサービスリンクロールはSCPの制約を受けません(サービスリンクロール一般の扱いと同じです)。

公式ドキュメント:Security model and permissions for Runtime Instances

運用上の注意

  • EC2 managed instancesはデフォルトで非表示です。表示設定(managed resource visibility)をvisibleにしても、手元ではdescribe-instancesのフィルタ検索には出てきませんでした。インスタンスIDはAgentCoreが作るAuto Scalingグループ(agentcore-managed-instances-*)から取得し、IDを直接指定して参照するのが確実です。課金は表示設定に関係なく発生します。
  • セッションの再開時は新しいイメージから起動されることがあります。EBSのデータは残りますが、インスタンス上のルートボリュームに書いたものは残りません。永続化したいデータは必ずVolumesで定義したボリュームのマウント先に置きます。
  • CREATE_FAILEDのときはstatusReasonを見るのが近道です。どのリソースの作成に失敗したかが記録されるので、そこからインフラストラクチャロールの権限やVPC設定を確認します。

8. まとめ

今回は、Amazon Bedrock AgentCore RuntimeのRuntime instancesコンピュートタイプを、AWS CDKで一式構築してみました。

  • Runtime instancesは、エージェントをご自身のアカウント内のAWS管理EC2で動かすコンピュートタイプです。最大14日のセッション、EBSによる永続ストレージ、GPU、同一ホストでの複数エージェント協調が可能で、既存のSavings PlansやODCR、アカウント統制がそのまま効きます。
  • 構成の要はCapacity provider(どこで実行するか)とAgent runtime(何を実行するか)の分離です。3種類のIAMロール(インフラストラクチャロール・インスタンスプロファイル・実行ロール)の役割を押さえると、全体像が見えやすくなります。
  • Capacity providerは作成後にdescriptionしか変更できないため、最初からIaCで組む価値が大きい構成です。CDKのL1 constructはCloudFormationと1対1なので、一次情報で裏取りしながら書けます。Terraformで組む場合は、執筆時点ではawsccプロバイダを使うことになります。
  • 見落としやすいのは、maxLifetimeのデフォルト(Capacity provider側・Agent runtime側ともに8時間)、停止中のセッションでもEBSは課金されること、そして同一インスタンス上のエージェントは分離されないことです。

実務での使い分けとしては、まずmicroVMから始めて、「8時間で足りない」「GPUが要る」「複数エージェントに作業ディレクトリを共有させたい」といった要件が具体的になった時点でRuntime instancesに移す、という進め方が負担が少ないと思います。そしてRuntime instancesを選んだときは、コンソールで動作確認をしたあと、この記事のようにIaCに落として運用に乗せる流れをおすすめします。

この記事が皆さんの一助になれば幸いです!

Related posts