1.2.2.1. Terraform 输入变量文件
variables.tf
也是一个 Terraform 模块必须包含的四个文件(main.tf
/variables.tf
/output.tf
/terraform.tf
)之一,其中包含了模块所有的输入变量。哪怕模块没有任何的 variable
块,也要保持一个 variables.tf
文件。允许多个输入变量文件,但文件名都必须含有 variables
,例如:variables_db.tf
,或者是 db_variables.tf
。此外,这些 variables
文件中应仅包含 variable
块。
假如把模块比作一个函数,那么 variables.tf
就是函数的参数列表。一个良好的模块应遵循一定的输入变量规范:
1.2.2.1.1. 所有的输入变量都应该在 variables.tf 中声明
1.2.2.1.2. 所有的输入变量都应设置准确的 type 值
1.2.2.1.3. 所有的输入变量都应设置准确的 description 值
1.2.2.1.4. 承接 Ephemeral 数据的输入变量,应声明 ephemeral = true
1.2.2.1.5. 赋值给敏感数据,例如密码属性的输入变量,应声明 sensitive = true
1.2.2.1.6. 敏感数据应单独保存一个输入变量,而非与其他成员一起定义在 object 类型中
试想一下这样一个输入变量
variable "virtual_machine" {
type = map(object({
name = string
///...
admin_password = string
}))
description = "Virtual Machine"
sensitive = true
}
在这个输入变量中,admin_password
是一个敏感数据,但它与其他成员一起定义在 object
类型中。Terraform 目前不支持将 object
的一个属性声明为 sensitive
,我们别无选择,只能将整个变量声明为 sensitive
。这样做的坏处是,Terraform 在执行 plan
和 apply
时会将整个对象都标记为敏感数据,这样就无法在命令行中看到其他成员的值了,更加麻烦的是下面这种情况:
resource "azurerm_windows_virtual_machine" "example" {
for_each = var.virtual_machine
name = each.value.name
# ...
admin_password = each.value.password
}
当我们通过一个 terraform.tfvars
文件给 var.virtual_machine
赋值,然后尝试运行 terraform plan
时,我们会看到:
╷
│ Error: Invalid for_each argument
│
│ on main.tf, in resource "azurerm_windows_virtual_machine" "example":
│ 66: for_each = var.virtual_machine
│ ├────────────────
│ │ var.virtual_machine has a sensitive value
│
│ Sensitive values, or values derived from sensitive values, cannot be used as for_each arguments. If used, the sensitive value
│ could be exposed as a resource instance key.
╵
这时我们就无法使用 for_each
来创建资源了。我们只能将 admin_password
声明在一个单独的输入变量里:
1.2.2.1.7. Ephemeral 或 Sensitive 的数据,不允许设置默认值
为敏感数据(如密码、API 密钥)设置默认值可能导致这些信息被猜到。攻击者可能会使用来自开源代码的默认值进行尝试,假设用户没有意识到这一点,那么我们的模块可能就会成为攻击者的大门。
1.2.2.1.8. 集合类型的输入变量,相比起设置 null 为默认值,应该设置空集合为默认值
使用空集合作为默认值,可以避免在模块中编写额外的条件判断。例如,使用 for_each
创建资源时,空集合会导致不创建任何资源,而 null
可能引发错误或需要额外的条件判断。
1.2.2.1.9. 如果可以,尽量声明 nullable = false 以简化后续代码逻辑
在 Terraform 中,输入变量是可以被显式设置为 null
的,这在搭配类似 bool
这样的类型时可能会引发问题,例如 var.log_enabled
:
variable "log_enabled" {
type = bool
description = "Whether to turn log on or not"
}
模块中 var.log_enabled
在运行时,它的值可以是 true
/false
/null
,如果我们有这样一个表达式:
count = var.log_enabled ? 1 : 0
那么在值是 null
时就会触发运行时错误,我们必须写成:
count = var.log_enabled == true ? 1 : 0
即使我们给它加上默认值也无法解决这个问题:
variable "log_enabled" {
type = bool
default = true
description = "Whether to turn log on or not"
}
即使是设置了默认值,如果模块调用者显式将之设置为 null
,运行时的值仍然会是 null
。
如果我们设置了 nullable = false
,那么在调用者设置为 null
时,会设置为默认值,这样状态就被简化为 true
/false
两种,可以减少错误,简化逻辑。
1.2.2.1.10. 用来搭配 for_each 创建一组资源的输入变量,其类型应优先选择 map 而非 set,并在 description 中清楚说明 map 的键应是静态字面量,而非动态值
不适用 set
的原因其实就是 set(object)
以对象所有数据的值整体为唯一性的判断依据,Terraform 是不支持用这样一个 set
作为 for_each
的对象的。
Terraform 中的 map
仅支持 string
作为键,所以 map
会是用来搭配 for_each
创建资源块极佳的数据类型,但是我们必须要确保 map
的键是静态的,这是什么意思呢?让我们来看这样一段代码:
variable "groups" {
type = map(object({
location = string
tags = map(string)
}))
}
resource "random_string" "random" {
for_each = var.groups
length = 6
special = false
upper = false
}
locals {
resource_groups = {
for k, v in var.groups : "${k}-${random_string.random[k].result}" => {
name = "rg-${random_string.random[k].result}"
location = v.location
tags = v.tags
}
}
}
resource "azurerm_resource_group" "main" {
for_each = local.resource_groups
name = each.value.name
location = each.value.location
tags = each.value.tags
}
以上代码在执行时会掷出这样的错误:
╷
│ Error: Invalid for_each argument
│
│ on main.tf line 51, in resource "azurerm_resource_group" "main":
│ 51: for_each = local.resource_groups
│ ├────────────────
│ │ local.resource_groups will be known only after apply
│
│ The "for_each" map includes keys derived from resource attributes that cannot be determined until apply, and so Terraform cannot
│ determine the full set of keys that will identify the instances of this resource.
│
│ When working with unknown values in for_each, it's better to define the map keys statically in your configuration and place
│ apply-time results only in the map values.
│
│ Alternatively, you could use the -target planning option to first apply only the resources that the for_each value depends on,
│ and then apply a second time to fully converge.
╵
原因就是 Terraform 要求在 Plan 阶段,for_each
的键必须是已知的,而我们的代码里,键包含了 random_string
的输出,这是只有在 Apply 成功以后才能知道的值。
这段代码故意被写的极其简单以使读者能够轻松理解,但很多时候实际使用模块时用户可能并没有意识到传递给 map
的参数的键拥有尚不知晓的值,所以我们建议必须在 description
中清晰明白地写清楚,请确保键是静态的。
1.2.2.1.11. 不允许声明与 Terraform 默认行为一致的值,例如 sensitive = false,nullable = true
无需多言,这种代码声明了和不声明没有区别,我们应尽全力确保代码是极简的。
1.2.2.1.12. variable 定义的顺序
输入变量应遵守以下顺序:
- 各种必填项,以字母序排序
各种选填项,以字母序排序
定义了 default 的 variable 为必填项,反之则为选填项。
1.2.2.1.13. variable 中的命名
在易于理解的前提下确保 variable
的 name
、description
、validation
与上下游 resource
、data
定义尽可能一致,并且确保不同模块间起相同作用的 variable
保持一致。
允许出于区分的目的为 variable
添加以 _ 结尾的前缀。例如:
resource "azurerm_linux_virtual_machine" "webserver" {
name = "webserver"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
source_image_id = var.webserver_source_image_id
...
}
在这里 webserver_source_image_id
中,source_image_id
与 resource
中的 Argument 保持了一致(是 webserver_source_image_id
被赋给了 source_image_id
,等号左右两边表达式拥有相同的后缀),webserver_
作为前缀是合法的。
假如一个 variable
是用来传值给某个资源的输入参数,那么该 variable
的命名应直接使用资源定义的输入参数名称(允许添加前缀), description
应尽量拷贝资源定义文档中的相关描述。
用作特性开关的 variable
名称使用肯定式:xxx_enabled
而不是 xxx_disabled
。防止在代码中出现双重否定: !xxx_disabled
。
请使用 xxx_enabled
而非 enable_xxx
作为 variable
名称。
1.2.2.1.14. validation
输入变量的断言可以允许我们定义针对输入变量的一些检查逻辑,我们允许甚至鼓励模块作者使用断言阻挡不合法的输入值,并且为用户提供更加清晰易懂的错误提示,但我们不鼓励将在 Provider 中已经实现过的校验逻辑重复用断言再实现一次,例如某字段必须符合某个正则表达式规则,可能在 Provider 的 Schema 中已经有过约束,那么继续在模块断言中重复实现这个约束并不能带来额外的收益。
假如定义了断言,那么 error_message
必须清晰无误地告知用户,我们(不)期待的输入是什么样子的。