Bedrock AgentCore Runtimeのマネージドセッションストレージでエージェントの作業状態を停止・再開をまたいで永続化する
目次
はじめに
2026年3月25日、AWSはAmazon Bedrock AgentCore Runtimeにマネージドセッションストレージ機能を追加した(パブリックプレビュー)。AIエージェントがファイルシステムに書き込んだ作業状態を、セッションの停止・再開をまたいで自動的に永続化する機能だ。
以前の記事ではInvokeAgentRuntimeCommand APIを検証し、セッション「内」でのファイルシステム共有を確認した。しかしセッションが停止するとmicroVMごと破棄され、ファイルはすべて失われていた。コーディングエージェントが夜通しプロジェクトを構築しても、翌朝再開するとゼロからやり直しになる。マネージドセッションストレージはこの課題を解決する。
本記事では、コード構成(S3 ZIP)で最小限のランタイムを構築し、5つの観点からセッションストレージの実挙動を検証した結果を共有する。公式ドキュメントはPersist session state across stop/resumeを参照。
セッションストレージの仕組み
ランタイム作成時にfilesystemConfigurationsでマウントパスを指定すると、各セッションに専用の永続ディレクトリが割り当てられる。
| 項目 | 内容 |
|---|---|
| マウントパス | 任意(例: /mnt/workspace) |
| 最大容量 | セッションあたり1 GB |
| データ保持期間 | アイドル14日間 |
| セッション隔離 | 各セッションは自分のストレージのみアクセス可能 |
| 対応操作 | 通常ファイル、ディレクトリ、シンボリックリンク、POSIX標準操作 |
| 非対応 | ハードリンク、デバイスファイル、FIFO、UNIXソケット、xattr、fallocate |
| リセット条件 | 14日間未使用、またはランタイムバージョン更新 |
ライフサイクルは以下の通りだ。
- 初回起動 — マウントパスに空ディレクトリが作られる
- エージェントがファイルを読み書き — バックグラウンドで耐久ストレージに非同期レプリケーション
- セッション停止 — 未永続化データがグレースフルシャットダウン中にフラッシュ
- 再開 — 新しいmicroVMが同じストレージをマウントし、ファイルシステムが復元
エージェント側にチェックポイントロジックやsave/restoreコードは一切不要だ。
なお、マウントパスはエージェント呼び出し時(invoke時)にのみ利用可能で、コンテナの初期化中はアクセスできない点に注意が必要だ。
検証環境の構築
前提条件:
- AWS CLIセットアップ済み(
bedrock-agentcore:*、iam:*、s3:*の操作権限) - AWS CLI 2.34.16以上(
--filesystem-configurationsパラメータのサポートが必要) - boto3 1.42.76以上(検証スクリプトで使用)
- 検証リージョン: us-west-2
検証結果だけ読みたい場合は検証1: 停止・再開でファイルは復元されるかまで飛ばしてよい。
デプロイ手順(検証環境の再現)
エージェントコード
最小限のエージェントコードを作成する。セッションストレージの検証にはInvokeAgentRuntimeCommandでシェルコマンドを実行するため、エージェント自体は最小実装で十分だ。
import json
import sys
def handle_invoke(event):
user_input = event.get("input", {}).get("text", "")
return {
"output": {
"text": f"Received: {user_input}. This is a minimal test agent."
}
}
def main():
for line in sys.stdin:
line = line.strip()
if not line:
continue
try:
request = json.loads(line)
response = handle_invoke(request)
print(json.dumps(response), flush=True)
except json.JSONDecodeError:
print(json.dumps({"error": "Invalid JSON"}), flush=True)
if __name__ == "__main__":
main()S3アップロードとIAMロール作成
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
BUCKET_NAME="agentcore-session-storage-test-${ACCOUNT_ID}"
REGION="us-west-2"
# コードをZIP化してS3にアップロード
zip agent.zip main.py
aws s3 mb "s3://${BUCKET_NAME}" --region "$REGION"
aws s3 cp agent.zip "s3://${BUCKET_NAME}/agent.zip"
# IAMロール作成
aws iam create-role \
--role-name AgentCoreSessionStorageTestRole \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "bedrock-agentcore.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}'
aws iam attach-role-policy \
--role-name AgentCoreSessionStorageTestRole \
--policy-arn arn:aws:iam::aws:policy/AmazonBedrockFullAccess
aws iam put-role-policy \
--role-name AgentCoreSessionStorageTestRole \
--policy-name S3Access \
--policy-document "{
\"Version\": \"2012-10-17\",
\"Statement\": [{
\"Effect\": \"Allow\",
\"Action\": [\"s3:GetObject\", \"s3:ListBucket\"],
\"Resource\": [
\"arn:aws:s3:::${BUCKET_NAME}\",
\"arn:aws:s3:::${BUCKET_NAME}/*\"
]
}]
}"ランタイム作成(セッションストレージ付き)
--filesystem-configurationsでセッションストレージのマウントパスを指定する。このパラメータはAWS CLI 2.34.16以降で利用可能だ。
aws bedrock-agentcore-control create-agent-runtime \
--region "$REGION" \
--agent-runtime-name session_storage_test_agent \
--role-arn "arn:aws:iam::${ACCOUNT_ID}:role/AgentCoreSessionStorageTestRole" \
--agent-runtime-artifact "{
\"codeConfiguration\": {
\"code\": {\"s3\": {\"bucket\": \"${BUCKET_NAME}\", \"prefix\": \"agent.zip\"}},
\"runtime\": \"PYTHON_3_13\",
\"entryPoint\": [\"main.py\"]
}
}" \
--network-configuration '{"networkMode": "PUBLIC"}' \
--filesystem-configurations '[{
"sessionStorage": {
"mountPath": "/mnt/workspace"
}
}]'
# → レスポンスの agentRuntimeId を控えるエンドポイント作成
RUNTIME_ID="session_storage_test_agent-XXXXXXXXXX" # 実際のIDに置き換え
aws bedrock-agentcore-control create-agent-runtime-endpoint \
--region us-west-2 \
--agent-runtime-id "$RUNTIME_ID" \
--name session_storage_test_endpoint
# READYまでポーリング
while true; do
STATUS=$(aws bedrock-agentcore-control get-agent-runtime-endpoint \
--region us-west-2 \
--agent-runtime-id "$RUNTIME_ID" \
--endpoint-name session_storage_test_endpoint \
--query 'status' --output text)
echo "Endpoint status: $STATUS"
[ "$STATUS" = "READY" ] && break
sleep 10
done以降のテストはすべてPythonヘルパー経由で実行している。InvokeAgentRuntimeCommandでシェルコマンドを実行し、StopRuntimeSession(データプレーンAPI)でセッションを停止する仕組みだ。
テストヘルパーコード(test_helper.py)
import boto3, uuid, json, time
REGION = "us-west-2"
RUNTIME_ARN = "arn:aws:bedrock-agentcore:us-west-2:111122223333:runtime/RUNTIME_ID"
client = boto3.client("bedrock-agentcore", region_name=REGION)
def make_session_id():
"""セッションIDは33文字以上が必要なため、UUIDを結合して生成する。"""
return str(uuid.uuid4()) + "-" + str(uuid.uuid4())[:8]
def run_command(command, timeout=60, session_id=None):
"""コマンドを /bin/bash -c で実行し、EventStreamを処理して結果を返す。"""
response = client.invoke_agent_runtime_command(
agentRuntimeArn=RUNTIME_ARN,
runtimeSessionId=session_id,
qualifier="DEFAULT",
contentType="application/json",
accept="application/vnd.amazon.eventstream",
body={"command": f'/bin/bash -c {json.dumps(command)}', "timeout": timeout},
)
stdout, stderr, exit_code, status = [], [], None, None
for event in response.get("stream", []):
if "chunk" in event:
chunk = event["chunk"]
if "contentDelta" in chunk:
d = chunk["contentDelta"]
if d.get("stdout"): stdout.append(d["stdout"])
if d.get("stderr"): stderr.append(d["stderr"])
if "contentStop" in chunk:
exit_code = chunk["contentStop"].get("exitCode")
status = chunk["contentStop"].get("status")
return {"stdout": "".join(stdout), "stderr": "".join(stderr),
"exit_code": exit_code, "status": status}
def stop_session(session_id):
"""データプレーンAPIでセッションを停止する。"""
return client.stop_runtime_session(
agentRuntimeArn=RUNTIME_ARN, runtimeSessionId=session_id
)検証1: 停止・再開でファイルは復元されるか
最も基本的な検証として、ファイル・ディレクトリ・シンボリックリンク・パーミッションが停止・再開をまたいで復元されるかを確認した。
マウントポイントの実態
まずdf -hでマウントポイントの情報を確認した。
Filesystem Size Used Avail Use% Mounted on
127.0.0.1:/export 1.0G 0 1.0G 0% /mnt/workspacemountコマンドの出力:
127.0.0.1:/export on /mnt/workspace type nfs4
(rw,relatime,vers=4.0,rsize=1048576,wsize=1048576,namlen=255,
acregmin=3600,acregmax=3600,acdirmin=3600,acdirmax=3600,
hard,nocto,proto=tcp,timeo=600,retrans=2,sec=sys,
clientaddr=127.0.0.1,local_lock=none,addr=127.0.0.1)セッションストレージの実装はNFS v4 over localhostだ。microVM内でNFSサーバーが動作し、AgentCore Runtimeがバックグラウンドで耐久ストレージへのレプリケーションを管理している。stat -f -c "%T"の結果もnfsと返る。
ファイル復元テスト
ファイル、ディレクトリ、シンボリックリンクを作成し、パーミッションを設定した上で停止・再開した。
検証1の再現コード
from test_helper import *
session = make_session_id()
# ファイル・ディレクトリ・シンボリックリンク作成
run_command(
'mkdir -p /mnt/workspace/project/src && '
'echo "hello from session storage" > /mnt/workspace/project/README.md && '
'echo "print(\'hello\')" > /mnt/workspace/project/src/main.py && '
'chmod 755 /mnt/workspace/project/src/main.py && '
'ln -s /mnt/workspace/project/README.md /mnt/workspace/project/link-to-readme',
session_id=session
)
# 停止 → 15秒待機 → 再開
stop_session(session)
time.sleep(15)
# 復元確認
r = run_command(
'find /mnt/workspace -type f -o -type l | sort && '
'echo "---README---" && cat /mnt/workspace/project/README.md && '
'echo "---SYMLINK---" && readlink /mnt/workspace/project/link-to-readme && '
'echo "---PERMS---" && stat -c "%a %n" /mnt/workspace/project/src/main.py',
session_id=session
)
print(r["stdout"])停止前に作成したファイル:
/mnt/workspace/project/README.md— 内容:hello from session storage/mnt/workspace/project/link-to-readme—/mnt/workspace/project/README.mdへのシンボリックリンク/mnt/workspace/project/src/main.py— パーミッション: 755
15秒待って再開した結果:
/mnt/workspace/project/README.md
/mnt/workspace/project/link-to-readme
/mnt/workspace/project/src/main.py
---README---
hello from session storage
---SYMLINK---
/mnt/workspace/project/README.md
---PERMS---
755 /mnt/workspace/project/src/main.pyファイル内容、ディレクトリ構造、シンボリックリンクのターゲット、パーミッションすべてが完全に復元された。
なお、ドキュメントによるとパーミッションは「保存されるが強制されない」。chmodとstatは正しく動作するが、microVM内ではエージェントが唯一のユーザーとして実行されるため、アクセスチェックは常に成功する。
検証2: コーディングエージェントのワークスペース再現
実際のコーディングエージェントを想定し、pipパッケージのインストールとgitリポジトリの作成を行い、停止・再開後に作業環境が復元されるかを検証した。
セットアップ
コード構成(PYTHON_3_13)ではpipコマンドが直接使えないが、python3 -m pipは利用可能だ。パッケージをマウントパス内にインストールすることで永続化できる。
検証2の再現コード
from test_helper import *
session = make_session_id()
# pip パッケージをワークスペース内にインストール
run_command(
'python3 -m pip install --target=/mnt/workspace/pylibs requests',
session_id=session, timeout=120
)
# git リポジトリ作成
run_command(
'cd /mnt/workspace && git init myproject && cd myproject && '
'echo "# Session Storage Test" > README.md && '
'mkdir src && echo "print(\'hello\')" > src/app.py && '
'git add -A && '
'git -c user.email="test@example.com" -c user.name="Test" commit -m "initial commit"',
session_id=session
)
# 停止 → 20秒待機 → 再開
stop_session(session)
time.sleep(20)
# 復元確認
r = run_command(
'echo "=== requests import ===" && '
'PYTHONPATH=/mnt/workspace/pylibs python3 -c "import requests; print(f\'requests {requests.__version__} - OK\')" && '
'echo "=== git log ===" && cd /mnt/workspace/myproject && git log --oneline && '
'echo "=== git status ===" && git status && '
'echo "=== file content ===" && cat src/app.py && '
'echo "=== workspace du ===" && du -sh /mnt/workspace/*',
session_id=session
)
print(r["stdout"])セットアップでは以下のコマンドをエージェントセッション内で実行した。
# pip パッケージをワークスペース内にインストール
python3 -m pip install --target=/mnt/workspace/pylibs requests
# git リポジトリ作成
cd /mnt/workspace && git init myproject
cd myproject
echo "# Session Storage Test" > README.md
mkdir src && echo "print('hello')" > src/app.py
git add -A
git commit -m "initial commit"停止前の状態:
requests 2.33.0 (PYTHONPATH=/mnt/workspace/pylibs)
d34e531 initial commit
37K /mnt/workspace/myproject
3.4M /mnt/workspace/pylibs再開後の確認
20秒待って再開した結果:
=== requests import ===
requests 2.33.0 - OK
=== git log ===
d34e531 initial commit
=== git status ===
On branch master
nothing to commit, working tree clean
=== file content ===
print('hello')
=== workspace du ===
37K /mnt/workspace/myproject
3.4M /mnt/workspace/pylibspipパッケージ(requests + 依存関係で3.4MB)、gitリポジトリ(コミット履歴、ブランチ情報、ワーキングツリー)、ソースコードすべてが完全に復元された。git statusがnothing to commit, working tree cleanを返しているのは、.gitディレクトリ内のインデックスも正しく復元されていることを意味する。
ドキュメントには「アドバイザリロックはセッション内では動作するが、停止・再開をまたいでは永続化されない。ただしgitのようなファイルベースロッキングを使うツールには影響しない」と記載されている。今回の検証でもgitは問題なく動作した。
検証3: セッション間隔離
異なるセッションIDのストレージが完全に隔離されているかを確認した。
検証3の再現コード
from test_helper import *
session_x = make_session_id()
session_y = make_session_id()
# Session X にファイル作成
run_command(
'echo "secret from session X" > /mnt/workspace/secret.txt',
session_id=session_x
)
# Session Y から参照を試みる
r = run_command(
'ls -la /mnt/workspace/ && '
'cat /mnt/workspace/secret.txt 2>&1 || echo "FILE NOT FOUND"',
session_id=session_y
)
print(r["stdout"])Session Xでファイルを作成した後、Session Yから同じパスを参照した。
ls -la /mnt/workspace/
total 4
drwxr-xr-x 2 root root 0 .
drwxr-xr-x 1 root root 4096 ..
cat /mnt/workspace/secret.txt
cat: /mnt/workspace/secret.txt: No such file or directorySession Yのワークスペースは空で、Session Xが作成したsecret.txtは見えない。逆方向も同様で、Session Yが作成したファイルはSession Xからアクセスできなかった。同一ランタイム上の別セッションであっても、ストレージは完全に隔離されている。
検証4: グレースフルシャットダウンの挙動
ドキュメントには「StopRuntimeSessionを呼んだら完了を待ってから再開すること」と記載されている。では待たなかったらどうなるか。
検証4の再現コード
from test_helper import *
# --- 小さいファイル(100行テキスト) ---
session_small = make_session_id()
run_command(
'for i in $(seq 1 100); do echo "line $i" >> /mnt/workspace/bigfile.txt; done && '
'wc -l /mnt/workspace/bigfile.txt',
session_id=session_small
)
stop_session(session_small)
time.sleep(2)
r = run_command('wc -l /mnt/workspace/bigfile.txt', session_id=session_small)
print(r["stdout"])
# --- 大きいファイル(50MB) ---
session_large = make_session_id()
run_command(
'dd if=/dev/urandom of=/mnt/workspace/random.bin bs=1M count=50 2>&1 && '
'md5sum /mnt/workspace/random.bin',
session_id=session_large, timeout=120
)
# 即停止 → 待ち時間なしで再開
stop_session(session_large)
r = run_command(
'md5sum /mnt/workspace/random.bin && du -sh /mnt/workspace/random.bin',
session_id=session_large
)
print(r["stdout"]) # md5sum が一致するか確認小さいファイル(100行テキスト)
100行のテキストファイルを作成し、即停止して2秒後に再開した。
# 停止前
wc -l /mnt/workspace/bigfile.txt → 100
# 2秒後に再開
wc -l /mnt/workspace/bigfile.txt → 100 # ✅ 全行復元大きいファイル(50MB)
# 50MB のランダムバイナリ作成
dd if=/dev/urandom of=/mnt/workspace/random.bin bs=1M count=50
md5sum: 4061cd0be8bf6f4619986721533f7669
# 即停止 → 待ち時間0秒で再開
md5sum: 4061cd0be8bf6f4619986721533f7669 # ✅ 完全一致
size: 50M # ✅ サイズ一致50MBのファイルでも、停止後に待ち時間なしで再開してmd5sumが完全一致した。NFS v4の非同期レプリケーションがセッション稼働中にほぼリアルタイムでデータを永続化しているためだと考えられる。
ただし、これはテスト時の条件(50MB、書き込み完了後に停止)での結果だ。書き込み中に停止した場合や、1GBに近い大量データの場合は挙動が異なる可能性がある。ドキュメントの推奨通り、本番環境ではStopRuntimeSessionの完了を待ってから再開すべきだ。
検証5: エッジケースと非対応操作
非対応操作のエラーメッセージ
ドキュメントに記載されている非対応操作を実際に試し、エラーメッセージを記録した。
| 操作 | コマンド | エラーメッセージ |
|---|---|---|
| ハードリンク | ln file hardlink | Unknown error 524 |
| FIFO | mkfifo testfifo | Unknown error 527 |
| デバイスファイル | mknod testdev c 1 3 | Input/output error |
| fallocate | fallocate -l 1M file | Operation not supported |
ハードリンクとFIFOは標準的なerrnoではなく、NFS固有のエラーコード(524、527)が返る。エージェントのエラーハンドリングでこれらを捕捉する場合、エラーメッセージの文字列マッチが必要になる点に注意が必要だ。
ドキュメントではこれらに加えてUNIXソケットと**拡張属性(xattr)**も非対応と記載されている。xattrはコード構成の環境にsetfattrがプリインストールされていなかったため未検証だ。また、アドバイザリロックはセッション内では動作するが停止・再開をまたいでは永続化されない点もドキュメントに明記されている。
ランタイムバージョン更新時のリセット
ドキュメントには「ランタイムバージョン更新時にファイルシステムがリセットされる」と記載されている。UpdateAgentRuntimeで同じコードのまま更新(バージョン1→2)した後、同じセッションIDで再開した結果:
検証5(バージョン更新)の再現コード
from test_helper import *
session = make_session_id()
# ファイル作成 → 停止
run_command(
'echo "version 1 data" > /mnt/workspace/version-test.txt',
session_id=session
)
stop_session(session)
time.sleep(10)
# ランタイムを更新(同じコードだが新バージョンになる)
control = boto3.client("bedrock-agentcore-control", region_name=REGION)
control.update_agent_runtime(
agentRuntimeId="RUNTIME_ID",
roleArn="arn:aws:iam::111122223333:role/AgentCoreSessionStorageTestRole",
agentRuntimeArtifact={...}, # 作成時と同じ設定
networkConfiguration={"networkMode": "PUBLIC"},
filesystemConfigurations=[{"sessionStorage": {"mountPath": "/mnt/workspace"}}],
)
# READY を待ってから同じセッションIDで再開
time.sleep(15)
r = run_command(
'cat /mnt/workspace/version-test.txt 2>&1 || echo "FILE NOT FOUND (storage was reset)"',
session_id=session
)
print(r["stdout"])ls -la /mnt/workspace/
total 4
drwxr-xr-x 2 root root 0 .
drwxr-xr-x 1 root root 4096 ..
cat /mnt/workspace/version-test.txt
cat: No such file or directory
FILE NOT FOUND (storage was reset)コードが同一でもバージョンが変わればストレージは完全にリセットされる。本番環境でランタイムを更新する際は、必要なデータを事前にS3等に退避する運用が必要だ。
まとめ
冒頭で提示した「セッション停止でエージェントの作業がすべて消える」という課題に対して、マネージドセッションストレージは期待通りに機能した。pipパッケージ、gitリポジトリ、ソースコードを含むワークスペース全体が、停止・再開をまたいで完全に復元される。エージェント側のコード変更は一切不要だ。
検証を通じて、ドキュメントだけでは読み取れない実挙動も明らかになった。
- NFS v4 over localhostが実装基盤 —
df -hで127.0.0.1:/export、mountでNFS v4オプションが確認できる。microVM内のNFSサーバーがAgentCore Runtimeの耐久ストレージへのレプリケーションを担っている。この実装により、標準的なLinuxファイル操作がそのまま動作する。 - 非同期レプリケーションは実質リアルタイム — 50MBのファイルでも停止後に待ち時間なしで再開してmd5sumが一致した。ドキュメントは「完了を待ってから再開」を推奨しているが、通常の使用量であればデータロスのリスクは低い。ただし本番では推奨に従うべきだ。
- コード構成でもpip + gitワークスペースは完全復元される —
--targetオプションでマウントパス内にパッケージをインストールすれば、PYTHONPATH経由で再開後も利用できる。gitの.gitディレクトリ(インデックス、コミット履歴)も正しく復元される。 - バージョン更新は全セッションのストレージをリセットする — コードが同一でも
UpdateAgentRuntimeでバージョンが変わればストレージは消える。CI/CDパイプラインでのランタイム更新時にデータ退避が必要だ。
クリーンアップ
検証後は以下の順序でリソースを削除する。エンドポイント → ランタイム → S3 → IAMの順が依存関係上安全だ。
REGION="us-west-2"
RUNTIME_ID="session_storage_test_agent-XXXXXXXXXX"
BUCKET_NAME="agentcore-session-storage-test-111122223333"
# エンドポイント削除
aws bedrock-agentcore-control delete-agent-runtime-endpoint \
--region "$REGION" \
--agent-runtime-id "$RUNTIME_ID" \
--endpoint-name session_storage_test_endpoint
sleep 15
# ランタイム削除
aws bedrock-agentcore-control delete-agent-runtime \
--region "$REGION" \
--agent-runtime-id "$RUNTIME_ID"
# S3バケット削除(中身ごと)
aws s3 rb "s3://${BUCKET_NAME}" --force --region "$REGION"
# IAMロール削除(ポリシーを先に外す)
aws iam delete-role-policy \
--role-name AgentCoreSessionStorageTestRole --policy-name S3Access
aws iam detach-role-policy \
--role-name AgentCoreSessionStorageTestRole \
--policy-arn arn:aws:iam::aws:policy/AmazonBedrockFullAccess
aws iam delete-role --role-name AgentCoreSessionStorageTestRole