1.4.6.1. 资源
资源是Terraform最重要的组成部分,而本节亦是本教程最重要的一节。资源通过resource块来定义,一个resource可以定义一个或多个基础设施资源对象,例如VPC、虚拟机,或是DNS记录、Consul的键值对数据等。
1.4.6.1.1. 资源语法
资源通过resource块定义,我们首先讲解通过resource块定义单个资源对象的场景。
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. 资源参数
不同资源定义了不同的可赋值的属性,官方文档将之称为参数(Argument),有些参数是必填的,有些参数是可选的。使用某项资源前可以通过阅读相关文档了解参数列表以及他们的含义、赋值的约束条件。
参数值可以是简单的字面量,也可以是一个复杂的表达式。
1.4.6.1.3. 资源类型的文档
每一个Terraform Provider都有自己的文档,用以描述它所支持的资源类型种类,以及每种资源类型所支持的属性列表。
大部分公共的Provider都是通过Terraform Registry连带文档一起发布的。当我们在Terraform Registry站点上浏览一个Provider的页面时,我们可以点击"Documentation"链接来浏览相关文档。Provider的文档都是版本化的,我们可以选择特定版本的Provider文档。
需要注意的是,Provider文档曾经是直接托管在terraform.io站点上的,也就是Terraform核心主站的一部分,有些Provider的文档目前依然托管在那里,但目前Terraform Rregistry才是所有公共Provider文档的主站(唯一的例外是用来读取其他Terraform状态数据的内建的terraform provider,它的文档目前不在Terraform Registry上)。
1.4.6.1.4. 资源的行为
一个resource块声明了作者想要创建的一个确切的基础设施对象,并且设定了各项属性的值。如果我们正在编写一个新的Terraform代码文件,那么代码所定义的资源仅仅只在代码中存在,并没有与之对应的实际的基础设施资源存在。
对一组Terraform代码执行terraform apply可以创建、更新或者销毁实际的基础设施对象,Terraform会制定并执行变更计划,以使得实际的基础设施符合代码的定义。
每当Terraform按照一个resource块创建了一个新的基础设施对象,这个实际的对象的id会被保存进Terraform状态中,使得将来Terraform可以根据变更计划对它进行更新或是销毁操作。如果一个resource块描述的资源在状态文件中已有记录,那么Terraform会比对记录的状态与代码描述的状态,如果有必要,Terraform会制定变更计划以使得资源状态能够符合代码的描述。
这种行为适用于所有资源而无关其类型。创建、更新、销毁一个资源的细节会根据资源类型而不同,但是这个行为规则却是普适的。
1.4.6.1.5. 访问资源输出属性
资源不但可以通过参数传值,成功创建的资源还对外输出一些通过调用API才能获得的只读数据,经常包含了一些我们在实际创建一个资源之前无法获知的数据,比如云主机的id等,官方文档将之称为属性(Attribute)。我们可以在同一模块内的代码中引用资源的属性来创建其他资源或是表达式。在表达式中引用资源属性的语法是<RESOURCE TYPE>.<NAME>.<ATTRIBUTE>
。
要获取一个资源类型输出的属性列表,我们可以查阅对应的Provider文档,一般在文档中会专门记录资源的输出属性列表。
1.4.6.1.5.1. 敏感的资源属性
在为资源类型定义架构时,Provider 开发着可以将某些属性标记为 sensitive
,在这种情况下,Terraform 将在展示涉及该属性的计划时显示占位符标记(sensitive)
而不是实际值。
标记为 sensitive
的 Provider 属性的行为类似于声明为 sensitive
的输入变量,Terraform 将隐藏计划中的值,还将隐藏从该值派生出的任何其他敏感值。但是,该行为存在一些限制,如 Terraform 可能暴露敏感变量。
如果使用资源属性中的敏感值作为输出值的一部分,Terraform 将要求将输出值本身标记为 sensitive
,以确认确实打算将其导出。
Terraform 仍会在状态中记录敏感值,因此任何可以访问状态数据的人都可以以明文形式访问敏感值。
注意:Terraform 从 v0.15 开始将从敏感资源属性派生的值视为敏感值本身。早期版本的 Terraform 将隐藏敏感资源属性的直接值,但不会自动隐藏从敏感资源属性派生的其他值。
1.4.6.1.6. 资源的依赖关系
我们在介绍输出值的depends_on
的时候已经简单介绍过了依赖关系。一般来说在Terraform代码定义的资源之间不会有特定的依赖关系,Terraform可以并行地对多个无依赖关系的资源执行变更,默认情况下这个并行度是10。
然而,创建某些资源所需要的信息依赖于另一个资源创建后输出的属性,又或者必须在某些资源成功创建后才可以被创建,这时资源之间就存在依赖关系。
大部分资源间的依赖关系可以被Terraform自动处理,Terraform会分析resource块内的表达式,根据表达式的引用链来确定资源之间的引用,进而计算出资源在创建、更新、销毁时的执行顺序。大部分情况下,我们不需要显式指定资源之间的依赖关系。
然而,有时候某些依赖关系是无法从代码中推导出来的。例如,Terraform必须要创建一个访问控制权限资源,以及另一个需要该权限才能成功创建的资源。后者的创建依赖于前者的成功创建,然而这种依赖在代码中没有表现为数据引用关联,这种情况下,我们需要用depends_on
来显式声明这种依赖关系。
1.4.6.1.7. 元参数
resource块支持几种元参数声明,这些元参数可以被声明在所有类型的resource块内,它们将会改变资源的行为:
- depends_on:显式声明依赖关系
- count:创建多个资源实例
- for_each:迭代集合,为集合中每一个元素创建一个对应的资源实例
- provider:指定非默认Provider实例
- lifecycle:自定义资源的生命周期行为
- provisioner 和 connection:在资源创建后执行一些额外的操作
下面我们将逐一讲解他们的用法。
1.4.6.1.7.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.7.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)。count
的表达式中可以引用来自data返回的输出属性,只要该data可以不依赖任何其他resource进行查询。
1.4.6.1.7.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.7.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}"
}
}
这种实现方法是脆弱的,因为资源仍然是以他们的下标而不是实际的string值来区分的。如果我们从subnet_ids
列表的中间移除了一个元素,那么从该位置起后续所有的aws_instance
都会发现它们的subnet_id
发生了变化,结果就是所有后续的aws_instance
都需要更新。这种场景下如果使用for_each
就更为妥当,如果使用for_each
,那么只有被移除的subnet_id
对应的aws_instance
会被销毁。
1.4.6.1.7.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.7.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.7.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
块是用来检查其他什么地方定义的对象的,从而允许 Terrform 以正确的顺序执行操作。
1.4.6.1.7.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.8. 创建时预置器
默认情况下,资源对象被创建时会运行预置器,在对象更新、销毁时则不会运行。预置器的默认行为时为了引导一个系统。
如果创建时预置器失败了,那么资源对象会被标记污点(我们将在介绍terraform taint
命令时详细介绍)。一个被标记污点的资源在下次执行terraform apply
命令时会被销毁并重建。Terrform的这种设计是因为当预置器运行失败时标志着资源处于半就绪的状态。由于Terraform无法衡量预置器的行为,所以唯一能够完全确保资源被正确初始化的方式就是删除重建。
我们可以通过设置on_failure
参数来改变这种行为。
1.4.6.1.9. 销毁时预置器
如果我们设置预置器的when
参数为destroy
,那么预置器会在资源被销毁时执行:
resource "aws_instance" "web" {
# ...
provisioner "local-exec" {
when = destroy
command = "echo 'Destroy-time provisioner'"
}
}
销毁时预置器在资源被实际销毁前运行。如果运行失败,Terraform会报错,并在下次运行terraform apply
操作时重新执行预置器。在这种情况下,需要仔细关注销毁时预置器以使之能够安全地反复执行。
销毁时预置器只有在存在于代码中的情况下才会在销毁时被执行。如果一个resource块连带内部的销毁时预置器块一起被从代码中删除,那么被删除的预置器在资源被销毁时不会被执行。要解决这个问题,我们需要使用多个步骤来绕过这个限制:
- 修改资源声明代码,添加
count = 0
参数 - 执行
terraform apply
,运行删除时预置器,然后删除资源实例 - 删除resource块
- 重新执行
terraform apply
,此时应该不会有任何变更需要执行
该限制在未来将会得到解决,但目前来说我们必须节制使用销毁时预置器。
1.4.6.1.10. 预置器失败行为
默认情况下,预置器运行失败会导致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.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"
}
}
可配置超时的操作类别由每种支持超时设定的资源类型自行决定。大部分资源类型不支持设置超时。使用超时前请先查阅相关文档。