Terraform
Refactor modules
By default, Terraform interprets a change as an instruction to destroy the existing resource and create a new resource at the new address. You can use a moved
block to update a resource address without destroying it.
Hands On: Try the Use Configuration to Move Resources tutorial.
Overview
Add a moved
block to your configuration to move or rename an object. Before creating a new plan for the resource specified in the to
argument, Terraform checks the state for an existing resource at the from
address. Terraform includes the update to the resource address in the next execution plan. The address change does not destroy the resource.
You can use moved
blocks for the following use cases:
- Move a resource or module
- Rename a resource
- Create multiple instances
- Rename a module call
- Split a module
When the module or resource is no longer necessary, you can remove moved
blocks from the configuration. Note that removing a moved
block is a breaking change.
Requirements
Terraform v1.1 and later is required to use moved
blocks to explicitly refactor module addresses. Instead, use the terraform state mv
CLI command.
Move a resource or module
Add a moved
block to your configuration and specify the following arguments:
In the following example, the resource named aws_instance.a
has been moved to aws_instance.b
:
moved {
from = aws_instance.a
to = aws_instance.b
}
Before creating a new plan for aws_instance.b
, Terraform first checks
whether there is an existing object for aws_instance.a
recorded in the state.
If there is an existing object, Terraform renames that object to
aws_instance.b
and then proceeds with creating a plan. The resulting plan is
as if the object had originally been created at aws_instance.b
, avoiding any
need to destroy it during apply.
The from
and to
addresses both use a special addressing syntax that allows
selecting modules, resources, and resources inside child modules.
Rename a resource
Consider this example module with a resource configuration:
resource "aws_instance" "a" {
count = 2
# (resource-type-specific configuration)
}
Applying this configuration for the first time would cause Terraform to
create aws_instance.a[0]
and aws_instance.a[1]
.
If you later choose a different name for this resource, then you can change the
name label in the resource
block and record the old name inside a moved
block:
resource "aws_instance" "b" {
count = 2
# (resource-type-specific configuration)
}
moved {
from = aws_instance.a
to = aws_instance.b
}
When creating the next plan for each configuration using this module, Terraform
treats any existing objects belonging to aws_instance.a
as if they had
been created for aws_instance.b
: aws_instance.a[0]
will be treated as
aws_instance.b[0]
, and aws_instance.a[1]
as aws_instance.b[1]
.
New instances of the module, which never had an
aws_instance.a
, will ignore the moved
block and propose to create
aws_instance.b[0]
and aws_instance.b[1]
as normal.
Both of the addresses in this example referred to a resource as a whole, and
so Terraform recognizes the move for all instances of the resource. That is,
it covers both aws_instance.a[0]
and aws_instance.a[1]
without the need
to identify each one separately.
Each resource type has a separate schema so objects of different types
are not typically compatible. You can always use the moved
block to change
the name of a resource, but some providers also let you change an object from
one resource type to another. Refer to the provider documentation for details
on which resources can move between types. You cannot use the moved
block to change a managed resource (a resource
block) into a data
resource (a data
block).
Create multiple instances
You can use a moved
block when creating mulitple instances of a resource or module call while preserving the original instance.
Enable count
or for_each
for a resource
The following example module contains a single-instance resource bound to the address aws_instance.a
:
resource "aws_instance" "a" {
# (resource-type-specific configuration)
}
Later, you use for_each
with this
resource to systematically declare multiple instances. To preserve an object
that was previously associated with aws_instance.a
, you must add a
moved
block to specify which instance key the object will take in the new
configuration.
In the following example, Terraform does not plan to destroy any existing object at
aws_instance.a
. Instead, Terraform treats that object as if it were originally
created as aws_instance.a["small"]
:
locals {
instances = tomap({
big = {
instance_type = "m3.large"
}
small = {
instance_type = "t2.medium"
}
})
}
resource "aws_instance" "a" {
for_each = local.instances
instance_type = each.value.instance_type
# (other resource-type-specific configuration)
}
moved {
from = aws_instance.a
to = aws_instance.a["small"]
}
When at least one of the two addresses includes an instance key, such as
["small"]
in the previous example, Terraform understands both addresses as
referring to specific instances of a resource rather than the resource as a
whole. That means you can use moved
to switch between keys and to add and
remove keys as you switch between count
, for_each
, or neither.
The following examples are also valid moved
blocks that record
changes to resource instance keys in a similar way:
# Both old and new configuration used "for_each", but the
# "small" element was renamed to "tiny".
moved {
from = aws_instance.b["small"]
to = aws_instance.b["tiny"]
}
# The old configuration used "count" and the new configuration
# uses "for_each", with the following mappings from
# index to key:
moved {
from = aws_instance.c[0]
to = aws_instance.c["small"]
}
moved {
from = aws_instance.c[1]
to = aws_instance.c["tiny"]
}
# The old configuration used "count", and the new configuration
# uses neither "count" nor "for_each", and you want to keep
# only the object at index 2.
moved {
from = aws_instance.d[2]
to = aws_instance.d
}
When you add count
to an existing resource that didn't previously have the argument,
Terraform automatically proposes moving the original object to instance 0
unless you write a moved
block that explicitly mentions that resource.
However, we recommend writing out the corresponding moved
block
explicitly to make the change clearer to future readers of the module.
Enable count
or for_each
for a module call
The following example is a single-instance module that creates objects whose
addresses begin with module.a
:
module "a" {
source = "../modules/example"
# (module arguments)
}
In later module versions, you may need to use
count
with this resource to systematically
declare multiple instances. To preserve an object that was previously associated
with aws_instance.a
alone, you can add a moved
block to specify which
instance key that object will take in the new configuration.
The following configuration above directs Terraform to treat all objects in module.a
as
if they were originally created in module.a[2]
. As a result, Terraform plans
to create new objects only for module.a[0]
and module.a[1]
:
module "a" {
source = "../modules/example"
count = 3
# (module arguments)
}
moved {
from = module.a
to = module.a[2]
}
When at least one of the two addresses includes an instance key, such as
[2]
in the previous example, Terraform understands both addresses as
referring to specific instances of a module call rather than the module
call as a whole. That means you can use moved
to switch between keys and to
add and remove keys as you switch between count
, for_each
, or neither.
Rename a module call
You can rename a call to a module in a similar way as renaming a resource. Consider the following original module version:
module "a" {
source = "../modules/example"
# (module arguments)
}
When applying this configuration, Terraform would prefix the addresses for
any resources declared in this module with the module path module.a
.
For example, a resource aws_instance.example
would have the full address
module.a.aws_instance.example
.
If you later choose a better name for this module call, then you can change the
name label in the module
block and record the old name inside a moved
block:
module "b" {
source = "../modules/example"
# (module arguments)
}
moved {
from = module.a
to = module.b
}
When creating the next plan for each configuration using this module, Terraform
will treat any existing object addresses beginning with module.a
as if
they had instead been created in module.b
. module.a.aws_instance.example
would be treated as module.b.aws_instance.example
.
Both of the addresses in this example referred to a module call as a whole, and
so Terraform recognizes the move for all instances of the call. If this
module call used count
or for_each
then it would apply to all of the
instances, without the need to specify each one separately.
Split a module
As a module grows to support new requirements, it might eventually grow big enough to warrant splitting into two separate modules.
Consider this example module:
resource "aws_instance" "a" {
# (other resource-type-specific configuration)
}
resource "aws_instance" "b" {
# (other resource-type-specific configuration)
}
resource "aws_instance" "c" {
# (other resource-type-specific configuration)
}
You can split this into two modules as follows:
aws_instance.a
now belongs to module "x".aws_instance.b
also belongs to module "x".aws_instance.c
belongs module "y".
To achieve this refactoring without replacing existing objects bound to the old resource addresses, you must:
- Write module "x", copying over the two resources it should contain.
- Write module "y", copying over the one resource it should contain.
- Edit the original module to no longer include any of these resources, and instead to contain only shim configuration to migrate existing users.
The new modules "x" and "y" should contain only resource
blocks:
# module "x"
resource "aws_instance" "a" {
# (other resource-type-specific configuration)
}
resource "aws_instance" "b" {
# (other resource-type-specific configuration)
}
# module "y"
resource "aws_instance" "c" {
# (other resource-type-specific configuration)
}
The original module, now only a shim for backward-compatibility, calls the two new modules and indicates that the resources moved into them:
module "x" {
source = "../modules/x"
# ...
}
module "y" {
source = "../modules/y"
# ...
}
moved {
from = aws_instance.a
to = module.x.aws_instance.a
}
moved {
from = aws_instance.b
to = module.x.aws_instance.b
}
moved {
from = aws_instance.c
to = module.y.aws_instance.c
}
When an existing user of the original module upgrades to the new "shim"
version, Terraform notices these three moved
blocks and behaves
as if the objects associated with the three old resource addresses were
originally created inside the two new modules.
New users of this family of modules may use either the combined shim module or the two new modules separately. You may wish to communicate to your existing users that the old module is now deprecated and so they should use the two separate modules for any new needs.
The multi-module refactoring situation is unusual in that it violates the typical rule that a parent module sees its child module as a "closed box", unaware of exactly which resources are declared inside it. This compromise assumes that all three of these modules are maintained by the same people and distributed together in a single module package.
Terraform resolves module references in moved
blocks relative to the module
instance they are defined in. For example, if the original module above were
already a child module named module.original
, the reference to
module.x.aws_instance.a
would resolve as
module.original.module.x.aws_instance.a
. A module may only make moved
statements about its own objects and objects of its child modules.
If you need to refer to resources within a module that was called using
count
or for_each
meta-arguments, you must specify a specific instance
key to use in order to match with the new location of the resource
configuration:
moved {
from = aws_instance.example
to = module.new[2].aws_instance.example
}
Remove a move
block
Over time, a long-lasting module may accumulate many moved
blocks.
Removing a moved
block is a breaking change because any configurations that refer to the old address will plan to delete the existing object instead of move it. We strongly recommend that you retain all historical moved
blocks from earlier versions of your modules to preserve the upgrade path for users of any previous version.
If you do decide to remove moved
blocks, proceed with caution. It can be safe to remove moved
blocks when you are maintaining private modules within an organization and you are certain that all users have successfully run terraform apply
with your new module version.
If you need to rename or move the same object twice, we recommend chaining moved
blocks to document the full change history:
moved {
from = aws_instance.a
to = aws_instance.b
}
moved {
from = aws_instance.b
to = aws_instance.c
}
Recording a sequence of moves in this way allows for successful upgrades for
both configurations with objects at aws_instance.a
and configurations with
objects at aws_instance.b
. In both cases, Terraform treats the existing
object as if it had been originally created as aws_instance.c
.