AWS Lambda MicroVMsでスナップショット由来の乱数・UUID重複を検証してみた

AWS Lambda MicroVMsでスナップショット由来の乱数・UUID重複を検証してみた

Lambda MicroVMsは同一スナップショットから複数のVMを起動するため、スナップショット作成時に保持した乱数やUUIDがVM間で重複する可能性があります。Pythonアプリで、重複する生成パターンとVMごとに異なる値を得られる生成パターンを検証しました
2026.06.24

はじめに

2026年6月22日、AWS Lambda MicroVMsが一般提供開始されました。

https://aws.amazon.com/jp/about-aws/whats-new/2026/06/aws-lambda-microvms/

Lambda MicroVMsはDockerfileから作成したスナップショットを複数のVMで共有して高速起動するサービスです。このスナップショットベースの起動方式には、Lambda SnapStartで知られた「一意性問題」が存在します。スナップショット作成時にメモリへ書き込まれた値は同一スナップショットから起動した全VMで同一になるため、乱数やUUIDの衝突リスクがあります。

MicroVMsは同一スナップショットから多数のVMを起動するため、VM間で擬似乱数の衝突が起こりえます。さらに最大8時間の長寿命でサスペンド/レジュームを繰り返すため、誤った生成方式の影響が長時間持続します。今回はPythonアプリを使い、危険な方式と安全な方式を実際に確認しました。

検証環境・アプリ構成

アプリの概要

検証用アプリのコードです。スナップショット作成時(グローバルスコープ実行時)に各種値を生成して保持し、/run フックでper-VM初期化を行います。

import random
import uuid
import time
import secrets
import hashlib
import json
from flask import Flask, jsonify, request

# === Snapshot-time values (baked into snapshot) ===
BUILD_RANDOM = random.random()
BUILD_UUID = str(uuid.uuid4())
BUILD_TIME = time.time()
BUILD_RANDOM_STATE_HASH = hashlib.md5(str(random.getstate()).encode()).hexdigest()[:16]

# === Runtime values (set by /run hook) ===
RUN_SECRET = None
RUN_UUID = None
RUN_RANDOM_STATE_HASH = None
RUN_HOOK_PAYLOAD = None

app = Flask(__name__)

@app.route("/uniqueness")
def uniqueness():
    return jsonify({
        "build_time": {
            "random": BUILD_RANDOM,
            "uuid": BUILD_UUID,
            "time": BUILD_TIME,
            "random_state_hash": BUILD_RANDOM_STATE_HASH,
        },
        "run_time": {
            "secret": RUN_SECRET,
            "uuid": RUN_UUID,
            "random_state_hash": RUN_RANDOM_STATE_HASH,
            "hook_payload": RUN_HOOK_PAYLOAD,
        },
    })

@app.route("/generate")
def generate():
    return jsonify({
        "secrets_token": secrets.token_hex(16),
        "random_random": random.random(),
        "uuid4": str(uuid.uuid4()),
    })

@app.route("/aws/lambda-microvms/runtime/v1/ready", methods=["GET", "POST"])
def ready_hook():
    return jsonify({"status": "ready"})

@app.route("/aws/lambda-microvms/runtime/v1/run", methods=["GET", "POST"])
def run_hook():
    global RUN_SECRET, RUN_UUID, RUN_RANDOM_STATE_HASH, RUN_HOOK_PAYLOAD
    RUN_SECRET = secrets.token_hex(16)
    RUN_UUID = str(uuid.uuid4())
    RUN_RANDOM_STATE_HASH = hashlib.md5(str(random.getstate()).encode()).hexdigest()[:16]
    try:
        RUN_HOOK_PAYLOAD = request.get_json(force=True)
    except Exception:
        RUN_HOOK_PAYLOAD = None
    return jsonify({"status": "initialized"})

Dockerfile

FROM public.ecr.aws/lambda/microvms:al2023-minimal
RUN dnf install -y python3.11 python3.11-pip && dnf clean all && \
    ln -sf /usr/bin/python3.11 /usr/bin/python3 && \
    ln -sf /usr/bin/pip3.11 /usr/bin/pip3
WORKDIR /opt/app
COPY requirements.txt .
RUN pip3 install --no-cache-dir -r requirements.txt
COPY app.py .
EXPOSE 8080 9000
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "--bind", "0.0.0.0:9000", "--workers", "1", "app:app"]

検証方法

同一イメージからMicroVMを3台起動し、各VMの /uniqueness/generate エンドポイントを叩いて値を比較しました。さらに1台をサスペンド→レジュームして値の変化を確認しました。

検証結果1: スナップショット作成時の生成値は全VMで同一になる

3台の /uniqueness レスポンスを比較した結果です。

項目 VM-1 VM-2 VM-3 一致
random.random() 0.3995975096870906 0.3995975096870906 0.3995975096870906 全同一
uuid.uuid4() 4f12ad9f-4985-44de-... 4f12ad9f-4985-44de-... 4f12ad9f-4985-44de-... 全同一
time.time() 1782232870.950651 1782232870.950651 1782232870.950651 全同一
random 状態ハッシュ 78bed98a6cf5fbac 78bed98a6cf5fbac 78bed98a6cf5fbac 全同一

