1.6.4.1. 重构
请注意,本节介绍的通过 moved
块进行模块重构的功能是从 Terraform v1.1 开始被引入的。如果要在之前的版本进行这样的操作,必须通过 terraform state mv
命令来完成。
对于一些旨在被人复用的老模块来说,最初的模块结构和资源名称可能会逐渐变得不再合适。例如,我们可能发现将以前的一个子模块分割成两个单独的模块会更合理,这需要将现有资源的一个子集移动到新的模块中。
Terraform 将以前的状态与新代码进行比较,资源与每个模块或资源的唯一地址相关联。因此,默认情况下,移动或重命名对象会被 Terraform 理解为销毁旧地址的对象并在新地址创建新的对象。
当我们在代码中添加 moved
块以记录我们移动或重命名对象过去的地址时,Terraform 会将旧地址的现有对象视为现在属于新地址。
1.6.4.1.1. moved 块语法
moved
块只包含 from
和 to
参数,没有名称:
moved {
from = aws_instance.a
to = aws_instance.b
}
上面的例子演示了模块先前版本中的 aws_instance.a
如今以 aws_instance.b
的名字存在。
在为 aws_instance.b
创建新的变更计划之前,Terraform 会首先检查当前状态中是否存在地址为 aws_instance.a
的记录。如果存在该记录,Terraform 会将之重命名为 aws_instance.b
然后继续创建变更计划。最终生成的变更计划中该对象就好像一开始就是以 aws_instance.b
的名字被创建的,防止它在执行变更时被删除。
from
和 to
的地址使用一种特殊的地址语法,该语法允许选定模块、资源以及子模块中的资源。下面是几种不同的重构场景中所需要的地址语法:
1.6.4.1.2. 重命名一个资源
考虑模块代码中这样一个资源:
resource "aws_instance" "a" {
count = 2
# (resource-type-specific configuration)
}
第一次应用该代码时 Terraform 会创建 aws_instance.a[0]
以及 aws_instance.a[1]
。
如果随后我们修改了该资源的名称,并且把旧名字记录在一个 moved
块里:
resource "aws_instance" "b" {
count = 2
# (resource-type-specific configuration)
}
moved {
from = aws_instance.a
to = aws_instance.b
}
当下一次应用使用了该模块的代码时,Terraform 会把所有地址为 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]
。
1.6.4.1.3. 为资源添加 count 或 for_each 声明
一开始代码中有这样一个单实例资源:
resource "aws_instance" "a" {
# (resource-type-specific configuration)
}
应用该代码会使得 Terraform 创建了一个地址为 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"]
}
上面的代码会防止 Terraform 在变更计划中销毁已经存在的 aws_instance.a
对象,并且将其看作是以 aws_instance.a["small"]
的地址创建的。
当 moved
块的两个地址中的至少一个包含实例键时,如上例中的 ["small"]
,Terraform 将这两个地址理解为引用资源的特定实例而不是整个资源。这意味着您可以使用 moved
在键之间切换以及在 count
、for_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
}
注意:当我们在原先没有声明 count
的资源上添加 count
时,Terraform 会自动将原先的对象移动到第 0 个位置,除非我们通过一个 moved
块显式声明该资源。然而,我们建议使用 moved
块显式声明资源的移动,使得读者在未来阅读模块的代码时能够更清楚地了解到这些变更。
1.6.4.1.4. 重命名对模块的调用
我们可以用类似重命名资源的方式来重命名对模块的调用。假设我们开始用以下代码调用一个模块:
module "a" {
source = "../modules/example"
# (module arguments)
}
当应用该代码时,Terraform 会在模块内声明的资源路径前面加上一个模块路径前缀 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
}
当下一次应用包含该模块调用的代码时,Terraform 会将所有路径前缀为 module.a
的对象看作从一开始就是以 module.b
为前缀创建的。module.a.aws_instance.example
会被看作是 module.b.aws_instance.example
。
该例子中的 moved
块中的两个地址都代表对模块的调用,而 Terraform 识别出将原模块地址中所有的资源移动到新的模块地址中。如果该模块声明时使用了 count
或是 for_each
,那么该移动也将被应用于所有的实例上,不需要逐个指定。
1.6.4.1.5. 为模块调用添加 count 或 for_each 声明
考虑一下单实例的模块:
module "a" {
source = "../modules/example"q
# (module arguments)
}
应用该段代码会导致 Terraform 创建的资源地址都拥有 module.a
的前缀。
随后如果我们可能需要再通过添加 count
来创建多个资源实例。为了保留先前的 aws_instance.a
实例不受影响,我们可以添加一个 moved
块来设置在新代码中该实例的对应的键。
module "a" {
source = "../modules/example"
count = 3
# (module arguments)
}
moved {
from = module.a
to = module.a[2]
}
上面的代码引导 Terraform 将所有 module.a
中的资源看作是从一开始就是以 module.a[2]
的前缀被创建的。结果就就是,Terraform 生成的变更计划中只会创建 module.a[0]
以及 module.a[1]
。
当 moved
块的两个地址中的至少一个包含实例键时,例如上面例子中的 [2]
那样,Terraform 会理解将这两个地址理解为对模块的特定实例的调用而非对模块所有实例的调用。这意味着我们可以使用 moved
块在不同键之间切换来添加或是删除键,该机制可用于 count
和 for_each
,或删除模块上的这种声明。
1.6.4.1.6. 将一个模块分割成多个模块
随着模块提供的功能越来越多,最终模块可能变得过大而不得不将之拆分成两个独立的模块。
我们看一下下面的这个例子:
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"。
要在不替换绑定到旧资源地址的现有对象的情况下实现此重构,我们需要:
- 编写模块 "x",将属于它的两个资源拷贝过去。
- 编写模块 "y",将属于它的一个资源拷贝过去。
- 编辑原有模块代码,删除这些资源,只包含有关迁移现有资源的非常简单的配置代码。
新的模块 "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)
}
而原有模块则被修改成只包含有向下兼容逻辑的垫片,调用两个新模块,并使用 moved
块定义哪些资源被移动到新模块中去了:
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
}
当一个原模块的调用者升级模块版本到这个“垫片”版本时,Terraform 会注意到这些 moved
块,并将那些关联到老地址的资源对象看作是从一开始就是由新模块创建的那样。
该模块的新用户可以选择使用这个垫片模块,或是独立调用两个新模块。我们需要通知老模块的现有用户老模块已被废弃,他们将来的开发中需要独立使用这两个新模块。
多模块重构的场景是不多见的,因为它违反了父模块将其子模块视为黑盒的典型规则,不知道在其中声明了哪些资源。这种妥协的前提是假设所有这三个模块都由同一个人维护并分布在一个模块包中。
为避免独立模块之间的耦合,Terraform 只允许声明在同一个目录下的模块间的移动。换句话讲,Terraform 不允许将资源移动到一个 source
地址不是本地路径的模块中去。
Terraform 使用定义 moved
块的模块实例的地址的地址来解析 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
}
1.6.4.1.7. 删除 moved 块
随着时间的推移,一些老模块可能会积累大量 moved
块。
删除 moved
块通常是一种破坏性变更,因为删除后所有使用旧地址引用的对象都将被删除而不是被移动。我们强烈建议保留历史上所有的 moved
块来保存用户从任意版本升级到当前版本的升级路径信息。
如果我们决定要删除 moved
块,需要谨慎行事。对于组织内部的私有模块来说删除 moved
块可能是安全的,因为我们可以确认所有用户都已经使用新版本模块代码运行过 terraform apply
了。
如果我们需要多次重命名或是移动一个对象,我们建议使用串联的 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
两种地址的资源都得到成功更新,Terraform 会将他们视作从一开始就是以 aws_instance.c
的地址创建的。
1.6.4.1.8. 删除模块
注意:removed
块是在 Terraform v1.7 引入的功能。对于早期的 Terraform 版本,您可以使用 terraform state rm
命令来处理。
要从 Terraform 中删除模块,只需从 Terraform 代码中删除模块调用即可。
默认情况下,删除模块块后,Terraform 将计划销毁由该模块中声明的所有资源。这是因为当您删除模块调用时,该模块的代码将不再包含在我们当前的 Terraform 代码中。
有时我们可能希望从 Terraform 代码中删除模块而不破坏它管理的实际基础设施对象。在这种情况下,资源将从 Terraform 状态中删除,但真正的基础设施对象不会被销毁。
要声明模块已从 Terraform 配置中删除,但不应销毁其托管对象,请从配置中删除 module
块并将其替换为 removed
块:
removed {
from = module.example
lifecycle {
destroy = false
}
}
from
参数是要删除的模块的地址,不带任何实例键(例如 module.example[1]
)。
lifecycle
块是必需的。 destroy
参数确定 Terraform 是否会尝试销毁模块管理的对象。 false
值表示 Terraform 将从状态中删除资源而不破坏它们。