Chapter 27Lesson 01~190 minutes

Infrastructure as Code, Terraform Plans, Policy Checks, and Deployment Workflows: Core Concepts and Mental Model

Model IaC delivery as exact source, provider-lock, backend/workspace, plan, policy, approval, apply, state, and verification transitions.

IaC modelTerraformPlan identityStatePolicy

Learning objectives

  • Explain why an IaC plan is evidence about one source, provider set and state snapshot rather than a generic approval document.
  • Separate repository revision, provider lock, backend/workspace, plan file, policy result, environment approval and applied state.
  • Inspect IaC state read-only before any plan or apply mutation.
  • Explain why a saved plan can be sensitive and why its digest does not replace state/backend identity.
  • Trace a plan-to-apply lifecycle that preserves first-failure and governance evidence.

1. The problem: infrastructure code is only half of the identity

Earlier chapters treated source SHAs, build artifacts, attestations and deployment targets as separate evidence. Infrastructure as code adds another stateful actor: the IaC backend. The same main.tf can produce a different plan against a different workspace, state snapshot, provider version or variable set. A review that says “the Terraform looks fine” is therefore not enough to authorize an apply.

The operational goal is narrower than teaching Terraform itself. GitHub Actions must prove which repository revision and provider selections produced the plan, which backend/workspace and state snapshot the plan observed, which policy checks accepted it, and whether the later apply is still acting on the same authorized context.

2. Mental model: source + state context → reviewed change → guarded mutation

Read this model from left to right. GitHub chooses an exact workflow/source revision. Terraform initializes the configured backend and locked provider versions, then formatting and validation establish structural quality. Planning compares desired configuration with current state and, where applicable, refreshed remote objects. Policy checks interpret the proposed change. A guarded apply may proceed only after the plan, source and target context still match.

IaC delivery causality
flowchart TD
  A[Exact source SHA] --> B[Terraform / OpenTofu version]
  B --> C[Provider lock + backend/workspace identity]
  C --> D[fmt + validate]
  D --> E[Saved plan + digest]
  E --> F[Policy/security checks]
  F --> G[Reviewed plan evidence]
  G --> H[Protected apply job]
  H --> I[State / infrastructure mutation]
  I --> J[Post-apply verification]
  I --> K[Failure / partial state evidence]

The arrows matter because each transition can fail independently. A green policy check does not prove the apply is authorized; an approved environment does not prove the saved plan still matches current state; and a successful Terraform exit does not automatically prove the external service is healthy.

3. Define the state before changing it

Layer Record Why it matters
Event/revision event, ref, GITHUB_SHA, workflow path/revision, run ID/attempt Binds automation to one repository revision.
Toolchain Terraform/OpenTofu exact version; setup action SHA Prevents silent CLI/runtime drift.
Dependencies .terraform.lock.hcl, provider source/version/checksums Binds provider selection; the lock file is source evidence, not backend state.
Backend/workspace backend type/address plus workspace/environment identity Names the state target against which changes are calculated.
Plan binary plan, sanitized human summary, SHA-256, source/state metadata Represents one proposed transition; it is not a portable policy document.
Policy rule set/version, input digest, pass/fail reasons Explains why the proposed change was accepted or denied.
Authorization GitHub environment/protection result and optional OIDC claims Controls who/what may cross into mutation authority.
Apply/state apply result, state serial/lineage or remote revision, changed object IDs Proves what state actually changed.
External verification provider/resource health or local fixture file content Proves target outcome independently of workflow color.

4. Read-only inspection comes before plan or apply

Start with commands that cannot mutate the target. Confirm the source SHA and toolchain, inspect the lock file, identify the backend/workspace, and use state-list/show only when a backend is already initialized and the operator is authorized to read it. In a production cloud environment, even a read operation may require credentials; do not widen privileges merely to make diagnostics convenient.

set -euo pipefail
printf 'run=%s attempt=%s sha=%s ref=%s
' "$GITHUB_RUN_ID" "$GITHUB_RUN_ATTEMPT" "$GITHUB_SHA" "$GITHUB_REF"
terraform version
sha256sum .terraform.lock.hcl
terraform workspace show
terraform state list || true

