跳至主要内容

重构

在共享模块和长期配置中,您最终可能会超出初始模块结构和资源名称的范围。例如,您可能决定之前的一个子模块更有意义地拆分为两个单独的模块,并将现有资源的一部分移动到新的模块中。

OpenTofu 将先前状态与新配置进行比较,通过每个模块或资源的唯一地址进行关联。因此,默认情况下,OpenTofu 将对象移动或重命名理解为销毁旧地址处对象的意图,并在新地址处创建新对象的意图。

当您在配置中添加 moved 块以记录您在历史上移动或重命名对象的地址时,OpenTofu 会将旧地址处存在的对象视为现在属于新地址。

moved 块语法

moved 块不期望任何标签,并且仅包含 fromto 参数

代码块
moved {
from = aws_instance.a
to = aws_instance.b
}

上面的示例记录了当前称为 aws_instance.b 的资源在该模块的先前版本中称为 aws_instance.a

在为 aws_instance.b 创建新的计划之前,OpenTofu 首先检查状态中是否记录了 aws_instance.a 的现有对象。如果存在现有对象,OpenTofu 会将该对象重命名为 aws_instance.b,然后继续创建计划。生成的计划就像该对象最初是在 aws_instance.b 创建的一样,避免了在应用过程中需要销毁它的任何需求。

fromto 地址都使用特殊的寻址语法,允许选择模块、资源以及子模块内的资源。下面,我们将描述几种重构用例以及每种情况下的适当寻址语法。

重命名资源

考虑此包含资源配置的示例模块

代码块
resource "aws_instance" "a" {
count = 2

# (resource-type-specific configuration)
}

首次应用此配置将导致 OpenTofu 创建 aws_instance.a[0]aws_instance.a[1]

如果您稍后为该资源选择不同的名称,则可以在 resource 块中更改名称标签,并在 moved 块中记录旧名称

代码块
resource "aws_instance" "b" {
count = 2

# (resource-type-specific configuration)
}

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

在使用此模块创建每个配置的下一个计划时,OpenTofu 会将属于 aws_instance.a 的任何现有对象视为已为 aws_instance.b 创建:aws_instance.a[0] 将被视为 aws_instance.b[0]aws_instance.a[1] 将被视为 aws_instance.b[1]

从未 有过 aws_instance.a 的模块的新实例将忽略 moved 块,并照常建议创建 aws_instance.b[0]aws_instance.b[1]

此示例中的两个地址都引用了整个资源,因此 OpenTofu 识别所有资源实例的移动。也就是说,它涵盖了 aws_instance.a[0]aws_instance.a[1],而无需分别识别每个实例。

每种资源类型都有一个单独的模式,因此不同类型的对象不兼容。因此,尽管您可以使用 moved 更改资源的名称,但您不能使用 moved 更改为不同的资源类型,或将托管资源(resource 块)更改为数据资源(data 块)。

为资源启用 countfor_each

考虑此包含单个实例资源的示例模块

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

应用此配置将导致 OpenTofu 创建绑定到地址 aws_instance.a 的对象。

稍后,您将使用 for_each 与此资源一起系统地声明多个实例。为了保留先前仅与 aws_instance.a 关联的对象,您必须添加一个 moved 块以指定该对象在新配置中将采用哪个实例键

代码块
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"]
}

以上操作将阻止 OpenTofu 计划销毁 aws_instance.a 处的任何现有对象,而是将该对象视为最初创建为 aws_instance.a["small"]

当两个地址中的至少一个包含实例键(如上面示例中的 ["small"])时,OpenTofu 会将两个地址都理解为引用资源的特定实例,而不是整个资源。这意味着您可以使用 moved 在键之间切换,以及在切换 countfor_each 或两者都不使用时添加和删除键。

以下是一些以类似方式记录对资源实例键的更改的有效 moved 块的其他示例

代码块
# 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
}

重命名模块调用

您可以像重命名资源一样重命名对模块的调用。考虑以下原始模块版本

代码块
module "a" {
source = "../modules/example"

# (module arguments)
}

应用此配置时,OpenTofu 会将此模块中声明的任何资源的地址前缀加上模块路径 `module.a`。例如,资源 `aws_instance.example` 的完整地址将为 `module.a.aws_instance.example`。

如果您稍后为该模块调用选择了一个更好的名称,则可以更改 `module` 块中的名称标签,并在 `moved` 块中记录旧名称

代码块
module "b" {
source = "../modules/example"

# (module arguments)
}

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

当为使用此模块的每个配置创建下一个计划时,OpenTofu 会将以 `module.a` 开头的任何现有对象地址视为在 `module.b` 中创建的。`module.a.aws_instance.example` 将被视为 `module.b.aws_instance.example`。

