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.web
toaws_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
count
tofor_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_each
resources)resource_type.resource_name[index]
(forcount
resources)
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 plan
First (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
moved
Blocks in the Same Commit/Change: Themoved
blocks 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
moved
Blocks Logically: You can placemoved
blocks anywhere in your root configuration. A common practice is to put them in a dedicated_moved.tf
orrefactor.tf
file to keep them separate from active resource definitions. - Remove
moved
Blocks After Successful Apply: Onceterraform apply
successfully executes the move operation, the information is updated in the state file. Themoved
blocks 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
from
andto
arguments. - Limitations:
- Within a Single State File:
moved
blocks 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
moved
block 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-state
can also play a role in older versions or specific setups. However,moved
blocks 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
moved
is safe, it’s always important to backup yourterraform.tfstate
file (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_each
logic. - Add
moved
Block(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 plan
again. It should show “No changes. Your infrastructure matches the configuration.” This confirms the state has been updated. - Remove
moved
Block(s): Delete themoved
blocks from your HCL files. They are no longer needed. - Commit Changes: Commit your HCL changes (first with
moved
blocks, 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