The moved block (Terraform 1.1+) lets you rename resources, move them between modules, and refactor from count to for_each — all without destroying and recreating the actual infrastructure. It’s the safe way to refactor Terraform code.
Why moved Matters
Without moved, renaming a resource destroys it and creates a new one:
# Renaming from "web" to "app" without moved:
# terraform plan
# - aws_instance.web (destroy)
# + aws_instance.app (create)
# 😱 Your production server gets destroyed and recreated!
With moved, Terraform updates the state without touching the real resource:
moved {
from = aws_instance.web
to = aws_instance.app
}
# terraform plan
# aws_instance.web has moved to aws_instance.app
# No changes to infrastructure ✅
Basic: Rename a Resource
# Step 1: Add moved block
moved {
from = aws_instance.web
to = aws_instance.application_server
}
# Step 2: Rename the resource block
resource "aws_instance" "application_server" {
ami = "ami-abc123"
instance_type = "t3.micro"
}
# Step 3: Apply
# terraform plan → "aws_instance.web has moved to aws_instance.application_server"
# terraform apply → State updated, no infrastructure changes
Move Into a Module
# Before: resource in root module
# resource "aws_vpc" "main" { ... }
# After: resource moved into a module
moved {
from = aws_vpc.main
to = module.networking.aws_vpc.this
}
module "networking" {
source = "./modules/networking"
# ...
}
# modules/networking/main.tf
resource "aws_vpc" "this" {
cidr_block = var.vpc_cidr
}
Move Between Modules
moved {
from = module.old_infra.aws_rds_cluster.main
to = module.database.aws_rds_cluster.main
}
Rename a Module
moved {
from = module.web_servers
to = module.application_cluster
}
module "application_cluster" {
source = "./modules/compute"
# ...
}
Migrate from count to for_each
This is one of the most common refactoring needs. count uses indexes, for_each uses keys:
# Before: count-based
# resource "aws_subnet" "public" {
# count = 3
# cidr_block = cidrsubnet("10.0.0.0/16", 8, count.index)
# }
# After: for_each-based
moved {
from = aws_subnet.public[0]
to = aws_subnet.public["us-east-1a"]
}
moved {
from = aws_subnet.public[1]
to = aws_subnet.public["us-east-1b"]
}
moved {
from = aws_subnet.public[2]
to = aws_subnet.public["us-east-1c"]
}
resource "aws_subnet" "public" {
for_each = {
"us-east-1a" = "10.0.0.0/24"
"us-east-1b" = "10.0.1.0/24"
"us-east-1c" = "10.0.2.0/24"
}
availability_zone = each.key
cidr_block = each.value
vpc_id = aws_vpc.main.id
}
Multiple Moves at Once
# Reorganize a flat structure into modules
moved {
from = aws_vpc.main
to = module.networking.aws_vpc.this
}
moved {
from = aws_subnet.public
to = module.networking.aws_subnet.public
}
moved {
from = aws_subnet.private
to = module.networking.aws_subnet.private
}
moved {
from = aws_db_instance.main
to = module.database.aws_db_instance.this
}
moved {
from = aws_ecs_cluster.main
to = module.compute.aws_ecs_cluster.this
}
moved vs terraform state mv
| Feature | moved block | terraform state mv |
|---|---|---|
| Declarative | ✅ In code | ❌ CLI command |
| Code-reviewable | ✅ PR review | ❌ Manual coordination |
| Team-friendly | ✅ Everyone gets the move on next apply | ❌ Must run on every workspace |
| CI/CD compatible | ✅ Automatic | ❌ Manual step needed |
| Reversible | ✅ Remove the block | ❌ Reverse mv command |
| Chained moves | ✅ A→B→C | ⚠️ More complex |
Use moved for: team workflows, CI/CD, any move you want code-reviewed.
Use state mv for: quick solo renames, emergency fixes, one-off migrations.
Lifecycle of a moved Block
1. Add moved block + rename resource → Commit to Git
2. Team members pull and run terraform plan → See "moved" in plan
3. Each environment applies → State updated automatically
4. After ALL environments have applied → Remove moved block
5. Commit removal → Clean code
Tip: Keep moved blocks for one release cycle. Remove them once every environment (dev, staging, prod) has applied.
Chained Moves
If a resource was renamed multiple times:
# Resource was originally "server", then "web", now "app"
moved {
from = aws_instance.server
to = aws_instance.web
}
moved {
from = aws_instance.web
to = aws_instance.app
}
resource "aws_instance" "app" { ... }
Terraform follows the chain: if state has server, it moves to web, then to app.
Common Errors
“Moved object still exists”
Error: Moved object still exists
The configuration still has a resource "aws_instance" "web"
Fix: Delete the old resource block. Keep only the new name.
“Cross-resource move statement”
You can’t move between different resource types:
# ❌ Cannot change resource type
moved {
from = aws_instance.web
to = aws_spot_instance_request.web # Different type!
}
Hands-On Courses
- Terraform for Beginners on CopyPasteLearn
- Terraform By Example — practical code examples
Conclusion
The moved block is the safe way to refactor Terraform code. Rename resources, reorganize into modules, and migrate from count to for_each — all without destroying infrastructure. It’s declarative, code-reviewable, and automatically applies when teammates pull your changes. Use it instead of terraform state mv for any team workflow.