如何使用Terraform模块和模板创建可重用的基础架构
作为 Write for DOnations 计划的一部分,作者选择了 Free and Open Source Fund 来接受捐赠。
介绍
Infrastructure as Code (IAC) 的主要好处之一是重用定义的基础设施的一部分。 在 Terraform 中,您可以使用模块将逻辑连接的组件封装到一个实体中,并使用您定义的输入变量对其进行自定义。 通过使用模块来定义您的基础架构,您可以通过仅将不同的值传递给相同的模块来分离开发、暂存和生产环境,从而最大限度地减少代码重复并最大限度地简洁。
您不仅限于使用您的自定义模块。 Terraform Registry 已集成到 Terraform 中,并列出了您可以通过在 required_providers
部分中定义立即将它们合并到项目中的模块和提供程序。 引用公共模块可以加快您的工作流程并减少代码重复。 如果您有一个有用的模块并想与世界分享它,您可以考虑将其发布到注册表中以供其他开发人员使用。
在本教程中,您将探索在 Terraform 项目中定义和重用代码的一些方法。 您将引用 Terraform Registry 中的模块,使用模块分离开发和生产环境,了解模板及其使用方式,并使用 depends_on
元参数显式指定资源依赖关系。
先决条件
- DigitalOcean 个人访问令牌,您可以通过 DigitalOcean 控制面板创建它。 您可以在 DigitalOcean 产品文档中找到说明,如何创建个人访问令牌。
- Terraform 安装在您的本地计算机上,并使用 DigitalOcean 提供程序 设置了一个项目。 完成 How To Use Terraform with DigitalOcean 教程的 Step 1 和 Step 2 并确保将项目文件夹命名为
terraform-reusability
,而不是loadbalance
。 在 Step 2 期间,不要包含pvt_key
变量和 SSH 密钥资源。 droplet-lb
模块在terraform-reusability
中的modules
下可用。 按照 如何构建自定义模块 教程进行操作,直到droplet-lb
模块功能完整。 (也就是说,直到 Creating a Module 部分中的cd ../..
命令。)- 了解 Terraform 项目结构方法。 有关更多信息,请参阅我们的教程 如何构建 Terraform 项目 。
- (可选)两个单独的域,其名称服务器指向您的注册商的 DigitalOcean。 您的域不得添加到您的 DigitalOcean 帐户。 请参阅 如何从公共域注册商 指向 DigitalOcean 名称服务器进行设置。 请注意,如果您不打算部署您将通过本教程创建的项目,则无需执行此操作。
注意: 本教程已使用 Terraform 1.0.2
进行测试。
分离开发和生产环境
在本节中,您将使用模块来分离您的目标部署环境。 您将根据更 复杂的项目 的结构来安排这些。 您将创建一个包含两个模块的项目:一个将定义 Droplet 和负载均衡器,另一个将设置 DNS 域记录。 之后,您将为两个不同的环境(dev
和 prod
)编写配置,它们将调用相同的模块。
创建 dns-records
模块
作为先决条件的一部分,您在 terraform-reusability
下设置了初始项目,并在 modules
下的自己的子目录中创建了 droplet-lb
模块。 您现在将设置第二个模块,称为 dns-records
,其中包含变量、输出和资源定义。 在 terraform-reusability
目录中,运行以下命令创建 dns-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" {}
保存并关闭文件。 现在,您将在名为 records.tf
的文件中定义域和随附的 A
和 CNAME
记录。 通过运行创建并打开它进行编辑:
nano records.tf
添加以下资源定义:
terraform-reusability/modules/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 = "@" }
首先,您将域名添加到您的 DigitalOcean 帐户。 云会自动将三个 DigitalOcean 域名服务器添加为 NS
记录。 您提供给 Terraform 的域名不得已存在于您的 DigitalOcean 帐户中,否则 Terraform 将在基础设施创建期间显示错误。
然后,您为您的域定义一个 A
记录,将它(@
作为 value
表示真正的域名,没有子域)路由到作为变量提供的 IP 地址ipv4_address
。 初始化模块实例时将传入实际 IP 地址。 为了完整起见,下面的 CNAME
记录指定 www
子域也应该指向同一个域。 完成后保存并关闭文件。
接下来,您将定义此模块的输出。 输出将显示创建记录的 FQDN(完全限定域名)。 创建并打开outputs.tf
进行编辑:
nano outputs.tf
添加以下行:
terraform-reusability/modules/dns-records/outputs.tf
output "A_fqdn" { value = digitalocean_record.domain_A.fqdn } output "CNAME_fqdn" { value = digitalocean_record.domain_CNAME.fqdn }
完成后保存并关闭文件。
定义了变量、DNS 记录和输出后,您需要指定的最后一件事是此模块的提供程序要求。 您将在名为 provider.tf
的文件中指定 dns-records
模块需要 digitalocean
提供程序。 创建并打开它进行编辑:
nano provider.tf
添加以下行:
terraform-reusability/modules/dns-records/provider.tf
terraform { required_providers { digitalocean = { source = "digitalocean/digitalocean" version = "~> 2.0" } } }
完成后,保存并关闭文件。 既然已经定义了 digitalocean
提供程序,那么 dns-records
模块的功能就完成了。
创建不同的环境
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 ├─ provider.tf
到目前为止,您的项目中有两个模块:一个是您刚刚创建的 (dns-records
),另一个是您作为先决条件的一部分创建的 (droplet-lb
)。
为了方便不同的环境,您将 dev
和 prod
环境配置文件存储在名为 environments
的目录下,该目录将驻留在项目的根目录中。 两种环境都将调用相同的两个模块,但参数值不同。 这样做的好处是,当模块将来在内部发生变化时,您只需要更新您传入的值。
首先,通过运行导航到项目的根目录:
cd ../..
然后,在environments
下同时创建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-lb
和 dns-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 = "~> 2.0" } } } variable "do_token" {} provider "digitalocean" { token = var.do_token }
在此代码中,您需要 digitalocean
提供程序可用并将 do_token
变量传递给其实例。 保存并关闭文件。
通过运行初始化配置:
terraform init
您将收到以下输出:
OutputInitializing modules... - dns in ../../modules/dns-records - droplets in ../../modules/droplet-lb Initializing the backend... Initializing provider plugins... - Finding digitalocean/digitalocean versions matching "~> 2.0"... - Installing digitalocean/digitalocean v2.10.1... - Installed digitalocean/digitalocean v2.10.1 (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/cli/plugins/signing.html Terraform has created a lock file .terraform.lock.hcl to record the provider selections it made above. Include this file in your version control repository so that Terraform can guarantee to make the same selections by default when you run "terraform init" in the future. 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-reusability/environments/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... 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.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 = "@" } # 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。
注意: 您可以使用以下命令将此配置应用于 dev
和 prod
环境:
terraform apply -var "do_token=${DO_PAT}"
要销毁它,请运行以下命令并在出现提示时输入 yes
:
terraform destroy -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 ├─ provider.tf
添加的是 environments
目录,其中包含 dev
和 prod
环境的代码。
这种方法的好处是对模块的进一步更改会自动传播到项目的所有区域。 除非对模块输入进行任何可能的定制,否则这种方法不会重复,并尽可能提高可重用性,即使跨部署环境也是如此。 总体而言,这减少了混乱,并允许您使用版本控制系统跟踪修改。
在本教程的最后两节中,您将回顾 depends_on
元参数和 templatefile
函数。
声明依赖以有序构建基础设施
在计划行动时,Terraform 会自动尝试识别现有的依赖关系并将它们构建到其依赖关系图中。 它可以检测到的主要依赖项是明确的引用; 例如,当一个模块的输出值被传递给另一个资源的参数时。 在这种情况下,模块必须首先完成其部署以提供输出值。
Terraform 无法检测到的依赖项是隐藏的——它们具有副作用和无法从代码中推断出的相互引用。 例如,当一个对象不依赖于存在,而是依赖于另一个对象的行为,并且不从代码访问其属性时。 为了克服这个问题,您可以使用 depends_on
以显式方式手动指定依赖项。 由于 Terraform 0.13
,您还可以在模块上使用 depends_on
来强制在部署模块本身之前完全部署列出的资源。 可以对每种资源类型使用 depends_on
元参数。 depends_on
还将接受其指定资源所依赖的其他资源的列表。
depends_on
接受对其他资源的引用列表。 它的语法如下所示:
resource "resource_type" "res" { depends_on = [...] # List of resources # Parameters... }
请记住,您应该只使用 depends_on
作为最后的选择。 如果使用,则应妥善记录,因为资源所依赖的行为可能不会立即显而易见。
在本教程的上一步中,您没有使用 depends_on
指定任何显式依赖项,因为您创建的资源没有无法从代码中推断出的副作用。 Terraform 能够检测到您编写的代码中的引用,并将相应地安排资源进行部署。
使用模板进行自定义
在 Terraform 中,模板化是在适当的地方替换表达式的结果,例如在资源上设置属性值或构造字符串时。 您已在前面的步骤和教程先决条件中使用它来动态生成 Droplet 名称和其他参数值。
当替换字符串中的值时,这些值被指定并被 ${}
包围。 模板替换经常在循环中使用,以方便对创建的资源进行定制。 它还允许通过替换资源属性中的输入来定制模块。
Terraform 提供了 templatefile
函数,它接受两个参数:要从磁盘读取的文件和与其值配对的变量映射。 它返回的值是用替换的表达式呈现的文件内容——就像 Terraform 在规划或应用项目时通常会做的那样。 因为函数不是依赖图的一部分,所以文件不能从项目的另一部分动态生成。
假设名为 droplets.tmpl
的模板文件内容如下:
%{ for address in addresses ~} ${address}:80 %{ endfor ~}
较长的声明必须用 %{}
包围,就像 for
和 endfor
声明的情况,分别表示 for
循环的开始和结束. 在调用函数并提供实际值之前,不知道 droplets
变量的内容和类型,如下所示:
templatefile("${path.module}/droplets.tmpl", { addresses = ["192.168.0.1", "192.168.1.1"] })
此 templatefile
调用将返回以下值:
Output192.168.0.1:80 192.168.1.1:80
此功能有其用例,但并不常见。 例如,当部分配置必须以专有格式存在但依赖于其余值并且必须动态生成时,您可以使用它。 在大多数情况下,最好尽可能直接在 Terraform 代码中指定所有配置参数。
结论
在本文中,您在示例 Terraform 项目中最大限度地重用了代码。 主要的方式是将常用的特性和配置打包成一个可定制的模块,在需要的时候使用。 通过这样做,您不会复制底层代码(这可能容易出错)并实现更快的周转时间,因为修改模块几乎是引入更改所需要做的所有事情。
您不仅限于自己的模块。 如您所见,Terraform Registry 提供了第三方模块和提供程序,您可以将它们合并到您的项目中。
本教程是 如何使用 Terraform 管理基础架构系列的一部分。 该系列涵盖了许多 Terraform 主题,从首次安装 Terraform 到管理复杂的项目。