如何构建Terraform项目

简介

根据用途和预估复杂度合理地构建Terraform项目是确保其在日常操作中的可维护性和可扩展性的关键。恰当地组织代码文件系统性方法对于确保项目在部署过程中保持可扩展性以及对您和您的团队可用至关重要。

在本教程中,您将了解如何根据其通用目的和复杂度构建Terraform项目。然后,您将使用Terraform的更多常用特性:变量、本地变量、数据源和提供程序,创建一个具有简单结构的项目。最后,您的项目将在DigitalOcean上部署一个Ubuntu 20.04服务器(Droplet),安装一个Apache网页服务器,并将您的域名指向网页服务器。

先决条件

  • 一个DigitalOcean个人访问令牌,您可以通过DigitalOcean控制面板创建。您可以在DigitalOcean产品文档中找到指导,如何创建个人访问令牌

  • 一个无密码的SSH密钥添加到您的DigitalOcean账户中,您可以按照如何使用SSH密钥与DigitalOcean Droplet创建。

  • Terraform已安装在您的本地机器上。有关根据您的操作系统进行操作的说明,请参阅如何使用Terraform与DigitalOcean教程的第步1

  • Python 3已经安装在你的本地机器上。你可以完成安装Python 3并设置本机编程环境的第一步为您的操作系统。
  • 一个完全注册的域名添加到您的DigitalOcean帐户。有关如何做到这一点的说明,请参阅官方文档

注意:本教程已特别测试与Terraform1.0.2

了解Terraform项目的结构

在这一部分中,您将了解Terraform如何定义项目,如何组织基础设施代码,以及何时选择特定的方法。您还将学习Terraform工作空间、它们的作用以及Terraform如何存储状态。

在Terraform中,资源是被定义为云服务中的一个实体(例如DigitalOcean Droplets),并在代码中根据指定的和隐含的属性进行创建。多个资源通过其相互连接关系构成了基础设施。资源是Terraform代码中定义的一个实体,它根据指定的和隐含的属性被创建。多个资源通过它们的相互连接关系构成了基础设施。

Terraform使用一种特殊的编程语言来定义基础设施,称为哈希科公司配置语言(HCL)。HCL代码通常保存在以.tf结尾的文件中。Terraform项目是指任何包含.tf文件的目录,并且已经使用init命令进行了初始化,该命令设置Terraform缓存和默认本地状态。

Terraform使用状态机制来跟踪实际部署在云中的资源。状态存储在后端(本地磁盘或远程文件存储云服务或专用状态管理软件)上,以确保最佳冗余性和可靠性。您可以阅读更多有关不同后端的信息,请参阅Terraform文档

项目工作区允许您在同一个后台拥有多个状态,这些状态与相同的配置相关联。这使您能够部署相同基础设施的多个独立实例。每个项目都有一个名为default的工作区开始 – 如果您没有明确创建或切换到另一个工作区,这将默认使用。

模块在Terraform中(在其他编程语言中的库类似)是参数化的代码容器,包含多个资源声明。它们允许您将基础设施的常见部分抽象化,并稍后用不同的输入重新使用。

Terraform项目还可以包括用于动态数据输入的外部代码文件,这些文件可以解析JSON输出的CLI命令,并在资源声明中提供使用。在本教程中,您将使用Python脚本进行此操作。

现在您已经知道Terraform项目包含什么,让我们回顾一下两种一般的Terraform项目结构方法。

简单结构

简单的结构适合于小型的测试项目,这类项目资源类型多样且变量较少。它包含一些配置文件,通常每种资源类型有一个(或者有一些辅助文件与主文件一起),并且没有自定义模块,因为大多数资源都是独一无二的,不足以通用和复用。遵循这种结构,大部分代码都存储在同一个目录中,彼此相邻。这些项目通常有一些变量(如访问云的API密钥)并且可能使用动态数据输入和其他Terraform和HCL特性,尽管不是主要的。

