When to touch state manually
Ideally, never. Terraform favors a declarative approach: you change HCL, run plan, see what will happen, then apply. Direct state operations are for cases where the declarative path does not work:
- Renaming a resource without destroy and create (historically via
state mv; with TF 1.1+ use amovedblock, see tf-moved-block). - Removing a resource from Terraform management without deleting it in
the cloud (with TF 1.7+ use a
removedblock, see tf-removed-block). - Emergency repair of a broken state:
state pull, edit the JSON by hand,state push. Rare and risky; usually handled through provider support. - Merging two states into one using
state mvwith cross-state notation.
Declarative blocks (moved, removed, import) are the preferred path.
They leave a trail in HCL and git and are applied automatically for every
team member. CLI operations are invisible: only the person who ran them
knows what happened.
terraform state list
The entry point: what is in state?
terraform state list
# aws_s3_bucket.demo
# module.logs.aws_s3_bucket.this
# random_id.suffix
Filter by address:
terraform state list 'module.logs.*'
# module.logs.aws_s3_bucket.this
Run this before any state mv/rm to confirm you are targeting the right resource.
terraform state show
Attributes of a specific resource:
terraform state show aws_s3_bucket.demo
Prints an HCL-like form with all attributes, including sensitive ones (unlike
terraform plan/apply, which masks them). This is why state must be protected
as carefully as secrets.
terraform state mv
The most common manual operation. It changes a resource address in state without destroy.
Case 1: rename a resource
Before:
resource "aws_s3_bucket" "logs" { ... }After:
resource "aws_s3_bucket" "log_storage" { ... }Without state mv, Terraform sees "logs is gone from HCL, log_storage
appeared" and does destroy+create. The bucket is destroyed along with its data.
With state mv:
terraform state mv aws_s3_bucket.logs aws_s3_bucket.log_storage
The resource is now called log_storage in state. Update HCL at the same time.
terraform plan → No changes.
Case 2: move into a module
A resource lives in the root module and you want to move it into
./modules/buckets.
# first add to HCL:
# module "buckets" { source = "./modules/buckets" }# inside the module: resource "aws_s3_bucket" "logs" { ... }terraform state mv aws_s3_bucket.logs module.buckets.aws_s3_bucket.logs
plan → No changes. This is a typical refactoring scenario; see
tf-refactor-patterns.
Case 3: between different states
You are splitting one root into two. Some resources migrate:
# in the source root:
terraform state mv -state-out=../other/terraform.tfstate \
aws_s3_bucket.logs aws_s3_bucket.logs
This is a rare operation, typically done when splitting a monolith into services.
terraform state rm
Removes a resource from state. The cloud resource remains.
terraform state rm aws_s3_bucket.demo
Terraform now "forgets" the bucket. The next apply (if the resource is
still in HCL) will see "not in state, present in HCL", try to create it
again, and fail with "bucket already exists". To recapture it, use import;
see tf-state-import.
Use cases:
- A resource ended up in the wrong state and should live in another. After
state rm, runimportin the correct state. - Switching from
count = 1tofor_each. The addresses differ, so without help Terraform does destroy+create.state rmthe old address andimportthe new one to avoid recreation. With TF 1.1+ amovedblock is better.
-dry-run
terraform state rm -dry-run aws_s3_bucket.demo
Shows what would happen, without making changes. Always run with -dry-run first, then repeat without it.
terraform state pull / push
An emergency mechanism. Download state as a file:
terraform state pull > terraform.tfstate.dump
Works with any backend: local, S3, remote. Saves to the current directory.
Upload it back:
terraform state push terraform.tfstate.dump
Terraform checks lineage and serial. If the serial is lower than the
current one, it asks for confirmation (this is a potential rollback). If
lineage differs, it refuses, protecting against replacing a state from
another project.
Use this when:
- You need to edit the JSON by hand (for example, a provider upgrade broke the
state format). This is a last resort; usually
terraform refreshor talking to the provider author is better. - Migrating state between backends by hand instead of
init -migrate-state. - Creating a backup before a risky operation:
state pull > backup-$(date).json.
Backup before any operation
Before any state mv/rm/push, take a dump first:
terraform state pull > backup-pre-refactor-$(date +%s).json
An S3 backend with versioning does this automatically, but an extra copy does not hurt. With a local backend, it is mandatory and manual.
Pitfalls
-
state mvdoes not update HCL. If you rename in state, rename in HCL too. Otherwise the nextplanshows destroy+create again. -
state rmhas no undo. There is genuinely no rollback. If the cloud resource matters, runstate pull > backup.jsonfirst, then rm. If something goes wrong,state push backup.json. -
With a remote backend, locking only works when you go through the Terraform CLI.
state pullacquires a lock;state pushdoes too. But if you copy the state file directly withaws s3 cp tfstate ./, there is no lock and you can conflict with someone else's apply. -
state mvforcount/for_eachresources requires the exact address. Useaws_iam_user.user[0]oraws_iam_user.user["alice"], notaws_iam_user.user. A wrong index moves the wrong resource. -
pushwill not work between states with differentlineage. This is intentional protection. If you genuinely need it (for example, bootstrapping a new state by importing from an old one), add-forceto push. But answer to yourself why first. -
movedandremovedblocks are better when applicable. They are declarative, visible in diffs, and run automatically for everyone.state mv/rmis a one-time manual act; others find out only when conflicts appear.