【Terraform】模块的反模式及其对应的最佳实践五选

序言

在Terraform的功能中,我认为module和workspace(之前称为environment)是受到评价分歧的两个方面之一。
我自己也曾经听到过多个人对module持否定的意见,并且在刚开始使用module的时候,我并没有觉得它真的很方便。

然而,在阅读官方网站上的模块页面12时,我注意到很多人在使用中犯了错误的模块使用方式,导致出现了不能使用或不实用的模块被误解的情况。为了避免出现这样的反模式,我注意到了使用模块数月后,才意识到模块是非常方便且具有高度通用性的功能。

因为希望更多人了解这个模块功能的实用性,所以在本页面上将会介绍官方网站中所述的模块使用的反模式及其最佳实践。通过理解这些,我们可以充分发挥模块功能的潜力!

反模式及其最佳实践

我认为,我更容易沉浸其中并感到更重要的事物排在了上面。

反模式1:创建只包含一个资源的模块

创建一个只包含以下这样一个资源的模块。

variable "vpc_id" {}
variable "cidr_block" {}
variable "availability_zone" {}

resource "aws_subnet" "subnet" {
  vpc_id            = var.vpc_id
  cidr_block        = var.cidr_block
  availability_zone = var.availability_zone
}

output "subnet_id" {
  value = aws_subnet.subnet.id
}

创建模块-哈希标公司的Terraform – 引用自:创建模块 – 哈希标公司的Terraform

反模式的原因

在引用页面中,我们将只包含一个资源的模块称为 “只含单一资源的薄包装器”。

这有以下的缺点。

    1. 模块过多成为了一个问题

理解多个模块的规范需要相应的成本

模块的通用性较低

通过从原始资源的参数/输出中提取一部分来创建“这种轻量级封装模块”,以创建适合初学者使用的资源定义。因此,随着系统复杂化、扩展和terraform写作人员的成长,它将很快无法满足需求。相反,如果增加变量/输出,最终将与资源定义本身等效,模块将失去意义。

模块文档的质量往往较低

由于必须写入许多变量/输出,通常会省略描述。模块创建者认为它是简单的模块,所以他们认为README是不需要的。然而,由于模块的方向不明确,后续的功能扩展和功能增加会使维护者陷入困境。

最佳实践

不要将资源直接封装在模块中,而是在需要的地方直接声明。只有当模块将2个或更多的资源组合成有意义的单元时,模块才能有效地发挥作用。

闲话

我曾在三家不同的公司中看到过这种反模式,主要是出于以下动机。

    1. 我想要設定資源屬性的預設值

 

    1. 我想要限制可指定的屬性

 

    我想要強制設定特定屬性的設定3

综上所述,可以看出背景是希望对上述资源类型进行某种标准化。这种动机本身并不完全错误,但根据Terraform的规范,似乎模块并不适用于这样的目的。

如果你想对资源的属性设立约束或规则,使用以下的Linter比使用模块更好。

    • terraform-linters/tflint

 

    • Sentinel – HashiCorp

 

    instrumenta/conftest

反模式2:将其他模块包含在子模块中

如果存在依赖于网络资源的Kubernetes模块,可以通过将网络模块包含在Kubernetes模块中来实现。

引用源:HashiCorp的Terraform – 创建模块

反模式的原因

让我们以上述的Kubernetes模块和网络模块的例子来思考。

如果要在这个基础设施上添加数据库,那么可以是这样的。

有一种方法可以将其添加到现有的Kubernetes模块中,但这样一来,当只想在其他Root模块中仅使用Kubernetes(模块)时,会引入不必要的数据库。

据说,由于子模块包含了其他模块,从而降低了模块的可重用性,因此被称为反模式。

最佳实践

在中文中,尽量使模块结构扁平化是一般来说是好的。这样一来,就不会出现像前面所提到的不必要的引用传递。同时,每个模块都可以变得高度通用且松散耦合。

例外 – 在中国,只需要一种选择,以下是对其的本地语言释义:

除外

由于引用多个模块中的嵌套(子孙)模块会导致问题,因此这种模式不好。所以,如果确定只有一个模块引用该模块,那就没有问题。
例如,通常情况下,每个构成Kubernetes集群的节点信息都不需要在Kubernetes模块之外使用,所以认为嵌套是没有问题的。4

