如何构建自定义的Terraform模块

作者选择了自由开源基金作为Write for DOnations计划的捐赠对象。

介绍

Terraform模块允许您将基础架构的不同资源分组到单个、统一的资源中。您可以在以后重复使用它们,并进行可能的定制,而无需每次需要它们时都重复定义资源,这对于大型和结构复杂的项目非常有益。您可以使用您定义的输入变量自定义模块实例,还可以使用输出从中提取信息。除了创建自己的自定义模块外,您还可以使用公开发布在Terraform Registry上的预制模块。开发人员可以像使用您创建的模块那样使用和定制它们,但它们的源代码存储在云中并从云中获取。

在本教程中,您将创建一个Terraform模块,该模块将为冗余性设置多个Droplets在负载平衡器后面。您还将使用Hashicorp配置语言(HCL)的for_eachcount循环功能同时部署多个定制实例的模块。

先决条件

注意:此教程已专门测试了 Terraform 1.1.3 版本。

模块结构和优势

在本节中,您将了解模块带来的好处,它们通常放置在项目中的位置,以及它们应该如何被结构化。

自定义的 Terraform 模块被创建来封装在更大项目中经常一起使用和部署的连接组件。它们是自包含的,仅捆绑它们需要的资源、变量和提供者。

模块通常存储在项目根目录下的中央文件夹中,每个模块位于其相应的子文件夹下面。为了保持模块之间的清晰分离,始终设计它们具有单一目的,并确保它们永远不包含子模块。

当您发现自己重复使用具有不经常定制的资源方案时,创建模块非常有用。将单个资源打包为模块可能是多余的,并逐渐消除了整体架构的简单性。

对于小型开发和测试项目,不需要合并模块,因为它们在这些情况下并不带来太多改进。通过其可定制性,模块是复杂结构项目的构建要素。开发人员在较大项目中使用模块,因为避免代码重复的重要优势。模块还提供了只需要在一个地方修改定义,然后将其传播到基础架构的其余部分的好处。

接下来,您将定义、使用和定制您 Terraform 项目中的模块。

创建模块

在这一部分中,您将定义多个 Droplets 和一个负载均衡器作为 Terraform 资源,并将它们打包到一个模块中。您还将使用模块输入使得结果模块可定制。

您将把该模块存储在一个名为 droplet-lb 的目录下,该目录位于一个名为 modules 的目录下。假设您位于作为先决条件创建的 terraform-modules 目录中,通过运行以下命令一次性创建两者:

  1. mkdir -p modules/droplet-lb

参数 -p 告诉 mkdir 在提供的路径中创建所有目录。

导航到该目录:

  1. cd modules/droplet-lb

如前一部分所述,模块包含其使用的资源和变量的定义。从 Terraform 0.13 开始,它们还必须包括它们使用的提供程序的定义。模块不需要任何特殊配置来指示代码表示一个模块,因为 Terraform 将每个包含 HCL 代码的目录都视为一个模块,即使是项目的根目录也是如此。

在模块中定义的变量被公开为其输入,并且可以在资源定义中使用以定制它们。您将创建的模块将具有两个输入:要创建的 Droplets 数量和它们组的名称。创建并打开名为 variables.tf 的文件进行编辑,您将在其中存储变量:

  1. nano variables.tf

添加以下行:

modules/droplet-lb/variables.tf
variable "droplet_count" {}
variable "group_name" {}

保存并关闭文件。

您将在名为droplets.tf的文件中存储Droplet定义。创建并打开它进行编辑:

  1. nano droplets.tf

添加以下行:

modules/droplet-lb/droplets.tf
resource "digitalocean_droplet" "droplets" {
  count  = var.droplet_count
  image  = "ubuntu-20-04-x64"
  name   = "${var.group_name}-${count.index}"
  region = "fra1"
  size   = "s-1vcpu-1gb"
}

对于指定要创建多少个资源实例的count参数,您将传递droplet_count变量。其值将在从主项目代码调用模块时指定。部署的每个Droplet的名称都将不同,您通过将当前Droplet的索引附加到提供的组名称来实现。Droplets的部署将在fra1区域进行,并且它们将运行Ubuntu 20.04。

完成后,保存并关闭文件。

