Deployment Example: RetroTool
A worked, real-world walkthrough of how the RetroTool app is deployed onto the platform — the actual repos, files, resource names, flow, and commands. Use it as the filled-in companion to the generic deploy tutorial.
1. Overview & the two-repo model
RetroTool is a Meteor (Node.js) application deployed onto the shared GKE clusters. A complete deployment spans two repositories: the application repo (retrotool) which owns the workload chart and CI/CD, and u2i-tenant-infra which owns the surrounding infrastructure (namespaces, Workload Identity bindings and GCS buckets) reconciled onto the cluster by Config Sync.
retrotool → builds the container image, packages the workload Helm chart, and rolls it out through Cloud Build + Cloud Deploy. u2i-tenant-infra → declares the namespaces the app runs in, binds its Kubernetes ServiceAccount to a GCP service account, and provisions the retrotool-assets-* buckets.
2. Foundation registration
The app is registered once in the foundation Terraform, in u2i-gcp-infrastructure/foundation/4-tenants/terramate.tm.hcl, under the u2i tenant's apps map:
retrotool-app = {
display_name = "RetroTool App"
github_repo = "retrotool"
branch = "main"
create_ar_repos = true
approver_group = "gcp-retrotool-prodsupport@u2i.com"
}Applying this (via foundation/4-tenants/app-cicd.tf) generates the full CI/CD scaffolding automatically:
# Cloud Build triggers
retrotool-app-dev-trigger # push to main -> dev
retrotool-app-qa-deployment # version tag v* -> qa + prod
retrotool-app-preview-deployment # pull request -> preview
# Artifact Registry repos (nonprod & prod)
retrotool-app-images retrotool-app-cache
# Service accounts
retrotool-app-ci # Cloud Build: build image, create release
retrotool-app-deploy-sa # Cloud Deploy: execute the rollout
# Cloud Deploy pipelines & targets
retrotool-app-dev-pipeline -> retrotool-app-dev-gke
retrotool-app-qa-prod-pipeline -> retrotool-app-qa-gke, retrotool-app-prod-gke
(prod target requires approval)3. Tenant infrastructure (Config Sync)
The namespaces, Workload Identity bindings and GCS buckets are declared in u2i-tenant-infra and reconciled by Config Sync — not by the app's own pipeline. RetroTool is enabled in k8s/nonprod/rootsync.yaml:
retrotool-app:
enabled: true
namespaces:
- name: retrotool-app # base
- name: retrotool-app-dev
- name: retrotool-app-qa
workloadIdentity:
bindings:
- namespace: retrotool-app-dev
serviceAccount: retrotool-app
- namespace: retrotool-app-qa
serviceAccount: retrotool-app
gke-tenant-storage:
buckets:
- name: retrotool-assets-dev
- name: retrotool-assets-qaThe subchart at chart/charts/apps/charts/retrotool-app/ depends on the shared gke-tenant-foundation (namespaces, RBAC, Gateway) and gke-tenant-storage (bucket + storage.objectUser IAM) charts from oci://europe-west1-docker.pkg.dev/u2i-bootstrap/helm-charts.
4. App repository layout
The retrotool repo carries the workload chart, the Cloud Build definitions, the Skaffold config Cloud Deploy renders with, and the Dockerfile:
retrotool/
├── Dockerfile # multi-stage; prod on node:22-alpine, port 3000
├── helm/retrotool-app/
│ ├── Chart.yaml # depends on gke-tenant-workload 0.11.1
│ ├── values.yaml # chart defaults (incl. gcsfuse.*)
│ ├── values/
│ │ ├── dev.yaml # app.dev.retrotool.u2i.dev
│ │ ├── qa.yaml # app.qa.retrotool.u2i.dev
│ │ ├── preview.yaml # pr-{N}.retrotool.u2i.dev, spot nodes
│ │ └── prod.yaml # app.retrotool.u2i.dev, managed-lb
│ └── templates/
└── deploy/
├── cloudbuild/{dev,qa,preview}.yaml
├── skaffold.yaml # profiles: dev, qa, prod
└── k8s/reposync-{dev,qa,prod}.yaml5. Helm chart & GCSFuse storage
The workload chart declares a single dependency on the shared gke-tenant-workload chart, which provides the ServiceAccount + Workload Identity binding, TLS certificate/listener, ExternalSecrets and KEDA autoscaling:
# helm/retrotool-app/Chart.yaml
dependencies:
- name: gke-tenant-workload
version: 0.11.1
repository: "oci://europe-west1-docker.pkg.dev/u2i-bootstrap/helm-charts"RetroTool stores uploaded board backgrounds in a GCS bucket mounted at /assets through GKE's native GCSFuse CSI driver — no sidecar. The bucket comes from meteorSettings.images.bucket (per environment), and cache sizes are tuned per environment: preview uses a 100Mi file cache on spot nodes, prod uses 1Gi.
# pod annotations (rendered by the chart)
gke-gcsfuse/volumes: "true"
gke-gcsfuse/ephemeral-storage-request: "..."
gke-gcsfuse/ephemeral-storage-limit: "..."
# CSI volume
driver: gcsfuse.csi.storage.gke.io
bucketName: retrotool-assets-dev # dev; -qa / -prod per env
mountOptions: "implicit-dirs,uid=1001,gid=1001"6. CI/CD flow
Three triggers map source events to environments. The image is tagged with the stage and commit SHA and pushed to the per-app Artifact Registry repo:
# push to main -> retrotool-app-dev-trigger (deploy/cloudbuild/dev.yaml)
image: europe-west1-docker.pkg.dev/c-u2i-nonprod/retrotool-app-images/retrotool-app:dev-{SHA}
-> release into retrotool-app-dev-pipeline -> namespace retrotool-app-dev
# tag v* -> retrotool-app-qa-deployment (deploy/cloudbuild/qa.yaml)
image tags: qa-{SHA}/qa-latest (nonprod) + prod-{SHA}/prod-latest (prod)
-> release into retrotool-app-qa-prod-pipeline
-> retrotool-app-qa (auto) -> retrotool-app-prod (manual approval)
# pull request -> retrotool-app-preview-deployment (deploy/cloudbuild/preview.yaml)
image: ...:preview-{SHA}
-> dynamic namespace retrotool-app-pr-{N}, host pr-{N}.retrotool.u2i.dev7. Example commands
Common commands a developer runs against this setup (all credentials are short-lived via ADC / Workload Identity — no keys):
# Get cluster credentials
gcloud container clusters get-credentials u2i-nonprod \
--region europe-west1 --project c-u2i-nonprod
# Inspect Config Sync (tenant infra) status
kubectl get rootsync u2i-infra -n config-management-system
kubectl get pods -n retrotool-app-dev
# Apply foundation / tenant infra changes
terramate run -- terraform apply
# Render the workload chart locally
helm dependency update helm/retrotool-app
helm template retrotool-app helm/retrotool-app -f helm/retrotool-app/values/dev.yaml
# Cut a qa/prod release (normally done by CI on a version tag)
git tag v1.4.0 && git push origin v1.4.08. Reference: repos & key files
| Repo | Path | What |
|---|---|---|
| u2i-gcp-infrastructure | foundation/4-tenants/terramate.tm.hcl | App registration (retrotool-app entry) |
| u2i-gcp-infrastructure | foundation/4-tenants/app-cicd.tf | Generates triggers, SAs, AR repos, pipelines |
| retrotool | helm/retrotool-app/ | Workload Helm chart + per-env values |
| retrotool | deploy/cloudbuild/{dev,qa,preview}.yaml | Cloud Build pipelines |
| retrotool | deploy/skaffold.yaml | Render config used by Cloud Deploy |
| retrotool | deploy/k8s/reposync-{env}.yaml | RepoSync manifests (config-sync source) |
| u2i-tenant-infra | k8s/nonprod/rootsync.yaml | Namespaces, Workload Identity, buckets |
| gke-tenant-charts | charts/gke-tenant-{workload,storage,foundation}/ | Shared charts the app depends on |