Prerequisites
- AWS CLI 2.35.10+ (the
lambda-microvms command was added in this version)
- An AWS account in a supported region (us-east-1, us-east-2, us-west-2, eu-west-1, ap-northeast-1)
- IAM permissions for Lambda, IAM, S3, CloudFront
# Check your CLI version
aws --version
# If below 2.35.10:
curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o /tmp/AWSCLIV2.pkg
sudo installer -pkg /tmp/AWSCLIV2.pkg -target /
Step 1: Create the S3 bucket and IAM roles
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REGION=eu-west-1
# S3 bucket for MicroVM image artifacts
aws s3api create-bucket \
--bucket microvm-artifacts-${ACCOUNT_ID}-${REGION} \
--create-bucket-configuration LocationConstraint=${REGION} \
--region ${REGION}
# Build role (used during image creation to read S3 + write logs)
aws iam create-role --role-name MicroVMBuildRole \
--assume-role-policy-document '{
"Version":"2012-10-17",
"Statement":[{
"Effect":"Allow",
"Principal":{"Service":"lambda.amazonaws.com"},
"Action":"sts:AssumeRole",
"Condition":{"StringEquals":{"aws:SourceAccount":"'${ACCOUNT_ID}'"}}
}]
}'
aws iam put-role-policy --role-name MicroVMBuildRole --policy-name BuildPolicy \
--policy-document '{
"Version":"2012-10-17",
"Statement":[
{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::microvm-artifacts-'${ACCOUNT_ID}'-'${REGION}'/*"},
{"Effect":"Allow","Action":["logs:CreateLogGroup","logs:CreateLogStream","logs:PutLogEvents"],"Resource":"arn:aws:logs:'${REGION}':'${ACCOUNT_ID}':log-group:/aws/lambda-microvms/*"}
]
}'
# Execution role (assumed by the running MicroVM)
aws iam create-role --role-name MicroVMExecutionRole \
--assume-role-policy-document '{
"Version":"2012-10-17",
"Statement":[{
"Effect":"Allow",
"Principal":{"Service":"lambda.amazonaws.com"},
"Action":"sts:AssumeRole",
"Condition":{"StringEquals":{"aws:SourceAccount":"'${ACCOUNT_ID}'"}}
}]
}'
aws iam put-role-policy --role-name MicroVMExecutionRole --policy-name ExecPolicy \
--policy-document '{
"Version":"2012-10-17",
"Statement":[{
"Effect":"Allow",
"Action":["logs:CreateLogGroup","logs:CreateLogStream","logs:PutLogEvents"],
"Resource":"arn:aws:logs:'${REGION}':'${ACCOUNT_ID}':log-group:/aws/lambda-microvms/*"
}]
}'
Important: Don't add ArnLike conditions referencing microvm-image/* in the trust policy. The service can't satisfy that condition before the image exists, and both builds and runs will fail with "unable to assume role."
Step 2: Package and build the MicroVM image
Create your app. Here's a simple Dockerfile for a Python web app:
FROM public.ecr.aws/lambda/microvms:al2023-minimal
RUN dnf install -y python3 python3-pip && dnf clean all
RUN python3 -m venv /app/venv
ENV PATH="/app/venv/bin:$PATH"
RUN pip install --no-cache-dir marimo pandas numpy matplotlib psutil
WORKDIR /app
COPY app.py /app/app.py
EXPOSE 2718
CMD ["marimo", "edit", "/app/app.py", "--host", "0.0.0.0", "--port", "2718", "--headless", "--no-token"]
Package and upload:
# Zip must contain Dockerfile at root
zip app.zip Dockerfile app.py
aws s3 cp app.zip s3://microvm-artifacts-${ACCOUNT_ID}-${REGION}/images/app.zip --region ${REGION}
# Create the image (takes 2-4 minutes)
aws lambda-microvms create-microvm-image \
--name my-web-app \
--base-image-arn arn:aws:lambda:${REGION}:aws:microvm-image:al2023-1 \
--build-role-arn arn:aws:iam::${ACCOUNT_ID}:role/MicroVMBuildRole \
--code-artifact '{"uri":"s3://microvm-artifacts-'${ACCOUNT_ID}'-'${REGION}'/images/app.zip"}' \
--additional-os-capabilities '["ALL"]' \
--resources '[{"minimumMemoryInMiB":4096}]' \
--region ${REGION}
# Poll until CREATED
watch -n 10 "aws lambda-microvms get-microvm-image \
--image-identifier arn:aws:lambda:${REGION}:${ACCOUNT_ID}:microvm-image:my-web-app \
--region ${REGION} --query state --output text"
If the build fails, check the reason:
aws lambda-microvms list-microvm-image-builds \
--image-identifier arn:aws:lambda:${REGION}:${ACCOUNT_ID}:microvm-image:my-web-app \
--image-version 1.0 --region ${REGION}
Step 3: Run the MicroVM
aws lambda-microvms run-microvm \
--image-identifier arn:aws:lambda:${REGION}:${ACCOUNT_ID}:microvm-image:my-web-app \
--image-version 1.0 \
--execution-role-arn arn:aws:iam::${ACCOUNT_ID}:role/MicroVMExecutionRole \
--idle-policy '{"maxIdleDurationSeconds":1800,"suspendedDurationSeconds":28800,"autoResumeEnabled":true}' \
--region ${REGION}
This returns a microvmId and endpoint. The idle policy means:
- Auto-suspend after 30 minutes of no traffic (compute billing stops)
- Stay suspended up to 8 hours before being terminated
- Auto-resume when the next request arrives (~1-2 seconds)
Step 4: Make it public with CloudFront + Lambda@Edge
This is the architecture that works:
Browser → CloudFront → Lambda@Edge (injects auth token) → MicroVM
CloudFront passes WebSocket through natively. Lambda@Edge fires on every origin-request and adds the auth header. No always-on server needed.
Create the Lambda@Edge function (must be us-east-1)
# lambda_function.py
"""Lambda@Edge: injects MicroVM auth token on origin-request."""
import json
import urllib.request
import ssl
import time
import botocore.session
import botocore.auth
import botocore.awsrequest
MICROVM_ID = "microvm-YOUR-ID-HERE"
REGION = "eu-west-1"
PORT = "2718"
_cache = {"token": None, "expires": 0}
def get_token():
if time.time() < _cache["expires"] - 120:
return _cache["token"]
session = botocore.session.get_session()
credentials = session.get_credentials().get_frozen_credentials()
url = f"https://lambda.{REGION}.amazonaws.com/2025-09-09/microvms/{MICROVM_ID}/auth-token"
body = json.dumps({"expirationInMinutes": 60, "allowedPorts": [{"allPorts": {}}]}).encode()
request = botocore.awsrequest.AWSRequest(method="POST", url=url, data=body, headers={"Content-Type": "application/json"})
botocore.auth.SigV4Auth(credentials, "lambda", REGION).add_auth(request)
req = urllib.request.Request(url, data=body, method="POST", headers=dict(request.headers))
with urllib.request.urlopen(req, context=ssl.create_default_context(), timeout=4) as resp:
data = json.loads(resp.read())
_cache["token"] = data["authToken"]["X-aws-proxy-auth"]
_cache["expires"] = time.time() + 3600
return _cache["token"]
def handler(event, context):
request = event["Records"][0]["cf"]["request"]
token = get_token()
request["headers"]["x-aws-proxy-auth"] = [{"key": "X-aws-proxy-auth", "value": token}]
request["headers"]["x-aws-proxy-port"] = [{"key": "X-aws-proxy-port", "value": PORT}]
return request
Note: We use raw sigv4 signing because the Lambda runtime's boto3 doesn't include the lambda-microvms service yet. The signing service name is lambda, API path is /2025-09-09/microvms/{id}/auth-token.
Deploy it:
zip edge-lambda.zip lambda_function.py
aws iam create-role --role-name MicroVMEdgeLambdaRole \
--assume-role-policy-document '{
"Version":"2012-10-17",
"Statement":[{
"Effect":"Allow",
"Principal":{"Service":["lambda.amazonaws.com","edgelambda.amazonaws.com"]},
"Action":"sts:AssumeRole"
}]
}'
aws iam put-role-policy --role-name MicroVMEdgeLambdaRole --policy-name EdgePolicy \
--policy-document '{
"Version":"2012-10-17",
"Statement":[
{"Effect":"Allow","Action":["logs:CreateLogGroup","logs:CreateLogStream","logs:PutLogEvents"],"Resource":"*"},
{"Effect":"Allow","Action":"lambda:CreateMicrovmAuthToken","Resource":"*"}
]
}'
sleep 10
aws lambda create-function \
--function-name microvm-edge-auth \
--runtime python3.12 \
--handler lambda_function.handler \
--role arn:aws:iam::${ACCOUNT_ID}:role/MicroVMEdgeLambdaRole \
--zip-file fileb://edge-lambda.zip \
--timeout 5 --memory-size 128 \
--region us-east-1
# Publish a version (required for Lambda@Edge)
EDGE_ARN=$(aws lambda publish-version --function-name microvm-edge-auth \
--region us-east-1 --query 'FunctionArn' --output text)
Create the CloudFront distribution
MICROVM_ENDPOINT="YOUR-ENDPOINT.lambda-microvm.eu-west-1.on.aws"
cat > cf-config.json << EOF
{
"CallerReference": "microvm-$(date +%s)",
"Comment": "MicroVM public proxy",
"Enabled": true,
"Origins": {
"Quantity": 1,
"Items": [{
"Id": "microvm",
"DomainName": "${MICROVM_ENDPOINT}",
"CustomOriginConfig": {
"HTTPPort": 80, "HTTPSPort": 443,
"OriginProtocolPolicy": "https-only",
"OriginSslProtocols": {"Quantity": 1, "Items": ["TLSv1.2"]}
}
}]
},
"DefaultCacheBehavior": {
"TargetOriginId": "microvm",
"ViewerProtocolPolicy": "redirect-to-https",
"AllowedMethods": {"Quantity": 7, "Items": ["GET","HEAD","OPTIONS","PUT","POST","PATCH","DELETE"], "CachedMethods": {"Quantity": 2, "Items": ["GET","HEAD"]}},
"CachePolicyId": "4135ea2d-6df8-44a3-9df3-4b5a84be39ad",
"OriginRequestPolicyId": "b689b0a8-53d0-40ab-baf2-68738e2966ac",
"LambdaFunctionAssociations": {
"Quantity": 1,
"Items": [{
"LambdaFunctionARN": "${EDGE_ARN}",
"EventType": "origin-request",
"IncludeBody": true
}]
},
"Compress": true
}
}
EOF
aws cloudfront create-distribution --distribution-config file://cf-config.json \
--query 'Distribution.[Id,DomainName]' --output text
Critical setting: The OriginRequestPolicyId must be b689b0a8-53d0-40ab-baf2-68738e2966ac (AllViewerExceptHostHeader). If you use AllViewer, CloudFront sends its own domain as the Host header and the MicroVM rejects the request with "Token authentication failed."
Wait 2-5 minutes for deployment, then open https://YOUR-ID.cloudfront.net in a browser.
Pricing breakdown (eu-west-1)
Compute (per-second billing)
| Dimension |
Price |
| vCPU per second |
0ドル.0000291572 |
| Memory per GB-second |
0ドル.0000038603 |
Snapshots
| Dimension |
Price |
| Storage |
0ドル.0952/GB-month |
| Data read (start/resume) |
0ドル.00164/GB |
| Data written (suspend) |
0ドル.00406/GB |
Cost examples for a 4 GB / 2 vCPU MicroVM
Scenario 1: Personal dev tool, 4 hours/day, 20 days/month
Active seconds: 4h ×ばつ 20d ×ばつ 3600 = 288,000s
vCPU: 288,000 ×ばつ 2 ×ばつ 0ドル.0000291572 = 16ドル.79
Memory: 288,000 ×ばつ 4 ×ばつ 0ドル.0000038603 = 4ドル.45
Suspend/resume (20 cycles ×ばつ 4GB):
Write: 20 ×ばつ 4 ×ばつ 0ドル.00406 = 0ドル.32
Read: 20 ×ばつ 4 ×ばつ 0ドル.00164 = 0ドル.13
Image storage: 2GB ×ばつ 0ドル.0952 = 0ドル.19
Total: ~22ドル/month
Scenario 2: Always-on 24/7
Active seconds: 30d ×ばつ 86,400 = 2,592,000s
vCPU: 2,592,000 ×ばつ 2 ×ばつ 0ドル.0000291572 = 151ドル.15
Memory: 2,592,000 ×ばつ 4 ×ばつ 0ドル.0000038603 = 40ドル.01
Total: ~191ドル/month
For comparison, a t4g.medium EC2 (2 vCPU, 4 GB) costs ~27ドル/month on-demand. MicroVMs are 7x more expensive for continuous workloads.
Scenario 3: Bursty AI coding assistant (100 users, 2.5h active/day)
This is where MicroVMs shine. With suspend/resume, you don't pay for the 21.5 idle hours:
Per user/day: 2.5h active + auto-suspend
Monthly compute per user: ~11ドル
vs. always-on EC2 per user: ~27ドル
Savings: 60% (and you get VM isolation between users)
When MicroVMs make economic sense
| Pattern |
MicroVM cost vs. EC2 |
| Always-on |
5-7x more expensive |
| 4-6 hours/day |
Roughly equivalent |
| Under 3 hours/day |
Cheaper than EC2 |
| Bursty multi-tenant |
Much cheaper (no idle pool) |
Practical pricing examples
Example A: PDF generation service (multi-tenant SaaS)
Your app generates invoices/reports on demand. Each PDF takes 8 seconds to render. You process 10,000 PDFs/month across 50 tenants. MicroVM config: 2 GB / 1 vCPU.
Compute per PDF: 8s ×ばつ (1 ×ばつ 0ドル.0000291572 + 2 ×ばつ 0ドル.0000038603) = 0ドル.000295
10,000 PDFs/month: 2ドル.95
Image storage (1 GB): 0ドル.10
Snapshot reads (10,000 launches ×ばつ 1 GB): 10,000 ×ばつ 0ドル.00164 = 16ドル.40
Total: ~19ドル.50/month for 10,000 isolated PDF renders
With suspend/resume (keep VMs warm per tenant, 50 tenants ×ばつ 6 resume cycles/day):
Active compute (8s ×ばつ 200 PDFs/tenant): 50 ×ばつ 200 ×ばつ 8s = 80,000s
vCPU: 80,000 ×ばつ 0ドル.0000291572 = 2ドル.33
Memory: 80,000 ×ばつ 2 ×ばつ 0ドル.0000038603 = 0ドル.62
Suspend/resume (50 ×ばつ 6 ×ばつ 2GB): reads 0ドル.98 + writes 2ドル.44
Total: ~6ドル.50/month
Compare: a dedicated Fargate task per tenant (50 ×ばつ 15ドル/month) = 750ドル. MicroVMs are 100x cheaper for this pattern.
Example B: CI test runner (isolated builds)
Each build runs for 3 minutes in an isolated VM. 500 builds/month. Config: 8 GB / 4 vCPU (compilation needs horsepower).
Seconds per build: 180s
vCPU: 500 ×ばつ 180 ×ばつ 4 ×ばつ 0ドル.0000291572 = 10ドル.49
Memory: 500 ×ばつ 180 ×ばつ 8 ×ばつ 0ドル.0000038603 = 2ドル.78
Snapshot reads (500 ×ばつ 4 GB image): 500 ×ばつ 4 ×ばつ 0ドル.00164 = 3ドル.28
Image storage: 4 GB ×ばつ 0ドル.0952 = 0ドル.38
Total: ~17ドル/month for 500 isolated CI builds
Compare: GitHub Actions at 0ドル.008/min ×ばつ 180s ×ばつ 500 = 12ドル/month (but shared runners, no VM isolation). A self-hosted runner on EC2 (m6g.xlarge) = ~115ドル/month always-on.
Example C: Playwright browser testing (ephemeral browsers)
E2E test suite spins up an isolated browser per test scenario. Each test runs 45 seconds. 2,000 tests/month. Config: 4 GB / 2 vCPU.
vCPU: 2,000 ×ばつ 45 ×ばつ 2 ×ばつ 0ドル.0000291572 = 5ドル.25
Memory: 2,000 ×ばつ 45 ×ばつ 4 ×ばつ 0ドル.0000038603 = 1ドル.39
Snapshot reads (2,000 ×ばつ 3 GB image with Chromium): 2,000 ×ばつ 3 ×ばつ 0ドル.00164 = 9ドル.84
Image storage: 3 GB ×ばつ 0ドル.0952 = 0ドル.29
Total: ~17ドル/month for 2,000 isolated browser tests
The snapshot-resume model is particularly good here. The Chromium binary and browser state are pre-loaded in the snapshot. No 10-second browser startup per test; it's already running when the MicroVM resumes.
Compare: BrowserStack/Sauce Labs charge 0ドル.01-0.05 per test minute. At 2,000 ×ばつ 45s = 15ドル-75/month. MicroVMs are competitive and fully under your control.
The breakeven is around 4-5 hours of daily active use. Below that, suspend/resume saves you money. Above that, EC2 wins on raw cost but loses on isolation and operational overhead.
Advanced: multi-tenant architecture with per-user MicroVMs
The single-MicroVM setup is a playground. The real value of this service is giving each user their own isolated environment. Here's the production pattern:
User A ──┐
User B ──┼─→ CloudFront → Lambda@Edge (auth + routing) → User A's MicroVM
User C ──┘ ↓ → User B's MicroVM
DynamoDB (user→MicroVM mapping) → User C's MicroVM
The Lambda@Edge function becomes a router:
def handler(event, context):
request = event["Records"][0]["cf"]["request"]
user_id = extract_user_from_jwt(request)
# Lookup or provision this user's MicroVM
microvm = get_or_create_microvm(user_id) # DynamoDB + RunMicrovm API
# Route to this user's specific MicroVM
request["origin"]["custom"]["domainName"] = microvm["endpoint"]
request["headers"]["host"] = [{"key": "Host", "value": microvm["endpoint"]}]
request["headers"]["x-aws-proxy-auth"] = [
{"key": "X-aws-proxy-auth", "value": get_token(microvm["id"])}
]
return request
Cost control per user:
- Each MicroVM auto-suspends after idle timeout (compute stops)
- Set
maximumDurationInSeconds to cap total runtime per session
- Set
suspendedDurationSeconds to terminate abandoned environments
- Track spend per user via CloudWatch metrics
This is the pattern behind Replit, CodeSandbox, and AI coding assistants. Each user gets VM-level isolation, billed only during active use.
Governance: controlling what agents do inside their sandbox
Isolation solves "User A can't access User B's data." It doesn't solve "User A's AI agent just sent 10,000 emails using its tool access."
If you're running AI agents inside MicroVMs (the primary use case AWS targets), you need a second layer: behavioral governance. MicroVMs isolate the environment. You still need something to govern the actions.
Shape addresses this gap. It wraps any tool-calling agent with hard constraints:
-
Lifecycle phases: agents can only read during exploration, can only write during commit
-
Budget gates: cost and time thresholds that change agent behavior in real time (at 75% budget, block commits; after 30 minutes, force wrap-up)
-
Transaction protection: multi-step actions are all-or-nothing with automatic compensation
-
Effect classification: each tool is labeled READ/REVERSIBLE/IRREVERSIBLE, enforced at runtime
-
Resource control: not just what an agent does, but how much — tokens spent, API calls made, wall-clock time consumed, dollars burned
# Inside the MicroVM, the agent runs under Shape governance
agent = Agent("code-assistant", budget=2.00)
agent.tool("run_code", effect=ToolEffect.REVERSIBLE, fn=execute_fn, cost=0.01)
agent.tool("call_llm", effect=ToolEffect.READ, fn=llm_fn, cost=0.05)
agent.tool("deploy", effect=ToolEffect.IRREVERSIBLE, fn=deploy_fn, cost=0.50)
agent.rules("""
BLOCK deploy WHEN phase IS NOT commit
BLOCK * WHEN budget ABOVE 75%
BLOCK * WHEN time ABOVE 1800
REQUIRE APPROVAL FOR * WHEN tool IS irreversible
""")
The budget isn't just dollars. It's a proxy for any consumable resource: time, tokens, API calls. An agent that has been running for 30 minutes and spent 1ドル.50 of its 2ドル budget will get forced into a different behavior mode — finish up, summarize, stop exploring. Without this, agents inside a MicroVM happily burn through compute until the 8-hour max runtime kills them.
The architecture becomes: MicroVMs for isolation, Shape for governance, CloudFront for access. Each layer solves a different problem. Remove any one and you have a gap.
Does this architecture make sense?
When to use MicroVMs vs Lambda functions
| Signal |
Use MicroVM |
Use Lambda function |
| Needs state between requests |
✓ |
— |
| Runs untrusted/user code |
✓ |
— |
| Long-running (>15 min) |
✓ |
— |
| WebSocket / persistent connection |
✓ |
— |
| Needs full OS (FUSE, eBPF, Docker) |
✓ |
— |
| High-volume, stateless |
— |
✓ |
| Event-driven (S3, SQS, etc.) |
— |
✓ |
| Sub-second billing granularity |
— |
✓ |
| Auto-scales to thousands |
— |
✓ |
The rule: if it needs isolation + state + long runtime, it's a MicroVM workload. If it's stateless + short + high-volume, Lambda functions win.
Where MicroVMs fit best
| Use case |
Why MicroVM wins |
| AI coding assistants |
Per-user sandbox, pip install persists, tools run in isolation |
| Browser testing (Playwright) |
Snapshot pre-loads Chromium, no 10s cold start per test |
| Local LLM sandboxes |
Ollama/llama.cpp in isolation per tenant, 8hr sessions |
| Game/simulation servers |
Stateful WebSocket, session-affine routing, suspend between matches |
| Dev environments |
VS Code Server per developer, suspend overnight, resume in 1s |
| CI/CD runners |
Docker-in-Docker, isolated builds, terminate after job |
| Training/workshop sandboxes |
Pre-configured environments that reset per session |
Where Lambda functions still win
PDF generation, image processing, webhook handlers, API backends with high concurrency, event-driven pipelines. These are stateless, short-lived, and benefit from Lambda's auto-scaling. Putting a PDF generator in a MicroVM works (we built one as a demo) but it's more expensive and complex than a Lambda function with a WeasyPrint layer.
Yes, if:
- You need VM-level isolation between tenants (not just containers)
- Usage is bursty (active for minutes/hours, idle for hours)
- You want zero infrastructure management (no patching, no scaling decisions)
- You need instant-on from a pre-initialized state (snapshot resume)
No, if:
- You need continuous compute (EC2/Fargate is cheaper)
- You need kernel modifications or non-Linux (EC2 only)
- You want a simple public web app (Lightsail at 3ドル.50/month is simpler)
- You need WebSocket without the CloudFront+Lambda@Edge setup
The service fills a real gap for platforms building multi-tenant code execution (think Replit, CodeSandbox, Cursor's cloud environments). For a single-user playground, it works but the CloudFront+Lambda@Edge layer adds complexity that a 3ドル.50 Lightsail instance doesn't need.
Region availability
Lambda MicroVMs launched on June 22, 2026 in five regions:
| Region |
Location |
| us-east-1 |
N. Virginia |
| us-east-2 |
Ohio |
| us-west-2 |
Oregon |
| eu-west-1 |
Ireland |
| ap-northeast-1 |
Tokyo |
ARM64 (Graviton) only. No x86 option at launch. Your S3 artifact bucket and any network connectors must be in the same region as the image.
Infrastructure as Code: CloudFormation and CDK
Lambda MicroVMs launched with full AWS CloudFormation and AWS CDK support. The AWS::Lambda::MicrovmImage resource type manages the image build lifecycle through the stack. Running MicroVMs (the per-user ephemeral instances) are still API/SDK-managed since they're dynamic runtime resources, not static infrastructure.
CloudFormation template
Resources:
MicrovmImage:
Type: AWS::Lambda::MicrovmImage
Properties:
Name: my-web-app
Description: "Myapplicationimage"
BaseImageArn: !Sub "arn:aws:lambda:${AWS::Region}:aws:microvm-image:al2023-1"
BaseImageVersion: "0"
BuildRoleArn: !GetAtt BuildRole.Arn
CodeArtifact:
Uri: !Sub "s3://${ArtifactBucket}/images/app.zip"
AdditionalOsCapabilities:
- ALL
CpuConfigurations:
- Architecture: ARM_64
Resources:
- MinimumMemoryInMiB: 4096
EgressNetworkConnectors: []
EnvironmentVariables: []
Hooks: {}
Logging:
CloudWatch: {}
Deploy with:
aws cloudformation deploy \
--template-file microvm-image.yaml \
--stack-name microvm-my-web-app \
--parameter-overrides AppName=my-web-app \
--capabilities CAPABILITY_NAMED_IAM \
--region eu-west-1
CDK (Python)
A single CDK stack manages image build, MicroVM lifecycle, and CloudFront in one command. A custom resource (orchestrator Lambda) handles the imperative steps: running the MicroVM, creating the Lambda@Edge function in us-east-1, and wiring CloudFront.
cd infra/cdk
pip install aws-cdk-lib constructs
# Build orchestrator dependencies (bundles boto3 + lambda-microvms service model)
./orchestrator/build.sh
# Upload your app code
aws s3 cp app.zip s3://microvm-artifacts-ACCT-eu-west-1/images/playground.zip
# Deploy everything: image build → run MicroVM → edge function → CloudFront
cdk deploy -c app_name=playground -c app_port=2718 --profile YOUR_PROFILE
The orchestrator Lambda:
- Calls
RunMicrovm and polls until RUNNING
- Creates the Lambda@Edge function in us-east-1 (CloudFront requirement)
- Bakes the MicroVM endpoint into the edge function code
- Publishes a version and returns it to CloudFormation
- On
cdk destroy, terminates the MicroVM and deletes the edge function
Key gotchas we hit building this:
- The orchestrator must bundle its own boto3 with the
lambda-microvms service model (not in the Lambda runtime's SDK yet). Run ./orchestrator/build.sh to install it.
- IAM actions use the
lambda: namespace (e.g., lambda:RunMicrovm), not lambda-microvms:. The signing name in the service model is lambda.
- Lambda@Edge functions must exist in us-east-1. The orchestrator creates them cross-region.
- Custom resource responses have a 4096-byte limit. Truncate error messages before sending.
-
PublishVersion races with UpdateFunctionCode. Wait for LastUpdateStatus == Successful before publishing.
The full source is at infra/cdk/ in the starter kit repo.
Gotcha: BaseImageVersion
The BaseImageVersion property is required but the correct value isn't obvious. You need to query it:
aws lambda-microvms list-managed-microvm-image-versions \
--image-identifier "arn:aws:lambda:eu-west-1:aws:microvm-image:al2023-1" \
--region eu-west-1
At launch, the only valid value is "0". Using "1" or "1.0" fails with "No managed runtime with arn ... and version X is available."
What gets managed by IaC vs. API
| Resource |
Managed by |
Why |
| MicroVM image |
CloudFormation/CDK |
Static infrastructure, versioned |
| IAM roles |
CloudFormation/CDK |
Static infrastructure |
| S3 artifact bucket |
CloudFormation/CDK |
Static infrastructure |
| CloudFront distribution |
CloudFormation/CDK |
Static infrastructure |
| Running MicroVMs |
API/SDK at runtime |
Dynamic, per-user, ephemeral |
The Agent Toolkit for AWS already includes a skill (aws-lambda-microvms) that teaches AI coding agents how to build and operate MicroVMs:
npx skills add aws/agent-toolkit-for-aws/skills/specialized-skills/serverless-skills/aws-lambda-microvms
VPC connectivity
MicroVMs can access private VPC resources (RDS, ElastiCache, internal APIs) through Lambda Network Connectors. The model is identical to Lambda functions in a VPC: the MicroVM itself does NOT run inside your subnet. It runs on AWS-managed infrastructure and connects to your VPC through ENIs that the network connector creates.
# Create a reusable network connector
aws lambda-microvms create-network-connector \
--name my-vpc-connector \
--subnet-ids '["subnet-xxx","subnet-yyy"]' \
--security-group-ids '["sg-xxx"]' \
--ip-address-type DUAL_STACK \
--role-arn arn:aws:iam::ACCT:role/NetworkConnectorRole \
--region eu-west-1
# Attach when running a MicroVM
aws lambda-microvms run-microvm \
--image-identifier arn:aws:lambda:eu-west-1:ACCT:microvm-image:my-app \
--egress-network-connectors '["arn:aws:lambda:eu-west-1:ACCT:network-connector:my-vpc-connector"]' \
...
Key points:
- Default egress is
INTERNET_EGRESS (public internet, no VPC)
- With a VPC connector, outbound goes through your subnets (need NAT gateway for internet)
- Network connectors are reusable across MicroVMs (create once, reference by ARN)
- Connectors create ENIs in your VPC that aren't visible by default (
DescribeNetworkInterfaces needs IncludeManagedResources=true)
- A network connector can't be changed after MicroVM launch (bound at run time, persists through suspend/resume)
- A network team can pre-create connectors and developers just reference the ARN
This separation means you get VPC access without the cold-start penalty that Lambda functions in VPCs used to have. The ENIs are managed by the connector, not per-MicroVM.
Cleanup
# Terminate MicroVM
aws lambda-microvms terminate-microvm --microvm-identifier MICROVM_ID --region eu-west-1
# Delete image (wait for MicroVM termination first)
aws lambda-microvms delete-microvm-image \
--image-identifier arn:aws:lambda:eu-west-1:${ACCOUNT_ID}:microvm-image:my-web-app \
--region eu-west-1
# Disable and delete CloudFront (takes a few minutes)
# ... update distribution with Enabled=false, then delete
# Delete Lambda functions, IAM roles, S3 bucket
Tested June 25-28, 2026 in eu-west-1. The service launched 6 days before this writeup. CloudFormation and CDK work for image management and full lifecycle (single-command deploy via custom resource). Expect the SDK coverage and documentation to improve as the service matures.