イメージのビルド待ちなしに AWS Lambda MicroVMs を最新のコードで動かしてみた
2026-10-05 | Author : 岡本 秀高 (CircleCI 合同会社)
はじめに
AWS Lambda MicroVMs では、実行環境をコンテナイメージとして用意し、そのイメージから MicroVM を起動して、中でコマンドを実行します。ジョブやバッチの実行環境として使う場合、ランタイムや依存パッケージはこのイメージに含めます。ここで実行対象のコードもイメージに含めると、コードを 1 行修正するたびにイメージを作り直すことになります。
この記事では、この待ち時間を挟まずにジョブを最新のコードで動かす構成の作り方を紹介します。イメージにはランタイムや依存パッケージだけを固定し、更新され続けるソースは実行のたびに取得します。
ご注意
本記事で紹介する AWS サービスを起動する際には、料金がかかります。builders.flash メールメンバー特典の、クラウドレシピ向けクレジットコードプレゼントの入手をお勧めします。
builders.flash メールメンバー登録
builders.flash メールメンバー登録で、毎月の最新アップデート情報とともに、AWS を無料でお試しいただけるクレジットコードを受け取ることができます。
利用するサービス
この記事では AWS Lambda MicroVMs、Amazon S3、AWS Identity and Access Management (IAM) を利用します。AWS Lambda MicroVMs は、Dockerfile から作ったイメージのスナップショットを取り、そのスナップショットから仮想マシンレベルの分離環境を起動するコンピューティング基盤です。
検証用アプリを用意する
実行のたびにコードベースを取得する構成では、前回のソースを残さないことが重要になります。展開や取得だけを繰り返すと、前回まで存在して今回削除されたファイルが作業ディレクトリに残り、最新のソースで実行したことになりません。そこで取得の前に作業ディレクトリを作り直します。
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 します。
// リポジトリから 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 の標準入力へ流し込みます。
// 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) を用意します。取得元の指定とコマンドを受け取り、作業ディレクトリの作り直し、取得、コマンド実行を順に行って、終了コードと標準出力・標準エラーを返します。取得が失敗した場合はコマンドを実行せず、その時点の終了コードを返します。取得できていないソースに対して実行しても、結果を判断できないためです。
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" を設定します。
{
"name": "microvm-verify-app",
"version": "1.0.0",
"type": "module",
"private": true
}
ビルド用の IAM ロールを用意する
イメージ作成時、AWS Lambda がビルドのために Amazon S3 からコードを取得し、ログを Amazon CloudWatch Logs に書き込みます。この操作を AWS Lambda に許可する IAM ロールを 1 つ用意します。
// 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 の書き込みを許可)
{
"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 のビルド処理用のロールを作成
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 のインストールを追加しました。
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 にアップロードします。
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 を使います。
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
実行結果
{
"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 で状態を確認します。
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 エンドポイントです。これを後のリクエストで使います。
aws lambda-microvms run-microvm \
--image-identifier <imageArn> \
--idle-policy '{
"autoResumeEnabled": true,
"maxIdleDurationSeconds": 900,
"suspendedDurationSeconds": 300
}'
実行結果
{
"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 になるまで確認します。
aws lambda-microvms get-microvm --microvm-identifier <microvmId> --query 'state'
Lambda MicroVM に認証トークンを発行
MicroVM への HTTPS リクエストには、認証トークンが必要です。create-microvm-auth-token で発行しておきましょう。--allowed-ports は "port=8080" の形で指定します。
aws lambda-microvms create-microvm-auth-token \
--microvm-identifier <microvmId> \
--expiration-in-minutes 60 \
--allowed-ports "port=8080"
実行結果
応答の authToken に入っている値を、リクエストの X-aws-proxy-auth ヘッダーに設定します。
{
"authToken": {
"X-aws-proxy-auth": "<jwe-token>"
}
}
動作を確認する
起動した MicroVM に対して、2 通りの取得元をそれぞれ試します。
取得元 1 : リポジトリから clone する
エンドポイントに対して repository でリポジトリ情報、command で実行したいコマンドを渡します。すると MicroVM 上で Git 経由でコードを取得してからコマンドを実行します。
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 には次の内容が入っていました (改行を展開しています)。
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 秒にしています。
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 として渡すと、この経路で取得してからコマンドを実行します。
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 に出力されていました。
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 を終了します。
# 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 を紹介するコンテンツ作成や登壇などを得意とする。
和太鼓・打楽器プレイヤーのヒカセン。