1import * as AWS from "@/AWS";
2import { ServerHost } from "@/Server/Process.ts";
3import * as Effect from "effect/Effect";
4import * as Ref from "effect/Ref";
5import * as Schedule from "effect/Schedule";
6import { HttpServerRequest } from "effect/http/HttpServerRequest";
7import * as HttpServerResponse from "effect/http/HttpServerResponse";
8
9/**
10 * Alchemy-managed EC2 key pair granting SSH access to the instance. Exported so
11 * the test can resolve it in the same deploy and read its (redacted) private
12 * key — yielding the same logical id returns the same resource.
13 */
14export const keyPair = AWS.EC2.KeyPair("Ec2E2EKeyPair", {
15 keyType: "ed25519",
16});
17
18/**
19 * End-to-end fixture for a hosted `AWS.EC2.Instance`: a long-running server.
20 *
21 * The props Effect provisions the networking (a public-subnet VPC) and the
22 * instance's security group, then launches the instance into it. The program
23 * Effect registers a `ServerHost.run` background loop (the #706 pattern) and
24 * returns a `{ fetch }` handler that the instance's Bun HTTP server serves on
25 * `port`. `/ticks` reports the loop counter so the test can prove the
26 * background loop runs inside the deployed instance.
27 */
28export default class TestInstance extends AWS.EC2.Instance<TestInstance>()(
29 "Ec2E2EInstance",
30 Effect.gen(function* () {
31 // This composition is re-executed inside the deployed instance's bundle:
32 // the AMI lookup is an `Output` resolved at plan/deploy time only, and
33 // resource yields resolve to references at runtime, so no runtime guard
34 // is needed.
35 const network = yield* AWS.EC2.Network("Ec2E2ENetwork", {
36 cidrBlock: "10.81.0.0/16",
37 availabilityZones: 1,
38 });
39 const securityGroup = yield* AWS.EC2.SecurityGroup("Ec2E2ESg", {
40 vpcId: network.vpcId,
41 description: "alchemy ec2 instance e2e",
42 ingress: [
43 {
44 ipProtocol: "tcp",
45 fromPort: 3000,
46 toPort: 3000,
47 cidrIpv4: "0.0.0.0/0",
48 description: "app",
49 },
50 {
51 ipProtocol: "tcp",
52 fromPort: 22,
53 toPort: 22,
54 cidrIpv4: "0.0.0.0/0",
55 description: "ssh",
56 },
57 ],
58 egress: [
59 {
60 ipProtocol: "-1",
61 cidrIpv4: "0.0.0.0/0",
62 description: "all outbound",
63 },
64 ],
65 });
66
67 // An Alchemy-managed EC2 key pair grants SSH access to the instance.
68 const key = yield* keyPair;
69
70 return {
71 main: import.meta.filename,
72 imageId: AWS.EC2.amazonLinux2023(),
73 instanceType: "t3.small",
74 subnetId: network.publicSubnetIds[0],
75 securityGroupIds: [securityGroup.groupId],
76 associatePublicIpAddress: true,
77 port: 3000,
78 keyName: key.keyName,
79 // SSM access so the instance is manageable via Session Manager.
80 roleManagedPolicyArns: [
81 "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore",
82 ],
83 };
84 }),
85 Effect.gen(function* () {
86 const host = yield* ServerHost;
87 const ticks = yield* Ref.make(0);
88
89 // Long-running background loop (the `host.run` pattern from #706).
90 yield* host.run(
91 Ref.update(ticks, (n) => n + 1).pipe(
92 Effect.repeat(Schedule.spaced("1 second")),
93 Effect.asVoid,
94 ),
95 );
96
97 return {
98 fetch: Effect.gen(function* () {
99 const request = yield* HttpServerRequest;
100 const url = new URL(request.url, "http://instance");
101 if (url.pathname === "/health") {
102 return yield* HttpServerResponse.json({ ok: true });
103 }
104 if (url.pathname === "/ticks") {
105 return yield* HttpServerResponse.json({
106 ticks: yield* Ref.get(ticks),
107 });
108 }
109 return HttpServerResponse.text("hello from ec2 instance");
110 }),
111 };
112 }),
113) {}