创建一个只有一个tf文件的模块,同时不写README和示例。

创建一个只有一个tf文件的模块,同时不编写README和示例。

将variable、resource和output全部写在一个tf文件中如下。

variable "vpc_id" {}
variable "cidr_block" {}
variable "availability_zone" {}

resource "aws_subnet" "subnet" {
  vpc_id            = var.vpc_id
  cidr_block        = var.cidr_block
  availability_zone = var.availability_zone
}

output "subnet_id" {
  value = aws_subnet.subnet.id
}

没有放置README或使用示例。

标准模块结构 | Terraform | HashiCorp 开发者 | 引自: Standard Module Structure | Terraform | HashiCorp Developer

反模式的原因

模块的文件结构按照后面所述的标准确定。

这个结构包含了自从Terraform发布以来几年间的最佳实践。如果忽视这些最佳实践,自己构建结构的话就会遇到先辈们已经遇到过的问题。例如,在所有tf文件中都声明变量而不使用variables.tf,在使用模块时,使用者将会很难确定需要传递哪些变量。

如果忽略标准结构而采用模块结构,会使习惯标准结构的人感到困惑。维护也将变得更加困难,除了模块创建者外的其他人参与会更加困难。

最佳实践

我们将按照官方网站所述的以下标准结构进行。

.
├── examples
│   └── basic
│       └── main.tf  [可能な限り] このモジュールのミニマムな使用例を書きます
|                                何も変更せずともこのbasicディレクトリで
|                                terraform init && terraform apply が通るようにします
├── modules          [必要に応じて] ネストしたモジュール(このモジュール内でのみ使用するモジュール)はここで宣言します
│   ├── master-nodes [必要に応じて]
│   └── worker-nodes [必要に応じて]
├── LICENSE          [必要に応じて] 公開モジュールの場合は必ず置きましょう
├── README.md        [必須] モジュールの概要と用途を書きます。場合によっては図を含めましょう
|                           使用例は examples/basic 以下に実際に動くコードとして書き、
|                           ここからはリンクするにとどめたほうがよいです
|── variables.tf     [必須] 何もvariableがない場合でも空のファイルを作ります。
|                           またvariableのdescriptionは必ず書きます。
|                           単にリソースのargumentへ引き回しており自明に思えるときは
|                           https://www.terraform.io/docs/providers/aws/r/instance.html#ami
|                           などそのargument項目へのリンクを書くと良いです。
├── main.tf          [必須] 基本的にはここへリソース宣言を置きます。
├── another.tf       [必要に応じて] main.tfが長くなりすぎた場合はRoot Moduleと同じく
|                                 種別ごとにtfファイルを分けて書きます。
└── outputs.tf       [必須] 何もoutputがない場合でも空のファイルを作ります。
                            またoutputのdescriptionは必ず書きます。
                            単にリソースのoutputを引き回しており自明に思えるときは
                            https://www.terraform.io/docs/providers/aws/r/instance.html#id
                            などそのoutput項目へのリンクを書くと良いです。

反例模式4. 在子模块中编写提供者(provider)块

.
├── LICENSE
├── README.md
|── variables.tf
├── main.tf
├── outputs.tf
└── providers.tf    <---- コレ

在子模块内编写提供者块如下所示。

provider "aws" {
  version = "~> 2.54"
  region  = "ap-northeast-1"
}

文档来源:模块 – 配置语言 – HashiCorp 的 Terraform

反模式的原因

这是因为当删除模块时,无法继续执行 terraform apply,并会产生以下错误。

$ terraform apply

Error: Provider configuration not present

To work with module.master_nodes.random_id.server its original provider
configuration at module.master_nodes.provider.random is required, but it has
been removed. This occurs when a provider configuration is removed while
objects created by that provider still exist in the state. Re-add the provider
configuration to destroy module.master_nodes.random_id.server, after which you
can remove the provider configuration again.

在Terraform中,每个资源都会与一个单独的提供者及其配置相关联,但这些提供者配置并未保存在tfstate文件中。在每次执行terraform命令时,它会查找配置文件(*.tf)内的provider块。

