第 7 章:基础设施即代码

本章内容概览:

  • 构建适用于应用程序的生产基础设施
  • 使用 Terraform 通过代码管理基础设施
  • 代码化地创建 Kubernetes 集群

本章中,我们将挑战使用代码来管理我们的基础设施,也就是采用所谓的基础设施即代码(Infrastructure as Code, IaC)方法来自动化基础设施的部署。

迄今为止,我们一直在通过 Azure 门户手动搭建云基础设施,如在第 3 章创建容器仓库和在第 6 章手动设置 Kubernetes 集群。现在,我们将用编写代码的方式,在本地机器上运行这些代码,以自动在云中搭建相同的基础设施。

本章内容较为复杂,如果你觉得难以理解或对此不感兴趣,可以选择跳过本章,直接阅读第 8 章。你可以随时回来学习本章内容。然而,如果你对学习 Terraform 和基础设施即代码感兴趣,或者想要彻底自动化基础设施和微服务应用的部署,建议继续阅读。

7.1 新工具介绍

本章将引入 Terraform,这是一个重要工具,足以被单独列入书名。我们将使用 Terraform 帮助创建微服务应用所需的基础设施,包括 Kubernetes 集群。

表 7.1 本章介绍的新工具

工具版本目的
Terraform1.5.6通过编写脚本来创建云资源和应用程序基础设施。

7.2 获取代码

要开始学习本章内容,你需要下载代码或克隆仓库:

  • 从此处下载代码压缩文件: http://mng.bz/ZR5A
  • 使用 Git 克隆代码,命令如下:
git clone https://github.com/bootstrapping-microservices-2nd-edition/chapter-7.git

关于安装和使用 Git 的更多帮助,请参考第 2 章。如遇到代码问题,请在 GitHub 对应仓库提交 issue。

7.3 基础设施原型化

如何开始编写用于创建基础设施的代码?这与编写任何其他生产代码并无太大差异。我们将首先在本地开发机上编写和测试这些代码。与传统编程不同的是,虽然我们在本地运行代码进行测试,但实际的结果——即正在创建的基础设施,将在云中呈现。通过 Terraform 构建基于云的基础设施的过程如图 7.1 所示。

图 7.1 使用 Terraform 创建基础设施
图 7.1 使用 Terraform 创建基础设施

这章的学习重点是在本地运行 Terraform 脚本,通过云端观察结果。这种方法不仅可重复,还极大地方便了基础设施的创建和管理。

7.4 基础设施即代码

基础设施即代码意味着我们不再通过手动方式(如使用 GUI — Azure 门户)来创建基础设施,而是通过编写代码来自动化这一过程。这些代码不仅描述了我们的基础设施,还会被执行以创建所需的基础设施。通过代码管理基础设施,我们可以确保基础设施的创建既可靠又可重复。

通过基础设施即代码的方法,我们的基础设施变成了可编码的任务。较好的基础设施即代码工具采用声明性语言,这意味着它们描述了基础设施应有的配置和布局,而不是具体的构建步骤。我们倾向于使用声明性工具,因为它们允许工具自动完成大部分工作,即找到对基础设施进行更新的最佳方式。

图 7.2 展示了基础设施即代码的概念。我们的基础设施代码存放在像 Git 这样的代码仓库中。从那里,我们运行代码来创建、配置和维护我们的基于云的基础设施。

图 7.2 使用 Terraform 创建基础设施
图 7.2 使用 Terraform 创建基础设施

7.5 使用你的 Azure 账户认证

在 Terraform 开始在 Azure 上为我们创建基础设施之前,我们需要首先使用 Azure CLI 工具进行认证,如同我们在第 6 章第 6.10 节中所做的。如果你尚未登录,请现在使用 Azure CLI 登录:

az login

注意 获取安装 Azure CLI 和使用 Azure 进行认证的更多信息,请参考第 6 章第 6.10 节。

7.6 选择 Kubernetes 的合适版本

在确定部署集群的具体区域时,我们可以使用 Azure CLI 来查找可用的 Kubernetes 版本。以下是一个示例,列出了美国东部区域可用的 Kubernetes 版本:

az aks get-versions --location eastus

默认输出为 JSON 格式,可能不易阅读。为了提高可读性,我们可以切换到表格格式输出,如下所示:

az aks get-versions --location eastus --output table

输出示例如下:

KubernetesVersion    Upgrades
-------------------  -----------------------
1.27.3               None available
1.27.1               1.27.3
1.26.6               1.27.1, 1.27.3
1.26.3               1.26.6, 1.27.1, 1.27.3
1.25.11              1.26.3, 1.26.6
1.25.6               1.25.11, 1.26.3, 1.26.6

从这个列表中,建议选择最新的 Kubernetes 版本,目前最新版本为 1.27.3。然而,根据你阅读本文的时间,可能存在更新的版本。请确保选择一个目前可用的版本号,并记录下来,因为你很快将需要它来创建集群。

值得一提的是,你可能会发现我们在这里使用的 Kubernetes 版本与上一章中提到的版本有所不同。通常,Docker Desktop 自带的 Kubernetes 版本会比 Azure 上可用的版本稍旧。不过,由于 Kubernetes 的向后兼容性,我们之前学习的内容仍然适用于更新的版本。

7.7 使用 Terraform 创建基础设施

