Terraform State Commands 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 CLI — resource addresses, state list, state show, state mv, state rm, state pull, state push, backup before modification, external verification after rm, mv vs moved blocks overview, push lineage warnings, and common mistakes. Does not cover import, full moved or removed block tutorial, backend configuration, locking, manual JSON editing, or disaster recovery.
Related guides Terraform state explained
Terraform backends and remote state
Terraform resource blocks
terraform apply command
Terraform Associate certification course

The terraform state command family inspects and deliberately modifies Terraform's state snapshot. Read commands such as list, show, and pull are safe for everyday troubleshooting. Write commands such as mv, rm, and push change what Terraform remembers — use them only when you understand the effect on the next plan.

text
Inspect:  list / show / pull
Modify:   mv / rm / push   (increasing caution)

The Terraform state lesson explains why state exists. This lesson walks the supported CLI for working with it. Examples use disposable terraform_data and local_file resources under ~/terraform-labs/terraform-state-command/.

NOTE
Run terraform init in each lab directory before other commands. Never practice state rm or state push on production state without a verified backup.

Lab setup

Create the main working directory:

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

Move into it — later commands in this section assume you are here:

bash
cd ~/terraform-labs/terraform-state-command/main

Add three disposable resources in main.tf:

hcl
resource "terraform_data" "example" {
  input = "inspect me"
}

resource "terraform_data" "spoke" {
  input = "second resource"
}

resource "local_file" "detach" {
  content  = "still on disk after state rm"
  filename = "${path.module}/detach.txt"
}

Declare providers in versions.tf:

hcl
terraform {
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "~> 2.5"
    }
  }
}

Initialize the directory:

bash
terraform init

Apply so state contains managed objects to inspect:

bash
terraform apply -auto-approve

Sample output:

output
Plan: 3 to add, 0 to change, 0 to destroy.
terraform_data.example: Creating...
terraform_data.spoke: Creating...
local_file.detach: Creating...
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

Three resource addresses now exist in terraform.tfstate — one for each block label.


Inspect state with list and show

Inspection commands read state without changing it. They answer what Terraform currently tracks and what attributes it last recorded.

terraform state list

List every resource address in the current state:

bash
terraform state list

Sample output:

output
local_file.detach
terraform_data.example
terraform_data.spoke

Each line is a resource address — the path Terraform uses in plans, dependency graphs, and state subcommands. Filter the list when you only care about one type:

bash
terraform state list | grep terraform_data

Sample output:

output
terraform_data.example
terraform_data.spoke

For count and for_each instances, addresses include index keys such as aws_instance.web[0] or aws_subnet.private["app"]. Quote those addresses in the shell so [ and ] are not treated as glob characters:

bash
terraform state show 'aws_instance.web[0]'

terraform state show

list prints addresses; show prints attributes for one address. Inspect the example resource:

bash
terraform state show terraform_data.example

Sample output:

output
# terraform_data.example:
resource "terraform_data" "example" {
    id     = "6d62185d-3ee6-61ba-b638-7bb338e0bf4a"
    input  = "inspect me"
    output = "inspect me"
}
text
list → addresses in state
show → attributes for one state object

The state entry contains the provider-managed identity and attributes Terraform needs to refresh this object later. Many resources expose that identity as an id, as terraform_data does here. Wrong addresses in show, mv, or rm produce errors — verify with list first.


Back up state before modification

Before state mv, state rm, or especially state push, capture a snapshot you can restore:

bash
terraform state pull > before-change.tfstate

state pull prints the current state JSON to stdout. Redirect it to a file outside version control. Remote backends may keep their own versioning, but a deliberate local copy still helps in a lab or during a controlled change. Terraform's state-modifying subcommands also write automatic backup files, but those backups cannot be disabled — an explicit state pull snapshot gives you a clearly named pre-change copy to retain during a controlled operation.

Confirm the backup captured your resources — three objects in this lab:

bash
jq '.resources | length' before-change.tfstate

Sample output:

output
3

Keep that file until you finish the modification section and verify the next plan.


Move resources with terraform state mv

Renaming a resource block in configuration changes its address. Without a state update, Terraform plans to destroy the old address and create the new one even when the real infrastructure is unchanged. terraform state mv retargets state to match a rename.

Create the mv demo directory:

bash
mkdir -p ~/terraform-labs/terraform-state-command/mv-demo

