1import * as AWS from "alchemy/AWS";
2import * as Cloudflare from "alchemy/Cloudflare";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Schedule from "effect/Schedule";
6import * as FetchHttpClient from "effect/http/FetchHttpClient";
7import * as HttpClient from "effect/http/HttpClient";
8import { HttpServerRequest } from "effect/http/HttpServerRequest";
9import * as HttpServerResponse from "effect/http/HttpServerResponse";
10import { PORTS } from "./ports.ts";
11import { ShellMicrovm } from "./ShellImage.ts";
12// <<VM_IMPORTS>>
13// <</VM_IMPORTS>>
14
15/**
16 * The headline cross-cloud resource: a **Cloudflare Worker that mounts an
17 * AWS Lambda MicroVM**.
18 *
19 * A Worker has no AWS execution role, so binding the MicroVM instance
20 * operations makes Alchemy mint an IAM User + AccessKey + assume-role Role
21 * once for this Worker and assume that role at runtime (see
22 * `AWS/Lambda/MicrovmBinding.ts`). Under `alchemy dev` every one of those
23 * calls resolves to the floci emulator from local `workerd`, over the
24 * emulator's own TLS certificate.
25 *
26 * `GET /roundtrip?message=…` boots a MicroVM, waits for it to run, mints a
27 * data-plane auth token, drives BOTH protocols the VM speaks (typed RPC and
28 * a raw HTTPS route), and terminates the VM again — on success or failure.
29 */
30export default class MicrovmWorker extends Cloudflare.Worker<MicrovmWorker>()(
31 "MicrovmWorker",
32 {
33 main: import.meta.url,
34 dev: { port: PORTS.microvm, strictPort: true },
35 },
36 Effect.gen(function* () {
37 const runMicrovm = yield* AWS.Lambda.RunMicrovm(ShellMicrovm);
38 const getMicrovm = yield* AWS.Lambda.GetMicrovm(ShellMicrovm);
39 const createAuthToken = yield* AWS.Lambda.CreateAuthToken(ShellMicrovm);
40 const terminateMicrovm = yield* AWS.Lambda.TerminateMicrovm(ShellMicrovm);
41 // <<VM_BINDINGS>>
42 // <</VM_BINDINGS>>
43
44 return {
45 fetch: Effect.gen(function* () {
46 const request = yield* HttpServerRequest;
47 const url = new URL(request.url, "http://microvm-worker");
48 // <<VM_ROUTES>>
49 // <</VM_ROUTES>>
50
51 if (url.pathname !== "/roundtrip") {
52 return HttpServerResponse.text("ok");
53 }
54 const message = url.searchParams.get("message") ?? "world";
55
56 const vm = yield* runMicrovm({
57 idlePolicy: {
58 maxIdleDurationSeconds: 900,
59 suspendedDurationSeconds: 300,
60 autoResumeEnabled: true,
61 },
62 });
63
64 return yield* Effect.gen(function* () {
65 yield* getMicrovm({ microvmIdentifier: vm.microvmId }).pipe(
66 Effect.flatMap((m) =>
67 m.state === "RUNNING"
68 ? Effect.void
69 : Effect.fail(new Error(`microvm ${m.state}`)),
70 ),
71 Effect.retry({ schedule: Schedule.spaced("2 seconds"), times: 45 }),
72 Effect.orDie,
73 );
74 const { authToken } = yield* createAuthToken({
75 microvmIdentifier: vm.microvmId,
76 expirationInMinutes: 5,
77 allowedPorts: [{ port: 8080 }],
78 });
79
80 // Typed RPC into the VM.
81 const sandbox = yield* AWS.Lambda.connectMicrovm(ShellMicrovm, {
82 endpoint: vm.endpoint,
83 authToken,
84 });
85 const reply = yield* sandbox.hello(message).pipe(
86 Effect.retry({
87 schedule: Schedule.exponential("500 millis"),
88 times: 8,
89 }),
90 Effect.orDie,
91 );
92
93 // Raw HTTPS into the same VM.
94 const client = yield* HttpClient.HttpClient;
95 const headers = AWS.Lambda.microvmAuthHeaders(authToken);
96 const response = yield* client
97 .get(
98 `https://${vm.endpoint}/echo?message=${encodeURIComponent(message)}`,
99 { headers },
100 )
101 .pipe(
102 Effect.retry({
103 schedule: Schedule.exponential("500 millis"),
104 times: 8,
105 }),
106 Effect.orDie,
107 );
108 const echo = (yield* response.json.pipe(Effect.orDie)) as {
109 marker: string;
110 message: string;
111 };
112
113 return yield* HttpServerResponse.json({
114 microvmId: vm.microvmId,
115 reply,
116 marker: echo.marker,
117 echo: echo.message,
118 });
119 }).pipe(
120 Effect.ensuring(
121 terminateMicrovm({ microvmIdentifier: vm.microvmId }).pipe(
122 Effect.ignore,
123 ),
124 ),
125 Effect.provide(FetchHttpClient.layer),
126 );
127 }).pipe(Effect.orDie),
128 };
129 }).pipe(
130 Effect.provide(
131 Layer.mergeAll(
132 AWS.Lambda.RunMicrovmHttp,
133 AWS.Lambda.GetMicrovmHttp,
134 AWS.Lambda.CreateAuthTokenHttp,
135 AWS.Lambda.TerminateMicrovmHttp,
136 // <<VM_LAYERS>>
137 // <</VM_LAYERS>>
138 ),
139 ),
140 ),
141) {}