我们即将开始创建基础设施!虽然可以手动构建,使用 GUI(如前一章中的 Azure 门户),或使用 Azure CLI 工具,但我们将选择自动化的方式。本章我们将采用基础设施即代码(IaC)的方式,通过 Terraform 进行自动化构建。Terraform 是一个灵活的工具,使用 HashiCorp 配置语言(HCL)来定义基础设施的声明性配置语言。执行这些 Terraform 脚本将在云中创建我们的基础设施。

注意 今后,我会将 HCL 简称为 Terraform 代码

Terraform 支持多个云供应商,通过各种插件提供这种支持,如图 7.3 所示。我们将通过 Terraform 脚本在 Microsoft Azure 上创建基础设施。

如果你对学习 HCL 感到不安,记住,HCL 实质上与 YAML 或 JSON 类似,但采用了不同的格式。HashiCorp 设计 HCL 时,旨在使其对人类和机器都易于阅读和转换。你可以将它看作是更适合人类阅读的 YAML 或 JSON 的替代品。

图 7.3 使用 Terraform 和各种云供应商构建基础设施
图 7.3 使用 Terraform 和各种云供应商构建基础设施

7.7.1 为什么选择 Terraform?

Terraform 是一种工具及语言,旨在简化云基础设施的配置和创建。通过 Terraform,可靠且重复的云基础设施部署变得简单易行。其功能通过插件提供者扩展,支持多个云平台,包括 Azure、亚马逊网络服务(AWS)和谷歌云。

学习 Terraform 相关的技能是可迁移的,适用于所有主要的云平台。无论使用哪个云服务提供商,我们都可以利用 Terraform 构建基础设施。我们甚至可以创建自己的插件,扩展 Terraform 以支持目前尚未支持的平台。

可以将 Terraform 视为一种 通用配置语言——这是一种用于创建所有类型基础设施的语言。Terraform 是开源的,其代码可在此处找到: https://github.com/hashicorp/terraform

7.7.2 安装 Terraform

要安装 Terraform,只需下载适合你操作系统的二进制文件,并将其放置在系统 PATH 环境变量所包含的目录中。从以下网址下载 Terraform 的最新版本: www.terraform.io/downloads.html

完成安装后,可以在终端使用以下命令测试 Terraform:

terraform --version

目前我使用的版本是 1.5.6,未来版本应保持向后兼容。

7.7.3 设置 Terraform 项目

在我们开始动手使用 Terraform 前,先来了解一下 Terraform 项目的基本结构。图 7.4 展示了本章后面的示例 -3,一个较为完整的 Terraform 项目。这里我们提前展示它,以便你对 Terraform 项目的结构有一个整体的认识。

如图 7.4 所示,一个 Terraform 项目通常包含多个扩展名为 .tf 的 Terraform 脚本文件。这些文件包含了创建基础设施所需的 Terraform 代码。

图 7.4 展示的文件名能帮你理解每个脚本的作用。我使用了一种命名规则,即根据它们所创建的基础设施部分来命名每个脚本文件。例如,container-registry.tf 用于创建容器仓库,kubernetes-cluster.tf 用于创建 Kubernetes 集群,resource-group.tf 用于创建 Azure 资源组。

图 7.4 更完整的 Terraform 项目结构(本章后面的示例 -3)
图 7.4 更完整的 Terraform 项目结构(本章后面的示例 -3)

请注意,这种项目结构和文件命名方式不是 Terraform 官方规定的,而是我个人喜欢使用的一种方式。你可以根据自己的项目需求尝试不同的结构,找到最适合你的方式。

7.8 创建 Azure 资源组

在探索了示例 -3 的项目结构之后,让我们回到更简单的示例 -1,降低一些复杂性。我们的 Terraform 之旅应从简单的例子开始。示例 -1 包含了一些基础的 Terraform 代码,适合作为创建基础设施的起点。

首先,我们需要创建一个 Azure 资源组,它会将本章中我们将要创建的所有其他 Azure 资源整合在一起。回顾第 3 章,我们曾通过 Azure 门户的 GUI 手动创建过资源组。这次,我们将不再手动操作,而是使用 Terraform 代码来自动完成资源组的构建。

示例 -1 包含了两个 Terraform 代码文件:providers.tf 和 resource-group.tf。其中,resource-group.tf 负责实际创建资源组,而 providers.tf 包含了配置 Terraform 提供者插件的信息;我们很快会详细讨论这一点。

我们将通过 terraform apply 命令执行 Terraform 代码。图 7.5 展示了我们如何通过 Terraform 执行代码文件,并在 Azure 中创建名为 flixtube 的资源组。

图 7.5 使用 Terraform 创建 Azure 资源组
图 7.5 使用 Terraform 创建 Azure 资源组

7.8.1 使用 Terraform 的演化架构

Terraform 是一种逐步迭代构建基础设施的工具,我们称之为 演化架构(见图 7.6)。本章中的每个示例均可单独执行,允许你跳转到任一示例并执行 Terraform,从而轻松构建各个环节的基础设施。通常,开发基础设施的过程是迭代的。你会编写一段 Terraform 代码,执行它,测试是否成功构建了预期的基础设施。接着,你再编写更多代码,执行并测试。这一过程与常规编码类似,不同之处在于,所创建的基础设施实际上是部署在云端的。这也是我们在本章中所模拟的流程:不断编写、执行和测试代码,直到基础设施按预期方式构建完成。

图 7.6 使用 Terraform 迭代演化基础设施
图 7.6 使用 Terraform 迭代演化基础设施

7.8.2 编写基础设施创建脚本