作为一个这种方法文件结构的例子,这就是你在这篇教程中构建的项目最终的样子:

.
└── tf/
    ├── versions.tf
    ├── variables.tf
    ├── provider.tf
    ├── droplets.tf
    ├── dns.tf
    ├── data-sources.tf
    └── external/
        └── name-generator.py

由于这个项目将部署一个Apache web服务器Droplet并设置DNS记录,因此项目变量定义、DigitalOcean Terraform提供者、Droplet和DNS记录将分别存储在它们的文件中。最小必需的Terraform和DigitalOcean提供者版本将在versions.tf中指定,而生成Droplet名称的Python脚本(将在data-sources.tf中作为动态数据源使用)将存储在external文件夹中,以与HCL代码分离。

复杂结构

与简单结构相反,这种方法适用于大型项目,这些项目具有清晰定义的子目录结构,其中包含多个复杂性各异的模块,以及常规代码。这些模块可以相互依赖。结合版本控制系统,这些项目可以充分利用工作区。这种方法适用于管理多个应用程序的大型项目,同时尽可能多地复用代码。

开发、暂存、质量保证和生产基础设施实例也可以通过依赖公共模块,在不同的目录下置于同一项目的同一目录下,从而消除重复代码,使项目成为中央真相来源。以下是具有更复杂结构的示例项目的文件结构,包含多个部署应用程序、Terraform模块和目标云环境:

.
└── tf/
    ├── modules/
    │   ├── network/
    │   │   ├── main.tf
    │   │   ├── dns.tf
    │   │   ├── outputs.tf
    │   │   └── variables.tf
    │   └── spaces/
    │       ├── main.tf
    │       ├── outputs.tf
    │       └── variables.tf
    └── applications/
        ├── backend-app/
        │   ├── env/
        │   │   ├── dev.tfvars
        │   │   ├── staging.tfvars
        │   │   ├── qa.tfvars
        │   │   └── production.tfvars
        │   └── main.tf
        └── frontend-app/
            ├── env/
            │   ├── dev.tfvars
            │   ├── staging.tfvars
            │   ├── qa.tfvars
            │   └── production.tfvars
            └── main.tf

关于如何使用Terraform管理基础设施的系列文章如何使用Terraform管理基础设施中进一步探讨了这种方法。

现在你知道了什么是Terraform项目,如何根据感知复杂度最佳地组织它,以及Terraform工作区的作用。在下一步中,你将创建一个具有简单结构的项目,该项目将为你的域名设置DNS记录并部署一个安装有Apache网络服务器的Droplet。你将首先用DigitalOcean提供者和变量初始化你的项目,然后继续定义Droplet,一个动态数据源以提供其名称,以及用于部署的DNS记录。

第一步 — 设置你的初始项目

在本节中,你将向你的项目添加DigitalOcean Terraform提供者,定义项目变量,并声明一个DigitalOcean提供者实例,这样Terraform就能够连接到你的账户。

首先,使用以下命令创建一个Terraform项目的目录:

  1. mkdir ~/apache-droplet-terraform

进入该目录:

  1. cd ~/apache-droplet-terraform

由于这个项目将遵循简单的结构化方法,你将把提供者、变量、Droplet和DNS记录代码分别存储在不同的文件中,文件结构如上一节所示。首先,你需要向你的项目添加DigitalOcean Terraform提供者作为一个必需的提供者。

创建一个名为versions.tf的文件,并通过运行以下命令打开它进行编辑:

  1. nano versions.tf

添加以下行:

~/apache-droplet-terraform/versions.tf
terraform {
  required_providers {
    digitalocean = {
      source = "digitalocean/digitalocean"
      version = "~> 2.0"
    }
  }
}

在这个terraform块中,你列出了所需的提供者(DigitalOcean,版本2.x)。完成后,保存并关闭文件。

然后,在variables.tf文件中定义你的项目将暴露的变量,采用将不同资源类型存储在单独的代码文件中的方法:

  1. nano variables.tf

