| 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 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.
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/.
state rm or state push on production state without a verified backup.
Lab setup
Create the main working directory:
mkdir -p ~/terraform-labs/terraform-state-command/mainMove into it — later commands in this section assume you are here:
cd ~/terraform-labs/terraform-state-command/mainAdd three disposable resources in main.tf:
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:
terraform {
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}Initialize the directory:
terraform initApply so state contains managed objects to inspect:
terraform apply -auto-approveSample 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:
terraform state listSample output:
local_file.detach
terraform_data.example
terraform_data.spokeEach 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:
terraform state list | grep terraform_dataSample output:
terraform_data.example
terraform_data.spokeFor 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:
terraform state show 'aws_instance.web[0]'terraform state show
list prints addresses; show prints attributes for one address. Inspect the example resource:
terraform state show terraform_data.exampleSample output:
# terraform_data.example:
resource "terraform_data" "example" {
id = "6d62185d-3ee6-61ba-b638-7bb338e0bf4a"
input = "inspect me"
output = "inspect me"
}list → addresses in state
show → attributes for one state objectThe 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:
terraform state pull > before-change.tfstatestate 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:
jq '.resources | length' before-change.tfstateSample output:
3Keep 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:
mkdir -p ~/terraform-labs/terraform-state-command/mv-demoWork inside that directory for the rename exercise:
cd ~/terraform-labs/terraform-state-command/mv-demoStart with a single resource labeled old in main.tf:
resource "terraform_data" "old" {
input = "rename lab"
}Initialize the mv demo working directory:
terraform initApply so terraform_data.old exists in state:
terraform apply -auto-approveRename the block label to new in main.tf — keep the same input value:
resource "terraform_data" "new" {
input = "rename lab"
}Plan before moving state — Terraform still tracks terraform_data.old:
terraform planSample 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:
terraform state pull > before-mv.tfstateRename the state address to match the new block label:
terraform state mv terraform_data.old terraform_data.newSample 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:
terraform planSample 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.
state rm → Terraform forgets the object
destroy → Terraform deletes the object through the providerReturn to the main lab directory:
cd ~/terraform-labs/terraform-state-command/mainRemove the file resource from state only:
terraform state rm local_file.detachSample 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:
ls -la detach.txtSample output:
-rwxr-xr-x 1 user user 28 Aug 12 10:12 detach.txtThe 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.
terraform planSample 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:
terraform apply -auto-approveIn 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:
terraform state pull > state-backup.jsonInspect 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:
mkdir -p ~/terraform-labs/terraform-state-command/push-demoSwitch into it for the push exercise:
cd ~/terraform-labs/terraform-state-command/push-demoSave that resource block in main.tf:
resource "terraform_data" "push_lab" {
input = "push demo"
}Run init in the push demo directory:
terraform initApply so the push demo has state to pull:
terraform apply -auto-approvePull the current snapshot:
terraform state pull > pulled.tfstatePushing the same file back succeeds when lineage matches — Terraform 1.15.8 exits silently on success:
terraform state push pulled.tfstateCheck the exit code when you need confirmation:
echo $?Sample output:
0Pushing unrelated state fails. The backup from mv-demo/ belongs to a different lineage:
terraform state push ../mv-demo/before-mv.tfstateSample 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.
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 |
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
- terraform state command
- State: CLI-driven refactoring
- moved block
- terraform state pull
- terraform state push
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.