此示例中的两个地址都引用了整个模块调用,因此 OpenTofu 识别对该调用的所有实例的移动。如果此模块调用使用了 `count` 或 `for_each`,则它将应用于所有实例,无需单独指定每个实例。

为模块调用启用 `count` 或 `for_each`

考虑以下单个实例模块的示例

代码块
module "a" {
source = "../modules/example"

# (module arguments)
}

应用此配置将导致 OpenTofu 创建地址以 `module.a` 开头的对象。

在以后的模块版本中,您可能需要使用 count 与此资源一起系统地声明多个实例。为了保留之前仅与 `aws_instance.a` 关联的对象,您可以添加一个 `moved` 块来指定该对象在新配置中将采用哪个实例键

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

# (module arguments)
}

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

上面的配置指示 OpenTofu 将 `module.a` 中的所有对象视为最初在 `module.a[2]` 中创建的。因此,OpenTofu 计划仅为 `module.a[0]` 和 `module.a[1]` 创建新对象。

当至少一个地址包含实例键(如上例中的 `[2]`)时,OpenTofu 会将这两个地址都理解为引用模块调用的特定实例,而不是整个模块调用。这意味着您可以使用 `moved` 在键之间切换,并在切换 `count`、`for_each` 或两者之间时添加和删除键。

有关记录与实例关联的移动的更多示例,请参阅类似部分 为资源启用 `count` 和 `for_each`

将一个模块拆分为多个模块

随着模块发展以支持新的需求,它最终可能会变得足够大,需要拆分为两个单独的模块。

考虑此模块示例

代码块
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)
}

您可以将其拆分为两个模块,如下所示

  • aws_instance.a 现在属于模块“x”。
  • aws_instance.b 也属于模块“x”。
  • aws_instance.c 属于模块“y”。

为了在不替换绑定到旧资源地址的现有对象的情况下实现此重构,您必须

  1. 编写模块“x”,复制它应该包含的两个资源。
  2. 编写模块“y”,复制它应该包含的一个资源。
  3. 编辑原始模块,使其不再包含任何这些资源,而是仅包含用于迁移现有用户的 shim 配置。

新的模块“x”和“y”应该只包含 `resource` 块

代码块
# 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)
}

原始模块现在只是一个向后兼容的 shim,它调用这两个新模块并指示资源已移动到其中

代码块
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
}

当原始模块的现有用户升级到新的“shim”版本时,OpenTofu 会注意到这三个 `moved` 块,并将其视为与三个旧资源地址关联的对象最初是在这两个新模块中创建的。

此模块系列的新用户可以使用组合的 shim 模块这两个新模块分别使用。您可能希望向现有用户传达旧模块现已弃用,因此他们应该将这两个单独的模块用于任何新需求。

多模块重构的情况不同寻常,因为它违反了父模块将其子模块视为“封闭盒子”的典型规则,不知道子模块内部声明了哪些资源。此折衷方案假设所有这三个模块都由相同的人员维护,并以单个 模块包 一起分发。

OpenTofu 在 `moved` 块中解析模块引用,相对于定义它们的模块实例。例如,如果上面的原始模块已经是名为 `module.original` 的子模块,则对 `module.x.aws_instance.a` 的引用将解析为 `module.original.module.x.aws_instance.a`。模块只能对其自己的对象及其子模块的对象进行 `moved` 语句。

如果您需要引用使用 `count` 或 `for_each` 元参数调用的模块内的资源,则必须指定要使用的特定实例键,以便与资源配置的新位置匹配

代码块
moved {
from = aws_instance.example
to = module.new[2].aws_instance.example
}

删除 `moved` 块

随着时间的推移,一个长期存在的模块可能会积累许多 `moved` 块。

删除 `moved` 块通常是破坏性更改,因为任何引用旧地址的配置都将计划删除该现有对象,而不是移动它。我们强烈建议您保留模块早期版本的所有历史 `moved` 块,以保留任何先前版本用户的升级路径。

如果您确实决定删除 `moved` 块,请谨慎操作。当您在组织内维护私有模块并且您确定所有用户都已成功使用您的新模块版本运行 `tofu apply` 时,删除 `moved` 块可能是安全的。

如果您需要重命名或移动同一个对象两次,我们建议使用链式 `moved` 块记录完整历史记录,其中新块引用现有块

代码块
moved {
from = aws_instance.a
to = aws_instance.b
}

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

以这种方式记录一系列移动允许在具有 `aws_instance.a` 处对象的配置具有 `aws_instance.b` 处对象的配置中成功升级。在这两种情况下,OpenTofu 都将现有对象视为最初创建为 `aws_instance.c`。