Skip to main content

Refactoring

In shared modules and long-lived configurations, you may eventually outgrow your initial module structure and resource names. For example, you might decide that what was previously one child module makes more sense as two separate modules and move a subset of the existing resources to the new one.

OpenTF compares previous state with new configuration, correlating by each module or resource's unique address. Therefore by default OpenTF understands moving or renaming an object as an intent to destroy the object at the old address and to create a new object at the new address.

When you add moved blocks in your configuration to record where you've historically moved or renamed an object, OpenTF treats an existing object at the old address as if it now belongs to the new address.

moved Block Syntax

A moved block expects no labels and contains only from and to arguments:

moved {
from = aws_instance.a
to = aws_instance.b
}

The example above records that the resource currently known as aws_instance.b was known as aws_instance.a in a previous version of this module.

Before creating a new plan for aws_instance.b, OpenTF first checks whether there is an existing object for aws_instance.a recorded in the state. If there is an existing object, OpenTF 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. Below, we describe several refactoring use-cases and the appropriate addressing syntax for each situation.

Renaming 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 OpenTF 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, OpenTF 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 OpenTF 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 and so objects of different types are not compatible. Therefore, although you can use moved to change the name of a resource, you cannot use moved to change to a different resource type or to change a managed resource (a resource block) into a data resource (a data block).

Enabling count or for_each For a Resource

Consider this example module containing a single-instance resource:

resource "aws_instance" "a" {
# (resource-type-specific configuration)
}

Applying this configuration would lead to OpenTF creating an object bound to the address aws_instance.a.

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 alone, you must add a moved block to specify which instance key the object will take in the new configuration:

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

The above will keep OpenTF from planning to destroy any existing object at aws_instance.a, treating that object instead as if it were originally created as aws_instance.a["small"].

When at least one of the two addresses includes an instance key, like ["small"] in the above example, OpenTF 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 are some other examples of 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
}

-> Note: When you add count to an existing resource that didn't use it, OpenTF automatically proposes to move the original object to instance zero, unless you write an moved block explicitly mentioning that resource. However, we recommend still writing out the corresponding moved block explicitly, to make the change clearer to future readers of the module.

Renaming 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, OpenTF 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, OpenTF 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 OpenTF 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.

Enabling count or for_each For a Module Call

Consider this example of a single-instance module:

module "a" {
source = "../modules/example"

# (module arguments)
}

Applying this configuration would cause OpenTF to create objects whose addresses begin with module.a.

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:

module "a" {
source = "../modules/example"
count = 3

# (module arguments)
}

moved {
from = module.a
to = module.a[2]
}

The configuration above directs OpenTF to treat all objects in module.a as if they were originally created in module.a[2]. As a result, OpenTF plans to create new objects only for module.a[0] and module.a[1].

When at least one of the two addresses includes an instance key, like [2] in the above example, OpenTF will understand 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.

For more examples of recording moves associated with instances, refer to the similar section Enabling count and for_each For a Resource.

Splitting One Module into Multiple

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:

  1. Write module "x", copying over the two resources it should contain.
  2. Write module "y", copying over the one resource it should contain.
  3. 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, OpenTF 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.

OpenTF 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
}

Removing moved Blocks

Over time, a long-lasting module may accumulate many moved blocks.

Removing a moved block is a generally breaking change because any configurations that refer to the old address will plan to delete that 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 opentf apply with your new module version.

If you need to rename or move the same object twice, we recommend documenting the full history using chained moved blocks, where the new block refers to the existing block:

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, OpenTF treats the existing object as if it had been originally created as aws_instance.c.