如何構建自定義 Terraform 模塊

作者選擇了自由開源基金作為寫作獎勵計劃的捐贈對象。

介紹

Terraform模塊允許您將基礎設施的不同資源分組為單一的統一資源。您可以稍後重複使用它們並進行可能的自定義,而無需每次需要它們時都重複定義資源,這對於大型和結構復雜的項目非常有益。您可以使用您定義的輸入變量自定義模塊實例,並使用輸出從中提取信息。除了創建自己的自定義模塊外,您還可以使用公開發布在Terraform Registry上的預製模塊。開發人員可以像使用您創建的模塊一樣使用和自定義它們,但它們的源代碼存儲在並從雲端提取。

在本教程中,您將創建一個Terraform模塊,用於在負載平衡器後設置多個Droplet以實現冗余。您還將使用Hashicorp配置語言(HCL)的for_eachcount循環功能同時部署多個自定義模塊實例。

先决条件

注意:此教程已专门针对 Terraform 1.1.3 进行了测试。

模組結構與好處

在這一部分,您將了解模組帶來的好處,它們通常放置在項目中的位置,以及它們應該如何組織。

自定義的 Terraform 模組被創建來封裝在較大的項目中經常一起使用和部署的相關組件。它們是自包含的,僅捆綁它們需要的資源、變數和提供者。

模組通常存儲在項目根目錄中的中央文件夾中,每個模組在其下面的各自子文件夾中。為了保持模組之間的清晰分離,始終將它們設計為具有單一目的,並確保它們永遠不包含子模組。

當您發現自己在重復使用資源方案並且不經常進行自定義時,創建模組是有用的。將單個資源打包為模組可能是多餘的,並逐漸削弱了整體架構的簡單性。

對於小型開發和測試項目,不需要將模組納入其中,因為在這些情況下它們不會帶來太多改進。由於其可自定義性,模組是複雜結構項目的構建元素。開發人員在較大的項目中使用模組,因為它們避免了代碼重複的顯著優勢。模組還提供了只需要在一個地方進行修改就可以在基礎設施的其餘部分中傳播的好處。

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

创建模块

在本节中,您将定义多个 Droplet 和一个 Load Balancer 作为 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
}

在此聲明中,您調用位於指定目錄 sourcedroplet-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: 模块组[0]。digitalocean_droplet。droplets[0]将被创建 ... 模块组[0]。digitalocean_droplet。droplets[1]将被创建 ... 模块组[0]。digitalocean_droplet。droplets[2]将被创建 ... 模块组[0]。digitalocean_loadbalancer。www-lb将被创建 ... 模块组[1]。digitalocean_droplet。droplets[0]将被创建 ... 模块组[1]。digitalocean_droplet。droplets[1]将被创建 ... 模块组[1]。digitalocean_droplet。droplets[2]将被创建 ... 模块组[1]。digitalocean_loadbalancer。www-lb将被创建 ... 模块组[2]。digitalocean_droplet。droplets[0]将被创建 ... 模块组[2]。digitalocean_droplet。droplets[1]将被创建 ... 模块组[2]。digitalocean_droplet。droplets[2]将被创建 ... 模块组[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: # 模块.groups["group1"].digitalocean_droplet.droplets[0] 将被创建 ... # 模块.groups["group1"].digitalocean_loadbalancer.www-lb 将被创建 ... # 模块.groups["group2"].digitalocean_droplet.droplets[0] 将被创建 ... # 模块.groups["group2"].digitalocean_droplet.droplets[1] 将被创建 ... # 模块.groups["group2"].digitalocean_droplet.droplets[2] 将被创建 ... # 模块.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