1/**
2 * A "guestbook" app on EKS Auto Mode that exercises the full Kubernetes
3 * container surface:
4 *
5 * - a stack-owned network + `compute: "auto"` cluster + DynamoDB table +
6 * a namespace applied as a raw manifest via `Kubernetes.Manifest` (`src/infra.ts`),
7 * - `Api` — an effectful `Kubernetes.Deployment` (bundled `main:` image source)
8 * behind an internet-facing NLB with DynamoDB bindings on the pod-identity
9 * role, plus the typed `podTemplate` escape hatch (`src/Api.ts`),
10 * - `Web` — an EXTERNAL `Kubernetes.Deployment` (registry `image:` source, no
11 * Effect runtime in the container), nginx behind its own NLB,
12 * - `SeedJob` — an inline-effect one-shot `Kubernetes.Job` (`{ run }`) that
13 * seeds the guestbook table when the batch/v1 Job is applied on deploy
14 * (`src/SeedJob.ts`).
15 */
16import * as Alchemy from "alchemy";
17import * as AWS from "alchemy/AWS";
18import * as Kubernetes from "alchemy/Kubernetes";
19import * as Effect from "effect/Effect";
20import * as Layer from "effect/Layer";
21import ApiLive, { Api } from "./src/Api.ts";
22import {
23 EntriesTable,
24 GuestbookCluster,
25 GuestbookNamespace,
26} from "./src/infra.ts";
27import SeedJob from "./src/SeedJob.ts";
28
29export default Alchemy.Stack(
30 "AwsEksExample",
31 {
32 providers: Layer.mergeAll(AWS.providers(), Kubernetes.providers()),
33 state: Alchemy.localState(),
34 },
35 Effect.gen(function* () {
36 const cluster = yield* GuestbookCluster;
37 const table = yield* EntriesTable;
38 const ns = yield* GuestbookNamespace;
39
40 // ── Api — effectful server in the TAGGED form (bundled `main:`).
41 // `ApiLive` (the `Api.make(...)` Layer, provided below) carries the
42 // props + init program; its init declares the DynamoDB bindings that
43 // land on the generated pod-identity role; see src/Api.ts.
44 const api = yield* Api;
45
46 // ── Web — EXTERNAL deployment: a pre-built registry image (mirrored
47 // into ECR), no Effect runtime in the container. Auto Mode's built-in
48 // controller provisions the NLB for the LoadBalancer Service; port 80,
49 // so `web.url` carries no port suffix.
50 const web = yield* Kubernetes.Deployment("Web", {
51 cluster,
52 image: "public.ecr.aws/nginx/nginx:1.27",
53 namespace: ns.name,
54 replicas: 2,
55 port: 80,
56 serviceType: "LoadBalancer",
57 resources: {
58 requests: { cpu: "50m", memory: "64Mi" },
59 limits: { cpu: "250m", memory: "128Mi" },
60 },
61 });
62
63 // ── SeedJob — inline-effect one-shot Job; runs to completion on deploy
64 // (Kubernetes runs a batch/v1 Job as soon as it is applied).
65 const seedJob = yield* SeedJob;
66
67 return {
68 clusterName: cluster.clusterName,
69 // Full URL including the Service port (http://<nlb-hostname>:3000).
70 apiUrl: api.url,
71 webUrl: web.url,
72 tableName: table.tableName,
73 namespace: ns.name,
74 seedJobName: seedJob.jobName,
75 };
76 }).pipe(Effect.provide(ApiLive)),
77);