简介
Hashicorp 配置语言(HCL),是 Terraform 所使用的,提供了许多在其他编程语言中也存在的有用结构和功能。在基础设施代码中使用循环可以大大减少代码重复,并提高可读性,使得未来的重构更加容易,增加了更大的灵活性。HCL 还提供了一些常见数据结构,比如列表和映射(在其他语言中也称为数组和字典),以及用于执行路径分叉的条件。
Terraform 独特之处在于,可以手动指定一个资源所依赖的其他资源。虽然在你运行代码时它构建的执行图已经包含了检测到的链接(在大多数场景下都是正确的),但你可能发现自己需要强制一个 Terraform 未能检测到的依赖关系。
在这篇文章中,我们将回顾 HCL 提供的数据结构,其针对资源(count
键,for_each
和 for
)的循环特性,处理已知和未知值的逻辑条件,以及资源之间的依赖关系。
先决条件
- DigitalOcean的个人访问令牌,您可以通过DigitalOcean控制面板创建。您可以在DigitalOcean产品文档中找到创建个人访问令牌的说明,如何在DigitalOcean中创建个人访问令牌。
- 在您的本地机器上安装Terraform,并设置了一个使用DigitalOcean提供者的项目。完成第1步和第2步的如何使用Terraform与DigitalOcean教程,并确保将项目文件夹命名为
terraform-flexibility
,而不是loadbalance
。在第第2步中,配置提供者时不需要包括pvt_key
变量和SSH密钥资源。
注意:本教程已针对Terraform 1.0.2
进行了特别测试。
HCL中的数据类型
在深入了解循环和其他使您的代码更具灵活性的HCL特性之前,我们首先介绍可用的数据类型及其用途。
HashiCorp配置语言支持原始和复杂数据类型。原始数据类型包括字符串、数字和布尔值,这些都是不能从其他类型派生出的基本类型。复杂类型则将多个值组合成一个。复杂值的两种类型是结构类型和集合类型。
结构类型允许不同类型的值组合在一起。主要的例子是你用来指定基础设施外观的资源定义。与结构类型相比,集合类型也组合值,但只组合相同类型的值。HCL中我们感兴趣的三种集合类型是列表、映射和集合。
列表
列表在其他编程语言中的数组类似。它们包含已知数量的相同类型的元素,可以使用数组表示法([]
)通过它们从0开始的整数索引来访问。以下是一个例子,声明了一个列表变量,它将包含在下一步中部署的Droplets的名称:
对于type
,您指定了它是一个元素类型为字符串的列表,然后提供了它的default
值。在HCL中,方括号中枚举的值表示一个列表。
映射
地图是由键值对组成的集合,每个值都是通过其键(类型为 string
)来访问的。在花括号内指定地图有两种方法:使用冒号(:
)或等号(=
)来指定值。在这两种情况下,值都必须用引号括起来。使用冒号时,键也必须被括起来。
以下包含不同环境下的Droplet名称的地图定义是使用等号写的:
如果键以数字开头,您必须使用冒号语法:
集合
集合不支持元素排序,这意味着遍历集合不能保证每次都产生相同的顺序,并且无法以目标方式访问它们的元素。它们包含唯一元素,重复一次,并且指定相同元素的多次将导致它们在集合中合并,只存在一个实例。
声明集合与声明列表类似,唯一的区别是变量的类型:
现在您已经了解了HCL提供的数据结构类型,并回顾了列表、地图和集合的语法,这些我们在本教程中将会使用,接下来您将尝试一些在Terraform中部署相同资源多个实例的灵活方法。
使用`count`键设置资源数量
在本节中,您将通过使用`count`键创建相同资源的多个实例。`count`键是所有资源上都可用的参数,它指定要创建的实例数量。
您将通过编写一个Droplet资源来了解它是如何工作的,并将该资源保存在名为`droplets.tf`的文件中,该文件位于您在先决条件中创建的项目目录中。通过运行以下命令创建并打开它进行编辑:
添加以下行:
这段代码定义了一个名为`test_droplet`的Droplet资源,它运行Ubuntu 20.04,配备1GB RAM和1个CPU核心。
请注意`count`的值被设置为`3`,这意味着Terraform将尝试创建相同资源的三个实例。完成后,保存并关闭文件。
您可以运行以下命令来规划项目,以查看Terraform将采取哪些操作:
输出将与以下类似:
翻译如下:
Terraform 将创建三个名为 test_droplet
的实例,所有实例名称都相同。虽然可以这样做,但通常不建议这样做,因此让我们修改 Droplet 的定义,使每个实例的名称都是唯一的。打开 droplets.tf
文件进行编辑:
修改以下高亮行:
保存并关闭文件。
使用 count
对象的 index
参数,该参数包含当前迭代的索引,从 0 开始。当前索引用于将变量动态地替换为 Droplet 的名称中,使用 字符串插值,允许您动态构建一个字符串。您可以再次计划项目以查看更改:
输出将类似于此:
这次,test_droplet
的三个实例将具有它们的索引在其名称中,使其更容易跟踪。
现在您已经知道如何使用 count
键创建多个资源实例,以及如何在配置期间获取和使用实例的索引。接下来,您将学习如何从列表中获取Droplet的名称。
从列表中获取Droplet名称
当需要为同一资源的多实例设置自定义名称时,您可以动态地从您定义的列表变量中检索它们。在本教程的剩余部分,您将看到几种自动化从名称列表部署Droplet的方法,这使得部署更加灵活和易于使用。
您首先需要定义一个包含Droplet名称的列表。创建一个名为variables.tf
的文件并打开它以进行编辑:
添加以下行:
保存并关闭文件。这段代码定义了一个名为droplet_names
的列表,其中包含字符串first
、second
、third
和fourth
。
打开droplets.tf
以进行编辑:
修改突出显示的行:
为了提高灵活性,您不是手动指定一个常数数量的元素,而是将droplet_names
列表的长度传递给count
参数,该参数将始终返回列表中的元素数量。对于名称,您使用数组括号表示法获取列表中位于count.index
位置的元素。完成后保存并关闭文件。
再次尝试规划项目。您将收到与以下类似的输出:
由于这些修改,将部署四个Droplet,依次以droplet_names
列表中的元素命名。
您已经学习了count
的作用、特点和语法,并且已经将其与列表一起使用来修改资源实例。接下来,您将了解它的缺点以及如何克服它们。
了解count
的缺点
现在您已经知道了count的使用方法,让我们来研究一下在修改它所使用的列表时它的缺点。
让我们尝试将Droplet部署到云上:
当提示时输入yes
。您的输出末端将与以下类似:
OutputApply complete! Resources: 4 added, 0 changed, 0 destroyed.
现在,让我们通过扩大droplet_names
列表再创建一个Droplet实例。打开variables.tf
进行编辑:
在列表的开始处添加一个新元素:
完成后,保存并关闭文件。
项目计划:
您将收到如下输出:
输出显示,Terraform会将前四个Droplets重命名,并创建一个名为“fourth”的第五个Droplet,因为Terraform将实例视为有序列表,并通过索引号识别元素(Droplets)。这就是为什么Terraform最初认为四个Droplets是这样的:
Index Number | 0 | 1 | 2 | 3 |
---|---|---|---|---|
Droplet Name | first | second | third | fourth |
当在开头添加一个新的Droplet“zero”时,其内部列表表示法看起来像这样:
Index Number | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
Droplet Name | zero | first | second | third | fourth |
最初的四个Droplets现在向右移动了一个位置。然后Terraform比较两个状态表:第一个表中位置为“0”的Droplet名为first,而在第二个表中它不同,因此计划进行更新操作。这个过程一直持续到位置“4”,该位置在第一个表中没有对应的元素,而是计划进行Droplet创建操作。
这意味着在任何地方添加新元素(除了列表的末尾)都会导致在不必要的情况下修改资源。如果从droplet_names
列表中删除一个元素,也会计划类似的更新操作。
使用count
来部署相同资源的不同实例的动态数量的主要缺点是不完整的资源跟踪。对于常数数量和常数实例,count
是一个简单且有效的解决方案。但在这种情况下,当一些属性从一个变量中获取时,本教程后面会介绍的for_each
循环是更好的选择。
引用当前资源(self
)
count
的另一个缺点是在某些情况下,通过索引引用资源的一个任意实例是不可能的。
主要例子是destroy-time provisioners,它会在资源计划被销毁时运行。原因是在请求的实例可能不存在(它已经被销毁)或者会创建一个相互依赖的循环。在这种情况下,通过实例列表访问对象,只能通过self
关键字访问当前资源。
为了展示其用法,现在您将向 test_droplet
定义添加一个 destroy-time 本地提供程序,该定义将在运行时显示一条消息。打开 droplets.tf
进行编辑:
添加以下高亮行:
保存并关闭文件。
使用 local-exec
提供程序在运行 Terraform 的本地机器上执行命令。因为 when
参数设置为 destroy
,它将在资源被销毁时运行。它运行的命令使用当前资源的名称(通过 self.name
)替换到输出中。
由于您将在下一部分以不同的方式创建 Droplet,因此需要销毁当前部署的所有实例。运行下面的命令:
输入 yes
回答提示。您将收到四次 local-exec
提供程序的运行:
在本节中,您学习了 count
的缺点。接下来,您将学习如何使用 for_each
循环结构,它克服了这些缺点,并且适用于更广泛的变量类型。
使用 for_each
循环
在这一节中,您将考虑 for_each
循环,它的语法以及它是如何帮助定义具有多个实例的资源的灵活性的。
`for_each` 是每个资源上都可用的一个参数,但与需要创建实例数量的 `count` 不同,`for_each` 接受一个映射或一个集合。提供的每个集合元素都会被遍历一次,并为它创建一个实例。`for_each` 使得键和值在 `each` 关键字下作为属性可用(键值对的键和值分别为 `each.key` 和 `each.value`)。当提供一个集合时,键和值将是相同的。
因为它在 `each` 对象中提供了当前元素,所以你不需要像使用列表时那样手动访问所需的元素。在集合的情况下,这是不可能的,因为集合内部没有可观察的顺序。也可以传递列表,但首先必须使用 `toset` 函数将其转换为集合。
使用 `for_each` 的主要优点是,除了能够枚举所有三种集合数据类型之外,只有受影响的元素才会被修改、创建或删除。如果你改变了输入元素的结构,将不会计划任何操作,如果你从输入中添加、删除或修改一个元素,只有那个元素会计划相应的操作。
让我们将 Droplet 资源从 `count` 转换为 `for_each`,看看它在实际中是如何工作的。通过运行以下命令编辑 `droplets.tf`:
修改高亮显示的行:
你可以移除 `local-exec` 供应器。完成后,保存并关闭文件。
首先,第一行用count
替换了for_index
,并调用了for_each
,将droplet_names
列表以集合的形式传递给toset
函数,该函数自动将给定的输入转换为集合。对于Droplet名称,您指定each.value
,它持有当前集合元素的值。
规划项目时运行:
输出将详细说明Terraform将采取的措施:
与使用count
不同,Terraform现在考虑每个实例都是单独的,而不是有序列表中的元素。每个实例都与给定集合中的一个元素相关联,如每个资源旁边显示的字符串元素所示,这些资源将被创建。
应用计划到云中,运行:
输入yes
然后提示。当它完成时,您将从droplet_names
列表中删除一个元素,以证明其他实例不会受到影响。打开variables.tf
进行编辑:
修改列表如下:
保存并关闭文件。
再次计划项目,您将收到以下输出:
这一次,Terraform只会销毁删除的实例(zero
),而不会影响其他任何实例,这是正确的操作。
在这一步中,您学习了关于for_each
的使用方法、它的优点以及如何正确地使用它。接下来,您将学习for
循环的语法和用法,以及在哪些情况下可以使用它来自动化某些任务。
使用for
循环
for
循环根据输入创建一个新的集合,并对该集合中的每个元素应用一个转换函数。这个循环的具体类型取决于是否被方括号([]
)或花括号({}
)包围,这将决定输出的数据类型——如果是列表的话。因此,它适用于查询资源并在稍后处理结构化输出。
for
循环的一般语法如下:
与其他编程语言类似,您首先命名遍历变量(element
),然后指定要枚举的集合。循环的主体是转换步骤,可选的if
子句可以用于过滤输入集合。
您现在将使用以下示例进行说明。您将在名为outputs.tf
的文件中存储输出。创建该文件以供编辑,可以使用以下命令:
添加以下行以输出部署的Droplet名称及其IP地址:
此代码指定了一个名为ip_addresses
的输出,并指定了一个对test_droplet
资源的实例进行迭代的for
循环。由于循环被花括号包围,其输出将是一个映射。地图的转换步骤与其它编程语言中的lambda函数相似,这里它通过将实例名作为键,私有IP作为值来创建键值对。
保存并关闭文件,然后运行以下命令刷新Terraform状态以反映新输出的内容:
Terraform的refresh
命令更新本地状态以反映云中的实际基础设施状态。
然后,检查输出内容:
Terraform 显示了 ip_addresses
输出的内容,这是一个由 for
循环构建的映射(条目的顺序可能对您来说有所不同)。循环将无缝地为每个条目数工作——这意味着您可以向 droplet_names
列表中添加一个新元素,新创建的 Droplet 将无需任何进一步的手动输入,也会自动显示在此输出中。
通过在 for
循环周围加上方括号,您可以使输出成为一个列表。例如,您可以仅输出 Droplet IP 地址,这对于可能解析数据的外部软件很有用。代码如下所示:
在这里,转换步骤选择了 IP 地址属性。它将给出以下输出:
如前所述,您还可以使用 if
子句过滤输入集合。以下是按 fra1
区域过滤循环的写法:
在 HCL 中,==
运算符检查两边的值的等价性——这里它检查 instance.region
是否等于 fra1
。如果是,检查通过并将 instance
转换并添加到输出,否则跳过。此代码的输出将与前一个示例相同,因为根据 test_droplet
资源定义,所有 Droplet 实例都在 fra1
区域。当您想根据项目中的其他值(如 Droplet 的大小或发行版)过滤输入集合时,if
条件也很有用。
因为接下来您将使用不同的方式创建资源,所以运行以下命令销毁当前部署的资源:
输入yes
来完成该过程。
我们已经学习了for
循环、其语法以及输出的示例。现在您将学习条件语句以及如何将其与count
一起使用。
指令和条件
在之前的章节中,您已经了解了count
键及其用法。现在您将学习三元条件运算符,以及如何在您的Terraform代码中与其他count
一起使用。
三元操作符的语法如下:
condition
是一个计算结果为布尔值(true或false)的表达式。如果条件为真,则表达式的值为value_if_true
;否则,结果为value_if_false
。
三元操作符的主要用途是根据变量的内容启用或禁用单个资源的创建。这可以通过将比较的结果(要么1
,要么0
)传递给所需的资源的count
键来实现。
在使用三元操作符从列表或集中获取单个元素时,可以使用 one
函数。如果给定的集合是空的,它返回 null
。否则,它返回集合中的单个元素,或者抛出一个错误,如果存在多个元素。
现在让我们添加一个变量叫做 create_droplet
,这个变量将控制是否创建Droplet。首先,打开 variables.tf
进行编辑:
添加高亮的行:
此代码定义了 create_droplet
变量为 bool
类型。保存并关闭文件。
然后,要修改Droplet声明,打开 droplets.tf
进行编辑,运行:
像下面这样修改你的文件:
对于 count
,你使用了一个三元操作符来返回,如果是 true
就返回 1
,如果是 false
就返回 0
,这将导致没有Droplets被创建。保存并关闭文件。
使用设置为false的变量执行项目计划,运行:
您将收到以下输出:
因为 create_droplet
被传递为 false
,count
的实例是 0
,所以不会创建任何Droplet,因此不会有IP地址可以输出。
您已经了解了如何使用三元条件运算符与 count 键一起使用,以启用在选择是否部署所需资源时更高的灵活性。接下来,您将学习如何显式设置资源依赖关系。
显式设置资源依赖关系
当创建项目的执行计划时,Terraform 会检测资源之间的依赖链,并隐式地按适当顺序构建它们。在大多数情况下,它能够通过扫描所有资源的表达式来检测这些关系并构建一个图。
但是,如果一个资源需要访问云提供商预先部署的控制设置才能进行配置,那么Terraform就无法明确知道它们之间存在行为上的依赖关系。因此,Terraform不会自动将它们连接起来。在这种情况下,必须手动指定依赖关系,使用depends_on
参数。
每个资源都具有depends_on
属性,用于显式指定两个特定资源之间的隐藏依赖关系。这种依赖关系形成于一个资源依赖于另一个资源的行为,而无需在其声明中使用其数据,这会提示Terraform连接它们。
下面是一个如何在代码中指定depends_on
的示例:
它接受其他资源的引用列表,不接受任意表达式。
depends_on
应该谨慎使用,并且只应在所有其他选项都耗尽时使用。它的使用意味着您尝试声明的内容超出了 Terraform 的自动依赖检测系统;它可能意味着您正在尝试声明的是依赖于比实际需要的更多的资源。
您现在已经了解了如何使用 depends_on
键显式地为资源设置附加依赖项,以及何时需要这样做。
结论
在本文中,我们讨论了改进您的代码灵活性和可伸缩性的 HCL 功能,例如用于指定要部署的资源实例数量的 count
以及用于循环集合数据类型并自定义实例的更高级方法。当正确使用时,它们大大减少了管理已部署基础设施的工作量。
您还学习了条件语句和三元运算符,以及如何利用它们来控制资源是否被部署。虽然 Terraform 的自动化依赖分析系统相当强大,但有时您可能需要手动指定资源之间的依赖关系,使用 depends_on
键。
本教程是《如何使用Terraform管理基础设施》系列的一部分。该系列涵盖了多个Terraform主题,从安装Terraform到首次使用,再到管理复杂项目。