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-qa

The 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}.yaml

5. 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.dev

7. 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.0

8. Reference: repos & key files

RepoPathWhat
u2i-gcp-infrastructurefoundation/4-tenants/terramate.tm.hclApp registration (retrotool-app entry)
u2i-gcp-infrastructurefoundation/4-tenants/app-cicd.tfGenerates triggers, SAs, AR repos, pipelines
retrotoolhelm/retrotool-app/Workload Helm chart + per-env values
retrotooldeploy/cloudbuild/{dev,qa,preview}.yamlCloud Build pipelines
retrotooldeploy/skaffold.yamlRender config used by Cloud Deploy
retrotooldeploy/k8s/reposync-{env}.yamlRepoSync manifests (config-sync source)
u2i-tenant-infrak8s/nonprod/rootsync.yamlNamespaces, Workload Identity, buckets
gke-tenant-chartscharts/gke-tenant-{workload,storage,foundation}/Shared charts the app depends on