编写通俗易懂的 Terraform 项目

926 阅读5分钟

Terraform代码是声明性的,我们使用它来声明我们想要的云提供商所需的内容。

如果可以将此代码翻译成简单的英语,它将像一个精美的购物清单一样阅读:

给我一个带数据库和 kubernetes 集群的私有虚拟网络。集群应该有一定数量的节点,并且它们都应该使用这种特定类型的CPU。数据库应该位于其中一个部分,并且需要有 Gb 级别的存储能力。。。

我们所描述的理想状态往往相当复杂,这也就是为什么 terrform 项目经常变得难以理解的原因。

所以本文的目的是分享我们如何编写可理解的 terrform 代码的问题。

1. Yaml 风格的输入

大多数人将他们的输入值以 HCL 的形式写入 .tfvars 文件中。

我更喜欢YAML,因为它更容易阅读。

当您使用YAML为terrform根模块定义输入值时,所需的状态变得更容易解释:

# ./my-project/live/prod/config.yaml

networks:
  - name: network-a
    region: europe-west1
  - name: network-b
    region: europe-north1

databases:
  - name: database-number1
    type: cloudsql
    network: network-a
    region: europe-west1-a
    disk_size: 20gb
  - name: database-number2
    type: postgresql
    network: network-b
    region: europe-north1-b
    disk_size: 40gb

clusters:
  - name: prod-blue
    region: europe-west1-a
    network: network-a
    min_nodes: 3
    max_nodes: 6
  - name: prod-green
    region: europe-west1-b
    network: network-a
    min_nodes: 3
    max_nodes: 6

而等价的 HCL 配置则看起来比较复杂:

networks = [
  {
    name = "network-a"
    location = "europe-west1"
  },
  {
    name = "network-b"
    location = "europe-west1"
  }
]

databases = [
  {
    name = "database-number1"
    type = "cloudsql"
    network = "network-a"
    location = "europe-west1-a"
    disk_size = "20gb"
  },
  {
    name = "database-number2"
    type = "postgresql"
    network = "network-b"
    region = "europe-west1-b"
    disk_size = "40gb"
  }
]

# etc..

YAML 和 HCL 支持相同的基本功能集合。

这使得 YAML 很容易转化为 HCL,甚至在 terrform 中还内置了一个函数来做这件事 yamldecode()

通过以下技巧,terrform 可以很方便的使用 YAML 配置:

# ./live/*/locals.tf
locals {
  config = yamldecode(file("./config.yaml"))
}

这使得 config.yaml 文件中的内容都可以被 .tf 文件中的 local.config 对象进行访问。

2. 以配置为导向

编写可理解的 Terraform 代码最关键的策略就是要配置导向。

通过配置,我们引用的是将传递给 terrform 模块的输入值。

如上所述,这些值可以以一种几乎感觉像是文档的方式组织起来,一种可以说明执行 terrform 代码时会发生什么的文档。

面向配置意味着您要将工程师可能长期与之交互的文件数量最小化。

工程师只会因为想要理解(或修改)所需的状态而重新访问旧的 terrform 代码。

如果您构建的输入值如上所述,则大多数代码访问需求可能仅限于 config.yaml 文件。

任何了解 YAML 的人都会直观地了解如何添加/删除数据库,集群和网络。诸如增加数据库大小之类的简单任务也变得非常直观。

3. 目录结构

许多 terrform 项目的一个共同缺陷是它们的结构很糟糕。

我曾见过有许多 .tf 文件的存储库,读者甚至不知道从哪里开始。

