1.4.6.1. 资源
资源是 Terraform 最重要的组成部分,而本节亦是本教程最重要的一节。资源通过 resource
块来定义,一个 resource
可以定义一个或多个基础设施资源对象,例如 VPC、虚拟机,或是 DNS 记录、Consul 的键值对数据等。
1.4.6.1.1. 资源语法
资源通过 resource
块定义,我们首先讲解通过 resource
块定义单个资源对象的场景。
resource "aws_vpc" "main" {
cidr_block = var.base_cidr_block
}
<BLOCK TYPE> "<BLOCK LABEL>" "<BLOCK LABEL>" {
# Block body
<IDENTIFIER> = <EXPRESSION> # Argument
}
- 块 是其他内容的容器,通常代表某种对象的配置,比如资源。块有一个块类型,可以有零个或多个标签,有一个包含任意数量的参数和嵌套块的块体。Terraform 的大部分功能都是由配置文件中的顶级块控制的。
- 参数 为一个名称赋值。它们出现在块内。
- 表达式 表示一个值,可以是字面量,也可以是引用和组合其他值。它们出现在参数的值中,或者在其他表达式中。
Terraform 是一种声明式语言,描述的是一个期望的资源状态,而不是达到期望状态所需要的步骤。块的顺序和它们所在的文件通常不重要;Terraform 只在确定操作顺序时考虑资源之间的隐式和显式关系。
在下面的例子里:
resource "aws_instance" "web" {
ami = "ami-a1b2c3d4"
instance_type = "t2.micro"
}
紧跟 resource
关键字的是资源类型,在上面的例子里就是 aws_instance
。后面是资源的 Local Name,例子里就是 web
。Local Name 可以在同一模块内的代码里被用来引用该资源,但类型加 Local Name 的组合在当前模块内必须是唯一的,不同类型的两个资源 Local Name 可以相同。随后的花括号内的内容就是块体,创建资源所用到的各种参数的值就在块体内定义。例子中我们定义了虚拟机所使用的镜像 id 以及虚拟机的尺寸。
请注意:资源名称必须以字母或下划线开头,只能包含字母、数字、下划线(_
)和连字符(-
)。
1.4.6.1.2. 资源类型
每个资源都与一个资源类型相关联,资源类型决定了它管理的基础设施对象的类型,以及资源支持的参数和其他属性。
1.4.6.1.2.1. Providers
Provider 是 Terraform 用以提供一组资源类型的插件。每个资源类型都是由一个 Provider 实现的。Provider 提供了管理单个云或本地基础设施平台的资源。Provider 与 Terraform 分开发布,但 Terraform 可以在初始化工作目录时自动安装大多数 Provider。
要管理资源,Terraform 模块必须指定所需的 Provider。有关更多信息,请参阅Provider 的声明。
大部分 Provider 需要一些配置来访问远程 API,这些配置是在根模块中配置的。有关更多信息,请参阅Provider 配置。
根据一个 resource
块的类型名,Terraform 通常可以确定使用哪个 Provider。按照约定,资源类型名以其 Provider 的首选 Local Name 开头。当使用一个 Provider 的多个配置或非首选的本地 Provider 名称时,你必须使用 provider 元参数 来手动选择一个 Provider 配置。
1.4.6.1.2.2. 资源参数
不同资源定义了不同的可赋值的属性,官方文档将之称为参数(Argument),有些参数是必填的,有些参数是可选的。使用某项资源前可以通过阅读相关文档了解参数列表以及他们的含义、赋值的约束条件。
参数值可以是简单的字面量,也可以是一个复杂的表达式。
1.4.6.1.2.3. 资源类型的文档
每一个 Terraform Provider 都有自己的文档,用以描述它所支持的资源类型种类,以及每种资源类型所支持的属性列表。
大部分公共的 Provider 都是通过 Terraform Registry 连带文档一起发布的。当我们在 Terraform Registry 站点上浏览一个 Provider 的页面时,我们可以点击 "Documentation" 链接来浏览相关文档。Provider 的文档都是版本化的,我们可以选择特定版本的 Provider 文档。
需要注意的是,Provider 文档曾经是直接托管在 terraform.io 站点上的,也就是 Terraform 核心主站的一部分,有些 Provider 的文档目前依然托管在那里,但目前 Terraform Registry 才是所有公共 Provider 文档的主站。
1.4.6.1.3. 资源的行为
一个 resource
块声明了作者想要创建的一个确切的基础设施对象,并且设定了各项属性的值。如果我们正在编写一个新的 Terraform 代码文件,那么代码所定义的资源仅仅只在代码中存在,并没有与之对应的实际的基础设施资源存在。
对一组 Terraform 代码执行 terraform apply
可以创建、更新或者销毁实际的基础设施对象,Terraform 会制定并执行变更计划,以使得实际的基础设施符合代码的定义。
每当 Terraform 按照一个 resource
块创建了一个新的基础设施对象,这个实际的对象的 id 会被保存进 Terraform 状态中,使得将来 Terraform 可以根据变更计划对它进行更新或是销毁操作。如果一个 resource
块描述的资源在状态文件中已有记录,那么 Terraform 会比对记录的状态与代码描述的状态,如果有必要,Terraform 会制定变更计划以使得资源状态能够符合代码的描述。
这种行为适用于所有资源而无关其类型。创建、更新、销毁一个资源的细节会根据资源类型而不同,但是这个行为规则却是普适的。
1.4.6.1.4. 访问资源输出属性
资源不但可以通过参数传值,成功创建的资源还对外输出一些通过调用 API 才能获得的只读数据,经常包含了一些我们在实际创建一个资源之前无法获知的数据,比如云主机的 id 等,官方文档将之称为属性(Attribute)。我们可以在同一模块内的代码中引用资源的属性来创建其他资源或是表达式。在表达式中引用资源属性的语法是<RESOURCE TYPE>.<NAME>.<ATTRIBUTE>
。
要获取一个资源类型输出的属性列表,我们可以查阅对应的 Provider 文档,一般在文档中会专门记录资源的输出属性列表。
1.4.6.1.4.1. 敏感的资源属性
在为资源类型定义架构时,Provider 开发着可以将某些属性标记为 sensitive
,在这种情况下,Terraform 将在展示涉及该属性的计划时显示占位符标记(sensitive)
而不是实际值。
标记为 sensitive
的 Provider 属性的行为类似于声明为 sensitive
的输入变量,Terraform 将隐藏计划中的值,还将隐藏从该值派生出的任何其他敏感值。但是,该行为存在一些限制,如 Terraform 可能暴露敏感变量。
如果使用资源属性中的敏感值作为输出值的一部分,Terraform 将要求将输出值本身标记为 sensitive
,以确认确实打算将其导出。
Terraform 仍会在状态中记录敏感值,因此任何可以访问状态数据的人都可以以明文形式访问敏感值。
注意:Terraform 从 v0.15 开始将从敏感资源属性派生的值视为敏感值本身。早期版本的 Terraform 将隐藏敏感资源属性的直接值,但不会自动隐藏从敏感资源属性派生的其他值。
1.4.6.1.5. 资源的依赖关系
我们在介绍输出值的depends_on
的时候已经简单介绍过了依赖关系。一般来说在 Terraform 代码定义的资源之间不会有特定的依赖关系,Terraform 可以并行地对多个无依赖关系的资源执行变更,默认情况下这个并行度是 10。
然而,创建某些资源所需要的信息依赖于另一个资源创建后输出的属性,又或者必须在某些资源成功创建后才可以被创建,这时资源之间就存在依赖关系。
大部分资源间的依赖关系可以被 Terraform 自动处理,Terraform 会分析 resource
块内的表达式,根据表达式的引用链来确定资源之间的引用,进而计算出资源在创建、更新、销毁时的执行顺序。大部分情况下,我们不需要显式指定资源之间的依赖关系。
然而,有时候某些依赖关系是无法从代码中推导出来的。例如,Terraform 必须要创建一个访问控制权限资源,以及另一个需要该权限才能成功创建的资源。后者的创建依赖于前者的成功创建,然而这种依赖在代码中没有表现为数据引用关联,这种情况下,我们需要用 depends_on
来显式声明这种依赖关系。
1.4.6.1.6. 元参数
resource
块支持几种元参数声明,这些元参数可以被声明在所有类型的 resource
块内,它们将会改变资源的行为:
depends_on
:显式声明依赖关系count
:创建多个资源实例for_each
:迭代集合,为集合中每一个元素创建一个对应的资源实例provider
:指定非默认 Provider 实例lifecycle
:自定义资源的生命周期行为provisioner
和connection
:在资源创建后执行一些额外的操作
下面我们将逐一讲解他们的用法。
1.4.6.1.6.1. depends_on
使用 depends_on
可以显式声明资源之间哪些 Terraform 无法自动推导出的隐含的依赖关系。只有当资源间确实存在依赖关系,但是彼此间又没有数据引用的场景下才有必要使用 depends_on
。
使用 depends_on
的例子是这样的:
resource "aws_iam_role" "example" {
name = "example"
# assume_role_policy is omitted for brevity in this example. See the
# documentation for aws_iam_role for a complete example.
assume_role_policy = "..."
}
resource "aws_iam_instance_profile" "example" {
# Because this expression refers to the role, Terraform can infer
# automatically that the role must be created first.
role = aws_iam_role.example.name
}
resource "aws_iam_role_policy" "example" {
name = "example"
role = aws_iam_role.example.name
policy = jsonencode({
"Statement" = [{
# This policy allows software running on the EC2 instance to
# access the S3 API.
"Action" = "s3:*",
"Effect" = "Allow",
}],
})
}
resource "aws_instance" "example" {
ami = "ami-a1b2c3d4"
instance_type = "t2.micro"
# Terraform can infer from this that the instance profile must
# be created before the EC2 instance.
iam_instance_profile = aws_iam_instance_profile.example
# However, if software running in this EC2 instance needs access
# to the S3 API in order to boot properly, there is also a "hidden"
# dependency on the aws_iam_role_policy that Terraform cannot
# automatically infer, so it must be declared explicitly:
depends_on = [
aws_iam_role_policy.example,
]
}
我们来分段解释一下这个场景,首先我们声明了一个 AWS IAM 角色,将角色绑定在一个主机实例配置文件上:
resource "aws_iam_role" "example" {
name = "example"
# assume_role_policy is omitted for brevity in this example. See the
# documentation for aws_iam_role for a complete example.
assume_role_policy = "..."
}
resource "aws_iam_instance_profile" "example" {
# Because this expression refers to the role, Terraform can infer
# automatically that the role must be created first.
role = aws_iam_role.example.name
}
虚拟机的声明代码中的这个赋值使得 Terraform 能够判断出虚拟机依赖于主机实例配置文件:
resource "aws_instance" "example" {
ami = "ami-a1b2c3d4"
instance_type = "t2.micro"
# Terraform can infer from this that the instance profile must
# be created before the EC2 instance.
iam_instance_profile = aws_iam_instance_profile.example
至此,Terraform 规划出的创建顺序是 IAM 角色 -> 主机实例配置文件 -> 主机实例。但是我们又为这个 IAM 角色添加了对 S3 存储服务的完全控制权限:
resource "aws_iam_role_policy" "example" {
name = "example"
role = aws_iam_role.example.name
policy = jsonencode({
"Statement" = [{
# This policy allows software running on the EC2 instance to
# access the S3 API.
"Action" = "s3:*",
"Effect" = "Allow",
}],
})
}
也就是说,虚拟机实例由于绑定了主机实例配置文件,从而在运行时拥有了一个 IAM 角色,而这个 IAM 角色又被赋予了 S3 的权限。但是虚拟机实例的声明代码中并没有引用 S3 权限的任何输出属性,这将导致 Terraform 无法理解他们之间存在依赖关系,进而可能会并行地创建两者,如果虚拟机实例被先创建了出来,内部的程序开始运行时,它所需要的 S3 权限却还没有创建完成,那么就将导致程序运行错误。为了确保虚拟机创建时 S3 权限一定已经存在,我们可以用 depends_on
显式声明它们的依赖关系:
# However, if software running in this EC2 instance needs access
# to the S3 API in order to boot properly, there is also a "hidden"
# dependency on the aws_iam_role_policy that Terraform cannot
# automatically infer, so it must be declared explicitly:
depends_on = [
aws_iam_role_policy.example,
]
depends_on
的赋值必须是包含同一模块内声明的其他资源名称的列表,不允许包含其他表达式,例如不允许使用其他资源的输出属性,这是因为 Terraform 必须在计算资源间关系之前就能理解列表中的值,为了能够安全地完成表达式计算,所以限制只能使用资源实例的名称。
depends_on
只能作为最后的手段使用,如果我们使用 depends_on
,我们应该用注释记录我们使用它的原因,以便今后代码的维护者能够理解隐藏的依赖关系。
1.4.6.1.6.2. count
一般来说,一个 resource 块定义了一个对应的实际基础设施资源对象。但是有时候我们希望创建多个相似的对象,比如创建一组虚拟机。Terraform 提供了两种方法实现这个目标:count
与 for_each
。
count
参数可以是任意自然数,Terraform 会创建 count
个资源实例,每一个实例都对应了一个独立的基础设施对象,并且在执行 Terraform 代码时,这些对象是被分别创建、更新或者销毁的:
resource "aws_instance" "server" {
count = 4 # create four similar EC2 instances
ami = "ami-a1b2c3d4"
instance_type = "t2.micro"
tags = {
Name = "Server ${count.index}"
}
}
我们可以在 resource
块中的表达式里使用 count
对象来获取当前的 count
索引号。count
对象只有一个属性:
count.index
:代表当前对象对应的count
下标索引(从0
开始)
如果一个 resource
块定义了 count
参数,那么 Terraform 会把这种多资源实例对象与没有 count
参数的单实例资源对象区别开:
- 访问单资源实例对象:
<TYPE>.<NAME>
(例如:aws_instance.server
) - 访问多资源实例对象:
<TYPE>.<NAME>[<INDEX>]
(例如:aws_instance.server[0]
,aws_instance.server[1]
)
声明了 count
或 for_each
的资源必须使用下标索引或者键来访问。
count
参数可以是任意自然数,然而与 resource
的其他参数不同,count
的值在 Terraform 进行任何远程资源操作(实际的增删改查)之前必须是已知的,这也就意味着赋予 count
参数的表达式不可以引用任何其他资源的输出属性(例如由其他资源对象创建时返回的一个唯一的 ID)。
1.4.6.1.6.3. for_each
for_each
是 Terraform 0.12.6 开始引入的新特性。一个 resource
块不允许同时声明 count
与 for_each
。for_each
参数可以是一个 map
或是一个 set(string)
,Terraform 会为集合中每一个元素都创建一个独立的基础设施资源对象,和 count
一样,每一个基础设施资源对象在执行 Terraform 代码时都是独立创建、修改、销毁的。
使用 map
的例子:
resource "azurerm_resource_group" "rg" {
for_each = {
a_group = "eastus"
another_group = "westus2"
}
name = each.key
location = each.value
}
使用 set(string)
的例子:
resource "aws_iam_user" "the-accounts" {
for_each = toset( ["Todd", "James", "Alice", "Dottie"] )
name = each.key
}
我们可以在声明了 for_each
参数的 resource
块内使用 each
对象来访问当前的迭代器对象:
each.key
:map
的键,或是set
中的值each.value
:map
的值,或是set
中的值
如果 for_each
的值是一个 set
,那么 each.key
和 each.value
是相等的。
使用 for_each
时,map
的所有键、set
的所有 string
值都必须是已知的,也就是状态文件中已有记录的值。所以有时候我们可能需要在执行 terraform apply
时添加 -target
参数,实现分步创建。另外,for_each
所使用的键集合不能够包含或依赖非纯函数,也就是反复执行会返回不同返回值的函数,例如 uuid
、bcrypt
、timestamp
等。
当一个 resource
声明了 for_each
时,Terraform 会把这种多资源实例对象与没有 count
参数的单资源实例对象区别开:
- 访问单资源实例对象:
<TYPE>.<NAME>
(例如:aws_instance.server
) - 访问多资源实例对象:
<TYPE>.<NAME>[<KE>]
(例如:aws_instance.server["ap-northeast-1"]
,aws_instance.server["ap-northeast-2"]
)
声明了count
或 for_each
的资源必须使用下标索引或者键来访问。
由于 Terraform 没有用以声明 set
的字面量,所以我们有时需要使用 toset
函数把 list(string)
转换为 set(string)
:
locals {
subnet_ids = toset([
"subnet-abcdef",
"subnet-012345",
])
}
resource "aws_instance" "server" {
for_each = local.subnet_ids
ami = "ami-a1b2c3d4"
instance_type = "t2.micro"
subnet_id = each.key # note: each.key and each.value are the same for a set
tags = {
Name = "Server ${each.key}"
}
}
在这里我们用 toset
把一个 list(string)
转换成了 set(string)
,然后赋予 for_each
。在转换过程中,list
中所有重复的元素会被抛弃,只剩下不重复的元素,例如 toset(["b", "a", "b"])
的结果只有"a"
和"b"
,并且 set
的元素没有特定顺序。
如果我们要把一个输入变量赋予 for_each
,我们可以直接定义变量的类型约束来避免显式调用 toset
转换类型:
variable "subnet_ids" {
type = set(string)
}
resource "aws_instance" "server" {
for_each = var.subnet_ids
# (and the other arguments as above)
}
1.4.6.1.6.4. 在 for_each 和 count 之间选择
如果创建的资源实例彼此之间几乎完全一致,那么 count
比较合适。如果彼此之间的参数差异无法直接从 count
的下标派生,那么使用 for_each
会更加安全。
在 Terraform 引入 for_each
之前,我们经常使用 count.index
搭配 length
函数和 list
来创建多个资源实例:
variable "subnet_ids" {
type = list(string)
}
resource "aws_instance" "server" {
# Create one instance for each subnet
count = length(var.subnet_ids)
ami = "ami-a1b2c3d4"
instance_type = "t2.micro"
subnet_id = var.subnet_ids[count.index]
tags = {
Name = "Server ${count.index}"
}
}
这种实现方法是脆弱的,因为资源仍然是以他们的下标而不是实际的字符串值来区分的。如果我们从 subnet_ids
列表的中间移除了一个元素,那么从该位置起后续所有的 aws_instance
都会发现它们的 subnet_id
发生了变化,结果就是所有后续的 aws_instance
都需要更新。这种场景下如果使用 for_each
就更为妥当,如果使用 for_each
,那么只有被移除的 subnet_id
对应的 aws_instance
会被销毁。
1.4.6.1.6.5. provider
关于 provider
的定义我们在前面介绍 Provider 的章节已经提到过了,如果我们声明了同一类型 Provider 的多个实例,那么我们在创建资源时可以通过指定 provider
参数选择要使用的 Provider 实例。如果没有指定 provider
参数,那么 Terraform 默认使用资源类型名中第一个单词所对应的 Provider 实例,例如 google_compute_instance
的默认 Provider 实例就是 google
,aws_instance
的默认 Provider 就是 aws
。
指定 provider
参数的例子:
# default configuration
provider "google" {
region = "us-central1"
}
# alternate configuration, whose alias is "europe"
provider "google" {
alias = "europe"
region = "europe-west1"
}
resource "google_compute_instance" "example" {
# This "provider" meta-argument selects the google provider
# configuration whose alias is "europe", rather than the
# default configuration.
provider = google.europe
# ...
}
provider
参数期待的赋值是<PROVIDER>
或是<PROVIDER>.<ALIAS>
,不需要双引号。因为在Terraform开始计算依赖路径图时,provider关系必须是已知的,所以除了这两种以外的表达式是不被接受的。
1.4.6.1.6.6. lifecycle
通常一个资源对象的生命周期在前面“资源的行为”一节中已经描述了,但是我们可以用 lifecycle
块来定一个不一样的行为方式,例如:
resource "azurerm_resource_group" "example" {
# ...
lifecycle {
create_before_destroy = true
}
}
lifecycle
块和它的内容都属于元参数,可以被声明于任意类型的资源块内部。Terraform 支持如下几种 lifecycle
:
create_before_destroy
(bool
):默认情况下,当 Terraform 需要修改一个由于服务端 API 限制导致无法直接升级的资源时,Terraform 会删除现有资源对象,然后用新的配置参数创建一个新的资源对象取代之。create_before_destroy
参数可以修改这个行为,使得 Terraform 首先创建新对象,只有在新对象成功创建并取代老对象后再销毁老对象。这并不是默认的行为,因为许多基础设施资源需要有一个唯一的名字或是别的什么标识属性,在新老对象并存时也要符合这种约束。有些资源类型有特别的参数可以为每个对象名称添加一个随机的前缀以防止冲突。Terraform 不能默认采用这种行为,所以在使用create_before_destroy
前你必须了解每一种资源类型在这方面的约束。prevent_destroy
(bool
):这个参数是一个保险措施,只要它被设置为true
时,Terraform 会拒绝执行任何可能会销毁该基础设施资源的变更计划。这个参数可以预防意外删除关键资源,例如错误地执行了terraform destroy
,或者是意外修改了资源的某个参数,导致 Terraform 决定删除并重建新的资源实例。在resource
块内声明了prevent_destroy = true
会导致无法执行terraform destroy
,所以对它的使用要节制。需要注意的是,该措施无法防止我们删除resource
块后 Terraform 删除相关资源,因为对应的prevent_destroy = true
声明也被一并删除了。ignore_changes
(list(string)
):默认情况下,Terraform 检测到代码描述的配置与真实基础设施对象之间有任何差异时都会计算一个变更计划来更新基础设施对象,使之符合代码描述的状态。在一些非常罕见的场景下,实际的基础设施对象会被 Terraform 之外的流程所修改,这就会使得 Terraform 不停地尝试修改基础设施对象以弥合和代码之间的差异。这种情况下,我们可以通过设定ignore_changes
来指示 Terraform 忽略某些属性的变更。ignore_changes
的值定义了一组在创建时需要按照代码定义的值来创建,但在更新时不需要考虑值的变化的属性名,例如:
resource "aws_instance" "example" {
# ...
lifecycle {
ignore_changes = [
# Ignore changes to tags, e.g. because a management agent
# updates these based on some ruleset managed elsewhere.
tags,
]
}
}
你也可以忽略 map
中特定的元素,例如 tags["Name"]
,但是要注意的是,如果你是想忽略 map
中特定元素的变更,那么你必须首先确保 map
中含有这个元素。如果一开始 map
中并没有这个键,而后外部系统添加了这个键,那么 Terraform 还是会把它当成一次变更来处理。比较好的方法是你在代码中先为这个键创建一个占位元素来确保这个键已经存在,这样在外部系统修改了键对应的值以后 Terraform 会忽略这个变更。
resource "aws_instance" "example" {
# ...
tags = {
# Initial value for Name is overridden by our automatic scheduled
# re-tagging process; changes to this are ignored by ignore_changes
# below.
Name = "placeholder"
}
lifecycle {
ignore_changes = [
tags["Name"],
]
}
}
除了使用一个 list(string)
,也可以使用关键字 all
,这时 Terraform 会忽略资源一切属性的变更,这样 Terraform 只会创建或销毁一个对象,但绝不会尝试更新一个对象。你只能在 ignore_changes
里忽略所属的 resource
的属性,ignore_changes
不可以赋予它自身或是其他任何元参数。
replace_triggered_by
(包含资源引用的列表):强制 Terraform 在引用的资源或是资源属性发生变更时替换声明该块的父资源,值为一个包含了托管资源、实例或是实例属性引用表达式的列表。当声明该块的资源声明了count
或是for_each
时,我们可以在表达式中使用count.index
或是each.key
来指定引用实例的序号。
replace_triggered_by
可以在以下几种场景中使用:
- 如果表达式指向多实例的资源声明(例如声明了
count
或是for_each
的资源),那么这组资源中任意实例发生变更或被替换时都将引发声明replace_triggered_by
的资源被替换 - 如果表达式指向单个资源实例,那么该实例发生变更或被替换时将引发声明
replace_triggered_by
的资源被替换 - 如果表达式指向单个资源实例的单个属性,那么该属性值的任何变化都将引发声明
replace_triggered_by
的资源被替换
我们在 replace_triggered_by
中只能引用托管资源。这允许我们在不引发强制替换的前提下修改这些表达式。
resource "aws_appautoscaling_target" "ecs_target" {
# ...
lifecycle {
replace_triggered_by = [
# Replace `aws_appautoscaling_target` each time this instance of
# the `aws_ecs_service` is replaced.
aws_ecs_service.svc.id
]
}
}
lifecycle
配置影响了 Terraform 如何构建并遍历依赖图。作为结果,lifecycle
内赋值仅支持字面量,因为它的计算过程发生在 Terraform 计算的极早期。这就是说,例如 prevent_destroy
、create_before_destroy
的值只能是 true
或者 false
,ignore_changes
、replace_triggered_by
的列表内只能是硬编码的属性名。
1.4.6.1.6.7. Precondition 与 Postcondition
请注意,Precondition 与 Postcondition 是从 Terraform v1.2.0 开始被引入的功能。
在 lifecycle
块中声明 precondition
与 postcondition
块可以为资源、数据源以及输出值创建自定义的验证规则。
Terraform 在计算一个对象之前会首先检查该对象关联的 precondition
,并且在对象计算完成后执行 postcondition
检查。Terraform 会尽可能早地执行自定义检查,但如果表达式中包含了只有在 apply
阶段才能知晓的值,那么该检查也将被推迟执行。
每一个 precondition
与 postcondition
块都需要一个 condition
参数。该参数是一个表达式,在满足条件时返回 true
,否则返回 false
。该表达式可以引用同一模块内的任意其他对象,只要这种引用不会产生环依赖。在 postcondition
表达式中也可以使用 self
对象引用声明 postcondition
的资源实例的属性。
如果 condition
表达式计算结果为 false
,Terraform 会生成一条错误信息,包含了 error_message
表达式的内容。如果我们声明了多条 precondition
或 postcondition
,Terraform 会返回所有失败条件对应的错误信息。
下面的例子演示了通过 postcondition
检测调用者是否不小心传入了错误的 AMI 参数:
data "aws_ami" "example" {
id = var.aws_ami_id
lifecycle {
# The AMI ID must refer to an existing AMI that has the tag "nomad-server".
postcondition {
condition = self.tags["Component"] == "nomad-server"
error_message = "tags[\"Component\"] must be \"nomad-server\"."
}
}
}
在 resource
或 data
块中的 lifecycle
块可以同时包含 precondition
与 postcondition
块。
- Terraform 会在计算完
count
和for_each
元参数后执行precondition
块。这使得 Terraform 可以对每一个实例独立进行检查,并允许在表达式中使用each.key
、count.index
等。Terraform 还会在计算资源的参数表达式之前执行precondition
检查。precondition
可以用来防止参数表达式计算中的错误被激发。 - Terraform 在计算和执行对一个托管资源的变更之后执行
postcondition
检查,或是在完成数据源读取后执行它关联的postcondition
检查。postcondition
失败会阻止其他依赖于此失败资源的其他资源的变更。
在大多数情况下,我们不建议在同一配置文件中同时包含表示同一个对象的 data
块和 resource
块。这样做会使得 Terraform 无法理解 data
块的结果会被 resource
块的变更所影响。然而,当我们需要检查一个 resource
块的结果,恰巧该结果又没有被资源直接输出时,我们可以使用 data
块并在块中直接使用 postcondition
来检查该对象。这等于告诉 Terraform 该 data
块是用来检查其他什么地方定义的对象的,从而允许 Terraform 以正确的顺序执行操作。
1.4.6.1.6.8. provisioner 和 connection
某些基础设施对象需要在创建后执行特定的操作才能正式工作。比如说,主机实例必须在上传了配置或是由配置管理工具初始化之后才能正常工作。
像这样创建后执行的操作可以使用预置器(Provisioner)。预置器是由 Terraform 所提供的另一组插件,每种预置器可以在资源对象创建后执行不同类型的操作。
使用预置器需要节制,因为他们采取的操作并非 Terraform 声明式的风格,所以 Terraform 无法对他们执行的变更进行建模和保存。
预置器也可以声明为资源销毁前执行,但会有一些限制。
作为元参数,provisioner
和 connection
可以声明在任意类型的 resource
块内。
举一个例子:
resource "aws_instance" "web" {
# ...
provisioner "file" {
source = "conf/myapp.conf"
destination = "/etc/myapp.conf"
connection {
type = "ssh"
user = "root"
password = var.root_password
host = self.public_ip
}
}
}
我们在 aws_instance
中定义了类型为 file
的预置器,该预置器可以本机文件或文件夹拷贝到目标机器的指定路径下。我们在预置器内部定义了connection
块,类型是ssh
。我们对connection
的host
赋值self.public_ip
,在这里self
代表预置器所在的母块,也就是aws_instance.web
,所以self.public_ip
代表着aws_instance.web.public_ip
,也就是创建出来的主机的公网ip。
file
类型预置器支持 ssh
和 winrm
两种类型的 connection
。
预置器根据运行的时机分为两种类型,创建时预置器以及销毁时预置器。
1.4.6.1.7. 创建时预置器
默认情况下,创建时资源对象会运行预置器,在对象更新、销毁时则不会运行。预置器的默认行为是为了引导一个系统。
如果创建时预置器失败了,那么资源对象会被标记污点(我们将在介绍 terraform taint
命令时详细介绍)。一个被标记污点的资源在下次执行 terraform apply
命令时会被销毁并重建。Terraform 的这种设计是因为当预置器运行失败时标志着资源处于半就绪的状态。由于 Terraform 无法衡量预置器的行为,所以唯一能够完全确保资源被正确初始化的方式就是删除重建。
我们可以通过设置 on_failure
参数来改变这种行为。
1.4.6.1.8. 销毁时预置器
如果我们设置预置器的 when
参数为 destroy
,那么预置器会在资源被销毁时执行:
resource "aws_instance" "web" {
# ...
provisioner "local-exec" {
when = destroy
command = "echo 'Destroy-time provisioner'"
}
}
销毁时预置器在资源被实际销毁前运行。如果运行失败,Terraform 会报错,并在下次运行 terraform apply
操作时重新执行预置器。在这种情况下,需要仔细关注销毁时预置器以使之能够安全地反复执行。
注意:销毁时预置器不会在 resource
块配置了 create_before_destroy = true
时运行。
销毁时预置器只有在存在于代码中的情况下才会在销毁时被执行。如果一个 resource
块连带内部的销毁时预置器块一起被从代码中删除,那么被删除的预置器在资源被销毁时不会被执行。要解决这个问题,我们需要使用多个步骤来绕过这个限制:
- 修改资源声明代码,添加
count = 0
参数 - 执行
terraform apply
,运行删除时预置器,然后删除资源实例 - 删除
resource
块 - 重新执行
terraform apply
,此时应该不会有任何变更需要执行
该限制在未来将会得到解决,但目前来说我们必须节制使用销毁时预置器。
注意:一个被标记污点的 resource
块内的销毁时预置器不会被执行。这包括了因为创建时预置器失败或是手动使用 terraform taint
命令标记污点的资源。
1.4.6.1.9. 预置器失败行为
默认情况下,预置器运行失败会导致terraform apply
执行失败。可以通过设置on_failure
参数来改变这一行为。可以设置的值为:
continue
:忽视错误,继续执行创建或是销毁fail
:报错并终止执行变更(这是默认行为)。如果这是一个创建时预置器,则在对应资源对象上标记污点
样例:
resource "aws_instance" "web" {
# ...
provisioner "local-exec" {
command = "echo The server's IP address is ${self.private_ip}"
on_failure = continue
}
}
1.4.6.1.10. 删除资源
注意:removed
块是在 Terraform v1.7 引入的功能。对于早期的 Terraform 版本,您可以使用 terraform state rm
命令来处理。
要从 Terraform 中删除资源,只需从 Terraform 代码中删除 resource
块即可。
默认情况下,删除 resource
块后,Terraform 将计划销毁该资源管理的所有实际基础设施对象。
有时,我们可能希望从 Terraform 配置中删除资源,而不破坏它管理的实际基础设施对象。在这种情况下,资源将从 Terraform 状态中删除,但真正的基础设施对象不会被破坏。
要声明资源已从 Terraform 配置中删除,但不应销毁其托管对象,请从配置中删除 resource
块并将其替换为 removed
块:
removed {
from = aws_instance.example
lifecycle {
destroy = false
}
}
from
参数是您要删除的资源的地址,没有任何实例键(例如 aws_instance.example[1]
)。
lifecycle
块是必需的。 destroy
参数确定 Terraform 是否会尝试销毁资源管理的对象。 false
值表示 Terraform 将从状态中删除资源而不销毁实际的远程资源。
removed
块还可以包含销毁时预置器,以便即使 resource
块已被删除,预制器也可以保留在代码中。
removed {
from = aws_instance.example
lifecycle {
destroy = true
}
provisioner "local-exec" {
when = destroy
command = "echo 'Instance ${self.id} has been destroyed.'"
}
}
与普通的销毁时预置器中的引用规则相同,仅允许使用 count.index
、each.key
和 self
。预置器必须指定 when = destroy
,并且 removed
块必须声明 destroy = true
才能执行预置器。
1.4.6.1.11. 本地资源
虽然大部分资源类型都对应的是通过远程基础设施 API 控制的一个资源对象,但也有一些资源对象他们只存在于 Terraform 进程自身内部,用来计算生成某些结果,并将这些结果保存在状态中以备日后使用。
比如说,我们可以用 tls_private_key
生成公私钥,用 tls_self_signed_cert
生成自签名证书,或者是用 random_id
生成随机 id。虽不像其他“真实”基础设施对象那般重要,但这些本地资源也可以成为连接其他资源有用的黏合剂。
本地资源的行为与其他类型资源是一致的,但是他们的结果数据仅存在于 Terraform 状态文件中。“销毁”这种资源只是将结果数据从状态中删除。
1.4.6.1.12. 操作超时设置
有些资源类型提供了特殊的 timeouts
内嵌块参数,它允许我们配置我们允许操作持续多长时间,超时将被认定为失败。比如说,aws_db_instance
资源允许我们分别为 create
,update
,delete
操作设置超时时间。
超时完全由资源对应的 Provider 来处理,但支持超时设置的 Provider 一般都遵循相同的传统,那就是由一个名为 timeouts
的内嵌块参数定义超时设置,timeouts
块内可以分别设置不同操作的超时时间。超时时间由 string
描述,比如 "60m"
代表 60 分钟,"10s"
代表 10 秒,"2h"
代表 2 小时。
resource "aws_db_instance" "example" {
# ...
timeouts {
create = "60m"
delete = "2h"
}
}
可配置超时的操作类别由每种支持超时设定的资源类型自行决定。大部分资源类型不支持设置超时。使用超时前请先查阅相关文档。