现在定义了Droplets,您可以继续创建负载均衡器。您将在名为lb.tf的文件中存储其资源定义。通过运行以下命令创建并打开它进行编辑:

  1. nano lb.tf

添加其资源定义:

modules/droplet-lb/lb.tf
resource "digitalocean_loadbalancer" "www-lb" {
  name   = "lb-${var.group_name}"
  region = "fra1"

  forwarding_rule {
    entry_port     = 80
    entry_protocol = "http"

    target_port     = 80
    target_protocol = "http"
  }

  healthcheck {
    port     = 22
    protocol = "tcp"
  }

  droplet_ids = [
    for droplet in digitalocean_droplet.droplets:
      droplet.id
  ]
}

您使用其名称中的组名来定义负载均衡器,以便使其能够区分。您将其与Droplets一起部署在fra1区域。接下来的两个部分指定了目标和监视端口以及协议。

突出显示的droplet_ids块接收Droplets的ID,这些Droplets应由负载均衡器管理。由于存在多个Droplets,并且其数量事先不知道,因此您使用for循环遍历Droplets的集合(digitalocean_droplet.droplets)并获取它们的ID。您将for循环用方括号([])括起来,以便结果集将是一个列表。

保存并关闭文件。

您现在已定义了Droplet、负载均衡器和模块的变量。您需要定义提供商要求,指定模块使用的提供商,包括它们的版本和位置。自Terraform 0.13以来,模块必须明确定义它们使用的非Hashicorp维护的提供商的来源;这是因为它们不会从父项目继承它们。

您将提供商要求存储在名为provider.tf的文件中。通过运行以下命令创建它以进行编辑:

  1. nano provider.tf

添加以下行以要求digitalocean提供商:

modules/droplet-lb/provider.tf
terraform {
  required_providers {
    digitalocean = {
      source = "digitalocean/digitalocean"
      version = "~> 2.0"
    }
  }
}

完成后保存并关闭文件。现在droplet-lb模块需要digitalocean提供商。

模块还支持输出,您可以使用它们提取有关其资源状态的内部信息。您将定义一个输出,公开负载均衡器的IP地址,并将其存储在名为outputs.tf的文件中。创建它以进行编辑:

  1. nano outputs.tf

添加以下定义:

modules/droplet-lb/outputs.tf
output "lb_ip" {
  value = digitalocean_loadbalancer.www-lb.ip
}

此输出检索负载均衡器的IP地址。保存并关闭文件。

droplet-lb 模块现已功能完备,准备好部署。您将从主代码中调用它,主代码将存储在项目的根目录中。首先,通过向上移动两次进入文件目录:

  1. cd ../..

然后,创建并打开一个名为 main.tf 的文件进行编辑,在其中您将使用该模块:

  1. nano main.tf

添加以下行:

main.tf
module "groups" {
  source = "./modules/droplet-lb"

  droplet_count = 3
  group_name    = "group1"
}

output "loadbalancer-ip" {
  value = module.groups.lb_ip
}

在这个声明中,您调用位于指定为 source 的目录中的 droplet-lb 模块。您配置提供的输入 droplet_countgroup_name,它被设置为 group1,以便您以后能够区分实例。

由于负载均衡器 IP 输出是在一个模块中定义的,当您应用项目时不会自动显示。解决此问题的方法是创建另一个输出以检索其值(loadbalancer_ip)。

完成后保存并关闭文件。

通过运行以下命令来初始化模块:

  1. terraform init

输出将如下所示:

Output
Initializing modules... - groups in modules/droplet-lb Initializing the backend... Initializing provider plugins... - Finding digitalocean/digitalocean versions matching "~> 2.0"... - Installing digitalocean/digitalocean v2.19.0... - Installed digitalocean/digitalocean v2.19.0 (signed by a HashiCorp partner, key ID F82037E524B9C0E8) ... Terraform has been successfully initialized! You may now begin working with Terraform. Try running "terraform plan" to see any changes that are required for your infrastructure. All Terraform commands should now work. If you ever set or change modules or backend configuration for Terraform, rerun this command to reinitialize your working directory. If you forget, other commands will detect it and remind you to do so if necessary.

您可以尝试规划该项目,查看 Terraform 将采取的操作,方法是运行:

  1. terraform plan -var "do_token=${DO_PAT}"

