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-imageMicroVMイメージを作成します。

    検証用アプリを用意する

    AIエージェントが生成したコードに対して、実際にlinttypechecktestを実行できる最小の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で実行してからexitCodestdoutstderrをJSONで返す最小限のHTTPサーバーです。AIエージェントが「実行結果の合否」を判断するには、標準出力だけでなく終了コードが必要になるため、error.codeexitCodeとして明示的に返す実装にしています。

    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
    
    

    ビルドは非同期で進むため、stateCREATEDになるまでポーリングします。

    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 + ba - 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でコードを書いては検証する用途には使いにくそうだと感じました。次は、イメージには実行環境だけを持たせ、任意のコードを起動後に流し込んで動かす方法を考えてみたいと思います。

    広告ここから
    広告ここまで
    Home
    Search
    Bookmark