1.2.1.1. Terraform 资源文件

几乎所有的 Terraform 入门教程与示例都会以一个 main.tf 文件作为开始,对于一个足够简单的模块来说,我们大可将所有 resource 块都声明在此文件内。但是如果我们的模块稍微复杂一些,我们就要考虑将 resource 块拆分到不同的文件中去了。例如:

terraform-azurerm-avm-res-storage-storageaccount 模块为例,它的目录下有这些文件名含有 main.tf 文件:

.
├── main.containers.tf
├── main.data_lake.tf
├── main.deprecated.tf
├── main.diagnostics.tf
├── main.locks.tf
├── main.management_policy.tf
├── main.privateendpoint.tf
├── main.queues.tf
├── main.shares.tf
├── main.tables.tf
├── main.telemetry.tf
└── main.tf

这是一个用来管理 Azure Storage Account 的模块。Azure Storage Account 管理了多种类型的数据存储服务:

  1. Blob
  2. File
  3. Table
  4. Queue

对应了 main.containers.tfmain.shares.tfmain.tables.tfmain.queues.tf 四个文件。每个文件中都包含了一个或多个 resource 块,分别用来管理不同类型的存储服务。azurerm_storage_account 资源块作为核心资源,被定义在 main.tf 文件中。

一个模块应始终含有 main.tf,其中包含了最核心的代码块。如果资源类型较多,关系较为复杂,那么可以根据相关性,将它们定义在不同的文件中。

1.2.1.1.1. 本地子模块 vs 拆分 .tf 代码文件

对应于将不同资源块拆分到不同的 .tf 文件中,我们还可以将它们拆分到不同的子模块中。考虑下这样的一种做法:将 azurerm_storage_account 资源块放在一个 main.tf 中,将 Blob Container、Table、Queue 等资源块放在不同的子模块(也就是子文件夹)中,然后在 main.tf 文件中通过 module 块来引用它们。这样做的好处是可以将不同的资源块进行更好的隔离,避免了不同资源块之间的相互影响,但同时也会使得模块的结构变得更加复杂。

HashiCorp 在其官方文档专门提到了这一点:

However, in most cases we strongly recommend keeping the module tree flat, with only one level of child modules, and use a technique similar to the above of using expressions to describe the relationships between the modules

在编写可复用模块时如果我们使用了本地子模块,那么对使用我们模块的用户来说,从他们的根模块看去,就有了三层资源结构:根模块 -> 可复用模块 -> 可复用模块的子模块,这会导致理解代码的难度指数增加。所以只有在你确信子模块的收益远大于复杂度增加带来的困难时,才应该这么做。另外,无需多言的是,仅包含一个资源块的子模块很明显是一种反模式,因为我们还不如直接把资源块声明在单独的代码文件中了。

存在一种特殊的模式,即子模块作为单独可复用模块地址发布,我们将在后续章节中介绍这种做法。

1.2.1.1.2. 同一个文件中 resource 和 data 定义的顺序

同一文件中的资源定义,被依赖的资源应定义在前,依赖方资源定义在后。

相互之间有依赖关系的资源,尽量定义在邻近位置。

1.2.1.1.3. 依赖于 resource 或 data 的 Attribute 的 local 定义

对于某些反复出现的表达式,或是比较复杂的表达式,为了代码可读性,我们鼓励将之抽至一个独立的 local 变量中引用。

如果表达式中涉及到 resource 或是 data,该 local 定义的位置,在表达式所涉及的 resource 或是 data 中最重要的那一个的定义块的下方。同一 resource 或是 data 块下方至多只能有一个 locals 块,所有定义于此的 local 都应该以字母序定义在该块中。两个 local 之间不应空行。

1.2.1.1.4. 使用 count 和 for_each

我们可以使用 countfor_each 来部署多个资源,但对 count 的不正确使用会导致意外的行为

只有在创建一组完全一致,或是接近一致的资源时,可以使用 count。比如说,如果我们使用 count 去迭代一个 list(string),那大概率是错误的,因为修改列表中的元素会导致资源的顺序发生变化,引发难以预知的问题。

另一种使用 count 的场景是有条件创建某种资源,例如:

resource "azurerm_network_security_group" "this" {
  count               = local.create_new_security_group ? 1 : 0
  name                = coalesce(var.new_network_security_group_name, "${var.subnet_name}-nsg")
  resource_group_name = var.resource_group_name
  location            = local.location
  tags                = var.new_network_security_group_tags
}

1.2.1.1.5. resource、 data、ephemeral 块的内部顺序

resourcedataephemeral 块中的赋值语句分为三种:Argument 、Meta-Argument与 Nested Block。Argument 的赋值表达式为参数名后接 = 的,例如:

location = azurerm_resource_group.example.location

又或者(注意,虽然有花括号,但也有 = 号,所以它也是 Argument):

tags = {
  environment = "Production"
}

Nested Block 的赋值表达式为参数名后接一个 {} 块(没有 = 号),例如:

subnet {
  name           = "subnet1"
  address_prefix = "10.0.1.0/24"
}

Meta-Argument 为所有 resourcedata 块都可以声明的赋值语句,有:

  • count
  • depends_on
  • for_each
  • lifecycle
  • provider

resourcedataephemeral 块内部赋值语句的声明顺序应为:

以下 Meta-Argument 在块中应声明在最上方,按顺序排列:

  1. provider
  2. count
  3. for_each

然后是必填的 Argument,以字母顺序排列