由于这个规则,如果子模块内有provider块,则子模块内的提供者和资源将关联在一起。
看起来一切都没有问题,但是当删除子模块声明并执行terraform apply时会出现问题。
当删除模块时,由于自然而然地删除了模块内的资源声明,Terraform会尝试删除该资源,但与该资源相关联的提供者块已经不存在了。因此,由于无法确定如何删除该资源,会出现错误,并且无法正常执行terraform apply。

最佳实践

在子模块中,不需要编写provider代码块,而是必须在根模块中声明。即使provider仅在特定的子模块内使用,也应该在根模块中进行声明。如果想限定provider的版本,请在子模块的terraform代码块中声明required_providers代码块。

terraform {
  required_providers {
    aws = ">= 2.7.0"
  }
}

闲谈

如果遇到 terraform apply 出错且无法继续进行的情况,一般有两个解决方案。

将提供者块从子模块移动到根模块

如果使用这种方法,可以解决根本问题。因为它是最简便的方法,所以我推荐它。

2. 使用 terraform destroy -target 命令,删除与问题的提供者关联的资源。

如果您绝对不想更改tf文件,这是一种方法。

正如在反模式的原因部分提到的那样,因为缺少提供程序声明,因此在删除资源时会发生错误。因此,如果在拥有模块声明的情况下按照以下步骤删除资源,则不会发生错误。

    1. 在不删除模块声明的情况下保留。

通过terraform destroy -target=…删除与子模块内部提供程序相关联的所有资源。

确认进行terraform apply后没有错误。

然而,这种方法存在以下问题。

    1. 根本性的问题无法解决

 

    1. 需要人工手动筛选需要手动删除的资源

 

    如果不小心未附加参数而执行terraform destroy命令,将导致大灾难

因此,我们应尽可能避免使用这种方法。

反例5:将模块版本指定为最低版本

module "consul" {
  source  = "hashicorp/consul/aws"
  version = "~> 1.2"
}

模块 – 配置语言 – Terraform by HashiCorp 引用元, 版本约束 – 配置语言 – Terraform by HashiCorp.

反模式的原因 de

由于执行terraform命令的个人电脑或实例使用了不同的版本。

在Terraform模块的版本指定中,可以使用像>=或~>这样冗长的版本指定。然而,与Gemfile的Gemfile.lock或package.json的package-lock.json不同,没有一种固定版本的方法来确定实际安装的模块版本,这取决于执行的日期和时间。另外,当terraform init时,如果已经存在已下载的提供程序/模块且其版本符合要求,则不会再下载最新版本。
在这种规范下,使用>=或~>等版本指定会导致由于执行的日期、时间或位置不同而导致版本不一致,从而可能引发非常棘手的问题,并且最终可能会导致terraform plan/apply的结果不同。

最佳实践

在中文中,不要使用诸如”>=”或”~>”等符号,而是直接指定特定版本。

module "consul" {
  source  = "hashicorp/consul/aws"
  version = "1.2"
}

有一种方式可以手动更新已固定模块的版本,或者可以交由Dependabot处理(Dependabot的作者还未确认)。

如果您非常想要指定范围,请确保使用最新版本进行初始化并使用 terraform init -upgrade 命令定期获取最新版本。

请留意

模块和供应者有很大不同,供应者现在开始使用锁定文件!
因此,建议在供应者方面使用>=或~>。

多此一举

我在这里总结了个人认为不错的做法,虽然它们不一定在官方网站上写着。如果方便的话,希望你也能参考一下。

    【Terraform】モジュールに関する個人的ベストプラクティス – Qiita

引用来源页面

    • Modules – Configuration Language – Terraform by HashiCorp

 

    Creating Modules – Terraform by HashiCorp
HashiCorp的Terraform模块配置语言模块创建-Terraform by HashiCorp。通过使用变量,可以强制指定。

实际上,甚至在Terraform的公共模块中,最具星数的EKS模块也采用了这种结构。

对于那些学习过面向对象编程或领域驱动设计的人来说,可能会觉得依赖关系是相反的,感觉非常不舒服,但在当前的Terraform中,这是最佳实践。

虽然一开始可能会认为每个人都遵循语义化版本控制,所以不太可能遇到问题,但即使是AWS提供者也会在2.51→2.52的次要版本升级中引入破坏性更改,所以无条件地信任是不明智的。此外,也可能出现简单地引入新错误的情况。

广告
将在 10 秒后关闭
bannerAds