Terraform Bulk Import: Bring Existing AWS Resources Under Management
Import dozens of existing AWS resources into Terraform at once using import blocks, for_each, and generate-config-out.
DevOps
Use Terraform moved blocks to rename resources, move into modules, and refactor from count to for_each without destroying infrastructure.
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.
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 ✅# 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# 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
}moved {
from = module.old_infra.aws_rds_cluster.main
to = module.database.aws_rds_cluster.main
}moved {
from = module.web_servers
to = module.application_cluster
}
module "application_cluster" {
source = "./modules/compute"
# ...
}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
}# 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
}| 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.
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 codeTip: Keep moved blocks for one release cycle. Remove them once every environment (dev, staging, prod) has applied.
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.
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.
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!
}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.
Import dozens of existing AWS resources into Terraform at once using import blocks, for_each, and generate-config-out.
Learn Terraform data sources to read existing AWS resources, look up AMIs, query remote state, and reference external information in your configurations.
Learn when to use Terraform depends_on for explicit resource dependencies. Understand implicit vs explicit dependencies, common use cases
Terraform for_each vs count explained with practical examples. Learn when to use each, how to migrate from count to for_each