メインコンテンツに移動

イメージのビルド待ちなしに AWS Lambda MicroVMs を最新のコードで動かしてみた

2026-10-05 | Author : 岡本 秀高 (CircleCI 合同会社)

Missing alt text value

はじめに

AWS Lambda MicroVMs では、実行環境をコンテナイメージとして用意し、そのイメージから MicroVM を起動して、中でコマンドを実行します。ジョブやバッチの実行環境として使う場合、ランタイムや依存パッケージはこのイメージに含めます。ここで実行対象のコードもイメージに含めると、コードを 1 行修正するたびにイメージを作り直すことになります。

この記事では、この待ち時間を挟まずにジョブを最新のコードで動かす構成の作り方を紹介します。イメージにはランタイムや依存パッケージだけを固定し、更新され続けるソースは実行のたびに取得します。
 

X ポスト » | Facebook シェア » | はてブ »

ご注意

本記事で紹介する AWS サービスを起動する際には、料金がかかります。builders.flash メールメンバー特典の、クラウドレシピ向けクレジットコードプレゼントの入手をお勧めします。

*ハンズオン記事およびソースコードにおける免責事項 »

builders.flash メールメンバー登録

builders.flash メールメンバー登録で、毎月の最新アップデート情報とともに、AWS を無料でお試しいただけるクレジットコードを受け取ることができます。

今すぐ登録 »

利用するサービス

この記事では AWS Lambda MicroVMs、Amazon S3、AWS Identity and Access Management (IAM) を利用します。AWS Lambda MicroVMs は、Dockerfile から作ったイメージのスナップショットを取り、そのスナップショットから仮想マシンレベルの分離環境を起動するコンピューティング基盤です。

検証用アプリを用意する

実行のたびにコードベースを取得する構成では、前回のソースを残さないことが重要になります。展開や取得だけを繰り返すと、前回まで存在して今回削除されたファイルが作業ディレクトリに残り、最新のソースで実行したことになりません。そこで取得の前に作業ディレクトリを作り直します。

javascript
import { exec, spawn } from "node:child_process";
import fs from "node:fs/promises";
import http from "node:http";

const WORK_DIR = "/app/src";

// 作業ディレクトリを作り直し、前回のソースを残さない
async function resetWorkDir() {
  await fs.rm(WORK_DIR, { recursive: true, force: true });
  await fs.mkdir(WORK_DIR, { recursive: true });
}

取得元には、リポジトリから直接 clone する方法と、Amazon S3 に置いた tar.gz を取得する方法の 2 通りを用意します。この後の HTTP サーバーは、リクエストの内容に応じてどちらかを呼び出します。

取得元 1 : リポジトリから clone する

MicroVM 内で Git コマンドを実行し、リポジトリから直接 clone します。

bash
// リポジトリから clone する
function cloneRepository(repository) {
  return new Promise((resolve) => {
    const git = spawn("git", ["clone", repository, WORK_DIR], {
      env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
    });
    let stderr = "";
    git.stderr.on("data", (c) => { stderr += c; });
    git.on("close", (code) => resolve({ exitCode: code, stdout: "", stderr }));
  });
}

GIT_TERMINAL_PROMPT=0 を設定しています。この設定がない場合、認証が必要なリポジトリを対象にすると git が資格情報の入力待ちに入り、リクエストがタイムアウトまで戻りません。なお、この記事では Public なリポジトリを対象としています。Private リポジトリではこれに追加して認証情報の受け渡し処理が必要となりますので、ご注意ください。

取得元 2 : Amazon S3 から取得する

もう 1 つの方法は、Amazon S3 を経由します。tar.gz を Amazon S3 に置き、MicroVM 側から取得して展開します。検証用アプリのイメージには curl が含まれていないため、Node.js 組み込みの fetch で取得し、tar の標準入力へ流し込みます。

