Skip to content

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.

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 (maincontextcodeArtifact.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.

  • 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.

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,
});

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 orchestrator
export 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");
}) };
}),
);

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 Lambda
export 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;

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,
});

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,
});

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 }],
});
const image = yield* AWS.Lambda.MicrovmImage("Logged", {
main: import.meta.filename,
buildRole,
logging: { cloudWatch: { logGroup: "/aws/microvm/my-app" } },
// or disable: logging: { disabled: {} }
});

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],
});

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,
),
),
),
) {}