A Terraform state file mapping a resource address to a real cloud ID, kept in a locked, versioned remote backend.
Terraform & OpenTofu

Terraform State File Explained: What It Holds, Where to Keep It, and How to Protect It

The Terraform state file is how Terraform knows what it owns. What is inside it, why it holds secrets, where to keep it, and what to do when it goes missing or stale.

Reviewed for technical accuracy: October 8, 2026

  • terraform.tfstate maps every resource in your code to a real cloud object ID, its attributes, and its dependencies
  • State stores sensitive values in plain text; ephemeral values (Terraform 1.10+) and write-only arguments (1.11+) keep secrets out
  • Keep state in a remote backend with locking and versioning, such as S3 with use_lockfile = true
  • Prefer configuration blocks over state mv, state rm, and terraform import, so state changes go through plan and review

The Terraform state file, terraform.tfstate by default, is a JSON file where Terraform records every real object it manages: the address of the resource in your code, the ID the cloud provider gave that object, its last known attributes, and what it depends on. Terraform reads it on every plan to work out what already exists and what needs to change. OpenTofu, the open-source fork of Terraform, uses state the same way.

Keep it in a remote backend, a storage bucket that Terraform reads and writes for you, with locking and versioning turned on. Never commit it to Git, and treat it as a secret: values a resource needs or returns, including database passwords, are stored in it as plain text unless you use ephemeral values or write-only arguments.

Why Terraform Needs a State File

HashiCorp gives four reasons Terraform keeps state instead of asking the cloud every time.

  • Mapping: Terraform has to link each resource block to one real object. Tags cannot do this reliably, because not every resource type or provider supports them.
  • Dependencies: When you delete a resource from your code, the code no longer says what that resource depended on. State keeps the last known dependencies, so Terraform can still destroy things in the right order.
  • Performance: State caches attribute values for every resource. On large estates, querying every object on every run would be slow and would hit API rate limits.
  • Team sync: Everyone on a team has to plan against the same state, or two engineers will apply conflicting changes to the same objects.

So state defines what Terraform owns. If state says nothing exists, Terraform plans to create everything. If state points at production, terraform destroy removes production.

What Is Inside terraform.tfstate

JSON
{
  "version": 4,
  "terraform_version": "1.11.4",
  "serial": 42,
  "lineage": "3f2b9c1e-7a4d-4e1b-9c55-0d8e2f6a7b10",
  "outputs": {},
  "resources": [
    {
      "mode": "managed",
      "type": "aws_db_instance",
      "name": "ledger",
      "instances": [
        {
          "attributes": {
            "id": "db-7KQ2XWJ4M3ZB5NHLP",
            "identifier": "rds-payments-ledger-prod",
            "engine": "postgres",
            "password": "plain-text-password-here"
          },
          "dependencies": ["aws_db_subnet_group.ledger"]
        }
      ]
    }
  ]
}

version is the state format version. serial goes up by one every time state is written, and lineage is a unique ID assigned when the state was first created, so Terraform can tell two copies of the same state apart from two unrelated states. Each entry under resources ties a code address (aws_db_instance.ledger) to a real ID and its attributes. HashiCorp discourages editing this file directly: use the terraform state commands instead.

Why the State File Contains Secrets

Marking a variable or output sensitive = true hides it from plan output and logs, but the value still lands in state. HashiCorp's documentation says that if you add secret values to your configuration, Terraform stores them in its state and plan files, and anyone who can read those files can read the values. The password attribute in the excerpt above is exactly that case.

Two newer features keep secrets out entirely:

  • Ephemeral values (Terraform 1.10 and later): variables and outputs marked ephemeral, and ephemeral resources, exist only during a run. Terraform omits them from state and plan files.
  • Write-only arguments (Terraform 1.11 and later): a managed resource receives a value without Terraform storing it. Because Terraform cannot see the old value, the provider adds a version argument that you increase when the secret changes.
HCL
resource "aws_db_instance" "ledger" {
  identifier          = "rds-payments-ledger-prod"
  engine              = "postgres"
  instance_class      = "db.t3.medium"
  allocated_storage   = 50
  username            = "ledger_admin"
  password_wo         = var.db_password
  password_wo_version = 1
}

