Lambda Managed Instances を検証 — プロビジョニング時間・マルチコンカレンシー・スケーリングの実態
目次
はじめに
2025年11月30日、AWS は Lambda Managed Instances (LMI) を発表した。Lambda のプログラミングモデルを維持しながら、EC2 インスタンス上で関数を実行できる新しいコンピュートオプションである。
LMI の本質は「即座にスケールする Lambda」ではなく「Lambda の開発体験を持った Fargate」に近い。コールドスタートを排除する代わりにデプロイ時にプロビジョニング待ちが発生し、スケーリングは CPU 使用率ベースで非同期に行われる。このメンタルモデルの転換が、LMI を正しく活用するための出発点になる。
本記事では LMI を実際にデプロイし、3つの核心的な挙動を計測する。結果をもとに「自分のワークロードは LMI に移行すべきか」を判断するためのチェックリストを提示する。公式ドキュメントは Lambda Managed Instances、構成手順の詳細は AWS Compute Blog の解説記事を参照。
標準 Lambda との主な違い
| 観点 | 標準 Lambda | Lambda Managed Instances |
|---|---|---|
| コンカレンシー | 1実行環境 = 1リクエスト | 1実行環境 = N リクエスト(マルチコンカレンシー) |
| スケーリング | リクエスト駆動(即時) | CPU 使用率ベース(非同期) |
| コールドスタート | あり | なし(publish-version 時にプロビジョニング) |
| 料金 | リクエスト + Duration 課金 | EC2 インスタンス課金 + 15% 管理手数料 |
| VPC | オプション | 必須(Capacity Provider で指定) |
| 最小メモリ | 128 MB | 2 GB |
前提条件:
- AWS CLI 設定済み(Lambda, EC2, IAM の権限)
- 検証リージョン: us-east-1
セットアップだけ見たい場合は検証環境のセットアップ、結果だけ見たい場合は検証 1に進んでほしい。
検証環境のセットアップ
IAM ロール・VPC・Capacity Provider の作成手順
LMI には2つの IAM ロールが必要である。Lambda 実行ロールと、Lambda が EC2 インスタンスを管理するための Capacity Provider オペレーターロールだ。
# Lambda 実行ロール
cat > lambda-trust-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Service": "lambda.amazonaws.com" },
"Action": "sts:AssumeRole"
}]
}
EOF
aws iam create-role \
--role-name LMI-Verification-ExecutionRole \
--assume-role-policy-document file://lambda-trust-policy.json
aws iam attach-role-policy \
--role-name LMI-Verification-ExecutionRole \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
# Capacity Provider オペレーターロール
aws iam create-role \
--role-name LMI-Verification-OperatorRole \
--assume-role-policy-document file://lambda-trust-policy.json
aws iam attach-role-policy \
--role-name LMI-Verification-OperatorRole \
--policy-arn arn:aws:iam::aws:policy/AWSLambdaManagedEC2ResourceOperatorLMI は VPC 必須で、最低3つの AZ にサブネットを配置する必要がある。CloudWatch Logs 送信のために NAT Gateway も必要だ。
REGION=us-east-1
# VPC
VPC_ID=$(aws ec2 create-vpc --cidr-block 10.0.0.0/16 \
--query 'Vpc.VpcId' --output text --region $REGION)
# プライベートサブネット(3 AZ)
PRIV_SUB1=$(aws ec2 create-subnet --vpc-id $VPC_ID \
--cidr-block 10.0.1.0/24 --availability-zone us-east-1a \
--query 'Subnet.SubnetId' --output text --region $REGION)
PRIV_SUB2=$(aws ec2 create-subnet --vpc-id $VPC_ID \
--cidr-block 10.0.2.0/24 --availability-zone us-east-1b \
--query 'Subnet.SubnetId' --output text --region $REGION)
PRIV_SUB3=$(aws ec2 create-subnet --vpc-id $VPC_ID \
--cidr-block 10.0.3.0/24 --availability-zone us-east-1c \
--query 'Subnet.SubnetId' --output text --region $REGION)
# パブリックサブネット + IGW + NAT Gateway
PUB_SUB=$(aws ec2 create-subnet --vpc-id $VPC_ID \
--cidr-block 10.0.100.0/24 --availability-zone us-east-1a \
--query 'Subnet.SubnetId' --output text --region $REGION)
IGW_ID=$(aws ec2 create-internet-gateway \
--query 'InternetGateway.InternetGatewayId' --output text --region $REGION)
aws ec2 attach-internet-gateway \
--internet-gateway-id $IGW_ID --vpc-id $VPC_ID --region $REGION
PUB_RT=$(aws ec2 create-route-table --vpc-id $VPC_ID \
--query 'RouteTable.RouteTableId' --output text --region $REGION)
aws ec2 create-route --route-table-id $PUB_RT \
--destination-cidr-block 0.0.0.0/0 --gateway-id $IGW_ID --region $REGION
aws ec2 associate-route-table \
--route-table-id $PUB_RT --subnet-id $PUB_SUB --region $REGION
EIP_ALLOC=$(aws ec2 allocate-address --domain vpc \
--query 'AllocationId' --output text --region $REGION)
NAT_GW=$(aws ec2 create-nat-gateway --subnet-id $PUB_SUB \
--allocation-id $EIP_ALLOC \
--query 'NatGateway.NatGatewayId' --output text --region $REGION)
aws ec2 wait nat-gateway-available --nat-gateway-ids $NAT_GW --region $REGION
PRIV_RT=$(aws ec2 create-route-table --vpc-id $VPC_ID \
--query 'RouteTable.RouteTableId' --output text --region $REGION)
aws ec2 create-route --route-table-id $PRIV_RT \
--destination-cidr-block 0.0.0.0/0 --nat-gateway-id $NAT_GW --region $REGION
for SUB in $PRIV_SUB1 $PRIV_SUB2 $PRIV_SUB3; do
aws ec2 associate-route-table \
--route-table-id $PRIV_RT --subnet-id $SUB --region $REGION
done
# セキュリティグループ
SG_ID=$(aws ec2 create-security-group --group-name lmi-verification-sg \
--description "Security group for LMI verification" \
--vpc-id $VPC_ID --query 'GroupId' --output text --region $REGION)ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
aws lambda create-capacity-provider \
--capacity-provider-name lmi-verification-cp \
--vpc-config "SubnetIds=$PRIV_SUB1,$PRIV_SUB2,$PRIV_SUB3,SecurityGroupIds=$SG_ID" \
--permissions-config "CapacityProviderOperatorRoleArn=arn:aws:iam::${ACCOUNT_ID}:role/LMI-Verification-OperatorRole" \
--instance-requirements "Architectures=x86_64" \
--capacity-provider-scaling-config "MaxVCpuCount=30" \
--region $REGION検証用の Lambda 関数を作成する。mode パラメータで info(基本情報)、io_bound(I/O シミュレート)、cpu_bound(CPU 負荷)を切り替えられる。
import json
import os
import time
import threading
import math
def lambda_handler(event, context):
mode = event.get("mode", "info")
result = {
"request_id": context.aws_request_id,
"function_version": context.function_version,
"pid": os.getpid(),
"thread_id": threading.current_thread().ident,
"timestamp": time.time(),
}
if mode == "io_bound":
sleep_sec = event.get("sleep_sec", 2)
time.sleep(sleep_sec)
result["mode"] = "io_bound"
result["sleep_sec"] = sleep_sec
elif mode == "cpu_bound":
iterations = event.get("iterations", 5_000_000)
start = time.time()
total = 0.0
for i in range(iterations):
total += math.sqrt(i) * math.sin(i)
result["mode"] = "cpu_bound"
result["compute_time_sec"] = round(time.time() - start, 3)
else:
result["mode"] = "info"
return {"statusCode": 200, "body": json.dumps(result)}# 関数コードを ZIP 化
mkdir -p /tmp/lmi-func && cp lambda_function.py /tmp/lmi-func/
cd /tmp/lmi-func && zip -j /tmp/lmi-function.zip lambda_function.py
CP_ARN="arn:aws:lambda:${REGION}:${ACCOUNT_ID}:capacity-provider:lmi-verification-cp"
aws lambda create-function \
--function-name lmi-verification-func \
--runtime python3.13 \
--role "arn:aws:iam::${ACCOUNT_ID}:role/LMI-Verification-ExecutionRole" \
--handler lambda_function.lambda_handler \
--zip-file fileb:///tmp/lmi-function.zip \
--memory-size 4096 \
--timeout 60 \
--capacity-provider-config "{
\"LambdaManagedInstancesCapacityProviderConfig\": {
\"CapacityProviderArn\": \"$CP_ARN\",
\"ExecutionEnvironmentMemoryGiBPerVCpu\": 4.0,
\"PerExecutionEnvironmentMaxConcurrency\": 10
}
}" \
--region $REGION
# 関数が Active になるのを待つ
aws lambda wait function-active-v2 --function-name lmi-verification-func --region $REGION
# バージョンを publish(EC2 プロビジョニングが開始される)
aws lambda publish-version --function-name lmi-verification-func --region $REGION今回の検証で使用した構成:
| 項目 | 値 |
|---|---|
| リージョン | us-east-1 |
| アーキテクチャ | x86_64 |
| MaxVCpuCount | 30 |
| メモリ | 4096 MB |
| メモリ/vCPU 比率 | 4:1(汎用) |
| PerExecutionEnvironmentMaxConcurrency | 10(Python デフォルトは 16/vCPU。上限到達時の挙動を観察しやすくするため低めに設定) |
| インスタンスタイプ | Lambda に委任(結果: m7i.xlarge が選択された) |
検証 1: Capacity Provider の起動は何分かかるか
標準 Lambda はデプロイ後すぐに呼び出せるが、LMI は publish-version 時に EC2 インスタンスをプロビジョニングする。このリードタイムがどの程度かを計測した。
計測結果
| フェーズ | 所要時間 |
|---|---|
| Capacity Provider 作成 API → Active | 即時(数秒) |
publish-version API 応答 | 約1.5秒 |
publish-version → バージョン Active | 約67秒 |
| 初回 invoke レイテンシ | 約1.2秒(コールドスタートなし) |
publish-version を実行すると、Lambda は3つの AZ に m7i.xlarge インスタンスを1台ずつ起動する。
aws ec2 describe-instances \
--filters "Name=tag:aws:lambda:capacity-provider,Values=*" \
--query 'Reservations[*].Instances[*].[InstanceType,Placement.AvailabilityZone]' \
--output table --region us-east-1| m7i.xlarge | us-east-1c |
| m7i.xlarge | us-east-1b |
| m7i.xlarge | us-east-1a |インスタンスタイプを指定しなかった場合、4GB メモリ / 4:1 比率の設定に対して Lambda は m7i.xlarge(4 vCPU / 16 GB)を自動選択した。
初回 invoke の確認
バージョンが Active になった後の invoke では、コールドスタートは発生しなかった。5回の invoke で安定して約1.2秒(ネットワーク往復含む)。info モードの関数実行自体はほぼ 0ms なので、大部分はローカル環境(東京)から us-east-1 への往復レイテンシである。
invoke #1: 1248ms | pid=18
invoke #2: 1288ms | pid=16
invoke #3: 1192ms | pid=22
invoke #4: 1178ms | pid=20
invoke #5: 1204ms | pid=15PID がすべて異なる点に注目してほしい。5回の逐次 invoke が5つの異なる実行環境にルーティングされている。m7i.xlarge(4 vCPU / 16 GB)が3台起動しており、関数は 4 GB メモリ / 1 vCPU の設定なので、1台あたり複数の実行環境を収容できる。実際に MinExecutionEnvironments=3 より多くの実行環境が配置されていた(後述)。
所感
67秒のプロビジョニング時間は、CI/CD パイプラインに組み込む場合に考慮が必要である。 標準 Lambda の「デプロイ即呼び出し可能」とは根本的に異なる。ただし、一度プロビジョニングが完了すれば、以降はコールドスタートなしで安定したレイテンシが得られる。頻繁にデプロイする関数よりも、長期間安定稼働する関数に向いている。
検証 2: 1つの実行環境で何リクエスト捌けるか
LMI の最大の差別化ポイントはマルチコンカレンシーである。標準 Lambda は1実行環境=1リクエストだが、LMI は1つの実行環境で複数リクエストを同時処理できる。I/O バウンドワークロード(3秒 sleep でシミュレート)で検証した。
なお、Python ランタイムではマルチコンカレンシーがスレッドではなくプロセスベースで実現される(Node.js はイベントループ、Java/.NET はスレッド)。そのため、スレッドセーフティよりもプロセス間の状態共有や /tmp へのファイルロックに注意が必要である。プロセスベースであるため、各実行環境は独立した PID を持つ。以降の検証では PID の分布を見ることで、リクエストがどの実行環境に振り分けられたかを確認する。
10 並列リクエスト
並列 invoke の実行コマンド
# N 並列で invoke を実行し、PID 分布を集計するスクリプト
N=10
for i in $(seq 1 $N); do
aws lambda invoke \
--function-name lmi-verification-func --qualifier 1 \
--payload '{"mode":"io_bound","sleep_sec":3}' \
--cli-binary-format raw-in-base64-out \
--cli-read-timeout 30 \
/tmp/lmi-result-${i}.json \
--region us-east-1 > /dev/null 2>&1 &
done
wait
# PID 分布を確認
for i in $(seq 1 $N); do
python3 -c "
import json
d=json.load(open('/tmp/lmi-result-${i}.json'))
b=json.loads(d['body'])
print(b['pid'])" 2>/dev/null
done | sort | uniq -c | sort -rnN の値を 10, 30, 40 に変更して各テストを実行した。CPU バウンドテストでは payload を '{"mode":"cpu_bound","iterations":10000000}' に変更する。
全リクエスト完了: 4551ms(3秒 sleep + ネットワーク往復)
PID 分布(同一 PID = 同一実行環境):
3 件 → PID 17
2 件 → PID 16
1 件 → PID 24, 21, 19, 18, 15PID 17 が3リクエストを同時処理している。マルチコンカレンシーが機能していることを確認できた。全体の完了時間が約4.5秒(3秒 sleep + 約1.5秒のオーバーヘッド)であり、10リクエストが並列に処理されている。
30 並列リクエスト(理論上限付近)
MinExecutionEnvironments=3、PerExecutionEnvironmentMaxConcurrency=10 なので、最小構成での同時処理数は 3 × 10 = 30 と予想した。
完了: 5899ms | 成功: 30/30 | スロットリング: 0
PID 分布:
3 件ずつ → PID 15, 16, 17, 18, 19, 20, 21, 22, 23, 24(計10 PID)30 並列で全て成功、スロットリングなし。 PID が 10 個確認された。これは3台の m7i.xlarge(各 4 vCPU / 16 GB)に合計10の実行環境(各 4 GB / 1 vCPU)が配置された結果である。MinExecutionEnvironments=3 を超える実行環境が自動的に配置されており、各実行環境に3リクエストずつ均等に分散されている。
40 並列リクエスト(上限超過テスト)
完了: 9220ms | 成功: 34/40 | スロットリング: 6
PID 分布: 10 PID(15〜24)、各 3-4 件40 並列で 6 件のスロットリングが発生した。完了時間も 9.2 秒に伸びている。10 実行環境 × MaxConcurrency 10 = 100 が環境レベルの同時処理上限だが、40 並列の時点で一部スロットリングが発生した。原因は特定できていないが、リクエストのルーティングやキューイングのオーバーヘッドが影響している可能性がある。
所感
マルチコンカレンシーは I/O バウンドワークロードで効果的に機能する。 3秒の sleep を含む処理が、10 並列でも約4.5秒で完了する。標準 Lambda なら10の実行環境が必要な処理を、より少ないリソースで処理できる。
一方で、コストの観点では注意が必要である。MinExecutionEnvironments=3 でも、実際には10の実行環境が配置されていたように、実行環境数は MinExecutionEnvironments だけでは決まらない。コスト見積もりは実行環境数ではなく、起動する EC2 インスタンスの台数とタイプで考える必要がある。
検証 3: CPU 負荷でスケールアウトはどれだけ速いか
LMI のスケーリングは CPU 使用率ベースで非同期に行われる。スケーリングには2つのレイヤーがある。既存インスタンス上への実行環境の追加と、インスタンスのリソースが不足した場合の EC2 インスタンスの追加である。標準 Lambda のリクエスト駆動スケーリング(即時)とどう違うかを、CPU バウンドワークロードで検証した。
CPU バウンド関数の特性
数値計算(sqrt + sin を 1000 万回)で約1秒の CPU 処理を行う関数を使用した。
50 並列 × 3 バッチ(15秒間隔)
まず、現在の構成(MaxExecutionEnvironments 未設定)のまま高負荷をかけ、実行環境やインスタンスの追加が自動的に発生するかを観察した。
バッチ 1: 18543ms | 成功: 33/50 | スロットリング: 17 | ユニーク PID: 10
バッチ 2: 15212ms | 成功: 20/50 | スロットリング: 30 | ユニーク PID: 10
バッチ 3: 19075ms | 成功: 18/50 | スロットリング: 32 | ユニーク PID: 10CPU バウンドでは大量のスロットリングが発生し、30秒の間隔を挟んでも実行環境の増加は観察されなかった。 PID 数は 10 のまま変化していない。CloudTrail の RunInstances イベントを確認したところ、CPU 負荷テスト中に EC2 インスタンスの追加も発生していなかった。検証中は初期の3台のまま推移した。
MaxExecutionEnvironments を明示的に 20 に設定し、実行環境の増加余地を与えた上で再テストしたところ、PID 25 が新たに出現(11 ユニーク PID)。実行環境の追加は始まったが、即座ではなかった。
aws lambda put-function-scaling-config \
--function-name lmi-verification-func --qualifier 1 \
--function-scaling-config "MinExecutionEnvironments=3,MaxExecutionEnvironments=20" \
--region us-east-130 並列 × 5 バッチ(15秒間隔)での安定性
50 並列ではスロットリングが多発したため、現在の実行環境数(10)で安定処理できる水準まで並列数を下げ、継続的な負荷での挙動を確認した。
バッチ 1: 18102ms | 成功: 29/30 | スロットリング: 1
バッチ 2: 18838ms | 成功: 30/30 | スロットリング: 0
バッチ 3: 17905ms | 成功: 30/30 | スロットリング: 0
バッチ 4: 15101ms | 成功: 30/30 | スロットリング: 0
バッチ 5: 12235ms | 成功: 30/30 | スロットリング: 010 の実行環境で 30 並列を安定処理でき、バッチごとに処理時間が短縮(18秒→12秒)している。短縮の正確な原因は不明だが、実行環境内の初回コスト(プロセス初期化やモジュールインポートなど)が償却された可能性がある。
CloudWatch メトリクス
15:57 51 件
15:58 186 件(CPU バウンド 50 並列テスト時のピーク)
15:59 96 件
16:00 3 件
16:01 0 件所感
CPU バウンドワークロードでは、LMI のスケーリングは「遅い」と感じる場面がある。 ドキュメントにも「トラフィックが5分以内に2倍以上になるとスロットリングが発生する可能性がある」と記載されているが、実際に体験すると、標準 Lambda のリクエスト駆動スケーリングとの差は大きい。
CPU バウンドの場合、マルチコンカレンシーの恩恵も限定的である。同一環境内で CPU リソースを共有するため、並列数が増えると個々のリクエストの処理時間が伸びる可能性がある。公式ブログでも CPU バウンドワークロードではコンカレンシーを vCPU 数以下に設定することが推奨されている。
検証結果から見える LMI の適性
パフォーマンス比較
| 観点 | 標準 Lambda | LMI(今回の計測値) |
|---|---|---|
| デプロイ→呼び出し可能 | 即時 | 約67秒 |
| コールドスタート | あり(数百ms〜数秒) | なし |
| I/O バウンド 10 並列 | 10 実行環境必要 | 4.5秒で完了(マルチコンカレンシー) |
| CPU バウンド 50 並列 | 50 実行環境で即時スケール | スロットリング発生(非同期スケール) |
| スケーリング速度 | リクエスト単位で即時 | CPU ベースで非同期(実行環境・インスタンスの2段階) |
コスト構造の違い
LMI は EC2 インスタンス課金 + 15% 管理手数料。今回の検証では m7i.xlarge($0.2016/時間 × 3台)が起動した。
- LMI の最低コスト: 3台 × 0.70/時間**(最低3台の EC2 インスタンスが常時稼働)
- 標準 Lambda: トラフィックがなければ $0
LMI はアイドル時もコストが発生する。高稼働率のワークロードでは EC2 Savings Plans / Reserved Instances の適用で標準 Lambda より安くなる可能性があるが、低稼働率では標準 Lambda が圧倒的に有利である。
まとめ — LMI 移行判断チェックリスト
| チェック項目 | 標準 Lambda 向き | LMI 向き |
|---|---|---|
| トラフィックパターン | バースト・スパイク型 | 定常的・予測可能 |
| コールドスタート許容度 | 許容できる | 許容できない |
| 実行時間 | 短時間・イベント駆動 | 長時間・定常処理 |
| ワークロード特性 | CPU バウンド(スケーリングが課題) | I/O バウンド(マルチコンカレンシーの恩恵大) |
| 稼働率 | 低稼働(リクエスト課金が有利) | 高稼働(EC2 課金が有利) |
| VPC 要件 | VPC 不要 | すでに VPC 内で運用 |
| デプロイ頻度 | 高頻度(即時デプロイが必要) | 低頻度(67秒の待ちを許容) |
- 「運用不要な EC2 クラスタ」がフィットするか — LMI は Lambda の開発体験を維持しつつ EC2 のコンピュート柔軟性を提供するが、スケーリングモデルは EC2 に近い。トラフィックの急増に即座に対応する必要があるなら、標準 Lambda の方が適している
- マルチコンカレンシーは I/O バウンドで真価を発揮する — 同一実行環境で複数リクエストを並列処理できるため、API コールや DB クエリの待ち時間が多いワークロードでリソース効率が大幅に向上する。CPU バウンドでは恩恵が限定的
- コスト最適化には稼働率の見極めが必要 — 最低3台の EC2 インスタンスが常時稼働するため、低トラフィック時のコスト効率は標準 Lambda に劣る。Savings Plans の適用を前提とした高稼働ワークロードで検討すべき
クリーンアップ
リソース削除コマンド
REGION=us-east-1
# Lambda 関数とバージョンの削除
aws lambda delete-function --function-name lmi-verification-func --region $REGION
# Capacity Provider の削除(EC2 インスタンスも自動終了)
aws lambda delete-capacity-provider \
--capacity-provider-name lmi-verification-cp --region $REGION
# NAT Gateway の削除(時間がかかる)
aws ec2 delete-nat-gateway --nat-gateway-id $NAT_GW --region $REGION
aws ec2 wait nat-gateway-deleted --nat-gateway-ids $NAT_GW --region $REGION
aws ec2 release-address --allocation-id $EIP_ALLOC --region $REGION
# ルートテーブル・サブネット・IGW・VPC の削除
# (関連付けを解除してから削除)
aws ec2 delete-route-table --route-table-id $PRIV_RT --region $REGION
aws ec2 delete-route-table --route-table-id $PUB_RT --region $REGION
for SUB in $PRIV_SUB1 $PRIV_SUB2 $PRIV_SUB3 $PUB_SUB; do
aws ec2 delete-subnet --subnet-id $SUB --region $REGION
done
aws ec2 detach-internet-gateway \
--internet-gateway-id $IGW_ID --vpc-id $VPC_ID --region $REGION
aws ec2 delete-internet-gateway --internet-gateway-id $IGW_ID --region $REGION
aws ec2 delete-security-group --group-id $SG_ID --region $REGION
aws ec2 delete-vpc --vpc-id $VPC_ID --region $REGION
# IAM ロールの削除
aws iam detach-role-policy --role-name LMI-Verification-ExecutionRole \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
aws iam delete-role --role-name LMI-Verification-ExecutionRole
aws iam detach-role-policy --role-name LMI-Verification-OperatorRole \
--policy-arn arn:aws:iam::aws:policy/AWSLambdaManagedEC2ResourceOperator
aws iam delete-role --role-name LMI-Verification-OperatorRole