清单 7.1 展示了第一个 Terraform 代码文件(chapter-7/example-1/resource-group.tf)。这个文件的简洁性达到了极致。通过使用 Azure 提供者,仅需三行 Terraform 代码,我们便能声明一个 Azure 资源组。

清单 7.1 创建一个 Azure 资源组

resource "azurerm_resource_group" "flixtube" {
  name     = "flixtube"
  location = "eastus"
}
  • 声明一个 Azure 资源组:该资源组将包含我们创建的所有资源,是新基础设施的核心起点。
  • 设置资源组的名称name = "flixtube"
  • 设置资源组的创建位置(数据中心)location = "eastus"

通过这些 Terraform 代码,我们定义了基础设施的关键部分。在清单 7.1 中,我们声明了一个名为 flixtube 的 Azure 资源组,该资源组由 Azure 提供者的 Terraform 资源类型 azurerm_resource_group 支持,允许我们在 Azure 上创建资源组。很快,我们将运行 Terraform,根据清单 7.1 的配置在 Azure 账户中创建此资源组。

7.8.3 固定提供者版本号

在初始化 Terraform 项目前,我们来看一下项目中的另一个文件。清单 7.2 展示了 providers.tfchapter-7/example-1/providers.tf),这是一个配置所有 Terraform 提供者插件的文件。这里我们仅需配置 Azure 提供者,无需传递额外参数,因此文件相对简单。

清单 7.2 配置 Terraform 提供者插件

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.71.0"
    }
  }

  required_version = ">= 1.5.6"
}

provider "azurerm" {
  features {}
}
  • 需要 Azure 提供者azurerm = { source = "hashicorp/azurerm" }
  • 固定所需的版本号。我们可以删除这一行,让 Terraform 自动安装最新版本version = "~> 3.71.0"
  • 设置 Terraform 的最低版本required_version = ">= 1.5.6"
  • Azure 提供者的配置可在此处设置provider "azurerm" { features {} }

请注意,清单 7.2 中固定 Azure 提供者为版本 3.71.0 的代码行是可选的,移除它将使 Terraform 自动下载最新版本。这是一种普遍的升级方式。不指定版本号时,Terraform 会下载最新版,并将其版本号输出,我们可以复制该版本号并重新填入 providers.tf,从而将项目锁定在该新版本上。

始终尽可能地固定依赖项的版本号。若不这么做,可能会在未来带来意外的问题。如果没有将项目固定在特定版本,项目会自动更新至最新发布的版本。这可能导致 Terraform 代码出现难以预测或理解的问题。因此,预先固定依赖项的版本号,以避免依赖自动更新带来的风险。

7.8.4 初始化 Terraform

我们已经开始着手创建基础设施,并编写了一个简单的脚本用于创建 Azure 资源组。然而,在执行这个脚本之前,我们必须先初始化 Terraform。

初始化过程中,Terraform 会下载脚本所需的所有提供者插件。在我们的案例中,我们仅需 Azure 提供者。要初始化 Terraform,请先切换到包含 Terraform 代码的目录:

cd chapter-7/example-1

接下来,执行 terraform init 命令:

terraform init

执行后,你将看到类似以下的输出,显示已成功下载 Azure 提供者插件:

Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/azurerm versions matching "~> 3.71.0"...
- Installing hashicorp/azurerm v3.71.0...
- Installed hashicorp/azurerm v3.71.0 (signed by HashiCorp)

--snip--

Terraform has been successfully initialized!

完成这些步骤后,我们便可以执行 Terraform 代码了。每个 Terraform 项目都至少需要在执行任何代码之前运行一次 terraform init 命令。此外,每当添加新的提供者时,也必须至少执行一次此命令。每次调用 terraform init,它将仅下载尚未缓存在本地的提供者。

如果你忘记执行 terraform init,Terraform 会提醒你需要先进行初始化。

7.8.5 Terraform 初始化的副产品

Terraform 初始化完成后,你可以检查 init 命令创建或下载的文件。虽然通常不需要手动检查,但了解 Terraform 的工作原理是有益的。图 7.7 展示了在执行 terraform init 后,example-1 目录中生成或下载的文件。

图 7.7 运行 <strong>terraform init</strong> 时下载或创建的文件
图 7.7 运行 terraform init 时下载或创建的文件

请注意,已创建名为 .terraform 的隐藏子目录,其中包含了多个文件。这些文件是 Terraform 存储其下载的提供者插件的位置。这些插件被缓存在此处,以便在每次调用 Terraform 时重复使用。

7.8.6 构建你的基础设施

在初始化 Terraform 项目之后,我们就可以执行 terraform apply 命令,通过 Terraform 代码开始构建基础设施的第一次迭代。为了更直观地了解 apply 命令的效果,你可以参考图 7.5 和 7.6。

在你之前运行 init 命令的同一目录下,现在执行以下命令:

terraform apply

apply 命令将汇总并执行项目中的所有 Terraform 代码文件。(到目前为止,我们只有一个资源组的代码文件,但未来将会增加更多。)

执行后,你将看到类似以下的输出:

Terraform used the selected providers to generate the following
execution plan. Resource actions are indicated with the
following symbols:
  + create

Terraform 计划执行以下操作:

  # azurerm_resource_group.flixtube will be created
  + resource "azurerm_resource_group" "flixtube" {
      + id       = (known after apply)
      + location = "eastus"
      + name     = "flixtube"
}

Plan: 1 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:

此输出详细描述了即将进行的基础设施更新。Terraform 现在等待你的批准,确认计划无误后,输入 yes 并按 Enter 键允许 Terraform 继续操作。

