Terraform · why it works that way
Why does Terraform need a state file at all?
Short answer
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
movedblock, 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-interraform_dataresource 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.tfstatein 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 listshows the one address Terraform tracks.terraform state showshows what it recorded, includingid = "d44afb6b-f342-04bd-36e3-eb3bc76a3526". That ID appears nowhere inmain.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, withid = (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-...]andNo 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 applyleft the previous version interraform.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 lockwithError message: resource temporarily unavailableand aLock Info:block- Another Terraform process holds the lock; reproduced with
terraform consoleleft open in the same folder. Let it finish or close it. Useterraform 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 listprints 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 fromterraform.tfstate.backupor your backend's versioning, or bring the objects back under management with import blocks. Error: Invalid use of ephemeral valuewithEphemeral 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 createdin 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
removedblock withlifecycle { destroy = false }, orterraform state rm. Aremovedblock without that setting destroys the object.
Security considerations
sensitive = truehides a value on screen, not in state. Reproduced: a variable marked sensitive appeared in plain text twice interraform.tfstate, asinputandoutput.terraform state showprintedinput = (sensitive value)but showed the same secret in full asoutput. Anyone who can read state can read the secret.- The local state file gets ordinary file permissions. Reproduced,
terraform.tfstateandterraform.tfstate.backupwere 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:GetObjecton the state key, and anyone granted it can download the whole file, so treat that permission as access to every secret in it. Setencrypt = true, and turn onuse_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 -outholds 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.
- Terraform — terraform plan command
- Terraform — Write-only arguments
- Terraform — local backend
- Terraform — S3 backend
- Terraform — terraform state rm
- Terraform — Refactor modules
- Terraform — terraform_data resource
- Terraform — Import existing resources
- Terraform — removed block reference
- Terraform — Purpose of Terraform State
- Terraform — Manage sensitive data in your configuration
- Terraform — State: Locking
Terms used here (2)plain definitions
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.
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