bash
// Amazon S3 から tar.gz を取得して作業ディレクトリに展開する
async function fetchObject(objectUrl) {
  const response = await fetch(objectUrl);
  if (!response.ok) {
    return { exitCode: 1, stdout: "", stderr: `status ${response.status}` };
  }
  const buffer = Buffer.from(await response.arrayBuffer());
  return new Promise((resolve) => {
    const tar = spawn("tar", ["xzf", "-", "-C", WORK_DIR]);
    tar.stdin.end(buffer);
    tar.on("close", (code) => resolve({ exitCode: code, stdout: "", stderr: "" }));
  });
}

取得元の tar.gz は、展開時に作業ディレクトリの直下にソースが並ぶよう、-C でソースディレクトリに入ってから固めることを想定しています。取得の認可には署名付き URL を使う想定で、期限があるため、実行のたびに取得する構成では期限内に実行が収まるように発行するか、実行のたびに発行することになります。

エンドポイントを実装する

続いて、取得から実行までを 1 回のリクエストで完結させる POST /run を持つ HTTP サーバー (server.js) を用意します。取得元の指定とコマンドを受け取り、作業ディレクトリの作り直し、取得、コマンド実行を順に行って、終了コードと標準出力・標準エラーを返します。取得が失敗した場合はコマンドを実行せず、その時点の終了コードを返します。取得できていないソースに対して実行しても、結果を判断できないためです。

javascript
const server = http.createServer((req, res) => {
  // /run: 作業ディレクトリを作り直してソースを取得し、コマンドを実行する
  if (req.method === "POST" && req.url === "/run") {
    let body = "";
    req.on("data", (c) => { body += c; });
    req.on("end", async () => {
      const { repository, objectUrl, command } = JSON.parse(body);
      await resetWorkDir();
      // 取得元の指定に応じて、リポジトリまたは Amazon S3 から取得します
      const fetched = repository
        ? await cloneRepository(repository)
        : await fetchObject(objectUrl);
      if (fetched.exitCode !== 0) {
        res.writeHead(200, { "Content-Type": "application/json" });
        res.end(JSON.stringify(fetched));
        return;
      }
      exec(command, { cwd: WORK_DIR, timeout: 600000 }, (error, stdout, stderr) => {
        // タイムアウト時は error.code が "ETIMEDOUT" のような文字列になるため、
        // 数値でない場合も失敗として 1 を返す
        const exitCode = error
          ? (typeof error.code === "number" ? error.code : 1)
          : 0;
        res.writeHead(200, { "Content-Type": "application/json" });
        res.end(JSON.stringify({ exitCode, stdout, stderr }));
      });
    });
    return;
  }
  res.writeHead(200);
  res.end(JSON.stringify({ status: "ok" }));
});
server.listen(8080, () => console.log("listening on 8080"));

package.json に "type": "module" を設定

server.js は ES Modules の import を使うため、package.json に "type": "module" を設定します。

json
{
  "name": "microvm-verify-app",
  "version": "1.0.0",
  "type": "module",
  "private": true
}

ビルド用の IAM ロールを用意する

イメージ作成時、AWS Lambda がビルドのために Amazon S3 からコードを取得し、ログを Amazon CloudWatch Logs に書き込みます。この操作を AWS Lambda に許可する IAM ロールを 1 つ用意します。

json
// trust-policy.json(AWS Lambda がこのロールを引き受けられるようにする)
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": [
        "sts:AssumeRole",
        "sts:TagSession"
      ]
    }
  ]
}

build-policy.json (Amazon S3 の取得と Amazon CloudWatch Logs の書き込みを許可)

json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject"
      ],
      "Resource": "arn:aws:s3:::<your-bucket-name>/*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:*:*:*"
    }
  ]
}

MicroVM のビルド処理用のロールを作成

bash
aws iam create-role --role-name MicrovmBuildRole \
  --assume-role-policy-document file://trust-policy.json
aws iam put-role-policy --role-name MicrovmBuildRole \
  --policy-name BuildPolicy --policy-document file://build-policy.json

イメージを作って起動する

server.js と package.json だけを含む Dockerfile を用意します。リポジトリからのコードベース取得で git を使うため、git のインストールを追加しました。

bash
FROM node:24-alpine
WORKDIR /app
RUN apk add --no-cache git
COPY package.json server.js ./
EXPOSE 8080
CMD ["node", "server.js"]