OpenTofu 1.11, released in December 2025, added ephemeral resources and write-only attributes too. Both need provider support, so many resources still write secrets to state.

Where to Keep State: Remote Backends and Locking

A backend is where Terraform stores state. The default is a local file in your working directory, fine for experiments and wrong for anything shared. A remote backend keeps state in object storage your team and pipeline can both reach. Locking stops two runs from writing state at the same time: while one plan or apply holds the lock, the next run waits or fails.

On AWS, the S3 backend has supported native locking since Terraform 1.10. Setting use_lockfile = true writes a .tflock file next to the state object, so you no longer need a DynamoDB table. HashiCorp now marks DynamoDB-based locking as deprecated.

HCL
terraform {
  backend "s3" {
    bucket       = "acme-terraform-state"
    key          = "payments/prod/terraform.tfstate"
    region       = "us-east-1"
    encrypt      = true
    use_lockfile = true
  }
}

The Google Cloud Storage (gcs) backend supports locking out of the box, and the Azure (azurerm) backend locks using Azure Blob Storage's native capabilities. To move existing local state into a new backend, add the backend block and run terraform init -migrate-state.

Set up the bucket the same way on any cloud:

  • Turn on versioning. HashiCorp calls it highly recommended for both S3 and GCS, because it is how you recover state after an accidental delete or a bad write.
  • Restrict access. Only your pipeline role and a small break-glass group should read state. Reading state means reading its secrets.
  • Split state by project and environment. A bad apply in staging then cannot touch production state.
  • Encrypt at rest. Server-side encryption protects the stored object, but anyone allowed to read it still sees every value.

OpenTofu Client-Side State Encryption

Since version 1.7, OpenTofu supports client-side encryption: state and plan files are encrypted before they leave the machine running OpenTofu, so the bucket only ever holds ciphertext. Keys come from a passphrase (pbkdf2) or a key management service such as AWS KMS or OpenBao.

HCL
terraform {
  encryption {
    key_provider "pbkdf2" "main" {
      passphrase = var.state_passphrase
    }
    method "aes_gcm" "main" {
      keys = key_provider.pbkdf2.main
    }
    state {
      method   = method.aes_gcm.main
      enforced = true
    }
    plan {
      method   = method.aes_gcm.main
      enforced = true
    }
  }
}

Back up your keys: the OpenTofu docs warn that state encrypted with a lost key cannot be recovered.

Terraform State Commands You Will Use

BASH
terraform state list
terraform state show aws_db_instance.ledger
terraform state mv aws_s3_bucket.logs module.logging.aws_s3_bucket.this
terraform state rm aws_instance.legacy
terraform import aws_s3_bucket.existing acme-old-logs
terraform state pull > state-backup.tfstate
terraform force-unlock LOCK_ID

list and show only read state, and state pull saves a backup copy. The mv, rm, and import commands change state the moment you run them, with no plan and no review. Their configuration equivalents do the same job inside a normal plan: a moved block (Terraform 1.1 and later) replaces state mv, a removed block with destroy = false (Terraform 1.7 and later) replaces state rm, and an import block (Terraform 1.5 and later) replaces terraform import. Prefer the blocks for shared state, so the change goes through code review like everything else. Our guide to refactoring with these blocks has worked examples.

Use force-unlock only after you have confirmed no run is still going. The command does not touch infrastructure, but clearing a lock while an apply is writing state is how state gets corrupted.

When State Is Lost, Stale, or Wrong

In February 2026, an engineer moved to a new laptop and let an AI coding agent run Terraform for a production platform. The state file had lived locally on the old machine, so the new plan wanted to create everything from scratch. While cleaning up the duplicates, the agent swapped in an older state file from an archived folder, then ran terraform destroy. Because that state pointed at production, the destroy removed the VPC, the RDS database, the ECS cluster, the load balancers, and the automated snapshots behind 2.5 years of course data. AWS support restored a snapshot about 24 hours later. The author's write-up walks through every step.

