`system.yaml` reference
The GitOps repository is configured by one file per system: applications/<system>/system.yaml (its environments, components, APIs and resources). Everything below is extracted from the source of render.py and local-cluster.sh, so it is always current.
system.yaml (render.py)
Section titled “system.yaml (render.py)”The GitOps repo owns every manifest. applications/<system>/system.yaml is the only hand-edited file of a system; this script derives: applications/<system>/applicationset.yaml one ApplicationSet per environment (dev/staging/prod) applications/<system>/overlays/<env>/<component>/ deployment.yaml, service.yaml, [httproute.yaml], kustomization.yaml applications/<system>/addons/<env>/<resource>/ cluster.yaml, kustomization.yaml (only where a running component `uses` the resource) applications/<system>/policies/<env>/ policy.yaml, kustomization.yaml (Kyverno guard rails, unless `policy: off`) applications/<system>/autonomy.json only when system.yaml declares autonomy (spec 124): what an engine reads operators/ Argo CD applications of the operators the system needs (versions table); with the answer property_operators: install (.copier-answers.yml, default owner) also External Secrets, Kyverno and LGTM (operators/apps/lgtm.yaml, operators/lgtm/); never for operators: owner applications/<system>/addons/<env>/<db>/restore-state.yaml written by `cplat resource restore`/`upgrade`: clusters and archives (server, major) bootstrap/<system>-root.yaml root Argo Application (app-of-apps), applied once by a human
A component only runs in the environments that are keys of its `instances` (none = dev), so a new component never reachesstaging/prod before `/instance:promote` adds the key. Image pins (`newTag`) live in the overlays and are preserved on re-render:versions never appear in system.yaml. A directory that still has app.yaml or services.yaml is not rendered: run`cplat platform update --migrate`.
system.yaml (every value explicit and reviewable; the model is described in docs/DOMAIN-MODEL.md): system: todo # required; equals the directory name owner: acme # required: a non-empty string gateway: {name: traefik-gateway, namespace: kube-system} # optional (default as shown) secretStore: {name: platform-secrets, kind: ClusterSecretStore} # optional: where `remote` secrets are read from registries: [docker.io/library/] # optional: extra image prefixes the guard rails accept environments: # required: exactly dev, staging, prod, in this order (the promotion order) dev: host: "{component}.{system}-dev.localhost" # placeholders {component} (expose.host or the name), {system}, {env}; none = not exposed url: "http://{host}:8088" # read by smoke/ship/verify, not by render: the URL smoke tests use policy: audit # audit | enforce | off (default audit in dev and staging, enforce in prod) observability: # optional: this environment gets the OpenTelemetry collector (ADR-021) ui: lgtm # none | lgtm; or exportTo: https://otlp.example.com (+ headersSecret: <secret>) alerts: true # alert issues (spec 092): false = the poll skips this environment rules: [{name: HighErrorRate, query: "<PromQL>", severity: warning, summary: "optional"}] # critical | warning | info queryURL: https://prom.example.com/api/prom # optional; querySecret: <name> or <namespace>/<name> (key AUTHORIZATION) alertmanagerURL: https://am.example.com # optional operators: owner # optional, the only value: the platform installs no operator here (spec 115) backup: {to: backups, schedule: "0 3 * * *", retention: 30d} # optional (spec 117): back the environment up to a bucket resource staging: promote: after-dev-smoke # after-dev-smoke | manual (default); dev takes none; prod is always promoted by hand prod: {} autonomy: {paused: true} # optional: every environment acts as observe (the only key; spec 124) # environments.<env>.autonomy (optional; prod at most act; every omitted field has its default; changes no manifest): # level: act # observe (default) | advise | propose | act | improve # engine: https://ai-ops.example.com # http(s) URL of the engine serving this environment # autoMerge: {routes: [fix], maxCriticality: low} # low | medium | high # riskGate: {maxFiles: 5, maxLines: 200, regressionTest: true, secondReview: true} # budget: {runsPerDay: 5, mergesPerDay: 2, perRun: {minutes: 60, invocations: 20, reruns: 2, cost: 3}} # window: {days: [mon, tue, wed, thu, fri], from: "09:00", to: "17:00", timezone: UTC} # undoWindow: 24h # <n>m | <n>h | <n>d # components.<c>.instances.<env>.autonomy: {max: propose} # caps the instance below its environment components: todo-api: type: service # service | website | library, required with lang lang: python # python | java | go | typescript (website) | unknown (migrated 4.x entries) repo: org/todo-api # where the image is built image: ghcr.io/org/todo-api # no tag: the tag is pinned in the overlay (dev: latest) port: 8080 probes: {liveness: /health, readiness: /ready} user: 65532 # numeric non-root UID the image runs as volumes: {tmp: /tmp} # writable emptyDirs (root filesystem is read-only) env: {LOG_LEVEL: INFO} # non-secret wiring, e.g. API_URL: http://todo-api metrics: /metrics # optional: the collector scrapes it secretRefs: [] # names of existing Secrets exposed as env vars secrets: # Secrets created by External Secrets Operator (never values in git, ADR-018) - {name: todo-api-auth, generate: [JWT_KEY]} # random values generated in the cluster; optional `length` (32) - {name: todo-api-stripe, remote: {keys: [STRIPE_KEY]}} # read from the secret store; optional `path`, `refreshInterval` # path placeholders {system}, {env}, {secret}, {component}; default {system}-{env}-{secret} replicas: 1 # one number resources: {requests: {cpu: 50m, memory: 128Mi}, limits: {memory: 512Mi}} expose: {host: api} # optional: publish through the Gateway (the environment needs `host`) provides: [todo-api-rest] # APIs declared under `apis:` (exactly one provider each) consumes: [] uses: [postgres, todo-lib] # resources of the system and library components (a library changes no manifest) instances: # the keys are the environments it runs in; absent = dev only dev: {} staging: {promote: manual} prod: {replicas: 2, env: {LOG_LEVEL: WARN}} # replicas, resources, env (merged), promote, secrets (replace declared fields by name) # scale (spec 120): on the component (default for every instance) or on an instance; excludes `replicas` on the same level. # scale: {min: 2, max: 10, on: {cpu: 70}} # HPA on CPU utilisation percent (needs resources.requests.cpu) # scale: {max: 6, on: {memory: 80}} # HPA on memory utilisation percent; min defaults to 1 # scale: {min: 0, max: 8, on: {consumes: orders, lag: 50}} # KEDA on a broker's events API queue; min 0 = scale to zero # scale: {max: 5, on: {cache: sessions, list: jobs, length: 20}} # KEDA on a Redis/Valkey list length # min and max are whole numbers, 1..20 (at most 20 replicas); min <= max; min: 0 only with a queue trigger (consumes, cache). # cpu/memory need a metrics server: answer resource_metrics (provided | install) in .copier-answers.yml; install renders it. todo-lib: {type: library, lang: python, repo: org/todo-lib} # a library has no instances and renders nothing apis: todo-api-rest: {type: openapi} # openapi | grpc | graphql | events; changes no manifest orders: {type: events, broker: bus} # spec 119: an events API is carried by a broker (optional key while there is one) resources: # any number, each named, typed (database, bucket, cache, broker) orders-db: # name: lowercase letters, digits, hyphens; at most 30 characters; not a component name type: database # required version: 17 # 15 | 16 | 17 | 18 (default 17); instances.<env>.version sets one environment's major (spec 121) replicas: 1 storage: 1Gi resources: {requests: {cpu: 100m, memory: 256Mi}, limits: {memory: 512Mi}} instances: {prod: {replicas: 3, storage: 20Gi, # per-environment overrides: external, remote, replicas, storage, resources, backup, version (spec 121: # the major of that environment, written by /resource:upgrade; held pins live in the rendered files) backup: {to: backups, schedule: "30 2 * * *", retention: 8w}}} # spec 117: to = a bucket resource; schedule is # a five-field cron in UTC (default "0 3 * * *"), retention <n>d or <n>w (default 30d) sessions: # a cache (spec 118): one Valkey node per environment, REDIS_URL and REDIS_* for a using component type: cache version: 9 # 8 | 9 (default 9) memory: 256Mi # default 256Mi, a quantity of at least 64Mi; the server gets 80 % as maxmemory maxmemoryPolicy: allkeys-lru # default; noeviction | allkeys-lru | allkeys-lfu | allkeys-random | volatile-lru | volatile-lfu | ... persistence: false # true = append-only file on a volume (default false) storage: 1Gi # only with persistence: true instances: {prod: {memory: 1Gi, persistence: true, storage: 5Gi}} # per environment: external, remote, memory, maxmemoryPolicy, persistence, storage, version bus: # a broker (spec 119): NATS with JetStream; providers and consumers of its events APIs use it type: broker # without listing it in `uses`; each gets NATS_* from <system>-bus-<component>-app version: "2.15" # "2.14" | "2.15" (default "2.15") replicas: 1 # 1 | 3 (default 1) storage: 1Gi # JetStream volume per replica (default 1Gi) instances: {prod: {replicas: 3, storage: 10Gi}} # per environment: external, remote, replicas, storage, resources, version billing-db: # external: runs nothing, its connection Secret is synced from the secretStore type: database external: true # true | false (default false); per environment under instances.<env>.external remote: {path: "{system}-{env}-{resource}", refreshInterval: 1h} # both optional; placeholders {system}, {env}, {resource} files: # a bucket (spec 116): the bucket is <system>-files; at most 45 characters in-cluster type: bucket storage: 10Gi # default 10Gi versioning: false # true | false (default false) instances: {dev: {versioning: true}, prod: {external: true}} # per environment: external, remote, storage, versioning backups: # an external bucket that backups write to needs the S3 coordinates in git (spec 117) type: bucket external: true remote: {s3: {endpoint: "https://s3.eu-central-1.amazonaws.com", bucket: acme-backups, region: eu-central-1, forcePathStyle: false}}
External component (spec 069, ADR-029): a component whose manifests this generator cannot express (CronJobs, volumes,a login proxy, a Helm chart with CRDs). The package is owned by this repo or pinned to the component repo's chart;render.py only wires it into the environments and pins its image, so pin-dev, /instance:promote and /system:status work.It takes only type, lang, repo, image, external, provides, consumes and instances (an instance sets only `promote`): stock-manager: {type: service, lang: unknown, repo: org/stock-manager, image: ghcr.io/org/stock-manager, external: {kustomize: external/stock-manager, namespace: stock-manager}, # {env} allowed in the path instances: {dev: {}}} ai-ops (a chart): external: {helm: {repoURL: https://github.com/org/ai-ops, path: charts/ai-ops, revision: main, imageTag: image.tag}, namespace: ai-ops, values: {replicas: 1}, envValues: {prod: {replicas: 2}}}devbox run cluster-up (local-cluster.sh)
Section titled “devbox run cluster-up (local-cluster.sh)”Local k3d cluster for this product: Gateway API (Traefik) and, by default, ArgoCD with access to the private GitHubrepo and GHCR images, then the root Application. For trying the application on your machine; real clusters arebootstrapped by a human with `KUBE_CONTEXT=<ctx> devbox run bootstrap`.
local-cluster.sh up create (or reuse) the cluster, install the Gateway API (+ ArgoCD, credentials, root app) local-cluster.sh down delete the cluster (and its registry) local-cluster.sh ui open k9s on the cluster, in the <app>-dev namespace (extra arguments go to k9s)
Environment: LOCAL_CLUSTER cluster name (default <app>-local) LOCAL_HTTP_PORT host port of the Gateway (unset: 8088, or the next free port when 8088 is busy; `auto`: the first free port; an explicit busy port is reported with its owner before anything is created) ARGOCD_VERSION default v3.4.12 (the versions table of this platform release) K3S_IMAGE default rancher/k3s:v1.32.5-k3s1@sha256:b75eac801bbc91b4584769c840f346683c7740d72f83e630088370f8e1eb2c8a — the version the Gateway setup below is verified against (k3d's own default may be older and ship a Traefik without Gateway API support) Every default of this script equals its row of the versions table (`cplat platform versions`). A variable that sets a different value prints `WARNING: <VAR>=<value> overrides the versions table (<row> <table version>); this cluster does not run what this platform release pins` before anything is created. GATEWAY_API_VERSION only used if k3s' Traefik chart did not install the Gateway API CRDs (default v1.2.1) GH_TOKEN default `gh auth token`; used for both credentials below unless they are set (ArgoCD mode only) REPO_TOKEN least-privilege token for ArgoCD to read this repo: a fine-grained PAT with only "Contents: read" on the GitOps repo (default GH_TOKEN) PULL_TOKEN token for the image pull secret: a classic PAT with only read:packages (default GH_TOKEN) WITH_ARGO=0 Gateway + namespaces only: no ArgoCD, no GitHub credentials, no root Application (used by the platform's end-to-end test, which applies the rendered manifests directly) WITH_SYSTEM=0 cluster only (a cluster repo, no applications/): needs LOCAL_CLUSTER; `up` stops after Argo CD and its credential, before the namespaces and bootstrap/ (default 1) ESO_VERSION External Secrets Operator chart version (default 2.12.0) WITH_ESO=0 skip External Secrets Operator (no `secrets:` support in that cluster) LGTM_VERSION grafana/otel-lgtm image tag for `observability: {ui: lgtm}` (default 0.35.0, with its digest) WITH_LGTM 1 = install the dev Grafana stack, 0 = never; default: only if system.yaml says `ui: lgtm` KYVERNO_VERSION Kyverno chart version (default 3.9.1) WITH_KYVERNO 1 = install Kyverno, 0 = never; default: only if an environment's policy in system.yaml is not off CNPG_VERSION CloudNativePG operator chart version (default: the targetRevision of operators/apps/cloudnative-pg.yaml, else 0.29.1) WITH_CNPG 1 = CloudNativePG, 0 = never; default: 1 exactly when operators/apps/cloudnative-pg.yaml exists. With Argo CD the operator comes from operators/root.yaml (applied before bootstrap/); only with WITH_ARGO=0 does this script install it through a k3s HelmChart. SEAWEEDFS_OPERATOR_VERSION SeaweedFS operator chart version (default: the targetRevision of operators/apps/seaweedfs.yaml, else 0.1.45) WITH_SEAWEEDFS 1 = SeaweedFS operator, 0 = never; default: 1 exactly when operators/apps/seaweedfs.yaml exists. Installed like CloudNativePG: from operators/root.yaml with Argo CD, a k3s HelmChart with WITH_ARGO=0. BARMAN_CLOUD_VERSION Barman Cloud plugin chart version (default: the targetRevision of operators/apps/plugin-barman-cloud.yaml, else 0.8.1) CERT_MANAGER_VERSION cert-manager chart version (default: the targetRevision of operators/apps/cert-manager.yaml, else v1.21.3) WITH_BARMAN_CLOUD 1 = Barman Cloud plugin (and cert-manager, which it needs), 0 = never; default: 1 exactly when operators/apps/plugin-barman-cloud.yaml exists. VELERO_VERSION Velero chart version (default: the targetRevision of operators/apps/velero.yaml, else 12.2.1) VELERO_AWS_PLUGIN_IMAGE Velero AWS plugin image (default velero/velero-plugin-for-aws:v1.14.4@sha256:c85a7fa803569e3128c33e4f5766f84ab156c8d171c9f1dd2715581d45ce98fa) WITH_VELERO 1 = Velero, 0 = never; default: 1 exactly when operators/apps/velero.yaml exists. All three are installed like CloudNativePG: from operators/root.yaml with Argo CD, k3s HelmCharts with WITH_ARGO=0. NACK_VERSION NACK (NATS JetStream controller) chart version (default: the targetRevision of operators/apps/nack.yaml, else 0.35.0) WITH_NACK 1 = NACK, 0 = never; default: 1 exactly when operators/apps/nack.yaml exists. Installed like CloudNativePG. KEDA_VERSION KEDA chart version (default: the targetRevision of operators/apps/keda.yaml, else 2.19.0) WITH_KEDA 1 = KEDA, 0 = never; default: 1 exactly when operators/apps/keda.yaml exists. Installed like CloudNativePG. METRICS_SERVER_VERSION metrics-server chart version (default: the targetRevision of operators/apps/metrics-server.yaml, else 3.14.0) WITH_METRICS_SERVER 1 = the pinned metrics-server, 0 = never; default: 1 exactly when operators/apps/metrics-server.yaml exists. With 1, a new cluster is created without k3s' bundled metrics-server. REGISTRY_PORT also create a local image registry reachable as localhost:<port> from the host and as k3d-<cluster>-registry:<port> from the clusterWith Argo CD, External Secrets, Kyverno and the LGTM stack come from operators/apps/<name>.yaml (applied from operators/root.yaml) whenthat file exists; otherwise (or with WITH_ARGO=0) this script installs them itself.Re-running `up` is safe; it re-applies credentials and the root Application.shellcheck source=ports.shWith Argo CD the property operators come from operators/apps/<name>.yaml when it exists (spec 121); a cluster-up of an olderrelease left External Secrets or Kyverno as k3s HelmCharts, and those stay.