一旦批准,Terraform 便开始创建所需的基础设施。在本例中,第一次执行 Terraform 时,我们已在 Azure 账户中成功创建了 flixtube 资源组。这个过程应该很快完成,因为当前的脚本较简单。成功后,你将看到以下消息:

azurerm_resource_group.flixtube: Creating...

azurerm_resource_group.flixtube: Creation complete after 3s [--snip--]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

输出将提供关于添加、更改和删除的资源的快速总结。在本例中,它确认我们已成功创建了一个云资源,即我们的 Azure 资源组。

现在,你可以手动检查所做的更改。打开网络浏览器,导航到 Azure 门户 https://portal.azure.com/ ,检查你的 Azure 账户是否确实已创建了 flixtube 资源组。在门户中查找资源组,确认 flixtube 资源组现在已列在其中。这正是你通过第一个 Terraform 代码所创建的!

通常,你不需要每次都手动通过 Azure 门户验证每个资源是否已创建。通常情况下,如果 Terraform 报告成功,你可以放心认为请求的资源已被创建。我们只是这样做是为了让你了解刚刚发生了什么。

7.8.7 理解 Terraform 状态

在项目中首次执行 terraform apply 后,Terraform 会生成一个名为 terraform.tfstate 的状态文件,该文件将位于与 Terraform 代码相同的目录中。

了解 Terraform 的持久状态管理是至关重要的。虽然我们大多数时间不需要关注状态文件的具体内容,但理解它存在的原因及其处理方式是有益的。

让我们检查一下在创建了基础设施的第一部分后的 Terraform 状态文件。现在是一个理想的时机来查看这个状态文件,因为其内容还很简单,容易理解。使用 cat 命令来查看状态文件的内容:

cat terraform.tfstate

对于 Windows 用户,可以使用 type 命令来查看:

type terraform.tfstate

输出将显示如下:

{
  "version": 4,
  "terraform_version": "1.5.6",
  "serial": 1,
  "lineage": "b10f693f-e27f-f223-a8ef-011948c56f9c",
  "outputs": {},
  "resources": [
    {
      "mode": "managed",
      "type": "azurerm_resource_group",
      "name": "flixtube",
      "provider": "provider[\"registry.terraform.io/hashicorp/azurerm\"]",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "id": "/subscriptions/snip/resourceGroups/flixtube",
            "location": "eastus",
            "managed_by": "",
            "name": "flixtube",
            "tags": null,
            "timeouts": null
          },
          "sensitive_attributes": [],
          "private": "snip"
        }
      ]
    }
  ],
  "check_results": null
}

从输出中可以看到,我们的 Terraform 状态文件在 resources 字段中记录了刚刚创建的资源组的详细信息。

每次执行 terraform apply 时,都会生成或更新状态文件,而后续的 apply 调用将使用此状态文件作为输入。Terraform 会加载此状态文件并刷新来自实际基础设施的信息。图 7.8 描述了如何通过现场基础设施和状态文件连接 Terraform 的连续调用。

图 7.8 理解 Terraform 状态对于使用 Terraform 至关重要。
图 7.8 理解 Terraform 状态对于使用 Terraform 至关重要。

那么,为什么需要状态文件呢?如果我们已在 Terraform 代码中定义了基础设施,并且 Terraform 可以直接从实际基础设施获取当前状态,为何还要在一个单独的文件中持久化状态呢?理解状态文件的必要性可以从两个角度考虑:

  1. Terraform 项目并不“拥有”你的 Azure 账户中的所有基础设施。
  2. 当我们更改 Terraform 代码以修改基础设施时,它将与现场基础设施发生不同步。

考虑到一个 Azure 订阅可能在多个项目间共享,账户中的基础设施可能是由其他 Terraform 项目或以其他方式创建的(如通过 Azure 门户或使用 Azure CLI 工具手动创建)。Terraform 不会假设它拥有对其有访问和修改权限的 Azure 账户中的一切。它只假定拥有那些在基础设施代码或状态文件中声明的资源。加载代码和状态文件是 Terraform 确定其拥有何种资源的方式。

此外,当我们通过代码的更改来指导基础设施的变更时,Terraform 如何知晓需作何更改?它通过将记录的状态与代码中的声明比较来自动计算出所需的更新集。这种智能化的处理方式显示了 Terraform 为我们自动执行的庞大工作量。

有了对 Terraform 状态的深入理解,你可以更有效地使用这个工具。在继续阅读本章和后续章节时,不妨时常查看状态文件,观察其如何随时间增长和变化。

7.8.8 销毁和重建我们的基础设施

我们的基础设施已经开始运行了!虽然规模还不大,但这是一个不错的开端。在我们进一步发展基础设施之前,让我们先尝试销毁并重建它。

选择现在进行这个实验的原因是,当基础设施规模较小时,这种操作更为高效。到本章结束时,我们将引入一个 Kubernetes 集群,到那时销毁和重建会花费更多时间。

此外,最终你可能需要清理这些 Azure 资源。除非你在开发一个真实的产品,否则不太可能希望为其付费。尽管我希望你是从 Azure 的免费额度开始的,但无论如何,不要让这些资源超出你所需的时间运行!

现在,使用以下 terraform destroy 命令来销毁当前的基础设施:

terraform destroy

你会看到如下的输出(部分省略):

Terraform used the selected providers to generate the following
execution plan. Resource actions are indicated with the
following symbols:
  - destroy
