Terraform State Explained with Examples

Tested on Ubuntu 26.04 LTS (Resolute Raccoon)
Package terraform 1.15.8-1
hashicorp/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.

text
Configuration
Prior state ──► Terraform Core ◄── Provider refresh
            Execution plan
             Real resources

The walkthrough uses hashicorp/local to manage a file on disk so you can inspect terraform.tfstate without cloud credentials.

NOTE
Complete install Terraform on Ubuntu and the Terraform lab environment first. Examples use ~/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:

bash
mkdir -p ~/terraform-labs/terraform-state

Change into that directory for the rest of the walkthrough:

bash
cd ~/terraform-labs/terraform-state

Write a minimal configuration:

bash
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"
}
EOF

Download the provider plugin:

bash
terraform init
output
Initializing 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/:

bash
ls terraform.tfstate 2>&1
output
ls: cannot access 'terraform.tfstate': No such file or directory

Create the managed file and the initial state record:

bash
terraform apply -auto-approve
output
local_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:

bash
ls -1 example.txt terraform.tfstate
output
example.txt
terraform.tfstate

The mapping Terraform just wrote:

text
local_file.example  ↔  example.txt on disk

View state with terraform show

Prefer Terraform commands over opening terraform.tfstate in an editor.

Show the current state in human-readable form:

bash
terraform show -no-color
output
# 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:

bash
terraform state list
output
local_file.example

Each line is a resource address — Terraform's name for one managed instance:

text
local_file.example
 │          └── resource name (label)
 └────────────── resource type

The 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:

bash
sed -i 's/Managed by Terraform/version 2/' main.tf

Preview how Terraform reconciles configuration, prior state, and the refreshed real file:

bash
terraform plan -no-color
output
# 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:

bash
terraform apply -auto-approve
output
local_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:

bash
terraform state list
output
local_file.example

terraform.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:

bash
sed -i 's/version 2/backed up content/' main.tf

Apply so Terraform updates persistent state and retains the prior snapshot:

bash
terraform apply -auto-approve

List state-related files:

bash
ls -1 terraform.tfstate*
output
terraform.tfstate
terraform.tfstate.backup

Do not edit either file by hand. Deep backup and restore workflows belong in state management guides.

Destroy a managed resource

Remove the managed object:

bash
terraform destroy -auto-approve
output
local_file.example: Destroying... [id=85de840f8b53a6677c4c23e9de47f3b377250cc9]
local_file.example: Destruction complete after 0s

Destroy complete! Resources: 1 destroyed.

State no longer lists managed resources:

bash
terraform state list

An 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:

bash
terraform apply -auto-approve
output
local_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:

bash
printf 'edited outside terraform\n' > example.txt

Run a plan to see how Terraform responds:

bash
terraform plan -no-color
output
local_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:

bash
terraform apply -auto-approve

What happens if terraform.tfstate is lost

IMPORTANT
Never delete state against important infrastructure to see what happens. State is not a disposable cache — losing it breaks Terraform's mapping to real objects and can lead to duplicate resources or destructive mistakes.

On a disposable lab resource only, back up state first:

bash
cp terraform.tfstate terraform.tfstate.lab-backup

Remove the state file while example.txt still exists on disk:

bash
rm -f terraform.tfstate

Run a plan — Terraform no longer knows it already manages the file:

bash
terraform plan -no-color
output
Terraform 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:

bash
mv terraform.tfstate.lab-backup terraform.tfstate

terraform 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:

bash
cat >> main.tf <<'EOF'

output "demo_token" {
  value     = "demo-token-not-real"
  sensitive = true
}
EOF

Apply so Terraform records the output in persistent state:

bash
terraform apply -auto-approve

Plain terraform output hides the value:

bash
terraform output
output
demo_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:

gitignore
*.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
text
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


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 show and terraform state list
  • Watched terraform.tfstate.backup appear 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.tfstate out of Git
  • Commit .terraform.lock.hcl instead

When teams need centralized storage, optional locking on supported backends, and shared access control, continue with Terraform backends and remote state.


Frequently Asked Questions

1. What is Terraform state?

Terraform state is a record that maps resource addresses in your configuration, such as local_file.example, to real managed objects and their last known attributes. Terraform reads state during plan and apply so it knows which API objects belong to which resource blocks.

2. Where is the Terraform state file stored locally?

By default Terraform writes terraform.tfstate in the working directory where you run terraform init and apply. Remote backends store state outside the module directory, which the remote backend guide covers separately.

3. Should I commit terraform.tfstate to Git?

No. State can contain sensitive values and describes your live infrastructure. Commit .terraform.lock.hcl for provider version consistency, but add terraform.tfstate and terraform.tfstate.backup to .gitignore.

4. What is the difference between terraform.tfstate and .terraform.lock.hcl?

terraform.tfstate records managed resources and their mapping to real infrastructure. .terraform.lock.hcl records which provider plugin versions terraform init selected. They solve different problems and both matter, but only the lock file belongs in version control by default.

5. What happens if I delete terraform.tfstate?

Terraform loses its mapping to existing objects. The next plan may propose creating replacements even when real resources still exist, which can cause duplicates or conflicts. Treat state deletion as destructive and never experiment on important infrastructure.

6. Does sensitive true keep secrets out of state?

No. sensitive redacts CLI output for outputs and some logs, but values can still be stored in terraform.tfstate. Protect state files with filesystem permissions, remote backend access controls, and encryption where available.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)