Skip to main content
DevOps Insightsguides & answers

Terraform · why it works that way

Why does Terraform need a state file at all?

Short answer

Because your configuration says what should exist, and the cloud says what does exist, and neither tells you which real resource corresponds to which block of code. State is that mapping — and it is also the only record of things you asked for that the provider does not report back.

Reproduced on Terraform 1.14.0, . Where the text says “reproduced”, the behaviour was run on that version, not taken from documentation.

First published

On this page · 7 sections

The constraint behind it

Consider what Terraform has to work out. Your configuration declares aws_instance.web. The provider can list every instance in the account. Nothing in either of those says which instance is aws_instance.web — cloud resources are identified by opaque IDs that your code never mentions.

Without a mapping, the only options are both bad. Terraform could match on attributes, which breaks the moment two resources look alike or one is renamed. Or it could tag everything it creates and search by tag, which fails for resource types and providers that do not support tags and turns every plan into a full inventory scan.

There is a second, less obvious reason. Some values exist only in what Terraform recorded when it created them: a password generated by a managed resource, an attribute the API returns once. Without that record, Terraform could not tell whether they had changed or let anything refer to them later. (Terraform 1.11 added write-only arguments for the opposite case: values it deliberately never stores, which you change by bumping a version argument where the provider offers one.)

And a third: dependency order. Deleting resources safely requires knowing what depended on what as of the most recent run, which your current configuration cannot tell you — you may have already removed the block that created the dependency.

Words used on this page

state
Terraform's record of which real object belongs to each resource in your configuration, with the attribute values and dependencies it last saw.
resource address
The name Terraform records a resource under, such as terraform_data.web. It comes from your configuration, not from the cloud.
remote object
The real thing a resource manages, such as a server or a bucket, known to the provider by its own ID.
backend
Where state is kept: a local file by default, or remote storage such as an S3 bucket or HCP Terraform.
state lock
A lock Terraform takes on the state during operations that could write it, so two runs cannot change it at once. Not every backend supports one.
refresh
The first step of a plan: Terraform asks the provider for the current values of every object in state.
drift
A difference between what state records and what really exists, for example because something was changed outside Terraform.
sensitive
A marking that hides a value in Terraform's output. The value is still stored in state and plan files.
ephemeral value
A value Terraform keeps out of state and plan files entirely, such as a variable with ephemeral = true (Terraform 1.10 and later).
write-only argument
A resource argument, from Terraform 1.11, that is sent to the provider but never stored. Only providers that implement it offer one.

If it were the other way

A stateless Terraform would have to reconstruct reality on every run by querying every resource type in every region and matching each one against your configuration. Plans would take a very long time and would be wrong whenever two resources were similar enough to confuse the matcher.

Renaming a resource in code could never become a move. Even today a rename is a destroy-and-create by default, but state is what lets a moved block or terraform state mv carry the existing object over to the new name. Without a record of the old identity there would be nothing to move, and every refactor would be a rebuild.

Generated values would be lost. A password Terraform created and the provider never returns would be unknowable, so nothing could refer to it later — and terraform destroy would have no reliable order to work in, because dependency information would have to come from a configuration that no longer describes what exists.

Some tools do work this way, and they cope by being much more constrained: they either require every resource to carry an identifying tag, or they only manage resources they can unambiguously name. That is a real design and it buys statelessness by giving up generality.

What it buys and costs you

What it buys you

  • A stable identity for each resource, which is what makes refactoring possible: with a moved block, renaming a block becomes a move rather than a rebuild.
  • Fast plans. Terraform refreshes what it knows about rather than discovering the account from scratch.
  • Memory of generated values, such as a password created by a managed resource, so later runs can compare and reference them.
  • A correct destruction order, from the most recent dependencies kept in state rather than inferred from a configuration that may have changed.

What it costs you

  • State becomes a critical artefact with its own operational needs: remote storage, locking, versioning, backups. Losing it does not destroy your infrastructure, but it does mean Terraform no longer knows it exists.
  • It can contain secrets. A password or key produced by an ordinary managed resource is stored in plain text. Ephemeral resources and write-only arguments, added in Terraform 1.10 and 1.11, exist to keep such values out. That makes the state backend a security boundary: anyone who can read the bucket can read those values.
  • Concurrency has to be managed, which is where locking comes from and why a stuck lock is one of the operational problems state brings.
  • It can disagree with reality, and that disagreement is drift — a whole category of problem that exists only because there is a third thing to be out of step with.

Where it bites in practice

Most sharply the first time somebody deletes a resource in a console and expects Terraform not to care. The configuration still declares it, so when the plan's refresh finds it gone, Terraform proposes creating it again — which reads as Terraform inventing work. It is doing precisely what it was built to do: reconciling three sources of truth, one of which somebody changed without telling it.

Try it yourself

Before you start

  • Terraform 1.4 or later (terraform version) for the built-in terraform_data resource used below. It needs no provider download and no cloud account. Everything here was reproduced on Terraform 1.14.0.
  • An empty scratch folder. With no backend configured, Terraform keeps state in terraform.tfstate in that folder, so you can look at it, move it and put it back safely.
  • Never practise on a real project's state. Moving or editing a shared state file changes what Terraform believes exists for everyone who uses it.
  • For a real project, read access to its state backend, and the understanding that this is also read access to every value stored in it, secrets included.
  • A way back before any manual state change: the local backend keeps the previous version in terraform.tfstate.backup, and for the S3 backend HashiCorp strongly recommends turning on bucket versioning.

What Terraform forgets when it loses its state

