3. Install Orchestra (Helm)
Step 3 of the deployment sequence. At this point the GKE Standard cluster exists and its system pool is Ready. This page installs the Orchestra platform (API, operator, frontend, bundled oauth2-proxy) via Helm.
The Helm Chart guide is the full values reference; this page is the ordered install procedure and the GKE-Standard-specific blocks. Orchestra installs via Helm per ADR-0003.
Values layering
Section titled “Values layering”The reference deployment layers values files, most-general first (later files win):
-f deploy/charts/orchestra/values.yaml # chart defaults-f deploy/gcp-values.yaml # GCP reference: domain, images, Cloud SQL, oauth2-proxy-f deploy/gcp-values-standard.yaml # GKE Standard: systemPool + operator tierMap-f deploy/gcp-values-secrets.yaml # gitignored: OAuth client id/secretvalues.yaml— chart defaults (cloud-neutral).gcp-values.yaml— committed GCP reference:global.domain, image repositories, the Cloud SQL Auth Proxy sidecar,ingress.tls.existingSecret: orchestra-wildcard-tls, and the bundled"oauth2-proxy"subchart block.gcp-values-standard.yaml— the GKE-Standard-only overlay: thesystemPoolblock and the operatortierMap. Kept separate so Autopilot/kind installs don’t pick it up.gcp-values-secrets.yaml— gitignored; the OAuthclientID/clientSecretfor the oauth2-proxy subchart. Stored in Google Secret Manager.
Fresh-install ordering (this matters)
Section titled “Fresh-install ordering (this matters)”On a fresh cluster the order below avoids the two classic failures — the migrate hook running with no database, and Helm refusing to adopt a pre-existing namespace/secret. (Both are covered in Troubleshooting.)
-
Pre-create the namespace. If you create it yourself (e.g. to attach Workload Identity or secrets first), add Helm-ownership metadata so
helm upgrade --installwill adopt it instead of erroring:Terminal window kubectl create namespace orchestra-systemkubectl label namespace orchestra-system app.kubernetes.io/managed-by=Helmkubectl annotate namespace orchestra-system \meta.helm.sh/release-name=orchestra \meta.helm.sh/release-namespace=orchestra-system -
Make the database reachable BEFORE install. The chart runs
alembic upgrade headas apre-install,pre-upgradeJob; if the DB isn’t reachable the hook fails and the release never proceeds. For Cloud SQL, create the instance/database and confirm the Cloud SQL Auth Proxy path works; for in-cluster Postgres, install and beReadyfirst. Create the DB secret if you use one:Terminal window kubectl create secret generic orchestra-db \--namespace orchestra-system \--from-literal=database-url='postgresql+asyncpg://orchestra:...@host:5432/orchestra' -
Create the OAuth secret (client id/secret + a generated cookie-secret):
Terminal window kubectl create secret generic orchestra-oauth-secrets \--namespace orchestra-system \--from-literal=client-id=<google-client-id> \--from-literal=client-secret=<google-client-secret> \--from-literal="cookie-secret=$(python3 -c 'import secrets; print(secrets.token_hex(16))')" -
Apply the CRDs, then
helm upgrade --install. The CRDs ship as a separate chart so the Workshop CRD is registered before the operator starts:Terminal window kubectl apply -f deploy/charts/orchestra-crds/templates/SHA=$(git rev-parse --short HEAD)helm upgrade --install orchestra ./deploy/charts/orchestra \-n orchestra-system --create-namespace \-f deploy/charts/orchestra/values.yaml \-f deploy/gcp-values.yaml \-f deploy/gcp-values-standard.yaml \-f deploy/gcp-values-secrets.yaml \--set operator.image.tag="$SHA" \--set api.image.tag="$SHA" \--set frontend.image.tag="$SHA" \--waitThis is the same shape as
just deploy-gcp(addjust build-pushfirst, or usejust ship-gcpto build+push+deploy atomically).
Pin platform pods to the system pool
Section titled “Pin platform pods to the system pool”On GKE Standard the platform’s own pods must sit on the tainted, always-on
system pool — not on
scale-to-zero NAP nodes. The chart’s systemPool block adds the
pool=system nodeSelector and the dedicated=system:NoSchedule toleration to
the operator, API, and frontend. It’s off by default, so kind/minikube/
Autopilot installs are unaffected. This lives in gcp-values-standard.yaml:
systemPool: enabled: true nodeSelectorKey: pool nodeSelectorValue: system taintKey: dedicated taintValue: system taintEffect: NoScheduleOperator tier map
Section titled “Operator tier map”Workshop pods get their scheduling from the operator’s config-driven tier map
(ADR-0005), and always carry
cluster-autoscaler.kubernetes.io/safe-to-evict: "false" +
terminationGracePeriodSeconds (default 120) so a session isn’t evicted
mid-workshop by autoscaler consolidation. For GKE, point the tiers at the
tenant-compute ComputeClass (also in gcp-values-standard.yaml):
operator: tierMap: small: { computeClass: tenant-compute } large: { computeClass: tenant-compute } computeClassLabelKey: cloud.google.com/compute-class terminationGracePeriodSeconds: 120An empty tierMap (the default) emits no scheduling constraints, so single-node
and non-GKE clusters keep working. Templates select a tier via their existing
tier: field (small/large).
Verify
Section titled “Verify”kubectl -n orchestra-system get pods -o wide # platform pods on the system nodekubectl -n orchestra-system rollout status deploy/orchestra-apikubectl logs -n orchestra-system job/orchestra-migrate-<revision> # migration succeededLaunch a jupyter and an rstudio session and confirm each lands on a NAP-provisioned tenant node, not the system pool:
kubectl get pods -A -o wide | grep -E 'jupyter|rstudio'kubectl get nodes -l cloud.google.com/compute-class=tenant-computeNext: Ingress, TLS & auth to make it reachable and authenticated over HTTPS.