1# GCP GKE Example
2
3A "guestbook" app on GKE Autopilot, fully TypeScript-driven — no YAML, no
4`kubectl apply`, no Helm, no Google service accounts.
5
6- [`src/infra.ts`](./src/infra.ts) — the shared infrastructure: a
7 `GCP.Container.Cluster` with `autopilot: true` (Google provisions and
8 scales the nodes; Workload Identity Federation is always on), a named
9 Firestore Native database, and a namespace applied as a raw manifest via
10 `Kubernetes.Manifest`.
11- [`src/Api.ts`](./src/Api.ts) — an effectful `Kubernetes.Deployment` in the
12 tagged form (`Api.make(props, impl)`): an Effect HTTP server bundled into a
13 generated image (pushed to a per-workload Artifact Registry repository),
14 exposed through an external load balancer, with a Firestore binding
15 granted to the Deployment's Kubernetes ServiceAccount principal. Includes
16 the typed `podTemplate` escape hatch.
17- [`src/SeedJob.ts`](./src/SeedJob.ts) — an inline-effect one-shot
18 `Kubernetes.Job` (`{ run }`) that seeds the guestbook when the Job is
19 applied on deploy.
20- [`alchemy.run.ts`](./alchemy.run.ts) — thin composition: yields the shared
21 infra, the tagged `Api` (via `Effect.provide(ApiLive)`), an EXTERNAL
22 nginx `Kubernetes.Deployment` (registry `image:` source), and the `SeedJob`.
23
24## Bindings and IAM
25
26GCP bindings on a GKE workload grant IAM directly to the workload's
27Kubernetes ServiceAccount (KSA) through
28[Workload Identity Federation for GKE](https://cloud.google.com/kubernetes-engine/docs/concepts/workload-identity):
29
30```
31principal://iam.googleapis.com/projects/<PROJECT_NUMBER>/locations/global/workloadIdentityPools/<PROJECT>.svc.id.goog/subject/ns/guestbook/sa/<ksa>
32```
33
34Inside the pod, the GKE metadata server hands out tokens for that principal,
35so the bundled program authenticates with no keys and no Google service
36account.
37
38| Workload | Binding | IAM granted to the workload's KSA principal |
39| --------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------- |
40| `Api` | `GCP.Firestore.ReadWriteDatabase(EntriesDatabase)` + `ReadWriteDatabaseHttp` | `roles/datastore.user` on the project, conditioned to the database |
41| `SeedJob` | `GCP.Firestore.WriteDatabase(EntriesDatabase)` + `WriteDatabaseHttp` | `roles/datastore.user` on the project, conditioned to the database |
42| `Web` | — | — |
43
44Firestore databases have no resource-level IAM policy, so the grant is
45project-level under an IAM Condition on the database name. It can take a
46few minutes to propagate after the first deploy; the Job retries with
47backoff and the API answers `500` until then. Removing a binding revokes its
48grant on the next deploy; destroying a workload revokes all of them.
49
50To run a workload as an existing Google service account instead, set
51`identity: { gcpServiceAccount: "<gsa-email>" }` on it: Alchemy annotates the
52KSA, grants its principal `roles/iam.workloadIdentityUser` on the GSA, and
53lands the binding grants on the GSA.
54
55## Commands
56
57```sh
58bun install
59bun run --filter gcp-gke deploy
60bun run --filter gcp-gke destroy
61```
62
63Docker must be running (images are built and mirrored locally). The
64Autopilot cluster takes ~5–10 minutes to provision.
65
66## Try it
67
68```sh
69# outputs: apiUrl (note the :3000 — the load balancer listens on the Service port), webUrl
70curl "$apiUrl/entries" # seeded by SeedJob
71curl -X POST "$apiUrl/entries?author=you&message=hello"
72curl "$apiUrl/entries/ada"
73curl "$webUrl" # external nginx deployment
74```
75
76## Live test
77
78```sh
79ALCHEMY_PROFILE=<name> bun test --timeout 3000000
80```
81
82The test deploys the stack, checks the Firestore grants landed on both KSA
83principals (project IAM policy, conditioned to the database), waits for the
84seed entries, signs and reads back an entry through the API (and out of band
85in Firestore), fetches the nginx page, then destroys the stack and checks the
86grants are revoked, no load balancer is left behind, and the cluster and
87database are gone.
88
89## Optional Inspection
90
91To inspect the cluster manually after deploy:
92
93```sh
94gcloud container clusters get-credentials <clusterId output> --region us-central1
95kubectl get pods -n guestbook
96```