Y-IaC: Technical Architecture
How Y-IaC works: an entrypoint stub, a Python CLI, generated Terragrunt stacks and one root.hcl, shown as three animated walkthroughs of a single run.
The most important architectural fact about Y-IaC is that a consumer repo contains no Terraform and no Terragrunt logic at all. It has YAML, an 86-line entrypoint.py stub, and a generated root.hcl that it never edits. Everything else is produced at run time from the shared repository. That’s what makes the platform consistent, and it’s also why a bug in the shared path is a bug in every consumer.
System context and component architecture
At the highest level there are two kinds of repository and one execution path. Y-IaC holds the modules, the CLI and the reusable workflows; each infra-* consumer holds only YAML. The same CLI runs on a laptop and on a GitHub Actions runner, and only the action (plan or apply) changes between review and deployment. Open any part to see what’s inside, pick a tab for credentials, logs, gates or delivery, or play one of the use cases:
Laying out the diagram…
Y-IaC system architecture
- Engineer (author + reviewer)
- GitHub (repos + Actions)
- infra-* consumer (YAML only): resources.yaml, common_vars.yaml, config.yaml, terragrunt-cicd.yaml
- Y-IaC repo (shared platform): CLI package, Terraform modules, Unit templates, Config catalog
- GitHub Actions (reusable workflows): PR and push, Drift workflows, Workflow artifacts, GitHub secrets
- Laptop or CI runner (one process tree): GitHub-hosted runners can't reach private endpoints; those consumers need a self-hosted runner.
- Python CLI (entrypoint.py): entrypoint.py stub, Bootstrap, Parse + validate, Export + authenticate, Stack generator, Redaction, Reports + logs
- Terragrunt stack (generated): terragrunt.stack.hcl, Generated units, Merged common_vars, root.hcl
- Terraform + providers (per unit)
- Plan gates (policy, security, cost): OPA, Trivy, Wiz CLI, Infracost, TFLint hook
- .env or az login (laptop identity)
- Remote state (one file per unit)
- Azure Blob (most consumers)
- S3 (infra-aws)
- GCS
- Targets (clouds + SaaS)
- Azure (most consumers)
- AWS (infra-aws)
- GCP
- SaaS providers (GitHub, JFrog, Grafana, ZPA)
- External services (registry, gates, alerts)
- Terraform registry (providers)
- Wiz service
- Infracost API
- Microsoft Teams (webhook)
- Jira (REST API)
Generate a stack: Inside the CLI: from one command to the provider and backend files Terraform runs with.
- The stub starts: One command in a consumer runs the 86-line stub, the same file in every consumer.
- Bootstrap the shared module: The stub hands over to the CLI from the Y-IaC checkout; bootstrap re-syncs root.hcl and friends when the hash changes.
- Validate the YAML: Paths, providers, the repo-to-subscription guardrail and duplicate entries are checked before any login.
- Export and authenticate: CLOUD, ENVIRONMENT, STACK and gate variables are exported, and ARM_* and BACKEND_* credentials are resolved.
- Write the stack file: resources.yaml becomes terragrunt.stack.hcl. A stack with 40 key vaults is still one unit.
- Merge the variables: Environment and stack common_vars.yaml are merged where units will find them first, so overrides apply.
- Expand the units: Each unit folder is generated and finds the merged variables with find_in_parent_folders.
- root.hcl picks the backend: Every unit includes root.hcl, which reads the backend entry whose repo matches the folder name.
- Pinned provider versions: Only the providers the stack lists appear, each pinned from the shared catalog.
- Ready for Terraform: With provider.tf and backend.tf in place, Terraform runs against the shared module source.
Plan a pull request: From a YAML change to a plan summary the reviewer reads.
- Open a pull request: A YAML entry is added and a pull request opened over HTTPS (443, default). No HCL is written.
- Detect changes: The consumer workflow calls Y-IaC's detect-changes at the pinned ref; changed files become a stack matrix.
- Plan job per stack: Each changed stack gets a terragrunt-ci job that checks Y-IaC out and runs the CLI with plan.
- The CLI builds the stack: The steps of Generate a stack run in order; Terraform gets the generated provider and backend files.
- Read state: Each unit has its own small state file, so units lock independently.
- Refresh against the cloud: Terraform compares the YAML-derived configuration with what really exists.
- Plan through the gates: Plan JSON is redacted, then OPA, the scanner and cost run on it (see Plan gates).
- Summarise the plan: The summary job reads per-unit add/change/destroy tables and gate results from the artifacts.
- Review the plan: The reviewer reads the plan, not just the YAML diff, and merges to main.
Plan gates: Policy, security and cost checks on the same redacted plan JSON.
- Redact the plan: Secrets are removed before Wiz, Trivy, OPA or an artifact upload sees the plan.
- Fan out to the gates: Each gate runs at advisory, soft-mandatory or mandatory; the scanner is chosen with iac_scanner.
- Wiz scans the plan: The Wiz CLI calls the service with WIZ_CLIENT_ID and WIZ_CLIENT_SECRET; without them it warns and skips.
- Price the change: When enabled (off by default), Infracost prices the plan with an API key.
- Collect the results: Each gate's outcome goes into the per-unit summary the reviewer reads.
- Keep the evidence: Plan files, plan JSON and scanner reports are uploaded with retention-days: 7.
Approve and apply: After merge: an environment approval releases the apply, then a plan proves it converged.
- Waiting for approval: Each apply job waits on its GitHub environment's protection rules; jobs run one at a time (max-parallel 1).
- Approver decides: A required reviewer for the environment approves or rejects the job.
- Not applied (reject): Nobody approves, or the approver rejects. The plan artifacts stay on the run as a record.
- Apply released (approve): terragrunt-cd runs the same CLI with apply; only the action differs from the reviewed plan.
- Same CLI path (approve): Identical CLI, root.hcl and modules, so what was reviewed is what runs.
- Lock the state (approve): A lock held by a dead run is released only if opted in and older than the minimum age.
- Change the cloud (approve): Transient errors are retried by root.hcl: 3 attempts, 30 s apart.
- Plan after apply (approve): A plan right after the apply should be empty; anything else means the module or provider didn't converge.
- Link the change in Jira (approve): The webhook workflow links the applied change to its ticket.
Weekly drift scan: A scheduled plan of every stack; drift goes to Teams and one Jira ticket per repo.
- Scheduled run: Weekly, Sundays at 06:00 UTC (or manually with drift_scan set). No YAML has changed.
- Plan every stack: Every stack is planned with the same CLI and gates.
- Compare with the cloud: A plan with changes means the cloud no longer matches the YAML on main.
- Analyze: drift-detection-analyze counts stacks with changes and stacks that failed to plan.
- Tell the team (drift): Teams gets a message from the same analysis outputs as the ticket.
- One ticket per repo (drift): An open ticket gets a comment, not a duplicate; a fingerprint keeps unchanged drift quiet; destroys raise priority.
- In sync (clean): Nothing to report, so no Teams message and no Jira comment.
Credentials for a run: Identity comes from where the CLI runs, never from YAML.
- CI secrets (ci): The reusable workflow injects the service principal with secrets: inherit. OIDC would remove this long-lived secret.
- Laptop identity (laptop): By default the engineer's Azure CLI login; with --use-service-principal, a service principal from .env.
- Set the subscription: Extra subscriptions get their own <SUB>_ARM_* credentials in resource mode.
- Env vars only: root.hcl reads credentials from the environment only; nothing secret is in YAML.
- Provider blocks: Today get_env renders secrets into provider.tf in the cache; the fix is to let providers read env vars directly.
- Separate backend identity: With backend_auth_alias set, the identity that writes state isn't the one that creates resources.
The components, and where they live in the repository:
| Component | Location | Job |
|---|---|---|
| Entrypoint stub | entrypoint.py (identical in every consumer) | Find the shared module, put src/ on sys.path, call the CLI |
| Shared CLI | src/<org>_iac/ | Parse, bootstrap, validate, authenticate, dispatch, generate, report |
| Terraform modules | cloud/<cloud>/terraform/<type>/, providers/<provider>/terraform/<type>/ | The actual resources: 146 cloud, 29 provider modules |
| Terragrunt unit templates | cloud/<cloud>/terragrunt/<type>/terragrunt.hcl | Map YAML entries to module inputs |
| Root template | template/root.hcl | Generate provider.tf, backend.tf, .tflint.hcl; retries; hooks |
| Configuration catalog | config/ | Provider versions, backends, OPA, Trivy, Wiz, Infracost, Jira, error catalog |
| Automation | .github/workflows/, .github/actions/ | Plan, apply, drift, summaries, notifications, compatibility |
| Validation tools | tools/validate_terraform_module.py | Per-module fmt, init, schema, validate, TFLint, docs |
How one run works, part 1: the Python CLI
This is the flow most people never see. One command in a consumer goes through the entrypoint stub, bootstrap sync, argument parsing and validation (reading common_vars.yaml and resources.yaml), then exports environment variables and authenticates (reading config.yaml and .env or CI secrets). Only then does the stack generator turn resources.yaml into terragrunt.stack.hcl, which Terragrunt expands into one generated unit per resource type:
- One command, five inputs: In infra-aws you run python entrypoint.py --cloud aws --environment dev --stack hello-world --terragrunt plan. Everything else the run needs is already on disk or in the environment.
- The stub finds the platform: The identical entrypoint stub finds the shared module (sibling folder locally, ./terragrunt_shared_module in CI), adds src/ to sys.path and calls cli/app.py.
- Bootstrap sync: A content hash decides whether the shared module changed. If so it is copied in, the stub is refreshed and root.hcl, the CI workflow, Taskfile and .gitignore are written.
- Parse arguments: argparse reads cloud, environment, stack and action. Unknown tokens such as -reconfigure become Terraform passthrough flags, and a dispatch target is chosen.
- Validate before anything runs: The stack folder must exist, the provider and stack must fit, the target account comes from common_vars.yaml, and resources.yaml is linted for duplicates.
- Export environment variables: CLOUD, ENVIRONMENT, TARGET_ACCOUNT and STACK are exported, plus gate settings (OPA, Trivy or Wiz, Infracost) and state-lock settings from config.yaml and common_vars.yaml.
- Authenticate: The backend entry is matched on the repo name, credentials are pre-flighted, and ARM_* (or AWS, GCP) plus BACKEND_* variables are set from .env, the CLI login or CI secrets.
- Hand over to the Terragrunt handler: The handler resolves environments/aws/dev/hello-world, clears the cache on -reconfigure, checks terragrunt is installed and starts the stack generator.
- YAML becomes a stack file: One unit block per resource type in resources.yaml (or the category files), with source = cloud/<cloud>/terragrunt/<type> and the declared instances as values.
- Terragrunt expands the units: terragrunt stack generate copies each unit template into .terragrunt-stack/<type>/ as terragrunt.hcl, next to a generated terragrunt.values.hcl.
- Merge common_vars: Environment and stack common_vars.yaml are merged into .terragrunt-stack/common_vars.yaml. common_tags merge key by key, so a stack tag never drops a cost-centre tag.
- Dependency waves: Dependency blocks in the generated units are sorted into execution waves and printed. Then terragrunt stack run plan starts, which is part 2.
The module paths below are relative to the package (src/<org>_iac/):
| Stage | Module | Reads | Writes or exports |
|---|---|---|---|
| Stub | entrypoint.py | GITHUB_ACTIONS | sys.path |
| Bootstrap | bootstrap/sync.py, templates.py, cache.py | Shared module checkout, content hash | Synced copy, stub, root.hcl, terragrunt-cicd.yaml, Taskfile, .gitignore |
| Parse | cli/parser.py, passthrough.py, dispatch.py | argv | Dispatch target, Terraform passthrough flags |
| Validate | cli/arg_validation.py | common_vars.yaml, resources.yaml | TARGET_ACCOUNT |
| Export | cli/env_export.py | config.yaml, common_vars.yaml, CLI flags | CLOUD, ENVIRONMENT, STACK, gate and lock variables |
| Authenticate | environment/handler.py, backend.py, credentials.py, azure_auth.py | config.yaml, .env, CLI login, CI secrets | ARM_*, BACKEND_* |
| Generate | generators/stack_generator.py | resources.yaml or category files | terragrunt.stack.hcl |
| Expand | terragrunt stack generate | terragrunt.stack.hcl | .terragrunt-stack/<type>/terragrunt.hcl |
| Merge | generators/common_vars_merge.py | Environment and stack common_vars.yaml | .terragrunt-stack/common_vars.yaml |
| Order | generators/dependency_graph.py | Generated units’ dependency blocks | Execution waves |
The merge step is subtle and important. Unit templates find variables with find_in_parent_folders("common_vars.yaml"), and searching upward from .terragrunt-stack/<unit>/ the first match is .terragrunt-stack/ itself. Writing the merged file there is what makes stack-level overrides work. Without it, the search silently falls through to the environment file, and you get a successful plan against the wrong variables.
How one run works, part 2: inside one generated unit
Part 2 follows one unit, aws_vpc. The unit reads the YAML itself and includes root.hcl. root.hcl reads the merged common_vars.yaml, the backend entry from config.yaml, the pinned versions from config/providers.yaml, and credentials from environment variables. From those it generates provider.tf and backend.tf, and then Terraform runs against the shared module source:
- One unit per resource type: Part 1 left .terragrunt-stack/aws_vpc/terragrunt.hcl, a copy of the shared template cloud/aws/terragrunt/aws_vpc. The inputs above are what it can see.
- The unit reads the YAML: Its locals find resources.yaml and the merged common_vars.yaml with find_in_parent_folders and map each aws_vpc entry to module inputs, with common_tags merged in.
- Include root.hcl: include "root" pulls in environments/aws/root.hcl, which bootstrap wrote from the shared template. Every unit in every consumer gets the same root.
- Which providers, which backend: root.hcl reads the providers list, subscriptions and override_versions from common_vars, and the backend entry whose repo matches the folder name from config.yaml.
- Versions and credentials: providers.yaml pins versions (aws 6.55.0, azurerm 5.0.0). Credentials only arrive as env vars such as ARM_CLIENT_ID, <SUB>_ARM_SUBSCRIPTION_ID or GH_TOKEN, never from YAML.
- Generate provider.tf and backend.tf: Only the providers the stack listed, plus one aliased azurerm per subscription. The backend (local, azurerm, s3 or gcs) keys state by repo, cloud and unit path.
- Point at the shared module: terraform.source resolves to terragrunt_shared_module/cloud/aws/terraform/aws_vpc. Consumers never hold their own copy of module code.
- terraform init: Terragrunt runs the tflint hook, downloads providers through a plugin cache and writes the generated files next to the module code.
- State, then plan: Init connects to this unit's own state file in the remote backend. Then terragrunt stack run plan runs the units in dependency waves.
- Refresh and convert: The plan refreshes against the cloud or SaaS APIs, retrying stale locks only when they are old enough. Each plan is converted to JSON with secrets redacted.
- Plan gates: The plan JSON is evaluated by OPA policies, Trivy or Wiz, and Infracost. Each gate can be advisory or blocking.
- Report (pass): Gates pass. The CLI prints a per-unit add/change/destroy summary and the final report, which CI posts to the pull request.
- Blocked (fail): A blocking policy fails, so the CLI exits non-zero and CI marks the plan red. OPA_OVERRIDE and similar flags exist for an approved exception.
What root.hcl decides, and from which input:
| Decision | Input | Rule |
|---|---|---|
| Backend type and location | config.yaml (repo root first, then the shared one) | Entry whose repo equals the repo folder name; common_vars.backend overrides |
| State key | Backend key, cloud folder, unit path | <key>/<cloud>/<unit path>/terraform.tfstate |
| Which providers | common_vars.providers, else a map by environment name | Only listed providers appear in required_providers |
| Provider versions | config/providers.yaml; override_versions per unit for azurerm | lookup with a fallback constraint |
| Azure subscriptions | common_vars.subscriptions | One aliased azurerm per subscription, data_only or resource mode |
| Credentials | Environment only | ARM_*, <SUB>_ARM_*, BACKEND_*, GH_TOKEN, JFROG_*, GRAFANA_*, ZPA_* |
One honest note from reading the code: the stack generator writes each resource’s instances into the unit’s values, and Terragrunt writes them to terragrunt.values.hcl. But none of the 132 cloud unit templates reference values.; they read resources.yaml directly. The values file is effectively a record of what was generated. That’s worth either using or removing, and I’ve put it on the technical debt list.
Technology and framework selection
| Choice | Why | Trade-off |
|---|---|---|
| Terraform | Provider coverage for every target: Azure, AWS, GCP, GitHub, JFrog, Grafana, Zscaler | Licence change pushes some teams to OpenTofu; nothing here blocks a switch |
| Terragrunt stacks | Generated units, per-unit state, DRY root config, dependency ordering | terragrunt stack is newer and its CLI has changed between releases |
| Python 3.12 CLI | One place for validation, auth, generation and reporting; testable with pytest | Another runtime on every runner; entrypoint.py installs PyYAML and python-dotenv if missing |
| YAML for consumers | Readable by people who don’t know HCL; easy to lint | No type system; validation has to be explicit |
| GitHub Actions reusable workflows | Consumers call shared workflows by ref; no pipeline copy | Ref management becomes a release concern |
| OPA, Trivy or Wiz, Infracost | Policy, security and cost views on the same plan JSON | Three tools to keep configured and updated |
The decision I’d defend hardest is the Python layer. Terragrunt alone can’t validate a subscription mapping, choose credentials per subscription, lint YAML for duplicates or merge tags safely. Before the restructure that logic lived under python/lib/, which the stub still falls back to for consumers that haven’t re-synced; the package splits it into modules with their own tests.
API and integration architecture
Y-IaC has no HTTP API. Its contracts are files, environment variables and workflow inputs, and they need the same versioning discipline an API would:
| Contract | Producer → consumer | Versioned by |
|---|---|---|
resources.yaml, common_vars.yaml, config.yaml shapes | Consumer → CLI and unit templates | Docs in 09-consumer-repo-config-reference.md |
| Module variables and outputs | Module → unit template → other units via dependency | Module README, validator |
| Dependency mock outputs | Unit template → first plan of a new stack | Unit template |
| Environment variable names | CLI → root.hcl → providers | cli/env_export.py, root.hcl |
| Reusable workflow inputs | Consumer workflow → Y-IaC workflows | Workflow file at the consumer’s pinned ref |
| Plan JSON and report formats | Terraform → gates and reports | Terraform’s JSON format version |
| Teams and Jira payloads | Drift workflows → Teams, Jira REST API | Workflow and config/jira/drift-ticket.yaml |
External integrations, all outbound: cloud and SaaS provider APIs, state backends, GitHub, the Wiz service, Microsoft Teams webhooks and Jira (/rest/api/2 and /rest/agile/1.0).
Data architecture and data models
There’s no application database. The durable data is:
- Terraform state, one file per unit, keyed
<key>/<cloud>/<unit path>/terraform.tfstate. - Consumer YAML and the generated Terragrunt files (generated files are reproducible and ignored by Git).
- Plan files, plan JSON, summaries, validation reports and logs as workflow artifacts.
- Git history, PRs, workflow runs and Jira tickets as the audit trail.
The core data model is the YAML hierarchy:
erDiagram
CONSUMER_REPO ||--|| CONFIG_YAML : "backend + workflow"
CONSUMER_REPO ||--|{ CLOUD_FOLDER : "environments/cloud"
CLOUD_FOLDER ||--|| ROOT_HCL : generated
CLOUD_FOLDER ||--|{ ENVIRONMENT : "environment folder"
ENVIRONMENT ||--|| COMMON_VARS : "account, providers, tags, gates"
ENVIRONMENT ||--|{ STACK : "stack folder"
STACK ||--o| STACK_COMMON_VARS : "overrides"
STACK ||--|| RESOURCES_YAML : "or category files"
RESOURCES_YAML ||--|{ RESOURCE_TYPE : "aws_vpc, azurerm_key_vault, ..."
RESOURCE_TYPE ||--|| UNIT : "one generated unit"
UNIT ||--|| STATE_FILE : "own state"
The data and state rules (ownership, retention, backup) are in Data and Integration.
Identity and access architecture
Identity comes from the execution environment, never from YAML:
| Context | Identity | How |
|---|---|---|
| Laptop (default) | The engineer’s Azure CLI login | az login; the CLI sets the active subscription |
Laptop (--use-service-principal) | A service principal from .env | ARM_CLIENT_ID, ARM_CLIENT_SECRET, ARM_TENANT_ID |
| CI | A service principal from GitHub secrets | Same variables, injected by the reusable workflow |
| Extra Azure subscriptions | Per-subscription credentials | <SUB>_ARM_SUBSCRIPTION_ID, plus client ID and secret in resource mode |
| State backend | Separate backend credentials when backend_auth_alias is set | BACKEND_ARM_*, BACKEND_AWS_*, BACKEND_GOOGLE_* |
| GitHub provider | Token | GH_TOKEN or GITHUB_TOKEN |
Separating backend credentials from deployment credentials is a good design: the identity that can write state doesn’t need to be the identity that can create resources, and vice versa. The weak point is the CI service principal secret, which is long-lived. I’d move CI to OIDC federated credentials; the details are in Security and Privacy.
My recommendation, as a best-practice default: Contributor scoped to the resource groups or subscription a consumer deploys to, plus User Access Administrator only for consumers that manage role assignments (ideally with an ABAC condition limiting which roles it can grant), and Storage Blob Data Contributor on the state container only for the backend identity. The DevOps team reviews these assignments quarterly.
Hosting and deployment model
Y-IaC itself isn’t deployed anywhere; it’s a repository that consumers execute. Delivery works like this:
- Locally, a consumer clones Y-IaC next to itself (
../terragrunt_shared_module). Bootstrap syncs the needed parts into the consumer on each run when the content hash changes. - In CI, the reusable workflow checks out Y-IaC into
./terragrunt_shared_moduleat the ref from the consumer’sconfig.yaml(iac_shared_module_ref), on the runner named there (ubuntu-latestfor the cloudutsukpilot consumers). - Infrastructure is created in the consumer’s targets: Azure subscriptions for most consumers, AWS for
infra-aws, GitHub forinfra-github, and SaaS providers where the stack asks for them.
The consequence is that the consumer’s ref is the release mechanism. A consumer on main gets every change immediately; a consumer on a branch or tag gets what that ref contains. Release management builds on that in CI/CD and Release Management.
Scalability and performance strategy
Scale for Y-IaC means stack count, unit count per stack, provider API limits, runner capacity, plan size and state backend throughput. The design choices that help:
- One unit per resource type, not per instance. A stack with 40 key vaults is still one unit.
- Per-unit state. Small state files lock independently, so two units never wait for each other.
- Dependency waves. Units with no dependency between them run in parallel within a wave.
- Plugin cache (
TERRAFORM_PLUGIN_CACHE_DIR) so providers download once per runner. - Change detection in CI, so only changed stacks plan.
- Applies run one stack at a time (
max-parallel: 1), which trades speed for predictability.
The limit I’d watch is provider API throttling on large Azure stacks during the weekly drift scan, when every stack in a consumer plans at once.
Resilience and disaster recovery
What can fail, and what the platform does about it:
| Failure | Behavior |
|---|---|
| Transient network error, “in use by” on delete, SSH reset | root.hcl errors.retry: 3 attempts, 30 s apart |
| State lock held by a dead run | Detect lock, release only if older than the minimum age, retry (opt-in) |
| Backend timeout during init | Runner retries the stack command |
| Partial apply (one unit fails) | Other units’ state is unaffected; re-run converges |
| State file corrupted or deleted | Restore from backend versioning; needs a tested procedure |
| Y-IaC ref broken | Consumers pin back to the previous ref |
Deterministic re-runs are the main resilience property: the same YAML and ref give the same generated files, so “run it again” is a safe instruction.
Blob versioning is enabled on the Azure state storage, and a state restore has been rehearsed. For the S3 backend used by infra-aws, bucket versioning should be enabled to the same standard.
Architectural decision records
The guide lists decisions that deserve ADRs. Several are already made in code, so the ADRs would record them rather than decide them:
| ADR | Decision (as implemented) |
|---|---|
| 001 Backend strategy | Per-repo backend in config.yaml, per-unit state key; local, azurerm, s3, gcs |
| 002 Provider version policy | Global pins in config/providers.yaml, per-unit azurerm overrides |
| 003 Conditional dependencies | mock_outputs so a unit can plan before its dependency exists |
| 004 Module release compatibility | Consumers pin iac_shared_module_ref; compatibility workflow |
| 005 Policy enforcement | Three levels: advisory, soft-mandatory, mandatory, per gate |
| 006 State-lock recovery | Opt-in, age-guarded, retry-limited |
| 007 Workflow ownership | Reusable workflows live in Y-IaC; consumers call, never copy |
| 008 Deprecation policy | Not decided |
A minimal template:
1
2
3
4
5
6
# ADR-006: State-lock recovery
Status: Accepted
Context: Killed CI jobs leave locks that block every later run on that state file.
Decision: Release locks automatically only when AUTO_RELEASE_LOCK is set and the lock is older than
LOCK_MIN_AGE_MINUTES. An unknown creation time is treated as unsafe.
Consequences: Fewer manual unlocks; a live apply's lock is never broken.
Checklist
- The three run walkthroughs above match the current code after each CLI refactor
- Every contract in the integration table has a named owner and a version note
- The
valuesversus directresources.yamlread is decided and cleaned up - CI moves from client secrets to OIDC federated credentials
- Backend versioning is confirmed for every state store
- ADRs 001 to 007 are written; ADR 008 (deprecation) is decided
API throttling during the drift scan is measured on the largest Azure consumer
