如何使用 Terraform 模块和模板创建可重用的基础设施

作为Write for DOnations计划的一部分,作者选择了免费和开源基金来接受捐赠

介绍

基础设施即代码 (IAC)的主要好处之一是重用已定义基础设施的部分内容。在 Terraform 中,您可以使用模块将逻辑连接的组件封装到一个实体中,并使用您定义的输入变量对其进行自定义。通过使用模块在高层定义您的基础设施,您可以通过仅将不同的值传递给相同的模块来分离开发、暂存和生产环境,从而最大限度地减少代码重复并最大限度地提高简洁性。

您不仅限于使用自定义模块。Terraform Registry集成到 Terraform 中,并列出了您可以通过在required_providers部分定义它们立即合并到项目中的模块和提供程序引用公共模块可以加快您的工作流程并减少代码重复。如果您有一个有用的模块并希望与全世界分享,您可以考虑将其发布到注册表上以供其他开发人员使用。

在本教程中,我们将考虑在 Terraform 项目中定义和重用代码的一些方法。您将引用 Terraform Registry 中的模块,使用模块分离开发和生产环境,了解模板及其使用方式,以及如何使用depends_onmeta 参数显式指定资源依赖关系

先决条件

  • DigitalOcean 个人访问令牌,您可以通过 DigitalOcean 控制面板创建。您可以在以下位置找到相关说明:如何生成个人访问令牌
  • Terraform 安装在您的本地计算机上,并使用DigitalOcean provider设置了一个项目完成如何在 DigitalOcean 中使用 Terraform教程的第 1 步第 2 步并确保将项目文件夹命名. 步骤 2 中,不要包含变量和 SSH 密钥资源。terraform-reusabilityloadbalancepvt_key
  • droplet-lb下可用的模块modulesterraform-reusability遵循如何构建自定义模块教程并完成它,直到droplet-lb模块在功能上完成。(也就是说,直到“创建模块”部分中cd ../..命令。)
  • 了解 Terraform 项目结构方法。有关详细信息,请参阅如何构建 Terraform 项目
  • (可选)两个单独的域,其名称服务器在您的注册商处指向 DigitalOcean。请参阅如何从公共域注册商指向 DigitalOcean 域名服务器教程进行设置。请注意,如果您不打算部署将通过本教程创建的项目,则无需执行此操作。

注意:我们已经使用 Terraform 专门测试了本教程0.13

分离开发和生产环境

