Skip to main content
In this lesson we cover how to refactor Terraform state in a controlled, auditable way — that is, changing how Terraform tracks your infrastructure without modifying the real-world resources themselves. Why this matters
  • Avoid accidental destroy/recreate cycles when you rename or move resources in code.
  • Keep state operations visible in version control and code review.
  • Validate refactors using terraform plan before applying them.
Common scenarios that require state refactoring:
  • You renamed a resource (for example, aws_instance.webaws_instance.webserver).
  • You reorganized code (moved resources between files or into modules).
  • You want Terraform to adopt existing infrastructure created outside Terraform.
  • You want Terraform to stop managing a resource while leaving it running in the cloud (e.g., handing it off to another team).
Historically, teams relied on CLI tools such as terraform state mv, terraform state rm, and terraform import. Those work, but they are procedural and happen outside of configuration — making them harder to review, test, and reproduce. HashiCorp introduced a configuration-driven approach: declare moved, removed, and import blocks in Terraform configuration. Because these refactoring declarations are code, they are version-controlled, reviewable in PRs, and validated by terraform plan before being applied.
The image compares "The Old Way" and "The New Way" of using Terraform. The old method involves using separate CLI commands that are risky and not version-controlled, while the new method uses configuration blocks that are version-controlled, reviewable in pull requests, and can be tested before application.
Key refactoring blocks
  • moved — Map an old Terraform address to a new one so state is updated without destroying the underlying resource.
  • removed — Remove a resource from Terraform state while leaving the resource running in-cloud.
  • import — Adopt an existing resource into Terraform state by providing the target address and the provider-specific ID.
Quick reference table
Refactor blockWhen to useMinimal example
movedYou only changed the resource address (renamed or moved in code)hcl\nmoved { from = aws_instance.web to = aws_instance.webserver }\n
removedTell Terraform to stop managing a resource without destroying ithcl\nremoved { from = aws_instance.production_db }\n
importAdopt an existing resource into Terraform controlhcl\nimport { to = aws_s3_bucket.terraform_state id = "terraform_state_bucket" }\n
Example: all three blocks together
moved {
  from = azurerm_subnet.subnet1
  to   = azurerm_subnet.prod_private
}

removed {
  from = aws_instance.production_db
}

import {
  to = aws_s3_bucket.terraform_state
  id = "terraform_state_bucket"
}
Best practices and notes moved
  • Use moved only when the real resource hasn’t changed — only its address in your configuration has.
  • from and to are Terraform addresses (for example, aws_instance.web, or module.db.aws_db_instance.main).
  • Add the moved block to your configuration and run terraform plan to validate the mapping before terraform apply.
removed
  • A removed block instructs Terraform to drop the resource from state. Terraform will not try to destroy the actual resource as part of this refactor.
  • If you are concerned that other code changes might try to destroy the resource accidentally, add a lifecycle block with prevent_destroy = true to the resource configuration before you remove it from state. Example:
resource "aws_instance" "production_db" {
  # ... other resource configuration ...

  lifecycle {
    prevent_destroy = true
  }
}
  • After you validate with terraform plan and are confident the resource is safe, add the removed block and run terraform apply to drop it from state.
If you use prevent_destroy = true, remember to remove or update that lifecycle rule when you want normal destroy behavior again. Leaving prevent_destroy in place can block legitimate destroy operations in future runs.
import
  • Use import when a resource already exists (created manually, by another tool, or in a different workspace) and you want Terraform to manage it.
  • to is the Terraform address you will declare in your configuration. id is the provider-specific identifier (for example, an S3 bucket name or an EC2 instance ID).
  • After adding the import block and validating with terraform plan, run terraform apply to create the state entry. You still must declare the corresponding resource in your configuration with matching arguments where applicable.
Safe workflow for configuration-driven refactoring
  1. Add the refactoring block(s) to your Terraform files (under the same root that has your state).
  2. Run terraform plan to preview how state will change and to validate that the mapping or import works as expected.
  3. If the plan looks correct, run terraform apply to update the state. The actual infrastructure will remain unchanged unless you also make other configuration changes.
Command examples
terraform plan
terraform apply
The image outlines "The Refactoring Workflow," consisting of three steps: Write (adding blocks to Terraform config), Plan (validating changes with terraform plan), and Apply (updating state with terraform apply).
Wrapping up Using configuration-driven refactoring blocks (moved, removed, import) is the recommended, modern workflow. It makes state changes auditable, reviewable, and safer by integrating refactors into the standard plan/apply lifecycle. These techniques are practical for day-to-day Terraform management and are relevant study topics for the HashiCorp Certified: Terraform Associate 004 exam. Further reading and references

Watch Video