然后是选填的 Argument,以字母顺序排列

然后是必填的 Nested Block,以字母顺序排列

然后是选填的 Nested Block,以字母顺序排列

以下 Meta-Argument 在块中应声明在最下方,按顺序排列:

  1. depends_on
  2. lifecycle

其中,lifecycle 块中的参数应以以下顺序出现:

  • create_before_destroy
  • ignore_changes
  • prevent_destroy

depends_onignore_changes 的成员以字母顺序排序。

Meta-Argument、Argument、Nested Block 之间,以空行分隔。

dynamic 形式出现的 Nested Block,以 dynamic 后的名字作为排序依据,例如:

dynamic "linux_profile" {
  for_each = var.admin_username == null ? [] : ["linux_profile"]

  content {
    admin_username = var.admin_username

    ssh_key {
      key_data = replace(coalesce(var.public_ssh_key, tls_private_key.ssh[0].public_key_openssh), "\n", "")
    }
  }
}

dynamic 块视同一个 linux_profile 块进行排序,Meta-Argument、Argument、Nested Block 之间,以空行分隔。

在 Nested Block 中的代码也按照上述规则排序,

1.2.1.1.6. module 块的内部顺序

以下 Meta-Argument 在 module 块中应声明在最上方,按顺序排列:

  1. source
  2. version

然后是

  1. count
  2. for_each

sourceversioncountfor_each 之间应以空行分隔。

然后是必填的 Argument,以字母顺序排列

然后是选填的 Argument,以字母顺序排列

以下 Meta-Argument 在 resource 块中应声明在最下方,按顺序排列:

  1. depends_on
  2. providers

Argument 部分与 Meta-Argument 之间应以空行分隔。

1.2.1.1.7. 传递给 provider, depends_on, lifecycle 块中的 ignore_changes 中的值,不允许使用双引号

1.2.1.1.8. 对于可以定义 tags 的资源,始终应该通过 variable 暴露给 Module 调用者,使其可以设置 tags

许多云资源管理工具重度依赖与资源的 tag,如果我们不暴露 tags 使用户可以自由定制,则会造成无法与这些管理工具整合。

1.2.1.1.9. 根据输入参数是否为 null 来判定是否创建某种资源的场景

有时我们要确保创建的资源符合某种最低限度的合规,例如所有的 subnet 都要关联至少一个 network_security_group。用户可能会传递一个 security_group_id 要求我们关联到一个已经存在的 security_group 上,也有可能用户希望我们为其创建安全组。

直觉上我们会直接这样定义:

variable "security_group_id" {
  type = string
}

resource "azurerm_network_security_group" "this" {
  count               = var.security_group_id == null ? 1 : 0
  name                = coalesce(var.new_network_security_group_name, "${var.subnet_name}-nsg")
  resource_group_name = var.resource_group_name
  location            = local.location
  tags                = var.new_network_security_group_tags
}

这种做法的缺点是如果用户直接在 Root Module 中创建安全组,并将其 id 作为 Module 的 variable 时,会引发一个问题,决定 count 具体值的表达式中出现了另一个 resourceattribute,该值在 Plan 阶段为 Known After Apply,所以 Terraform Core 会认为无法在 Plan 阶段得到一个确定的计划。

这种参数我们推荐使用 object 类型进行一次包装:

variable "security_group" {
  type = object({
    id   = string
  })
  default     = null
}

这样做的优点是将 Known After Apply 值封装在 object 内部,而 object 本身的引用是可以轻松判定是否为 null 的。鉴于一个 resourceid 不可能为 null,所以这种用法可以避免这种尴尬的情况。

仅在根据输入参数是否为 null 来判定是否创建某种资源的场景下使用该技巧。

1.2.1.1.10. 可选的 Nested Object Argument 要配合 dynamic

一个社区的例子是:

resource "azurerm_kubernetes_cluster" "main" {
  ...
  dynamic "identity" {
    for_each = var.client_id == "" || var.client_secret == "" ? [1] : []

    content {
      type                      = var.identity_type
      user_assigned_identity_id = var.user_assigned_identity_id
    }
  }
  ...
}

请参照例子中的写法,如果只是想有条件地声明某个 Nested 块时,请使用:

for_each = <condition> ? [<some_item>] : []

1.2.1.1.11. 为可空的表达式配置默认值时使用 coalesce

以下例子可以在 var.new_network_security_group_namenull 或是 "" 时使用 "${var.subnet_name}-nsg"

coalesce(var.new_network_security_group_name, "${var.subnet_name}-nsg")

有一种情况不适用 coalesce 函数,例如:

coalesce(var.a, var.b)

如果 var.avar.b 有可能同时为 null,那么 coalesce 在两者都为 null 时可能会有问题,这时可以使用 var.a == null ? var.b : var.a,或者 try(coalesce(var.a, var.b), null)

1.2.1.1.12. 灵活使用 try 函数

例如以下资源定义:

resource "azurerm_public_ip" "pip" {
  count = var.create_public_ip ?  : 0

  allocation_method   = "Dynamic"
  location            = local.resource_group.location
  name                = "pip-${random_id.id.hex}"
  resource_group_name = local.resource_group.name
}

我们使用它时可以不用判断 var.create_public_ip 的值,使用 try 函数简化代码:

ip_configurations = [
  {
    public_ip_address_id = try(azurerm_public_ip.pip[0].id, null)
    primary              = true
  }
]

results matching ""

    No results matching ""