Explain Terraform's moved blocks and refactoring capabilities. How do you safely rename resources, restructure modules, and migrate resources between modules without destroying and recreating infrastructure?
Quick Answer
Use 'moved' blocks to declaratively specify resource address changes (rename, move between modules, change count to for_each). Terraform plans the move as an in-place update with no infrastructure changes. This replaces the imperative 'terraform state mv' approach.
Detailed Answer
The Problem
When you rename a resource, change from count to for_each, or restructure modules, Terraform sees the old address as a destroy and the new address as a create. This causes unnecessary destruction of real infrastructure.
Moved Blocks (Terraform 1.1+)
The moved block declaratively tells Terraform that a resource has changed address. During plan, Terraform shows the move as a no-op (no infrastructure changes). This is safer than terraform state mv because: 1. It's version-controlled (code review the move) 2. It works in CI/CD (no manual state manipulation) 3. It's idempotent (safe to apply multiple times) 4. Team members get the move automatically when they pull code
Common Refactoring Scenarios
1. Simple rename: Resource aws_instance.web -> aws_instance.application 2. Move into module: aws_vpc.main -> module.networking.aws_vpc.main 3. Count to for_each: aws_subnet.private[0] -> aws_subnet.private["us-east-1a"] 4. Module rename: module.old_name -> module.new_name 5. Split module: Move resources from one module into two separate modules
Best Practices
- Keep moved blocks for at least 2 release cycles so all state files get updated - Add comments explaining why the move happened - Run terraform plan in all environments after adding moved blocks to verify no unintended changes - For complex refactoring, do moves in stages (don't rename AND restructure in the same PR)
Limitations
Moved blocks can't change resource types (e.g., aws_instance to aws_launch_template). For type changes, you must import the new resource and remove the old one.
Code Example
# Scenario 1: Simple resource rename
moved {
from = aws_instance.web
to = aws_instance.application_server
}
resource "aws_instance" "application_server" {
# Same configuration as before
ami = "ami-0123456789"
instance_type = "m5.xlarge"
}
# Scenario 2: Move resource into a module
moved {
from = aws_vpc.main
to = module.networking.aws_vpc.main
}
module "networking" {
source = "./modules/networking"
}
# Scenario 3: Convert count to for_each
moved {
from = aws_subnet.private[0]
to = aws_subnet.private["us-east-1a"]
}
moved {
from = aws_subnet.private[1]
to = aws_subnet.private["us-east-1b"]
}
moved {
from = aws_subnet.private[2]
to = aws_subnet.private["us-east-1c"]
}
resource "aws_subnet" "private" {
for_each = toset(["us-east-1a", "us-east-1b", "us-east-1c"])
vpc_id = aws_vpc.main.id
availability_zone = each.value
cidr_block = cidrsubnet(var.vpc_cidr, 4, index(["us-east-1a", "us-east-1b", "us-east-1c"], each.value))
}
# Scenario 4: Module rename
moved {
from = module.old_infra
to = module.core_infrastructure
}
# Verify no infrastructure changes
terraform plan
# Should show:
# # aws_instance.web has moved to aws_instance.application_server
# # (no changes, resource address updated)Interview Tip
Moved blocks are a relatively modern Terraform feature that many candidates don't know about. Demonstrating knowledge of this shows you do real-world Terraform maintenance, not just greenfield development. The key selling point over 'terraform state mv' is that moved blocks are version-controlled, reviewable, and work in CI/CD without manual state manipulation.