Skip to main content
How to Move Terraform State Safely

How to Move Terraform State Safely

KU
• 8 min read•Updated •39 views

A practical guide to renaming resources, migrating backends, and splitting or merging Terraform state without destroying and recreating infrastructure.

Key takeaways

  • Move Terraform state without destroying anything by backing it up with terraform state pull, then using a moved block for renames and module moves, terraform init -migrate-state for backend changes, and state mv or removed plus import blocks to split or merge states.
  • A zero-diff terraform plan afterwards proves the move worked.

Move Terraform state safely by backing it up with terraform state pull, then using a moved block for renames and module moves, terraform init -migrate-state for backend changes, and terraform state mv (or removed plus import blocks) to split or merge states. After every move, terraform plan must report no changes.

Verified against

Terraform 1.17.1 (current release), moved blocks from 1.1 and removed blocks from 1.7, against the developer.hashicorp.com refactoring, state mv, init and S3 backend docs. Checked October 2026.

State is where Terraform keeps its map between your configuration and the real resources it manages. Rename a resource in code without telling Terraform, and it reads the change as "destroy the old thing, create a new one." For a security group that might be harmless. For an RDS instance or a stateful volume, it is a very bad afternoon. Every common state-moving task has a path that avoids a destroy and recreate; these are the five you will actually hit.

Rule zero: back up first

Before touching state, take a copy. terraform state pull reads the current state from whatever backend is configured, local or remote, and writes it as JSON to stdout:

Bash
$ terraform state pull > backup.tfstate

If a migration goes sideways, you push the backup back:

Bash
$ terraform state push backup.tfstate

On S3, bucket versioning gives you a second safety net, and the backend docs call it "highly recommended". With versioning on, and a restore is just rolling back to the prior object version. Do not skip this step because "it's a small change." Backups cost seconds; a corrupted state costs a rebuild.

1. Renaming or moving a resource address

You refactored aws_instance.web into aws_instance.app. The infrastructure is identical, only the address changed. Two ways to tell Terraform.

The imperative way is terraform state mv:

Bash
$ terraform state mv -dry-run aws_instance.web aws_instance.app
$ terraform state mv aws_instance.web aws_instance.app

The -dry-run pass prints what would move without changing anything. It works, but it runs once, off the record, on whoever's laptop happens to execute it. Teammates never see it in review.

The better way is a moved block. It lives in your config, gets code-reviewed like everything else, and is safe to re-run:

HCL
# refactor.tf
moved {
  from = aws_instance.web
  to   = aws_instance.app
}

Rename the resource block itself, add the moved block, and run terraform plan. Terraform reads the block, updates the address, and shows no changes to the resource. moved blocks need Terraform 1.1 or later. In a root module you can delete the block once every workspace has applied it, but in a shared module keep it: HashiCorp's docs "strongly recommend" retaining historical moved blocks, because callers still on the old address would otherwise plan a delete. Prefer moved blocks over state mv whenever the source and target live in the same configuration. They are declarative, reviewable, and idempotent, which is exactly what you want when the alternative is silently deleting production.

2. Migrating the backend

Moving from a local state file to S3, or between two remote backends, is a backend migration. Update the backend block, then let Terraform copy the state:

Bash
$ terraform init -migrate-state

Terraform detects the backend changed, asks whether to copy existing state to the new location, and moves it. A local-to-S3 config looks like this:

HCL
# backend.tf
terraform {
  backend "s3" {
    bucket       = "my-tf-state"
    key          = "prod/network.tfstate"
    region       = "eu-west-1"
    use_lockfile = true
  }
}

use_lockfile turns on S3-native locking; it defaults to false, and the docs mark DynamoDB-based locking as deprecated.

After migration, confirm the state landed by running terraform plan and checking for zero diff. Then remove the old local terraform.tfstate so nobody edits the stale copy by accident. If you ever need a clean slate on the backend config without copying, terraform init -reconfigure "disregards any existing configuration, preventing migration of any existing state", so use it deliberately. In CI, -force-copy answers yes to the migration prompts.

3. Splitting one big state into smaller ones

A monolithic state that holds network, data, and app resources is slow to plan and risky to touch. Splitting it into per-domain states reduces blast radius.

One approach uses state mv with a target state file. The -state and -state-out flags are legacy options that only operate on local files, so with a remote backend, pull both states to local files first, move between them, then push each one back:

