Proposal: Configuration as code and environment promotion
Everything an operator configures — tenants, realms, clients, policies — is mutable only through the admin API (ADR-0021), and today only imperatively. Teams running more than one environment want that configuration declarative: versioned, reviewed, diffed, promoted. This specs config-as-code as a client of the admin API — no second write path, no new architecture.
Motivation
Identity configuration is production configuration: a mis-set redirect URI or a loosened policy is an outage or a security hole, and hand-applied changes survive neither review nor audit. Okta and Auth0 both grew Terraform providers because customers demanded them; for Vord the demand arrives with the M4 multi-tenant release, when the admin API surface is stable enough to build on.
Design
- The admin API is the single mutation surface (ADR-0021). Config-as-code is a client of it, never a second write path: an apply issues the same gRPC/REST calls an imperative caller would, authorized by the same Forseti scopes (ADR-0006, scoped admin roles per proposal 0016) — so nothing is expressible declaratively that is not expressible, and permitted, imperatively.
- A declarative resource format with three verbs: export current state, apply desired state, diff the two — and diff against live state is drift detection. Validation is server-side and identical to imperative calls; the format introduces no second validator to disagree with the first.
- A Terraform/OpenTofu provider generated over the same API. This is where API-first pays off — ADR-0021's automation consequence: the provider is generated from the API definitions, not hand-maintained against them.
- Environment promotion as reviewed diffs: dev → staging → prod realm/tenant configs move as artifacts whose diff is reviewed before apply. Secrets are never in config artifacts — artifacts carry secret references, and resolution stays in the deployment's secret store (proposal 0004).
- Every apply is an audited event (ADR-0022): who applied which artifact version, when, and what changed — the same trail imperative changes leave.
Alternatives considered
- A bespoke configuration DSL — rejected: a provider over the public API reuses the plan/review/apply ecosystem operators already run, and keeps the API the one contract.
- A GitOps reconciling operator only — deferred: the export/diff/apply primitive comes first and is what an operator (or Flux/Argo) would be built on; shipping only the operator would hide the primitive.
Graduation criteria
Export → edit → diff → apply round-trips a tenant's full configuration; drift detection flags an out-of-band imperative change; the Terraform/OpenTofu provider manages tenants, realms, clients, and policies in CI; every apply appears in the event stream; and a CI check proves a secret value can never appear in an exported artifact.
Backward compatibility
A client of the admin API — no schema or API change, and imperative administration remains fully supported (config-as-code is additive). The export format is versioned with the API, so an artifact declares which API version it round-trips against.
Open questions
- Registry naming and publication: the OpenTofu registry, the Terraform registry, or both.
- Whether promotion should ever carry principal data — currently no: promotion is configuration only; moving users between environments is migration, which is proposal 0014's.