iac-terraform-guardian
Infrastructure-as-code turns a typo into a deleted database. This skill makes IaC changes boring and reversible: state is remote and locked, every apply is preceded by a read plan, modules have clean boundaries, and the operations that can destroy data are gated behind explicit review. Terraform/OpenTofu idiom; the principles port to Pulumi/CDK.
Use BEFORE
- Authoring new IaC for real cloud resources.
- Any change to existing IaC — especially anything touching stateful resources (databases, disks, buckets).
- Refactoring modules or moving resources between states.
State is sacred
- Remote backend with locking (S3+DynamoDB, GCS, Terraform Cloud). Never local state for shared infra — two engineers applying against local state corrupt each other's world.
- Locking on so concurrent applies can't race. If a lock is stuck, understand why before force-unlocking — a force-unlock during an in-flight apply corrupts state.
- State contains secrets (DB passwords, keys are stored in plaintext in state). Encrypt the backend at rest, restrict access, and never commit state or
terraform.tfstate.backupto git. - Never hand-edit state. Use
terraform state mv/import/rmfor surgery; a hand-edited state file is how you get a resource Terraform thinks exists twice. - Import, don't recreate. Adopt existing resources with
importrather than letting Terraform destroy-and-recreate them.
Read the plan — every time
plan is the single most important safety mechanism. Read it, don't skim it.
- Count the destroys.
Plan: X to add, Y to change, Z to destroy.Any non-zerodestroyon a stateful resource is a stop-and-review. A change that unexpectedly wants to destroy a database is almost always a mistake (a forced replacement). - Watch for
-/+(replace). A change to an immutable attribute forces destroy-then-create. On a stateful resource that means data loss. Look for# forces replacementin the plan and confirm it's intended. - Apply exactly what you planned. Save the plan (
terraform plan -out=tfplan) andapply tfplanso the applied change is the reviewed one — not a fresh plan that drifted since review. - Plan in CI on PRs, post the plan output for review; apply only after merge, from CI, with approval on production.
Guardrails against destroy-surprises
lifecycle { prevent_destroy = true }on databases, disks, and anything holding data you can't lose. It turns an accidental destroy into a hard error.create_before_destroyfor resources that must not have a gap (so the replacement exists before the old one is torn down).- Deletion protection at the provider level (RDS deletion protection, S3 bucket with versioning + MFA delete) as defense in depth — don't rely on Terraform alone.
Modularity and structure
- Modules have a clear interface: documented inputs (variables with types + descriptions + validation), documented outputs, no hidden reliance on provider config leaking in. A module is a function — typed in, typed out.
- Separate state per environment (dev/staging/prod) via workspaces or separate state files, so a dev apply can never touch prod. Same modules, different variable values.
- Don't over-modularize. A module per resource is noise; module around a meaningful unit (a service's full footprint, a network). Duplication is cheaper than the wrong abstraction.
- Pin provider and module versions (
required_providerswith~>constraints, moduleversion =). Unpinned providers mean an unrelated apply suddenly upgrades your provider and changes plans.
Hygiene
- Tag everything — owner, environment, cost-center, managed-by=terraform. Untagged resources are unattributable cost and orphan risk. Enforce via a default_tags/provider block.
- No secrets in
.tfor.tfvarscommitted to git. Source them from a secrets manager / environment / a vault provider. (See secrets-management.) - Format and validate (
fmt,validate,tflint, a policy check like OPA/Sentinel/tfsec) in CI. - Detect drift on a schedule (
planagainst prod) — someone clicking in the console silently diverges reality from code.
Procedure
- Confirm remote, locked, encrypted backend; separate state for the target environment.
- Author/change with pinned providers + typed module interfaces; add
prevent_destroyto stateful resources. fmt→validate→tflint/tfsec→plan -out.- Read the plan: reconcile every add/change/destroy/replace with intent; stop on any unexpected destroy or replace of stateful infra.
- Review the saved plan (in CI/PR); get approval for production.
applythe saved plan. Verify resources and re-plan to confirm zero diff (no drift).- Confirm tags, and that no secret landed in state-in-git or
.tfvars-in-git.
Definition of done
- State remote, locked, encrypted, per-environment, never in git.
- Providers/modules pinned; modules have typed, documented interfaces.
- The saved plan was read and reviewed; no unintended destroy/replace of stateful resources.
prevent_destroy/deletion protection on data-bearing resources.- Everything tagged; no secrets in committed IaC;
fmt/validate/policy checks green in CI.