| 1 | # GCP GKE Example |
| 2 | |
| 3 | A "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 | |
| 26 | GCP bindings on a GKE workload grant IAM directly to the workload's |
| 27 | Kubernetes ServiceAccount (KSA) through |
| 28 | [Workload Identity Federation for GKE](https://cloud.google.com/kubernetes-engine/docs/concepts/workload-identity): |
| 29 | |
| 30 | ``` |
| 31 | principal://iam.googleapis.com/projects/<PROJECT_NUMBER>/locations/global/workloadIdentityPools/<PROJECT>.svc.id.goog/subject/ns/guestbook/sa/<ksa> |
| 32 | ``` |
| 33 | |
| 34 | Inside the pod, the GKE metadata server hands out tokens for that principal, |
| 35 | so the bundled program authenticates with no keys and no Google service |
| 36 | account. |
| 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 | |
| 44 | Firestore databases have no resource-level IAM policy, so the grant is |
| 45 | project-level under an IAM Condition on the database name. It can take a |
| 46 | few minutes to propagate after the first deploy; the Job retries with |
| 47 | backoff and the API answers `500` until then. Removing a binding revokes its |
| 48 | grant on the next deploy; destroying a workload revokes all of them. |
| 49 | |
| 50 | To run a workload as an existing Google service account instead, set |
| 51 | `identity: { gcpServiceAccount: "<gsa-email>" }` on it: Alchemy annotates the |
| 52 | KSA, grants its principal `roles/iam.workloadIdentityUser` on the GSA, and |
| 53 | lands the binding grants on the GSA. |
| 54 | |
| 55 | ## Commands |
| 56 | |
| 57 | ```sh |
| 58 | bun install |
| 59 | bun run --filter gcp-gke deploy |
| 60 | bun run --filter gcp-gke destroy |
| 61 | ``` |
| 62 | |
| 63 | Docker must be running (images are built and mirrored locally). The |
| 64 | Autopilot 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 |
| 70 | curl "$apiUrl/entries" # seeded by SeedJob |
| 71 | curl -X POST "$apiUrl/entries?author=you&message=hello" |
| 72 | curl "$apiUrl/entries/ada" |
| 73 | curl "$webUrl" # external nginx deployment |
| 74 | ``` |
| 75 | |
| 76 | ## Live test |
| 77 | |
| 78 | ```sh |
| 79 | ALCHEMY_PROFILE=<name> bun test --timeout 3000000 |
| 80 | ``` |
| 81 | |
| 82 | The test deploys the stack, checks the Firestore grants landed on both KSA |
| 83 | principals (project IAM policy, conditioned to the database), waits for the |
| 84 | seed entries, signs and reads back an entry through the API (and out of band |
| 85 | in Firestore), fetches the nginx page, then destroys the stack and checks the |
| 86 | grants are revoked, no load balancer is left behind, and the cluster and |
| 87 | database are gone. |
| 88 | |
| 89 | ## Optional Inspection |
| 90 | |
| 91 | To inspect the cluster manually after deploy: |
| 92 | |
| 93 | ```sh |
| 94 | gcloud container clusters get-credentials <clusterId output> --region us-central1 |
| 95 | kubectl get pods -n guestbook |
| 96 | ``` |