Infrastructure as Code, Terraform Plans, Policy Checks, and Deployment Workflows: Configuration, Design Patterns, and Trade-Offs
Choose plan/apply boundaries, saved-plan handling, state partitioning, OIDC, and GitOps patterns from explicit trust and evidence requirements.
Learning objectives
- Choose plan-on-PR versus apply-on-main/manual boundaries from trust and state requirements.
- Compare saved-plan promotion with re-planning and state the evidence trade-off.
- Design one backend/state partition per environment or blast-radius boundary.
- Prefer OIDC to static cloud secrets and distinguish push deployment from GitOps handoff.
- Use a decision table to justify an IaC workflow architecture.
1. IaC workflow design is a set of boundary choices
There is no single correct Terraform workflow. A small team may manually dispatch an apply from a reviewed commit; a regulated platform may require a saved-plan digest, environment approval and short-lived cloud federation; a GitOps system may prohibit GitHub Actions from directly applying infrastructure at all. The design test is whether source, state, authority and evidence remain independently observable.
2. Plan on pull request; apply only from trusted revision
PR planning gives fast feedback on proposed infrastructure change, but fork or untrusted PR code must not receive privileged cloud credentials. For public contribution models, either use a no-credential validation path or a carefully designed read-only planning service. Mutation belongs after merge/approval from a trusted base-repository revision.
Do not assume “merged to main” proves that the exact reviewed plan is still current. Main may advance, state may change externally, or another deployment may win the race. Bind apply to one source SHA and one plan/state context.
3. Saved plan versus re-plan at apply time
| Choice | Strength | Risk / requirement | Evidence expectation |
|---|---|---|---|
| Apply saved plan | Mutation executes the exact reviewed plan bytes | Plan is sensitive and state can become stale; compatible tool/provider/backend context required | plan digest, tool/provider lock, backend/workspace, stale check |
| Re-plan in apply job | Fresh against current state and simpler artifact handling | Reviewer did not approve those exact new plan bytes unless a new gate reviews them | new plan digest + explicit second review/policy decision |
| Plan text only | Easy human review | Text is not executable identity and may omit/sanitize details | never claim text hash proves applied binary plan |
4. One state boundary per environment/blast radius
State is an authority boundary because whoever can update it can often drive infrastructure mutation. Separate production from staging rather than selecting both through an unvalidated variable against one broad credential. Backend keys/workspaces can partition state, but naming alone is not enough: bind credentials, environment rules and concurrency to the same target identity.
State locking protects concurrent writers where the backend supports it. It is not a global deployment scheduler and it is not evidence that the human reviewed the current plan. Use GitHub concurrency to reduce overlapping workflow intent and backend locking to protect the state write itself.
5. OIDC versus static cloud secret
Static cloud access keys in repository secrets create long-lived
credential rotation and exfiltration risk. OIDC lets the apply job
request a short-lived token and lets the cloud provider decide
whether repository/workflow/environment claims are trusted. Keep
id-token: write only on the jobs that exchange it, and
scope the provider role to the exact backend/resources required.
OIDC is an authentication mechanism, not a Terraform policy engine. A correctly federated role can still be over-privileged, and a least-privilege role can still apply a destructive plan if governance fails.
6. Direct apply versus downstream GitOps handoff
Direct push deployment means GitHub Actions authenticates to the
backend/provider and performs apply. It gives immediate
run-to-change linkage but places mutation credentials/identity at
the CI boundary. A GitOps handoff instead updates an authorized
desired-state repository or change record and lets a dedicated
reconciler perform the mutation.
GitOps does not remove the need for plan evidence. It changes which system owns apply authority and target health. Record the handoff commit/change ID and the reconciler outcome separately from the GitHub Actions check.
7. Setup action versus preinstalled CLI
As verified September 10, 2026,
hashicorp/setup-terraform v4.0.1 uses the Node 24
action runtime and can install an exact Terraform version. The
mandatory lab pins the action by full commit SHA and requests
Terraform 1.16.1. Relying on whichever Terraform binary happens to
exist in a runner image makes evidence weaker and can produce
incompatible plan behavior after image updates.
8. Plan portability cautions
A saved Terraform plan is designed for a later apply in the same configuration/state context, not as a vendor-neutral serialized change request. Record Terraform version and provider lock, and avoid moving binary plans across arbitrary operating systems, architectures or long version gaps. If a plan must cross a job boundary, keep runner/tooling assumptions explicit and verify the digest before apply.
OpenTofu can implement a similar operating model, but do not assume a Terraform plan binary is interchangeable with OpenTofu or vice versa. Pick one CLI for a workflow run, pin it, and generate/apply plans with that tool.
9. Decision table
| Scenario | Recommended pattern | Prerequisites | Observable proof |
|---|---|---|---|
| Public repo PR | fmt/validate + no-secret simulation; privileged plan after trusted handoff if needed | hosted runner, read-only token | source SHA + validation result |
| Small internal service | PR plan + saved digest; manual/environment-gated apply | remote backend, committed lock, narrow role | plan digest + state target + approval + apply result |
| Multi-env platform | separate backend/role/environment/concurrency per target | target inventory and federated identities | environment→role→backend mapping |
| GitOps organization | policy-reviewed plan; publish desired-state change, reconciler applies | trusted GitOps controller | plan evidence + handoff commit + reconciler status |
| No cloud training | local provider + local backend fixture | free runner/local machine | lock hash + plan hash + file/state result |
10. Lesson summary
Architecture should make stale state, privilege boundaries and evidence ownership visible. Whether you save plans, re-plan, use OIDC or hand off to GitOps, never collapse review, authorization and mutation into one opaque job.
Knowledge check
What is the main benefit of applying a saved plan?
The mutation is tied to the exact reviewed plan bytes, provided the state/context is still valid.
Why might a team deliberately re-plan after approval?
To refresh against current state—but the new plan requires a new policy/review decision because it is not the previously reviewed executable plan.
Does GitHub concurrency replace Terraform backend locking?
No. Concurrency coordinates workflow intent; backend locking protects state writes. Both can matter.
What changes in a GitOps model?
The system that owns mutation authority changes; GitHub still needs evidence for source/plan and the handoff, while the reconciler supplies apply/health evidence.
Can Terraform and OpenTofu share a saved plan binary by assumption?
No. Treat plan binaries as tool-specific; pin one CLI and verify compatibility explicitly.
Official references and version notes
- Terraform CLI plan — Saved plans, planning modes, detailed exit codes and the security warning for plan files.
- Terraform CLI apply — Applying a saved plan and automation semantics.
- Terraform dependency lock file — Provider selections/checksums and why the lock file belongs with source.
- Terraform state locking — State-lock behavior and the operational risk of force-unlock.
- Terraform local backend — Local state backend used only by the disposable no-cloud lab.
- HashiCorp setup-terraform — Official setup action; current v4 line uses Node 24.
- GitHub environments — Approval/protection boundary for a guarded apply job.
- GitHub OIDC overview — Preferred short-lived cloud federation boundary for optional real-provider adapters.
- Workflow artifacts — Plan/evidence transfer is GitHub artifact state, not infrastructure state.
- OpenTofu documentation — Open-source alternative CLI with a similar plan/apply operating boundary; verify syntax/version separately before substitution.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.