Docs · Glidepath
ADR-0024: Cluster taxonomy: zone, roles and environment tier
Status: Accepted (2026-10-08). Built: the registry fields, the control plane’s own zone and roles, the
production environment flag and its checks; amended the same day so one fleet file feeds both Glidepath and
Airframe (see “One record per cluster”). Not built: Ground environments on other clusters
(known-gaps.md #39).
Context
Clusters had no declared purpose. “Dev” meant whichever cluster runs the control plane (Tekton, Pipelines-as-Code,
Tekton Results, the relay) and every Ground environment assumed it. “Upper” meant a cluster in the control plane’s
clusters: registry, which holds only a name and a relay secret. Tier (ground/flight) belongs to an environment,
not a cluster: a Flight environment may run on the dev cluster, and prod hosts staging, prod and Backstage.
Two questions had no answer in the platform: what makes a cluster upper, and what stops a production environment from landing somewhere a pipeline can write to directly. The owner raised them while weighing Ground environments on other clusters (envs-overhaul-requirements.md Q3).
Decision
Three separate properties.
| Property | Belongs to | Values | What it decides |
|---|---|---|---|
| zone | cluster | lower, upper |
Which credentials may reach it and how changes arrive |
| roles | cluster | control-plane, workloads, platform-services |
What the platform installs there |
tier (+ production) |
environment | ground/flight; production: true |
How a change reaches the environment |
An upper cluster is one that nothing below it holds a write credential to, and that changes only through a reviewed merge to a repository its own Argo CD watches (ADR-0005). Credentials flow down only: Backstage on prod may read kind-dev, never the reverse. A lower cluster may be written to by pipelines directly; the control plane’s cluster is always lower, since its pipelines deploy to it.
Production is a property of an environment, not a cluster: the one serving real users and real data. prod is an upper cluster that also hosts staging, which is fine.
Rules, enforced by glidepath-app’s deploy.environments checks:
- A production environment is a Flight environment.
- A production environment of a Kubernetes target runs on an upper cluster (its
cluster, or the control plane’s own cluster when unset, which is lower, so it must set one). - A named
clustermust be in the registry. - A Ground environment sets no
cluster(unchanged; multi-cluster Ground is a future feature, below).
A cloud target’s production environment has no cluster (its runtime is the cloud account), so only the tier rule applies to it.
Where it lives
glidepath-control-planevalues:clusterZone/clusterRolesdescribe the control plane’s own cluster (must belowerand includecontrol-plane); eachclusters[]entry gainszone(defaultupper, which every cluster registered before this was) androles(default[workloads];control-planeis not allowed there).- The
cluster-registryConfigMap carrieszoneandrolesper entry. - The control plane forwards
clusterTaxonomy: {self, zones}to every app’sglidepath-appthrough the tenant-onboarding ApplicationSet, the same way it forwardsdefaultChart. cicd.schema.json:deploy.environments[].production(boolean).
Consequences
- A production environment can no longer be declared on the control-plane cluster or on an unregistered or lower
cluster; the app’s
-cicdsync fails with the reason, andvalidate-cicd-configrejects the schema violation. - Existing apps are unaffected: no environment set
production, and every app rendered identically with the taxonomy forwarded (12 live apps, 2026-10-08). - Declaring a lower cluster in the registry is allowed but nothing deploys to it yet.
- There are now two cluster registries with overlapping meaning: Glidepath’s (
clusters:) and Airframe’s (gitops-cluster-dev/00-bootstrap/cluster-registry/*.yamlConfigMaps,type: dev|upper, read by the ApplicationEnvironment Composition). Airframe’stype: devis zone lower with the control-plane role;type: upperis zone upper. Consolidating them is known-gaps #40.
One record per cluster (amendment, 2026-10-08)
The first version left two registries: Glidepath’s clusters: values and Airframe’s hand-written
crossplane-system ConfigMaps (type: dev|upper, readiness flags), plus apron’s type - the same facts typed in
three places, already disagreeing (an unused dev duplicate of kind-dev). Now each cluster is one record in
the hub cluster repo’s clusters.yaml (next to cluster-defaults.yaml; format in apron docs/clusters-file.md):
clusters:
- name: kind-dev
zone: lower
roles: [control-plane, workloads]
tenantsRepo: gitops-cluster-dev-tenants
aliases: [dev]
airframe: { cicdReady: true, infisicalHost: true }
- name: prod
zone: upper
roles: [workloads, platform-services]
airframe: { crossplaneReady: true }
glidepath: { relaySecretName: cluster-prod-relay-token }
Shared fields at the top; anything only one product needs under that product’s key. Each product reads the file with its own adapter and ignores what it does not know:
- Glidepath: the control-plane Application passes the file as a value file. The record named
clusterNameis this control plane (must be lower, withcontrol-plane); every other record is a remote cluster and needsglidepath.relaySecretName. - Airframe:
charts/cluster-registryrenders the ConfigMaps its Compositions read,typederived from roles (control-plane->dev), readiness flags from theairframe:section, one ConfigMap per alias.
Neither product imports the other or reads its live objects, so each still installs alone: a Glidepath-only cluster
repo’s file has shared fields and glidepath: sections, an Airframe-only one airframe: sections. Rejected: Glidepath
reading Airframe’s ConfigMaps (Argo CD renders the chart without cluster lookups, and it would need Airframe);
Glidepath rendering them (couples Glidepath to Airframe’s format); a Cluster custom resource (Glidepath needs the
data at render time, so values would still carry it).
Names are identities, not runtimes. kind-dev is the dev cluster’s name although it now runs on kiac (kubectl
context dev): the name is a key in 15 Infisical projects (<app>-kind-dev, platform-cicd-kind-dev), 15
ClusterSecretStores, every Bootstrap XR’s devCluster, Backstage’s cluster config and Tower. A name should outlive the
tool that runs the cluster; aliases records the other names a cluster answers to.
Future: Ground environments on other clusters
Wanted (the owner likes several dev clusters run by one Glidepath), not built. With this taxonomy it means: a Ground
environment may name a lower cluster with the workloads role. What it needs, each currently tied to the control
plane’s cluster:
- The Ground ApplicationSet rendered by that cluster’s own Argo CD (watching the app repos’
glidepath/envs/), so the control plane holds no credential to it, or the control plane’s Argo CD holding one (acceptable in a lower zone, but it breaks “every workload cluster runs its own Argo CD”). deploy-manifestswaiting on Rollout health through the relay’s facts (ADR-0021), as Flight already does, instead of watching the Rollout through same-cluster RBAC. Worth doing first, on the dev cluster alone: one health path for Ground and Flight.- Per-cluster secrets store (ESO + the app’s Infisical project) and registry pull credentials, scaffolded by Airframe.
- Tests: Testkube runs on the control plane’s cluster and reaches services by in-cluster DNS.
- Tower and Backstage: a cluster entry per workload cluster; Tower stops treating Ground as
kind-dev.
Cheaper alternatives for some reasons to want it (architecture, GPU): node pools and taints on one dev cluster.