Amazon S3 にアップロード

zip のルートに Dockerfile が来るように固めて、Amazon S3 にアップロードします。

bash
zip -r app.zip . -x 'node_modules/*' -x '.git/*'
aws s3 cp app.zip s3://<your-bucket-name>/app.zip

イメージを作成

create-microvm-image でイメージを作ります。後続のコマンドに渡す --image-identifier には、レスポンスの imageArn を使います。

bash
aws lambda-microvms create-microvm-image \
  --name microvm-verify \
  --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

実行結果

json
{
    "imageArn": "arn:aws:lambda:us-east-1:<account-id>:microvm-image:microvm-verify",
    "name": "microvm-verify",
    "state": "CREATING",
    "baseImageArn": "arn:aws:lambda:us-east-1:aws:microvm-image:al2023-1",
    "baseImageVersion": "1.0",
    "imageVersion": "1.0"
}

状態を確認

--base-image-arn に指定できるベースイメージは aws lambda-microvms list-managed-microvm-images で確認できます。ビルドは非同期のため、get-microvm-image で状態を確認します。

bash
aws lambda-microvms get-microvm-image --image-identifier <imageArn> --query 'state'

MicroVM を起動できるのは、イメージの状態が CREATED または UPDATED で、かつそのバージョンのビルドが SUCCESSFUL かつ ACTIVE のときです。これらは独立した状態なので、ビルドが失敗した場合は stateReason か Amazon CloudWatch Logs の /aws/lambda/microvms/ を確認します。

Lambda MicroVM を起動