Terraform will perform the following actions:
  # azurerm_resource_group.flixtube will be destroyed
  - resource "azurerm_resource_group" "flixtube" {
  - id = "/subscriptions/snip/resourceGroups/flixtube" -> null
  - location = "eastus" -> null
  - name = "flixtube" -> null
  - tags = {} -> null
}

Plan: 0 to add, 0 to change, 1 to destroy.

Do you really want to destroy all resources?
 Terraform will destroy all your managed infrastructure, as shown above. There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value:

apply 命令类似,destroy 命令也会显示其执行计划。这显示了即将进行的更改。要继续,我们输入 yes 并按 Enter。然后 Terraform 将执行操作并显示总结:

azurerm_resource_group.flixtube: Destroying... --snip-- azurerm_resource_group.flixtube: Still destroying... —snip-- --snip--
 azurerm_resource_group.flixtube: Destruction complete after 1m23s

Destroy complete! Resources: 1 destroyed.

在完成本章中的每个示例后,你应该使用 destroy 命令来清理你创建的基础设施。然而,如果你在进行自己的迭代原型设计,不需要在每次进行新的 apply 之前都进行一次 destroy。相反,只需更改 Terraform 代码,然后再次调用 terraform apply。这个过程可以重复进行,随着你逐步构建基础设施。

你也可以选择手动通过 Azure 门户或 Azure CLI 工具删除 Azure 资源来进行清理。但使用 destroy 命令更简单,因为它减少了需要考虑的因素。这也意味着你不会意外删除其他基础设施,特别是如果你与他人共享 Azure 订阅的话。

完成 terraform destroy 操作后,可以通过再次执行 terraform apply 命令简单地重建基础设施:

terraform apply

你可以根据需要多次练习这个过程。销毁和重建基础设施的操作有助于你理解,你实际上是在用代码来管理基础设施!你可以随意销毁和创建基础设施,无需任何手动干预。在这个阶段,这可能看起来不多,但随着基础设施和应用变得更大更复杂,这种能力的重要性将逐渐增加。

事实上,你可能已经意识到,我们可以使用 Terraform 代码来创建基础设施的多个副本!在第 12 章,你将学习如何参数化 Terraform 代码,以便为开发、测试和生产环境创建独立的实例。如果这还不能激发你的兴趣,我不知道还有什么能。

7.8.9 我们达到了什么成就?

我们现在已经安装了 Terraform,并编写了一个初步的基础设施代码。Terraform 是我们用于实现基础设施即代码的工具。这是一种将基础设施配置存储为可执行代码(如在 Terraform 代码文件中)的技术,我们可以用它来创建、管理和销毁我们的基础设施。

我们创建了第一个 Terraform 代码文件,并使用 terraform init 初始化了项目。接着我们执行了 terraform apply 来运行代码,创建了一个 Azure 资源组。最后,你学习了如何使用 terraform destroyterraform apply 来销毁和重建基础设施。

7.9 创建我们的容器仓库

接下来,我们需要创建一个私有容器仓库,以便发布我们的 Docker 镜像,从而部署我们的微服务。如果你还记得,我们在第 3 章中已经介绍了如何构建和发布 Docker 镜像。那时我们通过 Azure 门户的 GUI 手动创建了一个容器仓库。现在你已经熟悉了 Terraform 的使用,我们将通过代码重新创建容器仓库。

7.9.1 继续我们的基础设施演化

我们将继续第 7 章的示例 -2,该示例基于示例 -1 进行扩展。你可以直接切换到示例 -2 项目目录并执行 terraform init 开始:

cd chapter-7/example-2
terraform init

在跳转到第二个示例之前,请确保销毁第一个示例创建的所有基础设施。

7.9.2 创建容器仓库

清单 7.3 展示了创建容器仓库的 Terraform 代码文件(取自 chapter-7/example-2/container-registry.tf)。在运行此代码之前,你需要先更改仓库的名称,因为 Azure 容器仓库的名称必须是全局唯一的,不能重用已存在的名称(如 flixtube)。

清单 7.3 创建我们的私有容器仓库

resource "azurerm_container_registry" "container_registry" {
  name                = "flixtube"
  resource_group_name = azurerm_resource_group.flixtube.name
  location            = "eastus"
  admin_enabled       = true
  sku                 = "Basic"
}
  • 声明容器仓库资源:resource "azurerm_container_registry" "container_registry"
  • 设置容器仓库的名称。必须唯一,因此需要更改:name = "flixtube"
  • 关联到之前创建的资源组:resource_group_name = azurerm_resource_group.flixtube.name
  • 指定容器仓库的位置:location = "eastus"
  • 启用管理员账户,方便远程管理:admin_enabled = true
  • 选择基础 SKU,成本较低,自动管理存储:sku = "Basic"

注意 SKU(库存单位)代表产品的不同规格。这里我们选择了容器仓库的基本规格。

请注意,我们从另一个文件(如第 6 章中的 resource-group.tf)获取 resource_group_name 的值。这些资源通过 Terraform 的 资源图 相互关联,Terraform 通过这种方式来管理资源之间的依赖关系,并决定执行脚本的顺序。这确保了在创建容器仓库之前必须先创建资源组。

现在,让我们使用以下命令创建或更新基础设施:

terraform apply -auto-approve

这里我们使用了 -auto-approve 选项,意味着无需每次都手动确认。这在快速原型开发时非常方便,且在自动化部署流程中至关重要。

创建更复杂的基础设施可能需要更长时间。完成后,Terraform 会显示和以前类似的输出,展示基础设施的变更详情。

