Skip to main content

Terraform moved Block Explained: Rename Resources Without Destroying Them

Key Takeaway

Use Terraform moved blocks to rename resources, move into modules, and refactor from count to for_each without destroying infrastructure. Complete guide with examples.

Table of Contents

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

Featuremoved blockterraform 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

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.

🚀

Level Up Your Terraform Skills

Hands-on courses, books, and resources from Luca Berton

Luca Berton
Written by

Luca Berton

DevOps Engineer, AWS Partner, Terraform expert, and author. Creator of Ansible Pilot, Terraform Pilot, and CopyPasteLearn.