Terraform 文件与目录
Terraform 文件与目录
1.5.1. Terraform 文件与目录
Terraform 语言的代码存储在文件扩展名为 .tf
的纯文本文件中。该语言还有一种基于 JSON 的变体,以 .tf.json
文件扩展名命名(本书精力所限将不涉及这方面的内容)。 包含 Terraform 代码的文件通常称为配置文件。
1.5.1.1. 文本编码
配置文件必须始终使用 UTF-8 编码,并且按照惯例通常使用 Unix 样式行结束符 (LF),而不是 Windows 样式行结束符 (CRLF),尽管两者都被接受。
1.5.1.2. 文件夹与模块
同一个文件夹中一起保存的一组 .tf
以及 .tf.json
文件组成一个模块。
Terraform 模块仅由直接隶属于该目录的配置文件组成;子目录被视为完全独立的模块,并且不会自动包含在配置中。
Terraform 执行模块中的所有配置文件,最终将整个模块视为单个代码文件。将各个块分成不同的文件纯粹是为了方便读者和维护者,对模块的行为没有影响。
Terraform 模块可以使用模块调用将其他模块显式包含到配置中。这些子模块可以来自本地目录(嵌套在父模块的目录中,或磁盘上的任意其他位置),也可以来自 Terraform 注册表等外部源。
1.5.1.3. 根模块
Terraform 始终在单一的根模块的上下文中运行。完整的 Terraform 配置由根模块和子模块树组成(其中包括根模块调用的模块、以及由这些模块调用的任何模块等)。
-
在 Terraform 命令行 中,根模块是执行 Terraform 命令的工作目录。 (我们可以使用命令行参数来指定不同于当前工作目录的根模块,但实际上这种情况很少见。)
-
在 HCP Terraform 和 Terraform Enterprise 中,工作区的根模块默认为配置目录的顶层(通过版本控制存储库或直接上传提供),但工作区设置可以指定要使用的子目录。
1.5.1.1. 重载(Override) 文件
Terraform 通常会加载目录中的所有 .tf
和 .tf.json
文件,并假设每个文件中配置的对象是不同的。如果两个文件尝试定义同一对象,Terraform 将返回错误。
在某些极少数情况下,能够很方便地重载某一文件中声明配置的对象的指定部分。例如,可以使用 JSON 语法以编程方式生成的文件部分重载那些使用 HCL 语法人工编辑的 Terraform 配置文件。
对于这些不常见的场景,Terraform 对名称以 _override.tf
或 _override.tf.json
结尾的配置文件进行特殊处理。这种特殊处理也适用于字面名称为 override.tf
或 override.tf.json
的文件。
Terraform 最初在加载配置时跳过这些重载文件,然后依次处理每个文件(按字典顺序)。对于重载文件中定义的每个顶级块,Terraform 尝试查找与该块对应的已定义对象,然后将重载块的内容合并到现有对象中。
请确保仅在特殊情况下使用重载文件。过度使用重载文件会损害可读性,因为仅查看原始文件的读者无法在不查阅所有重载文件的情况下轻松地认识到这些文件的某些部分已被重载。使用重载文件时,请在原文件中对重载文件修改的每一个块添加注释警告。
1.5.1.1.1. 示例
假设我们有一个名为 example.tf
的文件中包含如下内容:
1 | resource "aws_instance" "web" { |
同时,创建名为 override.tf
的文件:
1 | resource "aws_instance" "web" { |
Terraform 会将后者合并到前者中,其行为就好比我们一开始就写成这样:
1 | resource "aws_instance" "web" { |
1.5.1.1.2. 合并规则
不同的块类型所对应的合并行为略有不同,某些块内的一些特殊结构以特殊方式合并。
适用于大多数情况的一般规则是:
- 重载文件中的顶级块与具有相同块头的普通配置文件中的块合并。块头是块类型及其后面的所有标签。
- 顶级块中的属性值将被替换为重载块中的同名属性值。
- 顶级块中的内嵌块全部都会被重载块中相同类型的块替换。任何未出现在重载块中的块类型仍保留在原始块中。
- 内嵌块的内容不会合并。
- 合并后的块仍必须符合所有适用于该类型块的验证规则。
如果多个重载文件定义相同的顶级块,则重载效果会叠加,后面的块优先于前面的块。重载首先按文件名(按字典顺序)然后按每个文件中的位置进行处理。
以下部分描述了适用于某些顶级块类型中的特定参数的特殊合并行为。
1.5.1.1.2.1. 合并 resource 和 data 块
在 resource
块内,所有 lifecycle
块的内容都会按参数进行合并。例如,如果重载块仅设置了 create_before_destroy
参数,则原始块中 ignore_changes
参数会被保留。
如果重载 resource
块包含一个或多个 provisioner
块,则原始块中的所有 provisioner
块都将被忽略。
如果重载 resource
块包含 connection
块,则它完全取代原始块中存在的所有 connection
块。
不允许在重载块中声明 depends_on
元参数,那将会返回错误。
1.5.1.1.2.2. 合并 variable 块
variable
块内的参数按照上述的标准方式合并,但由于 type
和 default
参数之间存在相互作用,有一些特殊规则。
如果原始块定义了 default
值并且重载块更改了变量的 type
,Terraform 会尝试将默认值转换为重载的类型,如果无法进行这样的转换,则会产生错误。
相反,如果原始块定义 type
并且重载块更改 default
值,则重载的默认值必须与原始块定义的类型兼容。
1.5.1.1.2.3. 合并 output 块
不允许在重载块中声明 depends_on
元参数,那将会返回错误。
1.5.1.1.2.4. 合并 locals 块
每个 locals
块定义了键值对。重载是在逐个值进行的,不论它们定义在哪个 locals
块中。
1.5.1.1.2.5. 合并 terraform 块
terraform
块内的配置在合并时会有不同的规则。
如果设置了 required_providers
参数,则其值将以 Provider 的尺度进行合并,这允许重载块调整单个 Provider 的约束,而不影响其他 Provider 的约束。
在 required_version
和 required_providers
设置中,所有重载的约束都会彻底替换原始块中同一组件的约束。如果原始块和重载块都设置了 required_version
,则原始块中的约束将被彻底忽略。
重载文件中定义 Backend(cloud
或 backend
)的块始终优先于原始配置中定义 Backend 的块。也就是说,如果在原始配置中设置了 cloud
块,并在重载文件中设置了 backend
块,则 Terraform 将在合并时使用重载文件中指定的 backend
块。同样,如果在原始配置中设置了 backend
块,并且在重载文件中设置了 cloud
块,则 Terraform 将在合并时使用重载文件中指定的 cloud
块。
1.5.2.1. 依赖锁(Dependency Lock)文件
注意:该功能是自 Terraform 0.14 起引入的一项功能。 Terraform 的更早期版本不跟踪依赖项,因此本节与这些早期版本无关。
Terraform 配置文件中可以引用来自其自身代码库之外的两种不同类型的外部依赖项:
这两种类型的依赖都可以独立于 Terraform 本身以及依赖于它们的代码进行发布和更新。因此,Terraform 必须确定这些依赖项的哪些版本可能与当前配置兼容以及当前选择使用哪些版本。
配置本身内的版本约束决定了哪些版本的依赖项可能与当前配置兼容,但在为每个依赖项选定的特定版本后,Terraform 会记住它在依赖锁文件中所做的决定,以便它可以(默认情况下)在将来再次做出相同的决定。
目前,依赖锁文件仅追踪 Provider 的依赖项。 Terraform 不会保存远程模块的版本选择,因此 Terraform 将始终选择满足指定版本约束的最新可用模块版本。我们可以使用精确的版本约束来确保 Terraform 始终选择相同版本的模块。
1.5.2.1.1. 锁文件的位置
依赖锁文件是记录的是整个配置的依赖,而不是配置代码中使用每个单独的模块的依赖。因此,Terraform 在运行 Terraform 时的当前工作目录中创建该文件,并假设该文件存在于此,同时该目录也包含了配置根模块的 .tf
文件的。
依赖锁文件的名字始终是 .terraform.lock.hcl
,此名称旨在表示它是 Terraform 缓存在工作目录的 .terraform
子目录中的各种项目的锁定文件。
每次运行 terraform init
命令时,Terraform 都会自动创建或更新依赖项锁定文件。我们应该将此文件包含在版本控制存储库中,以便我们可以在代码审查过程中对那些外部依赖项的更改进行审查,就像对配置本身的更改进行审查一样。
依赖锁定文件使用与 Terraform 语言相同的语法,但依赖锁定文件本身并不是 Terraform 语言配置文件。它以后缀 .hcl
而不是 .tf
命名,以显示这种差异。
1.5.2.1.2. 安装依赖的行为
当 terraform init
正在安装配置所需的所有 Provider 程序时,Terraform 会同时参考配置中的版本约束以及锁定文件中记录的版本选择。
如果某个 Provider 程序没有在锁文件中找到对应的记录,Terraform 将选择与给定版本约束匹配的最新可用版本,然后更新锁定文件以保存该选择。
如果某个 Provider 程序已在锁文件中保存了一个版本选择记录,Terraform 将始终重新选择该版本进行安装,即使有更新的版本可用。我们可以通过在运行 terraform init
时添加 -upgrade
选项来重载该行为,在这种情况下,Terraform 将忽略现有选择并再次选择与版本约束匹配的最新可用版本。
如果某次 terraform init
调用对锁文件进行了更改,Terraform 会在其输出中提到这一点:
1 | Terraform has made some changes to the provider dependency selections recorded |
当我们看到这样的消息时,我们可以使用版本控制系统来查看 Terraform 提到的那些在文件中的变更,如果变更的确是我们有意为之的,那我们可以将这些变更发送至团队的常规代码审核流程进行审查。
1.5.2.1.2.1. 验证校验和
Terraform 还将验证它安装的每个包是否与之前在锁定文件中记录的校验和至少一个相匹配(如果有),如果没有校验和匹配,则返回错误:
1 | Error: Failed to install provider |
此校验和验证采用“初次使用时信任”原则。当我们第一次添加新的 Provider 程序时,我们可以通过各种方式或相关法规要求的所有方法对其进行验证,然后确信如果未来运行 terraform init
时,在安装相同 Provider 程序时遇到不匹配的包时 Terraform 会返回错误。
“初次使用时”模型有两个特殊考虑因素:
- 如果我们从源注册表安装 Provider 程序时,该 Provider 程序提供了使用加密签名算法签名的校验和,那么只要有一个校验和匹配,Terraform 就会将所有已签名的校验和视为有效。因此,锁文件将包含该 Provider 所适用的所有平台的所有不同包的校验和。
在这种情况下,terraform init
的输出将包括对校验和进行签名的密钥的指纹,以及像这样的信息 (signed by a HashiCorp partner, key ID DC9FC6B1FCE47986)
。在提交包含签名校验和的锁文件之前,最好先确认我们能够信任给该密钥的持有者,或者检索并验证指定 Provider 版本的所有可用包的集合。
- 如果我们首次安装 Provider 时使用的是替代安装方法(例如文件系统或网络镜像),Terraform 将无法验证除运行
terraform init
的平台之外的其他所有平台的校验和,因此它不会记录其他平台的校验和,导致该配置将无法在任何其他平台上使用。
为了避免此问题,您可以使用 terraform providers lock
命令在锁文件中预先记录各种不同平台的校验和,这样我们将来可以调用 terraform init
命令来验证我们选择的镜像中可用的包是否与该 Provider 源注册表提供的官方包相匹配。
1.5.2.1.3. 理解锁文件的变更
由于依赖锁文件主要由 Terraform 本身自动维护,而不是手动更新,因此我们的版本控制系统可能会向我们显示该文件已更改。
Terraform 可能会对我们的锁文件进行几种不同类型的变更,我们可能需要了解这些更改才能查看建议的变更。以下各节将描述这些常见变更。
1.5.2.1.3.1. 新 Provider 的依赖
如果我们向配置中任意模块的 Provider 的声明配置节中添加新条目,或者添加了包含新 Provider 程序依赖项的外部模块,terraform init
将通过选择满足配置中所有版本约束的最新版本的 Provider 程序,并将这条决策保存为依赖锁文件中的一个新的 Provider 程序块。
1 | --- .terraform.lock.hcl 2020-10-07 16:12:07.539570634 -0700 |
新的锁文件条目记录了几条信息:
version
:Terraform 根据配置中的版本约束所选择的版本。constraints
:Terraform 在进行此选择时所遵守的所有的版本约束。 (Terraform 实际上并不使用此信息来做出安装决策,而是保存该信息以帮助向人类读者解释之前的决策是如何做出的。)hashes
:给定 Provider 程序在不同平台上有效的安装包所对应的一组校验和。这些哈希值的含义在下面的新 Provider 程序包校验和中有进一步的解释。
1.5.2.1.3.2. 现存 Provider 的新版本
如果我们运行 terraform init -upgrade
来命令 Terraform 在遵守配置的版本约束匹配的前提下将 Provider 升级到更新的版本,那么 Terraform 可能会为 Provider 程序选择较新的版本并更新其现有的 Provider 程序块,以体现该变更。
1 | --- .terraform.lock.hcl 2020-10-07 16:44:25.819579509 -0700 |
选择新的 Provider 程序版本的对锁文件进行的修改主要是更改了 provider
块中 version
的值。如果升级伴随着对配置文件中声明的版本约束的变更,Terraform 还将在 constraints
中记录该变更。
由于每个版本都有自己的一组分发包,因此切换到新版本也往往会替换 hashes
中的所有值,以体现新版本包的校验和。
1.5.2.1.3.3. 新 Provider 程序包校验和
我们可能在 provider
块中看到的一个很细微的变化是添加了以前未记录的新校验和,即使 provider
块中的所有其他内容都没有更改:
1 | --- .terraform.lock.hcl 2020-10-07 17:24:23.397892140 -0700 |
在 hashes
中添加的新的校验和代表 Terraform 在不同哈希方案之间逐渐过渡。这些值上的 h1:
和 zh:
前缀代表不同的哈希方案,每个方案都代表使用不同的算法计算校验和。如果现有方案存在某种局限性或者新方案提供了一些相当明显的收益,有时我们可能会引入新的哈希方案。
目前支持两种哈希方案:
zh:
: 代表“zip hash”,这是一种遗留哈希格式,是 Terraform Provider 程序注册表协议的一部分,因此用于直接从源注册表安装的 Provider 程序。
此哈希方案捕记录下注册表中索引的每个官方 .zip
包的 SHA256 哈希值。这是验证从注册表安装的官方发布包的有效方案,但它无法验证来自其他 Provider 程序安装方法的包,例如文件系统中指向解压后的安装包的镜像。
h1:
: 代表“hash schema 1”,它是当前首选的哈希方案。
Hash Schema 1 也是 SHA256 哈希,但它是根据提供 Provider 安装包内包含的内容计算得出的,而不是根据其包含的 .zip
文件的内容计算得出的。因此,该方案的优点是可以针对官方 .zip
文件、具有相同内容的解压目录或包含相同文件但可能具有不同元数据或压缩方案的重新压缩 .zip
文件计算出相同的校验和。
由于 zh:
方案的局限性,Terraform 将在计算出相应的 h1:
校验和时适时添加它们,这就是导致在上面所示的示例更改中添加第二个 h1:
校验和的原因。
Terraform 只会在安装包与已有哈希值之一相匹配时,才会把新计算出的哈希值添加到锁文件中。在上面的示例中,Terraform 在与生成原始 h1:
校验和的平台不同的平台安装了 hashcorp/azurerm
包,但能够匹配到之前记录的 zh:
校验和的其中之一,确认 zh:
校验和匹配后,Terraform 保存相应的 h1:
校验和,以便逐步从旧方案迁移到新方案。
首次安装某个 Provider 程序时(没有已有的 provider
块),Terraform 将使用 Provider 插件开发人员的加密签名(通常涵盖所有可用包)所涵盖适用于所有受支持平台上的该 Provider 程序版本校验集合来预先填充 hashes
。但是,由于 Provider 程序注册表协议仍然使用 zh:
方案,因此初始集将主要包含使用该方案的哈希值,然后当我们在不同平台上安装软件包时,Terraform 将适时升级该哈希值。
如果我们希望避免在新目标平台上使用配置时不断添加新的 h1:
哈希值,或者如果因为我们是从镜像安装的 Provider 程序,因此无法提供官方签名的校验和,您可以使用 terraform providers lock
命令要求 Terraform 预填充一组选定平台的哈希值:
1 | terraform providers lock \ |
上述命令将下载并验证所有四个给定平台上所有我们指定的 Provider 程序的官方软件包,然后在锁文件中记录每个软件包的 zh:
和 h1:
校验和,从而避免 Terraform 在之后只能得到 h1:
校验和这种情况。有关此命令的更多信息,请参阅 terraform providers lock
命令。
1.5.2.1.3.4. 那些不再被使用的 Provider
为了确定是否仍然存在对某个 Provider 程序的依赖,Terraform 使用两个事实来源:配置代码和状态。如果我们从配置和状态中删除对某个 Provider 程序的最后一个依赖项,则 terraform init
将从锁文件中删除该 Provider 的现存记录。
1 | --- .terraform.lock.hcl 2020-10-07 16:12:07.539570634 -0700 |
如果我们稍后为同一 Provider 程序添加新的引用并再次运行 terraform init
,Terraform 会将其视为全新的 Provider 程序,因此不一定会选择之前选择的相同版本,并且无法验证校验和是否保持不变。
注意:在 Terraform v1.0 及更早版本中,terraform init
不会自动从锁文件中删除现在不需要的 Provider 程序,而只是忽略它们。如果您之前使用早期版本的 Terraform 时删除了提供 Provider 依赖项,然后升级到 Terraform v1.1 或更高版本,那么您可能会看到错误“missing or corrupted provider plugins”,指向过时的锁文件条目。如果是这样,请使用新的 Terraform 版本运行 terraform init
以整理那些不需要的条目,然后重试之前的操作。
1.5.3.1. 测试文件
某些的 Terraform 命令(例如 test
、init
和 validate
)会针对配置代码加载 Terraform 测试文件。
测试文件包含 Terraform 测试执行的代码。有关 Terraform 测试命令的更多信息,请参阅命令:test
。有关语法和 Terraform 测试文件语言的更多信息,请参阅测试。
1.5.3.1.1. 文件扩展名
测试文件的扩展名是 .tftest.hcl
以及 .tftest.json
。
1.5.3.1.2. 测试文件的位置
Terraform 加载根模块目录中的所有测试文件。
Terraform 还会加载测试目录中的测试文件。我们可以通过在那些会加载测试文件的命令后添加 -test-directory
参数来修改测试目录的位置。默认测试目录是根模块目录下的 tests
目录。