AWS Lambda MicroVMsでAIエージェント向けの検証サンドボックスを自作してみた
AIエージェントに生成コードを実行させるとき、実行環境の分離方法で悩んだことがありました。今回は、AWS Lambda MicroVMsを使って、AIエージェント向けの検証サンドボックス(コードを受け取り、lint・型チ […]
目次
AIエージェントに生成コードを実行させるとき、実行環境の分離方法で悩んだことがありました。今回は、AWS Lambda MicroVMsを使って、AIエージェント向けの検証サンドボックス(コードを受け取り、lint・型チェック・テストを実行して結果を返すだけの最小構成)を実際に構築したので、動かした結果をまとめます。
前提条件の確認
検証を始める前に、CPUアーキテクチャとAWS CLIのバージョンの2点を確認しました。
アーキテクチャを確認する
update-microvm-imageのヘルプを見ると、CPUアーキテクチャを指定するcpuConfigurationsパラメータのarchitectureで選べる値はARM_64のみでした。
aws lambda-microvms update-microvm-image help
architecture -> (string) [required]
The CPU architecture.
Possible values:
+o ARM_64
今回の検証用アプリ(node:24-alpineベース)はこのARM64環境でそのまま動作しました。
AWS CLIのバージョンを確認する
lambda-microvmsはLambda本体とは別に追加された、独立したコマンド名前空間です。手元のAWS CLIが古いと、コマンド自体が認識されません。
aws --version
aws lambda-microvms help
検証時点でのAWS CLI(2.32.13)では、以下のエラーが返ってきました。
aws: [ERROR]: argument command: Found invalid choice 'lambda-microvms'
2.35.24へアップデートしたところ、lambda-microvms helpが正常に表示されました。
Dockerfileを用意してMicroVMイメージを作る
Lambda MicroVMsでは、アプリケーションコードとDockerfileをzipにまとめてAmazon S3にアップロードし、そこからcreate-microvm-imageでMicroVMイメージを作成します。
検証用アプリを用意する
AIエージェントが生成したコードに対して、実際にlint・typecheck・testを実行できる最小のTypeScriptプロジェクトを用意しました。
agent-sandbox/
├── package.json # biome / typescript / vitest を devDependencies に指定
├── biome.json # linter + formatter 設定
├── tsconfig.json # strict + noEmit
├── src/
│ ├── sum.ts # 検証対象のソース
│ └── sum.test.ts # vitest のユニットテスト
├── server.js # コード実行を受け付ける HTTP サーバー
└── Dockerfile
server.jsは、POST /execでコマンドを受け取り、child_process.execで実行してからexitCode・stdout・stderrをJSONで返す最小限のHTTPサーバーです。AIエージェントが「実行結果の合否」を判断するには、標準出力だけでなく終了コードが必要になるため、error.codeをexitCodeとして明示的に返す実装にしています。
import { exec } from "node:child_process";
import http from "node:http";
const server = http.createServer((req, res) => {
if (req.method === "POST" && req.url === "/exec") {
let body = "";
req.on("data", (c) => { body += c; });
req.on("end", () => {
const { command } = JSON.parse(body);
exec(command, { cwd: "/app", timeout: 600000 }, (error, stdout, stderr) => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({
exitCode: error && typeof error.code === "number" ? error.code : 0,
stdout,
stderr,
}));
});
});
return;
}
res.writeHead(200);
res.end(JSON.stringify({ status: "ok" }));
});
server.listen(8080, () => console.log("listening on 8080"));
Dockerfileは、依存パッケージをビルド時に焼き込む方式にしています。
FROM node:24-alpine
WORKDIR /app
COPY package.json ./
RUN npm install
COPY biome.json tsconfig.json server.js ./
COPY src ./src
EXPOSE 8080
CMD ["node", "server.js"]
S3にアップロードしてMicroVMイメージを作成する
コードは一旦S3にアップロードします。その後Dockerfileも使ってAWS CLIからイメージをつくりました。
cd agent-sandbox
zip -r ../app.zip .
aws s3 cp ../app.zip s3://<your-bucket-name>/app.zip
aws lambda-microvms create-microvm-image \
--name agent-sandbox \
--code-artifact uri=s3://<your-bucket-name>/app.zip \
--base-image-arn arn:aws:lambda:us-east-1:aws:microvm-image:al2023-1 \
--build-role-arn arn:aws:iam::<account-id>:role/MicrovmBuildRole
ビルドは非同期で進むため、stateがCREATEDになるまでポーリングします。
aws lambda-microvms get-microvm-image --image-identifier <imageArn>
イメージの指定にはARNを使う
get-microvm-imageなどの後続コマンドに、作成時に指定した--nameの値をそのまま渡すと、以下のエラーになりました。
An error occurred (ValidationException) when calling the GetMicrovmImage operation:
Invalid ARN format: agent-sandbox
--image-identifierにはARNを渡す必要があります。create-microvm-imageのレスポンスに含まれるimageArnを控えておくと、以降のコマンドで迷わずに済みます。
起動して、lint・typecheck・testを実際に走らせる
MicroVMイメージができたら、run-microvmで起動します。
aws lambda-microvms run-microvm \
--image-identifier <imageArn> \
--ingress-network-connectors "arn:aws:lambda:us-east-1:aws:network-connector:aws-network-connector:ALL_INGRESS" \
--egress-network-connectors "arn:aws:lambda:us-east-1:aws:network-connector:aws-network-connector:INTERNET_EGRESS" \
--idle-policy '{"autoResumeEnabled":true,"maxIdleDurationSeconds":900,"suspendedDurationSeconds":300}'
このコマンドの応答には、そのMicroVM専用のHTTPSエンドポイントが含まれています。--ingress-network-connectorsと--egress-network-connectorsで指定しているのは、AWSが用意するネットワークコネクタのARNです。起動には初回で約33秒かかりましたが、同一イメージから2回目以降に起動した場合は7〜8秒程度でした。
認証トークンを取得してリクエストを送る
リクエストにはX-aws-proxy-authヘッダーで認証トークンを付与しました。トークンはcreate-microvm-auth-tokenで発行します。
aws lambda-microvms create-microvm-auth-token \
--microvm-identifier <microvmId> \
--expiration-in-minutes 60 \
--allowed-ports '[{"allPorts":{}}]'
検証用アプリはポート8080で待ち受けており、追加のヘッダーなしでリクエストが届きました。
curl "https://<endpoint>/exec" \
-H "X-aws-proxy-auth: <token>" \
-H "Content-Type: application/json" \
-d '{"command":"npm run lint"}'
実際にlint・typecheck・testを実行する
biome ci .(lint)、tsc --noEmit(typecheck)、vitest run(test)をそれぞれ/exec経由で実行しました。最終的にはすべてexitCode 0で完了し、実行時間はいずれも1秒程度でした。
{"exitCode":0,"stdout":"\n> test\n> vitest run\n\n\n RUN v4.1.10 /app\n\n ✓ src/sum.test.ts (1 test) 3ms\n\n Test Files 1 passed (1)\n Tests 1 passed (1)\n","stderr":""}
失敗してもMicroVMは終了しない
src/sum.tsのロジックを意図的に壊し(a + bをa - bに変更)、テストを実行しました。
{"exitCode":1,"stdout":"\n ❯ src/sum.test.ts (1 test | 1 failed)\n × sums two numbers\n","stderr":"AssertionError: expected -1 to be 3\n"}
テストはexitCode 1で失敗しましたが、直後に送った別のリクエスト(echo still alive)にも正常に応答し、MicroVMの状態はRUNNINGのままでした。
長時間かかるコマンドについても確認しました。sleep 150 && echo doneを送信したところ、150.8秒後に正常なレスポンスが返り、リクエスト自体がタイムアウトすることはありませんでした。
停止(suspend)と再開(resume)も試してみる
microVMは停止させたり再開させることができます。このあたりも簡単に試してみました。
--idle-policyで指定したmaxIdleDurationSecondsの間トラフィックがない場合、MicroVMは自動的にサスペンドされます。さらにsuspendedDurationSecondsで指定した時間の間、suspend状態のままリクエストが来なければ、MicroVMは終了(TERMINATED)します。
最初の検証ではmaxIdleDurationSeconds=900(15分)・suspendedDurationSeconds=300(5分)を指定していました。最後のリクエストから約20分後に確認したところ、SUSPENDEDを経てTERMINATEDまで進んでおり、これは指定した2つの秒数(900秒+300秒)どおりの挙動でした。
サスペンドからのresumeを直接確認するため、maxIdleDurationSeconds=120(2分)・suspendedDurationSeconds=600(10分)に変更して再実行しました。
| タイムライン | 経過 |
|---|---|
| 最終リクエストからSUSPENDED確認まで | 約2分16秒 |
| SUSPENDED中にリクエストを送信してから応答まで | 約2秒 |
autoResumeEnabled: trueを指定していたため、suspend中のMicroVMにリクエストを送ると自動的にresumeし、約2秒で応答が返りました。
まとめ
Lambda MicroVMsを使って、AIエージェント向けの検証サンドボックスを一から自作すると、S3へのアップロード、IAMロールの用意、MicroVMイメージのビルド、認証トークンの発行、アイドルポリシーの調整まで、一通りの手順が必要になりました。--image-identifierにARNが必須であることや、イメージの状態がCreated/Updatedで分かれること、サスペンドからそのままターミネートまで進んでしまうタイミングなど、手を動かして初めて気づいた挙動がいくつかありました。
一方で、実際にlint・typecheck・testを実行し、失敗してもMicroVM自体は落ちず、設定ミスをMicroVMの中で直して再検証できることも確認できました。
ただし、チュートリアルに沿ってそのまま組んだ今回の構成では、検証対象のソースをDockerfileでイメージに焼き込んでいます。そのため、検証したいコードが変わるたびにイメージの再ビルド(今回の実測で約3分)が必要になり、ローカルやCIでコードを書いては検証する用途には使いにくそうだと感じました。次は、イメージには実行環境だけを持たせ、任意のコードを起動後に流し込んで動かす方法を考えてみたいと思います。