MicrovmImage
Source:
src/AWS/Lambda/MicrovmImage.ts
A Lambda MicroVM image — a Firecracker snapshot that boots a fully initialized application in milliseconds. The model is image-then-launch: you create an image once (this resource), then launch isolated, stateful MicroVM instances from it at runtime (one per end-user/session) with the {@link RunMicrovm} binding from a Lambda Function.
How the build works
Section titled “How the build works”The build runs server-side on AWS — there is no local Docker. You supply
a code artifact (a zip of a Dockerfile + your code) and a base image
(baseImage); AWS runs your Dockerfile on top of the base, initializes
the app, and takes a Firecracker snapshot. The build is asynchronous: the
provider uploads the artifact, calls CreateMicrovmImage/UpdateMicrovmImage,
and polls until the image reaches CREATED/UPDATED (or surfaces the build
failure). Build logs stream to CloudWatch at /aws/lambda/microvms/<name>.
Alchemy produces the code artifact for you in three ways, selected by which
prop you set (main → context → codeArtifact.uri):
- Effectful (
main): write the in-VM HTTP server in TypeScript as an Effect. Alchemy bundles it (with its capability bindings), generates a Dockerfile on the MicroVM base, zips both, and uploads to the Assets bucket. - External (
context/dockerfile): bring your own Dockerfile + build context (any language). Alchemy zips the directory and uploads it. - Prebuilt (
codeArtifact.uri): point at an existing S3 zip or ECR image URI; nothing is built or uploaded.
Re-deploys only trigger a new build when the artifact’s content hash or a build-affecting prop changes; otherwise the image is left untouched.
Prerequisites
Section titled “Prerequisites”- A build role (
buildRole) Lambda assumes to read the code artifact and write build logs. Pass a {@link Role} instance and the required permissions are granted automatically — see the example below. - A bootstrapped Assets bucket (
alchemy aws bootstrap) for effectful / external modes, which upload the artifact to S3. - The account must be onboarded to the Lambda MicroVM preview.
Creating the Build Role
Section titled “Creating the Build Role”Pass a bare {@link Role} as buildRole and the MicroVM image grants
everything it needs via a binding: the trust policy (so Lambda can assume it)
plus the S3 (Assets bucket) and CloudWatch-logs permissions (folded into an
alchemy-bindings inline policy). You don’t write any policy yourself.
const buildRole = yield* AWS.IAM.Role("MicrovmBuildRole", {});
// Pass the role instance; trust + permissions are attached for you.const image = yield* AWS.Lambda.MicrovmImage("Sandbox", { main: import.meta.filename, buildRole,});Effectful MicroVMs
Section titled “Effectful MicroVMs”Write the in-VM server in TypeScript. Alchemy bundles main and bakes it into
the image; the server listens on port (default 8080), which becomes the
MicroVM endpoint.
In-VM HTTP server (single file)
export default class Sandbox extends AWS.Lambda.MicrovmImage<Sandbox>()( "Sandbox", { main: import.meta.filename, buildRole }, Effect.gen(function* () { return { fetch: Effect.gen(function* () { const request = yield* HttpServerRequest; return HttpServerResponse.text(`hello from ${request.url}`); }), }; }),) {}With capability bindings and env vars
export default class Sandbox extends AWS.Lambda.MicrovmImage<Sandbox>()( "Sandbox", { main: import.meta.filename, buildRole, runtime: "bun", env: { LOG_LEVEL: "info" }, }, Effect.gen(function* () { // bindings are bundled into the image and resolved at runtime const getObject = yield* AWS.S3.GetObject(bucket); return { fetch: Effect.gen(function* () { const obj = yield* getObject({ key: "data.json" }); return HttpServerResponse.json(yield* obj.json); }), }; }).pipe(Effect.provide(AWS.S3.GetObjectHttp)),) {}Class + .make() (two files, for a Lambda orchestrator)
When a Lambda Function imports the image to bind its instance operations, keep
the class (a typed handle) and the .make() runtime in separate files so the
orchestrator’s bundle doesn’t pull in the VM’s runtime deps.
// sandbox.ts — imported by the orchestratorexport class Sandbox extends AWS.Lambda.MicrovmImage<Sandbox>()("Sandbox") {}
// sandbox.live.ts — provided on the Stack; bundled into the image.// Must be the `default` export: the bundler resolves the image entrypoint// from the code artifact's default export.export default Sandbox.make( { main: import.meta.filename, buildRole }, Effect.gen(function* () { return { fetch: Effect.gen(function* () { return HttpServerResponse.text("ok"); }) }; }),);Tagged RPC
Section titled “Tagged RPC”Beyond (or instead of) a raw fetch handler, an image can expose a typed
RPC Shape as the second type parameter. The in-VM runtime serves those
methods over an /__rpc__/* protocol and falls through to fetch for every
other request, so an image can offer BOTH a typed RPC surface and ordinary
HTTP routes. A caller gets a fully-typed client with {@link connectMicrovm}:
value methods yield* as Effects, streaming methods pipe as Streams.
Define a tagged-RPC image (RPC + fetch)
// sandbox.ts — the typed handle imported by the orchestrator Lambdaexport class Sandbox extends AWS.Lambda.MicrovmImage< Sandbox, { hello: (message: string) => Effect.Effect<string> }>()("Sandbox") {}
// sandbox.live.ts — provided on the Stack; bundled into the image (default export)export default Sandbox.make( { main: import.meta.filename, buildRole }, Effect.gen(function* () { return { // RPC method — reached with `connectMicrovm` below hello: (message: string) => Effect.succeed(`hello, ${message}!`), // raw HTTP route — reached with a plain HTTPS request to the endpoint fetch: Effect.gen(function* () { const request = yield* HttpServerRequest; const url = new URL(request.url, "http://microvm"); return HttpServerResponse.json({ path: url.pathname }); }), }; }),);Call the RPC method from a Lambda
connectMicrovm builds the typed stub over an HttpClient (provide
FetchHttpClient.layer for the request scope), pointing at the running
MicroVM’s endpoint and authenticating with the authToken headers.
const vm = yield* runMicrovm({});// ...wait until `vm.state` is RUNNING (poll `getMicrovm`)...const { authToken } = yield* createAuthToken({ microvmIdentifier: vm.microvmId, expirationInMinutes: 5, allowedPorts: [{ port: 8080 }], // the in-VM server's port});
const sandbox = yield* AWS.Lambda.connectMicrovm(Sandbox, { endpoint: vm.endpoint, authToken,});const reply = yield* sandbox.hello("world"); // "hello, world!"Call the same MicroVM’s fetch route directly
For the raw HTTP path, send the auth token as request headers via {@link microvmAuthHeaders}.
const client = yield* HttpClient.HttpClient;const res = yield* client.get(`https://${vm.endpoint}/echo?message=hi`, { headers: AWS.Lambda.microvmAuthHeaders(authToken),});const body = yield* res.json;External Images (your own Dockerfile)
Section titled “External Images (your own Dockerfile)”Bring a build context directory containing a Dockerfile (any language).
Alchemy zips and uploads it; AWS runs your Dockerfile. Your Dockerfile should
build on a MicroVM-compatible base (e.g.
FROM public.ecr.aws/lambda/microvms:al2023-minimal).
Flask app from a Dockerfile
const image = yield* AWS.Lambda.MicrovmImage("Flask", { context: `${import.meta.dirname}/app`, // dir with Dockerfile + app.py buildRole,});Custom Dockerfile path within the context
const image = yield* AWS.Lambda.MicrovmImage("Worker", { context: `${import.meta.dirname}/app`, dockerfile: "docker/worker.Dockerfile", // relative to `context` buildRole,});Prebuilt Artifacts
Section titled “Prebuilt Artifacts”Skip the build entirely and point at an artifact you already produced.
const image = yield* AWS.Lambda.MicrovmImage("Prebuilt", { codeArtifact: { uri: "s3://my-bucket/app.zip" }, // or an ECR image URI buildRole,});Sizing and Base Image
Section titled “Sizing and Base Image”baseImage defaults to the latest AWS-managed al2023 base (discovered
via listManagedMicrovmImages). Override it, and tune CPU/memory, explicitly.
const image = yield* AWS.Lambda.MicrovmImage("Sized", { main: import.meta.filename, buildRole, baseImage: "arn:aws:lambda:us-east-1:aws:microvm-image:al2023-1", cpuConfigurations: [{ architecture: "ARM_64" }], resources: [{ minimumMemoryInMiB: 2048 }],});Logging
Section titled “Logging”const image = yield* AWS.Lambda.MicrovmImage("Logged", { main: import.meta.filename, buildRole, logging: { cloudWatch: { logGroup: "/aws/microvm/my-app" } }, // or disable: logging: { disabled: {} }});VPC Egress
Section titled “VPC Egress”Give the MicroVM a managed egress path into your VPC with a {@link NetworkConnector} (reference it by ARN).
const egress = yield* AWS.Lambda.NetworkConnector("Egress", { subnetIds: [subnet.subnetId], securityGroupIds: [sg.groupId], operatorRole: operatorRole.roleArn,});
const image = yield* AWS.Lambda.MicrovmImage("Connected", { main: import.meta.filename, buildRole, egressNetworkConnectors: [egress.networkConnectorArn],});Launching MicroVMs
Section titled “Launching MicroVMs”The image is just the template. Launch and drive instances from a Lambda Function using the per-operation bindings ({@link RunMicrovm}, {@link GetMicrovm}, {@link CreateAuthToken}, {@link TerminateMicrovm}, …). Each binding’s IAM policy is scoped to this image automatically.
Always terminate the MicroVM you launched — wrap the work in
Effect.ensuring so a failure (or a client retry) never leaks a running
MicroVM against your account’s memory quota. Give the Function a generous
timeout since it waits for the MicroVM to reach RUNNING in-line.
export default class Api extends AWS.Lambda.Function<Api>()( "Api", { main: import.meta.filename, url: true, timeout: Duration.seconds(120) }, Effect.gen(function* () { const runMicrovm = yield* AWS.Lambda.RunMicrovm(Sandbox); const getMicrovm = yield* AWS.Lambda.GetMicrovm(Sandbox); const createAuthToken = yield* AWS.Lambda.CreateAuthToken(Sandbox); const terminateMicrovm = yield* AWS.Lambda.TerminateMicrovm(Sandbox); return { fetch: Effect.gen(function* () { const vm = yield* runMicrovm({ idlePolicy: { maxIdleDurationSeconds: 900, suspendedDurationSeconds: 300, autoResumeEnabled: true, }, }); return yield* Effect.gen(function* () { // wait until the MicroVM is RUNNING before connecting yield* getMicrovm({ microvmIdentifier: vm.microvmId }).pipe( Effect.flatMap((m) => m.state === "RUNNING" ? Effect.void : Effect.fail(new Error(`microvm ${m.state}`)), ), Effect.retry({ schedule: Schedule.spaced("2 seconds"), times: 30 }), ); const { authToken } = yield* createAuthToken({ microvmIdentifier: vm.microvmId, expirationInMinutes: 5, allowedPorts: [{ port: 8080 }], }); const sandbox = yield* AWS.Lambda.connectMicrovm(Sandbox, { endpoint: vm.endpoint, authToken, }); const reply = yield* sandbox.hello("world"); return yield* HttpServerResponse.json({ reply }); }).pipe( // terminate on success OR failure — never leak a running MicroVM Effect.ensuring( terminateMicrovm({ microvmIdentifier: vm.microvmId }).pipe( Effect.ignore, ), ), // the in-VM endpoint calls need an HttpClient for this scope Effect.provide(FetchHttpClient.layer), ); }), }; }).pipe( Effect.provide( Layer.mergeAll( AWS.Lambda.RunMicrovmHttp, AWS.Lambda.GetMicrovmHttp, AWS.Lambda.CreateAuthTokenHttp, AWS.Lambda.TerminateMicrovmHttp, ), ), ),) {}