Two things failed: state lived on one laptop instead of a locked, versioned backend, and nothing stood between the agent and a destroy. Our post on stopping a coding agent from running terraform destroy covers the second. For the first, when a plan looks wrong:

  • Stop and do not apply. A plan that creates or destroys far more than you expect usually means Terraform is reading the wrong state, or an old copy of it.
  • Check which state you are reading. Confirm the backend key and run terraform workspace show.
  • Restore from versioning. Roll the state object back to the last good version in the bucket.
  • Rebuild if no copy exists. Write import blocks for the existing objects and run a plan until it shows no changes.
  • Reconcile drift. terraform plan -refresh-only shows how real objects differ from state without proposing changes to them.

Common Terraform State Problems and Fixes

ProblemLikely causeFix
Error acquiring the state lockA crashed or cancelled run left its lockConfirm no run is active, then terraform force-unlock LOCK_ID
Plan wants to create resources that already existWrong backend, workspace, or a missing local state filePoint at the right state or restore it; do not apply
Plan destroys and recreates a renamed resourceThe resource address changed in codeAdd a moved block
Secrets readable in stateSensitive values are still written to stateTighten bucket access, use ephemeral values and write-only arguments, or OpenTofu encryption
State overwritten or corruptedConcurrent writes without locking, or an old copy pushedRestore the previous version from bucket versioning
Resource changed in the consoleDrift between state and the real objectterraform plan -refresh-only, then update code or apply
Resource deleted outside TerraformState lists an object that is goneterraform plan -refresh-only, then apply it

How ops0 Handles Terraform State

In ops0, every Terraform and OpenTofu project gets its own isolated state backend, set up in a click. Choose Amazon S3, Google Cloud Storage, Azure Blob Storage, or Oracle Object Storage in your own bucket, or let ops0 manage state on AWS, isolated per organization.

State can sit in a different account or cloud from the one you deploy to, with separate credentials, so a team shipping to Azure can keep its state in AWS. Downloading state is permission-gated per project, ops0 reads your buckets only through your organization's own cloud connection, and changing a backend requires Configure permission on the project.

Oxid, ops0's open-source infrastructure engine, keeps state in PostgreSQL instead of a JSON file, so engineers can answer questions like "which instances across all our projects are missing an Owner tag" with one read-only SQL query instead of a script that downloads and parses state files.

For teams using AI coding agents on infrastructure repos, the ops0 CLI adds a pre-command hook that blocks terraform destroy, tofu destroy, and oxid destroy, and records each attempt. Engineers keep the speed of agent-written changes, and every change still goes through plan, policy checks, and approval. See IaC in ops0.

The docs walk through importing an existing Terraform project and detecting drift against state.

Quick answers

Where is the Terraform state file stored?

By default, in a file called terraform.tfstate in the directory where you run Terraform. Teams should configure a remote backend instead, such as an S3, Google Cloud Storage, or Azure Blob Storage bucket, so state is shared, locked during runs, and versioned.

Should I commit terraform.tfstate to Git?

No. State contains sensitive values in plain text, Git has no locking, and two engineers working from different copies will overwrite each other. Add *.tfstate and *.tfstate.backup to .gitignore and use a remote backend.

Does the Terraform state file contain passwords?

Often, yes. Marking a value sensitive hides it from output but still stores it in state. Ephemeral values in Terraform 1.10 and later, write-only arguments in Terraform 1.11 and later, and the same features in OpenTofu 1.11 keep secrets out of state where the provider supports them.

What happens if I delete or lose the state file?

Terraform forgets what it manages. The next plan proposes creating every resource again, and the real objects keep running unmanaged. Restore the last good version from bucket versioning, or rebuild state with import blocks. Do not apply a plan that recreates everything.

How do I fix "Error acquiring the state lock"?

A previous run still holds the lock, or crashed without releasing it. Check that no plan or apply is still running in your pipeline or on a teammate's machine, then run terraform force-unlock with the lock ID shown in the error.

Sources
From article to workflow

Turn infrastructure context into a reviewed change.

Review generated infrastructure code with policy checks, cost context, approval, and audit evidence before deployment.

Related articles

All articles