CREATED になったら`run-microvm で起動します。--idle-policy には、一定時間アイドルで自動的にサスペンドし、次のリクエストで自動復帰する設定を指定しました。レスポンスの endpoint が MicroVM ごとの HTTPS エンドポイントです。これを後のリクエストで使います。

bash
aws lambda-microvms run-microvm \
  --image-identifier <imageArn> \
  --idle-policy '{
    "autoResumeEnabled": true,
    "maxIdleDurationSeconds": 900,
    "suspendedDurationSeconds": 300
  }'

実行結果

json
{
    "microvmId": "microvm-<uuid>",
    "state": "PENDING",
    "endpoint": "<endpoint-id>.lambda-microvm.us-east-1.on.aws",
    "imageArn": "arn:aws:lambda:us-east-1:<account-id>:microvm-image:microvm-verify",
    "imageVersion": "1.0",
    "egressNetworkConnectors": [
        "arn:aws:lambda:us-east-1:aws:network-connector:aws-network-connector:INTERNET_EGRESS"
    ]
}

起動状態を確認

起動直後は PENDING なので、get-microvm で RUNNING になるまで確認します。

bash
aws lambda-microvms get-microvm --microvm-identifier <microvmId> --query 'state'

Lambda MicroVM に認証トークンを発行

MicroVM への HTTPS リクエストには、認証トークンが必要です。create-microvm-auth-token で発行しておきましょう。--allowed-ports は "port=8080" の形で指定します。

bash
aws lambda-microvms create-microvm-auth-token \
  --microvm-identifier <microvmId> \
  --expiration-in-minutes 60 \
  --allowed-ports "port=8080"

実行結果

応答の authToken に入っている値を、リクエストの X-aws-proxy-auth ヘッダーに設定します。

bash
{
  "authToken": {
    "X-aws-proxy-auth": "<jwe-token>"
  }
}

動作を確認する

起動した MicroVM に対して、2 通りの取得元をそれぞれ試します。

取得元 1 : リポジトリから clone する

エンドポイントに対して repository でリポジトリ情報、command で実行したいコマンドを渡します。すると MicroVM 上で Git 経由でコードを取得してからコマンドを実行します。

bash
curl -s "https://<endpoint>/run" \
  -H "X-aws-proxy-auth: <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "repository": "https://github.com/octocat/Hello-World.git",
    "command": "ls -la && git log -1"
  }'

実行結果

応答は exitCode が 0 で、stdout には次の内容が入っていました (改行を展開しています)。

bash
total 16
drwxr-xr-x    3 root     root          4096 Aug 31 06:39 .
drwxr-xr-x    4 root     root          4096 Aug 31 06:39 ..
drwxr-xr-x    7 root     root          4096 Aug 31 06:39 .git
-rw-r--r--    1 root     root            13 Aug 31 06:39 README
commit 7fd1a60b01f91b314f59955a4e4d4e80d8edf11d
Merge: 553c207 7629413
Author: The Octocat <octocat@nowhere.com>
Date:   Tue Mar 6 15:06:50 2012 -0800

    Merge pull request #6 from Spaceghost/patch-1
    
    New line at end of file.

取得元 2 : Amazon S3 から取得する

検証用のソースを `tar.gz` に固めて Amazon S3 にアップロードし、署名付き URL を発行します。検証では index.js と package.json を置いたディレクトリを対象にし、有効期限は 600 秒にしています。

bash
tar czf src.tar.gz -C <project-root>/src .
aws s3 cp src.tar.gz s3://<your-bucket-name>/src.tar.gz
aws s3 presign s3://<your-bucket-name>/src.tar.gz --expires-in 600

HTTP リクエストを送信

発行された署名付き URL を objectUrl として渡すと、この経路で取得してからコマンドを実行します。

bash
curl -s "https://<endpoint>/run" \
  -H "X-aws-proxy-auth: <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "objectUrl": "<presignedUrl>",
    "command": "ls -la && cat index.js"
  }'

実行結果

こちらも exitCode は 0 で、配置したファイルの中身が stdout に出力されていました。

bash
total 16
drwxr-xr-x    2 501      dialout       4096 Aug 31 06:40 .
drwxr-xr-x    4 root     root          4096 Aug 31 06:40 ..
-rw-r--r--    1 501      dialout         41 Aug 31 06:40 index.js
-rw-r--r--    1 501      dialout         18 Aug 31 06:40 package.json
console.log("hello from s3 fetch test");

リソースの削除

検証後に料金が発生し続けないよう、作成したリソースを削除します。MicroVM は稼働中に加えてサスペンド中もスナップショットのストレージに対する課金が続き、終了すると課金されなくなります (Running and using MicroVMs を参照)。自動サスペンドを設定しているため、まず MicroVM を終了します。

bash
# MicroVM を終了する
aws lambda-microvms terminate-microvm --microvm-identifier <microvmId>

# MicroVM イメージを削除する
aws lambda-microvms delete-microvm-image --image-identifier <imageArn>

# Amazon S3 のオブジェクトとバケットを削除する(バケットが空でないと削除できません)
aws s3 rm s3://<your-bucket-name>/app.zip
aws s3 rm s3://<your-bucket-name>/src.tar.gz
aws s3 rb s3://<your-bucket-name>

# IAM ロールを削除する(先にインラインポリシーを削除します)
aws iam delete-role-policy --role-name MicrovmBuildRole --policy-name BuildPolicy
aws iam delete-role --role-name MicrovmBuildRole

料金について

料金はリージョンや利用状況によって異なり、また改定される場合があります。最新の料金は AWS Lambda の料金ページ をご確認ください。

まとめ

この記事では、AWS Lambda MicroVMs で環境だけをイメージに固定し、実行のたびにソースを取得して実行する構成を作りました。取得元はリポジトリと Amazon S3 の 2 通りを試し、どちらも取得したソースに対してコマンドを実行できています。作業ディレクトリを取得前に作り直すことで、前回のソースが残らない状態を保っています。

この構成によって、コードが更新され続ける対象を、イメージの作り直しなしに実行できるようになります。

筆者プロフィール

岡本 秀高
CircleCI 合同会社 
Senior Field Engineer

AWS や Cloudflare 上へのサーバーレスなアプリ開発を得意とする開発者。
元 Stripe Developer Advocate / AWS Samurai 2017 など、サービスの使い方や活用 Tips を紹介するコンテンツ作成や登壇などを得意とする。
和太鼓・打楽器プレイヤーのヒカセン。

Missing alt text value