在本节中,您将使用模块来实现目标部署环境之间的分离。您将根据更复杂项目的结构来安排这些您将首先创建一个包含两个模块的项目,其中一个将定义 Droplet 和负载均衡器,另一个将设置 DNS 域记录。之后,您将为两个不同的环境(devprod编写配置,这将调用相同的模块。

创建dns-records模块

作为先决条件的一部分,你已经下设立的项目开始terraform-reusability,创造了droplet-lb在自己的子目录下的模块modules您现在将设置名为 的第二个模块dns-records,其中包含变量、输出和资源定义。假设您在terraform-reusabilitydns-records通过运行创建

  • mkdir modules/dns-records

导航到它:

  • cd modules/dns-records

此模块将包含您的域的定义和稍后将指向负载均衡器的 DNS 记录。您将首先定义变量,这些变量将成为该模块将公开的输入。您将它们存储在一个名为variables.tf. 创建它以进行编辑:

  • nano variables.tf

添加以下变量定义:

terraform-reusability/modules/dns-records/variables.tf
variable "domain_name" {}
variable "ipv4_address" {}

保存并关闭文件。现在,您将定义域和随行ACNAME在一个名为文件中的记录records.tf通过运行创建并打开它进行编辑:

  • nano records.tf

添加以下资源定义:

terraform-可重用性/模块/dns-records/records.tf
resource "digitalocean_domain" "domain" {
  name = var.domain_name
}

resource "digitalocean_record" "domain_A" {
  domain = digitalocean_domain.domain.name
  type   = "A"
  name   = "@"
  value  = var.ipv4_address
}

resource "digitalocean_record" "domain_CNAME" {
  domain = digitalocean_domain.domain.name
  type   = "CNAME"
  name   = "www"
  value  = var.ipv4_address
}

首先,在您的 DigitalOcean 帐户中为您的域名定义域。云会自动添加三个 DigitalOcean 域名服务器作为NS记录。然后,您A为您的域定义一条记录,将其路由(@asvalue表示真正的域名,没有子域)到作为变量提供的 IP 地址ipv4_address为完整起见,后面的CNAME记录指定www子域也应指向相同的 IP 地址。完成后保存并关闭文件。

接下来,您将定义此模块的输出。输出将显示所创建记录的 FQDN(完全限定域名)。创建并打开outputs.tf以进行编辑:

  • nano outputs.tf

添加以下几行:

terraform-可重用性/模块/dns-records/outputs.tf
output "A_fqdn" {
  value = digitalocean_record.domain_A.fqdn
}

output "CNAME_fqdn" {
  value = digitalocean_record.domain_CNAME.fqdn
}

完成后保存并关闭文件。

定义变量、DNS 记录和输出后,您需要指定的最后一件事是此模块的提供程序要求。你会指定该dns-records模块需要digitalocean在一个名为文件提供provider.tf创建并打开它进行编辑:

  • nano provider.tf

添加以下几行:

terraform-可重用性/模块/dns-records/provider.tf
terraform {
  required_providers {
    digitalocean = {
      source = "digitalocean/digitalocean"
    }
  }
  required_version = ">= 0.13"
}

完成后,保存并关闭文件。dns-records模块现在需要digitalocean提供程序并且功能完整。

创建不同的环境

以下是该terraform-reusability项目的当前结构

terraform_reusability/
├─ modules/
│  ├─ dns-records/
│  │  ├─ outputs.tf
│  │  ├─ provider.tf
│  │  ├─ records.tf
│  │  ├─ variables.tf
│  ├─ droplet-lb/
│  │  ├─ droplets.tf
│  │  ├─ lb.tf
│  │  ├─ outputs.tf
│  │  ├─ provider.tf
│  │  ├─ variables.tf
├─ main.tf
├─ provider.tf

到目前为止,您的项目中有两个模块:您刚刚创建的模块 ( dns-records) 和droplet-lb作为先决条件的一部分创建的模块。

为了方便不同的环境中,您将存储devprod环境配置文件名为目录下environments,这将驻留在项目的根。两种环境都将调用相同的两个模块,但具有不同的参数值。这样做的好处是当模块将来在内部发生变化时,您只需要更新您传入的值。

首先,通过运行导航到项目的根目录:

  • cd ../..

然后,dev同时prod在环境下创建目录:

  • mkdir -p environments/dev && mkdir environments/prod

-p参数的命令mkdir创建在给定的路径中的所有目录。

导航到该dev目录,因为您将首先配置该环境:

  • cd environments/dev

您将代码存储在名为 的文件中main.tf,因此创建它以进行编辑:

  • nano main.tf

添加以下几行:

terraform-reusability/environments/dev/main.tf
module "droplets" {
  source   = "../../modules/droplet-lb"

  droplet_count = 2
  group_name    = "dev"
}

module "dns" {
  source   = "../../modules/dns-records"

  domain_name   = "your_dev_domain"
  ipv4_address  = module.droplets.lb_ip
}

在这里你调用和配置两个模块,droplet-lbdns-records,这将共同导致两个 Droplet 的创建。它们前面有一个负载均衡器;所提供域的 DNS 记录设置为指向该负载均衡器。请记住将环境替换your_dev_domain为您想要的域名dev,然后保存并关闭文件。

接下来,您将配置 DigitalOcean 提供程序并为其创建一个变量,以便能够接受您作为先决条件的一部分创建的个人访问令牌。打开一个名为 的新文件provider.tf进行编辑:

  • nano provider.tf

添加以下几行:

terraform-reusability/environments/dev/provider.tf
terraform {
  required_providers {
    digitalocean = {
      source = "digitalocean/digitalocean"
      version = "1.22.2"
    }
  }
}

variable "do_token" {}

provider "digitalocean" {
  token = var.do_token
}

在此代码中,您要求digitalocean提供程序可用并将do_token变量传递给其实例。保存并关闭文件。

通过运行初始化配置:

  • terraform init

您将收到以下输出:

Output
Initializing modules... - dns in ../../modules/dns-records - droplets in ../../modules/droplet-lb Initializing the backend... Initializing provider plugins... - Finding latest version of digitalocean/digitalocean... - Installing digitalocean/digitalocean v2.0.2... - Installed digitalocean/digitalocean v2.0.2 (signed by a HashiCorp partner, key ID F82037E524B9C0E8) Partner and community providers are signed by their developers. If you'd like to know more about provider signing, you can read about it here: https://www.terraform.io/docs/plugins/signing.html The following providers do not have any version constraints in configuration, so the latest version was installed. To prevent automatic upgrades to new major versions that may contain breaking changes, we recommend adding version constraints in a required_providers block in your configuration, with the constraint strings suggested below. * digitalocean/digitalocean: version = "~> 2.0.2" 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.

prod环境的配置类似。通过运行导航到其目录:

  • cd ../prod

创建并打开main.tf以进行编辑:

  • nano main.tf

添加以下几行:

terraform-可重用性/环境/prod/main.tf
module "droplets" {
  source   = "../../modules/droplet-lb"

  droplet_count = 5
  group_name    = "prod"
}

module "dns" {
  source   = "../../modules/dns-records"

  domain_name   = "your_prod_domain"
  ipv4_address  = module.droplets.lb_ip
}

这与您的dev代码之间的区别在于将部署五个 Droplet。此外,您应该用您的prod域名替换的域名会有所不同。完成后保存并关闭文件。

然后,从dev以下位置复制提供程序配置

  • cp ../dev/provider.tf .

也初始化此配置:

  • terraform init

此命令的输出将与您上次运行时相同。

您可以尝试通过运行以下命令来规划配置以查看 Terraform 将创建哪些资源:

  • terraform plan -var "do_token=${DO_PAT}"

的输出prod如下:

Output
... An execution plan has been generated and is shown below. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: # module.dns.digitalocean_domain.domain will be created + resource "digitalocean_domain" "domain" { + id = (known after apply) + name = "your_prod_domain" + urn = (known after apply) } # module.dns.digitalocean_record.domain_A will be created + resource "digitalocean_record" "domain_A" { + domain = "your_prod_domain" + fqdn = (known after apply) + id = (known after apply) + name = "@" + ttl = (known after apply) + type = "A" + value = (known after apply) } # module.dns.digitalocean_record.domain_CNAME will be created + resource "digitalocean_record" "domain_CNAME" { + domain = "your_prod_domain" + fqdn = (known after apply) + id = (known after apply) + name = "www" + ttl = (known after apply) + type = "CNAME" + value = (known after apply) } # module.droplets.digitalocean_droplet.droplets[0] will be created + resource "digitalocean_droplet" "droplets" { ... + name = "prod-0" ... } # module.droplets.digitalocean_droplet.droplets[1] will be created + resource "digitalocean_droplet" "droplets" { ... + name = "prod-1" ... } # module.droplets.digitalocean_droplet.droplets[2] will be created + resource "digitalocean_droplet" "droplets" { ... + name = "prod-2" ... } # module.droplets.digitalocean_droplet.droplets[3] will be created + resource "digitalocean_droplet" "droplets" { ... + name = "prod-3" ... } # module.droplets.digitalocean_droplet.droplets[4] will be created + resource "digitalocean_droplet" "droplets" { ... + name = "prod-4" ... } # module.droplets.digitalocean_loadbalancer.www-lb will be created + resource "digitalocean_loadbalancer" "www-lb" { ... + name = "lb-prod" ... Plan: 9 to add, 0 to change, 0 to destroy. ...

这将部署五个带有负载均衡器的 Droplet。它还会创建prod您指定域,其中两个 DNS 记录指向负载均衡器。您也可以尝试为dev环境规划配置– 您会注意到将计划部署两个 Droplet。

注意:您可以使用以下命令将此配置应用于devprod环境:

  • terraform apply -var "do_token=${DO_PAT}"

下面演示了您如何构建此项目:

terraform_reusability/
├─ environments/
│  ├─ dev/
│  │  ├─ main.tf
│  │  ├─ provider.tf
│  ├─ prod/
│  │  ├─ main.tf
│  │  ├─ provider.tf
├─ modules/
│  ├─ dns-records/
│  │  ├─ outputs.tf
│  │  ├─ provider.tf
│  │  ├─ records.tf
│  │  ├─ variables.tf
│  ├─ droplet-lb/
│  │  ├─ droplets.tf
│  │  ├─ lb.tf
│  │  ├─ outputs.tf
│  │  ├─ provider.tf
│  │  ├─ variables.tf
├─ main.tf
├─ provider.tf

添加的是environments目录,其中包含devprod环境的代码

这种方法的好处是对模块的进一步更改会自动传播到项目的所有区域。除非对模块输入进行任何可能的自定义,否则这种方法不是重复的,并且尽可能地提高了可重用性,即使是跨部署环境。总的来说,这减少了混乱,并允许您使用版本控制系统跟踪修改。

在本教程的最后两节中,您将回顾depends_on元参数和templatefile函数。

声明依赖以按顺序构建基础设施

在计划操作时,Terraform 会自动尝试感知现有依赖项并将它们构建到其依赖项图中。它可以检测到的主要依赖项是明确的引用;例如,当模块的输出值传递给另一个资源上的参数时。在这种情况下,模块必须首先完成其部署以提供输出值。

Terraform 无法检测到的依赖项是隐藏的——它们具有副作用和无法从代码推断出的相互引用。一个例子是当一个对象不依赖于存在,而是依赖于另一个对象的行为,并且不从代码访问它的属性时。为了克服这个问题,您可以使用depends_on以显式方式手动指定依赖项。从 Terraform 开始0.13,您还可以使用depends_onon modules 强制在部署模块本身之前完全部署列出的资源。可以对depends_on每种资源类型使用meta 参数。depends_on还将接受其指定资源所依赖的其他资源列表。

在本教程的上一步中,您没有使用 指定任何显式依赖项depends_on,因为您创建的资源没有无法从代码中推断出的副作用。Terraform 能够检测您编写的代码中的引用,并相应地安排资源进行部署。

depends_on接受对其他资源的引用列表。它的语法如下所示:

resource "resource_type" "res" {
  depends_on = [...] # List of resources

  # Parameters...
}

请记住,您只应将其depends_on用作最后的选择。如果使用,则应妥善记录,因为资源所依赖的行为可能不会立即显现。

使用模板进行自定义

在 Terraform 中,模板是在适当的地方替换表达式的结果,例如在资源上设置属性值或构造字符串时。您已在前面的步骤和教程先决条件中使用它来动态生成 Droplet 名称和其他参数值。

替换字符串中的值时,这些值被指定并用 括起来${}模板替换经常在循环中使用,以便于定制所创建的资源。它还允许通过替换资源属性中的输入来定制模块。

Terraform 提供了该templatefile函数,该函数接受两个参数:要从磁盘读取的文件和与其值配对的变量映射。它返回的值是用替换的表达式呈现的文件的内容——就像 Terraform 在规划或应用项目时通常所做的那样。由于函数不是依赖关系图的一部分,因此无法从项目的其他部分动态生成该文件。

想象一下调用的模板文件内容droplets.tmpl如下:

%{ for address in addresses ~}
${address}:80
%{ endfor ~}

更长的声明必须用 包围%{},就像forendfor声明一样,它们分别表示for循环的开始和结束droplets调用函数并提供实际值之前变量的内容和类型是未知的,如下所示:

templatefile("${path.module}/droplets.tmpl", { addresses = ["192.168.0.1", "192.168.1.1"] })

templatefile调用将返回的值如下:

Output
192.168.0.1:80 192.168.1.1:80

这个函数有它的用例,但它们并不常见。例如,当配置的一部分需要以专有格式存在时,您可以使用它,但它依赖于其余的值并且必须动态生成。在大多数情况下,最好尽可能直接在 Terraform 代码中指定所有配置参数。

结论

在本文中,您在一个示例 Terraform 项目中最大限度地重用了代码。主要方式是将常用的特性和配置打包成一个可定制的模块,并在需要时使用。通过这样做,您不会重复底层代码(这可能容易出错)并实现更快的周转时间,因为修改模块几乎是您引入更改所需要做的全部。

您不仅限于自己的模块。如您所见,Terraform Registry提供了可以合并到项目中的第三方模块和提供程序。

查看如何使用 Terraform 系列管理基础设施的其余部分

觉得文章有用?

点个广告表达一下你的爱意吧 !😁