Azure 53: Terraform plan/apply with exit-code-2 gating
easy⏱ 5 mincourseazure
Exit codes are a contract
terraform plan -detailed-exitcode returns: 0 (no changes), 1 (error), 2 (changes detected). Lythia's pipeline checks $LASTEXITCODE after plan. 0 → skip apply, emit 'no-op' artifact. 1 → fail the run. 2 → publish plan as artifact, gate on prod approval if env=prod, then apply.
set -e
terraform init -backend-config=backend.tfvars
terraform plan -detailed-exitcode -out=tfplan.binary
echo "##vso[task.setvariable variable=PLAN_EXIT;isOutput=true]$?"
Artifact naming convention
Plan artifacts are named plan+{env}+{project}+{buildNumber}. The apply stage downloads by exact name. This means every apply is provably operating on the plan the engineer reviewed — no race conditions, no 'plan on dev, apply on prod' mishaps.
Backend state lives in a Storage Account
Each env has its own state Storage Account in the matching subscription. The pipeline's set_backend_config_vars.sh reads the env from a parameter and exports the right account/container/key. Lock contention is avoided because each env's state is independent; same module deploying to dev and prod cannot conflict.
Decide and emit
Write decideApply({ exitCode, env, isProdApproved }) returning { action: 'skip'|'apply'|'fail', artifactName?, reason }. Rules: exitCode 1 → fail. exitCode 0 → skip. exitCode 2 + env != prod → apply. exitCode 2 + env == prod + isProdApproved → apply. exitCode 2 + env == prod + !isProdApproved → skip with reason 'awaiting prod approval'. Log decisions for 5 input combos.
Use terraform plan -lock-timeout=10m
On busy environments the state file can be locked by another pipeline. Default lock timeout is 0 → instant failure. Setting -lock-timeout=10m makes the pipeline wait politely. Combined with state file split (per env, per service), contention almost never happens.
Quiz: an engineer ran terraform apply from their laptop
Q: Is that immediately bad? A: Possibly catastrophic. If they used the prod backend config, they bypassed: ADO Change Request, plan review, the matching pipeline tag in git, the deploy notification to Slack. Lythia's mitigation: prod state Storage Account's RBAC denies AAD users — only the prod pipeline's SPN can write. Local apply silently fails with 403.