How to read Terraform you did not write
Every earlier module in this course taught you to write Terraform and Terragrunt. This module teaches the other half: reading configuration someone — or something — else wrote, and deciding whether it is actually correct. An AI assistant hands you a module, a terragrunt.hcl, a CI pipeline that looks exactly like the examples you have been studying, uses resource types and arguments that read as fluent and current, and — most of the time — terraform validate and terraform plan both come back clean. That last part is the trap this module exists to close: of the 23 patterns the next lesson catalogs, only a minority fail validate/plan/terragrunt outright. The rest are schema-valid, plan-clean configuration that fails later — at a real apply against a real cloud account, at the next plan after someone edits an unrelated list, or never, while quietly doing the wrong thing (protecting the wrong resource, deploying prod against dev’s account, shipping a secret nobody reviewed).
Three training-data problems compound this. First, Terraform’s provider ecosystem genuinely changes shape release over release — aws_s3_bucket.acl was the way to set an ACL for years and is now deprecated in favor of aws_s3_bucket_acl, terragrunt run-all/plan-all were the only commands that existed before Terragrunt 1.1’s CLI redesign — and a huge fraction of public training data predates each change while still reading as completely fluent, current-looking HCL. Second, count is the first iteration construct most Terraform tutorials teach, so AI assistants reach for it by default even where for_each is the only safe choice — a list can be reordered, count cannot tell the difference between “reorder” and “replace.” Third, the fastest way past a permission or destroy error is almost always the broadest fix — -auto-approve genuinely removes the friction, prevent_destroy on some resource genuinely makes an error message go away — at the cost of removing the one safeguard that mattered. The six steps below are the order that actually surfaces these failures, and every later lesson in this module drills one part of it.
flowchart TD A["1. Providers & versions<br/>pinned with ~>? lock file committed?"] --> B["2. Backend & state<br/>real locking? remote_state points at applied state?"] B --> C["3. Variables & locals<br/>nullable, ephemeral, validation blocks"] C --> D["4. Resources & lifecycle<br/>count vs for_each, prevent_destroy on what?"] D --> E["5. Modules<br/>provider blocks, source ref, depends_on"] E --> F["6. Terragrunt live tree<br/>run --all order, hardcoded inputs per env"] F --> G["7. CI & policy<br/>-auto-approve, unpinned actions, Rego v1, real rule IDs"]
1. Providers & versions
Section titled “1. Providers & versions”Before anything else, check whether the provider constraint actually protects you, and whether the lock file that pins the real resolved version is even committed — because a floating constraint changes what “the same configuration” resolves to on two different days, on two different machines, with zero diff to review.
- What to look for:
~>or>=inrequired_providers? Is.terraform.lock.hclcommitted, or gitignored alongside.terraform/? Does any resource use an argument the installed provider’s own schema marksdeprecated: true? - The question to ask: if a teammate ran
terraform inittoday versus a month from now, would they resolve the identical provider version — and wouldterraform validate -jsoncome back with zero warnings, not just zero errors? - Which lesson explains it: the AI Bug Catalog’s Versions & providers group (entries 1–3) for the real
.terraform.lock.hclresolution and the real, schema-sourced deprecation warnings; Installing & Project Structure forrequired_providersbasics.
2. Backend & state
Section titled “2. Backend & state”Once the provider is pinned, check what actually locks and stores state — because terraform_remote_state and a floating backend-locking argument both fail in ways validate cannot see from the consuming configuration alone.
- What to look for: does the backend’s locking mechanism match what this Terraform/backend version actually supports (S3’s
use_lockfilevs the deprecateddynamodb_table)? Does everyterraform_remote_statedata source point at akey/paththat has actually been applied? Is state ever used to separate environments viaterraform workspacewhere a completely separate backend config would be safer? - The question to ask: if this exact backend config ran
initagainst a brand-new bucket right now, would locking actually work — and if the state this reads viaterraform_remote_statedoesn’t exist yet, does the plan fail loud, or does something silently proceed with defaults? - Which lesson explains it: the AI Bug Catalog’s State & backend group (entries 4–6) for the real gitignored-lockfile drift and the real “Unable to find remote state” hard-fail; The State File and Why Terragrunt for both mechanisms in full depth.
3. Variables & locals
Section titled “3. Variables & locals”A variable block that looks like ordinary input validation can silently change what value actually reaches a resource — nullable and ephemeral both affect behavior in ways no error message calls out.
- What to look for: does a
nullable = falsevariable’s default silently stand in for an explicitnulloverride, with nothing in the plan output calling that out? Does anephemeralvariable ever carry a literal secretdefault, which still leaks into a saved plan’sshow -jsonoutput even though it never reaches state? - The question to ask: if this exact variable were passed
nullfrom a caller, would the effective value actually becomenull— or silently become the default, with the CLI output identical either way? - Which lesson explains it: the AI Bug Catalog’s Variables group (entries 7–8) for the real side-by-side
nullableapply output and the real ephemeral-default leak inshow -json.
4. Resources & lifecycle
Section titled “4. Resources & lifecycle”A resource block can be perfectly valid HCL and still be wrong in ways that only show up on the next plan, or the day someone actually needs the lifecycle block to do what its name implies.
- What to look for: is
countused over a list that could ever be reordered, wherefor_eachis the only safe choice? Doeslifecycle { prevent_destroy = true }sit on the resource that actually matters, or on something disposable nearby? Doesfor_eachever derive its keys from another managed resource’s computed attribute? Is a hallucinated argument (copied from a similar-but-different resource type) present anywhere? - The question to ask: if the middle item were removed from this exact list right now, would
terraform planshow a clean delete of one item — or a cascade of replacements shifted by position? And if this resource were destroyed right now, doesprevent_destroyactually block it, or only look like it does from a different resource’s error message? - Which lesson explains it: the AI Bug Catalog’s Resources & lifecycle group (entries 9–13) for the real
plan -destroyproof thatprevent_destroyprotects only the resource it’s declared on, and the realcountindex-shift plan.
5. Modules
Section titled “5. Modules”A module call can hide a provider configuration or an unpinned source that only becomes a problem the moment a caller needs count, for_each, or a second environment.
- What to look for: does a reusable module declare its own
providerblock, making it incompatible withcount/for_each/depends_onon the calling side? Does agit::/registrysourceomit?ref=/a version constraint, floating on the default branch? Does adepends_onon a module call exist to paper over what’s actually a genuine reference cycle? - The question to ask: does this module’s source resolve to the exact same commit/version every time it’s initialized, on any machine, on any day — and if a caller needed to loop this module with
count, would that even work? - Which lesson explains it: the AI Bug Catalog’s Modules group (entries 14–15) for the real “Module is incompatible with count, for_each, and depends_on” error and the real reference-cycle error no
depends_oncan suppress; Writing a Reusable Module and Module Composition & Sources for both mechanisms in full.
6. The Terragrunt live tree
Section titled “6. The Terragrunt live tree”terragrunt.hcl files are copy-pasted between environments constantly — checking whether that copy actually changed the values that matter is the single highest-leverage read in this whole module.
- What to look for: does a command AI wrote use
run-all/plan-all— both removed in the Terragrunt 1.1 CLI redesign? Dodevandprod(or any two environments)terragrunt.hclfiles carry the identicalaccount_id/region/subscription/project value in theirinputs, where a real difference is required? - The question to ask: does
terragrunt hcl validatepassing on both environment files actually prove they deploy against different accounts — or only that both are syntactically valid HCL? - Which lesson explains it: the AI Bug Catalog’s Terragrunt group (entries 16–17) for the real “unknown command” rejection and the real byte-identical
dev/prodcopy-paste thathcl validatecannot catch; Terragrunt HCL & the Root Config and The Multi-Environment Problem for the mechanism proven in full.
7. CI & policy
Section titled “7. CI & policy”Last, check whether the pipeline that actually applies this configuration has a real approval gate, a real policy check, and dependencies it can trust — because every earlier reading step can be perfect and still ship through a pipeline that removes the one safeguard that mattered.
- What to look for: does
-auto-approverun against a fresh plan rather than a saved, reviewed one? Is a third-party GitHub Action pinned to a commit SHA, or floating on@master/@main? Does a Rego policy use pre-v1deny[msg] { ... }syntax that fails to even load under current OPA/conftest? Does a.tflint.hcl/checkov config reference a rule ID that doesn’t actually exist in that linter’s real rule catalog? - The question to ask: if this exact pipeline ran right now, is there a point between “plan” and “real infrastructure changes” where a human or a policy gate could actually stop it — and does every check in this pipeline provably run at all, or does one silently do nothing?
- Which lesson explains it: the AI Bug Catalog’s CI & policy group (entries 18–21) for the real side-by-side
-auto-approveproof, the real Rego v1 parse error, and the real tflint run where an invented rule name silently checked nothing; Verification Tools for every tool in this list run for real.
What this module covers next
Section titled “What this module covers next”- The AI Bug Catalog — 23 specific patterns, each with the config AI typically writes, the real
validate/plan/test/terragrunt/tflint/checkov/conftest output it produces, and the fix. - Review Checklist — the same 23 patterns as a one-page, eight-group checklist, each line linking the lesson that proves it.
- Verification Tools — what
terraform fmt -check,validate,plan -detailed-exitcode,show -json+jq,terraform testwithmock_provider, tflint, checkov, conftest,terragrunt hcl validate/render/run --all plan, and this course’s own harness each actually prove. - Three worked reviews — Worked Review 1, Worked Review 2, Worked Review 3 — applying this reading order to full, AI-generated configuration sets (a network+compute module, a multi-environment Terragrunt live tree, and a CI pipeline with policy checks). These three lessons are written and maintained separately from this one; if a link below 404s while that work is still in progress, the reading order and the catalog above stand on their own regardless.
Mental Model
Section titled “Mental Model”Reading order for Terraform/Terragrunt you did not write:
1. Providers & versions → pinned with ~>? lock file committed? any deprecated args?2. Backend & state → real locking mechanism? does remote_state point at applied state?3. Variables & locals → does nullable/ephemeral actually behave the way its name implies?4. Resources & lifecycle → count where for_each belongs? prevent_destroy on what, really?5. Modules → provider block inside? source pinned? does depends_on hide a cycle?6. Terragrunt live tree → run --all, not run-all/plan-all? do dev and prod actually differ?7. CI & policy → -auto-approve on a fresh plan? actions pinned? policy syntax current?
"terraform validate and plan both came back clean" answers almost none of these — most ofthis module's 23-entry catalog is schema-valid, plan-clean configuration that is simply wrong.Common Misconceptions
Section titled “Common Misconceptions”- “If
validateandplanboth succeed, it’s correct.” Proven throughout this module’s catalog: of 23 patterns, only a handful are rejected outright byvalidate/plan/terragruntat all. The rest are syntactically and semantically valid HCL that Terraform has no way to flag, because nothing in the language can tell “the wrong resource is protected” from “a resource is protected.” - “AI-generated Terraform just needs a newer prompt.” The problem isn’t staleness alone —
dynamodb_tableandterragrunt run-allare specifically fluent-looking, syntactically valid configuration that happens to target a shape a current provider/CLI version has moved past, sometimes with a warning, sometimes with none at all. - “A silent failure is rarer than a rejected
apply, so it matters less.” The opposite is closer to true in this catalog:prevent_destroyon the wrong resource, a.tflint.hclrule name that silently checks nothing, orproddeploying againstdev’saccount_idall ship with zero errors, zero warnings, and zero indication anything is wrong — until the specific moment something depends on the exact behavior that’s wrong. - “
-auto-approveis just a CI convenience for skipping a prompt.” It removes the one point in the entire pipeline where a saved, reviewed plan could be checked against reality before anything real changes — the fix is a separateplan -out+ policy gate +apply <planfile>, not simply tolerating the flag because CI can’t typeyes.