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 在执行 planapply 时会将整个对象都标记为敏感数据,这样就无法在命令行中看到其他成员的值了,更加麻烦的是下面这种情况:

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 定义的顺序

输入变量应遵守以下顺序:

  1. 各种必填项,以字母序排序
  2. 各种选填项,以字母序排序

  3. 定义了 default 的 variable 为必填项,反之则为选填项。

1.2.2.1.13. variable 中的命名

在易于理解的前提下确保 variablenamedescriptionvalidation 与上下游 resourcedata 定义尽可能一致,并且确保不同模块间起相同作用的 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_idresource 中的 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 必须清晰无误地告知用户,我们(不)期待的输入是什么样子的。

results matching ""

    No results matching ""