Plan, apply, drift
Infrastructure as code keeps three things apart - what you wrote, what the tool last did, and what really exists. Read plans that update, replace and destroy, catch a hand change in the console, and stop a deletion before it happens.
Clicking through a cloud console works once. The second time, nobody remembers which boxes were ticked, and the third environment is a little different from the first two. Infrastructure as code writes the infrastructure down in files instead, and a tool - Terraform, OpenTofu, Pulumi, Crossplane - makes reality match them.
The tool keeps three things apart, and every surprise in this lesson lives in the gaps between them:
- The configuration - what you wrote.
- The state - what the tool remembers creating, as of its last apply.
- The cloud - what actually exists right now.
- You change the code. Nothing happens to the cloud yet.
- Plan reads the real objects, compares them with your code and its state, and proposes actions. It changes nothing.
- Apply carries out the plan you read. Now the cloud, the code and the state agree.
- Someone fixes something by hand in the console. The cloud moved; the code and state did not. That gap is drift.
Reading a plan
The OpenTofu docs describe what a plan does in three steps: it reads the current state of the real objects, compares the configuration with what it knows, and proposes the changes that would make them match. A plan is safe to run as often as you like. It’s the review.
Each proposed change carries a symbol, and the symbols are the whole skill:
| Symbol | Meaning | Ask yourself |
|---|---|---|
+ |
create | is this new thing meant to exist? |
~ |
update in-place | the object stays; one setting changes |
-/+ |
replace: destroy, then create | what was on the old one, and what points at it? |
- |
destroy | is anything in there you need? |
A replace is the one people miss. Some settings can’t be changed on a live object - HashiCorp’s docs put it as arguments that cannot be updated in-place due to remote API limitations - so the tool destroys the object and creates a new one. A new server gets a new address, and everything pointing at the old one has to change in the same plan.
Try it
Drift is a symptom
HashiCorp’s drift tutorial says it directly: you should not make manual changes to resources a tool controls, because the state drifts from the real infrastructure. The next plan will quietly put your code back - undoing a 3am console fix that was holding something up. When you must change things by hand, write the change into the code straight afterwards. Then the plan agrees with you.
This is the same idea as Git as the source of truth, where a controller re-applies the repository on a timer instead of waiting for someone to run a plan.
A plan line reads: -/+ server.web must be replaced, with image marked '# forces replacement'. What happens on apply?
-/+ is destroy-then-create. Anything stored on the old server, or anything pointing at its address, is affected - which is why the same plan updated the DNS record.
Someone resized a server in the console last night. You change an unrelated tag in the code and run plan. What does the plan propose for the size?
The plan compares reality with your code, not with your intentions. The hand change is drift, and the plan proposes undoing it. If the resize was needed, put it in the code.
You removed a bucket from the code by accident. Where is the cheapest place to catch it?
The plan shows the destroy before anything happens. That's why teams post plans on pull requests and refuse to apply what nobody has read.
Everything above ran in your browser. Now do it on a real cluster with tilt lab 01-deploy-a-service (profile minimal). You need Docker, kind and Tilt, about 15 GB of free disk and 8 GB of spare RAM. Setup guide · Can my machine run it?
Kubernetes manifests are infrastructure as code too, and kubectl diff is their plan: it exits 0 when nothing differs and 1 when something does. On the running platform, after lab 01:
kubectl diff -k examples/hello-world; echo "exit $?" # 0: live matches the files
kubectl -n hello scale deploy/hello --replicas=3 # the console click
kubectl diff -k examples/hello-world; echo "exit $?" # 1: replicas 3 -> 1, drift found
kubectl apply -k examples/hello-world # apply: the files winSources
Missions and quizzes above earn their own XP. This marks the lesson done.