Mastering Terraform Moved Block

In today’s blog post, we will discuss terraform moved block, used to rename or move terraform resources. Terraform is the most widely used Infrastructure as Code (IaC) tool in the cloud and DevOps field. But as your infrastructure as code (IaC) grows, refactoring your configurations becomes a nightmare. Renaming a resource, reorganizing modules, or switching from count to for_each can be difficult tasks, often carrying the risk of unintended infrastructure destruction and recreation.

Before Terraform v1.1, these operations typically required manual terraform state mv commands, a process prone to errors and difficult to integrate into automated CI/CD pipelines.

Introduced in Terraform v1.1, moved block provides a declarative, safe, and easy way to handle changes in your resource addresses, ensuring your infrastructure remains intact during major refactoring efforts without any outage.

What is the moved Block?

The moved block is a special top-level configuration block that you add to your Terraform code to declare that a resource or module has changed its address within the configuration. When Terraform sees a moved block, it understands that the resource at the from address in the state file should now be managed by the resource at the to address in the current configuration.

This prevents Terraform from interpreting the change as a destruction of the old resource and a creation of a new one. Instead, it performs an in-place state migration.

Why is the moved Block Important?

Without the moved block, if you:

  • Rename a resource (e.g., aws_instance.web to aws_instance.app_server).
  • Move a resource from a root configuration into a module.
  • Move a resource from one module to another.
  • Refactor a resource from count to for_each (or vice-versa).

Terraform’s default behavior would be to see the old address as “gone” (requiring destruction) and the new address as “new” (requiring creation). The moved block explicitly tells Terraform: “No, this is the same resource, just at a different place in my code.”

Syntax of the moved Block

The moved block has the following syntax:

moved {
  from = <old_resource_or_module_address>
  to   = <new_resource_or_module_address>
}
  • from: The old resource or module address as it exists in your Terraform state file. This is the address that Terraform currently associates with the existing infrastructure.
  • to: The new resource or module address as you have defined it in your updated Terraform configuration files. This is where Terraform should now look to manage that infrastructure.

Both from and to arguments must be resource addresses, which identify a single resource instance. These can be:

  • resource_type.resource_name (e.g., aws_instance.web)
  • module.module_name.resource_type.resource_name (e.g., module.vpc.aws_vpc.main)
  • resource_type.resource_name[key] (for for_each resources)
  • resource_type.resource_name[index] (for count resources)

Common Use Cases and Examples

Let us explore the most common scenarios where the moved block is used with examples.

Renaming a Resource

This is the most common use case. Sometimes you may want to rename the logical resource name for better readability. For example, say you have a resource initially created with logical name “web_server” but now you want to rename the resource logical name to “app_server”.

Before (e.g., main.tf):

resource "aws_instance" "web_server" {
  ami           = "ami-0abcdef1234567890"
  instance_type = "t2.micro"
}

After (e.g., main.tf): You decide app_server is a better name.

resource "aws_instance" "app_server" { # Renamed from web_server
  ami           = "ami-0abcdef1234567890"
  instance_type = "t2.micro"
}

# Add the moved block to inform Terraform
moved {
  from = aws_instance.web_server
  to   = aws_instance.app_server
}

Run terraform plan. Instead of seeing aws_instance.web_server to be destroyed and aws_instance.app_server to be created, you will see a message indicating the state will be moved.

Terraform will perform the following actions:

Terraform will move the following objects:
  - aws_instance.web_server to aws_instance.app_server

Moving a Resource into a Module

This is another very common use case of moved block. Say initially you created all your resources from a single main.tf file. But later as your infrastructure grows, you decided to modularize your terraform configuration blocks. The move block can be used to move a resource from one logical address to another.

Before (e.g., main.tf):

resource "aws_s3_bucket" "my_app_bucket" {
  bucket = "my-unique-app-bucket"
  acl    = "private"
}

After: You create a new module modules/s3_bucket/ with main.tf:

# modules/s3_bucket/main.tf
resource "aws_s3_bucket" "this" { # Common pattern to name resource "this" inside a module
  bucket = var.bucket_name
  acl    = "private"
}

And in your root/main.tf:

# root/main.tf
module "app_bucket" {
  source      = "./modules/s3_bucket"
  bucket_name = "my-unique-app-bucket"
}

# Add the moved block in the root configuration
moved {
  from = aws_s3_bucket.my_app_bucket
  to   = module.app_bucket.aws_s3_bucket.this
}

Refactoring from count to for_each

When you want to switch from using a count meta-argument to for_each for more explicit instance management.

Before (e.g., main.tf):

resource "aws_instance" "app_server" {
  count         = 2
  ami           = "ami-0abcdef1234567890"
  instance_type = "t2.micro"
}

After (e.g., main.tf): You introduce a map to drive for_each.

locals {
  instance_configs = {
    "server-a" = { ami_id = "ami-0abcdef1234567890", instance_type = "t2.micro" }
    "server-b" = { ami_id = "ami-0abcdef1234567890", instance_type = "t2.micro" }
  }
}

resource "aws_instance" "app_server" {
  for_each      = local.instance_configs
  ami           = each.value.ami_id
  instance_type = each.value.instance_type
}

# Add moved blocks for each instance
moved {
  from = aws_instance.app_server[0]
  to   = aws_instance.app_server["server-a"]
}