Bash
$ terraform state pull > source.tfstate
$ terraform state mv -state=source.tfstate -state-out=network.tfstate \
    aws_vpc.main aws_vpc.main
$ terraform state mv -state=source.tfstate -state-out=network.tfstate \
    aws_subnet.public aws_subnet.public

Then, in the ../network directory, add matching resource blocks, terraform state push the new file, and run terraform plan. Zero diff means the resources now live in the new state and config, untouched. Back in the original directory, the moved resources are gone from state and should also be removed from that config.

The declarative alternative, available since Terraform 1.7, is a removed block with lifecycle { destroy = false } in the source and an import block in the target. Both go through plan and review, which is why we prefer it for remote backends. Typos still show up as a create, so read the plan. Importing existing resources into Terraform covers the import half.

4. Merging states

Merging is splitting in reverse. Pull resources from one state into another with state mv, this time pointing -state-out at the destination state file, or run state mv from within the destination while pointing at the source. Copy the corresponding resource blocks into the destination config, run terraform plan, and confirm nothing changes. The order matters: move state first, add config, verify, then delete the emptied config and its now-orphaned state.

5. Moving a resource into or out of a module

Module boundaries change the address, so Terraform needs to know. A moved block handles this too:

HCL
# refactor.tf
moved {
  from = aws_security_group.db
  to   = module.database.aws_security_group.db
}

The same block works in reverse to pull a resource out of a module. For count or for_each members, include the index or key in both addresses. This is where moved blocks really earn their keep, since module refactors touch many addresses at once and you want every one of them in the diff a reviewer sees.

The gotchas that bite

State lock: Migrations acquire a lock. If a previous run crashed, the lock can linger and block you. Confirm no apply is running, then clear it with terraform force-unlock <LOCK_ID>. Never force-unlock while a real operation is in flight; fixing a stuck Terraform state lock has the details.

Provider and workspace mismatches: Moving state between configurations only works if both sides use compatible provider versions and the same workspace. A resource moved into a config with a newer provider schema can surface unexpected diffs. Line up provider versions before you move anything, and check you are in the right workspace with terraform workspace show.

Verify with a zero diff: The only proof a state move worked is terraform plan reporting no changes. Run it after every move. A clean plan means the address changed and the resource did not. Any proposed create or destroy means stop and investigate before you apply.

For the broader set of state and plan failures you might trip over along the way, see our Terraform error troubleshooting guide.

Questions people ask

How do I rename a resource in Terraform without destroying it?

Rename the resource block and add a moved block with the old address in from and the new one in to, then run terraform plan. It should show the move and no create or destroy. moved blocks need Terraform 1.1 or later and are reviewable in a pull request. The imperative equivalent is terraform state mv old_address new_address.

How do I migrate Terraform state from local to S3?

Back up with terraform state pull > backup.tfstate, add an s3 backend block with bucket, key, region and use_lockfile = true, then run terraform init -migrate-state and confirm the copy prompt. Run terraform plan and expect no changes. Then delete the old local terraform.tfstate so nobody edits the stale copy.

Does terraform state mv work with remote state?

Yes for moves inside one state: terraform state mv operates on the configured backend. The -state and -state-out flags for moving between state files are legacy options that only act on local files, so pull remote states to local copies, move between them, and push them back. Since Terraform 1.7, removed plus import blocks avoid hand-editing files.

The call we'd make

Default to moved blocks for anything inside a single configuration: renames, module moves, refactors. They ship through review, they re-run cleanly, and they leave a paper trail. For cross-state work like splitting and merging, use removed plus import blocks, or terraform state mv against pulled local copies. Back up before every move, verify with a zero-diff plan after, and treat a lingering lock as a signal to slow down rather than force through. State migrations feel scary, but done in this order they are just another reviewed change.

Sources

Explore topics:TerraformDevOps
React
Share this post
Cite this article
Plain text

Kiril Urbonas (2026). How to Move Terraform State Safely. DevOpsNess. Updated 2026-10-31. https://www.devopsness.com/blog/terraform-state-migration

Markdown

[How to Move Terraform State Safely](https://www.devopsness.com/blog/terraform-state-migration) (Kiril Urbonas, DevOpsNess, updated 2026-10-31)

KU

Kiril Urbonas

AI Engineer

585 articles
View all articles by Kiril Urbonas

You might have missed

Evergreen posts worth revisiting.