添加以下变量:

~/apache-droplet-terraform/variables.tf
variable "do_token" {}
variable "domain_name" {}

保存并关闭文件。

`do_token`变量将持有您的DigitalOcean个人访问令牌,而`domain_name`将指定您所需的域名。部署的Droplet将自动安装由SSH指纹标识的SSH密钥。

接下来,让我们为该项目定义一个DigitalOcean提供商实例。您将其存储在一个名为`provider.tf`的文件中。通过运行以下命令创建并打开它以进行编辑:

  1. nano provider.tf

添加提供商:

~/apache-droplet-terraform/provider.tf
provider "digitalocean" {
  token = var.do_token
}

完成定义后保存并退出。您已经定义了`digitalocean`提供商,它对应于您之前在`provider.tf`中指定的所需提供商,并将其令牌设置为变量的值,该值将在运行时提供。

在此步骤中,您为您的项目创建了一个目录,请求DigitalOcean提供商可用,声明了项目变量,并设置了连接到DigitalOcean提供商实例的认证令牌,该令牌将在稍后提供。接下来,您将编写一个脚本来为您的项目定义生成动态数据。

第2步 — 创建用于动态数据的Python脚本

在继续定义Droplet之前,您将创建一个Python脚本,该脚本将动态生成Droplet的名称,并声明一个数据源资源以解析它。名称将通过连接一个常量字符串(web)与本地机器的当前时间来生成,时间以UNIX纪元格式表示。命名脚本在根据命名方案生成多个Droplet时很有用,以便轻松区分它们。

您将脚本保存在名为name-generator.py的文件中,该文件位于名为external的目录中。首先,通过运行:

  1. mkdir external

位于项目根目录的external目录将存储非HCL代码文件,如您将编写的Python脚本。

external目录下创建name-generator.py并打开它进行编辑:

  1. nano external/name-generator.py

添加以下代码:

external/name-generator.py
import json, time

fixed_name = "web"
result = {
  "name": f"{fixed_name}-{int(time.time())}",
}

print(json.dumps(result))

此Python脚本导入了jsontime模块,声明了一个名为字典result变量,并将name键的值设置为一个插值字符串,该字符串将fixed_name与运行它的机器的当前UNIX时间相结合。然后,将result转换为JSON格式,并输出到stdout。每次运行脚本时,输出都会有所不同:

Output
{"name": "web-1597747959"}

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

注意:对于大型和复杂的结构化项目,需要更多地考虑外部数据源的创建和使用,特别是在可移植性和错误处理方面。Terraform期望执行的程序将人类可读的错误消息写入stderr,并以非零状态优雅地退出,这一点在本步骤中没有展示,因为任务的简单性。另外,它期望程序没有副作用,以便可以多次重新运行。

有关Terraform期望的数据源的更多信息,请访问官方文档关于数据源的部分。

现在脚本已经准备好,您可以定义数据源,它将从脚本中提取数据。您将数据源存储在名为data-sources.tf的文件中,该文件位于项目的根目录下,按照简单的结构化方法。

通过运行以下命令来创建并编辑它:

  1. nano data-sources.tf

添加以下定义:

~/apache-droplet-terraform/data-sources.tf
data "external" "droplet_name" {
  program = ["python3", "${path.module}/external/name-generator.py"]
}

保存并关闭文件。

这个数据源称为droplet_name,它使用Python 3执行位于您刚刚创建的external目录中的name-generator.py脚本。它自动解析其输出,并在其result属性下提供反序列化的数据,供其他资源定义中使用。

现在数据源已经声明,您可以定义Apache将运行的Droplet。

步骤3 — 定义Droplet

在这一步中,您将根据简单的结构化方法,编写Droplet资源的定义,并将其保存在一个专门用于Droplets的代码文件中。它的名称将来自您刚刚创建的动态数据源,并且在每次部署时都会有所不同。