输出将类似于此:

Output
... Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: # 将创建 module.groups.digitalocean_droplet.droplets[0] + resource "digitalocean_droplet" "droplets" { ... + name = "group1-0" ... } # 将创建 module.groups.digitalocean_droplet.droplets[1] + resource "digitalocean_droplet" "droplets" { ... + name = "group1-1" ... } # 将创建 module.groups.digitalocean_droplet.droplets[2] + resource "digitalocean_droplet" "droplets" { ... + name = "group1-2" ... } # 将创建 module.groups.digitalocean_loadbalancer.www-lb + resource "digitalocean_loadbalancer" "www-lb" { ... + name = "lb-group1" ... } Plan: 4 to add, 0 to change, 0 to destroy. ...

此输出详细说明 Terraform 将创建三个 Droplets,命名为 group1-0group1-1group1-2,同时还将创建一个名为 group1-lb 的负载均衡器,该负载均衡器将管理这三个 Droplets 的流量。

您可以通过运行以下命令将项目应用到云端:

  1. terraform apply -var "do_token=${DO_PAT}"

在提示时输入 yes。输出将显示所有操作,并且负载均衡器的 IP 地址也将显示:

Output
module.groups.digitalocean_droplet.droplets[1]: Creating... module.groups.digitalocean_droplet.droplets[0]: Creating... module.groups.digitalocean_droplet.droplets[2]: Creating... ... Apply complete! Resources: 4 added, 0 changed, 0 destroyed. Outputs: loadbalancer-ip = ip_address

您已经创建了一个包含可定制数量的 Droplets 和一个自动配置为管理其流入和流出流量的负载均衡器的模块。

重命名已部署资源

在上一节中,您部署了您定义的模块并将其命名为groups。如果您想要更改其名称,仅仅重命名模块调用将不会产生预期的结果。重命名调用将促使 Terraform 销毁并重新创建资源,导致过多的停机时间。

例如,通过运行以下命令打开main.tf进行编辑:

  1. nano main.tf

groups模块重命名为groups_renamed,如下所示:

main.tf
module "groups_renamed" {
  source = "./modules/droplet-lb"

  droplet_count = 3
  group_name    = "group1"
}

output "loadbalancer-ip" {
  value = module.groups_renamed.lb_ip
}

保存并关闭文件。然后,再次初始化项目:

  1. terraform init

现在您可以规划项目:

  1. terraform plan -var "do_token=${DO_PAT}"

输出会很长,但会类似于这样:

Output
... Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create - destroy Terraform will perform the following actions: # module.groups.digitalocean_droplet.droplets[0] 将被销毁 ... # module.groups_renamed.digitalocean_droplet.droplets[0] 将被创建 ...

Terraform 将提示您销毁现有实例并创建新实例。这是破坏性和不必要的,可能会导致不必要的停机时间。

相反,使用moved块,您可以指示 Terraform 将旧资源移动到新名称下。打开main.tf进行编辑,并在文件末尾添加以下行:

moved {
  from = module.groups
  to   = module.groups_renamed
}

完成后,保存并关闭文件。

现在您可以规划项目:

  1. terraform plan -var "do_token=${DO_PAT}"

当您在main.tf中包含moved块时进行规划,Terraform 希望移动资源,而不是重新创建它们:

Output
Terraform will perform the following actions: # 模块.groups.digitalocean_droplet.droplets[0] 已移动到 module.groups_renamed.digitalocean_droplet.droplets[0] ... # 模块.groups.digitalocean_droplet.droplets[1] 已移动到 module.groups_renamed.digitalocean_droplet.droplets[1] ...

移动资源会改变其在 Terraform 状态中的位置,这意味着实际的云资源不会被修改、销毁或重新创建。

因为您将在下一步中显著修改配置,请通过运行以下命令销毁已部署的资源:

  1. terraform destroy -var "do_token=${DO_PAT}"

在提示时输入 yes。输出将以如下形式结束:

Output
... Destroy complete! Resources: 4 destroyed.

在本节中,您已在 Terraform 项目中重命名了资源而无需在此过程中销毁它们。现在,您将使用 for_eachcount 多次部署同一代码的模块实例。

