State & Drift
The Terraform page ended on a question: how does plan know what already exists?
The answer is the most important — and most misunderstood — concept in Terraform: state. Get state
wrong and you get duplicated resources, corrupted infrastructure, and teams overwriting each other. Get
it right and Terraform’s whole safety story works.
Why state has to exist
Section titled “Why state has to exist”Terraform’s job is to make reality match your code. To compute the difference between the two, it needs
to know three things at every plan:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ DESIRED │ │ LAST-KNOWN │ │ ACTUAL │ │ your .tf code│ │ the state │ │ the real │ │ (the what) │ │ file (memory)│ │ cloud (truth)│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ └─── plan compares all three to compute the diff ───┘Your code says what you want. The cloud holds what is. But Terraform also needs a record of what it
created and the identities it assigned — the AWS instance ID i-0abc123, for example, which appears
nowhere in your HCL. The state file (terraform.tfstate, JSON) is that record: a map from each
resource in your code (aws_instance.web) to the real-world object it manages (i-0abc123), plus the
attributes it last saw.
Without state, Terraform couldn’t tell the difference between “this resource doesn’t exist yet, create
it” and “this resource exists, I made it, leave it alone.” Every apply would try to create everything
from scratch. State is Terraform’s memory of what it owns.
Remote state and locking
Section titled “Remote state and locking”By default state is a local file. That’s fine for one person learning, and a disaster for a team. If
state lives on your laptop, your teammate can’t see what’s deployed, and two people running apply at
the same time will fight over the same resources — a classic race condition that can corrupt
infrastructure.
The fix is a remote backend: store state in a shared, durable location (an S3 bucket, HCP Terraform, a GCS bucket) instead of locally.
terraform { backend "s3" { bucket = "acme-terraform-state" key = "prod/network.tfstate" region = "us-east-1" use_lockfile = true # ← enables locking (S3-native, Terraform ≥ 1.10) encrypt = true }}Remote state buys two things:
- Shared truth — everyone (and the CI/CD pipeline) reads and writes one authoritative state, so it doesn’t matter who runs Terraform or from where.
- Locking — before mutating state, Terraform acquires a lock. A second
applywaits (or fails fast) instead of running concurrently. This is the same mutual-exclusion idea that keeps two processes from corrupting a shared file — applied to your whole infrastructure. Without it, simultaneous applies are the fastest way to wreck a production environment.
Local state Remote state + locking ───────────────────── ───────────────────────────────── state on one laptop state in a shared bucket teammates can't see it everyone reads one source of truth two applies → corruption lock serializes applies → safe CI can't run it CI runs Terraform like anyone elseDrift: when reality stops matching state
Section titled “Drift: when reality stops matching state”The state file records what Terraform last saw. But the real world can change behind Terraform’s back — someone opens a port in the console, an autoscaler resizes a group, a resource is deleted by hand. Now there are three values in play and they no longer agree:
- Code says: instance type
t3.micro. - State says: last I checked, it was
t3.micro. - Reality says: someone changed it to
t3.largein the console yesterday.
That gap between your code/state and the real world is drift, first introduced in Why IaC. Drift quietly breaks the promise that your repo describes reality.
plan is the drift detector
Section titled “plan is the drift detector”Here is where it all comes together. terraform plan doesn’t just compare code to state — it
refreshes state by reading the real world first, then diffs. So plan surfaces drift
automatically:
terraform plan
# ~ resource "aws_instance" "web" {# ~ instance_type = "t3.large" -> "t3.micro" # drift caught!# }## Plan: 0 to add, 1 to change, 0 to destroy.The ~ line is Terraform telling you: reality drifted to t3.large, your code says t3.micro, and
apply will pull it back. plan is the diff between your declared intent and the actual world, with
state as the bookkeeping that makes the comparison possible. Run it in CI on a schedule and you have a
standing drift detector that catches every out-of-band change before it becomes a mystery.
The deeper cure for drift isn’t catching it faster — it’s building infrastructure that can’t drift in the first place. That’s the idea behind immutable infrastructure. But first, a different layer of the problem: once a server exists, what configures the software inside it? That’s configuration management.
The architect’s lens
Section titled “The architect’s lens”Five questions for state — and for adopting a remote backend with locking:
- Why does it exist? Because to compute a diff Terraform needs a memory of what it built — the AWS
instance ID
i-0abc123appears nowhere in your HCL — so the state file maps each resource to the real object it manages. - What problem does it solve? Without state every
applywould recreate everything; with it,planrefreshes against reality and surfaces drift as an exact~diff, so a 2 a.m. console fix becomes visible and correctable instead of a silent landmine. - What are the trade-offs? State can hold secrets (passwords, generated keys), so it must be encrypted and
access-controlled; a team needs a remote backend with locking (S3 with
use_lockfile, HCP Terraform) — andforce-unlockre-creates the very concurrent-write race the lock exists to prevent. - When should I avoid it? A local state file is fine for one person learning, and a throwaway stack doesn’t need a remote backend — but the moment two people or CI touch it, local state is a corruption risk.
- What breaks if I remove it? With no state, Terraform can’t tell “create this” from “I already made this”; with no remote backend or locking, teammates run blind and concurrent applies corrupt infrastructure.
Check your understanding
Section titled “Check your understanding”- Terraform needs three values to compute a
plan. Name them and say where each one lives. - The AWS instance ID
i-0abc123appears nowhere in your HCL. Why does Terraform still need to know it, and where is it stored? - Why is local state a problem for a team? Name the two things a remote backend with locking gives you, and what each one prevents.
- Define drift in terms of code, state, and reality. Give a concrete example of how it gets created.
- Explain how
terraform plandetects drift. What does the~symbol in plan output mean, and why is runningplanon a schedule useful?
Show answers
- Desired — your
.tfcode (the what); last-known — the state file (Terraform’s memory of what it built); actual — the real cloud (the truth).plancompares all three to compute the diff. - The instance ID is the real-world identity Terraform assigned but which appears nowhere in your HCL.
Terraform needs it to map your resource (
aws_instance.web) to the object it manages, so it can tell “create this” from “I already made this, leave it alone.” It’s stored in the state file. - Local state lives on one laptop, so teammates can’t see what’s deployed and two simultaneous
applys race and can corrupt infrastructure. A remote backend with locking gives shared truth (one authoritative state everyone, including CI, reads/writes — prevents divergent views) and locking (serializes applies — prevents concurrent-apply corruption). - Drift is when reality stops matching your code/state: code says
t3.micro, state last sawt3.micro, but reality ist3.large. It gets created by an out-of-band change — e.g. someone resizes the instance in the console, an autoscaler edits a group, or a resource is deleted by hand. planfirst refreshes state by reading the real world, then diffs — so an out-of-band change shows up. The~means an in-place change: the live value differs from your code andapplywill pull it back (e.g.t3.large -> t3.micro). Runningplanon a schedule turns it into a standing drift detector that catches every divergence before it becomes a mystery.