Domain model
The nouns the platform is built around: what a system is made of, where it runs, and how it changes. The model takes Backstage’s system model for the first part and adds environments and the delivery process. It is a model only: the platform does not use or need Backstage. Decisions and the migration path are in ADR-053: A domain model based on Backstage’s system model, with environments.
Status: 5.0 is current. system.yaml and the noun-based commands described below are what the platform runs (spec 114, ADR-054: The 5.0 interface: system.yaml checked by the renderer, noun plugins with tombstones, and a migration that proves itself); cplat model show prints a system in these nouns (JSON schema: 2). A repo from 4.x moves over with cplat platform update --migrate (Migrating from 4.x to 5.0). The model in 4.7 and 4.7 features on the model keep the old names as history.
Three questions
Section titled “Three questions”| You ask | The model answers with |
|---|---|
| What is my product made of? | the System and its parts: Components, APIs, Resources, and its Owner |
| Where does it run, at which version? | Environments × parts: each cell is an Instance (or a Resource instance); the environments run on a Cluster that several systems share |
| How does it change? | an Issue becomes a Spec; a Run ships it; a Release goes to prod |
The system model
Section titled “The system model”Three layers:
- What it is is Backstage’s system model, reduced to the kinds a user needs. An Owner owns a System; Components, APIs and Resources are part of it. A Component provides or consumes APIs, uses Resources, and may use a library (another Component).
- Where it runs adds environments and the cluster. Every Component and Resource exists once per Environment as an Instance (Resource instance). Environments are ordered (
promotes to), so each Instance knows the Instance its version was promoted from. Every Environment runs on a Cluster; the Cluster hosts the System and runs the shared operators that manage its Resource instances (see Cluster). - Work connects changes to the model: a Spec changes the System or one Component, and the Run that ships it updates Instances, starting in dev.
A library has no Instances; other Components use it.
An example: the todo system
Section titled “An example: the todo system”Read the left column as “what the system is” and the grid as “where each part runs right now”. A row is one part across environments; promoting moves a version one cell to the right. Versions and states in the picture are example values. The platform writes versions when it pins and promotes; health and smoke results are looked up when you ask (/system:status), never stored.
The delivery model
Section titled “The delivery model”The documented SDLC (specs per ADR-026: Feature specs drive the build, unattended delivery, the deploy gate, the feedback loops) attaches to the core model:
- Specs have the model’s two levels. A system spec (
/system:spec) is split by a Plan into component specs (/component:spec), and the plan is where an API contract is set (spec 110). - A Criterion (
AC-<NNN>.<n>) is traced by a Test in the component’s repo and judged in the Verification. A(smoke)criterion is checked on an Instance in an Environment, not in a repo. - A Run ships a spec: it opens a Pull request, CI builds an Image (
sha-<40>and its digest), the image is pinned in dev and promoted as an Instance, and a Release tag goes to prod with the digest staging verified. - Every feedback loop ends as an Issue about a model object: a firing alert or a smoke failure is about an Instance; a platform failure or a retro proposal is about the platform.
Following the arrows from Issue to Instance is the evidence chain of spec 109.
Lifecycles
Section titled “Lifecycles”Thick borders are where you decide: approving a spec, an escalation, promoting to prod. Everything else runs unattended. The run’s deploy phase takes a version from “dev pinned” to “dev verified”; staging covers the automatic staging promotion. A smoke that did not run is NOT VERIFIED and promotes nothing.
The nouns
Section titled “The nouns”| Noun | What it means | Example | Today |
|---|---|---|---|
| System | A product: everything below, delivered together | todo | product, app, gitops-app repo |
| Component | Something you build, in its own repo: a service, a website or a library | todo-api | service, shape |
| API | What a component offers others, and who uses it | todo-api REST | plan ## Contract (prose) |
| Resource | Something you use but don’t build; named and typed, run by the platform or external | users-db (database) | addon |
| Cluster | The place systems run, with its shared operators, External Secrets with the secret store, the Kyverno engine and the list of its systems | home | cluster repo, operators/ |
| Owner | Who is responsible for the system and its parts | team-todo | not modelled |
| Environment | A stage the system passes through, with its own rules for getting in | staging | env key in several files |
| Instance | One component running in one environment, at one version | todo-api in staging | pin, overlay, Argo app |
| Resource instance | One resource in one environment | the staging database | addons/<env>/<addon>/ |
| Issue | Something to look at: a request, a failure, an alert, a proposal | alert in todo-api/prod | GitHub issue |
| Spec | What should change in a system or a component, with numbered criteria | spec 110 | product spec, spec |
| Plan | How a system spec is split across its components, with the contract between them | docs/plan/110.md |
plan, design |
| Run | An agent shipping a spec or a fix, within a budget | ship of spec 110 | run, ship |
| Release | A tagged version of a component that may go to prod | todo-api 1.4.0 | release, tag |
Test, Verification, Pull request and Image are evidence you can look at; they need no commands of their own.
Three placements are decided (ADR-053: A domain model based on Backstage’s system model, with environments):
- Observability (the OpenTelemetry collector, alert rules, the UI) is a property of an Environment, not a Resource: every component in the environment gets it.
- Secrets are declared on the Component (generated or read from the store) and can be overridden per Instance; their values are set per Instance.
- Names in the cluster do not change: the namespace
<system>-<env>, the Argo application<component>-<env>, every resource’s Secret<system>-<name>-app(so<system>-postgres-appfor a resource calledpostgres). The model maps onto them.
Cluster
Section titled “Cluster”A Cluster is the place systems run. Its own repo (cluster.yaml, shape cluster-gitops) owns what several systems share: the operators (for example CloudNativePG), External Secrets with the secret store, and the Kyverno engine. The decision is ADR-060: A cluster repo owns what a cluster shares and lists the systems it runs; the steps are in Shared clusters.
Its relations to the rest of the model:
- A Cluster hosts Systems.
cluster.yamllists each system by its gitops repo, and the cluster repo renders one Argo applicationsystem-<name>per system (/cluster:add,/cluster:remove). A System is hosted by one Cluster; a cluster without a cluster repo hosts just the one system that installs its own operators. - An Environment runs on its System’s Cluster. All environments of a system run on the same cluster, each in its namespace
<system>-<env>. Spreading one system’s environments over several clusters is not modelled yet. - A Resource instance is managed by the Cluster’s operators. An in-cluster database instance is run by CloudNativePG, a bucket instance by the SeaweedFS operator, and an external resource’s Secret is filled by External Secrets through the cluster’s secret store. The cluster repo’s check fails when a hosted system needs an operator that
cluster.yamldoes not list. - A System hands its operators to the Cluster by setting
operators: ownerin every environment; it then no longer rendersoperators/. Kyverno policies stay with each System; only the engine is the Cluster’s. - Components and their Instances do not know the Cluster. Names in the cluster stay the same, so a system renders the same manifests whether it shares a cluster or has its own.
One file per system
Section titled “One file per system”system.yaml is the only file you edit by hand in a system’s GitOps repo (applications/<system>/system.yaml). Each environment’s rules live in one place. Versions are never in the file: the platform writes them when it pins and promotes. render.py checks the file and refuses unknown keys with a message that names the key.
system: todoowner: team-todo
environments: # exactly dev, staging, prod, in promotion order dev: {host: "{component}.todo-dev.localhost", policy: audit, observability: {ui: lgtm}} staging: {promote: after-dev-smoke, policy: audit} prod: {promote: manual, policy: enforce}
components: todo-api: type: service lang: python repo: team-todo/todo-api provides: [todo-api-rest] uses: [postgres] secrets: [{name: api-keys, remote: {keys: [STRIPE_KEY]}}] instances: # the environments it runs in dev: {} staging: {} prod: {replicas: 2} todo-web: type: website lang: typescript repo: team-todo/todo-web consumes: [todo-api-rest] expose: true instances: {dev: {}, staging: {}, prod: {}} todo-lib: {type: library, lang: python, repo: team-todo/todo-lib}
apis: todo-api-rest: {type: openapi}
resources: postgres: type: database version: 17 replicas: 1 storage: 1Gi instances: {prod: {replicas: 3, storage: 20Gi}} users-db: type: database external: true # the database lives outside the cluster remote: {path: "{system}-{env}-{resource}"}| Where | Allowed keys |
|---|---|
| top level | system, owner (required, non-empty), environments, components, apis, resources, gateway, secretStore, registries, autonomy (only paused) |
environments.<env> |
host, url, policy, promote, observability, operators, backup, autonomy |
| service or website component | type, lang, repo, image, port, probes, user, volumes, env, secretRefs, secrets, replicas, resources, expose, metrics, provides, consumes, uses, instances, scale |
| external component | type, lang, repo, image, external, provides, consumes, instances (an instance sets only promote and autonomy) |
| library component | type, lang, repo, provides, consumes |
components.<c>.instances.<env> |
replicas, resources, env, promote, secrets, autonomy (only max), scale |
apis.<api> |
type: openapi, grpc, graphql or events; an events API also takes broker (apis.<api>.broker, the name of a broker resource) |
resources.<name> |
type, external, remote, version, replicas, storage, resources, instances |
resources.<name>.remote |
path, refreshInterval; on a bucket also s3 (endpoint, bucket, region, forcePathStyle) |
database resources.<name>.instances.<env> |
external, remote, replicas, storage, resources, backup, version |
backup (environment or database instance) |
to, schedule, retention |
bucket resources.<name> (type: bucket) |
type, external, remote, storage, versioning, instances |
bucket resources.<name>.instances.<env> |
external, remote, storage, versioning |
cache resources.<name> (type: cache) |
type, external, remote, version, memory, maxmemoryPolicy, persistence, storage, instances |
cache resources.<name>.instances.<env> |
external, remote, memory, maxmemoryPolicy, persistence, storage, version |
broker resources.<name> (type: broker) |
type, external, remote, version, replicas, storage, resources, instances |
broker resources.<name>.instances.<env> |
external, remote, replicas, storage, resources, version |
Valid type/lang pairs: service/python, service/java, service/go, website/typescript, library/python, and service/unknown for a migrated component without a known shape.
Semantics
Section titled “Semantics”- A component runs in exactly the environments that are keys of its
instances; withoutinstancesit runs in dev only; a library has none. Instanceenvis merged over the component’s (the instance wins);replicasandresourcesreplace; instancesecretsoverride the fields of the same-named declared secret in that environment only. hostplaceholders:{component},{system},{env}.urlplaceholders:{host},{component},{system},{env}; aurlis read byinstance verifyand ignored by the renderer.policyisaudit,enforceoroff(defaultauditin dev and staging,enforcein prod).observabilityon an environment gives that environment the collector and gives every instance there theOTEL_*variables.backupon an environment backs up its persistent volumes and objects (Velero); on a database instance it backs up that database with continuous archiving and point-in-time restore.tois a bucket resource with an instance in that environment,schedulea five-field UTC cron (default0 3 * * *),retention<n>dor<n>w(default30d); an external bucket as target declaresremote.s3. Databases are restored only with/resource:restore. See Backup and restore (spec 117).promote:after-dev-smokeormanualon staging (defaultmanual), onlymanualon prod, none on dev. An instance value wins over the environment’s.resources.<name>declares any number of named, typed resources.typeis required insystem.yaml,postgresincluded (/resource:add postgresfills intype: databasewhen--typeis left out). The types aredatabase,bucket,cacheandbroker(all rendered; the bucket is spec 116, the cache spec 118, the broker spec 119). A resource is rendered in every environment where a running component uses it. Database instances takereplicas(default 1),storage(default1Gi) andversion(default 17; the supported majors are 15, 16, 17 and 18), with per-environment overrides underinstances.<env>. The Argo application isaddon-<name>-<env>and the Secret is<system>-<name>-app.type: bucketis an S3-compatible object store for files and backups (spec 116). It takesstorage(default10Gi) andversioning(defaultfalse;truekeeps earlier versions of an overwritten object,falserenders the S3 stateSuspended), at the resource level or per instance underinstances.<env>. For an in-cluster bucket the name<system>-<name>is the bucket name and may be at most 45 characters. Each bucket instance runs its own SeaweedFS, managed by the SeaweedFS operator that the platform installs fromoperators/. The component gets eight variables from the Secret<system>-<name>-app(keysendpoint,bucket,region,forcePathStyle,accessKeyId,secretAccessKey):S3_ENDPOINT,S3_BUCKET,S3_REGION,S3_FORCE_PATH_STYLE(truein-cluster),AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_REGIONandAWS_ENDPOINT_URL_S3. With two or more buckets the prefix rule applies, for exampleUPLOADS_S3_BUCKET, and no bareS3_*orAWS_*variable is set. Anexternal: truebucket renders no store; its secret store entry (default path{system}-{env}-{resource}) carries the same keys as the Secret:endpoint,bucket,region,forcePathStyle,accessKeyIdandsecretAccessKey;storageandversioningare refused there, since an external bucket is sized and configured at its provider. Removing a bucket resource keeps the in-cluster bucket and its data until a human deletes them.type: cacheis an in-memory cache that speaks the Redis protocol (spec 118). It runs Valkey, not Redis: Valkey has a BSD licence, while Redis 8 is licensed only under AGPLv3, RSAL or SSPL. The type is calledcache, notredis;type: redisandtype: valkeyfail the render with a message that namescache. Keys and defaults:version(Valkey major 8 or 9, default 9),memory(default256Mi, at least64Mi; the container’s request and limit),maxmemoryPolicy(defaultallkeys-lru),persistence(defaultfalse) andstorage(default1Gi, only withpersistence: true), at the resource level or per instance underinstances.<env>. The server getsmaxmemoryof 80 % ofmemory, which leaves room for its own overhead. Withpersistence: truethe data is written to an append-only file on a volume ofstoragesize; withfalsethere is no volume. Each in-cluster instance is one StatefulSet that the renderer writes itself, with a ClusterIP Service on port 6379 and no route. It needs no operator, sooperators: ownerchanges nothing for it and/platform:doctorhas no row for it. The component gets four variables from the Secret<system>-<name>-app, whose keysurl,host,portandpasswordare the contract that later specs (spec 120, the scaler) rely on:REDIS_URL,REDIS_HOST,REDIS_PORTandREDIS_PASSWORD. All use database0, shared by every component that uses the cache, with one password that the platform’s credentials Job creates once in the cluster (an existing Secret is kept; deleting it makes a new password on the next sync). With two or more caches the prefix rule applies, for exampleSESSIONS_REDIS_URL. Anexternal: truecache renders no server; its secret store entry (default path{system}-{env}-{resource}) carries the keysurl,host,portandpassword, andmemory,maxmemoryPolicy,persistenceandstorageare refused there, since an external cache is sized and configured at its provider. Cache data is not backed up, with or withoutpersistence: the environment backup leaves caches out by name (see Backup and restore). Three limits to know: a write-heavy persistent cache nearmaxmemorycan need more than the 20 % headroom while the append-only file is rewritten, so give persistent caches morememory; going down a major (version: 9to8) is not refused before spec 121, and withpersistence: truethe pod then crash-loops until its volume is deleted, because version 8 may not read version 9’s file; andpersistenceorstoragecannot change on a running cache in place: delete its StatefulSet withkubectl delete statefulsets.apps <system>-<name> -n <system>-<env> --cascade=orphan, then sync (the pod restarts; an existing volume is reused, and a largerstoragedoes not resize it).type: brokeris a message broker for events APIs (spec 119). It runs NATS with JetStream, the persistent messaging layer of NATS. Keys and defaults:version("2.14"or"2.15", the NATS server minor line, default"2.15"),replicas(1 or 3, default 1),storage(default1Gi, JetStream file storage per replica) andresources(requests 100m CPU and 256Mi memory, memory limit 512Mi), at the resource level or per instance underinstances.<env>(versionper instance is the rollout state of/resource:upgrade). Each in-cluster instance is the officialnatsHelm chart, installed as an Argo CD application that the broker’saddon-<name>-<env>directory renders. NACK, the NATS JetStream controller, is a platform operator: it creates the streams and consumers that git declares, sooperators/apps/nack.yamlappears when an environment runs an in-cluster broker (operators: ownerleaves it to the cluster owner, and/platform:doctorhas a rowoperator nackand onebroker <name> (<env>)row per in-cluster broker).- Binding. A broker carries an events API when the API names it with
apis.<api>.broker: <name>, or, without that key, when the system has exactly one broker. With two or more brokers an events API must choose one;brokeron an API that is not an events API, or naming something that is not a broker, fails the render. An events API in a system with no broker stays unbound: the render prints a note that names the API and/resource:add <name> --type brokerand gives the components nothing. An API name must be a lowercase DNS label to be bound, because it names the stream. - Implicit use. The provider and the consumers of a bound events API use its broker without listing it in
uses; listing it changes nothing. Listing a broker that carries none of the component’s events APIs fails the render, and a component’senvmay not set a variable the broker gives it. - Names. For the events API
<api>of system<system>, the stream is<system>-<api>with the subjects<system>.<api>.>. Each consuming component has a durable pull consumer named after the component. The stream keeps messages for 7 days (file storage, oldest first when the volume is full). A consumer must acknowledge a message within 30 s, otherwise it is delivered again, without a limit on redeliveries. - Environment contract. A component that provides or consumes a bound events API gets
NATS_URL,NATS_USERandNATS_PASSWORDfrom its own Secret<system>-<name>-<component>-app(keysurl,user,password), not from a shared one; spec 120 relies on this Secret as its contract. With two brokers the prefix rule applies (BUS_NATS_URL). For every events API it provides or consumes it also getsEVENTS_<API>_STREAM(<system>-<api>) andEVENTS_<API>_SUBJECT_PREFIX(<system>.<api>), andEVENTS_<API>_CONSUMER(the component name) when it consumes the API;<API>is the API name in upper case with-becoming_. Last,NATS_INBOX_PREFIXis_INBOX.<component>. - Rights. Every component has its own user, with a password that the platform’s credentials Job creates once in the cluster (an existing Secret is kept; deleting it makes a new password on the next sync). The provider of an API may publish under
<system>.<api>.>. A consumer may only fetch from and acknowledge on its own consumer. Every user may subscribe only to its own inbox prefix_INBOX.<component>.>. The default inbox_INBOX.>would let one component read what another fetches, so a client must passNATS_INBOX_PREFIXas its inbox prefix; the example below does. - Monitoring. The server’s monitoring endpoint,
http://<system>-<name>.<system>-<env>.svc:8222/jsz?accounts=true&streams=true&consumers=true, lists per stream the stored messages and per consumernum_pending(not yet delivered) andnum_ack_pending(delivered, not yet acknowledged). It needs no login and is reachable only inside the cluster. - Example. A provider and a consumer each use half of this Python code (
nats-py). No template ships publisher or subscriber code.
- Binding. A broker carries an events API when the API names it with
import asyncio, osimport nats
async def main(): nc = await nats.connect(os.environ["NATS_URL"], user=os.environ["NATS_USER"], password=os.environ["NATS_PASSWORD"], inbox_prefix=os.environ["NATS_INBOX_PREFIX"]) js = nc.jetstream() # provider of the events API `orders`: publish under its subject prefix await js.publish(os.environ["EVENTS_ORDERS_SUBJECT_PREFIX"] + ".created", b'{"id": 42}') # consumer: read through the component's own durable consumer, acknowledge within 30 s sub = await js.pull_subscribe_bind(durable=os.environ["EVENTS_ORDERS_CONSUMER"], stream=os.environ["EVENTS_ORDERS_STREAM"]) for msg in await sub.fetch(10, timeout=5): print(msg.subject, msg.data) await msg.ack() await nc.close()
asyncio.run(main())- Removal. Streams and consumers hold messages, so nothing deletes them: not a render, not an Argo CD sync, not
/resource:remove./resource:removeis refused while an events API is bound to the broker. When you remove an events API or a consumer, the render prints a note such asstream <system>-<api> is no longer declared; it and its messages stay on the broker until you delete them, and the stream or consumer stays until a person deletes it. To delete one by hand, use thenatscommand line against the broker (nats stream rm <system>-<api>,nats consumer rm <system>-<api> <component>), orkubectl delete stream.jetstream.nats.ioandconsumer.jetstream.nats.ioobjects for the ones that NACK still holds; that removes only the declaration (preventDelete): the stream and its messages stay on the server untilnats stream rm/nats consumer rm. Removing the broker resource itself leaves its server, its volumes and its data in the cluster in the same way. - External broker. With
external: truethe platform renders no server and creates nothing on the broker: the owner creates the streams and consumers, and the render prints each name to create (stream<system>-<api>with its subjects, file storage and a maximum age of 7 days, and the durable consumers, pull with explicit acknowledgement and an ack wait of 30 s). Each component with an events API reads its own entry from the secret store, by default under{system}-{env}-{resource}-{component}(remote.pathalso accepts{component}), with the keysurl,userandpassword. Use any NATS service with JetStream, such as Synadia Cloud or your own cluster. A path without{component}makes every component read the same entry, so they share one user and the rights are the broker owner’s business.replicas,storageandresourcesare refused on an external broker. - Limits. Broker data is part of the environment backup (see Backup and restore), with nothing to configure. A new component user restarts the broker pods; with one replica that is a short outage that clients ride out by reconnecting, and the messages stay on the volume. Changing
replicasfrom 1 to 3 turns a single server into a cluster; start production with 3 rather than switching later.storagecannot change on a running broker in place: delete its StatefulSet withkubectl delete statefulsets.apps <system>-<name> -n <system>-<env> --cascade=orphan, then sync (an existing volume is reused, and a largerstoragedoes not resize it). Going down a minor version (2.15to2.14) is not refused before spec 121, and a server may not read the files of a newer one. NACK reconciles on change, not continuously: a stream deleted by hand on the server is recreated only when its object changes or NACK restarts. The environment backup copies the broker volume while the server runs, so it may catch a block mid-write; NATS truncates a torn tail block on start. external: truemeans the resource lives outside the cluster. The platform renders no database, only an External SecretsExternalSecretthat fills<system>-<name>-appfrom the secret store, so components see the same contract.remote(path,refreshInterval) says where in the store; the default path is{system}-{env}-{resource}, andremoteneedsexternal: true.instances.<env>may overrideexternalandremote, for example an in-cluster database in dev and an external one in prod.- Prefix rule: a component that uses exactly one resource of a type gets the bare variables (
DATABASE_URL,PGHOST, …). With two or more resources of that type, every variable is prefixed with the upper-cased resource name,-becoming_:uses: [orders-db, users-db]yieldsORDERS_DB_DATABASE_URL,USERS_DB_PGHOSTand so on, and no bareDATABASE_URL. The variables are the same in every environment, in-cluster or external. operators: owneron an environment (the only value) says the cluster owner installs the operators there. Otherwise the platform installs them from the repo’soperators/folder (operators/root.yaml, applied once per cluster withdevbox run bootstrap;cluster-updoes it locally).autonomy(optional, off by default: every environment is atobserve) says what an engine may do on its own in an Environment:level(observe,advise,propose,act,improve; prod at mostact),engine,autoMerge,riskGate,budget,windowandundoWindow. On an Instance,autonomy: {max: <level>}can only lower it; the top-levelautonomy: {paused: true}pauses every environment. A system that declares any of it also getsapplications/<system>/autonomy.json, which an engine reads./system:autonomyshows and changes it; every key and default is in autonomy.md.- Every name in
usesis a resource or a library of the system; an API has exactly one provider.
Scaling an instance
Section titled “Scaling an instance”An instance can scale between a minimum and a maximum number of replicas instead of running a fixed count (spec 120). scale is an Instance property. Written on components.<c>, it is the default of every instance of that component that has neither scale nor replicas; an instance scale replaces the component’s as a whole, key by key nothing is merged. scale and replicas exclude each other on one level (the render refuses both: replicas is a fixed number, scale lets the autoscaler choose), and component set --replicas is refused on a scaled instance. An external component takes no scale.
scale has min (0 to max, default 1), max (1 to 20, required) and on (required, exactly one trigger). The four trigger forms (on is written in quotes in the examples so that YAML 1.1 readers do not turn it into true; the renderer accepts it either way):
components: web: instances: prod: scale: {min: 2, max: 10, "on": {cpu: 70}} # average CPU use, in percent of the CPU requestcomponents: report: instances: prod: scale: {max: 6, "on": {memory: 80}} # average memory use, in percent of the memory requestcomponents: worker: consumes: [jobs] instances: dev: scale: {min: 0, max: 5, "on": {consumes: jobs, lag: 10}} # one replica per 10 pending messages of the events API jobscomponents: mailer: uses: [sessions] scale: {min: 1, max: 4, "on": {cache: sessions, list: jobs, length: 10}} # one replica per 10 entries of the cache list jobs- Resource triggers.
cpuandmemoryrender a plain HorizontalPodAutoscaler; the instance needs the matching resource request. They need a metrics source: the gitops repo declares it in.copier-answers.ymlwithresource_metrics: provided | install(defaultprovided, the cluster already serves resource metrics;installadds the pinned metrics-server as an operator,/platform:doctorchecks it). - Queue triggers.
consumes(the lag is the pending plus the unacknowledged messages of the consumer, read from the broker’s monitoring endpoint, which needs no credentials) andcache(the length of a list, read with the cache’s own credentials) render a KEDAScaledObject, and aTriggerAuthenticationfor a cache. KEDA is a platform operator, pinned and installed per cluster like NACK:operators/apps/keda.yamlappears when an instance has a queue trigger,operators: ownerleaves it to the cluster owner, and/platform:doctorhas a rowoperator keda. An external broker has no monitoring endpoint in its contract, so aconsumestrigger on one is refused. - Scale to zero.
min: 0is allowed for queue triggers only, since a CPU trigger has nothing to measure at zero replicas. It is refused for a component that is exposed or provides a request API (openapi, grpc or graphql), because a request would find no pod. Zero replicas are woken within a minute of the first message. - Timings. The queue is polled every 15 seconds. Scaling up follows the load at once; scaling down waits: a queue-scaled instance goes to zero 300 to 330 seconds after the queue empties, and to its minimum up to about 360 seconds after (about 6 minutes; the cool-down of 5 minutes starts when the queue is empty).
autoscaling. The environment propertyautoscalingis derived: it iskedafor an environment with a queue-triggered instance and null otherwise. It is exposed incplat model show, and/system:statusshows the replica count and bounds of a scaled instance (notautoscalingitself). Settingenvironments.<env>.autoscalingyourself fails with an unknown key.- Limits. The cache trigger connects without TLS, so an external cache that needs
rediss://is not supported. KEDA’s generated HPA copies theScaledObject’s labels, so Argo CD’s label-based resource tracking would treat it as extraneous; annotation tracking (the default since Argo CD 3.0) does not. - Argo CD. A scaled Deployment renders
replicas: <min>, and the environment’s ApplicationSet carries anignoreDifferencesentry for/spec/replicasof each scaled Deployment, so Argo CD does not undo the autoscaler and the application stays Synced. A render that dropsscaleremoves the autoscaler object and the entry (ADR-066, ADR-067).
Upgrading a resource
Section titled “Upgrading a resource”A database, cache or broker has a version, and an instance may carry its own under instances.<env>.version. /resource:upgrade <name> --to <major> [--env <env>] changes it for one environment as one pull request: dev first, staging after dev is verified, prod by hand. A database major step needs a backup from the last 60 minutes and upgrades in place; each step is verified before the next environment follows. While a rollout is under way the per-environment version shows where it stands, and the step on the last environment folds the entries back into the resource’s version. A bucket has no version. Staging and prod keep the engine image they run when the platform updates (a held pin) until their step. See Upgrades for the procedure, the refusals and the way back (spec 121).
Commands follow the nouns
Section titled “Commands follow the nouns”Every command names the thing it acts on; the same verbs work for a system and for a component. /component:fix is the quick-task pipeline: it ships a small change or a bug fix without a spec. /component:check is the read-only quality and security check.
| Noun | Verbs |
|---|---|
system |
new · set · status · spec · plan · ship · restore |
component |
new · add · set · remove (add and set take --provides and --consumes) · spec · plan · build · verify · check · ship · fix · release |
api |
list (check comes with spec 110) |
resource |
add · remove · backup · restore · upgrade |
cluster |
new · add · remove (/cluster:new, /cluster:add, /cluster:remove) |
instance |
promote <component> --to <env> · secret set <component>/<env> · verify |
issue |
triage · report |
platform |
doctor · update · fleet · runs · retro · ui · release |
The contract check of an API is not built yet: it comes with spec 110.
4.7 features on the model
Section titled “4.7 features on the model”| Layer | Feature today | Acts on | In 5.0 |
|---|---|---|---|
| What it is | /shared:new-app |
System and its components | system new |
/shared:new-service |
Component (a System with --gitops) |
component new |
|
/gitops:compose |
Component in a System | component add | set | remove |
|
/gitops:addon add postgres |
Resource | resource add |
|
/gitops:addon add observability |
Environment property | environments: in system.yaml |
|
| spec 110 contract check | API | api check |
|
| Where it runs | /gitops:promote, digest check (spec 106) |
Instance, next Environment; Release for prod | instance promote --to |
/gitops:secret |
Instance | instance secret |
|
/shared:status |
every Instance of a System | system status |
|
cplat smoke, deploy gate (spec 105) |
Instance | instance verify |
|
promotion: (spec 093), Kyverno policies |
Environment rules | environments: in system.yaml |
|
| Delivery | /shared:triage, /shared:report-issue |
Issue | issue triage | report |
/app:spec, /svc:spec |
Spec of a System or a Component | system spec, component spec |
|
/app:plan, /svc:plan |
Plan | system plan, component plan |
|
/svc:build, /svc:verify |
Criterion, Test, Verification | component build | verify |
|
/svc:ship, /app:ship, budgets (spec 102) |
Run | component ship, system ship |
|
/svc:quick-task, /svc:fix-bug |
Run without a spec | component fix |
|
/svc:release |
Release | component release |
|
| Feedback | run records, agent metrics, weekly retro | Runs as data; proposals as Issues | platform runs | retro |
| alert poll (spec 092), smoke failures (spec 091) | Instance → Issue | automatic | |
| Platform | /shared:doctor, /shared:update-service, /shared:fleet |
your setup, repo skeletons | platform doctor | update | fleet |
The model in 4.7
Section titled “The model in 4.7”As built in 4.7.0, in the platform’s own names:
- Composition was solid. The 4.7 concepts in the table below each had a file, a schema and an identity. The work was naming, not structure.
- Names drifted in four places: one thing had three names; the services file and the new-system manifest disagreed on service and component; add-ons were also called backing services; pin and image tag.
- Environment was the gap. It is the axis every delivery command works on (pin, promote, smoke, status, alerts), yet the list of environments was hard-coded in
render.pyand an environment’s settings were spread over four keys in one file, per-environment maps in another and the overlay paths. Service × environment and database × environment had no names. - Three relations were missing: which services use a library, which services provide or consume an API (only prose in the plan), and who owns what. 5.0 adds them.
| Concept today | Names in use | Defined in | Model noun |
|---|---|---|---|
| Product | product, app, application | gitops-app repo, app.yaml; app: in the new-app manifest |
System |
| Service | service; component in the new-app manifest | services.yaml entry, generated or external: |
Component |
| Shape | shape | shapes.yml, .copier-answers.yml |
Component type + lang |
| Library | library | a library-python repo |
Component, type library |
| Addon | addon, backing service | app.yaml addons:, uses: |
Resource (postgres) or Environment property (observability) |
| Secret | secrets, secretRefs | services.yaml, External Secrets |
Component config, per Instance |
| Environment | env, environment | ENVS in render.py; keys in app.yaml, services.yaml, overlays |
Environment |
| Pin | pin, image tag | overlays/<env>/<svc>/kustomization.yaml |
Instance version |
| Addon instance | (no name) | addons/<env>/<addon>/ |
Resource instance |
| Contract | contract | plan ## Contract, prose |
API |