概要
AWS Lambda MicroVMs を触ってみました! 隔離された VM を API 1 回で立てて、手元からファイルとコマンドを送り、重い処理を走らせる…という使い方ができるか、東京リージョンで実際に測っています。
先に数字です(どれも 2026-10-10 に私の環境で 1〜2 回測った値です)。
| 項目 | 実測 |
|---|---|
イメージのビルド(create-microvm-image) |
約 2 分(更新は 2〜3 分) |
run-microvm から RUNNING まで |
10 秒以内(/run フックは約 1.4 秒で届く) |
| VM の中から見えたスペック | 4 vCPU / 約 8 GB |
| 東京から HTTPS エンドポイントへの応答 | 1 回目 0.28 秒、2 回目 0.08 秒 |
| ffmpeg で 1280x720・15 秒の動画を生成 | 8.5 秒 |
| できた mp4(10.6 MB)を手元に取り出す | 1.7 秒 |
| 一時停止中の VM に API を呼んで自動再開 | 0.66 秒で応答 |
| 一時停止 / 終了が反映されるまで | 約 4 秒 / 約 3 秒 |
:::note warn Lambda MicroVMs は 2026 年 6 月に出たばかりのサービスです。仕様は公式ドキュメントと公式スキル(agent-toolkit-for-aws)で確かめたものだけを書き、確かめられなかったところは「未確認」と書いています。 :::
やりたかったこと
手元の Claude Code から指示を出し、重い処理やエージェントの作業はクラウドの使い捨て VM に任せる構成を作りたかったのが出発点です。
- 入力(コード)は手元 → VM、成果物(動画・スライドなど)は VM → 手元の決まったフォルダにだけ戻す
- VM に送るのは
git ls-files -co --exclude-standardの範囲だけ。.envなどは送らない - VM の寿命は最大 8 時間なので、成果物はできた時点で手元に戻す
今回はその PoC として、「ファイルを送る → VM の中で ffmpeg を実行 → mp4 を手元に取り出す」まで動かしました。
Lambda MicroVMs のざっくりした仕組み
公式ドキュメントで確認した範囲です。
- イメージは Dockerfile をルートに置いた zip を S3 に置いて
create-microvm-imageで作る - ビルド時に「起動済みの状態」のスナップショットを撮り、VM はそこから復元して起動する
- 寿命は
maximumDurationInSecondsで最大 28,800 秒(8 時間)。一時停止中も含めた総寿命 - VM ごとに HTTPS のエンドポイントが付き、既定でポート 8080 に転送される。呼ぶときは
create-microvm-auth-tokenで取ったトークン(最大 60 分)をX-aws-proxy-authヘッダーに付ける - ライフサイクルのフック(
/ready/validate/run/resume/suspend/terminate)を VM 側のポート 9000 で受ける
作った構成
VM 側の worker
VM の中では Node の小さな HTTP サーバーを 2 つ立てています。
| ポート | 役割 |
|---|---|
| 9000 | ライフサイクルのフックを受ける |
| 8080 | 手元から呼ぶ API |
| API | 内容 |
|---|---|
GET / |
VM の情報(vCPU 数・メモリ・Node の版) |
POST /exec |
{"cmd": "...", "timeoutSec": 600} を受けて、/workspace で bash を実行し、終了コード・標準出力・所要時間を返す |
PUT /files/<path> |
/workspace/<path> にファイルを書き込む |
GET /files/<path> |
/workspace/<path> のファイルを返す |
/files/ は /workspace の外に出るパス(../ など)を弾き、/exec の出力は 1 MB を超えたら古い側を捨てるようにしました。認証はエンドポイント側のトークンに任せています。
イメージには ffmpeg(BtbN の arm64 静的ビルド)と日本語フォント(Noto Sans CJK JP Bold)を入れました。どちらもダウンロード後にハッシュ値を照合しています。
FROM public.ecr.aws/lambda/microvms:al2023-minimal
# 日本語フォント。ハッシュ値を固定して照合する
RUN mkdir -p /usr/local/share/fonts && \
curl -fsSL -o /usr/local/share/fonts/NotoSansCJKjp-Bold.otf "<フォントの URL>" && \
echo "${FONT_SHA256} /usr/local/share/fonts/NotoSansCJKjp-Bold.otf" | sha256sum -c -
WORKDIR /opt/app
COPY server.mjs ./server.mjs
RUN mkdir -p /workspace
EXPOSE 8080 9000
CMD ["node", "/opt/app/server.mjs"]
IAM の考え方: permissions boundary で「作れるロール」を縛る
VM の中では AI が書いたコードも動かすつもりなので、権限はかなり気を使いました。
| 誰 | 何ができるか |
|---|---|
| 作業用の IAM ユーザー | PowerUserAccess(IAM 以外の操作)+ 名前が <PREFIX>- で始まるロールだけ 作成・変更・iam:PassRole できる。作るときは権限上限ポリシー <PREFIX>-boundary の付与が必須で、上限を外す操作は明示的に拒否 |
| ビルド用ロール | イメージの zip を置く S3 のプレフィックス(microvm-images/)の読み取りと、ビルドログの書き込みだけ |
| 実行用ロール | ログの書き込みだけ。VM の中のコードがこの権限を使えるので、必要になった権限だけを足す |
ポイントは、作業用ユーザー(や、その上で動くエージェント)にロールを作らせつつ、どんなロールを作っても boundary を超える権限は持てないようにしたことです。boundary を付けずにロールを作ろうとすると拒否されます。
aws iam create-role \
--role-name <PREFIX>-execution-role \
--permissions-boundary arn:aws:iam::<ACCOUNT_ID>:policy/<PREFIX>-boundary \
--assume-role-policy-document file://trust.json
信頼ポリシーは lambda.amazonaws.com に sts:AssumeRole と sts:TagSession を許可し、aws:SourceAccount で自分のアカウントに絞りました。公式スキルには aws:SourceArn(microvm-image:*)で絞る例もありますが、実行用ロールで合うかは未確認なので使っていません。
手順
1. VM を起動する
AWS CLI は 2.37.12 以降が必要です(2.32.26 には lambda-microvms コマンドがありませんでした)。
aws lambda-microvms run-microvm \
--image-identifier "${IMAGE_ARN}" \
--image-version 2.0 \
--execution-role-arn "arn:aws:iam::<ACCOUNT_ID>:role/<PREFIX>-execution-role" \
--idle-policy '{"maxIdleDurationSeconds":600,"suspendedDurationSeconds":600,"autoResumeEnabled":true}' \
--maximum-duration-in-seconds 3600 \
--logging '{"cloudWatch":{"logGroup":"/aws/lambda-microvms/<IMAGE_NAME>-runtime"}}' \
--query '[microvmId,endpoint]' --output text
run-microvm から約 1.4 秒で /run フックが届き、10 秒以内に RUNNING になりました。VM のプロセスの起動時刻はビルドしたときのままで、スナップショットから復元されていることが分かります。
VM の中から見ると 4 vCPU / 約 8 GB でした。イメージの最小メモリ 2 GB のちょうど 4 倍なので、「ベースの 4 倍まで縦に伸びる」という仕様どおりなのだと思います(ここは推測です)。
2. ファイルを送って、コマンドを実行する
TOKEN=$(aws lambda-microvms create-microvm-auth-token \
--microvm-identifier "${MICROVM_ID}" --expiration-in-minutes 30 \
--allowed-ports '[{"port":8080}]' \
--query 'authToken."X-aws-proxy-auth"' --output text)
H="X-aws-proxy-auth: ${TOKEN}"
# ファイルを送る
printf 'こんにちは' | curl -sS -X PUT "https://${ENDPOINT}/files/job/hello.txt" -H "${H}" --data-binary @-
# コマンドを実行する
curl -sS -X POST "https://${ENDPOINT}/exec" -H "${H}" -d '{"cmd":"cat job/hello.txt; nproc"}'
# 成果物を取り出す
curl -sS -o out/result.bin "https://${ENDPOINT}/files/out/result.bin" -H "${H}"
トークンなしで呼ぶと 403 になります。東京からの応答は 1 回目 0.28 秒、2 回目 0.08 秒でした。
3. VM の中で動画を作る
字幕のテキストファイルを PUT /files で送り、POST /exec で ffmpeg を実行して、フラクタルの背景に日本語の字幕と音を付けた 1280x720・15 秒の動画を作りました。