Do not print backend credentials, cloud tokens, raw OIDC JWTs, Terraform state or plan JSON merely for debugging. State and saved-plan representations can contain sensitive values even when normal CLI output marks values as sensitive.

5. A saved plan is bound evidence, not a timeless artifact

A saved plan records the actions Terraform intends to take after evaluating a particular configuration and state context. Applying that file tells Terraform to execute the recorded plan rather than silently calculate a fresh one. This is valuable for review because the mutation can be tied to the reviewed plan bytes.

But the plan must remain bound to its context. If state changes after planning, Terraform can reject the old plan as stale. Plan files are opaque implementation artifacts, may include sensitive data, and should not be treated as a stable cross-version interchange format. Record the CLI version, provider lock, backend/workspace identity, source SHA and plan digest alongside them.

6. Provider lock and backend state are different controls

The dependency lock file records provider versions and checksums. Commit it with IaC source and verify it before planning; this controls executable provider dependency drift. The backend stores infrastructure state and may provide locking. A committed lock file cannot prevent two workflows from racing to change the same backend, and a backend lock cannot stop a workflow from using an unexpected provider version if dependency selection is not controlled.

7. Policy checks evaluate proposed change, not operator intent

A useful policy stage consumes machine-readable plan information and produces a deterministic decision such as “no deletes,” “only approved resource classes,” or “estimated count below a threshold.” The policy input itself must be traceable to the saved plan digest. In the mandatory lab the plan contains only synthetic local-file data, so a small Python policy checker is sufficient; production systems may use OPA, Sentinel, Conftest or cloud-specific policy services.

Policy is not an approval substitute. A policy can say the change satisfies machine rules while the environment gate determines whether this workflow identity may mutate the target now. Conversely, a human approval must not override a deterministic policy failure without an explicit exception process and audit record.

8. Plan jobs and apply jobs should not inherit the same authority

A pull-request plan usually needs source read access plus whatever read-only backend/provider identity is required to calculate drift. The apply job is a different trust boundary: it should run only from an authorized base-repository revision, behind the intended environment, with narrowly scoped OIDC or credentials. Do not give PR code production credentials simply because it runs Terraform.

For the free lab there is no cloud credential at all. The apply changes only a runner-local file through the HashiCorp local provider. This lets learners practice plan identity, provider locking, evidence transfer, stale-plan rejection and environment gating without creating cloud resources.

9. The evidence chain must survive reruns

Evidence Preserve before cleanup/rerun Do not confuse with
Run identity run ID, attempt, source/workflow SHA plan digest
Dependency identity setup-action SHA, Terraform version, lock-file hash backend lock
Target identity backend/workspace/state lineage/serial where available repository branch
Plan identity plan SHA-256 + summary/policy input hash human approval
Policy result rule version + reasons Terraform validation
Authorization environment approval / OIDC subject provider permission
Mutation apply result + state/object change health
Recovery failure/partial state captured before corrective action blind rerun

10. Common wrong model: “plan in PR, terraform apply on main”

Those two lines sound safe but omit the binding. Was main the same source revision that produced the plan? Did the workspace/backend change? Did a provider upgrade occur? Was a fresh plan silently generated during apply? Did state advance because another run applied first? Production automation should make those questions answerable from retained evidence rather than team memory.

11. Lesson summary

IaC automation is trustworthy when the proposed transition and the mutation are bound to exact source, dependencies and state context. Plan digest, policy decision, approval, credentials and apply result are separate evidence fields.

Next lesson

Infrastructure as Code, Terraform Plans, Policy Checks, and Deployment Workflows: Guided Hands-On Workflow

Continue with the next lesson to build on the current concepts, evidence, security boundaries, and operational practices.

Knowledge check

Why is a Terraform plan digest alone insufficient to authorize apply?

What does .terraform.lock.hcl control?

Why can a stale saved plan be a desirable failure?

Should a pull-request plan job receive production write credentials?

Why is plan JSON handled carefully?

Official references and version notes

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.