创建并打开droplets.tf文件进行编辑:

  1. nano droplets.tf

添加以下Droplet资源定义:

~/apache-droplet-terraform/droplets.tf
data "digitalocean_ssh_key" "ssh_key" {
  name = "your_ssh_key_name"
}

resource "digitalocean_droplet" "web" {
  image  = "ubuntu-20-04-x64"
  name   = data.external.droplet_name.result.name
  region = "fra1"
  size   = "s-1vcpu-1gb"
  ssh_keys = [
    data.digitalocean_ssh_key.ssh_key.id
  ]
}

您首先声明一个名为ssh_key的DigitalOcean SSH密钥资源,它将通过其名称从您的账户中获取密钥。确保将高亮显示的代码替换为您自己的SSH密钥名称。

然后,您声明一个名为web的Droplet资源。在云中其实际名称将不同,因为它是从droplet_name外部数据源请求的。为了在每次部署时使用SSH密钥引导Droplet资源,将ssh_key的ID传递给ssh_keys参数,以便DigitalOcean知道应应用哪个密钥。

目前,您需要配置与droplet.tf相关的所有内容就这么多,所以完成编辑后保存并关闭文件。

接下来,您将编写DNS记录的配置,该记录将使您的域名指向刚刚声明的Droplet。

步骤 4 — 定义 DNS 记录

配置过程的最后一步是指定一个 DNS 记录,将域名指向您的 Droplet。

您将把 DNS 配置保存在一个名为 dns.tf 的文件中,因为它是之前步骤中创建的其他资源类型之外的独立资源类型。创建并打开它以供编辑:

  1. nano dns.tf

添加以下行:

~/apache-droplet-terraform/dns.tf
resource "digitalocean_record" "www" {
  domain = var.domain_name
  type   = "A"
  name   = "@"
  value  = digitalocean_droplet.web.ipv4_address
}

这段代码声明了一个 DigitalOcean DNS 记录,在您的域名下(通过变量传递),类型为 A。该记录的名为 @,这是一个占位符,路由到域名本身,并且 value 处是 Droplet 的 IP 地址。您可以将 name 值替换为其他内容,这将导致创建一个子域名。

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

现在您已经配置了 Droplet、名称生成数据源和一个 DNS 记录,接下来您将把项目部署到云端。

步骤 5 — 规划和应用配置

在这一部分中,您将初始化您的Terraform项目,将其部署到云中,并检查是否一切都正确配置。

首先,您需要初始化您的Terraform项目。为此,请运行以下命令:

  1. terraform init

执行此命令后,您将收到如下输出:

Output
Initializing the backend... Initializing provider plugins... - Finding digitalocean/digitalocean versions matching "~> 2.0"... - Finding latest version of hashicorp/external... - Installing digitalocean/digitalocean v2.10.1... - Installed digitalocean/digitalocean v2.10.1 (signed by a HashiCorp partner, key ID F82037E524B9C0E8) - Installing hashicorp/external v2.1.0... - Installed hashicorp/external v2.1.0 (signed by HashiCorp) 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.

现在您可以使用以下命令部署您的Droplet,其中Droplet将具有动态生成的名称和与之关联的域名:

首先,定义域名、SSH密钥指纹和个人访问令牌的环境变量,这样您就无需每次运行Terraform时都复制这些值了。运行以下命令,替换高亮显示的值:

  1. export DO_PAT="your_do_api_token"
  2. export DO_DOMAIN_NAME="your_domain"

您可以在DigitalOcean控制台中找到您的API令牌。

然后,运行plan命令,传递上述变量值以查看Terraform将采取哪些步骤来部署您的项目:

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

