
Moved blocks are typically used after importing existing infrastructure into Terraform to reorganize resources without destroying them.
Terraform's moved block feature, introduced in version 1.1, allows developers to safely rename or relocate resources in their Terraform configurations without destroying and recreating them. This declarative approach tells Terraform that a resource has changed address, instructing it to update the state file accordingly while preserving the underlying infrastructure. The feature addresses one of infrastructure-as-code's persistent challenges: evolving your configuration structure without disrupting live environments. Moved blocks make refactoring safer by documenting resource address changes directly in your configuration code, eliminating the need for manual state manipulation commands in most scenarios.
The moved block serves a critical purpose in Terraform's infrastructure-as-code ecosystem: it enables safe, non-destructive refactoring of your resource structure. Before this feature, renaming a resource from aws_instance.old_name to aws_instance.new_name would cause Terraform to interpret this as a command to destroy the old resource and create a new one – potentially causing downtime and data loss.
Moved blocks solve this problem by explicitly declaring the relationship between old and new resource addresses:
moved {
from = aws_instance.old_name
to = aws_instance.new_name
}This simple declaration tells Terraform to update its state file to reflect the new address without modifying the underlying infrastructure. The feature supports several common refactoring scenarios, including:
count to for_each for more explicit resource mappingUsing moved blocks makes these changes safe and declarative, documenting the evolution of your infrastructure in the code itself rather than through external processes or manual state commands.
The moved block has a straightforward syntax that requires just two arguments:
moved {
from = <old_resource_address>
to = <new_resource_address>
}Both arguments use Terraform's resource addressing syntax and must refer to the same kind of object. The addresses can refer to:
aws_instance.exampleaws_instance.example[0] or aws_instance.example["primary"]module.networkingmodule.networking.aws_vpc.mainThe moved block works seamlessly within the standard Terraform workflow. When you run terraform plan after adding moved blocks, Terraform:
# aws_instance.old_name has moved to aws_instance.new_nameDuring terraform apply, these state updates become permanent. No actual infrastructure changes occur – only the state mapping changes. This integration with the normal workflow makes moved blocks automation-friendly and compatible with CI/CD pipelines, unlike manual state manipulation commands.
When you need to rename a resource for better clarity or consistency:
# Before
resource "aws_security_group" "sg" {
name = "api-security-group"
# configuration...
}
# After
resource "aws_security_group" "api_security_group" {
name = "api-security-group"
# configuration...
}
moved {
from = aws_security_group.sg
to = aws_security_group.api_security_group
}When restructuring your configuration to use modules:
# Before (in root module)
resource "aws_s3_bucket" "logs" {
bucket = "application-logs"
# configuration...
}
# After
# In modules/storage/main.tf
resource "aws_s3_bucket" "logs" {
bucket = var.bucket_name
# configuration...
}
# In root module
module "storage" {
source = "./modules/storage"
bucket_name = "application-logs"
}
moved {
from = aws_s3_bucket.logs
to = module.storage.aws_s3_bucket.logs
}When migrating from index-based to key-based resource creation:
# Before
resource "aws_instance" "server" {
count = 2
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
tags = {
Name = "server-${count.index}"
}
}
# After
locals {
servers = {
"web" = { name = "server-0" }
"api" = { name = "server-1" }
}
}
resource "aws_instance" "server" {
for_each = local.servers
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
tags = {
Name = each.value.name
}
}
moved {
from = aws_instance.server[0]
to = aws_instance.server["web"]
}
moved {
from = aws_instance.server[1]
to = aws_instance.server["api"]
}When giving a module a more descriptive name:
# Before
module "app" {
source = "./modules/application"
# arguments...
}
# After
module "frontend" {
source = "./modules/application"
# arguments...
}
moved {
from = module.app
to = module.frontend
}These examples demonstrate how moved blocks can support various refactoring patterns while preserving state continuity and avoiding resource recreation.
Add explanatory comments to moved blocks to document when and why the refactoring occurred:
# Resource renamed from aws_instance.app to aws_instance.web_server
# as part of the application module refactoring on 2024-06-15
moved {
from = aws_instance.app
to = aws_instance.web_server
}Unlike many other Terraform constructs, HashiCorp recommends retaining moved blocks in your configuration indefinitely, particularly in shared modules. This provides a clear migration path for users who might be using different versions of your module.
When resources need to move multiple times, use chained moved blocks to document the full history:
moved {
from = aws_instance.original
to = aws_instance.intermediate
}
moved {
from = aws_instance.intermediate
to = aws_instance.final
}This pattern ensures that users can successfully update from any previous version to the current one.
Always run terraform plan after adding moved blocks to verify that Terraform correctly interprets your refactoring intentions. Look for the move messages in the plan output before proceeding with apply.
For large configurations, refactor resources in smaller batches rather than attempting to move everything at once. This approach makes it easier to identify and troubleshoot any issues that arise.
When breaking a module into smaller pieces, create a shim module that:
This approach allows for a gradual transition without breaking changes for module consumers.
Terraform doesn't support dynamic generation of moved blocks using for_each or similar constructs. For large-scale moves, you might need to generate the moved blocks using external scripts:
#!/bin/bash
# Generate moved blocks for converting from count to for_each
for i in {0..20}; do
echo "moved {"
echo " from = aws_instance.server[$i]"
echo " to = aws_instance.server[\"server_$i\"]"
echo "}"
done > moved.tfSome limitations exist:
When refactoring modules, remember that:
terraform state mv insteadWhen converting from a numeric index to a string key (count to for_each), be very explicit about the mapping:
moved {
from = aws_security_group.example[0]
to = aws_security_group.example["first"] # Not aws_security_group.example[first]
}Missing quotation marks around string keys is a common mistake that can lead to confusing errors.
When you run terraform plan with moved blocks, Terraform processes them early in the planning phase:
# aws_instance.old_name has moved to aws_instance.new_nameThis approach makes the moved block operation transparent and predictable. The actual state file is only updated when you run terraform apply, at which point the resource addresses are permanently changed in the state file.
If you add a moved block but the resource at the "from" address doesn't exist in the state, Terraform simply ignores the moved block without any errors. This makes moved blocks safe to add preemptively.
During apply, Terraform handles moved blocks atomically with other state changes, ensuring that the state remains consistent even if the operation is interrupted.
While both moved blocks and the terraform state mv command accomplish similar results, they have important differences:
| Feature | moved Block |
terraform state mv Command |
|---|---|---|
| Implementation | Declarative, in code | Imperative, CLI command |
| Execution | During plan/apply workflow | Immediate, outside workflow |
| Documentation | Self-documents in code | No code documentation |
| CI/CD Compatibility | Works with automation | Requires manual steps or scripting |
| Workspace Handling | Applies to all workspaces | Must be run for each workspace |
| Error Handling | Validates during planning | No validation before execution |
| Portability | Moves travel with code | Requires separate documentation |
The moved block is generally preferred for:
The terraform state mv command remains useful for:
Since its introduction in Terraform v1.1, the moved block feature has seen several improvements:
As of 2025, Terraform continues to refine the moved block functionality, with ongoing discussions about supporting:
Terraform's moved block feature provides a powerful, declarative way to safely refactor your infrastructure code without risking downtime or resource recreation. By documenting resource address changes directly in your configuration, moved blocks improve code maintainability, enhance team collaboration, and reduce the risk associated with evolving your infrastructure over time. While the feature does have some limitations, particularly around dynamic generation and certain edge cases, it represents a significant improvement over manual state manipulation for most refactoring scenarios.