部署多个模块实例

在本节中,您将使用 countfor_each 多次部署带有自定义配置的 droplet-lb 模块。

使用 count

一种同时部署多个相同模块实例的方法是将要部署的数量传递给count参数,该参数对每个模块都是自动可用的。打开main.tf进行编辑:

  1. nano main.tf

将其修改为以下内容,删除现有的输出定义和moved块:

main.tf
module "groups" {
  source = "./modules/droplet-lb"

  count  = 3

  droplet_count = 3
  group_name    = "group1-${count.index}"
}

通过将count设置为3,您指示Terraform部署该模块三次,每次使用不同的组名称。完成后,保存并关闭文件。

运行以下命令进行部署计划:

  1. terraform plan -var "do_token=${DO_PAT}"

输出将会很长,看起来像这样:

Output
... Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: 将创建模块.groups[0].digitalocean_droplet.droplets[0] ... 将创建模块.groups[0].digitalocean_droplet.droplets[1] ... 将创建模块.groups[0].digitalocean_droplet.droplets[2] ... 将创建模块.groups[0].digitalocean_loadbalancer.www-lb ... 将创建模块.groups[1].digitalocean_droplet.droplets[0] ... 将创建模块.groups[1].digitalocean_droplet.droplets[1] ... 将创建模块.groups[1].digitalocean_droplet.droplets[2] ... 将创建模块.groups[1].digitalocean_loadbalancer.www-lb ... 将创建模块.groups[2].digitalocean_droplet.droplets[0] ... 将创建模块.groups[2].digitalocean_droplet.droplets[1] ... 将创建模块.groups[2].digitalocean_droplet.droplets[2] ... 将创建模块.groups[2].digitalocean_loadbalancer.www-lb ... Plan: 12 to add, 0 to change, 0 to destroy. ...

输出的Terraform细节显示每个三个模块实例都会有三个Droplets和一个关联的负载均衡器。

使用for_each

当您需要更复杂的实例定制,或者实例数量取决于第三方数据(通常以映射形式呈现),而在编写代码时这些数据是未知的时,您可以对模块使用for_each

现在,您将定义一个将组名与Droplet计数配对的映射,并根据它部署droplet-lb实例。通过运行打开main.tf进行编辑:

  1. nano main.tf

修改文件使其如下所示:

main.tf
variable "group_counts" {
  type    = map
  default = {
    "group1" = 1
    "group2" = 3
  }
}

module "groups" {
  source   = "./modules/droplet-lb"
  for_each = var.group_counts

  droplet_count = each.value
  group_name    = each.key
}

首先定义了一个名为group_counts的映射,其中包含给定组应具有的Droplets数量。然后,调用模块droplet-lb,但指定for_each循环应该在var.group_counts上操作,这是您刚刚定义的映射。droplet_count接受each.value,即当前对的值,即当前组的Droplets数量。group_name接收组的名称。

完成后保存并关闭文件。

尝试运行以下命令应用配置:

  1. terraform plan -var "do_token=${DO_PAT}"

输出将详细说明Terraform将执行的操作,以创建具有它们的Droplets和负载均衡器的两个组:

Output
... Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: # 将创建 module.groups["group1"].digitalocean_droplet.droplets[0] ... # 将创建 module.groups["group1"].digitalocean_loadbalancer.www-lb ... # 将创建 module.groups["group2"].digitalocean_droplet.droplets[0] ... # 将创建 module.groups["group2"].digitalocean_droplet.droplets[1] ... # 将创建 module.groups["group2"].digitalocean_droplet.droplets[2] ... # 将创建 module.groups["group2"].digitalocean_loadbalancer.www-lb ...

在此步骤中,您使用了 countfor_each 来部署同一模块的多个自定义实例,这些实例来自相同的代码。

结论

在本教程中,您创建并部署了 Terraform 模块。您使用模块将逻辑链接的资源分组在一起,并对其进行了定制,以从中心代码定义中部署多个不同的实例。您还使用输出来显示模块中包含的资源的属性。

如果您想了解更多关于 Terraform 的信息,请查看我们的如何使用 Terraform 管理基础设施系列。

Source:
https://www.digitalocean.com/community/tutorials/how-to-build-a-custom-terraform-module