输出将类似于以下内容:

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: # digitalocean_droplet.web将会被创建 + resource "digitalocean_droplet" "web" { + backups = false + created_at = (known after apply) + disk = (known after apply) + id = (known after apply) + image = "ubuntu-20-04-x64" + ipv4_address = (known after apply) + ipv4_address_private = (known after apply) + ipv6 = false + ipv6_address = (known after apply) + locked = (known after apply) + memory = (known after apply) + monitoring = false + name = "web-1625908814" + price_hourly = (known after apply) + price_monthly = (known after apply) + private_networking = (known after apply) + region = "fra1" + resize_disk = true + size = "s-1vcpu-1gb" + ssh_keys = [ + "...", ] + status = (known after apply) + urn = (known after apply) + vcpus = (known after apply) + volume_ids = (known after apply) + vpc_uuid = (known after apply) } # digitalocean_record.www将会被创建 + resource "digitalocean_record" "www" { + domain = "your_domain'" + fqdn = (known after apply) + id = (known after apply) + name = "@" + ttl = (known after apply) + type = "A" + value = (known after apply) } Plan: 2 to add, 0 to change, 0 to destroy. ...

接下来,Terraform会为您创建每个资源——所以您应该看到的是带有绿色+号的行,这表示Terraform将在之后创建每个资源。现在您可以运行apply命令:

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

您将被提示确认:

Output
Plan: 2 to add, 0 to change, 0 to destroy. Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: `yes`

输入yes,Terraform将为您创建Droplet和DNS记录。

Output
digitalocean_droplet.web: Creating... ... digitalocean_droplet.web: Creation complete after 33s [id=204432105] digitalocean_record.www: Creating... digitalocean_record.www: Creation complete after 1s [id=110657456] Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Terraform已经记录了部署的资源在其状态中。要确认DNS记录和Droplet成功连接,您可以从本地状态提取Droplet的IP地址并检查它是否与您的域的公共DNS记录匹配。运行以下命令以获取Droplet的IP地址:

  1. terraform show | grep "ipv4"

您将收到Droplet的IP地址:

Output
ipv4_address = "your_Droplet_IP" ...

您可以使用以下命令检查公共A记录:

  1. nslookup -type=a your_domain | grep "Address" | tail -1

输出将显示A记录指向的IP地址:

Output
Address: your_Droplet_IP

它们是相同的,如预期一样,这意味着Droplet和DNS记录已正确配置。

为了进行下一步的变化,销毁部署的资源,运行:

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

当提示时,输入yes继续。

在这一步中,您已经在DigitalOcean帐户上创建了基础设施并应用了它。您现在将修改它以使用Terraform提供程序自动在提供的Droplet上安装Apache Web服务器。

步骤6——使用提供程序运行代码

现在,您将设置使用remote-exec提供程序在部署的Droplet上执行自定义命令来安装Apache Web服务器。

Terraform配置器可用于在创建的远程资源(使用remote-exec配置器)或执行代码的本地计算机上执行特定操作。如果配置器失败,节点将在当前状态下标记为污染,这意味着在下次运行时它将被删除并重新创建。

要连接到已配置的Droplet,Terraform需要设置在Droplet上的私有SSH密钥的位置。传递私钥位置的最佳方法是使用变量,因此请打开variables.tf进行编辑:

  1. nano variables.tf

添加突出显示的行:

~/apache-droplet-terraform/variables.tf
variable "do_token" {}
variable "domain_name" {}
variable "private_key" {}

您现在已经向项目中添加了一个新变量,称为private_key。保存并关闭文件。

接下来,您将在Droplet配置中添加连接数据和远程配置器声明。通过运行以下命令打开droplets.tf进行编辑:

  1. nano droplets.tf

用突出显示的行扩展现有代码:

~/apache-droplet-terraform/droplets.tf
data "digitalocean_ssh_key" "ssh_key" {
  name = "your_ssh_key_name"
}

