AVOCAD

Lambda MicroVMs を触ってみた実測値

AWSLambdaMicroVMs

概要

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 の中の ffmpeg で作った動画の 1 コマ

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 台に並べて、同時実行したときの所要時間とコストを測ってみるつもりです。

← ブログ一覧へ戻る