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 项目都有一个固定的目录布局。假设我们希望跨两个环境部署资源(dev 和 prod)
└── 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 中的 module 和 data。
这种方法有一些很好的好处:
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-blue 的 max_nodes 为6,而 prod-green 为 12。
当你无法控制一个 modules.tf 中模块的来源时,你可能没有定义为空的字段(它将默认为 true)。
如果您希望一个值在配置中既可配置又可选。在 Yaml 中,只需在 modules.tf 中的 lookup() 命令中提供一个默认值。
但是要小心,你做的越多,工程师就越难弄清楚某个模块是如何配置的。
只有当模块没有合适的默认值时,我才倾向于在 lookup() 函数中放入默认值,而且我不太可能关心长期的输入值。
以上,就是作者所在团队在编写 Terraform 项目时的一些比较好规范和建议,希望对大家也有所帮助。
原文: itnext.io/understanda…
翻译: GoOps
公众号: CloudNativeOps