
Moved blocks are typically used after importing existing infrastructure into Terraform to reorganize resources without destroying them.
The moved block, added in Terraform 1.1, lets you rename or move resources without destroying and recreating them. You put the old and new address in your configuration, and on the next plan Terraform updates its state to match instead of reading the rename as a delete and a create. The infrastructure itself stays put. Before this existed, the same refactor meant running terraform state mv by hand, which was easy to get wrong and left nothing behind in the code.
The moved block lets you refactor your resource structure without destroying anything. Before it existed, renaming a resource from aws_instance.old_name to aws_instance.new_name made Terraform read the change as "destroy the old one, create a new one," which could mean downtime and data loss.
Moved blocks fix that by spelling out the link between the old and new addresses:
moved {
from = aws_instance.old_name
to = aws_instance.new_name
}This one declaration tells Terraform to point the state file at the new address without touching the underlying infrastructure. It handles several common refactoring jobs:
count to for_each for more explicit resource mappingMoved blocks keep these changes safe and declarative, and they record how your infrastructure changed right there in the code instead of in some external process or a manual state command.
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 fits into 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, those state updates become permanent. Nothing happens to the real infrastructure; only the state mapping changes. Because they run in the normal workflow, moved blocks are automation-friendly and work in CI/CD pipelines, which manual state commands don't.
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 show how moved blocks handle a range of 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 most other Terraform constructs, HashiCorp recommends you keep moved blocks in your configuration indefinitely, especially in shared modules. That gives a clear migration path to people who might be on 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 lets you make a gradual transition without breaking things for the people who use your module.
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 makes the moved block operation transparent and predictable. The real state file only changes when you run terraform apply, and that's when the resource addresses are updated for good.
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:
Most of the time, moved blocks are the right way to refactor. The address change lives in your code, goes through review like any other change, and applies without recreating resources. They don't cover everything: you can't generate them dynamically, and some moves still need terraform state mv. But for the common job of renaming and reorganizing resources, they're better than editing state by hand.
As your configuration grows across many workspaces and CI/CD pipelines, the platform you run Terraform on matters as much as the refactoring technique itself. See the trade-offs in our guide to choosing a Terraform Cloud alternative. Refactoring is also where pricing models diverge: a moved-block change touching hundreds of resources is still a single run on Scalr, whatever the resource count.
