Terraform State, Explained
Introduction¶
Almost every painful Terraform incident is a state problem: two people ran
apply at once, someone renamed a resource and Terraform wanted to destroy and
recreate it, or a resource was changed by hand and now every plan is noisy.
None of it is mysterious once you know what state is and isn't.
Concepts¶
Terraform's job is to make the world match your .tf files. To do that it needs
a third thing: a record of what it created and what it's called. That
record is the state file (terraform.tfstate), a JSON document mapping each
resource address in your config (aws_instance.web) to a real object (instance
i-0abc...) plus all of its attributes.
State exists because:
- Cloud APIs have no "list everything Terraform made" call. State is that list.
- A plan needs to compare desired (config) against current (a refresh of the real resource) — and it needs the resource's ID to do the refresh.
- Some attributes (a generated password, an ARN) aren't in your config at all; they only live in state.
State is not a backup and not a source of truth for infrastructure — it's Terraform's bookkeeping. It also frequently contains secrets in plaintext (database passwords, private keys), which is why where you store it matters.
Hands-on¶
Use a remote backend with locking¶
Local state on a laptop is fine for a demo and a disaster for a team. Use a
backend that supports locking so two applys can't run at once:
terraform {
backend "s3" {
bucket = "acme-tf-state"
key = "prod/network/terraform.tfstate"
region = "eu-west-1"
encrypt = true
use_lockfile = true # S3 native locking (Terraform 1.10+)
}
}
(Older setups use a DynamoDB table for the lock; use_lockfile replaces it.)
Give each independent stack its own key — one giant state file means one giant
blast radius and slow plans.
When you rename a resource¶
Changing aws_instance.web to aws_instance.app in your config makes Terraform
plan a destroy + create — it sees the old address gone and a new one
appear. Tell it they're the same thing.
Modern way, in config (Terraform 1.1+):
moved {
from = aws_instance.web
to = aws_instance.app
}
Imperative way:
terraform state mv aws_instance.web aws_instance.app
Adopt an existing resource¶
Something was created by hand or by another tool and you want Terraform to manage it. Write the resource block, then:
import {
to = aws_s3_bucket.logs
id = "acme-prod-logs"
}
terraform plan -generate-config-out=generated.tf will even scaffold the
resource block for you. The older terraform import aws_s3_bucket.logs acme-prod-logs
still works but doesn't generate config.
Stop managing a resource without destroying it¶
terraform state rm aws_db_instance.legacy
The database keeps running; Terraform just forgets about it. Useful before handing a resource to a different stack.
Inspect state¶
terraform state list # every managed address
terraform state show aws_instance.app # full attributes of one
terraform show -json | jq '.values.root_module.resources[].address'
Verification and troubleshooting¶
- "Error acquiring the state lock" — a previous run crashed or is still
going. Confirm nothing is running, then
terraform force-unlock <LOCK_ID>. Never routinely-lock=false. - Every plan shows changes to a resource you didn't touch — something is
modifying it outside Terraform (an autoscaler, a console click, a default tag
policy). Either bring the drift into config, or add a
lifecycle { ignore_changes = [tags["LastModified"]] }. - Plan wants to destroy and recreate on a trivial change — you changed a
force-new attribute (an EC2 AMI, an RDS engine version in some cases), or
you renamed the address without a
movedblock. - State file is out of date after a manual fix —
terraform apply -refresh-onlyupdates state from reality without changing infrastructure. - Secrets in state — they're unavoidable for some resources. Encrypt the
backend, lock the bucket down to the CI role, and never commit
.tfstateor*.tfstate.backup(add them to.gitignore). - Two stacks fighting over one resource — pick one owner;
state rmit from the other.
Related tools and reading¶
- On-site: JSON → Code for turning API responses into typed
structures, JSON Formatter for reading
terraform show -json. - Related posts: Terraform modules and remote state, Secrets in CI without leaking them.
Stuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.More in DevOps & CI/CD
OpenTelemetry for Distributed Tracing
A trace follows one request across services. OpenTelemetry is the vendor-neutral way to produce them — here's the model, the Collector, and sampling that won't bankrupt you.
October 24, 2026pre-commit Hooks That Actually Help
pre-commit runs checks before a commit lands, catching formatting, secrets, and syntax errors locally. Here's a config that helps without being annoying.
October 24, 2026Chaos Engineering Basics
Chaos engineering is controlled experiments that inject failure to verify your system tolerates it. Here's the method, safe blast radius, starter experiments, and game days.
October 24, 2026