本节介绍的通过moved
块进行模块重构的功能是从Terraform v1.1开始被引入的,如果要在之前的版本进行这样的操作,必须通过terraform state mv
命令来完成。
对一些旨在被人复用的老模块来说,最初的模块结构和资源名称可能会逐渐变得不再合适。例如,可能发现将以前的一个子模块分割成两个单独的模块会更合理,这需要将现有资源的一个子集移动到新的模块中。Terraform将以前的状态与新代码进行比较,资源与每个模块或资源的唯一地址相关联。因此,默认情况下,移动或重命名对象会被Terraform理解为销毁旧地址的对象并在新地址创建新的对象。
当在代码中添加moved
块以记录移动或重命名对象过去的地址时,Terraform会将旧地址的现有对象视为现在属于新地址。
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
的地址使用一种特殊的地址语法,该语法允许选定模块、资源以及子模块中的资源。
不同重构场景下的语法
重命名一个资源
考虑模块代码中这样一个资源:
resource "aws_instance" "a" {
count = 2
}
第一次应用该代码时Terraform会创建资源aws_instance.a[0]
以及aws_instance.a[1]
。
如果随后修改了该资源的名称,并且把旧名字记录在一个moved
块里:
resource "aws_instance" "b" {
count = 2
}
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]
。
为资源添加count或for_each声明
一开始代码中有这样一个单实例资源:
resource "aws_instance" "a" {
# 资源属性配置
}
应用该代码会使得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
块:
moved {
from = aws_instance.b["small"]
to = aws_instance.b["tiny"]
}
moved {
from = aws_instance.c[0]
to = aws_instance.c["small"]
}
moved {
from = aws_instance.c[1]
to = aws_instance.c["tiny"]
}
moved {
from = aws_instance.d[2]
to = aws_instance.d
}
注意: 当在原先没有声明count
的资源上添加count
时,Terraform会自动将原先的对象移动到第0个位置,除非通过一个moved
块显式声明该资源。然而,建议使用moved
块显式声明资源的移动,使得读者在未来阅读模块的代码时能够更清楚地了解到这些变更。
重命名对模块的调用
可以用类似重命名资源的方式来重命名对模块的调用。
假设开始用以下代码调用一个模块:
module "a" {
source = "../modules/example"
}
当应用该代码时,Terraform会在模块内声明的资源路径前面加上一个模块路径前缀module.a
。例如模块内的aws_instance.example
的完整地址为module.a.aws_instance.example
。
如果随后打算修改模块名称,可以直接修改module
块的标签,并且在一个moved
块内部记录该变更:
module "b" {
source = "../modules/example"
}
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
,那么该移动也将被应用于所有的实例上,不需要逐个指定。
为模块调用添加count或for_each声明
考虑一下单实例的模块:
module "a" {
source = "../modules/example"q
}
应用该段代码会导致Terraform创建的资源地址都拥有module.a
的前缀。
随后如果可能需要再通过添加count
来创建多个资源实例。为了保留先前的aws_instance.a
实例不受影响,可以添加一个moved
块来设置在新代码中该实例的对应的键。
module "a" {
source = "../modules/example"
count = 3
}
moved {
from = module.a
to = module.a[2]
}
上面的代码引导Terraform将所有module.a
中的资源看作是从一开始就是以module.a[2]
的前缀被创建的。结果就是Terrafor 生成的变更计划中只会创建module.a[0]
以及module.a[1]
。
当moved
块的两个地址中的至少一个包含实例键时,例如上面例子中的[2]
那样,Terraform会理解将这两个地址理解为对模块的特定实例的调用而非对模块所有实例的调用。这意味着可以使用moved
块在不同键之间切换来添加或是删除键,该机制可用于count
和for_each
,或删除模块上的这种声明。
将一个模块分割成多个模块
随着模块提供的功能越来越多,最终模块可能变得过大而不得不将之拆分成两个独立的模块。
如下示例:
resource "aws_instance" "a" {
}
resource "aws_instance" "b" {
}
resource "aws_instance" "c" {
}
可以将该模块分割为三个部分:
aws_instance.a
现在归属于模块"x"。aws_instance.b
也属于模块"x"。aws_instance.c
现在归属于模块"y"。
要在不替换绑定到旧资源地址的现有对象的情况下实现此重构,需要:
- 编写模块"x",将属于它的两个资源拷贝过去。
- 编写模块"y",将属于它的一个资源拷贝过去。
- 编辑原有模块代码,删除这些资源,只包含有关迁移现有资源的非常简单的配置代码。
新的模块"x"和"y"应该只包含resource
块:
# 模块"x"
resource "aws_instance" "a" {
}
resource "aws_instance" "b" {
}
# 模块"y"
resource "aws_instance" "c" {
}
而原有模块则被修改成只包含有向下兼容逻辑的垫片,调用两个新模块,并使用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
}
删除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
的地址创建的。