| 1 | --- |
| 2 | title: Lambda MicroVMs |
| 3 | description: Build a Firecracker MicroVM image from TypeScript or a Dockerfile, then launch and drive isolated stateful instances from a Lambda Function with typed lifecycle bindings and RPC. |
| 4 | sidebar: |
| 5 | order: 8 |
| 6 | --- |
| 7 | |
| 8 | A Lambda **MicroVM image** is a Firecracker snapshot that boots a fully |
| 9 | initialized application in milliseconds. The model is *image-then-launch*: |
| 10 | `AWS.Lambda.MicrovmImage` builds the image once at deploy time, then a Lambda |
| 11 | Function launches isolated, stateful MicroVM instances from it at runtime — |
| 12 | one per end-user or session. That makes MicroVMs a natural fit for per-user |
| 13 | sandboxes: code interpreters, agent workspaces, preview environments. |
| 14 | |
| 15 | The build runs **server-side on AWS** — no local Docker. Alchemy produces the |
| 16 | code artifact in one of three modes, selected by which prop you set: |
| 17 | |
| 18 | - **Effectful** (`main`) — write the in-VM HTTP server in TypeScript; Alchemy |
| 19 | bundles it, generates a Dockerfile, and uploads the zip. |
| 20 | - **External** (`context`/`dockerfile`) — bring your own Dockerfile and build |
| 21 | context in any language; Alchemy zips and uploads the directory. |
| 22 | - **Prebuilt** (`codeArtifact`) — point at an existing S3 zip or ECR image URI. |
| 23 | |
| 24 | :::caution |
| 25 | Lambda MicroVMs are a **preview feature**: your account must be onboarded to |
| 26 | the preview, and effectful/external builds need a bootstrapped Assets bucket |
| 27 | (`alchemy provider aws bootstrap`). Builds are asynchronous and can take several |
| 28 | minutes — the provider polls until the image reaches `CREATED` and streams |
| 29 | build logs to CloudWatch. |
| 30 | ::: |
| 31 | |
| 32 | ## Define an effectful image |
| 33 | |
| 34 | Write the in-VM server as an Effect. The class is a typed handle the |
| 35 | orchestrator imports; the default-exported `.make()` is what gets bundled into |
| 36 | the image. The shape can expose both a typed **RPC method** (`hello`) and a |
| 37 | raw **`fetch`** route — the in-VM runtime serves RPC over an internal |
| 38 | `/__rpc__/*` protocol and falls through to `fetch` for everything else. The |
| 39 | `{ fetch, ...rpcs }` shape is [Schemaless RPC](/apis/schemaless) — the same |
| 40 | contract every Function and Server returns: |
| 41 | |
| 42 | ```typescript |
| 43 | // sandbox.ts |
| 44 | import * as AWS from "alchemy/AWS"; |
| 45 | import * as Effect from "effect/Effect"; |
| 46 | import { HttpServerRequest } from "effect/http/HttpServerRequest"; |
| 47 | import * as HttpServerResponse from "effect/http/HttpServerResponse"; |
| 48 | |
| 49 | export const BuildRole = AWS.IAM.Role("MicrovmBuildRole"); |
| 50 | |
| 51 | export class Sandbox extends AWS.Lambda.MicrovmImage< |
| 52 | Sandbox, |
| 53 | { hello: (message: string) => Effect.Effect<string> } |
| 54 | >()("Sandbox") {} |
| 55 | |
| 56 | export default Sandbox.make( |
| 57 | BuildRole.pipe( |
| 58 | Effect.map((buildRole) => ({ main: import.meta.filename, buildRole })), |
| 59 | ), |
| 60 | Effect.gen(function* () { |
| 61 | return { |
| 62 | // typed RPC method — reached with `connectMicrovm` |
| 63 | hello: (message: string) => Effect.succeed(`hello, ${message}!`), |
| 64 | // raw HTTP route — reached with a plain HTTPS request to the endpoint |
| 65 | fetch: Effect.gen(function* () { |
| 66 | const request = yield* HttpServerRequest; |
| 67 | const url = new URL(request.url, "http://microvm"); |
| 68 | return yield* HttpServerResponse.json({ |
| 69 | message: url.searchParams.get("message") ?? "", |
| 70 | }); |
| 71 | }), |
| 72 | }; |
| 73 | }), |
| 74 | ); |
| 75 | ``` |
| 76 | |
| 77 | The bare `Role` passed as `buildRole` needs no policy of its own — the image |
| 78 | grants it the Lambda trust statement plus the S3 and CloudWatch-logs |
| 79 | permissions the build needs via a binding. |
| 80 | |
| 81 | ## Launch and drive MicroVMs from a Lambda |
| 82 | |
| 83 | The image is just the template. A Lambda Function binds the per-instance |
| 84 | lifecycle operations — `RunMicrovm`, `GetMicrovm`, `CreateAuthToken`, |
| 85 | `TerminateMicrovm` — each scoped to the image by IAM automatically. Launch a |
| 86 | VM, wait for `RUNNING`, mint an auth token, then call the typed RPC stub with |
| 87 | `connectMicrovm`: |
| 88 | |
| 89 | ```typescript |
| 90 | // api.ts |
| 91 | import * as AWS from "alchemy/AWS"; |
| 92 | import * as Duration from "effect/Duration"; |
| 93 | import * as Effect from "effect/Effect"; |
| 94 | import * as Layer from "effect/Layer"; |
| 95 | import * as Schedule from "effect/Schedule"; |
| 96 | import * as FetchHttpClient from "effect/http/FetchHttpClient"; |
| 97 | import * as HttpServerResponse from "effect/http/HttpServerResponse"; |
| 98 | import { Sandbox } from "./sandbox.ts"; |
| 99 | |
| 100 | export default class Api extends AWS.Lambda.Function<Api>()( |
| 101 | "Api", |
| 102 | // generous timeout: the handler waits for the MicroVM to reach RUNNING |
| 103 | { main: import.meta.filename, functionUrl: true, timeout: Duration.seconds(120) }, |
| 104 | Effect.gen(function* () { |
| 105 | const runMicrovm = yield* AWS.Lambda.RunMicrovm(Sandbox); |
| 106 | const getMicrovm = yield* AWS.Lambda.GetMicrovm(Sandbox); |
| 107 | const createAuthToken = yield* AWS.Lambda.CreateAuthToken(Sandbox); |
| 108 | const terminateMicrovm = yield* AWS.Lambda.TerminateMicrovm(Sandbox); |
| 109 | |
| 110 | return { |
| 111 | fetch: Effect.gen(function* () { |
| 112 | const vm = yield* runMicrovm({}); |
| 113 | return yield* Effect.gen(function* () { |
| 114 | // wait until the MicroVM is RUNNING before connecting |
| 115 | yield* getMicrovm({ microvmIdentifier: vm.microvmId }).pipe( |
| 116 | Effect.flatMap((m) => |
| 117 | m.state === "RUNNING" |
| 118 | ? Effect.void |
| 119 | : Effect.fail(new Error(`microvm ${m.state}`)), |
| 120 | ), |
| 121 | Effect.retry({ schedule: Schedule.spaced("2 seconds"), times: 30 }), |
| 122 | ); |
| 123 | const { authToken } = yield* createAuthToken({ |
| 124 | microvmIdentifier: vm.microvmId, |
| 125 | expirationInMinutes: 5, |
| 126 | allowedPorts: [{ port: 8080 }], // the in-VM server's port |
| 127 | }); |
| 128 | const sandbox = yield* AWS.Lambda.connectMicrovm(Sandbox, { |
| 129 | endpoint: vm.endpoint, |
| 130 | authToken, |
| 131 | }); |
| 132 | const reply = yield* sandbox.hello("world"); // "hello, world!" |
| 133 | return yield* HttpServerResponse.json({ reply }); |
| 134 | }).pipe( |
| 135 | // terminate on success OR failure — never leak a running MicroVM |
| 136 | Effect.ensuring( |
| 137 | terminateMicrovm({ microvmIdentifier: vm.microvmId }).pipe( |
| 138 | Effect.ignore, |
| 139 | ), |
| 140 | ), |
| 141 | // the in-VM endpoint calls need an HttpClient for this scope |
| 142 | Effect.provide(FetchHttpClient.layer), |
| 143 | ); |
| 144 | }), |
| 145 | }; |
| 146 | }).pipe( |
| 147 | Effect.provide( |
| 148 | Layer.mergeAll( |
| 149 | AWS.Lambda.RunMicrovmHttp, |
| 150 | AWS.Lambda.GetMicrovmHttp, |
| 151 | AWS.Lambda.CreateAuthTokenHttp, |
| 152 | AWS.Lambda.TerminateMicrovmHttp, |
| 153 | ), |
| 154 | ), |
| 155 | ), |
| 156 | ) {} |
| 157 | ``` |
| 158 | |
| 159 | The stub is [Schemaless RPC](/apis/schemaless) — the same typed client you get |
| 160 | binding a Durable Object or Container into a Worker, here a MicroVM into a |
| 161 | Lambda Function. |
| 162 | |
| 163 | Always wrap the work in `Effect.ensuring(terminateMicrovm(...))` — a failure |
| 164 | (or a client retry) must never leak a running MicroVM against your account's |
| 165 | memory quota. `SuspendMicrovm`, `ResumeMicrovm`, and `ListMicrovms` follow the |
| 166 | same bind-then-call pattern for longer-lived sessions. |
| 167 | |
| 168 | ## Call the fetch route directly |
| 169 | |
| 170 | The `fetch` handler is plain HTTPS against the MicroVM's endpoint (a bare |
| 171 | hostname like `<id>.lambda-microvm.<region>.on.aws`). Authenticate with the |
| 172 | same token via `microvmAuthHeaders`: |
| 173 | |
| 174 | ```typescript |
| 175 | const client = yield* HttpClient.HttpClient; |
| 176 | const res = yield* client.get(`https://${vm.endpoint}/echo?message=hi`, { |
| 177 | headers: AWS.Lambda.microvmAuthHeaders(authToken), |
| 178 | }); |
| 179 | const body = yield* res.json; // { message: "hi" } |
| 180 | ``` |
| 181 | |
| 182 | Both surfaces coexist on one image: the RPC stub for typed calls from your own |
| 183 | code, the fetch route for anything that speaks HTTP. |
| 184 | |
| 185 | ## Wire the stack |
| 186 | |
| 187 | The Lambda pulls in the `Sandbox` class handle; the image's `.make()` runtime |
| 188 | is provided on the Stack so it gets built and deployed alongside: |
| 189 | |
| 190 | ```typescript |
| 191 | // alchemy.run.ts |
| 192 | import * as Alchemy from "alchemy"; |
| 193 | import * as AWS from "alchemy/AWS"; |
| 194 | import * as Effect from "effect/Effect"; |
| 195 | import Api from "./api.ts"; |
| 196 | import SandboxLive from "./sandbox.ts"; |
| 197 | |
| 198 | export default Alchemy.Stack( |
| 199 | "MicrovmApp", |
| 200 | { providers: AWS.providers(), state: AWS.state() }, |
| 201 | Effect.gen(function* () { |
| 202 | const api = yield* Api; |
| 203 | return { url: api.functionUrl.as<string>() }; |
| 204 | }).pipe(Effect.provide(SandboxLive)), |
| 205 | ); |
| 206 | ``` |
| 207 | |
| 208 | ## Bring your own Dockerfile |
| 209 | |
| 210 | For non-TypeScript workloads, point `context` at a directory containing a |
| 211 | Dockerfile (built `FROM public.ecr.aws/lambda/microvms:al2023-minimal`) and |
| 212 | the files it copies. Alchemy zips and uploads the directory; AWS runs the |
| 213 | Dockerfile server-side. There is no in-VM Effect runtime to bind, so both |
| 214 | the bare build Role and the image live directly in the Stack's `Effect.gen`: |
| 215 | |
| 216 | ```typescript |
| 217 | // alchemy.run.ts |
| 218 | import * as Alchemy from "alchemy"; |
| 219 | import * as AWS from "alchemy/AWS"; |
| 220 | import * as Effect from "effect/Effect"; |
| 221 | |
| 222 | export default Alchemy.Stack( |
| 223 | "FlaskMicrovm", |
| 224 | { providers: AWS.providers(), state: AWS.state() }, |
| 225 | Effect.gen(function* () { |
| 226 | const buildRole = yield* AWS.IAM.Role("ExternalBuildRole", {}); |
| 227 | const image = yield* AWS.Lambda.MicrovmImage("Flask", { |
| 228 | context: `${import.meta.dirname}/app`, // dir with Dockerfile + app.py |
| 229 | buildRole, |
| 230 | }); |
| 231 | return { imageArn: image.imageArn.as<string>() }; |
| 232 | }), |
| 233 | ); |
| 234 | ``` |
| 235 | |
| 236 | Re-deploys only trigger a new build when the artifact's content hash or a |
| 237 | build-affecting prop changes; otherwise the image is left untouched. |
| 238 | |
| 239 | ## Where next |
| 240 | |
| 241 | - [Lambda](/aws/compute/lambda) — the Function model the orchestrator is built on. |
| 242 | - [MicrovmImage API reference](/providers/aws/lambda/microvmimage) — all props, |
| 243 | build modes, sizing, logging, and VPC egress via `NetworkConnector`. |
| 244 | - [RunMicrovm](/providers/aws/lambda/runmicrovm) and the other per-instance |
| 245 | lifecycle bindings. |