Reproduced on Terraform 1.14.0 on macOS in an empty folder, with the time zone set to UTC, using only the built-in terraform_data resource: terraform init printed terraform.io/builtin/terraform is built in to Terraform and downloaded nothing. terraform apply -auto-approve had already created the one resource. Moving the state file aside stands in for losing it.

Command

cat main.tf
terraform state list
terraform state show terraform_data.web
mv terraform.tfstate lost.tfstate
terraform plan -no-color
mv lost.tfstate terraform.tfstate
terraform plan -no-color

Output

$ cat main.tf
resource "terraform_data" "web" {
  input = "web-1"
}

$ terraform state list
terraform_data.web

$ terraform state show terraform_data.web
# terraform_data.web:
resource "terraform_data" "web" {
    id     = "d44afb6b-f342-04bd-36e3-eb3bc76a3526"
    input  = "web-1"
    output = "web-1"
}

$ mv terraform.tfstate lost.tfstate

$ terraform plan -no-color

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # terraform_data.web will be created
  + resource "terraform_data" "web" {
      + id     = (known after apply)
      + input  = "web-1"
      + output = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

─────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.

$ mv lost.tfstate terraform.tfstate

$ terraform plan -no-color
terraform_data.web: Refreshing state... [id=d44afb6b-f342-04bd-36e3-eb3bc76a3526]

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration
and found no differences, so no changes are needed.

How to read it

  • terraform state list shows the one address Terraform tracks. terraform state show shows what it recorded, including id = "d44afb6b-f342-04bd-36e3-eb3bc76a3526". That ID appears nowhere in main.tf: Terraform generated it when it created the resource, and state is the only place it is kept.
  • With the state file moved away, the same configuration plans terraform_data.web will be created, with id = (known after apply). The object still exists; what is gone is the link between your block and it. With a real provider, applying this plan would create a second object beside the first, with a new ID.
  • With the file back, the plan prints Refreshing state... [id=d44afb6b-...] and No changes. Terraform checked only the object state pointed it to. It did not have to search the account for it.
  • In the same folder, a second terraform apply left the previous version in terraform.tfstate.backup. That file is the local backend's only built-in way back, and it holds the same values, secrets included, as the state.

If it goes wrong

Error: Error acquiring the state lock with Error message: resource temporarily unavailable and a Lock Info: block
Another Terraform process holds the lock; reproduced with terraform console left open in the same folder. Let it finish or close it. Use terraform force-unlock <ID> only when you are sure the holder has gone: HashiCorp warns that unlocking while someone else holds the lock can cause multiple writers.
terraform state list prints nothing, and the plan proposes creating resources that already exist
Terraform is looking at an empty or different state: another folder, another workspace (terraform workspace show), or a backend that has not been initialised. Do not apply. Point Terraform back at the right state, restore it from terraform.tfstate.backup or your backend's versioning, or bring the objects back under management with import blocks.
Error: Invalid use of ephemeral value with Ephemeral values are not valid for "input", because it is not a write-only attribute and must be persisted to state.
You passed an ephemeral value into an argument Terraform has to store. Use the provider's write-only argument for it if one exists (Terraform 1.11 and later), or keep the value out of that resource.
A resource someone deleted in the console shows as will be created in the next plan
The configuration still declares it, so Terraform proposes to recreate it. Apply if it should exist. If it should stay gone, delete the block. To stop managing an object without destroying it, use a removed block with lifecycle { destroy = false }, or terraform state rm. A removed block without that setting destroys the object.

Security considerations

  • sensitive = true hides a value on screen, not in state. Reproduced: a variable marked sensitive appeared in plain text twice in terraform.tfstate, as input and output. terraform state show printed input = (sensitive value) but showed the same secret in full as output. Anyone who can read state can read the secret.
  • The local state file gets ordinary file permissions. Reproduced, terraform.tfstate and terraform.tfstate.backup were created -rw-r--r-- under umask 022, readable by every account on the machine. HashiCorp says to keep state out of Git. Keep it off shared machines too, or move to a remote backend.
  • With the S3 backend, the bucket is the security boundary. The backend's documented permissions include s3:GetObject on the state key, and anyone granted it can download the whole file, so treat that permission as access to every secret in it. Set encrypt = true, and turn on use_lockfile: locking is opt-in for S3, and without it two runs can write the same state.
  • Keep secrets out of state where you can. Ephemeral variables (Terraform 1.10 and later) and write-only arguments (1.11 and later) are never written to state or plan files. Reproduced, Terraform refuses an ephemeral value in an argument it would have to persist, so the mistake fails at plan time instead of leaking.
  • Saved plans carry secrets too. The plan documentation says a file from terraform plan -out holds sensitive values in cleartext, even when the terminal hides them. Protect CI plan artifacts like the state itself, and delete them after apply.

Sources

The behaviour this page describes, as documented by the projects themselves.

Terms used here (2)

Short definitions for the vocabulary on this page, in case any of it is new. Each links to the full entry.

Infrastructure as Code
The practice of managing and provisioning infrastructure through machine-readable configuration files rather than manual processes. IaC tools like Terraform and Ansible make infrastructure reproducible, version-controlled, and auditable.
Terraform
An Infrastructure as Code tool by HashiCorp that lets you define and provision cloud and on-premises infrastructure using declarative configuration files (HCL). Since version 1.6 it has been source-available under the Business Source License rather than open source; OpenTofu is the open-source fork. Terraform manages the full lifecycle of resources through providers, plugins for cloud platforms and other APIs that number in the thousands.

All 59 terms in the glossary

Did this get you to an answer?

No text box on purpose — please do not paste production logs anywhere

Something wrong or out of date? Report it