7.9.3 Terraform 输出

在输出的末尾,Terraform 会显示一些关键信息,包括我们新容器仓库的详细信息:

输出:

registry_hostname = "flixtube.azurecr.io"
registry_pw = <sensitive>
registry_un = "flixtube"

Terraform 或其插件提供者常会生成重要的配置信息。我们可以通过 Terraform 的输出功能从 Terraform 配置中提取这些生成的信息。在清单 7.4 中(取自 chapter-7/example-2/container-registry.tf),你会看到如何声明输出 URL、用户名和密码的代码。这样的输出有助于我们调试 Terraform 代码和理解其代表我们创建的基础设施的详细信息。

清单 7.4 Terraform 输出

--snip--

output "registry_hostname" {
  value = azurerm_container_registry.container_registry.login_server
  # 创建输出
  # 设置输出值
}

output "registry_un" {
  value = azurerm_container_registry.container_registry.admin_username
  # 创建输出
  # 设置输出值
}

output "registry_pw" {
  value     = azurerm_container_registry.container_registry.admin_password
  sensitive = true
  # 创建输出
  # 设置输出值
  # 标记容器仓库密码为敏感信息,避免 Terraform 显示此信息。
}

7.9.4 从 Terraform 输出敏感值

在上一节中,你可能注意到容器仓库的密码被标记为 <sensitive>。这是因为在 Terraform 中,我们将密码视为敏感信息。实际上,在清单 7.4 中,我们添加了 sensitive = true 这一行。如果没有这行代码,Terraform 会拒绝执行这段代码(你可以尝试删除它然后运行代码看看)。通过这种方式,Terraform 强制我们将这个值标记为敏感,确保我们意识到这些信息可能不应该被输出。

那么,如果密码被隐藏,我们如何获取它呢?实际上,我们可以使用以下命令从 Terraform 中按名称检索隐藏的值:

terraform output -raw registry_pw

执行此命令后,你将在终端看到密码的实际值。很快你将需要这个密码(以及 URL 和用户名)来使用 Docker 登录到你的容器仓库,并推送镜像。

为什么要隐藏敏感值呢?Terraform 设计之初就考虑到了自动部署流程,这些流程会记录所有终端输出,以供后续审查。这些日志对于理解自动化部署过程中发生的事件至关重要。然而,将密码以纯文本形式存储在输出日志中会降低部署的安全性。Terraform 识别出这些值的敏感性,并从终端输出中隐藏它们,避免它们被记录在部署日志中。

但这是否意味着敏感数据仍然存储在本地呢?是的,事实上,如果你运行 cat terraform.tfstate 来查看 Terraform 状态文件的内容,你会看到密码以明文形式存储!

虽然理想的持续部署流程会在任务完成后销毁这些本地状态文件,但将明文密码留在文件系统中并不是一个安全的做法,因为你无法预测将来谁可能会访问到它。

7.9.5 不要输出敏感值

处理输出敏感值的最佳做法是简单明了:不要输出它们。在示例 -2 中包括容器仓库密码主要是为了讨论其相关问题。虽然在学习和实验 Terraform 时可以在输出中使用敏感值,但在安全至关重要的生产环境中,最好避免这样做。在示例 -3 的 container-registry.tf 文件中,我们已经移除了密码输出。事实上,我去除了所有输出。尽管 Terraform 的输出在调试时有帮助,但我们不需要它们来获取创建的云资源的详细信息。

7.9.6 获取容器仓库的详细信息

既然我们已经明确不输出敏感信息的重要性,那么实际上我们不需要在 Terraform 中输出任何信息。相反,我们可以使用 Azure CLI 工具来检索容器仓库的详细信息,而无需在本地存储敏感信息:

执行以下命令:

az acr show --name flixtube --output table
az acr credential show --name flixtube --output table

第一个命令显示容器仓库的基本信息。第二个命令显示仓库的认证信息。我们添加了 --output table 选项以获取更易于阅读的输出格式;没有这个选项,输出将是 JSON 格式的。

不要忘记使用你自己的容器仓库名称替换 <your-container-registry-name>。如下所示:

az acr show --name <your-container-registry-name> --output table
az acr credential show --name <your-container-registry-name> --output table

这些命令提供了与容器仓库互动所需的所有信息(URL、用户名和密码)。你也可以在 Azure 门户中查看容器仓库页面以获取这些信息。

7.9.7 我们达成了什么目标?

通过添加容器仓库,我们进一步发展了基础设施,这是部署微服务 Docker 镜像所必需的。

在本节中,我们通过执行新的 Terraform 代码文件,成功在 Azure 账户中创建了一个新的容器仓库。你已经学习了如何使用 Terraform 输出来调试代码,并了解为什么在生产环境中应避免输出敏感值。

7.10 重构以共享配置数据

你可能已经注意到,在之前的示例中,一些配置值在不同文件中被重复使用。当需要修改这些值时,这种重复可能导致问题。理想情况下,我们希望能在一个地方修改这些关键值,并在所有 Terraform 代码文件中共享它们。我们可以通过使用 Terraform 变量 来实现这一目标。现在,我们将重构代码以利用变量和共享配置数据。

7.10.1 继续我们的基础设施演化

我们将转到第 7 章代码库中的示例 -3。你可以直接跳到示例 -3 项目并执行 terraform init 开始。进行这一步骤之前,请确保销毁之前示例创建的所有基础设施。

7.10.2 引入 Terraform 变量

