HAMZA KAREEM / CONCEPTS hamzawi.dev

Lesson 472 min read

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:

  1. Config — your .tf files (what you want)
  2. State — terraform.tfstate: a map from resource ADDRESS (aws_db_instance.main) to real ID (db-prod-7f2a)
  3. 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?