Text size
Tried & Tested6 min read

Terraform state, explained

The file that knows everything about your infrastructure, and how to keep it safe.

Terraform state, explained
AI-generated editorial illustration

When people start with Terraform, they focus on the .tf files. The resources, the modules, the variables. The state file is just something that appears after the first apply, and most people don't think about it until something goes wrong.

And something always goes wrong eventually. A lost state file, a corrupted one, two people applying at once, a secret leaked through state. Understanding state is what separates using Terraform from trusting it. (OpenTofu works the same way.)

What state actually is

Terraform's state is a JSON file that maps your code to real-world resources.

When you write resource "aws_s3_bucket" "logs", Terraform needs to remember that this particular block corresponds to the bucket with ID acme-logs-prod in your AWS account. That mapping, plus the last known attributes of every resource, is the state.

Terraform uses it to:

  • Know what exists, so it can update or delete the right resources.
  • Calculate the plan by comparing code, state and reality.
  • Track dependencies and outputs between resources.
  • Speed things up, by not looking up everything from scratch every time.

Without state, Terraform doesn't know which resources it manages. Delete the state file, and Terraform will try to create everything again, while the old resources keep running, unmanaged.

Remote state, always

For anything beyond a personal experiment, keep state in a remote backend with locking:

  • S3 (with native lock files), Google Cloud Storage, Azure Blob Storage
  • HCP Terraform or other infrastructure automation platforms

Locking prevents two people (or pipelines) from applying at the same time and corrupting state. Versioning on the bucket gives you history to recover from mistakes.

Your state file is the memory of your infrastructure. Lose it, and Terraform wakes up with amnesia in a house full of strangers.

Keeping it secret

State files often include secrets in plain text: database passwords, private keys, tokens, or anything a resource returns as an attribute. Even if you mark a variable as sensitive, it's hidden in the plan output, not in the state file.

Protect state like you protect secrets:

  • Encryption at rest on the storage bucket.
  • Strict access control: only the pipeline and a few admins can read production state.
  • Never commit state to Git. Add *.tfstate and *.tfstate.backup to .gitignore.
  • OpenTofu's client-side state encryption encrypts the state before it leaves your machine or pipeline, using a KMS key.
  • Terraform's ephemeral resources and write-only arguments keep certain secret values out of state entirely.
  • Generate secrets outside Terraform where possible, in a secret manager, and only pass references around.

Moving and refactoring

Renaming a resource or moving it into a module changes its address. Without telling Terraform, it would destroy the old resource and create a new one. For a database, that's a disaster.

Modern Terraform handles this in code with moved blocks:

moved {
  from = aws_s3_bucket.logs
  to   = module.logging.aws_s3_bucket.this
}

The plan shows a move instead of destroy-and-create. Because it's in code, it's reviewed and works across every environment. Much safer than the older terraform state mv command.

Importing existing resources

To bring a manually created resource under Terraform's management, use import blocks:

import {
  to = aws_s3_bucket.legacy
  id = "acme-legacy-bucket"
}

Run terraform plan -generate-config-out=generated.tf and Terraform even writes a first draft of the resource configuration. Clean it up, plan again, and apply.

Removing resources from management

Sometimes you want Terraform to stop managing a resource without deleting it, for example when handing it over to another stack. Use a removed block:

removed {
  from = aws_s3_bucket.handover

  lifecycle {
    destroy = false
  }
}

The resource stays in the cloud. Terraform just forgets about it.

When it breaks

A few situations and what to do:

  • Stale lock after a crashed pipeline: terraform force-unlock <lock-id>, but only after checking that nothing is actually running.
  • Corrupted or wrong state: restore a previous version from the bucket's version history. This is why versioning matters.
  • State out of sync with reality: terraform plan -refresh-only shows the differences, and terraform apply -refresh-only updates state to match reality without changing infrastructure.
  • Need to inspect state: terraform state list and terraform state show <address> are safe, read-only ways to look inside.

Avoid editing the JSON by hand. If you really must, take a backup first, and know that you're on your own.

So, what should you check today?

Is every state file remote, locked, versioned, and encrypted? Who has access to production state? Are you still using state mv commands instead of moved blocks? Those three questions cover most of the risk.

My take: treat state as the most sensitive file in your infrastructure. Protect it, version it, and never let it live on a laptop.

— Desly