第 7 章代码库中的示例 -3 对示例 -2 进行了重构,改进了代码文件之间共享配置值的方法,并添加了名为 variables.tf 的新文件。清单 7.5 展示了这个新的文件内容。

在这个文件中,我们定义了一些关键配置值的 Terraform 变量,例如应用名称(flixtube)、资源位置(eastus)等。

清单 7.5 设置 Terraform 变量

variable "app_name" {
  description = "The name of the application"
}

variable "location" {
  description = "The location for the resources"
  default = "eastus"
}

variable "kubernetes_version" {
  description = "The Kubernetes version to use"
}

注意,清单 7.5 中的 app_namekubernetes_version 变量没有设置默认值。当你运行这段 Terraform 代码时,系统将要求你为这些变量提供值。

清单 7.6 和清单 7.7 展示了如何使用这些新变量。你会看到我们的资源组和容器仓库的名称都是从 app_name 变量中获取的。资源的位置则通过 location 变量设置。

清单 7.6 使用变量的资源组配置

resource "azurerm_resource_group" "flixtube" {
  name     = var.app_name    # 使用 app_name 变量设置资源组的名称
  location = var.location    # 使用 location 变量设置位置
}

清单 7.7 使用变量的容器仓库配置

resource "azurerm_container_registry" "container_registry" {
  name                = var.app_name                               # 使用 app_name 变量设置容器仓库的名称
  resource_group_name = azurerm_resource_group.flixtube.name
  location            = var.location                               # 使用 location 变量设置位置
  admin_enabled       = true
  sku                 = "Basic"
}

通过这种重构,我们已经成功地使用 Terraform 变量在代码文件之间共享了相关配置值。现在,如果我们需要更改应用的位置,只需在 variables.tf 中修改 location 变量即可。这种方法使整个代码库更加模块化和易于管理。

7.11 创建我们的 Kubernetes 集群

现在我们将关注基础设施的关键部分:创建一个托管生产环境微服务的平台。我们将使用 Terraform 在 Azure 中创建一个 Kubernetes 集群。

7.11.1 编写创建集群的脚本

继续使用示例 -3,我们将了解如何创建 Kubernetes 集群的代码。清单 7.8 展示了 kubernetes-cluster.tf 文件中定义的 Kubernetes 集群配置。

我们将继续利用 Terraform 变量。namelocationresource_group_name 等字段应该已经很熟悉了。此外,还有一些新的字段需要注意。

清单 7.8 创建我们的 Kubernetes 集群

resource "azurerm_kubernetes_cluster" "cluster" {            # 声明 Kubernetes 集群资源
  name                = var.app_name
  location            = var.location
  resource_group_name = azurerm_resource_group.flixtube.name

  dns_prefix          = var.app_name
  kubernetes_version  = var.kubernetes_version                # 指定 Kubernetes 版本

  default_node_pool {                                         # 配置集群节点
    name       = "default"
    node_count = 1
    vm_size    = "Standard_B2s"
  }

  identity {                                                  # 使用系统分配的身份认证
    type = "SystemAssigned"
  }
}

这段代码定义了集群节点的配置和 VM 大小。当前我们在单个节点上构建集群,可以轻松扩展以添加更多节点,这将在第 12 章中讨论。

7.11.2 将仓库附加到集群

在我们准备创建 Kubernetes 集群之前,还需要完成一个步骤。我们需要将容器仓库与集群“绑定”,从而预授权集群从仓库拉取镜像。我们之前是通过 Azure CLI 命令完成这一步骤;现在,我们将通过 Terraform 实现。

清单 7.9 展示了如何创建一个“角色分配”,赋予集群从容器仓库拉取镜像的权限。

清单 7.9 将容器仓库附加到集群

resource "azurerm_role_assignment" "role_assignment" {      # 为集群分配角色
  principal_id              = azurerm_kubernetes_cluster
                               .cluster.kubelet_identity[0].object_id
  role_definition_name      = "AcrPull"
  scope                     = azurerm_container_registry.container_registry.id  # 赋予集群从仓库拉取镜像的权限
  skip_service_principal_aad_check = true
}

7.11.3 构建我们的集群

现在我们准备好运行最新的 Terraform 代码以创建 Kubernetes 集群。使用以下命令应用配置:

terraform apply -auto-approve

Terraform 将提示输入那些没有默认值的变量。我们需要为 app_namekubernetes_version 提供值,因为在 variables.tf 文件中这些变量没有默认值:

var.app_name
    输入一个值:flixtube

var.kubernetes_version
    输入一个值:1.24.6

选择 app_name 为 flixtube,你需要为你的应用选择一个新的、独一无二的名字,因为容器仓库的名称由 app_name 决定,且必须全球唯一。使用任何你喜欢的名称,只要它未被占用即可。

值得一提的是,通过将代码参数化为 app_name,我们现在可以部署多个版本的基础设施,例如,flixtube-prodflixtube-test 可用于区分生产和测试环境。我们将在第 12 章进一步探讨这一点。

kubernetes_version 的值应选择你在 7.6 节中得到的信息。如果你未掌握如何选择 Kubernetes 版本,请返回查看该节。

Terraform 将创建 Kubernetes 集群,这可能需要一段时间,因此这是一个喝杯咖啡的好机会。完成后,你的 Kubernetes 集群将准备好投入使用。

注意 如果你尝试使用我提供的 Kubernetes 版本号(1.24.6)但不成功,可能是因为该版本在 Azure 上已不可用。请参阅 7.6 节以了解如何选择一个可用的版本。

