Server

Terraform State, Explained

By Prabath Thalangama· October 24, 2026· 4 min read
#terraform#ci-cd#automation

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 moved block.
  • State file is out of date after a manual fix — terraform apply -refresh-only updates 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 .tfstate or *.tfstate.backup (add them to .gitignore).
  • Two stacks fighting over one resource — pick one owner; state rm it from the other.
PrabathStuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.