In today’s blog post, we will discuss terraform moved block, used to rename or move terraform resources. Terraform is the most widely used Infrastructure as Code (IaC) tool in the cloud and DevOps field. But as your infrastructure as code (IaC) grows, refactoring your configurations becomes a nightmare. Renaming a resource, reorganizing modules, or switching from count to for_each can be difficult tasks, often carrying the risk of unintended infrastructure destruction and recreation.
Before Terraform v1.1, these operations typically required manual terraform state mv commands, a process prone to errors and difficult to integrate into automated CI/CD pipelines.
Introduced in Terraform v1.1, moved block provides a declarative, safe, and easy way to handle changes in your resource addresses, ensuring your infrastructure remains intact during major refactoring efforts without any outage.
What is the moved Block?
The moved block is a special top-level configuration block that you add to your Terraform code to declare that a resource or module has changed its address within the configuration. When Terraform sees a moved block, it understands that the resource at the from address in the state file should now be managed by the resource at the to address in the current configuration.
This prevents Terraform from interpreting the change as a destruction of the old resource and a creation of a new one. Instead, it performs an in-place state migration.
Why is the moved Block Important?
Without the moved block, if you:
- Rename a resource (e.g.,
aws_instance.webtoaws_instance.app_server). - Move a resource from a root configuration into a module.
- Move a resource from one module to another.
- Refactor a resource from
counttofor_each(or vice-versa).
Terraform’s default behavior would be to see the old address as “gone” (requiring destruction) and the new address as “new” (requiring creation). The moved block explicitly tells Terraform: “No, this is the same resource, just at a different place in my code.”
Syntax of the moved Block
The moved block has the following syntax:
moved {
from = <old_resource_or_module_address>
to = <new_resource_or_module_address>
}
from: The old resource or module address as it exists in your Terraform state file. This is the address that Terraform currently associates with the existing infrastructure.to: The new resource or module address as you have defined it in your updated Terraform configuration files. This is where Terraform should now look to manage that infrastructure.
Both from and to arguments must be resource addresses, which identify a single resource instance. These can be:
resource_type.resource_name(e.g.,aws_instance.web)module.module_name.resource_type.resource_name(e.g.,module.vpc.aws_vpc.main)resource_type.resource_name[key](forfor_eachresources)resource_type.resource_name[index](forcountresources)
Common Use Cases and Examples
Let us explore the most common scenarios where the moved block is used with examples.
Renaming a Resource
This is the most common use case. Sometimes you may want to rename the logical resource name for better readability. For example, say you have a resource initially created with logical name “web_server” but now you want to rename the resource logical name to “app_server”.
Before (e.g., main.tf):
resource "aws_instance" "web_server" {
ami = "ami-0abcdef1234567890"
instance_type = "t2.micro"
}
After (e.g., main.tf): You decide app_server is a better name.
resource "aws_instance" "app_server" { # Renamed from web_server
ami = "ami-0abcdef1234567890"
instance_type = "t2.micro"
}
# Add the moved block to inform Terraform
moved {
from = aws_instance.web_server
to = aws_instance.app_server
}
Run terraform plan. Instead of seeing aws_instance.web_server to be destroyed and aws_instance.app_server to be created, you will see a message indicating the state will be moved.
Terraform will perform the following actions:
Terraform will move the following objects:
- aws_instance.web_server to aws_instance.app_server
Moving a Resource into a Module
This is another very common use case of moved block. Say initially you created all your resources from a single main.tf file. But later as your infrastructure grows, you decided to modularize your terraform configuration blocks. The move block can be used to move a resource from one logical address to another.
Before (e.g., main.tf):
resource "aws_s3_bucket" "my_app_bucket" {
bucket = "my-unique-app-bucket"
acl = "private"
}
After: You create a new module modules/s3_bucket/ with main.tf:
# modules/s3_bucket/main.tf
resource "aws_s3_bucket" "this" { # Common pattern to name resource "this" inside a module
bucket = var.bucket_name
acl = "private"
}
And in your root/main.tf:
# root/main.tf
module "app_bucket" {
source = "./modules/s3_bucket"
bucket_name = "my-unique-app-bucket"
}
# Add the moved block in the root configuration
moved {
from = aws_s3_bucket.my_app_bucket
to = module.app_bucket.aws_s3_bucket.this
}
Refactoring from count to for_each
When you want to switch from using a count meta-argument to for_each for more explicit instance management.
Before (e.g., main.tf):
resource "aws_instance" "app_server" {
count = 2
ami = "ami-0abcdef1234567890"
instance_type = "t2.micro"
}
After (e.g., main.tf): You introduce a map to drive for_each.
locals {
instance_configs = {
"server-a" = { ami_id = "ami-0abcdef1234567890", instance_type = "t2.micro" }
"server-b" = { ami_id = "ami-0abcdef1234567890", instance_type = "t2.micro" }
}
}
resource "aws_instance" "app_server" {
for_each = local.instance_configs
ami = each.value.ami_id
instance_type = each.value.instance_type
}
# Add moved blocks for each instance
moved {
from = aws_instance.app_server[0]
to = aws_instance.app_server["server-a"]
}
moved {
from = aws_instance.app_server[1]
to = aws_instance.app_server["server-b"]
}
Note: For large numbers of instances, manually writing moved blocks can be tedious. You can use a for loop to generate these moved blocks for common transformations (e.g., if your for_each keys are just strings like “0”, “1”, “2”).
# Example: Generating moved blocks for a count to for_each where keys are numeric strings
# (Only if your target for_each keys map directly to old count indices)
# This example is illustrative and requires careful thought for complex transformations.
# The 'moved' block is a top-level block and cannot contain arbitrary expressions
# so this specific use case is more about generating the HCL using a script if needed.
# For direct HCL, you would still write them out.
# However, if your 'for_each' keys are strings that match the old indices:
moved { from = aws_instance.app_server[0] to = aws_instance.app_server["0"] }
moved { from = aws_instance.app_server[1] to = aws_instance.app_server["1"] }
# ... and so on.
# For more complex transformations where your keys are derived, you'll need
# a strategy that aligns the 'from' and 'to' based on your logic.
# Often, this still means explicit 'moved' blocks or a well-defined mapping strategy.
Best Practices for Using moved Blocks
- Run
terraform planFirst (Always!): You should always run the terraform plan first. The plan output will clearly show the “move” operation instead of “destroy” and “create.” Always confirm the plan before applying. - Add
movedBlocks in the Same Commit/Change: Themovedblocks should be part of the same HCL change that renames or moves the resources. This make sures Terraform always sees both the old and new addresses in the same configuration. - Place
movedBlocks Logically: You can placemovedblocks anywhere in your root configuration. A common practice is to put them in a dedicated_moved.tforrefactor.tffile to keep them separate from active resource definitions. - Remove
movedBlocks After Successful Apply: Onceterraform applysuccessfully executes the move operation, the information is updated in the state file. Themovedblocks are no longer needed and should be removed from your HCL files. This keeps your configuration clean and prevents potential confusion or errors in the future.- Why remove them? If you keep them and later rename the resource again, Terraform might get confused, or it adds unnecessary clutter to your code.
- Use Fully Qualified Addresses: Always use the full resource address, including module paths and instance keys/indices, in both
fromandtoarguments. - Limitations:
- Within a Single State File:
movedblocks only work for resources managed within the same Terraform state file. You cannot use them to move resources between different state files. - Provider-Specific: The
movedblock works at the Terraform core level. It assumes the underlying provider resource can be identified as the same logical object. terraform init -migrate-state(for root module moves): For moving resources from the root module into a child module (especially if the child module is locally sourced),terraform init -migrate-statecan also play a role in older versions or specific setups. However,movedblocks are generally more easy to use and declarative.
- Within a Single State File:
moved Block vs. terraform state mv
Before the moved block, the terraform state mv command was the go-to for state refactoring.
| Feature | moved Block | terraform state mv Command |
|---|---|---|
| Type | Declarative HCL (part of your configuration) | Imperative CLI command (manual operation) |
| Auditable | Yes, part of Git history | No, outside Git history; relies on operational logs |
| Automated | Yes, runs automatically during plan/apply | No, requires manual execution (prone to human error) |
| Collaboration | Easier for teams; baked into terraform plan | Requires careful coordination among team members |
| Safety | Terraform validates move in plan before apply | Direct state manipulation; higher risk if misused |
| Temporary | Removed after successful apply | No lasting trace in HCL; requires careful documentation |
Conclusion: The moved block is almost always the preferred method for state migration over terraform state mv due to its declarative nature, safety, and integration with Terraform’s planning phase. Use terraform state mv only for very specific, ad-hoc, and well-understood scenarios, or if you’re on a Terraform version prior to 1.1.
Step-by-Step Refactoring Process with moved Block
- Backup Your State (Optional but Recommended): While
movedis safe, it’s always important to backup yourterraform.tfstatefile (or your remote backend state) before major refactoring’s. - Modify Your HCL: Change the resource name, move it into a module, or refactor its
count/for_eachlogic. - Add
movedBlock(s): In your root configuration, add the necessarymoved { from = ... to = ... }blocks to map the old state addresses to the new configuration addresses. - Run
terraform plan: Carefully review the plan output. Make sure it shows “Terraform will move the following objects:” and does not show any unexpected destructions or creations. - Run
terraform apply: Execute the plan. Terraform will perform the state migration. - Verify: After a successful apply, run
terraform planagain. It should show “No changes. Your infrastructure matches the configuration.” This confirms the state has been updated. - Remove
movedBlock(s): Delete themovedblocks from your HCL files. They are no longer needed. - Commit Changes: Commit your HCL changes (first with
movedblocks, then a separate commit removing them after successful apply).
Conclusion
The moved block is an important tool for any Terraform developer. It transforms complex and risky refactoring operations into straightforward, declarative steps. By understanding its mechanics and following best practices, you can confidently reorganize your Terraform configurations, improve readability, and maintain a healthy, manageable state without fear of unintended infrastructure changes.
Author

Experienced Cloud & DevOps Engineer with hands-on experience in AWS, GCP, Terraform, Ansible, ELK, Docker, Git, GitLab, Python, PowerShell, Shell, and theoretical knowledge on Azure, Kubernetes & Jenkins.
In my free time, I write blogs on ckdbtech.com