アプリケーションの起動時(グローバルスコープ)で生成した値は、すべてスナップショットに含まれ同一スナップショットから起動した全VMで完全一致しました。重要なのは、uuid.uuid4() 自体が問題なのではなく、スナップショット作成時に一度だけ生成して変数に保持した値がスナップショットに焼き込まれる点です。リクエスト時に呼ぶ uuid.uuid4() は一意識別子用途には適しています。

検証結果2: ランタイム生成値の安全性

/run フックで生成した値

項目 VM-1 VM-2 VM-3 一致
secrets.token_hex() 30e9a8fb... 56d12747... d10758f4... 全異なる ✅
uuid.uuid4() 7adcdead-... b5647d99-... 461ccc8e-... 全異なる ✅
random 状態ハッシュ 78bed98a6cf5fbac 78bed98a6cf5fbac 78bed98a6cf5fbac 全同一 ❌

secretsuuid.uuid4()os.urandom() 経由)は /run フック時点でVMごとに異なる値を生成しました。一方、random モジュールの内部状態(Mersenne Twister)はスナップショット由来のまま全VMで同一です。random状態ハッシュが全VMで同一なのは、/run フック内で random.random() を呼んでいないためです。random の内部状態はスナップショット由来のまま変わらず、/run フックで secrets を初期化しても解消されません。

/generate でリクエスト時に生成した値(1回目)

項目 VM-1 VM-2 VM-3 判定
secrets.token_hex() e80f01cf... fe202571... 0a23139f... 安全 ✅
uuid.uuid4() 6613ae41-... 954cdcdc-... cac1817c-... 安全 ✅
random.random() 0.2806583363131997 0.2806583363131997 0.2806583363131997 危険 ❌

2回目の /generate 呼び出しでは random.random() は全VMで 0.19694213223565982 という別の同一値を返しました。値自体は1回目と異なりますが、VM間では一致しています。Mersenne Twisterの内部状態がスナップショット時点から全VMで同じ軌道を辿るためです。

検証結果3: サスペンド→レジュームでの挙動

VM-1をサスペンド→レジュームし、/generate を繰り返し呼んだ結果です。

呼び出し random.random() secrets.token_hex()
3回目(suspend前) 0.0738771467350261 c223192a...
4回目(1回目resume後) 0.3744877383767621 85281b9f...
5回目(1回目resume後) 0.9874792268134623 2afee1a4...
6回目(2回目resume後) 0.7756036035370809 479b8c76...

secrets.token_hex() は本検証ではレジューム後も毎回異なる値を返しました。SnapStartの一意性ガイダンスはレジューム時に乱数プールがリシードされると説明しており、MicroVMsでも同様の仕組みが働いていると考えられます。

random.random() は「スナップショット時点に巻き戻って同じシーケンスを繰り返す」という仮説を立てていましたが、実際にはメモリ上の現在状態から継続しました。ただし、これは同一VM内の話です。異なるVM間では、各VMがスナップショット時点と同じMersenne Twister状態から起動します。各VMが同じ回数・同じ順序で呼べば同じ値が返ります(途中で random を消費する処理が挟まると軌道がずれます)。

検証結果4: runHookPayload による per-VM データ注入

RunMicrovm APIの --run-hook-payload パラメータでVMごとに固有のJSONを渡せます。

aws lambda run-microvm \
  --microvm-image-arn arn:aws:lambda:ap-northeast-1:123456789012:microvm-image:snapshot-uniqueness-test \
  --run-hook-payload '{"vm_id":"vm-1","tenant":"tenant-1"}'

/run フックのリクエストボディには以下の形式で届きます。

{
  "microvmId": "microvm-c54eb9de-edfd-3e27-89d9-133fd8ca9913",
  "runHookPayload": "{\"vm_id\":\"vm-1\",\"tenant\":\"tenant-1\"}"
}

今回のようにJSON文字列を runHookPayload に渡した場合、外側のリクエストボディをパース後に runHookPayload の文字列を json.loads() する必要があります。microvmId はシステムが付与するVMインスタンス識別子です。本検証では3台それぞれ異なる microvmId が届きました。アプリ側で明示的にIDを渡さなくてもVMを自己識別できます。

注意事項

フック API のパス

開発者ガイドのフック設定では "path": "/run" のように相対パスを指定します。しかし本検証環境では、アプリが実際に受けたリクエストのパスは /aws/lambda-microvms/runtime/v1/run でした。/ready フックも同様に /aws/lambda-microvms/runtime/v1/ready に届きました。本構成ではこのパスに対するルーティングを用意する必要がありました。

gunicorn のバージョン

gunicorn 26.0.0では --bind を複数指定した場合にready hookがタイムアウトし、イメージ作成が CREATE_FAILED になりました。gunicorn 23.0.0にピン留めすることで解決しました。

まとめ

Lambda MicroVMsのスナップショット一意性問題を実験で確認しました。スナップショット作成時にメモリ上へ保持された値は、同一スナップショットから起動したVM間で同一になります。randomモジュールのような擬似乱数生成器は内部状態ごとスナップショットに含まれるため、同じ順序・同じ回数で呼び出すと異なるVMでも同一のシーケンスを出力します。

一方、カーネルのCSPRNG経由で値を得るAPI(secretsos.urandom、リクエスト時のuuid.uuid4())では、今回の検証範囲で同一値の再利用は観測されませんでした。セキュリティトークンにはsecrets、一意識別子にはリクエスト時のuuid.uuid4()を使い、per-VMの論理IDが必要な場合はrunHookPayloadで明示的に渡すのが適しています。

参考リンク

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事