作者所在的公司,所有的 terrform 项目都有一个固定的目录布局。假设我们希望跨两个环境部署资源(devprod

└── my-project
    ├── live
    │   ├── dev
    │   │   ├── config.yaml
    │   │   ├── modules.tf
    │   │   ├── providers.tf
    │   │   ├── locals.tf
    │   │   └── terraform.tf
    │   └── prod
    │       ├── config.yaml
    │       ├── modules.tf
    │       ├── providers.tf
    │       ├── locals.tf
    │       └── terraform.tf
    └── modules
        ├── kubernetes
        │   └── main.tf
        ├── network
        │   └── main.tf
        └── database
            └── main.tf

作者所在的公司 GitHub 组织中的每个 terrform 项目都是这样结构的,这使得他们的工程师很容易导航。

4. 使用 modules

Terraform modules 我们一般会放置在 modules/ 目录。

它们是用HCL编写的,通常需要简单的输入值,如字符串、数字或列表。如果它们跨多个根模块使用,我们将它们存储在它们自己的GitHub存储库中,以便重用。

我们遵循的一个固定的风格决定是,对于有多个环境的项目,我们不使用根模块中的资源块(比如 prod/dev/staging)。

换句话说,我们只使用 modules.tf 中的 moduledata

这种方法有一些很好的好处:

  • modules.tf 文件 ,在不同的环境下内容总是相同的 (容易拷贝和复制)
  • Terraform code 变得很容易在多个环境进行修改
  • terraform state 命令 变得很容易处理

对于单一环境项目,可能不需要这种样式限制。

我们 YAML 配置文件中的内容在 modules.tf 中看起来是如下结构:

# ./my-project/live/*/modules.tf

module "network" {
  for_each = { for x in local.config.networks : x.name => x }
  source   = "../../modules/network"

  name      = each.value.name
  region    = each.value.region
}

module "database" {
  for_each = { for x in local.config.databases : x.name => x }
  source   = "../../modules/database"

  name      = each.value.name
  type      = each.value.type
  region    = each.value.region
  disk_size = each.value.disk_size
  network   = module.network[each.value.network].name
}

module "kubernetes" {
  for_each = { for x in local.config.clusters : x.name => x }
  source   = "../../modules/kubernetes"

  name      = each.value.name
  region    = each.value.region
  min_nodes = each.value.min_nodes
  max_nodes = each.value.max_nodes
  network   = module.network[each.value.network].name
}

5. 默认值

Terraform modules 中经常会包含很多入参。

在YAML配置文件中包含所有这些可能会对它们的可读性产生负面影响。

当我们编写自己的模块时,我们可以在 modules/ 目录中使用默认值,以使不必要的复杂性排除在 YAML 文件之外。

使用 YAML 风格的方法时遇到的一个问题是如何在 config.yaml 中支持可选值。

在 Terraform v1.1.0 版本,有一个 a neat trick 可以遵循模块的默认值。

让我们假设 kubernetes 模块中的 max nodes 变量默认为6。

我们希望能够在配置中直接覆盖 config.yaml 中的默认值,但是如果没有指定该变量,我们还是希望使用默认的6:

# ./my-project/live/prod/config.yaml

clusters:
  - name: prod-blue
    region: europe-west1-a
    network: network-a
    min_nodes: 3
  - name: prod-green
    region: europe-west1-b
    network: network-a
    min_nodes: 3
    max_nodes: 12
# ./my-project/modules/kubernetes/main.tf

variable "max_nodes" {
  default = 6
  nullable = false
}
# ./my-project/live/*/modules.tf

module "kubernetes" {
  for_each = { for x in local.config.clusters : x.name => x }
  source   = "../../modules/kubernetes"

  name      = each.value.name
  region    = each.value.region
  min_nodes = each.value.min_nodes
  max_nodes = lookup(each.value, "max_nodes", null)
  network   = module.network[each.value.network].name
}

main.tf 中的 nullable = false 参数意味着如果从 modules.tf 接收到了 null ,它将直接回退到默认值,即使用 6 。

如上就可以实现 prod-bluemax_nodes 为6,而 prod-green 为 12。

当你无法控制一个 modules.tf 中模块的来源时,你可能没有定义为空的字段(它将默认为 true)。

如果您希望一个值在配置中既可配置又可选。在 Yaml 中,只需在 modules.tf 中的 lookup() 命令中提供一个默认值。

但是要小心,你做的越多,工程师就越难弄清楚某个模块是如何配置的。

只有当模块没有合适的默认值时,我才倾向于在 lookup() 函数中放入默认值,而且我不太可能关心长期的输入值。

以上,就是作者所在团队在编写 Terraform 项目时的一些比较好规范和建议,希望对大家也有所帮助。


原文: itnext.io/understanda…
翻译: GoOps
公众号: CloudNativeOps