Terraform moved blocks: refactoring without pain

Learn how Terraform's ‘moved’ blocks let you rename or relocate resources safely, keep state intact, and refactor infrastructure without downtime.

Terraform's moved block feature, introduced in version 1.1, allows developers to safely rename or relocate resources in their Terraform configurations without destroying and recreating them. This declarative approach tells Terraform that a resource has changed address, instructing it to update the state file accordingly while preserving the underlying infrastructure. The feature addresses one of infrastructure-as-code's persistent challenges: evolving your configuration structure without disrupting live environments. Moved blocks make refactoring safer by documenting resource address changes directly in your configuration code, eliminating the need for manual state manipulation commands in most scenarios.

What moved blocks are and why you need them

The moved block serves a critical purpose in Terraform's infrastructure-as-code ecosystem: it enables safe, non-destructive refactoring of your resource structure. Before this feature, renaming a resource from aws_instance.old_name to aws_instance.new_name would cause Terraform to interpret this as a command to destroy the old resource and create a new one – potentially causing downtime and data loss.

Moved blocks solve this problem by explicitly declaring the relationship between old and new resource addresses:

moved {
  from = aws_instance.old_name
  to   = aws_instance.new_name
}

This simple declaration tells Terraform to update its state file to reflect the new address without modifying the underlying infrastructure. The feature supports several common refactoring scenarios, including:

  • Renaming resources for better clarity
  • Moving resources into or out of modules
  • Restructuring from count to for_each for more explicit resource mapping
  • Reorganizing module hierarchies
  • Splitting monolithic modules into smaller, focused modules

Using moved blocks makes these changes safe and declarative, documenting the evolution of your infrastructure in the code itself rather than through external processes or manual state commands.

Syntax and implementation details

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:

  • Individual resources: aws_instance.example
  • Resource instances using count or for_each: aws_instance.example[0] or aws_instance.example["primary"]
  • Modules: module.networking
  • Resources within modules: module.networking.aws_vpc.main

The moved block works seamlessly within the standard Terraform workflow. When you run terraform plan after adding moved blocks, Terraform:

  1. Reads the moved blocks and looks for resources at the "from" addresses in the state
  2. Updates the state in memory to rename those objects to their "to" addresses
  3. Creates a plan based on the updated in-memory state
  4. Shows a helpful message like # aws_instance.old_name has moved to aws_instance.new_name

During terraform apply, these state updates become permanent. No actual infrastructure changes occur – only the state mapping changes. This integration with the normal workflow makes moved blocks automation-friendly and compatible with CI/CD pipelines, unlike manual state manipulation commands.

Practical examples of moved blocks in action

Renaming a simple resource

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
}

Moving a resource into a module

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
}

Converting from count to for_each

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"]
}

Renaming a module

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 demonstrate how moved blocks can support various refactoring patterns while preserving state continuity and avoiding resource recreation.

Best practices for using moved blocks

Document and retain your moved blocks

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 many other Terraform constructs, HashiCorp recommends retaining moved blocks in your configuration indefinitely, particularly in shared modules. This provides a clear migration path for users who might be using different versions of your module.

Use the chained moves pattern

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.

Plan before applying

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.

Refactor incrementally

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.

Consider using module shims for backward compatibility

When breaking a module into smaller pieces, create a shim module that:

  1. Calls the new modules
  2. Contains moved blocks to map resources to their new locations
  3. Provides backward compatibility for existing users

This approach allows for a gradual transition without breaking changes for module consumers.

Limitations and gotchas to be aware of

No dynamic generation of moved blocks

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.tf

Not all move types are supported

Some limitations exist:

  • You cannot convert managed resources to data sources or vice versa
  • Earlier versions (before 1.3) had limitations with moving resources to external registry modules
  • Provider version constraints may limit certain resource type moves

Module boundaries matter

When refactoring modules, remember that:

  • Each module must include its own moved blocks for resources it contains
  • You cannot place moved blocks in one module to move resources in another module
  • Moving resources between entirely different module packages may require terraform state mv instead

For_each key type changes require special handling

When 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.

Working with terraform plan and apply

When you run terraform plan with moved blocks, Terraform processes them early in the planning phase:

  1. Terraform reads the configuration and state
  2. It processes all moved blocks before any other operations
  3. The resource addresses in the state are updated in memory
  4. The planning operation continues using the updated addresses
  5. The plan output includes statements like # aws_instance.old_name has moved to aws_instance.new_name

This approach makes the moved block operation transparent and predictable. The actual state file is only updated when you run terraform apply, at which point the resource addresses are permanently changed in the state file.

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.

Moved blocks vs. terraform state mv

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:

  • Shared modules used by others
  • Configurations managed through CI/CD pipelines
  • Teams working with multiple workspaces
  • Scenarios where documenting the change in code is valuable

The terraform state mv command remains useful for:

  • One-time migrations that don't need to be documented in code
  • Complex refactoring scenarios not expressible with moved blocks
  • Legacy Terraform versions (before v1.1)
  • Emergency state repairs

Recent updates and enhancements

Since its introduction in Terraform v1.1, the moved block feature has seen several improvements:

  • Terraform v1.3 (2022): Added support for moving resources to modules sourced from external registries, including the Terraform Registry and private registries.
  • Improved error messaging: Better diagnostic information when moved blocks are incorrectly configured or lead to ambiguous situations.
  • Enhanced documentation: Expanded examples and best practices in the official documentation.
  • Integration with import blocks: Seamless interaction with the newer configuration-driven import feature (introduced in v1.5.0).

As of 2025, Terraform continues to refine the moved block functionality, with ongoing discussions about supporting:

  • Dynamic moved block generation
  • Better handling of nested modules
  • More comprehensive validation during planning

Conclusion

Terraform's moved block feature provides a powerful, declarative way to safely refactor your infrastructure code without risking downtime or resource recreation. By documenting resource address changes directly in your configuration, moved blocks improve code maintainability, enhance team collaboration, and reduce the risk associated with evolving your infrastructure over time. While the feature does have some limitations, particularly around dynamic generation and certain edge cases, it represents a significant improvement over manual state manipulation for most refactoring scenarios.