2026 年 9 月 18 日に Amazon Bedrock AgentCore Runtime の platformVersion V2 がリリースされました。
V2 における変更内容を理解し、自作の AI エージェントで効果を実測してみました。
TL;DR
- V2 は「コンテナを毎回起動する」のではなく、一度初期化した環境のスナップショットを取り、新しいインスタンスはそれを復元して起動する方式です。
- コールドスタートを 66MB と 2GB のイメージで測ったところ、V1 はコールドスタートが 5.6 秒 → 30 秒と線形に悪化したのに対し、V2 はどちらも約 2 秒で一定でした。公称どおりです。
- V2 の価値は 起動時間の安定性にあります。V1 は 0.5 秒と 30 秒にばらけますが、V2 はどのリクエストも約 2 秒です(P95: 30 秒 → 2.3 秒)。
- 副作用も実証できました。起動時に生成した UUID などの値は、新規セッション 10 回ですべて同一でした。一方、ハンドラで生成した値は毎回正しく変化しました。
環境: リージョン
ap-northeast-1、実測日 2026-09-23、boto3 1.43.100、bedrock-agentcore SDK 1.4.7。AWS アカウント ID などは伏せています。
はじめに
AgentCore Runtime は、セッションごとに専用の microVM を立ち上げてエージェントを動かすサーバレス実行環境です。セッション分離が強い反面、「新しいセッションの最初の応答が遅い(コールドスタート)」「コンテナイメージが大きいほど遅い」という課題がありました。
2026 年 9 月 18 日に GA した platformVersion V2 は、この起動方式を根本から変えるものです。公式ドキュメントには次のように書かれています。
Amazon Bedrock AgentCore Runtime V2 starts your agent from a snapshot, which keeps cold starts fast and consistent regardless of concurrency or image size.
(Platform versions - How AgentCore Runtime works)
Amazon Bedrock AgentCore Runtime V2 はスナップショットからエージェントを起動するため、同時実行数やイメージサイズにかかわらず、コールドスタートが速く一定に保たれます。
V2 の概要
V2 への切り替え方法
切り替えは platformVersion を V2 にするだけで、コンテナイメージやコードの変更は不要です。新規作成なら create-agent-runtime に、既存の Runtime なら update-agent-runtime に指定します。update で省略した場合は現在の platformVersion が維持されます。
# 既存の Runtime を V2 に切り替える(他のパラメータは現状と同じ値を渡す)
aws bedrock-agentcore-control update-agent-runtime \
--agent-runtime-id <runtime-id> \
--role-arn <実行ロール ARN> \
--agent-runtime-artifact '{"containerConfiguration": {"containerUri": "<ECR イメージ URI>"}}' \
--network-configuration '{"networkMode": "PUBLIC"}' \
--platform-version V2
# 反映の確認(update の応答には platformVersion が含まれないため)
aws bedrock-agentcore-control get-agent-runtime \
--agent-runtime-id <runtime-id> --query '[status, platformVersion]'
update を呼ぶと新しいバージョンが作られ、DEFAULT エンドポイントが自動でそれを指します。スナップショットの準備があるため、status が READY になるまで数分かかります(後述の実証用エージェントで 186〜232 秒)。その間に再度 update や delete を呼ぶと ConflictException になるので、get-agent-runtime で READY を待ってください。
コンソールからは Runtime の作成・編集画面の「Platform version」で V2 を選べます。CloudFormation と CDK は platformVersion に未対応(2026 年 9 月時点)なので、IaC で管理している Runtime も、切り替え自体は CLI または SDK から呼ぶことになります。
V1 と V2 の起動方式の違い
| V1(デフォルト値) | V2 | |
|---|---|---|
| 新規インスタンスの起動 | イメージを pull してコンテナを起動し、初期化コードを毎回実行 | 事前に用意した スナップショットを復元 |
| スナップショットの作成タイミング | なし | create / update 時に環境を起動し、最初に /ping が Healthy を返した時点で取得 |
| create / update の所要時間 | 数秒で READY | スナップショット準備のため 数分 |
| 対象リージョン | 全リージョン | us-east-1 / us-east-2 / us-west-2 / eu-west-1 / ap-northeast-1 |
| 課金 | 従来どおり | 実際に使ったリソースに基づく(メモリを解放すると回収される) |
V2 利用時の制約と注意点
公式ページに書かれている制約を列記します。デプロイ・運用に関わるものと、エージェントのコードに関わるものに分けています。
デプロイ・運用の制約(How AgentCore Runtime works より)
| 制約 | 内容 |
|---|---|
| 対象リージョン | us-east-1 / us-east-2 / us-west-2 / eu-west-1 / ap-northeast-1 の 5 リージョン |
| create / update の所要時間 | スナップショット準備のため数分かかる(V1 は数秒) |
| ヘルスチェックの期限 | 起動から 120 秒以内に /ping が Healthy を返さないと、ヘルスチェックエラーで作成に失敗する |
| スナップショットの取得時点 | 最初に /ping が Healthy を返した時点 |
| 終端状態の待機 | create / update は CREATING / UPDATING のまま返る。終端状態になる前に update / delete を呼ぶと ConflictException
|
| 環境変数のサイズ | 合計 1.5KB(直接コードデプロイ)/ 2.5KB(コンテナ)。V1 の 4KB より小さい。超えると ValidationException。ただし、V1 と同じ上限に引き上げる予定と明記あり |
| スナップショットのライフサイクル | エンドポイントが指すバージョンごとに 1 つ。update で新しいものが作られ、古いものは既存セッション終了後に削除(最長 8 時間) |
| Infrastructure as Code |
CloudFormation と CDK は platformVersion の設定に未対応(2026 年 9 月時点) |
コードの制約(Optimize your agent for AgentCore Runtime V2 より)
Use one rule to decide where code belongs. Compute a value at startup if it stays the same for the life of the snapshot. Compute it in your handler if it varies per request or can expire.
起動時(モジュールスコープ)で計算したものはスナップショットに入り、全インスタンスにコピーされます。この規則から、次の制約が導かれます。
| # | 制約 | 内容 |
|---|---|---|
| 1 | 初期化は起動時に 1 回 | 依存の import、モデル重みの読み込み、静的設定の読み込みは app.run() より前に済ませる。SDK を使う場合はサーバーが起動するまで /ping が成功しないので、ゲートは不要。自前サーバーなら初期化完了後に Healthy を返すように実装する必要がある |
| 2 | 再デプロイなしに変わるものを起動時に固定しない | 例として Gateway から取得したツールカタログ。起動時にキャッシュするとスナップショット時点で凍結される |
| 3 | 乱数・識別子・トークン | リクエストごとに os.urandom() / secrets / uuid.uuid4() で生成する。起動時の値は全インスタンスで同一。random モジュールも起動時にシードされるため、復元されたインスタンスは同じ列を繰り返す |
| 4 | 現在時刻 | リクエストごとに取得する。起動時のタイムスタンプはスナップショット時点で固定 |
| 5 | 経過時間 | 基準時刻をハンドラ内で取る。time.monotonic() は復元をまたいで進まないため、起動時基準の経過時間はエラーにならずに誤った値になる |
| 6 | クレデンシャル | ハンドラ内で、期限切れ時に更新する。起動時に読んだものはインスタンス起動前に期限切れになりうる |
| 7 | インスタンスの識別 |
全インスタンスが hostname=localhost、PID=1 を返す。ホスト名や PID を ID に使うとメトリクス・ログストリーム・ロック所有者が全インスタンスで衝突する。リクエストごとに ID を生成する |
| 8 | 暗号ライブラリ | 起動時に乱数状態をキャッシュするライブラリは、復元後に同じ状態を使い回す。自前で持ち込む場合は snapsafe ビルド(Amazon Linux 2023 なら openssl-snapsafe-libs)を使う。直接コードデプロイの基盤イメージは対応済み |
| 9 | ネットワーク | 起動時に開いたソケットは復元後に残らないが、クライアントがキャッシュした設定(エンドポイント解決、認証情報解決、接続プール)は残る。クライアントは起動時に作って一度呼び出しておき、復元後の初回呼び出しで透過的に再接続されることを想定する。固定の送信元ポートにバインドしない |
このうち 3「乱数・識別子・トークン」、4「現在時刻」、5「経過時間」、7「インスタンスの識別」が実際にどう見えるのかを、計測 B で確認します。
計測の設計
測りたいことは 2 つです。
| 記号 | 目的 | 方法 |
|---|---|---|
| A | コールドスタートはイメージサイズに依存するか |
mode=echo の最小応答を、毎回新しい runtimeSessionId で 20 回呼ぶ。イメージは 66MB(base)と 2.06GB(2gb)の 2 種類 |
| B | 起動時に生成した値とハンドラで生成した値はどうなるか |
mode=boot で起動時に生成した値を返し、新規セッション 10 回で比較する |
ポイントは次の 3 点です。
-
runtimeSessionIdを毎回変えると、AgentCore は初見のセッションとして新しい microVM を用意します。これで各回がコールドスタート込みの計測になります。比較用に、同じセッション ID を続けて使う「ウォーム」も 5 回測ります。 -
2GB イメージは
/dev/urandomで作る。ゼロ埋めだと圧縮されて転送サイズが小さくなるので、乱数で 1900MB のパディング層を追加しました。ECR 上の実サイズは 2,059,136,866 バイトでした。 -
起動時の値の代表として、UUID・時刻・PID・ホスト名・疑似乱数器をモジュールスコープで生成し、対照群としてリクエスト時の
os.urandom()/uuid4()も返します。
実証用のエージェントを開発
依存は bedrock-agentcore SDK だけです。全文は巻末の「付録 A: agent.py 全文」に載せています。ここでは構造だけ説明します。
エージェントに送る JSON(payload)の mode で動作を切り替えます。
| mode | 動作 | 使う計測 |
|---|---|---|
echo(デフォルト値) |
何もせず、受け取った payload をそのまま返す | 計測 A(コールドスタート) |
boot |
起動時に生成した UUID・時刻・PID・ホスト名・乱数列と、対照としてリクエスト時の os.urandom() / uuid4() / 現在時刻を返す |
計測 B(起動時の値とハンドラの値) |
計測 B の観測対象は、モジュールスコープで生成している次の値です。
# ---- 起動時(モジュールスコープ)に生成される値 --------------------------------
# V1: インスタンスごとに異なる / V2: スナップショット復元により全インスタンス同一(想定)
BOOT_ID = str(uuid.uuid4()) # 起動時 UUID
BOOT_TIME = time.time() # 起動時の壁時計
BOOT_MONO = time.monotonic() # monotonic の基準値
BOOT_PID = os.getpid()
BOOT_HOST = socket.gethostname()
# 疑似乱数器。作成時に OS の乱数器から取った値で内部状態を初期化し、random() を呼ぶたびに
# 内部状態が 1 歩進んで次の値が決まる。同じ内部状態から同じ回数呼べば、同じ値が同じ順で出る。
_RNG = random.Random()
app = BedrockAgentCoreApp()
Dockerfile はこうです。PAD_MB でイメージサイズの変種を作れます。
FROM --platform=linux/arm64 public.ecr.aws/docker/library/python:3.13-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PIP_NO_CACHE_DIR=1
COPY requirements.txt .
RUN pip install -r requirements.txt # bedrock-agentcore==1.4.7
ARG PAD_MB=0
RUN if [ "$PAD_MB" -gt 0 ]; then dd if=/dev/urandom of=/pad.bin bs=1048576 count="$PAD_MB"; fi
COPY agent.py .
RUN useradd -m -u 1000 bedrock_agentcore
USER bedrock_agentcore
EXPOSE 8080
CMD ["python", "agent.py"]
計測 A: コールドスタートはイメージサイズに依存するか
4 条件の比較
イメージ 2 種類(base 66MB / 2gb 2.06GB)× platformVersion 2 種類で、新規セッション 20 回ずつ測りました。
公式ブログでも使われていますが、念のため P50 / P75 / P95 を説明します。これらはパーセンタイル(percentile)と呼ばれる指標で、測った応答時間を小さい順に並べたとき、下から 50% / 75% / 95% の位置にある値です。P50 は中央値で「典型的な 1 回」を、P95 は「20 回中 19 回はこの時間以内に収まった」という上限側の目安を表します。平均は少数の極端に遅い値に引きずられますが、パーセンタイルは分布の形をそのまま見せてくれるので、レイテンシの比較ではこちらを使うのが一般的です。
| base-V1 | base-V2 | 2gb-V1 | 2gb-V2 | |
|---|---|---|---|---|
| P50 | 0.522 s | 1.893 s | 0.643 s | 1.962 s |
| P75 | 0.645 s | 2.048 s | 0.797 s | 2.096 s |
| P95 | 5.636 s | 2.388 s | 29.826 s | 2.333 s |
| max | 5.650 s | 2.918 s | 30.142 s | 2.580 s |
| mean | 1.782 s | 1.996 s | 6.467 s | 1.993 s |
| warm P50(同一セッション ID を 5 回続けて指定) | 0.220 s | 0.206 s | 0.192 s | 0.225 s |
実測して分かったこと
1. V1 の応答時間は 2 つの群にはっきり分かれ、P50 だけ見ると速い
V1 の 20 回の応答時間は、0.5 秒前後の群と 5.5 秒(2GB では 30 秒)前後の群にはっきり分かれます。
| base-V1 | 2gb-V1 | |
|---|---|---|
| コールドスタート群 | 5 回、5.50〜5.65 s | 4 回、29.6〜30.1 s |
| ウォームスタート群 | 15 回、0.43〜0.65 s | 16 回、0.50〜0.85 s |
0.5 秒前後の群は、AgentCore が事前に起動しておいた microVM に当たったウォームスタート、5.5 秒(2GB では 30 秒)前後の群はイメージの pull から始まるコールドスタートと解釈しています。どちらの Runtime でもコールドスタートが 1〜2 回目と 13 回目前後に固まって出ており、事前起動分が尽きた直後に補充される挙動が見えます。
2. V1 のコールドスタートはイメージサイズに線形に悪化する
コールドスタートは 66MB で 5.5 秒、2.06GB で 30 秒でした。差の約 24.5 秒を追加転送量 1.99GB で割ると 約 80MB/s で、イメージの pull がそのまま起動時間に乗っていることが分かります。ウォームスタート群はほとんど変わりません(0.1〜0.2 秒差)。
3. V2 はサイズによらず約 2 秒で一定
V2 は base と 2gb で P50 / P75 / P95 の差がすべて 0.1 秒以内でした。公式が「echo で P75 約 2 秒」「イメージサイズに依存しない」と言っている数字と、そのまま一致しました。20 回中に 5 秒を超える外れ値は 1 つもありません。
4. V2 の価値は起動時間の安定性
P50 で比べると V1 のウォームスタート(0.5 秒)のほうが V2(1.9 秒)より速いので、「V2 にすれば常に速くなる」わけではありません。V1 はウォームスタートかコールドスタートかで 0.5 秒と 30 秒にばらけますが、V2 は どのリクエストも安定して約 2 秒です(P95 は 30 秒から 2.3 秒)。ユーザー対話の初回応答に上限時間の要件があるなら、この差は決定的です。
計測 B: 起動時に生成した値とハンドラで生成した値はどうなるか
mode=boot を新規セッション 10 回で呼び、起動時(モジュールスコープ)に生成した値とハンドラ内で生成した値が、セッションをまたいでどう変わるかを比べました。
| 生成場所 | 値 | V1(10 回) | V2(10 回) |
|---|---|---|---|
| 起動時 | UUID(uuid4()) |
10 種類 | 1 種類 |
| 起動時 | 疑似乱数器の値列(random.Random() から 3 個) |
10 種類 |
1 種類(全回 [0.624890, 0.360717, 0.103971]) |
| 起動時 | タイムスタンプ(time.time()) |
各インスタンスの起動時刻 | スナップショット取得時刻で固定(update 開始から 88 秒後) |
| 起動時 | hostname / PID |
localhost / 1
|
localhost / 1
|
| ハンドラ内 | os.urandom(8) |
毎回異なる | 毎回異なる |
| ハンドラ内 | uuid4() |
毎回異なる | 毎回異なる |
| ハンドラ内 | time.time() |
現在時刻 | 現在時刻 |
実測して分かったこと
- V2 では、起動時に生成した値は全インスタンスで同一になります。 スナップショットにはプロセスのメモリがそのまま入るので、UUID も、疑似乱数器の内部状態も、タイムスタンプも、スナップショット取得時点の値がコピーされます。起動時にセッションキーやトークンや乱数シードを作っているコードは、V2 では全インスタンスが同じ値を持ちます。
-
V2 でも、ハンドラ内で生成した値は正しく変化します。
os.urandom()/uuid4()/time.time()はどれも 10 回とも別の値で、復元後の OS の乱数器と時計は正しく動いています。乱数・識別子・時刻はハンドラ内で生成すれば安全です。 -
time.monotonic()は復元をまたいで進みません。 追加で 1 回呼んだとき、壁時計の差now - boot_timeは 240 秒でしたが、time.monotonic()の差は 68.6 秒でした。起動時を基準にした経過時間は、エラーにならずに誤った値になります。 - hostname と PID は V1 でも V2 でも同じ値でした。 V1 では偶然の一致ですが、V2 では構造的に同じになります。ホスト名や PID をインスタンス ID に使うと、全インスタンスで衝突します。
これらはすべて、公式ページ「Optimize your agent for AgentCore Runtime V2」の「Keep per-request state fresh」の表に書かれているとおりの結果でした。
まとめ
- V2 は「一度初期化してスナップショットを取り、以後は復元して起動する」方式です。
- コールドスタートは V1 が 66MB で 5.6 秒、2GB で 30 秒と線形に悪化する一方、V2 はどちらも約 2 秒でした。価値は 起動時間の安定性にあります。
- 起動時に生成した UUID などの値は、全インスタンスで同一になります。一方、ハンドラで設定する値は、正しく変化します。
- V1 で稼働中の実運用エージェント(VPC + JWT + APM)も 無改変で V2 に移行できました。
付録 A: agent.py 全文
agent.py として保存します。
"""AgentCore Runtime platformVersion V1/V2 比較実測用の最小エージェント。
payload の mode で用途を切り替える:
{"mode": "echo"} … コールドスタート計測(デフォルト値。何もせず即応答)
{"mode": "boot"} … 起動時生成値を返す(V2 では全インスタンス同値になるはず)
"""
import os
import random
import socket
import time
import uuid
from bedrock_agentcore.runtime import BedrockAgentCoreApp
# ---- 起動時(モジュールスコープ)に生成される値 --------------------------------
# V1: インスタンスごとに異なる / V2: スナップショット復元により全インスタンス同一(想定)
BOOT_ID = str(uuid.uuid4()) # 起動時 UUID
BOOT_TIME = time.time() # 起動時の壁時計
BOOT_MONO = time.monotonic() # monotonic の基準値
BOOT_PID = os.getpid()
BOOT_HOST = socket.gethostname()
# 疑似乱数器。作成時に OS の乱数器から取った値で内部状態を初期化し、random() を呼ぶたびに
# 内部状態が 1 歩進んで次の値が決まる。同じ内部状態から同じ回数呼べば、同じ値が同じ順で出る。
_RNG = random.Random()
app = BedrockAgentCoreApp()
@app.entrypoint
def handler(payload, context):
mode = (payload or {}).get("mode", "echo")
if mode == "boot":
return {
"boot_id": BOOT_ID,
"boot_time": BOOT_TIME,
"boot_monotonic_base": BOOT_MONO,
"monotonic_since_boot": time.monotonic() - BOOT_MONO,
"pid": BOOT_PID,
"hostname": BOOT_HOST,
# 起動時シードの乱数器からの値: V2 なら全インスタンスで同一列を返すはず
"rng_seq": [_RNG.random() for _ in range(3)],
# 対照群: 呼び出しごとに OS の乱数器から直接取る値(スナップショット復元後でも毎回異なるはず)
"urandom_hex": os.urandom(8).hex(),
"request_uuid4": str(uuid.uuid4()),
"now": time.time(),
}
# echo: 最小の即時応答
return {"echo": payload, "boot_id": BOOT_ID}
if __name__ == "__main__":
app.run()