Terraform State & Drift
the file that thinks it knows your cloud
Cloud infrastructurecloud infrastructure / IaC
The symptom
You add one tag to an RDS instance. Plan says:
# aws_db_instance.main must be replaced
-/+ resource "aws_db_instance" "main"
Nobody asked to replace the production database. What happened?
Reason it through
Terraform never diffs your .tf files against the cloud directly. There are THREE worlds:
- Config — your .tf files (what you want)
- State — terraform.tfstate: a map from resource ADDRESS (aws_db_instance.main) to real ID (db-prod-7f2a)
- Reality — what the AWS API actually returns
plan = refresh state from reality, then diff config against state.
So the ADDRESS is the primary key. Not the name, not the ARN.
Cause #1: someone renamed the block
A refactor turned aws_db_instance.main into aws_db_instance.primary. Terraform sees one address that vanished (destroy) and one that never existed (create). Same database, two "changes".
Fix without downtime:
terraform state mv aws_db_instance.main aws_db_instance.primary
Or keep it in code (TF ≥ 1.1), so teammates get it too:
moved {
from = aws_db_instance.main
to = aws_db_instance.primary
}
Cause #2: drift
During an incident someone opened the console and bumped allocated_storage 100 → 200. Refresh pulls 200 into state, your config still says 100, and the next plan quietly shrinks the disk back.
See drift without changing anything:
terraform plan -refresh-only
That's the audit command almost nobody runs. Run it weekly in CI and alert when the output isn't empty.
The two things that actually bite teams
- No locking = corrupted state. Two CI jobs apply at once, both read version N, both write N+1. Use an S3 backend with use_lockfile = true (or a DynamoDB lock table on older versions). Without it you eventually get a state referencing resources that no longer exist — and real resources nothing references.
- State is plaintext secrets. Generated DB passwords, private keys, and every sensitive output sit unencrypted in that JSON. sensitive = true only hides values from CLI output, not from the file. Never commit tfstate, encrypt the bucket, and treat "who can run terraform state pull" as a security boundary.
The mental model to keep
State is a cache of reality, keyed by address, that Terraform trusts more than reality itself.
Every weird plan is one of three things:
- the address changed (rename / module refactor) → state mv or moved
- reality changed behind your back → drift → -refresh-only, then reconcile
- state never knew about it → import
import {
to = aws_s3_bucket.logs
id = "acme-logs-prod"
}
Your exercise
Take a small Terraform project. Change one tag in the cloud console by hand, then run terraform plan -refresh-only and read exactly what it reports. Next, rename a resource block, confirm plan wants destroy+create, add a moved block, and confirm plan drops to "0 to add, 0 to change, 0 to destroy".
❓Then answer this: your state lives in S3 with no locking, and two engineers apply in the same second. What ends up in the file — and which real resources become invisible to Terraform afterwards?