terraform · level 2

State

The mapping that ties config to reality.

100 XP

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:

  1. 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-logs is the one your config calls aws_s3_bucket.logs."
  2. 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 = true attribute 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.