Skip to content

How Terraform Works

Every earlier module in this course taught you to use Terraform and Terragrunt: writing HCL, running init/plan/apply, storing state remotely, wiring variables and modules, building a multi-environment live tree, and shipping through CI with policy checks. None of it required knowing what terraform plan actually builds before it prints a diff, what terraform.tfstate actually is at the byte level, what -out=tfplan actually writes to disk, how hashicorp/google (or aws, or azurerm) actually talks to terraform as a process, or how terragrunt run --all actually decides which unit runs before which.

That gap is exactly where AI-generated Terraform tooling and reviews fail in ways that look fine on the surface: a “policy check” script that greps terraform plan text output instead of reading the structured resource_changes[].change.actions array terraform show -json actually produces; a CI step that assumes terraform apply -parallelism=50 is always faster with no idea what the flag bounds; a teammate who “fixes” a stuck state lock by hand-editing terraform.tfstate’s serial field, not knowing state push will simply refuse a stale one; a dev_overrides block copy-pasted from a blog post that silently breaks init for a reason nobody can explain from the error text alone. None of these are HCL syntax errors. None of them are caught by terraform validate. All of them are facts about the runtime this module makes provable.

This module has one job: give you five pieces of runtime knowledge that let you look at any Terraform or Terragrunt output — a graph, a state file, a -out plan, a provider crash, a run --all log — and know exactly what produced it and why.

  1. What does terraform plan/apply actually walk, and in what order? (The Graph and the Walk — a real terraform graph, DOT and Mermaid, -parallelism, depends_on vs implicit references)
  2. What is terraform.tfstate, really, at the schema level? (State File Anatomy — real state pull JSON, serial/lineage enforcement proven with a real refusal, state replace-provider, show -json vs raw state)
  3. What is a saved plan file, and what does CI/policy tooling actually read from it? (Plan File and JSON Output — -out is a real zip, proven with unzip -l; the full resource_changes[].change.actions matrix; -detailed-exitcode)
  4. How does Terraform actually talk to a provider? (Provider Plugin Protocol — go-plugin over gRPC, a real TF_LOG=TRACE handshake and GetProviderSchema call, why init downloads a binary per OS/arch, a real induced provider crash, dev_overrides)
  5. How does Terragrunt decide what to run, and in what order? (Terragrunt Discovery and DAG — real terragrunt find/list --dag, a real run --all --parallelism 1 ordered log, .terragrunt-cache, mock_outputs)

Every claim in this module is backed by one of three things: a real, reproduced local run against Terraform 1.16.4 and Terragrunt 1.1.0 (this course’s pinned versions) — pasted below the claim, trimmed of machine-specific paths but never invented; the official docs at developer.hashicorp.com or terragrunt.gruntwork.io; or, where neither is possible offline, an explicit note saying so.

LessonAnswers the question
The Graph and the WalkWhat does Terraform actually build from your .tf files before it does anything, and how does -parallelism change what happens on apply?
State File AnatomyWhat, exactly, is inside terraform.tfstate, and what stops two people from silently clobbering each other’s state?
Plan File and JSON OutputWhat is a -out plan file made of, and what does a policy engine or cost tool actually parse out of show -json?
Provider Plugin ProtocolWhat actually happens between terraform init and a provider being ready to plan, and what does a provider crash look like?
Terragrunt Discovery and DAGHow does Terragrunt find your units, build its own dependency order, and decide what run --all does first?

The State File already introduced state as “a JSON file that maps configuration to real infrastructure.” This module goes past that framing into the actual schema, the actual graph algorithm, the actual bytes on disk, and the actual process boundary between Terraform Core and a provider.

flowchart TD
  hcl["Your .tf files"] --> graphbuild["Build the dependency graph\n(The Graph and the Walk)"]
  graphbuild --> walk["Walk the graph, up to -parallelism nodes concurrently\n(default 10)"]
  walk --> rpc["Per node: gRPC call to the provider plugin\n(Provider Plugin Protocol)"]
  rpc --> plan["Plan result assembled"]
  plan --> outfile["terraform plan -out=tfplan\n(a real zip -- Plan File and JSON Output)"]
  outfile --> apply["terraform apply tfplan"]
  apply --> state["Written to state\n(State File Anatomy)"]
  state --> tg["Terragrunt units: their own DAG over many state files\n(Terragrunt Discovery and DAG)"]
