Terraform keeps a state file that maps the resources in your code to the real resources it created (servers, DNS records, buckets). Every plan compares your code, the state and reality. Lose or corrupt the state, and Terraform no longer knows what it manages. Two people applying at once can create conflicting changes. Handling state properly is the most important operational part of Terraform.

Basics: Terraform basics. Most of this applies equally to OpenTofu. See OpenTofu.

What’s in the state

terraform state list
terraform state show cloudflare_dns_record.www

The state (terraform.tfstate, JSON) contains resource IDs, attributes, dependencies, and output values, often including sensitive data: generated passwords, private keys, database connection strings. Treat the state file as a secret.

Why local state is risky

By default, the state is a local file in the working directory:

  • lost if the laptop dies, or the directory is deleted;
  • not shared: a teammate running Terraform has a different (or no) state, and creates duplicates;
  • no locking: two concurrent applies can corrupt it;
  • easy to commit to Git by accident, with its secrets. Add *.tfstate* to .gitignore.

Local state is fine only for personal experiments.

Remote backend on S3-compatible storage

Store state in a bucket (AWS S3, MinIO, Backblaze B2, Cloudflare R2, Wasabi):

terraform {
  backend "s3" {
    bucket = "tf-state"
    key    = "prod/network.tfstate"
    region = "us-east-1"

    # S3-compatible (non-AWS) endpoints:
    endpoints                   = { s3 = "https://s3.example-provider.com" }
    skip_credentials_validation = true
    skip_region_validation      = true
    skip_requesting_account_id  = true
    use_path_style              = true

    use_lockfile = true          # S3-native locking (Terraform 1.10+)
  }
}
export AWS_ACCESS_KEY_ID=… AWS_SECRET_ACCESS_KEY=…
terraform init -migrate-state      # moves existing local state into the bucket
  • Enable bucket versioning, so you can recover earlier state versions.
  • Encrypt at rest, and restrict access to the people and CI jobs that run Terraform.
  • Self-hosted MinIO on a separate server works well. See MinIO.

Other backends: Terraform Cloud/HCP, GitLab-managed state (HTTP backend), PostgreSQL (pg backend), Consul.

Locking

Locking prevents two apply runs at once. With use_lockfile = true, Terraform creates a .tflock object next to the state (older setups used a DynamoDB table on AWS). If a run crashes and leaves a stale lock:

terraform force-unlock <LOCK_ID>

Only do that when you’re sure no other run is active.

Organise state

  • Split state by environment and component (prod/network, prod/servers, staging/…), using different keys or directories. Smaller states mean faster plans and a smaller blast radius.
  • Share outputs between states with terraform_remote_state data sources, or by passing values explicitly.
  • Workspaces (terraform workspace new staging) give separate states for the same code. Many teams prefer separate directories for clarity.

State commands for refactoring

terraform state mv aws_instance.web aws_instance.app        # renamed resource in code
terraform state rm cloudflare_dns_record.old               # stop managing (doesn't delete the real resource)
terraform import cloudflare_dns_record.www <zone_id>/<record_id>   # adopt an existing resource
terraform plan                                             # always verify: expect "no changes"

Modern Terraform also supports declarative moved {} and import {} blocks in code, which are reviewable in pull requests.

Drift

If someone changes infrastructure by hand, terraform plan shows the difference. Decide whether to update the code or let Terraform revert it. terraform apply -refresh-only updates the state to match reality, without changing resources.

Quick reference

Task Command / setting
List resources terraform state list
Remote state backend "s3" { … } + terraform init -migrate-state
Locking use_lockfile = true
Unlock stale lock terraform force-unlock <id>
Rename terraform state mv / moved {}
Adopt existing terraform import / import {}
Never Commit *.tfstate to Git

Related: Terraform modules and secrets management.

  • Terraform
  • DevOps
  • Infrastructure as Code