1/**
2 * A "guestbook" app on GKE Autopilot that exercises the full Kubernetes
3 * container surface:
4 *
5 * - a stack-owned Autopilot cluster + Firestore database + a namespace
6 * applied as a raw manifest via `Kubernetes.Manifest` (`src/infra.ts`),
7 * - `Api` — an effectful `Kubernetes.Deployment` (bundled `main:` image source)
8 * behind an external Network Load Balancer, with a Firestore binding
9 * granted to its Kubernetes ServiceAccount's Workload Identity principal,
10 * plus the typed `podTemplate` escape hatch (`src/Api.ts`),
11 * - `Web` — an EXTERNAL `Kubernetes.Deployment` (registry `image:` source, no
12 * Effect runtime in the container), nginx behind its own load balancer,
13 * - `SeedJob` — an inline-effect one-shot `Kubernetes.Job` (`{ run }`) that
14 * seeds the guestbook database when the batch/v1 Job is applied on deploy
15 * (`src/SeedJob.ts`).
16 */
17import * as Alchemy from "alchemy";
18import * as GCP from "alchemy/GCP";
19import * as Kubernetes from "alchemy/Kubernetes";
20import * as Effect from "effect/Effect";
21import * as Layer from "effect/Layer";
22import ApiLive, { Api } from "./src/Api.ts";
23import {
24 EntriesDatabase,
25 GuestbookCluster,
26 GuestbookNamespace,
27} from "./src/infra.ts";
28import SeedJob from "./src/SeedJob.ts";
29
30export default Alchemy.Stack(
31 "GcpGkeExample",
32 {
33 providers: Layer.mergeAll(GCP.providers(), Kubernetes.providers()),
34 state: Alchemy.localState(),
35 },
36 Effect.gen(function* () {
37 const cluster = yield* GuestbookCluster;
38 const database = yield* EntriesDatabase;
39 const ns = yield* GuestbookNamespace;
40
41 // ── Api — effectful server in the TAGGED form (bundled `main:`).
42 // `ApiLive` (the `Api.make(...)` Layer, provided below) carries the
43 // props + init program; its init declares the Firestore binding whose
44 // grant lands on the Deployment's Workload Identity principal; see
45 // src/Api.ts.
46 const api = yield* Api;
47
48 // ── Web — EXTERNAL deployment: a pre-built registry image (mirrored
49 // into Artifact Registry), no Effect runtime in the container. GKE
50 // provisions a load balancer for the LoadBalancer Service; port 80,
51 // so `web.url` carries no port suffix.
52 const web = yield* Kubernetes.Deployment("Web", {
53 cluster,
54 image: "nginx:1.27",
55 namespace: ns.name,
56 replicas: 2,
57 port: 80,
58 serviceType: "LoadBalancer",
59 resources: {
60 requests: { cpu: "250m", memory: "512Mi" },
61 limits: { cpu: "250m", memory: "512Mi" },
62 },
63 });
64
65 // ── SeedJob — inline-effect one-shot Job; runs to completion on deploy
66 // (Kubernetes runs a batch/v1 Job as soon as it is applied).
67 const seedJob = yield* SeedJob;
68
69 return {
70 clusterId: cluster.clusterId,
71 clusterName: cluster.name,
72 // Full URL including the Service port (http://<ip>:3000).
73 apiUrl: api.url,
74 webUrl: web.url,
75 databaseName: database.name,
76 namespace: ns.name,
77 apiServiceAccount: api.serviceAccountName,
78 seedJobName: seedJob.jobName,
79 seedJobServiceAccount: seedJob.serviceAccountName,
80 };
81 }).pipe(Effect.provide(ApiLive)),
82);