02. Multi-environment deployment configuration
Multi-environment deployment config: org → team → service → env layering
Exercises ranked */** defaults, includes, close(), constraint atoms, pack, filter, why
Rendered from
use-cases/02-deploy-config/README.md
in the engine repository. The models, the expected output and the
check.sh that drives the CLI over all of it are in
use-cases/02-deploy-config/.
A fleet of four services (web, auth, billing, reports) deployed
to three environments (dev, staging, prod) under four layers of
authority (org policy, team defaults, service catalog, per-environment
overlays), composed with @ includes and gated in CI by aontu vet.
This is the shape Helm values files and Kustomize overlays are written
for: many writers, one document, and any contradiction between them
must be a loud, located error rather than a last-writer-wins surprise.
Everything quoted below is real CLI output (ANSI stripped), reproduced
by check.sh.
Files
| File | Layer / role |
|---|---|
org-policy.aon | org: workload shape (closed), org-rank *** defaults |
team-defaults.aon | team: ** defaults, release train, on-call |
fleet.aon | service catalog: the source pack() iterates |
envs/{dev,staging,prod}.aon | overlays: * defaults via &: spreads + concrete pins |
stack.aon | entry: includes, pack() per env, envguard, filter() alerts, rollout arithmetic |
guardrails.aon | org bounds, enforced by vet over the built output |
request-schema.aon | gate for agent-emitted change candidates (data/*.json) |
probes/*.aon | one file per behaviour below, each pinned by check.sh |
Probes that reach @"../stack.aon" resolve an include outside their
own entry root; the CLI warns once per such include, --trust system
keeps it, and --include-root confines it.
The model tree
stack.aon is the entry: four layers of authority unified into one
document. deploy holds one map per environment and each of those a
map of workloads; fleet is the service catalog pack() iterates,
org, team and environments the layers it draws on, and alerts,
rollout and envguard what the document DERIVES from all of it.
$
├── alerts
│ ├── auth (3)
│ └── billing (3)
├── defs
│ └── workload (9)
├── deploy
│ ├── dev (3)
│ ├── prod (3)
│ └── staging (3)
├── envguard
│ ├── dev (3)
│ ├── prod (3)
│ └── staging (3)
├── environments
│ ├── dev (2)
│ ├── prod (2)
│ └── staging (2)
├── fleet
│ ├── auth (3)
│ ├── billing (3)
│ ├── reports (3)
│ └── web (3)
├── org
│ ├── name "acme"
│ ├── registry "registry.acme.internal"
│ └── runbookBase "https://runbooks.acme.internal"
├── release
│ └── tag "2025.34.2"
├── rollout
│ └── billingProdMaxSurge 13
└── team
├── name "payments"
└── oncall "#payments-oncall"
aontu view doc --depth 2 stack.aon draws it, and check.sh pins it
with --out --check. A key with (n) after it is a container the
depth bound stopped at, and n is how many keys are not drawn; a
leaf carries its canon, which is the kind of thing it is rather
than its value.
Design: which features carry it
-
Ranked preferences are the layering mechanism. Generation picks the lowest rank: the fewer stars, the stronger the default.
*warnbeats**debugbeats***info, in any statement order, and a concrete value beats them all (probes/rank-ladder.aon). So the ladder is: org***, team**, environment*, service pin concrete. Two disagreeing defaults of the same rank are a conflict ([aontu/pref_rank_clash]), which is governance-friendly: two teams cannot both claim the same rank silently.Four layers, six files, one
aontu stack.aon, and every value lands where the ladder says: dev logsdebug(team**beats org***), prod logswarn(env*), prodauthlogsinfo(concrete pin), billing runs 12 while the prod env default is 4. Order-independent, idempotent, no last-writer-wins anywhere. -
pack()over hidden tables (environments,fleet) generates the per-env blocks and per-env workloads, so an environment or service cannot exist anywhere except its table: no drift by construction. The catalog’s static facts arrive throughworkloads: copy($.fleet)beside the generator; the pack template adds the generated fields (service: key()and the image name). -
close()on the workload shape (applied inside the pack template asclose($.defs.workload) & {...}) makes every overlay key-checked. A misspelt overlay key is refused at its exact path, not silently ignored:[aontu/closed]: Cannot resolve value at path $.deploy.prod.workloads.auth.replcas Cannot add to closed structure. ... --> probes/typo-overlay.aon:7:41deployitself is left open. A hiddenenvguard: hide($.deploy & close(pack($.environments, {})))seals the environment set instead: a meet of a clone ofdeploywith a closed pack of the env table, one line long, that never touches the output tree. A typo’ddeploy: prod2:overlay fails with[aontu/closed]at$.envguard, pointing at the offending line.close(pack(...))also composes with an overlay directly (probes/close-pack-absorb.aon); the overlay lands on the generated child:deploy: close(pack($.environments, _ & { x: ***1 | integer })) deploy: prod: x: 2{"deploy": {"dev": {"p": false, "x": 1}, "prod": {"p": true, "x": 2}}} -
Conflicts are located and attributable. Two files pinning billing at 12 and 6 produce
[aontu/scalar_value]at$.deploy.prod.workloads.billing.replicasnaming both files with line and column (probes/conflict.aon). A layer cannot remove a lower layer’s key either: unification only adds information, sonullover a boolean is[aontu/scalar_kind](probes/remove-key.aon). -
whyis multi-layer attribution, at plain paths and through the generator alike:$ aontu model why '$.defs.workload.logLevel' stack.aon $.defs.workload.logLevel = **"debug"|***"info"|string 1. ***"info"|string .../org-policy.aon:31:15 2. **"debug"|string .../team-defaults.aon:12:28 $ aontu model why '$.deploy.dev.workloads.web.logLevel' stack.aon $.deploy.dev.workloads.web.logLevel = **"debug"|***"info"|string 1. **"debug"|***"info"|string .../team-defaults.aon:12:28 $ aontu model why '$.deploy.prod.workloads.billing.replicas' stack.aon $.deploy.prod.workloads.billing.replicas = 12 1. *4|integer .../envs/prod.aon:12:15 (spread) 2. 12 .../envs/prod.aon:18:22 3. ***2|integer .../org-policy.aon:30:15A value that reached a generated path by being copied is reported as the value the author wrote, at the line they wrote it on, and a spread contribution carries its real position.
-
filter()+pack()derive the prod paging policy from the catalog: onlycritical: trueservices get an alert route, so adding a critical service to the catalog creates its paging route in the same commit. -
Constraint atoms live in
guardrails.aonandrequest-schema.aonand are enforced withvetover the built output, where every value is concrete: build, then vet the built JSON. Exit codes are a contract (0/1/2/3/4); findings carry the code, the path, expected vs actual, and both the data and schema locations:$.replicas: constraint [conflict] [aontu/constraint]: Cannot unify values at path $.replicas expected: integer&min(1)&max(24) actual: 64 data: data/rollout-bad.json:5:15 (64) schema: request-schema.aon:17:23 (integer&min(1)&max(24))--format jsonand--format sarifemit the same findings for machines. A hallucinated field in an agent candidate is refused by the schema’sclose()with the exact key path, andlength()bounds the free-text rationale.must()carries the author’s message into the report, which is what a policy file wants:[aontu/must]: Cannot unify values at path $.replicas This value fails an evaluate-only check written with must(). The author's message is: prod workloads need >= 2 replicas for zero-downtime rolloutsguardrails.aonspreads the workload rules per environment from one hiddencommonblock; the stacked spelling, one generic block underdeploy: &: {workloads: &: ...}plus a prod-only floor (probes/spread-crosswire.aon), vets the built output as valid too. Defaults and bounds also compose inside a model:replicas: *2 | integerbesidereplicas: min(1) & max(24)generates 2 (probes/lost-default.aon), andreplicas: *2 | (integer & min(1) & max(24))refuses an override of 40 with[aontu/empty](probes/bypassed-bound.aon), so one field carries both a default and an enforced bound. -
Projection over the catalog.
unique(port)states “every service port must be unique” directly on the fleet (probes/unique-port.aon: two services sharing 8080 are refused with[aontu/constraint]).pick($.fleet, port)produces the list where the ports are wanted as a value rather than only checked (a firewall rule, say), andleast/greatest/sumfold it (probes/pick-ports.aon). -
Derived arithmetic lives in
rollout:, a plain map outside the generated tree, and reads a concrete pin: billing’s prodreplicas + 1is 13. The operand has to be concrete; against a ranked default such as*4 | integerthe sum is[aontu/mapval_no_gen](probes/surge-from-default.aon).*is the preference marker, not multiplication:$.replicas * 2is a parse error ([aontu/unexpected],probes/multiply.aon), and doubling is writtenmul($.replicas, 2). -
setcannot write a contradiction. It vets before writing (verdict: valid/wrote:), and refuses a change that contradicts a pinned value with a located conflict, leaving the file untouched: a safe primitive for an agent to hold. The overlay it writes is a file the entry does not include:setcomposes entry and overlay itself, and an entry that includes both makes the change effective. -
Canon is deterministic.
--canonoutput is goldened byte-for-byte incheck.sh: one line, reparseable, with defaults, ranks and spreads still visible, so the policy can be read as a whole and hashed for drift detection.
What check.sh proves
- The build matches
expected/stack.json(four layers, three environments, four services), and--canonmatchesexpected/stack.canon.txtbyte for byte. - Values land where the rank ladder says (
aontu model get): prod billing replicas 12 (concrete pin beats the env default); prod web 4 and dev web 1 (env*defaults); devlogLeveldebug(team**beats org***); prodlogLevelwarn(env*beats both); prod authlogLevelinfo(concrete pin beats every rank); stagingtracingtrue(team default survives with no env override); prodtracingfalse(concrete env mandate). $.rollout.billingProdMaxSurgeis 13:replicas + 1from a concrete pin, outside the generated tree.$.alerts.billing.runbookis generated from the catalog byfilter()overcritical: trueandpack().whyon$.defs.workload.logLevelnames bothorg-policy.aonandteam-defaults.aonwith their ranks and lines; on$.deploy.prod.workloads.billing.replicasit namesenvs/prod.aonand the pin 12; on the generated$.deploy.dev.workloads.web.logLevelit namesteam-defaults.aonand the winningdebug.vet guardrails.aon expected/stack.jsonisverdict: valid.- Agent candidates:
rollout-good.jsonis valid;rollout-bad.jsonis refused with three located[aontu/constraint]findings ($.service,$.replicasagainstmax(24),$.reasonvialength);rollout-unknown-key.jsonis refused byclose()at$.forceRestart([aontu/closed]);--format jsonemits"code": "constraint". - The rank ladder golden (
probes/rank-ladder.aon):*beats**beats***, concrete beats all, in any statement order. - Two disagreeing defaults of equal rank are
[aontu/pref_rank_clash](probes/equal-rank.aon). - A cross-file conflict is
[aontu/scalar_value]at$.deploy.prod.workloads.billing.replicas, naming bothconflict-capacity.aonandconflict-costcut.aon(probes/conflict.aon). - A misspelt overlay key is
[aontu/closed]at$.deploy.prod.workloads.auth.replcas(probes/typo-overlay.aon). - An unknown environment is
[aontu/closed]at$.envguard, namingprod2(probes/env-typo.aon). - A layer cannot remove a lower layer’s key:
nullover a boolean is[aontu/scalar_kind](probes/remove-key.aon). $.replicas * 2is a parse error,[aontu/unexpected](probes/multiply.aon);$.replicas + 1against*4 | integeris[aontu/mapval_no_gen](probes/surge-from-default.aon).unique(port)refuses two services sharing a port with[aontu/constraint]at$.fleet(probes/unique-port.aon);pick($.fleet, port)produces the port list andleast()reads its floor, 8080 (probes/pick-ports.aon).- Stacked spreads at different depths (
probes/spread-crosswire.aon) vet the built output as valid. must()fires with the author’s own message,[aontu/must](probes/must-floor.aon).- A ranked default beside a bound generates the default:
replicas: *2 | integerwithreplicas: min(1) & max(24)gives 2 (probes/lost-default.aon); an override outside a disjoined bound is refused with[aontu/empty], exit 1 (probes/bypassed-bound.aon). close(pack(...))with an overlay merges onto the generated child, matchingexpected/close-pack-absorb.json(probes/close-pack-absorb.aon).set: an override of a default ($.deploy.prod.workloads.web.replicas=8) is vetted (verdict: valid), written to the overlay (wrote:), and visible on re-evaluation through an entry that includes both files; a change that conflicts with a pinned value (billing replicas 14 against the pin 12) is refused with[aontu/scalar_value], exit 1, and no overlay file is written.
Running it
From this directory, ./check.sh runs all 37 assertions and exits 0.
The pipeline the checks drive, by hand:
aontu stack.aon > built.json # build the deployment document
aontu vet guardrails.aon built.json # org bounds over the built output
aontu vet request-schema.aon data/rollout-good.json # gate an agent candidate