0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AWS Bedrock AgentCore Runtime V2 は起動が速くなった!?

0
Last updated at Posted at 2026-09-23

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()

参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?