Reference · Terraform

State Operations

Why state exists, what drift really does, and the commands that manipulate Terraform's memory.

Why state exists at all (the four official reasons)

ReasonThe first-principles version
MappingTerraform must know which real object each config block owns. HashiCorp tried using cloud tags in early prototypes — failed, because not all resources/providers support tags. So the mapping is explicit, in state.
DependenciesWhen you delete a resource from config, its dependency info is gone from config too — state remembers it, so Terraform still knows the correct destroy order.
PerformanceState caches attributes. Querying every real resource on every plan is too slow at scale (API latency, rate limits) — hence -refresh=false exists.
SyncingTeams need one agreed memory. Remote state + locking ensures runs start from the latest state and can't stomp each other.

Source: Purpose of Terraform State

The drift rule that surprises everyone

Config wins. Refresh folds reality into state, but the diff is still Desired-vs-State — so a plain plan/apply after drift proposes to undo the manual change, pushing reality back to match config. Refresh never changes your config, and drift never becomes "accepted" on its own.

To accept drift instead of reverting it: update state with a reviewed terraform apply -refresh-only, and update your config to match (if the drifted attribute is set in config). One without the other and the next plan still wants to revert.

Forget vs. destroy — the distinction that saves production

🗑️ Destroy (remove the real thing)

Delete the resource block → next apply destroys the real resource.

Or keep the block and run terraform destroy -target=….

🧠 Forget (stop managing, keep the real thing)

terraform state rm ADDR — removes from state only. Real resource untouched, Terraform no longer knows it exists.

Modern (≥1.7): a removed block in config — declarative, reviewable forget.

Gotcha: after state rm, the block is still in config → next plan says + create. For globally-named things (S3 buckets) the apply then fails — name taken. The inverse of forgetting is terraform import (or an import block): adopt an existing real resource into state.

Command cheat sheet

CommandTouchesUse when
terraform state listreads stateWhat does Terraform think it manages?
terraform state show ADDRreads stateInspect one resource's recorded attributes.
terraform plan -refresh-onlynothing (preview)Detect drift: shows what refresh would change in state.
terraform apply -refresh-onlystate onlyAccept reality into state — reviewed, no infra changes. Replaces deprecated terraform refresh.
terraform plan -refresh=falsenothingSkip the slow refresh; diff config vs last-known state. Fast, but blind to drift.
terraform state rm ADDRstate onlyForget a resource (keep it alive, unmanaged).
terraform state mv A Bstate onlyRename/move without destroy+create (modern alt: moved block).
terraform import ADDR IDstate onlyAdopt an existing real resource (modern alt: import block + plan).

Sources: refresh-only tutorial · drift tutorial · state command docs

What's actually in terraform.tfstate

JSON. For each resource: its address (aws_s3_bucket.logs), the real-world ID (the mapping), all known attributes (the cache), and dependencies (the destroy-order memory). Plus outputs and the state serial (version counter used for sync). It can contain secrets in plaintext — treat state like a credential, never commit it.