Introduction
When refactoring Terraform configurations, it is common to move existing resources from a root module into a dedicated child module for better organization and reusability. However, if this change is not communicated to Terraform correctly, it may interpret the move as a request to destroy the old resource and create a new one, which can cause downtime or data loss.
Terraform tracks resources in its state file by their address. When you move a resource into a module, its address changes from aws_s3_bucket.logs to module.logs_bucket.aws_s3_bucket.this. Without explicit instruction, Terraform sees this as deleting one resource and adding another.
This guide demonstrates how to use the moved block to safely refactor resources into modules, ensuring Terraform understands the resource was relocated, not replaced.
Expected Outcome
You will successfully move a resource from your root configuration into a module and update the Terraform state to reflect the new resource address without destroying and recreating the underlying infrastructure.
Procedure
Follow these steps to migrate a resource into a module without causing resource recreation.
-
Identify the Current Resource
Your configuration starts with a resource defined in the root module. For this example, it is an S3 bucket in
main.tf.# root/main.tf resource "aws_s3_bucket" "logs" { bucket = "acme-logs-prod" } -
Create the New Module
Create a directory for your new module and define the resource within it. Use a variable to pass in the bucket name.
# modules/s3_bucket/main.tf variable "name" { type = string } resource "aws_s3_bucket" "this" { bucket = var.name # ...rest of your config... } -
Reference the Module and Remove the Original Block
In your root
main.tf, remove the originalresourceblock and add amoduleblock that references your new module.# root/main.tf module "logs_bucket" { source = "./modules/s3_bucket" name = "acme-logs-prod" } -
Add a
movedBlockIn your root module, create a new file such as
moved.tfand add amovedblock. This block maps the resource's old state address to its new one.# root/moved.tf moved { from = aws_s3_bucket.logs to = module.logs_bucket.aws_s3_bucket.this } -
Apply the Changes
Run
terraform planto confirm that Terraform intends to move the resource in the state file rather than destroying and creating a new one. Then, apply the change.$ terraform init $ terraform plan ## The plan should show a "moved" action, not create/destroy actions. $ terraform apply
Terraform updates the state reference with no changes to the deployed infrastructure.
Additional Information
Handling for_each and count
For resources created with for_each or count, you must include the instance key or index in the moved block addresses.
moved {
from = aws_s3_bucket.logs["prod"]
to = module.logs_bucket.aws_s3_bucket.this["prod"]
}Handling Multiple Resources
If a module refactoring splits a single legacy resource into multiple new resources (a common pattern in provider updates), add a separate moved block for each new resource.
moved {
from = aws_s3_bucket_public_access_block.logs
to = module.logs_bucket.aws_s3_bucket_public_access_block.this
}
moved {
from = aws_s3_bucket_versioning.logs
to = module.logs_bucket.aws_s3_bucket_versioning.this
}Best Practices for moved Blocks
It is a good practice to keep moved blocks in your codebase for a period after the migration. This ensures that team members or CI/CD pipelines running older versions of the configuration do not generate incorrect plans. You can remove the blocks once the state transition is complete across all environments and branches.
For more details on refactoring, refer to the official documentation on moved blocks.