| Tested on | Ubuntu 26.04 LTS (Resolute Raccoon) |
|---|---|
| Package | terraform 1.15.8-1hashicorp/local 2.9.0 |
| Applies to | Any host with Terraform installed |
| Lab environment | Single Ubuntu VM with Terraform — Terraform lab environment on Ubuntu |
| Privilege | Normal user |
| Scope | Terraform state fundamentals — why state exists, configuration vs state vs real infrastructure, terraform.tfstate contents, terraform show and state list, lifecycle updates, drift introduction, sensitive data and Git guidance, and local vs remote state overview. Does not cover state subcommands in depth, backend configuration, import, refresh-only mode, moved blocks, or disaster recovery procedures. |
| Related guides | Terraform providers Terraform provider version lock file Terraform HCL syntax Terraform lab environment on Ubuntu Terraform Associate certification course |
Terraform state maps resource addresses in your configuration to real managed objects. Terraform Core combines configuration, prior state, and provider-reported reality to build a plan — state is not a pipeline stage configuration flows through.
Configuration
│
▼
Prior state ──► Terraform Core ◄── Provider refresh
│
▼
Execution plan
│
▼
Real resourcesThe walkthrough uses hashicorp/local to manage a file on disk so you can inspect terraform.tfstate without cloud credentials.
~/terraform-labs/terraform-state so they stay separate from other lessons and parallel lab work.
How Terraform state works
Why Terraform needs state
Terraform must know which real object belongs to each resource address in your configuration. Cloud and service APIs expose instances by their own IDs — S3 bucket names, file paths, container IDs — not by local_file.example. State stores that mapping plus attribute values Terraform recorded during the last successful write to persistent state.
Without state, Terraform could not reliably update or destroy an object it created earlier — it would not know which API object corresponds to local_file.example in your module.
Configuration vs state vs real infrastructure
These three layers answer different questions:
| Layer | Question it answers | Example in this lab |
|---|---|---|
| Configuration | What should exist? | content = "Managed by Terraform" in main.tf |
| State | What does Terraform last record about managed objects? | terraform.tfstate after a successful apply |
| Real infrastructure | What actually exists right now? | Bytes in example.txt on disk |
During terraform plan, Terraform refreshes its in-memory view of real objects through the provider, compares that to configuration and prior state, and proposes create, update, or destroy actions. You will change configuration and run plan in the lab below.
What Terraform stores in state
terraform.tfstate is JSON that Terraform owns. The internal schema can change between Terraform versions — treat it as implementation data, not a public API to parse by hand.
Useful categories after apply:
- Resource addresses (
local_file.example) - Provider configuration references
- Resource IDs returned by the provider
- Last known attribute values
- Dependency metadata Terraform uses for ordering
- Output values recorded in state
Prefer terraform show and terraform state list over hand-editing the file. Do not depend on a stable JSON layout across upgrades.
Create and inspect local Terraform state
Create the local_file lab
Create the state lab directory:
mkdir -p ~/terraform-labs/terraform-stateChange into that directory for the rest of the walkthrough:
cd ~/terraform-labs/terraform-stateWrite a minimal configuration:
cat > main.tf <<'EOF'
terraform {
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
provider "local" {}
resource "local_file" "example" {
filename = "${path.module}/example.txt"
content = "Managed by Terraform"
}
EOFDownload the provider plugin:
terraform initInitializing provider plugins...
- Finding hashicorp/local versions matching "~> 2.5"...
- Installing hashicorp/local v2.9.0...
- Installed hashicorp/local v2.9.0 (signed by HashiCorp)
Terraform has been successfully initialized!Before the first apply, Terraform has no resource mapping yet — only provider plugins under .terraform/:
ls terraform.tfstate 2>&1ls: cannot access 'terraform.tfstate': No such file or directoryCreate the managed file and the initial state record:
terraform apply -auto-approvelocal_file.example: Creating...
local_file.example: Creation complete after 0s [id=837c55ffe364651b77f5c4ba046777d02d10e2dc]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.Confirm both the real file and the state file exist:
ls -1 example.txt terraform.tfstateexample.txt
terraform.tfstateThe mapping Terraform just wrote:
local_file.example ↔ example.txt on diskView state with terraform show
Prefer Terraform commands over opening terraform.tfstate in an editor.
Show the current state in human-readable form:
terraform show -no-color# local_file.example:
resource "local_file" "example" {
content = "Managed by Terraform"
content_sha1 = "837c55ffe364651b77f5c4ba046777d02d10e2dc"
filename = "./example.txt"
id = "837c55ffe364651b77f5c4ba046777d02d10e2dc"
}If you need machine-readable output, terraform show -json exists for tooling — but JSON output can expose sensitive values that CLI redaction hides elsewhere.
List resource addresses with terraform state list
List addresses tracked in persistent state:
terraform state listlocal_file.exampleEach line is a resource address — Terraform's name for one managed instance:
local_file.example
│ └── resource name (label)
└────────────── resource typeThe same address appears in references such as local_file.example.id. With count or for_each later, addresses gain index keys such as aws_instance.web[0] — this lab uses a single instance.
terraform state list is enough for this fundamentals lesson. Subcommands such as terraform state show and terraform state mv belong in the dedicated terraform state commands guide.
How Terraform state changes
Terraform refreshes its view of managed objects while planning. Persistent state is written when Terraform applies approved changes — including a normal apply or an approved -refresh-only apply. A plan alone updates Terraform's in-memory picture; it does not commit those refresh results to terraform.tfstate until you apply.
Change configuration and apply
Change the desired content in configuration:
sed -i 's/Managed by Terraform/version 2/' main.tfPreview how Terraform reconciles configuration, prior state, and the refreshed real file:
terraform plan -no-color# local_file.example must be replaced
-/+ resource "local_file" "example" {
~ content = "Managed by Terraform" -> "version 2" # forces replacement
# ...
}
Plan: 1 to add, 0 to change, 1 to destroy.Here local_file replaces the resource because content is a force-new attribute. Other resource types may show in-place updates instead — a dedicated plan lesson covers reading plan output in depth.
Apply the approved change so Terraform writes the updated mapping to disk:
terraform apply -auto-approvelocal_file.example: Destroying... [id=837c55ffe364651b77f5c4ba046777d02d10e2dc]
local_file.example: Destruction complete after 0s
local_file.example: Creating...
local_file.example: Creation complete after 0s [id=85de840f8b53a6677c4c23e9de47f3b377250cc9]
Apply complete! Resources: 1 added, 0 changed, 1 destroyed.Confirm state still tracks the resource:
terraform state listlocal_file.exampleterraform.tfstate.backup
With local state, Terraform keeps the previous state snapshot in terraform.tfstate.backup when it writes a new state snapshot. That one-generation backup helps on the same machine — it is not a substitute for remote state versioning or external backups.
Trigger another state rewrite:
sed -i 's/version 2/backed up content/' main.tfApply so Terraform updates persistent state and retains the prior snapshot:
terraform apply -auto-approveList state-related files:
ls -1 terraform.tfstate*terraform.tfstate
terraform.tfstate.backupDo not edit either file by hand. Deep backup and restore workflows belong in state management guides.
Destroy a managed resource
Remove the managed object:
terraform destroy -auto-approvelocal_file.example: Destroying... [id=85de840f8b53a6677c4c23e9de47f3b377250cc9]
local_file.example: Destruction complete after 0s
Destroy complete! Resources: 1 destroyed.State no longer lists managed resources:
terraform state listAn empty result means Terraform is not tracking objects in this working directory. The terraform.tfstate file may still exist with an empty resource list.
Re-create the resource so local_file.example is present in state for the remaining demonstrations:
terraform apply -auto-approvelocal_file.example: Creating...
local_file.example: Creation complete after 0s [id=58a74a7aa4a0d8bdb599afa96c365260a458f712]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.Terraform drift and lost state
Detect changes made outside Terraform
Drift means the real object changed outside Terraform while persistent state still describes the previous reality. Terraform detects drift during the refresh step at the start of plan or apply.
Overwrite the managed file manually:
printf 'edited outside terraform\n' > example.txtRun a plan to see how Terraform responds:
terraform plan -no-colorlocal_file.example: Refreshing state... [id=58a74a7aa4a0d8bdb599afa96c365260a458f712]
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+ create
# local_file.example will be created
+ resource "local_file" "example" {
+ content = "backed up content"
# ...
}
Plan: 1 to add, 0 to change, 0 to destroy.The Refreshing state line is the provider reporting current reality back to Terraform Core. The proposed actions depend on the provider — here Terraform plans to reconcile toward the declared content. Full refresh-only workflows and production drift handling are covered in the drift and refresh-only guide.
Restore the file to match configuration:
terraform apply -auto-approveWhat happens if terraform.tfstate is lost
On a disposable lab resource only, back up state first:
cp terraform.tfstate terraform.tfstate.lab-backupRemove the state file while example.txt still exists on disk:
rm -f terraform.tfstateRun a plan — Terraform no longer knows it already manages the file:
terraform plan -no-colorTerraform used the selected providers to generate the following execution
plan. Resource actions are indicated by the following symbols:
+ create
# local_file.example will be created
+ resource "local_file" "example" {
+ content = "backed up content"
# ...
}
Plan: 1 to add, 0 to change, 0 to destroy.Without state, Terraform treats the resource as uncreated even though the file is still present. Restore the lab backup before continuing:
mv terraform.tfstate.lab-backup terraform.tfstateterraform state list should show local_file.example again.
Protect Terraform state
Sensitive values in state
State can contain values that never appear in plain CLI output.
Add a harmless sensitive output:
cat >> main.tf <<'EOF'
output "demo_token" {
value = "demo-token-not-real"
sensitive = true
}
EOFApply so Terraform records the output in persistent state:
terraform apply -auto-approvePlain terraform output hides the value:
terraform outputdemo_token = <sensitive>sensitive = true affects CLI presentation — it does not remove the value from terraform.tfstate. Protect local state with filesystem permissions. Teams using remote backends rely on access control and encryption there. See manage secrets and sensitive data in Terraform for output redaction and variable patterns.
Should terraform.tfstate be committed to Git?
Because state may embed sensitive values and describes live infrastructure, do not commit state files — even when outputs use sensitive = true.
Add ignore rules such as:
*.tfstate
*.tfstate.*Commit provider lock data instead:
| File | Commit to Git? |
|---|---|
terraform.tfstate |
No — mapping to real infrastructure |
terraform.tfstate.backup |
No — previous local snapshot |
.terraform.lock.hcl |
Yes — provider versions selected by terraform init |
Lock file behavior is documented in the provider version lock file guide.
Local vs remote Terraform state
By default Terraform persists state in the working directory as terraform.tfstate. That local state suits solo labs on one machine.
Teams commonly use a remote backend so state is stored centrally.
Supported backends can also provide state locking to prevent concurrent writes:
- Without locking, two operators applying at once can corrupt or overwrite state
- Terraform acquires a lock automatically when the configured backend supports it
Local state
terraform.tfstate in the module directory
Remote backend
state stored through a configured backend (S3, HCP Terraform, etc.)Backend configuration is out of scope here — see Terraform backends and remote state.
Common Terraform state misconceptions
| Misconception | Reality |
|---|---|
| State is just a cache | State is authoritative for Terraform's view of managed objects. Terraform needs it to map configuration to real APIs. |
| Terraform can rediscover everything without state | Providers do not expose a global search by resource address. State stores the IDs and metadata required for updates and destroys. |
sensitive = true removes secrets from state |
Redaction applies to CLI output in many cases; state can still contain the value. |
| Deleting state fixes errors | Usually makes things worse — Terraform may recreate or orphan real infrastructure. |
.terraform.lock.hcl is a state file |
The lock file records provider plugin versions for terraform init, not managed resources. |
plan writes an updated state file |
Plan refreshes in memory; persistent state updates on apply (including -refresh-only when approved). |
References
- State — Terraform documentation
- Resource addressing — Terraform documentation
- Sensitive values in state — Terraform documentation
- State locking — Terraform documentation
- local provider documentation
- Terraform Associate 004 Learning Path
Summary
Terraform state maps resource addresses such as local_file.example to real infrastructure.
Terraform Core combines configuration, prior state, and provider refresh to build a plan. Persistent state in terraform.tfstate updates when you apply approved changes — not during plan alone.
In the lab you:
- Created state with
apply - Inspected it through
terraform showandterraform state list - Watched
terraform.tfstate.backupappear after rewrites - Saw drift when the on-disk file changed outside Terraform
Deleting state breaks the mapping — back up first on disposable labs only.
State can hold sensitive values even when outputs use sensitive = true:
- Keep
terraform.tfstateout of Git - Commit
.terraform.lock.hclinstead
When teams need centralized storage, optional locking on supported backends, and shared access control, continue with Terraform backends and remote state.