resource "digitalocean_droplet" "web" {
  image  = "ubuntu-20-04-x64"
  name   = data.external.droplet_name.result.name
  region = "fra1"
  size   = "s-1vcpu-1gb"
  ssh_keys = [
    data.digitalocean_ssh_key.ssh_key.id
  ]

  connection {
    host        = self.ipv4_address
    user        = "root"
    type        = "ssh"
    private_key = file(var.private_key)
    timeout     = "2m"
  }

  provisioner "remote-exec" {
    inline = [
      "export PATH=$PATH:/usr/bin",
      # 安装Apache
      "apt update",
      "apt -y install apache2"
    ]
  }
}

`connection`块指定了Terraform应该如何连接到目标Droplet。`provisioner`块包含一个命令数组,在`inline`参数内,将在配置后执行。也就是说,更新包管理器缓存并安装Apache。完成后保存并退出。

您还可以为私有密钥路径创建一个临时环境变量:

  1. export DO_PRIVATE_KEY="private_key_location"

注意: 私钥以及您希望在Terraform中加载的任何其他文件,都必须放在项目中。您可以在如何在Linux服务器上配置基于SSH密钥的认证教程中查看有关在Ubuntu 20.04或其他发行版上设置SSH密钥的信息。

尝试再次应用配置:

  1. terraform apply -var "do_token=${DO_PAT}" -var "domain_name=${DO_DOMAIN_NAME}" -var "private_key=${DO_PRIVATE_KEY}"

在提示时输入 yes。您将收到与之前类似的输出,但随后会跟来自remote-exec供应程序的长输出:

Output
digitalocean_droplet.web: Creating... digitalocean_droplet.web: Still creating... [10s elapsed] digitalocean_droplet.web: Still creating... [20s elapsed] digitalocean_droplet.web: Still creating... [30s elapsed] digitalocean_droplet.web: Provisioning with 'remote-exec'... digitalocean_droplet.web (remote-exec): Connecting to remote host via SSH... digitalocean_droplet.web (remote-exec): Host: ... digitalocean_droplet.web (remote-exec): User: root digitalocean_droplet.web (remote-exec): Password: false digitalocean_droplet.web (remote-exec): Private key: true digitalocean_droplet.web (remote-exec): Certificate: false digitalocean_droplet.web (remote-exec): SSH Agent: false digitalocean_droplet.web (remote-exec): Checking Host Key: false digitalocean_droplet.web (remote-exec): Connected! ... digitalocean_droplet.web: Creation complete after 1m5s [id=204442200] digitalocean_record.www: Creating... digitalocean_record.www: Creation complete after 1s [id=110666268] Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

现在您可以打开网页浏览器,输入您的域名。您将看到默认的Apache欢迎页面。

这意味着Apache已成功安装,Terraform已正确配置了所有内容。

要销毁已部署的资源,请运行以下命令,并在提示时输入 yes

  1. terraform destroy -var "do_token=${DO_PAT}" -var "domain_name=${DO_DOMAIN_NAME}" -var "private_key=${DO_PRIVATE_KEY}"

您现在已完成了一个具有简单结构的Terraform小项目,该项目在Droplet上部署了Apache网页服务器,并为所需的域名设置了DNS记录。

结论

您已经了解了根据复杂度,两种结构化Terraform项目的通用方法。按照简单的结构化方法,并使用remote-exec配置器执行命令,您然后部署了一个运行Apache的Droplet,并为您的域设置了DNS记录。

作为参考,以下是您在本教程中创建的项目的文件结构:

.
└── tf/
    ├── versions.tf
    ├── variables.tf
    ├── provider.tf
    ├── droplets.tf
    ├── dns.tf
    ├── data-sources.tf
    └── external/
        └── name-generator.py

您定义的资源(Droplet、DNS记录和动态数据源、DigitalOcean提供者和变量)根据本教程第一节中概述的简单项目结构,各自存储在单独的文件中。

有关Terraform配置器的更多信息及其参数,请访问官方文档

本教程是如何使用Terraform管理基础设施系列的一部分。该系列涵盖了从第一次安装Terraform到管理复杂项目的多个Terraform主题。

Source:
https://www.digitalocean.com/community/tutorials/how-to-structure-a-terraform-project