One terraform apply, from HCL to a provider RPC

Everything in this module is one story told from five different points on the same pipeline: Terraform never executes your configuration top to bottom — it compiles it into a graph, and every command (plan, apply, destroy, graph itself) is a walk over that graph, dispatching each node to whichever provider plugin owns it over gRPC, and recording the result in one versioned state document. Terragrunt sits one layer above this and does the same thing at a coarser grain: instead of resources, its nodes are whole Terraform units (directories), its edges come from dependency/dependencies blocks instead of resource references, and its own --parallelism bounds how many units run concurrently — completely independent of the -parallelism any individual terraform apply inside a unit uses. Once you can place any fact in this module against “graph, walk, gRPC call, state write” (and, one layer up, “Terragrunt’s own graph, walk, unit run”), reading an unfamiliar plan diff, a state file, a CI policy step, or a run --all log stops being five separate mysteries.

  • “terraform apply runs resources in the order they’re written in the file.” The Graph and the Walk proves the opposite with a real terraform graph: order comes entirely from the dependency graph (implicit references and explicit depends_on), and independent resources are dispatched concurrently, up to -parallelism (default 10) — proven with real wall-clock timing showing 5 independent 2-second operations finish in ~2s by default and ~10s at -parallelism=1.
  • “State is just a cache; you can hand-edit it if terraform state commands are inconvenient.” State File Anatomy proves state is versioned and enforced: a real terraform state push with a stale serial or a mismatched lineage is refused outright with the real error text, not silently merged.
  • “A saved plan file is basically the same as the human-readable plan output, just cached.” Plan File and JSON Output proves -out produces a real zip archive (unzip -l shown live) containing the plan, prior state, post-refresh state, and a full config snapshot — enough for apply to proceed with zero re-evaluation of your .tf files.
  • “Terraform ‘just knows’ how to talk to AWS, Azure, and GCP built in.” Provider Plugin Protocol proves every provider is a separate subprocess Terraform starts and speaks gRPC to over a Unix socket, with a real, visible GetProviderSchema call captured under TF_LOG=TRACE.
  • “terragrunt run --all walks units in the order find lists them.” Terragrunt Discovery and DAG proves ordering comes from dependency/dependencies blocks forming Terragrunt’s own DAG, independent of alphabetical or discovery order — proven with a real --parallelism 1 log showing a dependency unit’s plan completing before either of its two dependents starts.

The whole point of this module is this section, five times over — one per lesson. As a preview, here is the survey-level version: a CI “policy gate” script that looks reasonable and silently checks nothing.

🐛 Spot the bug

#!/usr/bin/env bash
# A teammate's "policy gate" before terraform apply in CI
terraform plan -out=tfplan > plan_output.txt
if grep -q "Plan: 0 to add, 0 to change, 0 to destroy" plan_output.txt; then
echo "no changes, skipping approval"
else
echo "changes detected, needs approval"
fi
terraform apply tfplan
Show the answer

This greps the human-readable summary line, which is a localization- and formatting-fragile string, not a stable interface — Terraform has never documented that exact text as an API. Plan File and JSON Output proves the real, stable interface is terraform show -json tfplan, which exposes resource_changes[].change.actions as a real array (["no-op"], ["create"], ["update"], ["delete","create"], ["forget"], …) meant exactly for this use case — and terraform plan -detailed-exitcode (proven in the same lesson: exit code 2 for “changes present”, 0 for “no changes”, 1 for a real error) replaces the whole grep with a documented exit code the script can branch on directly, with no text parsing at all.

What does Terraform actually build from your .tf files before running plan or apply?
A real terraform state push is attempted with a lineage that does not match the backend's current state. What happens, proven in this module?
What does terraform plan -out=tfplan actually produce on disk?
How does Terraform Core actually communicate with a provider like hashicorp/google during a plan?
In a live tree with vpc, compute (depends on vpc), and db (depends on vpc), what does a real terragrunt run --all --parallelism 1 -- plan log prove about ordering?