VM の中での生成が 8.5 秒、10.6 MB の mp4 を手元に取り出すのが 1.7 秒でした。起動からここまでを 1 本の Node スクリプトにまとめて、最後に VM を一時停止しています。
ブラウザで VM 内のサーバーを見たいときは、ブラウザが X-aws-proxy-auth ヘッダーを付けられないので、手元に小さな中継プロキシを立てて、ヘッダーを付けて転送するようにしました(トークンは 50 分ごとに取り直し)。
4. 止める
aws lambda-microvms suspend-microvm --microvm-identifier "${MICROVM_ID}" # 一時停止
aws lambda-microvms terminate-microvm --microvm-identifier "${MICROVM_ID}" # 終了(再開不可)
一時停止は約 4 秒、終了は約 3 秒で反映されました。autoResumeEnabled=true にしておくと、一時停止中の VM に API を呼ぶだけで自動で再開し、0.66 秒で応答が返ってきました。
ハマりどころ
suspend した VM が終了しない
suspendedDurationSeconds を 600 秒にしていたので、一時停止から 10 分で終了すると思っていたのですが、約 17 分後も SUSPENDED のままでした。手動で suspend-microvm した VM は、maximumDurationInSeconds の上限まで残るようです。
:::note warn
使い終わった VM は terminate-microvm で明示的に終了させましょう。「一時停止しておけばそのうち消える」と思っていると残り続けます。
:::
スナップショットが全 VM 共通なので、乱数と秘密は /run でやり直す
全 VM が同じスナップショットから起動するので、ビルド時に初期化した乱数の状態や取得した値は、全部の VM で同じになります。乱数生成の再初期化と秘密情報の取得は、ビルド時(/ready)ではなく /run /resume のフックで行う必要があります。
一方で、フック(/run /resume /suspend /terminate)は通知専用でタイムアウトが 1〜60 秒なので、重い準備は /ready 側に寄せます。この切り分けが MicroVMs の設計で一番大事なところだと感じました。
ほかにも、ビルド時は /ready → スナップショット → 復元した別の個体で /validate が呼ばれるので、/validate で書いた記録は起動した VM のメモリには残っていませんでした。
ログの出力先が資料によって食い違う
ロググループの名前が、公式スキルでは /aws/lambda-microvms/、公式サンプルでは /aws/lambda/microvms/ と食い違っていました。ロールには両方を許可しておき、実際に試すと、ビルドログは /aws/lambda-microvms/<イメージ名> に出ました。実行時のログは run-microvm --logging で指定したロググループに出ます。
Homebrew の python@3.14 で aws コマンドが起動しない
lambda-microvms コマンドを使うために brew upgrade awscli したら、依存する python@3.14(3.14.8)も上がって、aws コマンド自体が起動しなくなりました。
pyexpat が _XML_SetAllocTrackerActivationThreshold を見つけられない
macOS 26 向けの bottle が 26.3 以降を前提にしていて、当時の私の macOS 26.2 では合わなかったのが原因です(homebrew-core#277330)。同じ Python に依存する gcloud-cli や pipx も巻き込まれます。
応急処置としては、Homebrew の expat を入れて、pyexpat の .so が参照する libexpat を差し替え、署名し直すと動きました。
brew install expat
install_name_tool -change /usr/lib/libexpat.1.dylib \
"$(brew --prefix expat)/lib/libexpat.1.dylib" <pyexpat の .so>
codesign --sign - --force <pyexpat の .so>
ただし Python が更新されると元に戻ります(戻すときは brew reinstall python@3.14)。根本的には macOS を 26.3 以降に上げるのが正解で、私も最終的に macOS を更新して解決しました。
その他の細かいところ
- ベースイメージ(
al2023-minimal)のdnfは microdnf 相当で、--setopt=strict=0を渡すとUnable to handle: strict=0でビルドが失敗する - AL2023 の
dnf install nodejsで入るのは Node 18(v18.20.8)。新しい版が要るなら版付きのパッケージ名を指定する - ffmpeg の
drawtextでtextfile=job/c0.txtのような相対パスは読めなかった。/workspace/job/c0.txtの絶対パスにする - VM の中でサーバーを起動した直後にエンドポイント経由で呼ぶと 502 になる。数秒待つ
- zsh で
$A:roleのように変数の直後に:rなどを書くと、修飾子として解釈されて文字が消える。ARN を組み立てるときは${A}:roleと書く
まとめ
- 10 秒以内に起動して、一時停止からは 0.66 秒で戻るので、「必要なときだけ立てる隔離環境」としてかなり使いやすい印象です
- 15 秒の 720p 動画を 8.5 秒で作れて、取り出しも速いので、手元の Mac で重い処理を回すより気楽でした
- 一方で、スナップショット前提の設計(乱数・秘密は
/runで)と、一時停止した VM が勝手に消えない点は、知らないとハマります
次は同じ処理を 2〜3 台に並べて、同時実行したときの所要時間とコストを測ってみるつもりです。
