- Step 3 contract fields table: stale uses/module → real id/name/environment/infrastructure (REQ-276) - Step 4 caller: add environment: dev to match Step 2 (REQ-277) - Step 5 stage 8: (dev only) → (autonomous in dev; higher envs apply after HITL) (REQ-278) - Step 8: rewrite with Shape A destroy-then-rebuild + Shape B cross-ref (REQ-279) - Per-env section: add Shape B lead sentence (REQ-280) - Reference table: @v1.19 wording + .yaml→.yml extension fix (REQ-281) - Tests: rename no-field-editing → both-promotion-shapes + new destroy-on-env-change test (REQ-290) ---ci--- project: acdl phase: 1 milestone: v1.24 status: execute requirements: [REQ-276,REQ-277,REQ-278,REQ-279,REQ-280,REQ-281,REQ-290] ---/ci---
21 KiB
Consumer Guide — Declare intent, deploy to AWS
This guide walks a consumer through creating their pipeline and defining a
contract that deploys any Nova module to AWS. It is generic across all
modules in the registry; static-assets is the worked example, but every
step applies to microservice and any future module.
The model
Consumers have their own repos and consume Nova by writing a contract that declares infrastructure (one or more modules), an environment, and inputs. The consumer declares a contract (which infrastructure, which environment, which inputs); the Nova platform owns the pipelines, modules, engine adapter, and evidence stream.
You do not write infrastructure modules, workflow YAML, or adapter code. You write a contract YAML file and the platform does the rest. Your repository contains only your application code, your contracts, and your CI definitions.
flowchart LR
A["your repo<br/>(app code + contracts + CI definitions)"] -->|uses: nova/.github/workflows/deploy.yml@v1.19| B
B["platform runners<br/>(modules + pipelines + adapters + schemas)"] -->|contract -> resolver -> stack -> adapter<br/>-> security checks -> infrastructure plan -> policy checks<br/>-> confidence -> apply -> evidence event| C
C["your resources in AWS"]
Versioning the uses: reference
The central deployment pipeline is always versioned with floating MAJOR
and MINOR tags (e.g. nova/pipelines/contract.yml@v1.19). Version
constraints cannot be expressed inside the contract, so the tag in
uses: is the only immutability lever a consumer has. See
Versioning for the full rationale.
Unversioned references are discouraged. Do not use @main or a bare
nova/pipelines/contract.yml.
Prerequisites
These are the only prerequisites for a consumer repo. You do not need an AWS account, infrastructure tooling, or a runner key — those are platform-managed. See Environments.
- A consumer GitHub repository for your application code + contracts.
- A platform-managed environment bound to your repo. The platform team provisions the AWS account, network, state backend, and IAM role. If no environment is bound, your first pipeline run emits a friendly onboarding prompt. See Environments.
- Authorization to reference the central pipeline. Onboarding grants
your repo the right to
uses: nova/.github/workflows/deploy.yml@v1.19. Contact the platform team if you have not been onboarded.
Step 1 — Create a consumer repo
Create a repository for your application. The top level holds your app
code; your contract lives at .nova/contract.yml. Example for a static
site:
my-static-site/
index.html
assets/
style.css
logo.png
.nova/
contract.yaml
.github/
workflows/
deploy.yml
Example for a microservice:
my-microservice/
app.py
Dockerfile
.nova/
contract.yaml
.github/
workflows/
deploy.yml
Your app code lives at the top level. Your contract lives at
.nova/contract.yml regardless of the module you deploy. Your CI
definition lives at .github/workflows/deploy.yml.
Step 2 — Reference the central pipeline
In your CI workflow (.github/workflows/deploy.yml), reference the central
Nova deployment workflow with a versioned tag (floating MAJOR + MINOR):
jobs:
deploy:
uses: nova/.github/workflows/deploy.yml@v1.19
with:
contract: .nova/contract.yml
environment: dev
The versioned tag is the only immutability lever — the consumer's CI workflow
pins the platform version. The contract itself no longer carries a uses:
field; the version pin lives in the CI workflow reference.
Step 3 — Define the contract
Write .nova/contract.yml. The static-assets example:
environment: dev
id: assets
infrastructure:
static-assets:
inputs:
bucket_name: my-static-site-assets
region: us-east-1
version: 1.0.0
name: static-assets
A microservice example:
environment: dev
id: msvc
infrastructure:
microservice:
inputs:
env:
LOG_LEVEL: info
image: my-registry/my-microservice:latest
port: 8080
version: 1.0.0
name: microservice
Contract fields
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | Short operational acronym (3-6 chars, lowercase + digits + hyphens). Becomes stack.name: the Terraform state key (spike/<id>/<env>/terraform.tfstate), the outbox event identity, and the resource naming prefix. Stable across deploys and environment promotions. |
name |
string | yes | Full human-readable stack name. Becomes stack.title: the display name in PR comments, evidence records, and dashboards. |
environment |
string | yes | The platform-managed environment to deploy to (dev, qa, prod, or dr). See Environments. |
infrastructure |
object | yes | Map of modules to deploy, keyed by module name (matching a registry key in modules/registry.json). Each entry carries an optional version (defaults to latest published) and per-module inputs. One entry = single-module deploy; N entries = multi-module manifest. |
Module inputs
Each module declares its inputs in its interface.json (primitives) or
composition.json (modules). Consult the module catalog for
the full list, or read the module's own README under modules/l1/<name>/
or modules/l2/<name>/. Each module also has an examples/ directory
with validated consumer contract examples (simple.yaml + complex.yaml
- variation files) that demonstrate real usage — see the module's
## Examplessection.
The contract is validated against the contract schema. An invalid contract (missing field, unknown module, wrong type) fails at the validate-contract stage with a clear error.
Step 4 — Run the pipeline
You do not run platform scripts locally for the happy path. The central deploy workflow is a reusable workflow that the platform runners fetch and execute for you.
The consumer CI definition
Add a thin workflow file to your repo that invokes the reusable Nova
deploy workflow with a versioned tag (.github/workflows/deploy.yml):
name: deploy
on:
push:
branches: [main]
jobs:
deploy:
uses: nova/.github/workflows/deploy.yml@v1.19
with:
contract: .nova/contract.yml
environment: dev
That is the entire consumer-side workflow. When you push to main:
- The platform runner resolves
uses: nova/.github/workflows/deploy.yml@v1.19to the reusable workflow at the pinned tag. - A platform-provided runner checks out your repo.
- The runner checks out the Nova platform repo into the workspace — this is how the pipeline fetches the platform code at run time. You never clone the platform repo yourself.
- The runner installs the runtime dependencies the platform requires.
- The runner invokes
scripts/run_platform.shagainst your.nova/contract.yml.
You see the streamed output (infrastructure plan, policy-check results,
confidence signal) in your run logs. The --check-only and --plan-only
flags are platform-side modes visible in the pipeline logs; you do not pass
them yourself — the reusable workflow selects the mode based on the
environment in your contract (dev = full apply; higher environments
hold for attestation).
Local validation (optional)
A consumer may clone the Nova platform repo to run --check-only against
their contract before pushing — this is optional and not required for the
happy path. If you do this, the runtime dependencies must be installed
locally, and any AWS credentials follow the
Credentials override model: a
static key in .env.secrets (gitignored) is rotated out of band by you
— the platform guarantees daily rotation for platform-runner runs, not for
locally-held copies.
bash scripts/run_platform.sh --check-only path/to/your/.nova/contract.yml
Step 5 — What the pipeline does
Each stage of the central deployment pipeline:
flowchart TD
S1["validate-contract<br/>schema check"] --> S2
S2["resolve-stack<br/>contract -> Target Stack"] --> S3
S3["security checks<br/>(adapter)"] --> S4
S4["infrastructure plan<br/>(adapter compiles the stack)"] --> S5
S5["policy checks<br/>(adapter -> PolicyCheckResult)"] --> S6
S6["confidence<br/>score + band (dev >= 0.50)"] --> S7
S7["evidence event<br/>to the audit outbox"] --> S8
S8["infrastructure apply<br/>(autonomous in dev;<br/>higher envs apply after HITL)"]
- validate-contract — validates your contract YAML against the contract schema. Fails fast on missing fields, unknown modules, or wrong types.
- resolve-stack — the contract resolver resolves your contract to a Target Stack instance. It loads the module's pattern, expands its children, wires your contract inputs to the children's inputs, and emits a stack JSON instance.
- security checks (adapter) — security checks run on the resolved stack before any infrastructure is planned.
- infrastructure plan (adapter) — the engine adapter compiles the stack to an infrastructure plan. You see the plan in your run logs.
- policy checks (adapter) — policy checks run on the plan. The results
are normalized to
PolicyCheckResultrecords. Each result has a severity, rule ID, and pass/fail status. - confidence — the confidence signal computes a score from 6 inputs
(policy, validation, freshness, source, history, NFRs). For
dev, the threshold is ≥ 0.50. If the band ispass, the pipeline proceeds. - evidence event — a hash-chained evidence event is written to the audit outbox.
- infrastructure apply (autonomous in dev; higher environments apply after HITL attestation) — the infrastructure plan is applied, creating the resources in your AWS account. An evidence event for the apply is recorded.
Step 6 — What gets created
After a successful dev run, the resources declared by your module's
pattern exist in your AWS account, and an evidence event is recorded.
For the static-assets example:
- An S3 bucket named
my-static-site-assetsinus-east-1with versioning enabled. - A CloudFront distribution with the S3 bucket as the origin (via Origin Access Control) and HTTPS redirection.
- A WAFv2 Web ACL (CloudFront-scoped) associated with the distribution.
- An evidence event in the audit outbox with the contract ID, stack
name (
static-assets), confidence score, and band. - A confidence band of
pass(score ≥ 0.50 for dev).
For other modules, consult the module's README
(modules/l1/<name>/README.md or modules/l2/<name>/README.md) for the
exact resources created.
Step 7 — Upload your content (static-assets example)
The platform provisions the infrastructure; you upload your content. For
the static-assets module:
aws s3 sync ./assets s3://my-static-site-assets/ --acl public-read
For a microservice, the platform provisions the ECS service and ALB; you
push your container image to the ECR repo the platform created.
Step 8 — Promote to qa / prod
There are two supported promotion shapes. Both are valid; pick the one that fits your repo's workflow.
Shape A — edit the environment field (destroy-then-rebuild)
Change environment in your contract (the infrastructure stays the same).
The contract id stays stable, so the platform knows this is the same
stack moving to a new environment:
id: assets
name: static-assets
environment: qa # QA attestation + confidence >= 0.75
infrastructure:
static-assets:
version: "1.0.0"
inputs: { ... }
What happens when you change environment: dev → environment: qa:
the platform detects that the environment changed on a known contract id.
Before building the new environment, it destroys the prior environment's
resources (Terraform state key spike/{id}/dev/) and records an evidence
event for the destroy. Only then does it apply the new environment (state
key spike/{id}/qa/). There is no orphan path — if the destroy fails,
the pipeline fails closed (no apply runs, no resources are left behind).
This is full lifecycle management: the platform never creates a state
where prior-environment resources are abandoned.
Higher environments require human attestation (a platform-runner deployment approval) and higher confidence thresholds. See Environments for the full table.
Note: the destroy-then-rebuild runs within the same AWS account (the current platform scaffold uses one account). Cross-account promotion (separate accounts per env) is a future milestone.
Shape B — per-environment caller workflows (no editing)
Alternatively, keep one contract per environment (or one contract + the
environment workflow input) and run the matching CI job to promote. This
avoids the destroy step because each environment has its own state from the
first deploy. See Per-environment deployment
below for the full pattern.
Step 9 — Compliance extensions
Each module lists compliance extension points for the future compliance
milestone (GDPR, SOX, SOC2, DORA). See each module's README under
modules/l1/<name>/README.md or modules/l2/<name>/README.md for the
per-module extension points. Common examples:
- KMS key — shared encryption key for SSE.
- S3 access logs — access logging to a separate audit bucket.
- Object Lock — 7-year immutable retention for evidence.
- Public access block — prevent data exfiltration.
Reference
| Resource | Path | Description |
|---|---|---|
| Central deployment pipeline contract | pipelines/contract.yml |
The pipeline stages your contract references. |
| Reusable deploy workflow | .github/workflows/deploy.yml |
The workflow your repo invokes via uses:. |
| Contract schema | schemas/contract.schema.json |
JSON Schema for consumer contracts. |
| Stack schema | schemas/stack.schema.json |
JSON Schema for the resolved stack instance. |
| Module catalog | modules/ | All primitives and modules. |
| Sample contract | contracts/static-assets.yml |
The reference example contract (used with caller workflow @v1.19). |
| Sample contract | contracts/microservice.yml |
The microservice example contract (used with caller workflow @v1.19). |
| Module examples | modules/<name>/examples/ |
Validated per-module example contracts (simple.yaml + complex.yaml). |
| Contract resolver | core/contract_resolver.py |
Resolves contracts to stack instances. |
| Angine adapter | adapters/terraform/adapter.py |
Compiles stack instances to infrastructure. |
| Platform pipeline runner | scripts/run_platform.sh |
The pipeline runner (platform-side; consumers do not invoke it directly). |
| Environments | environments/ | Platform-managed environments + onboarding. |
| Versioning | pipeline/versioning | The uses: tag + module versioning. |
| Platform README | README.md |
How the platform works + how to run the platform repo locally. |
| Credentials & zero-trust | README.md#credentials--zero-trust |
The OIDC/ABAC default + static-key override model. |
Decommissioning a stack
When a consumer needs to tear down a deployed stack, the platform provides a decommission mode on the same deploy pipeline. The decommission process is a 2-step pipeline with HITL SRE gates to prevent accidental destruction:
-
Request a change request (CR): Contact the platform team to create a change request in the platform CMDB (DynamoDB
nova-change-requeststable). The CR must be approved before decommission can proceed. The CR includes the consumer repo, contract ID, and the reason for decommission. -
Trigger decommission: Update the consumer's deploy workflow call to use
mode: decommissionwith thechangeRequestIdinput:uses: nova/.github/workflows/deploy.yml@v1.19 with: contract: .nova/contract.yml mode: decommission changeRequestId: "CHG0678912" -
Step 1 — Disable deletion protection (HITL SRE gate): The pipeline validates the CR ID against the CMDB (status must be
approved). Then it resolves the contract withdeletion_protection: falseinjected into all resources and runsterraform plan+terraform apply. This removes theprevent_destroylifecycle meta-argument from all resources. An SRE must approve this step via the GitHub environmentdecommission-gate-sre. -
Step 2 — Zero counts + destroy (HITL SRE gate): The pipeline applies
decommission_transformwhich sets all scalable counts to 0 (desired_count=0,min_capacity=0,max_capacity=0) anddeletion_protection=falseon all resources. Then it runsterraform plan+terraform applywhich destroys all resources (now that deletion protection is off and counts are zeroed). A second SRE must approve this step via the GitHub environmentdecommission-destroy-sre. -
Confirmation: The pipeline confirms the stack is destroyed (terraform state is empty for the stack).
What happens to the per-stack CMK?
The per-stack CMK is not immediately destroyed — it enters a deletion
window (default 30 days, configurable via the deletion_window_days input).
This ensures any encrypted data can still be decrypted during the deletion
window if needed. The CMK is permanently deleted after the window expires.
What happens to the uptime monitoring?
The uptime monitoring stack (deployed with separate state) is not automatically destroyed by the decommission. It must be destroyed separately (or left running to monitor the decommissioned stack's endpoints going dark).
Per-environment deployment
This is Shape B (the alternative to Shape A's edit-and-destroy path in Step 8). Shape B avoids the destroy step because each environment has its own state from the first deploy — no prior environment to tear down.
Nova supports a promotion-without-editing model: you do not edit the
environment: field in a contract to promote dev → qa → prod → dr.
Instead, there is one CI job per environment, each pointing at its
respective contract (or the same contract + the environment workflow
input). Promotion = running the matching job.
Two shapes (both supported)
Shape 1 — per-environment contract files: a consumer repo has one
contract per environment (e.g. .nova/static-assets.dev.yml,
.nova/static-assets.qa.yml, …). Each sets environment: to its own
name and uses interpolation so env-specific values differ automatically:
environment: qa
id: assets
infrastructure:
static-assets:
inputs:
bucket_name: acdl-${env.environment}-${contract.id}-${env.account_id}-${env.region}
region: ${env.region}
version: 1.0.0
name: static-assets
Shape 2 — single contract + environment workflow input: the
reusable deploy workflow (nova/.github/workflows/deploy.yml@v1.19)
declares an environment input. When non-empty, it overrides the
contract's environment field at load time (before interpolation), so
the same contract can be promoted by passing a different environment:
# .github/workflows/deploy-qa.yml (caller workflow)
on: workflow_dispatch:
inputs:
approve_qa:
description: "Set to true to approve the QA promotion"
type: boolean
required: true
jobs:
deploy-qa:
uses: nova/.github/workflows/deploy.yml@v1.19
with:
environment: qa
contract: .nova/contract.yml
One job per environment
A consumer repo's .github/workflows/ directory has one caller workflow
per environment:
| File | Environment | Gate |
|---|---|---|
deploy-dev.yml |
dev | autonomous (no gate, confidence ≥ 0.50) |
deploy-qa.yml |
qa | QA HITL (approve_qa workflow_dispatch input; github.actor is the approver of record) |
deploy-prod.yml |
prod | SRE HITL (approve_prod; separation-of-duties enforced) |
deploy-dr.yml |
dr | SRE HITL (approve_dr) |
Promotion = running the matching job. No environment: field editing.
The approver identity is recorded to the DynamoDB outbox
(approver_qa / approver_prod / approver_dr) and the separation-of-
duties check blocks a prod promotion when approver_qa == approver_prod
(see core/hitl_matrix_design.md).
Interpolation reference
| Token | Resolves to | Example |
|---|---|---|
${env.environment} |
the environment name (dev/qa/prod/dr) | qa |
${env.region} |
the environment's AWS region | us-east-1 |
${env.account_id} |
the environment's AWS account id | 123456789012 |
${env.state_backend.bucket} |
the environment's state bucket | nova-qa-state |
${env.network.vpc_cidr} |
the environment's VPC CIDR | 10.1.0.0/16 |
${contract.id} |
the contract's operational acronym | assets |
${contract.environment} |
the contract's environment field | qa |
${contract.inputs.<name>} |
a contract input value | (as declared) |
Unknown tokens raise ValueError (fail loud). Expansion is recursive
(nested map/list values expand too).