ADR-0008: Cluster provisioning boundary
Status: Accepted Date: 2026-07-02
Context
Section titled “Context”Orchestra now runs on a GKE Standard cluster (ADR-0005). Standing it up raised a repo-ownership question that ADR-0005 did not settle: where does cluster provisioning live relative to the platform? Two framings were floated — leave the cluster’s OpenTofu in the infra repo and assume a cluster exists when installing Orchestra, or move cluster setup into the platform repo.
The question is muddier than a two-way split because three distinct things get conflated into two repos:
- Orchestra the platform — the operator, API, and Helm chart. Cloud-neutral by
design: ADR-0005 made tenant
targeting a config-driven tier map, not GKE constants, and storage/ingress are
already parameterised. Lives in the platform repo (
orchestraplatform). - A generic, reusable cluster module — parameterised OpenTofu for “build a cluster that satisfies Orchestra’s requirements on GKE,” with no project-, backend-, or account-specific values.
- A specific production instance — the actual cluster, wired to a GCP project, a Terraform state backend, Secret Manager, DNS, and neighbouring infrastructure.
The current production instance lives in a private infra monorepo (monode)
alongside the rest of the operators’ infrastructure (shared Terraform state bucket,
Secret Manager, Cloudflare, Postgres, etc.), where items 2 and 3 are currently
merged — the module carries the production project and state backend inline.
Two forces pull in opposite directions:
- Separation of state and lifecycle. A cluster’s Terraform state can destroy the cluster; it must not share a repo (or a review surface) with high-churn application changes. Infra changes rarely and deliberately; the platform ships often. The Orchestra cluster is also genuinely entangled with the infra monorepo’s ecosystem (shared state bucket, secrets, DNS, sibling infra).
- Deployable by others. Orchestra is a Bioconductor community platform meant to be stood up by other organisers at other venues (BioC, CSHL). Cluster provisioning that is only expressible inside a private infra monorepo is not reproducible by them.
Decision
Section titled “Decision”The platform depends on a cluster contract, not a specific cluster; cluster provisioning and its state live in the infra layer, separate from the app repo.
-
Contract, not cluster. The operator and chart target a conformant cluster — an ingress controller, a storage class, node targeting via the config-driven tier map / a
tenant-compute-style ComputeClass, and egress for image pulls — without owning how that cluster is built. This extends ADR-0005’s cloud-neutrality from “the operator emits portable primitives” to “the platform never provisions infrastructure.” -
Provisioning + state live in the infra layer, not the app repo. The specific production instance stays in the infra monorepo (
monode), which owns its Terraform state, backend, and secrets. The platform repo assumes a cluster exists. This keeps the destructive state surface, the slow/deliberate cadence, and the entanglement with neighbouring infra out of the fast-moving app repo. -
just doctoris the contract validator. Rather than documenting cluster requirements only in prose, the platform ships an executable preflight (scripts/doctor.sh) that checks whether a given cluster satisfies Orchestra’s needs (node egress, ephemeral-storage headroom vs. template requests, ingress/cert-manager/oauth2-proxy presence, requests ≤ limits, secrets). The deployment docs carry the generic requirements plus a GKE reference.
Deferred (with an explicit trigger)
Section titled “Deferred (with an explicit trigger)”Extracting the generic module (item 2) into the platform repo — e.g.
orchestraplatform/deploy/tofu/gke-standard — that the infra monorepo consumes
via a versioned source reference, is deliberately deferred, not rejected.
- Trigger to do it: when reproduction by an external operator becomes a real near-term goal rather than a possibility.
- Why it’s safe to defer: the module as written is already ~90% variables (project, network, node privacy, disk size, NAP bounds, balloon shape are all inputs), so genericising it and repointing the infra monorepo at it is a small, bounded change — deferring does not paint us into a corner.
- Why extract there when we do: the cluster contract (the ComputeClass name, node-targeting labels, egress needs) is coupled to what the operator emits, so the reference module should be versioned with the platform and change in lockstep — while the infra monorepo remains the layer that supplies the production project, backend, and secrets.
Recording this as deferred-with-a-trigger is the point of this ADR: leaving the module in the infra monorepo today is a deliberate, revisitable choice, not an oversight, and there is a named condition under which we revisit it.
Consequences
Section titled “Consequences”Positive:
- Clean separation of blast radius, cadence, and state ownership between the platform and its infrastructure.
- The platform stays honestly cloud-neutral: “assumes a cluster, validates it with
just doctor” is a healthy, portable posture that works for GKE, EKS, AKS, or a single-node dev cluster. - The reproducibility gap for external operators is acknowledged and owned, with a concrete, cheap path to close it when needed — not accidental.
Negative / trade-offs:
- Until the trigger fires, the reusable module lives in a private repo, so an external operator reproduces the cluster from the docs + a reference rather than by consuming a published module. This is the deliberately-accepted cost.
- Two repos must stay coherent: a change to what the operator expects of the cluster
(the contract) may require a matching infra change.
just doctoris the guard that surfaces drift.
Not chosen
Section titled “Not chosen”- Cluster IaC in the platform (app) repo — rejected: couples a destructive Terraform state surface and a slow infra cadence to a high-churn app repo, and drags the app repo into the infra monorepo’s state/secret/DNS entanglement.
- A third dedicated infra repo for the module — rejected as over-fragmentation
for an artifact this small; when extraction happens it belongs in-repo under
deploy/tofu/, consumed by the infra monorepo.