| 1 | import type { Credentials } from "@distilled.cloud/aws/Credentials"; |
| 2 | import type { Region } from "@distilled.cloud/aws/Region"; |
| 3 | import type * as microvms from "@distilled.cloud/aws/lambda-microvms"; |
| 4 | |
| 5 | import * as Effect from "effect/Effect"; |
| 6 | import type * as Bundle from "../../Bundle/Bundle.ts"; |
| 7 | import { Platform } from "../../Platform.ts"; |
| 8 | import type { Main } from "../../Platform.ts"; |
| 9 | import type { Resource } from "../../Resource.ts"; |
| 10 | import type * as Server from "../../Server/index.ts"; |
| 11 | import type { PolicyStatement } from "../IAM/Policy.ts"; |
| 12 | import type { Role } from "../IAM/Role.ts"; |
| 13 | import type { Providers } from "../Providers.ts"; |
| 14 | import { |
| 15 | makeMicrovmRuntimeContext, |
| 16 | MicrovmImageTypeId, |
| 17 | } from "./MicrovmRuntimeContext.ts"; |
| 18 | |
| 19 | /** |
| 20 | * The IAM permissions a build role needs: read the code artifact from the |
| 21 | * Assets bucket(s) and write build logs to CloudWatch. Used to auto-grant a |
| 22 | * {@link Role} passed as `buildRole`. |
| 23 | */ |
| 24 | const buildRolePolicyStatements: PolicyStatement[] = [ |
| 25 | { |
| 26 | Effect: "Allow", |
| 27 | Action: ["s3:GetObject"], |
| 28 | Resource: ["arn:aws:s3:::alchemy-assets-*-an/*"], |
| 29 | }, |
| 30 | { |
| 31 | Effect: "Allow", |
| 32 | Action: [ |
| 33 | "logs:CreateLogGroup", |
| 34 | "logs:CreateLogStream", |
| 35 | "logs:PutLogEvents", |
| 36 | ], |
| 37 | Resource: ["arn:aws:logs:*:*:log-group:/aws/lambda/microvms/*"], |
| 38 | }, |
| 39 | ]; |
| 40 | |
| 41 | export { MicrovmImageTypeId } from "./MicrovmRuntimeContext.ts"; |
| 42 | |
| 43 | export interface MicrovmImageProps { |
| 44 | /** |
| 45 | * The name of the MicroVM image. Must be 1-64 characters of letters, |
| 46 | * numbers, hyphens, or underscores. If omitted, a unique name is generated. |
| 47 | * Changing the name replaces the image. |
| 48 | */ |
| 49 | name?: string; |
| 50 | |
| 51 | /** |
| 52 | * **Effectful mode.** Entrypoint file for an Effect-native HTTP server, |
| 53 | * typically `import.meta.filename`. Alchemy bundles it (with its bindings), |
| 54 | * generates a Dockerfile, zips the result, and uploads it as the |
| 55 | * `codeArtifact`. |
| 56 | */ |
| 57 | main?: string; |
| 58 | |
| 59 | /** |
| 60 | * **External mode.** A build-context directory containing your Dockerfile and |
| 61 | * the files it copies. Alchemy zips and uploads it; AWS runs your Dockerfile. |
| 62 | * @default "./" |
| 63 | */ |
| 64 | context?: string; |
| 65 | |
| 66 | /** |
| 67 | * The Dockerfile. In effectful mode this is an inline base string (defaults |
| 68 | * to `FROM public.ecr.aws/lambda/microvms:al2023-minimal`). In external mode |
| 69 | * it is a path relative to {@link context} (defaults to `<context>/Dockerfile`). |
| 70 | */ |
| 71 | dockerfile?: string; |
| 72 | |
| 73 | /** |
| 74 | * The port the in-VM HTTP server listens on (effectful mode). |
| 75 | * @default 8080 |
| 76 | */ |
| 77 | port?: number; |
| 78 | |
| 79 | /** |
| 80 | * The JS runtime to bundle for (effectful mode). |
| 81 | * @default "node" |
| 82 | */ |
| 83 | runtime?: "bun" | "node"; |
| 84 | |
| 85 | /** |
| 86 | * Extra module ids to leave external when bundling (effectful mode). |
| 87 | */ |
| 88 | external?: string[]; |
| 89 | |
| 90 | /** |
| 91 | * Bundler configuration for {@link main} (effectful mode). Unused |
| 92 | * code is tree-shaken. `effect`, alchemy, and `@distilled.cloud` are |
| 93 | * marked pure so unused parts prune more aggressively. List extra |
| 94 | * packages with `pure.packages`, or disable with `pure: false`. |
| 95 | */ |
| 96 | build?: Bundle.BundleConfig; |
| 97 | |
| 98 | /** |
| 99 | * The base MicroVM image to build on top of — either its ARN or another |
| 100 | * {@link MicrovmImage} instance. If omitted, the latest AWS-managed `al2023` |
| 101 | * base is used (discover bases with {@link microvms.listManagedMicrovmImages}). |
| 102 | */ |
| 103 | baseImage?: string | MicrovmImage; |
| 104 | |
| 105 | /** |
| 106 | * The specific version of the base MicroVM image. |
| 107 | */ |
| 108 | baseImageVersion?: string; |
| 109 | |
| 110 | /** |
| 111 | * The IAM role Lambda assumes to build the image (read the code artifact, |
| 112 | * write build logs) — either its ARN or an {@link Role} instance. When you |
| 113 | * pass a `Role`, the required S3 (Assets bucket) and CloudWatch-logs |
| 114 | * permissions are granted to it automatically via a binding, so you don't |
| 115 | * have to write the inline policy yourself. Required for all builds. |
| 116 | */ |
| 117 | buildRole?: string | Role; |
| 118 | |
| 119 | /** |
| 120 | * **Prebuilt mode.** A code artifact (S3 zip path or ECR image URI) to use |
| 121 | * as-is, instead of bundling (`main`) or building a context (`context`). |
| 122 | */ |
| 123 | codeArtifact?: microvms.CodeArtifact; |
| 124 | |
| 125 | /** |
| 126 | * A description of the image version. |
| 127 | */ |
| 128 | description?: string; |
| 129 | |
| 130 | /** |
| 131 | * Logging configuration for the MicroVM runtime/build. Specify exactly one of |
| 132 | * `cloudWatch` or `disabled`. |
| 133 | */ |
| 134 | logging?: microvms.Logging; |
| 135 | |
| 136 | /** |
| 137 | * The names/ARNs of egress network connectors available to the MicroVM at |
| 138 | * runtime (see {@link NetworkConnector}). |
| 139 | */ |
| 140 | egressNetworkConnectors?: string[]; |
| 141 | |
| 142 | /** |
| 143 | * The CPU configurations the image supports. |
| 144 | */ |
| 145 | cpuConfigurations?: microvms.CpuConfiguration[]; |
| 146 | |
| 147 | /** |
| 148 | * The resource requirements (e.g. minimum memory) for the MicroVM. |
| 149 | */ |
| 150 | resources?: microvms.Resources[]; |
| 151 | |
| 152 | /** |
| 153 | * Additional OS capabilities granted to the MicroVM runtime. |
| 154 | */ |
| 155 | additionalOsCapabilities?: microvms.Capability[]; |
| 156 | |
| 157 | /** |
| 158 | * Lifecycle hook configuration for the MicroVM and its image. |
| 159 | */ |
| 160 | hooks?: microvms.Hooks; |
| 161 | |
| 162 | /** |
| 163 | * Environment variables set in the MicroVM runtime environment. Capability |
| 164 | * bindings (e.g. S3/DynamoDB clients) also contribute entries here during |
| 165 | * plan; all of them are written to the image's environment. |
| 166 | */ |
| 167 | env?: Record<string, any>; |
| 168 | |
| 169 | /** |
| 170 | * Tags to apply to the image. |
| 171 | */ |
| 172 | tags?: Record<string, string>; |
| 173 | |
| 174 | /** |
| 175 | * @internal Platform-managed: the bundled runtime entrypoint. |
| 176 | */ |
| 177 | exports?: string[] | Record<string, any>; |
| 178 | |
| 179 | /** |
| 180 | * @internal Platform-managed: signals a non-Effect-native (external) image. |
| 181 | */ |
| 182 | isExternal?: boolean; |
| 183 | } |
| 184 | |
| 185 | export interface MicrovmImage extends Resource< |
| 186 | "AWS.Lambda.MicrovmImage", |
| 187 | MicrovmImageProps, |
| 188 | { |
| 189 | /** The ARN of the MicroVM image. */ |
| 190 | imageArn: string; |
| 191 | /** The name of the MicroVM image. */ |
| 192 | name: string; |
| 193 | /** The current state of the image (e.g. `CREATED`, `CREATING`). */ |
| 194 | state: microvms.MicrovmImageState; |
| 195 | /** The image version produced by the most recent build. */ |
| 196 | imageVersion?: string; |
| 197 | /** The latest version that built successfully. */ |
| 198 | latestActiveImageVersion?: string; |
| 199 | /** The latest version that failed to build, if any. */ |
| 200 | latestFailedImageVersion?: string; |
| 201 | /** When the image was created (ISO 8601). */ |
| 202 | createdAt?: string; |
| 203 | /** When the image was last updated (ISO 8601). */ |
| 204 | updatedAt?: string; |
| 205 | /** |
| 206 | * The resolved code artifact and its build identity hash. `contentHash` |
| 207 | * covers only the bundled program / packaged context (no props), so a |
| 208 | * content-only edit is detectable at plan time before the props resolve. |
| 209 | */ |
| 210 | codeArtifact?: { uri?: string; hash?: string; contentHash?: string }; |
| 211 | }, |
| 212 | { |
| 213 | env?: Record<string, any>; |
| 214 | policyStatements?: PolicyStatement[]; |
| 215 | }, |
| 216 | Providers |
| 217 | > {} |
| 218 | |
| 219 | export type MicrovmImageServices = Credentials | Region; |
| 220 | export type MicrovmImageShape = Main<MicrovmImageServices>; |
| 221 | |
| 222 | /** |
| 223 | * A Lambda **MicroVM image** — a Firecracker snapshot that boots a fully |
| 224 | * initialized application in milliseconds. The model is *image-then-launch*: |
| 225 | * you create an image once (this resource), then launch isolated, stateful |
| 226 | * MicroVM instances from it at runtime (one per end-user/session) with the |
| 227 | * {@link RunMicrovm} binding from a Lambda Function. |
| 228 | * |
| 229 | * ### How the build works <!-- api-prose --> |
| 230 | * |
| 231 | * The build runs **server-side on AWS** — there is no local Docker. You supply |
| 232 | * a *code artifact* (a zip of a Dockerfile + your code) and a *base image* |
| 233 | * (`baseImage`); AWS runs your Dockerfile on top of the base, initializes |
| 234 | * the app, and takes a Firecracker snapshot. The build is asynchronous: the |
| 235 | * provider uploads the artifact, calls `CreateMicrovmImage`/`UpdateMicrovmImage`, |
| 236 | * and polls until the image reaches `CREATED`/`UPDATED` (or surfaces the build |
| 237 | * failure). Build logs stream to CloudWatch at `/aws/lambda/microvms/<name>`. |
| 238 | * |
| 239 | * Alchemy produces the code artifact for you in three ways, selected by which |
| 240 | * prop you set (`main` → `context` → `codeArtifact.uri`): |
| 241 | * |
| 242 | * - **Effectful** (`main`): write the in-VM HTTP server in TypeScript as an |
| 243 | * Effect. Alchemy bundles it (with its capability bindings), generates a |
| 244 | * Dockerfile on the MicroVM base, zips both, and uploads to the Assets bucket. |
| 245 | * - **External** (`context`/`dockerfile`): bring your own Dockerfile + build |
| 246 | * context (any language). Alchemy zips the directory and uploads it. |
| 247 | * - **Prebuilt** (`codeArtifact.uri`): point at an existing S3 zip or ECR image |
| 248 | * URI; nothing is built or uploaded. |
| 249 | * |
| 250 | * Re-deploys only trigger a new build when the artifact's content hash or a |
| 251 | * build-affecting prop changes; otherwise the image is left untouched. |
| 252 | * |
| 253 | * ### Prerequisites <!-- api-prose --> |
| 254 | * |
| 255 | * - A **build role** (`buildRole`) Lambda assumes to read the code artifact and |
| 256 | * write build logs. Pass a {@link Role} instance and the required permissions |
| 257 | * are granted automatically — see the example below. |
| 258 | * - A **bootstrapped Assets bucket** (`alchemy provider aws bootstrap`) for effectful / |
| 259 | * external modes, which upload the artifact to S3. |
| 260 | * - The account must be **onboarded to the Lambda MicroVM preview**. |
| 261 | * |
| 262 | * |
| 263 | * ### Creating the Build Role |
| 264 | * Pass a bare {@link Role} as `buildRole` and the MicroVM image grants |
| 265 | * everything it needs via a binding: the trust policy (so Lambda can assume it) |
| 266 | * plus the S3 (Assets bucket) and CloudWatch-logs permissions (folded into an |
| 267 | * `alchemy-bindings` inline policy). You don't write any policy yourself. |
| 268 | * **Example:** Bare build role |
| 269 | * ```typescript |
| 270 | * const buildRole = yield* AWS.IAM.Role("MicrovmBuildRole", {}); |
| 271 | * |
| 272 | * // Pass the role instance; trust + permissions are attached for you. |
| 273 | * const image = yield* AWS.Lambda.MicrovmImage("Sandbox", { |
| 274 | * main: import.meta.filename, |
| 275 | * buildRole, |
| 276 | * }); |
| 277 | * ``` |
| 278 | * |
| 279 | * ### Effectful MicroVMs |
| 280 | * Write the in-VM server in TypeScript. Alchemy bundles `main` and bakes it into |
| 281 | * the image; the server listens on `port` (default 8080), which becomes the |
| 282 | * MicroVM `endpoint`. |
| 283 | * **Example:** In-VM HTTP server (single file) |
| 284 | * ```typescript |
| 285 | * export default class Sandbox extends AWS.Lambda.MicrovmImage<Sandbox>()( |
| 286 | * "Sandbox", |
| 287 | * { main: import.meta.filename, buildRole }, |
| 288 | * Effect.gen(function* () { |
| 289 | * return { |
| 290 | * fetch: Effect.gen(function* () { |
| 291 | * const request = yield* HttpServerRequest; |
| 292 | * return HttpServerResponse.text(`hello from ${request.url}`); |
| 293 | * }), |
| 294 | * }; |
| 295 | * }), |
| 296 | * ) {} |
| 297 | * ``` |
| 298 | * |
| 299 | * **Example:** With capability bindings and env vars |
| 300 | * ```typescript |
| 301 | * export default class Sandbox extends AWS.Lambda.MicrovmImage<Sandbox>()( |
| 302 | * "Sandbox", |
| 303 | * { |
| 304 | * main: import.meta.filename, |
| 305 | * buildRole, |
| 306 | * runtime: "bun", |
| 307 | * env: { LOG_LEVEL: "info" }, |
| 308 | * }, |
| 309 | * Effect.gen(function* () { |
| 310 | * // bindings are bundled into the image and resolved at runtime |
| 311 | * const getObject = yield* AWS.S3.GetObject(bucket); |
| 312 | * return { |
| 313 | * fetch: Effect.gen(function* () { |
| 314 | * const obj = yield* getObject({ key: "data.json" }); |
| 315 | * return HttpServerResponse.json(yield* obj.json); |
| 316 | * }), |
| 317 | * }; |
| 318 | * }).pipe(Effect.provide(AWS.S3.GetObjectHttp)), |
| 319 | * ) {} |
| 320 | * ``` |
| 321 | * |
| 322 | * **Example:** Class + `.make()` (two files, for a Lambda orchestrator) |
| 323 | * When a Lambda Function imports the image to bind its instance operations, keep |
| 324 | * the class (a typed handle) and the `.make()` runtime in separate files so the |
| 325 | * orchestrator's bundle doesn't pull in the VM's runtime deps. |
| 326 | * ```typescript |
| 327 | * // sandbox.ts — imported by the orchestrator |
| 328 | * export class Sandbox extends AWS.Lambda.MicrovmImage<Sandbox>()("Sandbox") {} |
| 329 | * |
| 330 | * // sandbox.live.ts — provided on the Stack; bundled into the image. |
| 331 | * // Must be the `default` export: the bundler resolves the image entrypoint |
| 332 | * // from the code artifact's default export. |
| 333 | * export default Sandbox.make( |
| 334 | * { main: import.meta.filename, buildRole }, |
| 335 | * Effect.gen(function* () { |
| 336 | * return { fetch: Effect.gen(function* () { |
| 337 | * return HttpServerResponse.text("ok"); |
| 338 | * }) }; |
| 339 | * }), |
| 340 | * ); |
| 341 | * ``` |
| 342 | * |
| 343 | * ### Tagged RPC |
| 344 | * Beyond (or instead of) a raw `fetch` handler, an image can expose a typed |
| 345 | * **RPC `Shape`** as the second type parameter. The in-VM runtime serves those |
| 346 | * methods over an `/__rpc__/*` protocol and falls through to `fetch` for every |
| 347 | * other request, so an image can offer BOTH a typed RPC surface and ordinary |
| 348 | * HTTP routes. A caller gets a fully-typed client with {@link connectMicrovm}: |
| 349 | * value methods `yield*` as `Effect`s, streaming methods pipe as `Stream`s. |
| 350 | * **Example:** Define a tagged-RPC image (RPC + fetch) |
| 351 | * ```typescript |
| 352 | * // sandbox.ts — the typed handle imported by the orchestrator Lambda |
| 353 | * export class Sandbox extends AWS.Lambda.MicrovmImage< |
| 354 | * Sandbox, |
| 355 | * { hello: (message: string) => Effect.Effect<string> } |
| 356 | * >()("Sandbox") {} |
| 357 | * |
| 358 | * // sandbox.live.ts — provided on the Stack; bundled into the image (default export) |
| 359 | * export default Sandbox.make( |
| 360 | * { main: import.meta.filename, buildRole }, |
| 361 | * Effect.gen(function* () { |
| 362 | * return { |
| 363 | * // RPC method — reached with `connectMicrovm` below |
| 364 | * hello: (message: string) => Effect.succeed(`hello, ${message}!`), |
| 365 | * // raw HTTP route — reached with a plain HTTPS request to the endpoint |
| 366 | * fetch: Effect.gen(function* () { |
| 367 | * const request = yield* HttpServerRequest; |
| 368 | * const url = new URL(request.url, "http://microvm"); |
| 369 | * return HttpServerResponse.json({ path: url.pathname }); |
| 370 | * }), |
| 371 | * }; |
| 372 | * }), |
| 373 | * ); |
| 374 | * ``` |
| 375 | * |
| 376 | * **Example:** Call the RPC method from a Lambda |
| 377 | * `connectMicrovm` builds the typed stub over an `HttpClient` (provide |
| 378 | * `FetchHttpClient.layer` for the request scope), pointing at the running |
| 379 | * MicroVM's `endpoint` and authenticating with the `authToken` headers. |
| 380 | * ```typescript |
| 381 | * const vm = yield* runMicrovm({}); |
| 382 | * // ...wait until `vm.state` is RUNNING (poll `getMicrovm`)... |
| 383 | * const { authToken } = yield* createAuthToken({ |
| 384 | * microvmIdentifier: vm.microvmId, |
| 385 | * expirationInMinutes: 5, |
| 386 | * allowedPorts: [{ port: 8080 }], // the in-VM server's port |
| 387 | * }); |
| 388 | * |
| 389 | * const sandbox = yield* AWS.Lambda.connectMicrovm(Sandbox, { |
| 390 | * endpoint: vm.endpoint, |
| 391 | * authToken, |
| 392 | * }); |
| 393 | * const reply = yield* sandbox.hello("world"); // "hello, world!" |
| 394 | * ``` |
| 395 | * |
| 396 | * **Example:** Call the same MicroVM's `fetch` route directly |
| 397 | * For the raw HTTP path, send the auth token as request headers via |
| 398 | * {@link microvmAuthHeaders}. |
| 399 | * ```typescript |
| 400 | * const client = yield* HttpClient.HttpClient; |
| 401 | * const res = yield* client.get(`https://${vm.endpoint}/echo?message=hi`, { |
| 402 | * headers: AWS.Lambda.microvmAuthHeaders(authToken), |
| 403 | * }); |
| 404 | * const body = yield* res.json; |
| 405 | * ``` |
| 406 | * |
| 407 | * ### Bundling & Tree-shaking |
| 408 | * `main` is bundled with rolldown at deploy time. Unused code is |
| 409 | * tree-shaken. `effect`, alchemy, and `@distilled.cloud` are marked |
| 410 | * pure so unused parts prune more aggressively. Your app is not |
| 411 | * marked pure. |
| 412 | * |
| 413 | * **Example:** Mark additional packages as pure |
| 414 | * Only list packages with no top-level side effects. |
| 415 | * ```typescript |
| 416 | * { |
| 417 | * main: import.meta.url, |
| 418 | * build: { |
| 419 | * pure: { packages: ["my-lib", "@my-scope/*"] }, |
| 420 | * }, |
| 421 | * } |
| 422 | * ``` |
| 423 | * |
| 424 | * **Example:** Turn it off |
| 425 | * ```typescript |
| 426 | * { |
| 427 | * main: import.meta.url, |
| 428 | * build: { pure: false }, |
| 429 | * } |
| 430 | * ``` |
| 431 | * |
| 432 | * ### External Images (your own Dockerfile) |
| 433 | * Bring a build context directory containing a Dockerfile (any language). |
| 434 | * Alchemy zips and uploads it; AWS runs your Dockerfile. Your Dockerfile should |
| 435 | * build on a MicroVM-compatible base (e.g. |
| 436 | * `FROM public.ecr.aws/lambda/microvms:al2023-minimal`). |
| 437 | * **Example:** Flask app from a Dockerfile |
| 438 | * ```typescript |
| 439 | * const image = yield* AWS.Lambda.MicrovmImage("Flask", { |
| 440 | * context: `${import.meta.dirname}/app`, // dir with Dockerfile + app.py |
| 441 | * buildRole, |
| 442 | * }); |
| 443 | * ``` |
| 444 | * **Example:** Custom Dockerfile path within the context |
| 445 | * ```typescript |
| 446 | * const image = yield* AWS.Lambda.MicrovmImage("Worker", { |
| 447 | * context: `${import.meta.dirname}/app`, |
| 448 | * dockerfile: "docker/worker.Dockerfile", // relative to `context` |
| 449 | * buildRole, |
| 450 | * }); |
| 451 | * ``` |
| 452 | * |
| 453 | * ### Prebuilt Artifacts |
| 454 | * Skip the build entirely and point at an artifact you already produced. |
| 455 | * **Example:** From an existing S3 zip |
| 456 | * ```typescript |
| 457 | * const image = yield* AWS.Lambda.MicrovmImage("Prebuilt", { |
| 458 | * codeArtifact: { uri: "s3://my-bucket/app.zip" }, // or an ECR image URI |
| 459 | * buildRole, |
| 460 | * }); |
| 461 | * ``` |
| 462 | * |
| 463 | * ### Sizing and Base Image |
| 464 | * `baseImage` defaults to the latest AWS-managed `al2023` base (discovered |
| 465 | * via `listManagedMicrovmImages`). Override it, and tune CPU/memory, explicitly. |
| 466 | * **Example:** Pin the base image and resources |
| 467 | * ```typescript |
| 468 | * const image = yield* AWS.Lambda.MicrovmImage("Sized", { |
| 469 | * main: import.meta.filename, |
| 470 | * buildRole, |
| 471 | * baseImage: "arn:aws:lambda:us-east-1:aws:microvm-image:al2023-1", |
| 472 | * cpuConfigurations: [{ architecture: "ARM_64" }], |
| 473 | * resources: [{ minimumMemoryInMiB: 2048 }], |
| 474 | * }); |
| 475 | * ``` |
| 476 | * |
| 477 | * ### Logging |
| 478 | * **Example:** Stream runtime + build logs to a custom CloudWatch group |
| 479 | * ```typescript |
| 480 | * const image = yield* AWS.Lambda.MicrovmImage("Logged", { |
| 481 | * main: import.meta.filename, |
| 482 | * buildRole, |
| 483 | * logging: { cloudWatch: { logGroup: "/aws/microvm/my-app" } }, |
| 484 | * // or disable: logging: { disabled: {} } |
| 485 | * }); |
| 486 | * ``` |
| 487 | * |
| 488 | * ### VPC Egress |
| 489 | * Give the MicroVM a managed egress path into your VPC with a |
| 490 | * {@link NetworkConnector} (reference it by ARN). |
| 491 | * **Example:** Image-level egress connector |
| 492 | * ```typescript |
| 493 | * const egress = yield* AWS.Lambda.NetworkConnector("Egress", { |
| 494 | * subnetIds: [subnet.subnetId], |
| 495 | * securityGroupIds: [sg.groupId], |
| 496 | * operatorRole: operatorRole.roleArn, |
| 497 | * }); |
| 498 | * |
| 499 | * const image = yield* AWS.Lambda.MicrovmImage("Connected", { |
| 500 | * main: import.meta.filename, |
| 501 | * buildRole, |
| 502 | * egressNetworkConnectors: [egress.networkConnectorArn], |
| 503 | * }); |
| 504 | * ``` |
| 505 | * |
| 506 | * ### Launching MicroVMs |
| 507 | * The image is just the template. Launch and drive instances from a Lambda |
| 508 | * Function using the per-operation bindings ({@link RunMicrovm}, |
| 509 | * {@link GetMicrovm}, {@link CreateAuthToken}, {@link TerminateMicrovm}, …). |
| 510 | * Each binding's IAM policy is scoped to this image automatically. |
| 511 | * **Example:** Run a MicroVM, call its RPC, and tear it down |
| 512 | * Always terminate the MicroVM you launched — wrap the work in |
| 513 | * `Effect.ensuring` so a failure (or a client retry) never leaks a running |
| 514 | * MicroVM against your account's memory quota. Give the Function a generous |
| 515 | * `timeout` since it waits for the MicroVM to reach RUNNING in-line. |
| 516 | * ```typescript |
| 517 | * export default class Api extends AWS.Lambda.Function<Api>()( |
| 518 | * "Api", |
| 519 | * { main: import.meta.filename, functionUrl: true, timeout: Duration.seconds(120) }, |
| 520 | * Effect.gen(function* () { |
| 521 | * const runMicrovm = yield* AWS.Lambda.RunMicrovm(Sandbox); |
| 522 | * const getMicrovm = yield* AWS.Lambda.GetMicrovm(Sandbox); |
| 523 | * const createAuthToken = yield* AWS.Lambda.CreateAuthToken(Sandbox); |
| 524 | * const terminateMicrovm = yield* AWS.Lambda.TerminateMicrovm(Sandbox); |
| 525 | * return { |
| 526 | * fetch: Effect.gen(function* () { |
| 527 | * const vm = yield* runMicrovm({ |
| 528 | * idlePolicy: { |
| 529 | * maxIdleDurationSeconds: 900, |
| 530 | * suspendedDurationSeconds: 300, |
| 531 | * autoResumeEnabled: true, |
| 532 | * }, |
| 533 | * }); |
| 534 | * return yield* Effect.gen(function* () { |
| 535 | * // wait until the MicroVM is RUNNING before connecting |
| 536 | * yield* getMicrovm({ microvmIdentifier: vm.microvmId }).pipe( |
| 537 | * Effect.flatMap((m) => |
| 538 | * m.state === "RUNNING" |
| 539 | * ? Effect.void |
| 540 | * : Effect.fail(new Error(`microvm ${m.state}`)), |
| 541 | * ), |
| 542 | * Effect.retry({ schedule: Schedule.spaced("2 seconds"), times: 30 }), |
| 543 | * ); |
| 544 | * const { authToken } = yield* createAuthToken({ |
| 545 | * microvmIdentifier: vm.microvmId, |
| 546 | * expirationInMinutes: 5, |
| 547 | * allowedPorts: [{ port: 8080 }], |
| 548 | * }); |
| 549 | * const sandbox = yield* AWS.Lambda.connectMicrovm(Sandbox, { |
| 550 | * endpoint: vm.endpoint, |
| 551 | * authToken, |
| 552 | * }); |
| 553 | * const reply = yield* sandbox.hello("world"); |
| 554 | * return yield* HttpServerResponse.json({ reply }); |
| 555 | * }).pipe( |
| 556 | * // terminate on success OR failure — never leak a running MicroVM |
| 557 | * Effect.ensuring( |
| 558 | * terminateMicrovm({ microvmIdentifier: vm.microvmId }).pipe( |
| 559 | * Effect.ignore, |
| 560 | * ), |
| 561 | * ), |
| 562 | * // the in-VM endpoint calls need an HttpClient for this scope |
| 563 | * Effect.provide(FetchHttpClient.layer), |
| 564 | * ); |
| 565 | * }), |
| 566 | * }; |
| 567 | * }).pipe( |
| 568 | * Effect.provide( |
| 569 | * Layer.mergeAll( |
| 570 | * AWS.Lambda.RunMicrovmHttp, |
| 571 | * AWS.Lambda.GetMicrovmHttp, |
| 572 | * AWS.Lambda.CreateAuthTokenHttp, |
| 573 | * AWS.Lambda.TerminateMicrovmHttp, |
| 574 | * ), |
| 575 | * ), |
| 576 | * ), |
| 577 | * ) {} |
| 578 | * ``` |
| 579 | * |
| 580 | * @resource |
| 581 | */ |
| 582 | export const MicrovmImage: Platform< |
| 583 | MicrovmImage, |
| 584 | MicrovmImageServices, |
| 585 | MicrovmImageShape, |
| 586 | Server.ProcessContext |
| 587 | > = Platform(MicrovmImageTypeId, { |
| 588 | createRuntimeContext: makeMicrovmRuntimeContext, |
| 589 | // When `buildRole` is a Role instance, grant it the build permissions via a |
| 590 | // binding so the user doesn't have to author the inline policy. |
| 591 | onCreate: (_resource, props: MicrovmImageProps) => |
| 592 | props.buildRole && typeof props.buildRole !== "string" |
| 593 | ? props.buildRole |
| 594 | .bind`Allow(${_resource}, AWS.Lambda.MicrovmImage.build)`({ |
| 595 | // Grant the build permissions... |
| 596 | policyStatements: buildRolePolicyStatements, |
| 597 | // ...and the trust statement, so a build role passed as a bare |
| 598 | // `Role` (no `assumeRolePolicyDocument`) can be assumed by Lambda. |
| 599 | assumeRolePolicyStatements: [ |
| 600 | { |
| 601 | Effect: "Allow", |
| 602 | Principal: { Service: "lambda.amazonaws.com" }, |
| 603 | Action: ["sts:AssumeRole"], |
| 604 | }, |
| 605 | ], |
| 606 | }) |
| 607 | : Effect.void, |
| 608 | }); |