Work inside that directory for the rename exercise:

bash
cd ~/terraform-labs/terraform-state-command/mv-demo

Start with a single resource labeled old in main.tf:

hcl
resource "terraform_data" "old" {
  input = "rename lab"
}

Initialize the mv demo working directory:

bash
terraform init

Apply so terraform_data.old exists in state:

bash
terraform apply -auto-approve

Rename the block label to new in main.tf — keep the same input value:

hcl
resource "terraform_data" "new" {
  input = "rename lab"
}

Plan before moving state — Terraform still tracks terraform_data.old:

bash
terraform plan

Sample output:

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

  # terraform_data.new will be created
  + resource "terraform_data" "new" { ... }

  # terraform_data.old will be destroyed
  - resource "terraform_data" "old" {
      - id = "24cdf3ce-8b32-ae69-c406-b5a2f32ce9af" -> null
    }

That destroy-and-create pair is unnecessary when only the label changed. Pull a backup first:

bash
terraform state pull > before-mv.tfstate

Rename the state address to match the new block label:

bash
terraform state mv terraform_data.old terraform_data.new

Sample output:

output
Move "terraform_data.old" to "terraform_data.new"
Successfully moved 1 object(s).

Plan again — Terraform should refresh the same underlying object under the new address:

bash
terraform plan

Sample output:

output
terraform_data.new: Refreshing state... [id=24cdf3ce-8b32-ae69-c406-b5a2f32ce9af]

No changes. Your infrastructure matches the configuration.

The unchanged terraform_data id confirms that this lab continues tracking the same resource instance under its new Terraform address. For refactors that should travel with configuration in Git, declarative moved blocks are often preferable — see the dedicated moved and removed blocks lesson. state mv remains useful for one-off repairs and exam-style scenarios.


Remove resources with terraform state rm

terraform state rm drops a resource address from state. It does not call the provider to delete the real object.

text
state rm  → Terraform forgets the object
destroy   → Terraform deletes the object through the provider

Return to the main lab directory:

bash
cd ~/terraform-labs/terraform-state-command/main

Remove the file resource from state only:

bash
terraform state rm local_file.detach

Sample output:

output
Removed local_file.detach
Successfully removed 1 resource instance(s).

Verify the file still exists on disk — state removal does not touch the filesystem:

bash
ls -la detach.txt

Sample output:

output
-rwxr-xr-x 1 user user 28 Aug 12 10:12 detach.txt

The bytes are still there; Terraform simply no longer maps local_file.detach to that path. Because the resource block still exists in configuration but its state binding is gone, Terraform now plans to create local_file.detach again. It does not automatically reconnect to the existing file.

bash
terraform plan

Sample output:

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

  # local_file.detach will be created
  + resource "local_file" "detach" {
      + content  = "still on disk after state rm"
      + filename = "./detach.txt"
    }

Re-apply to restore state tracking for the lab:

bash
terraform apply -auto-approve

In this disposable local_file lab, apply recreates or manages the configured file and restores a state binding. For real infrastructure, do not assume re-applying after state rm is safe — resource names or identifiers may conflict with the object Terraform was told to forget.

Use state rm when infrastructure should survive but Terraform should stop managing it — for example before handoff to another tool. Use terraform destroy when you want Terraform to delete managed objects.


Pull and push Terraform state

terraform state pull and terraform state push read and write the entire state snapshot through the CLI. They are most useful for backups, out-of-band inspection, and controlled recovery — not for routine editing.

terraform state pull

You already used pull for backups. It always prints JSON to stdout:

bash
terraform state pull > state-backup.json

Inspect the file with jq or a text editor when troubleshooting drift or verifying what Terraform last persisted. Do not treat manual JSON edits as a normal workflow.

terraform state push

state push writes a local state file back to the configured backend. Terraform checks lineage and serial metadata to reject snapshots that do not belong to the current state lineage or that would roll back a newer write.

Create the push demo directory:

bash
mkdir -p ~/terraform-labs/terraform-state-command/push-demo

Switch into it for the push exercise:

bash
cd ~/terraform-labs/terraform-state-command/push-demo

Save that resource block in main.tf:

hcl
resource "terraform_data" "push_lab" {
  input = "push demo"
}

Run init in the push demo directory:

bash
terraform init

Apply so the push demo has state to pull:

bash
terraform apply -auto-approve

Pull the current snapshot:

bash
terraform state pull > pulled.tfstate

Pushing the same file back succeeds when lineage matches — Terraform 1.15.8 exits silently on success:

bash
terraform state push pulled.tfstate

Check the exit code when you need confirmation:

bash
echo $?

Sample output:

output
0

Pushing unrelated state fails. The backup from mv-demo/ belongs to a different lineage:

bash
terraform state push ../mv-demo/before-mv.tfstate

Sample output:

output
Failed to write state: cannot import state with lineage "95b1c0d9-6dda-145a-d831-f9a8cd751ff9" over unrelated state with lineage "61a3acd3-f221-87bc-09db-deec0911e585"

That guard prevents accidentally overwriting live state with a snapshot from another workspace. Terraform also rejects pushes when the snapshot serial is older than the destination unless you pass -force, which disables those protections — use it only in controlled recovery with a reviewed backup. Treat state push as an emergency tool after review — not a substitute for apply, state mv, or moved blocks.

IMPORTANT
Do not edit state JSON by hand and push it back as a routine fix. Corrupted lineage, serial, or resource records can break the entire workspace. Prefer supported subcommands and declarative refactor blocks.

Common Terraform state command mistakes

Symptom Likely cause Fix
No state file was found Wrong directory or init not run cd to the module root; run terraform init
No such resource instance Typo in address Run terraform state list and copy the exact address
Shell glob errors on show / rm Unquoted [index] in address Quote the full address: 'module.x.aws_instance.web[0]'
Real infrastructure deleted unexpectedly Used destroy when you meant to forget Use state rm only when objects should survive
Duplicate resources after state rm Configuration still declares the block Remove or comment the resource block, or re-import later
state push lineage error Pushing backup from another workspace Push only snapshots pulled from the same working directory lineage
Destroy/create on a simple rename Renamed block without state mv or moved Run state mv OLD NEW or add a moved block and apply
Manual JSON edit broke apply Hand-edited pulled state Restore from before-change.tfstate; avoid manual edits
text
Inspect:  list / show / pull
Modify:   mv / rm / push   (increasing caution)

Read commands help you understand what Terraform tracks. Write commands change that memory — back up first, verify real infrastructure independently after rm, and prefer declarative refactors over push whenever possible.


References


Summary

The terraform state subcommands let you inspect and deliberately adjust what Terraform remembers about managed infrastructure. terraform state list prints resource addresses; terraform state show prints attributes for one address. Those read paths are safe for everyday debugging when you need to confirm what state contains before planning a change.

Write operations demand more care. terraform state mv retargets an address after a configuration rename without recreating the underlying object — you saw the plan flip from destroy-and-create to no changes once the move matched the new block label. terraform state rm removes the state binding while leaving real infrastructure in place; Terraform then plans a create, not an automatic reconnect, if the resource block remains. terraform state pull backs up JSON snapshots; terraform state push writes them back only when lineage and serial checks pass.

The mistakes to avoid are treating rm like destroy, quoting indexed addresses incorrectly in the shell, and pushing hand-edited JSON as a routine fix. Back up with pull before any modification, verify real objects independently after rm, and reach for declarative moved blocks when refactors should live in version control. Backend storage and locking live in the backends and remote state and state locking lessons on this course track.


Frequently Asked Questions

1. What does terraform state list do?

terraform state list prints the resource addresses currently recorded in state for the working directory. It answers which objects Terraform tracks, not their full attribute details.

2. What is the difference between terraform state rm and terraform destroy?

terraform state rm removes a resource address from state without calling the provider to delete the real object. terraform destroy updates state by deleting managed objects through the provider. State rm forgets; destroy deletes.

3. What does terraform state mv do?

terraform state mv renames a resource address in state without recreating the underlying object. Use it when you renamed a resource block in configuration and need Terraform to keep tracking the same real infrastructure under the new address.

4. Is it safe to edit terraform.tfstate JSON and push it back?

No for routine workflows. Manual JSON edits bypass Terraform safety checks and can corrupt lineage or serial metadata. Use terraform state subcommands or declarative moved blocks instead, and treat state push as an emergency path with strong review.

5. When should I use moved blocks instead of terraform state mv?

Prefer moved blocks when the rename should live in version-controlled configuration history and apply consistently across environments. Use terraform state mv for one-off repairs or lab scenarios where you need an immediate state-only fix.
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)