1.3.1.1. Terraform 基础概念 —— Provider
Terraform 被设计成一个多云基础设施编排工具,不像 CloudFormation 那样绑定 AWS 平台,Terraform 可以同时编排各种云平台或是其他基础设施的资源。Terraform 实现多云编排的方法就是 Provider 插件机制。
Terraform 使用的是 HashiCorp 自研的 go-plugin
库),本质上各个 Provider 插件都是独立的进程,与 Terraform 进程之间通过 Rpc 进行调用。Terraform 引擎首先读取并分析用户编写的 Terraform 代码,形成一个由 data
与 resource
组成的图(Graph),再通过 Rpc 调用这些 data
与 resource
所对应的 Provider 插件;Provider 插件的编写者根据 Terraform 所制定的插件框架来定义各种 data
和 resource
,并实现相应的 CRUD 方法;在实现这些 CRUD 方法时,可以调用目标平台提供的 SDK,或是直接通过调用 Http(s) API来操作目标平台。
1.3.1.1.1. 下载 Provider
我们在第一章的小例子中,写完代码后在 apply
之前,首先我们执行了一次terraform init
。terraform init
会分析代码中所使用到的 Provider,并尝试下载 Provider 插件到本地。如果我们观察执行完第一章例子的文件夹,我们会发现有一个 .terraform
文件夹,我们所使用的 AWS Provider 插件就被下载安装在里面。
.terraform
└── providers
└── registry.terraform.io
└── hashicorp
└── aws
└── 5.37.0
└── windows_amd64
└── terraform-provider-aws_v5.37.0_x5.exe
有的时候下载某些 Provider 会非常缓慢,或是在开发环境中存在许多的 Terraform 项目,每个项目都保有自己独立的插件文件夹非常浪费磁盘,这时我们可以使用插件缓存。
有两种方式可以启用插件缓存:
第一种方法是配置 TF_PLUGIN_CACHE_DIR
这个环境变量:
export TF_PLUGIN_CACHE_DIR="$HOME/.terraform.d/plugin-cache"
第二种方法是使用 CLI 配置文件。Windows 下是在相关用户的 %APPDATA%
目录下创建名为 "terraform.rc"
的文件,Macos 和 Linux 用户则是在用户的 home
下创建名为 ".terraformrc"
的文件。在文件中配置如下:
plugin_cache_dir = "$HOME/.terraform.d/plugin-cache"
当启用插件缓存之后,每当执行 terraform init
命令时,Terraform 引擎会首先检查期望使用的插件在缓存文件夹中是否已经存在,如果存在,那么就会将缓存的插件拷贝到当前工作目录下的 .terraform
文件夹内。如果插件不存在,那么 Terraform 仍然会像之前那样下载插件,并首先保存在插件文件夹中,随后再从插件文件夹拷贝到当前工作目录下的 .terraform
文件夹内。为了尽量避免同一份插件被保存多次,只要操作系统提供支持,Terraform 就会使用符号连接而不是实际从插件缓存目录拷贝到工作目录。
需要特别注意的是,Windows 系统下 plugin_cache_dir
的路径也必须使用 /
作为分隔符,应使用 C:/somefolder/plugin_cahce
而不是 C:\somefolder\plugin_cache
Terraform 引擎永远不会主动删除缓存文件夹中的插件,缓存文件夹的尺寸可能会随着时间而增长到非常大,这时需要手工清理。
1.3.1.1.2. 搜索 Provider
想要了解有哪些被官方接纳的 Provider,就是前往registry.terraform.io进行搜索:
一般来说,相关 Provider 如何声明,以及相关 data
、resource
的使用说明,都可以在 registry 上查阅到相关文档。
registry.terraform.io
不但可以查询 Provider,也可以用来发布 Provider;并且它也可以用来查询和发布模块(Module),不过模块将是我们后续篇章讨论的话题。
1.3.1.1.3. Provider 的声明
一组 Terraform 代码要被执行,相关的 Provider 必须在代码中被声明。不少的 Provider 在声明时需要传入一些关键信息才能被使用,例如我们在第一章的例子中,必须给出访问密钥以及期望执行的 AWS 区域(Region)信息。
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~>5.0"
}
}
}
provider "aws" {
access_key = "test"
secret_key = "test"
region = "us-east-1"
s3_use_path_style = false
skip_credentials_validation = true
skip_metadata_api_check = true
skip_requesting_account_id = true
endpoints {
apigateway = "http://localhost:4566"
apigatewayv2 = "http://localhost:4566"
cloudformation = "http://localhost:4566"
cloudwatch = "http://localhost:4566"
dynamodb = "http://localhost:4566"
ec2 = "http://localhost:4566"
es = "http://localhost:4566"
elasticache = "http://localhost:4566"
firehose = "http://localhost:4566"
iam = "http://localhost:4566"
kinesis = "http://localhost:4566"
lambda = "http://localhost:4566"
rds = "http://localhost:4566"
redshift = "http://localhost:4566"
route53 = "http://localhost:4566"
s3 = "http://s3.localhost.localstack.cloud:4566"
secretsmanager = "http://localhost:4566"
ses = "http://localhost:4566"
sns = "http://localhost:4566"
sqs = "http://localhost:4566"
ssm = "http://localhost:4566"
stepfunctions = "http://localhost:4566"
sts = "http://localhost:4566"
}
}
在这段 Provider 声明中,首先在 terraform
块的 required_providers
里声明了本段代码必须要名为 aws
的 Provider 才可以执行,source = "hashicorp/aws"
这一行声明了 aws
这个插件的源地址(Source Address)。一个源地址是全球唯一的,它指示了 Terraform 如何下载该插件。一个源地址由三部分组成:
[<HOSTNAME>/]<NAMESPACE>/<TYPE>
Hostname
是选填的,默认是官方的 registry.terraform.io
,读者也可以构建自己私有的Terraform仓库。Namespace
是在 Terraform 仓库内注册的组织名,这代表了发布和维护插件的组织或是个人。Type
是代表插件的一个短名,在特定的 HostName
/Namespace
下 Type
必须唯一。
required_providers
中的插件声明还声明了该源码所需要的插件的版本约束,在例子里就是 version = "~>5.0"
。Terraform 插件的版本号采用 MAJOR.MINOR.PATCH
的语义化格式,版本约束通常使用操作符和版本号表达约束条件,条件之间可以用逗号拼接,表达 AND
关联,例如 ">= 1.2.0, < 2.0.0"
。可以采用的操作符有:
=
(或者不加=
,直接使用版本号):只允许特定版本号,不允许与其他条件合并使用!=
:不允许特定版本号\>
,>=
,<
,<=
:与特定版本号进行比较,可以是大于、大于等于、小于、小于等于~>
:只允许 最右边 的版本号增加。这种格式被称为 悲观约束 操作符。例如,要允许在特定的MINOR
版本中允许新的PATCH
版本:~> 1.0.4
:允许 Terraform 安装1.0.5
和1.0.10
,但不允许1.1.0
。~> 1.1
:允许 Terraform 安装1.2
和1.10
,但不允许2.0
。
Terraform 会检查当前工作环境或是插件缓存中是否存在满足版本约束的插件,如果不存在,那么 Terraform 会尝试下载。如果 Terraform 无法获得任何满足版本约束条件的插件,那么它会拒绝继续执行任何后续操作。
可以用添加后缀的方式来声明预览版,例如:1.2.0-beta
。预览版只能通过 "="
操作符(或是忽略操作符)后接明确的版本号的方式来指定,不可以与>=
、~>
等搭配使用。
- 当依赖第三方模块时,需要指定特定版本,以确保只在您需要的时候进行更新。
- 对于在您的组织内维护的模块,如果一致使用语义版本控制,或者有一个定义良好的发布流程可以避免不必要的更新,那么指定版本范围可能是合适的。
- 可重用的模块应仅限制其 Terraform 和 Provider 的最低允许版本,例如
>= 0.12.0
。这有助于避免已知的不兼容性,同时允许模块的用户在不改变模块的情况下升级到 Terraform 的新版本。 - 根模块应使用
~>
约束为它们依赖的每个 Provider 设置一个下限和上限版本。
以上建议来自于 HashiCorp 官方文档,笔者个人给出一条个人建议:
- 可复用的模块不但应该限制 Provider 的最低版本,同时也应该限制 Provider 的
MAJOR
版本。例如,>= 1.5.0, < 2.0
。这样可以避免在 Provider 的MAJOR
版本升级时,因为不兼容性导致的问题,Provider 的MAJOR
版本升级通常会伴随着不兼容的改动,不应该在未加测试的情况下轻易升级。
1.3.1.1.4. 内建 Provider
绝大多数 Provider 是以插件形式单独分发的,但是目前有一个 Provider 是内建于Terraform主进程中的,那就是 terraform_remote_state
data source。该 Provider 由于是内建的,所以使用时不需要在 terraform
中声明 required_providers
。这个内建Provider的源地址是 terraform.io/builtin/terraform
。这个地址有时可能出现在 Terraform 的错误消息和其他输出中,以便无歧义地引用内建 Provider,而不是假设的第三方提供者,其类型名称为 "terraform"。
还存在一个源地址为 hashicorp/terraform
的 Provider,这是现在内置 Provider 的旧版本,被 Terraform 的旧版本使用。hashicorp/terraform
与 Terraform v0.11 或更高版本不兼容,因此永远不应在 required_providers
块中声明。
1.3.1.1.5. Provider 的配置
Provider 的配置是声明在根模块中的一组 Terraform 配置。(子模块接收来自于根模块的 Provider 配置,更多信息,请参阅模块的 provider
元参数)
一个 Provider 配置是通过 provider
块来创建的:
provider "google" {
project = "acme-app"
region = "us-central1"
}
块头部设置的名称(例子中的 "google"
)就是要配置的 Provider 的Local Name。这个 Provider 必须已在 required_providers
块中声明。
块体({
和 }
中间的内容)包含了 Provider 的配置参数。这些参数大多数是由 Provider 自己定义的;在这个例子中,project
和 region
都是 google
Provider 特有的。
你可以在这些配置的值当中使用表达式,但是只能引用在配置 Provider 时已知的值。这意味着你可以安全地引用输入变量,但是不能引用从 resource
返回的属性(一个例外是直接在配置中硬编码的 resource
参数)。
一个 Provider 的文档应该列出它所需要的配置参数。对那些注册在 Terraform Registry 上的 Provider 来说,每个 Provider 的页面上都有版本化的文档,可以通过 Provider 页头的 "Documentation" 链接访问。
一些 Provider 可以使用环境变量(或是其他替代配置源,例如 AWS 的虚拟机实例 Profile)作为某些配置参数的值;我们建议尽可能使用这种方式来避免将凭证保存于版本控制的 Terraform 代码中。
There are also two "meta-arguments" that are defined by Terraform itself
and available for all provider
blocks:
有两个由 Terraform 自身定义的“元参数”,对所有 provider
块都可用:
alias
,用以为不同的resource
块配置参数不同的同类 Provider 实例version
, 废弃,不推荐使用,现在请使用required_providers
与 Terraform 语言中的许多其他对象不同,如果 provider
块的内容为空,则可以省略该块。Terraform 假定未显式配置的任何 Provider 程序都具有空的默认配置。
1.3.1.1.6. 多 Provider 实例
provider
块声明了 aws
这个 Provider 所需要的各项配置。在上文的代码示例中,provider "aws"
和required_providers
中aws = {...}
块里的aws
,都是 Provider 的 Local Name,一个 Local Name 是在一个模块中对一个 Provider 的唯一的标识。
你可以选择为同一个 Provider 定义多个配置,并且可以根据每个资源或每个模块来选择使用哪一个。这主要是为了支持云平台的多个区域;其他例子包括针对多个 Docker 主机,多个 Consul 主机等。
要为某一个 Provider 创建多个配置,包括具有相同提供者名称的多个 provider
块。对于每个额外的非默认配置,使用 alias
元参数提供额外的名称段。例如:
# The default provider configuration; resources that begin with `aws_` will use
# it as the default, and it can be referenced as `aws`.
provider "aws" {
region = "us-east-1"
}
# Additional provider configuration for west coast region; resources can
# reference this as `aws.west`.
provider "aws" {
alias = "west"
region = "us-west-2"
}
在模块内声明配置 alias
以从父模块接收备用的 provider
配置,需要在该 provider
的 required_providers
条目中添加 configuration_aliases
参数。以下示例在包含的模块中声明了 mycloud
和 mycloud.alternate
的 provider
配置名称:
terraform {
required_providers {
mycloud = {
source = "mycorp/mycloud"
version = "~> 1.0"
configuration_aliases = [ mycloud.alternate ]
}
}
}
1.3.1.1.6.1. 默认 Provider 配置
没有 alias
参数的 provider
块是该 provider 的 默认 配置。未设置 provider
元参数的资源将使用与资源类型名称的第一个单词匹配的默认 provider 配置。(例如,除非另有说明,否则 aws_instance
资源将使用默认的 aws
provider 配置。)
如果 provider 的每个显式配置都有别名,Terraform 将使用隐含的空配置作为该 provider 的默认配置。(如果 provider 有任何必需的配置参数,当资源默认使用空配置时,Terraform 将引发错误。)
1.3.1.1.6.2. 引用备用 Provider 配置
当 Terraform 需要 provider 配置的名称时,它期望的是 <PROVIDER NAME>.<ALIAS>
形式的引用。在上面的例子中,aws.west
将引用 us-west-2
区域的 provider。
这些引用是特殊的表达式。像对其他命名实体(例如 var.image_id
)的引用一样,它们不是字符串,不需要引号。但是它们只在 resource
、data
和 module
块的特定元参数中有效,不能在任意表达式中使用。
1.3.1.1.6.3. 选择备用 Provider 配置
默认情况下,资源使用从资源类型名称的第一个单词推断出的默认 provider 配置(没有 alias
参数的配置)。
要为资源或数据源指定备用 provider 配置,将其 provider
元参数设置为 <PROVIDER NAME>.<ALIAS>
引用:
resource "aws_instance" "foo" {
provider = aws.west
# ...
}
要为子模块指定备用 provider 配置,使用其 providers
元参数指定应将哪些 provider 配置映射到模块内的哪些本地 provider 名称:
module "aws_vpc" {
source = "./aws_vpc"
providers = {
aws = aws.west
}
}
在传递 provider 时,模块有一些特殊要求;有关更多详细信息,请参见 模块 providers
元参数。在大多数情况下,只有 根模块 应定义 provider 配置,所有子模块都应从其父模块获取其 provider 配置。