moved {
  from = aws_instance.app_server[1]
  to   = aws_instance.app_server["server-b"]
}

Note: For large numbers of instances, manually writing moved blocks can be tedious. You can use a for loop to generate these moved blocks for common transformations (e.g., if your for_each keys are just strings like “0”, “1”, “2”).

# Example: Generating moved blocks for a count to for_each where keys are numeric strings
# (Only if your target for_each keys map directly to old count indices)
# This example is illustrative and requires careful thought for complex transformations.
# The 'moved' block is a top-level block and cannot contain arbitrary expressions
# so this specific use case is more about generating the HCL using a script if needed.
# For direct HCL, you would still write them out.
# However, if your 'for_each' keys are strings that match the old indices:
moved { from = aws_instance.app_server[0] to = aws_instance.app_server["0"] }
moved { from = aws_instance.app_server[1] to = aws_instance.app_server["1"] }
# ... and so on.

# For more complex transformations where your keys are derived, you'll need
# a strategy that aligns the 'from' and 'to' based on your logic.
# Often, this still means explicit 'moved' blocks or a well-defined mapping strategy.

Best Practices for Using moved Blocks

  1. Run terraform plan First (Always!): You should always run the terraform plan first. The plan output will clearly show the “move” operation instead of “destroy” and “create.” Always confirm the plan before applying.
  2. Add moved Blocks in the Same Commit/Change: The moved blocks should be part of the same HCL change that renames or moves the resources. This make sures Terraform always sees both the old and new addresses in the same configuration.
  3. Place moved Blocks Logically: You can place moved blocks anywhere in your root configuration. A common practice is to put them in a dedicated _moved.tf or refactor.tf file to keep them separate from active resource definitions.
  4. Remove moved Blocks After Successful Apply: Once terraform apply successfully executes the move operation, the information is updated in the state file. The moved blocks are no longer needed and should be removed from your HCL files. This keeps your configuration clean and prevents potential confusion or errors in the future.
    • Why remove them? If you keep them and later rename the resource again, Terraform might get confused, or it adds unnecessary clutter to your code.
  5. Use Fully Qualified Addresses: Always use the full resource address, including module paths and instance keys/indices, in both from and to arguments.
  6. Limitations:
    • Within a Single State File: moved blocks only work for resources managed within the same Terraform state file. You cannot use them to move resources between different state files.
    • Provider-Specific: The moved block works at the Terraform core level. It assumes the underlying provider resource can be identified as the same logical object.
    • terraform init -migrate-state (for root module moves): For moving resources from the root module into a child module (especially if the child module is locally sourced), terraform init -migrate-state can also play a role in older versions or specific setups. However, moved blocks are generally more easy to use and declarative.

moved Block vs. terraform state mv

Before the moved block, the terraform state mv command was the go-to for state refactoring.

Featuremoved Blockterraform state mv Command
TypeDeclarative HCL (part of your configuration)Imperative CLI command (manual operation)
AuditableYes, part of Git historyNo, outside Git history; relies on operational logs
AutomatedYes, runs automatically during plan/applyNo, requires manual execution (prone to human error)
CollaborationEasier for teams; baked into terraform planRequires careful coordination among team members
SafetyTerraform validates move in plan before applyDirect state manipulation; higher risk if misused
TemporaryRemoved after successful applyNo lasting trace in HCL; requires careful documentation

Conclusion: The moved block is almost always the preferred method for state migration over terraform state mv due to its declarative nature, safety, and integration with Terraform’s planning phase. Use terraform state mv only for very specific, ad-hoc, and well-understood scenarios, or if you’re on a Terraform version prior to 1.1.

Step-by-Step Refactoring Process with moved Block

  1. Backup Your State (Optional but Recommended): While moved is safe, it’s always important to backup your terraform.tfstate file (or your remote backend state) before major refactoring’s.
  2. Modify Your HCL: Change the resource name, move it into a module, or refactor its count/for_each logic.
  3. Add moved Block(s): In your root configuration, add the necessary moved { from = ... to = ... } blocks to map the old state addresses to the new configuration addresses.
  4. Run terraform plan: Carefully review the plan output. Make sure it shows “Terraform will move the following objects:” and does not show any unexpected destructions or creations.
  5. Run terraform apply: Execute the plan. Terraform will perform the state migration.
  6. Verify: After a successful apply, run terraform plan again. It should show “No changes. Your infrastructure matches the configuration.” This confirms the state has been updated.
  7. Remove moved Block(s): Delete the moved blocks from your HCL files. They are no longer needed.
  8. Commit Changes: Commit your HCL changes (first with moved blocks, then a separate commit removing them after successful apply).

Conclusion

The moved block is an important tool for any Terraform developer. It transforms complex and risky refactoring operations into straightforward, declarative steps. By understanding its mechanics and following best practices, you can confidently reorganize your Terraform configurations, improve readability, and maintain a healthy, manageable state without fear of unintended infrastructure changes.

Author

Debjeet Bhowmik

Experienced Cloud & DevOps Engineer with hands-on experience in AWS, GCP, Terraform, Ansible, ELK, Docker, Git, GitLab, Python, PowerShell, Shell, and theoretical knowledge on Azure, Kubernetes & Jenkins.
In my free time, I write blogs on ckdbtech.com

Leave a Comment