7.11.4 我们达到了什么成就?

干得好!我们已通过 Terraform 代码成功创建了一个 Kubernetes 集群。如果你之前认为设置 Kubernetes 集群非常复杂,可能会惊讶于它实际上比预期简单得多。

这是向生产环境迈进的重要一步。我们通过代码逐步演进我们的基础设施,最终增加了一个 Kubernetes 集群。在此过程中,我们进行了重构,使用 Terraform 变量在不同的 Terraform 代码文件间共享了重要的配置信息。

7.12 部署到我们的集群

现在我们拥有一个 Kubernetes 集群,下一步是尝试部署一个微服务,以验证集群的功能。为了测试新集群,我们将从第 7 章代码库中的 example-4 子目录部署一个微服务。这个微服务与第 6 章中的示例 -2 相同,我们现在将重新部署它以测试我们使用 Terraform 创建的集群。

首先,确保 kubectl 正确连接到我们的新集群:

az aks get-credentials --resource-group <resource-group> --name <cluster>

填写你的资源组和集群名称的详细信息。如果你的 app_name 是 flixtube,命令将如下所示:

az aks get-credentials --resource-group flixtube --name flixtube

接下来,在第 7 章的 example-4 项目中构建 Docker 镜像:

cd chapter-7/example-4
docker build -t video-streaming:1 --file Dockerfile-prod .

获取容器仓库的详细信息(请参阅 7.9.6 节)。使用你的容器仓库的 URL 来标记镜像:

docker tag video-streaming:1 <registry-url>/video-streaming:1

然后,登录到容器仓库:

docker login <registry-url>

登录成功后,将微服务的镜像推送到仓库:

docker push <registry-url>/video-streaming:1

这可能需要一些时间,可以趁此机会休息一下。

在进行测试部署之前,必须更新 deploy.yaml 文件,确保其引用了正确的容器仓库 URL:

spec:
  containers:
    - name: video-streaming
      image: <registry-url>/video-streaming:1
      imagePullPolicy: IfNotPresent
      env:
        - name: PORT
          value: "4000"

更新完 deploy.yaml 后,可以将微服务部署到 Kubernetes 集群:

kubectl apply -f scripts/deploy.yaml

之后,检查部署状态:

kubectl get pods
kubectl get deployments
kubectl get services

kubectl get services 的输出中获取 EXTERNAL-IP 值,并用它测试视频流媒体微服务。确保查看视频路由。测试完成后,可以删除部署并清理集群:

kubectl delete -f scripts/deploy.yaml

我们通过这个过程验证了集群的功能。有关使用 Docker 构建和推送镜像的更多详细信息,请参见第 3 章。关于 Kubernetes 部署的更多信息,请查看第 6 章。

7.13 销毁我们的基础设施

除非你正在开发一个生产应用,否则没必要持续运行基础设施。这最终会耗尽你在 Azure 上的免费额度,开始产生费用。因此,完成对 Terraform 和 Kubernetes 的实验后,你可以使用以下命令销毁你的基础设施:

terraform destroy

不必担心销毁基础设施,因为我们的基础设施是通过代码创建的,我们可以随时重新创建。当然,如果有人(客户或团队成员)依赖我们的基础设施,我们就不应在他们使用时销毁它!在第 12 章中,我们将讨论如何在不中断用户的情况下最小化升级或更换基础设施的风险。

7.14 Terraform 回顾

我们已完成了另一个重要的章节!请随时回顾这些章节,练习所学内容。

回顾一下,Terraform 是创建和配置基于云的基础设施的强大工具。我们已经用它创建了完整的微服务应用基础设施。在继续之前,让我们复习一下我们添加到工具箱中的 Terraform 命令。

表 7.2 Terraform 命令回顾

命令描述
terraform init初始化 Terraform 项目并下载提供者插件。
terraform apply -auto-approve应用工作目录中的 Terraform 代码文件,增量更新基础设施。
terraform destroy销毁由 Terraform 项目创建的所有基础设施。

7.15 继续你的学习

我们使用 Terraform 和“基础设施即代码”技术创建了一个基于 Kubernetes 的生产环境。关于基础设施即代码和 Terraform 还有很多可以学习的知识。如果你想深入了解,可以参考以下资源:

  • 《Terraform 实战》作者 Scott Winkler(Manning, 2021)
  • 《Terraform 深度解析》作者 Robert Hafner(Manning, 预计 2024 年春)
  • 《基础设施即代码:模式与实践》作者 Rosemary Wang(Manning, 2022)

此外,Terraform 的官方文档是最好的资源之一。建议从他们的网站开始,深入教程和文档。作为练习,尝试找到并了解我们本章中使用的 Terraform 资源的更多信息,请访问 www.terraform.io

总结

  • 基础设施即代码是一种技术,我们将基础设施配置存储为代码。通过编辑和执行该代码来更新基础设施。
  • Terraform 是一种工具和语言,用于通过代码脚本化创建云资源和应用基础设施。
  • 编写创建基础设施的代码与编写希望在生产中运行的代码没有太大区别,唯一的不同是我们可以在本地开发计算机上运行 Terraform 代码,而结果总是在云中体现。通过运行 Terraform 代码创建的资源总是在云中创建的。
  • Terraform 允许我们以迭代的方式逐步构建基础设施,这被称为演化架构。
  • Terraform 由提供者插件支持,这意味着它可以用于在所有主要云平台上创建资源。

文章导航

独立页面

这是书籍中的独立页面。

书籍首页

评论区