State
The mapping that ties config to reality.
State
Terraform's state is a JSON file that records what it created, what it's tracking, and what real cloud resources those declarations point to. Almost every confusing Terraform behaviour traces back to state.
What state knows
For every resource you've declared, state records:
- The resource address (
aws_s3_bucket.logs). - The provider's idea of its real-world ID (
my-app-logs). - A snapshot of every attribute (region, ARN, tags, etc.).
- Dependency information for the graph.
That snapshot is how plan decides what's changed. It compares your config + the cloud's current state against the file's recorded state.
Why state exists
Two reasons:
- Mapping. Your config says "I want a bucket called logs". The cloud has a bucket. State is the link that says "the bucket whose ARN is
arn:aws:...:my-app-logsis the one your config callsaws_s3_bucket.logs." - Performance. Without state, Terraform would have to query every cloud API on every run to figure out what already exists. The state file is a cached snapshot that makes plans fast.
Local vs remote
By default, state lives in terraform.tfstate next to your config. Two big problems:
- It's not shared between teammates — your apply happens, mine doesn't see it.
- It can contain secrets in plain text (RDS passwords, API keys).
Real projects use a remote backend: S3, GCS, Azure Blob, or Terraform Cloud:
terraform {
backend "s3" {
bucket = "my-tf-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "tf-locks" # state locking
encrypt = true
}
}
S3 stores the state. DynamoDB provides a lock so two applys can't run at the same time and corrupt each other.
Locking
When you terraform apply, it grabs a lock. Anyone else who runs apply against the same state has to wait. Without a lock, two simultaneous applies can leave the cloud in an inconsistent shape.
Terraform Cloud, S3+DynamoDB, GCS, and Azure backends all support locking. Local state has no lock — never share a local state file via Dropbox.
Reading state
terraform state list # every resource
terraform state show aws_s3_bucket.logs # full attributes
terraform state mv aws_s3_bucket.logs aws_s3_bucket.archive # rename
terraform state rm aws_s3_bucket.logs # forget about a resource
# (does NOT destroy it)
state rm is the most surgical: it tells Terraform to stop tracking a resource without touching the cloud. Useful when you want to migrate a resource into a different config or hand it to a different module.
Drift
When the cloud changes outside Terraform — someone clicks in the console, an autoscaler adjusts capacity, an operator runs aws cli — your state goes stale. The next plan shows drift:
~ resource "aws_security_group" "web" {
~ description = "Web traffic" -> "Web traffic (updated by alice)"
}
Refresh on each plan picks it up. You then either accept the change (update your config) or revert it (run apply to push the config's value back).
Sensitive values
Anything Terraform reads or writes lands in state — including database passwords, API keys, private keys. Treat your state file like a credentials file:
- Never commit it to git.
- Use a remote backend with encryption + restricted access.
- Use the
sensitive = trueattribute on outputs / variables to keep them out of CLI output (still in state, but not echoed to logs).
Don't edit state by hand
State is JSON. You technically can edit it. You almost always shouldn't. The supported editing commands (state mv, state rm, state pull, state push) handle locking and validation for you. Hand-editing the file is the fastest way to break a plan in subtle ways.