原文:
annas-archive.org/md5/3c34d287e2879a0f121f1884a118ac03译者:飞龙
第八章:使用 Terraform 实现基础设施即代码(IaC)
云计算是当今推动 DevOps 实现的主要因素之一。关于云计算的初步担忧已经成为过去。随着一支安全和合规专家 24 小时驻守在云平台,组织现在比以往任何时候都更加信任公共云。与云计算一起,另一个热词也席卷了整个行业——基础设施即代码(IaC)。本章将重点讲解使用 Terraform 实现 IaC,到了本章结束时,你将理解这一概念,并拥有足够的 Terraform 实践经验,帮助你开始自己的旅程。
在本章中,我们将涵盖以下主要主题:
-
IaC 简介
-
设置 Terraform 和 Azure 提供者
-
理解 Terraform 工作流并使用 Terraform 创建你的第一个资源
-
Terraform 模块
-
Terraform 状态和后端
-
Terraform 工作空间
-
Terraform 输出、状态、控制台和图表
技术要求
本章中,你可以使用任何机器来运行 Terraform。Terraform 支持多个平台,包括 Windows、Linux 和 macOS。
你将需要一个有效的 Azure 订阅来进行练习。目前,Azure 提供为期 30 天的免费试用,并赠送价值 $200 的免费积分;你可以在 azure.microsoft.com/en-in/free 注册。
你还需要克隆以下 GitHub 仓库来进行一些练习:
github.com/PacktPublishing/Modern-DevOps-Practices-2e
运行以下命令将仓库克隆到你的主目录,并 cd 进入 ch8 目录以访问所需资源:
$ git clone https://github.com/PacktPublishing/Modern-DevOps-Practices-2e.git \
modern-devops
$ cd modern-devops/ch8
那么,让我们开始吧!
IaC 简介
基础设施即代码(IaC)是使用代码来定义基础设施的概念。虽然大多数人能将基础设施想象成某种有形的东西,但虚拟基础设施已经是司空见惯的存在,并且已经存在了大约二十年。云服务提供商提供了一个基于 Web 的控制台,通过它,你可以直观地管理你的基础设施。然而,这个过程不可重复且没有记录。
如果你在一个环境中使用控制台启动一组基础设施组件,并希望在另一个环境中复制它,那就相当于重复劳动。为了解决这个问题,云平台提供了 API 来操作云中的资源,并提供一些命令行工具帮助触发这些 API。你可以开始编写脚本,使用命令来创建基础设施,并将其参数化以便在另一个环境中使用同样的脚本。嗯,这样就解决问题了,对吧?
并不是完全如此!编写脚本是管理基础设施的一种命令式方式。尽管你仍然可以称之为 IaC,但它的问题在于没有有效地管理基础设施变更。让我举几个例子:
-
如果你需要修改脚本中已经存在的内容,怎么办?在脚本的中间某个地方进行更改,并重新运行整个脚本,可能会对你的基础设施造成混乱。命令式管理基础设施是不可幂等的。因此,管理更改会成为一个问题。
-
如果有人使用控制台手动更改了脚本管理的基础设施,怎么办?你的脚本能正确检测到吗?如果你想用脚本改变同样的内容,会发生什么?这很快就会变得杂乱无章。
-
随着混合云架构的出现,大多数组织都使用多个云平台来满足他们的需求。当你处于这种情况时,使用命令式脚本管理多个云很快就会变得成问题。不同的云平台有不同的与 API 交互的方式,并且有各自独特的命令行工具。
解决所有这些问题的方案是像 Terraform 这样的声明性 IaC 解决方案。HashiCorp 的 Terraform 是市场上最流行的 IaC 工具。它帮助你使用代码自动化和管理基础设施,并能在各种平台上运行。由于它是声明式的,你只需定义你需要的内容(期望的最终状态),而不需要描述如何实现它。它具有以下特性:
-
它通过提供程序支持多个云平台,并公开一个基于HashiCorp 配置语言(HCL)的单一声明性接口与其交互。因此,它允许你使用相似的语言和语法管理各种云平台。所以,团队中如果有几位 Terraform 专家,就能处理你所有的 IaC 需求。
-
它通过状态文件跟踪其管理的资源的状态,并支持本地和远程后端存储和管理这些状态文件。这有助于使 Terraform 配置具有幂等性。因此,如果有人手动更改了 Terraform 管理的资源,Terraform 可以在下次运行时检测到差异,并提示进行纠正操作,以将其恢复到定义的配置。管理员可以在应用更改之前吸收此更改或解决任何冲突。
-
它在基础设施管理中实现了 GitOps。通过 Terraform,你可以将基础设施配置与应用程序代码放在一起,使版本控制、管理和发布基础设施的方式与管理代码相同。你还可以通过拉取请求包含代码扫描和门控,以便在应用更高环境之前,有人可以审查并批准对更高环境的更改。确实是一项强大的功能!
Terraform 提供了多个版本——开源、云和企业版。开源版是一个简单的基于命令行界面(CLI)的工具,你可以在任何支持的操作系统(OS)上下载并使用。云和企业版更多的是对开源版的包装。它们提供基于 Web 的 GUI 和高级功能,如代码即策略(Sentinel)、成本分析、私有模块、GitOps和CI/CD 流水线。
本章将讨论开源提供和其核心功能。
Terraform 开源分为两个主要部分——Terraform Core 和 Terraform 提供者,如下图所示:
图 8.1 – Terraform 架构
让我们来看看这两个组件的功能:
-
Terraform Core 是我们用来与 Terraform 交互的 CLI。它有两个主要输入——你的 Terraform 配置文件和现有状态。然后,它会计算配置差异并应用它。
-
Terraform 提供者 是 Terraform 用来与云提供商交互的插件。提供者将 Terraform 配置转换为相应云的 REST API 调用,以便 Terraform 管理相关的基础设施。例如,如果你希望 Terraform 管理 AWS 基础设施,则必须使用 Terraform AWS 提供者。
现在让我们看看如何安装开源 Terraform。
安装 Terraform
安装 Terraform 非常简单;只需访问www.terraform.io/downloads.html,并根据你的平台按照说明进行操作。大部分内容将要求你下载一个二进制文件并将其移到系统路径中。
由于我们在本书中一直使用 Ubuntu,因此我将展示如何在 Ubuntu 上安装 Terraform。使用以下命令,借助 apt 包管理器来安装 Terraform:
$ wget -O- https://apt.releases.hashicorp.com/gpg | \
sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
$ echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \
sudo tee /etc/apt/sources.list.d/hashicorp.list
$ sudo apt update && sudo apt install terraform
使用以下命令检查 Terraform 是否已成功安装:
$ terraform version
Terraform v1.5.2
它显示 Terraform 已成功安装。Terraform 使用 Terraform 提供者与云提供商进行交互,接下来我们将在下一节中讨论这些内容。
Terraform 提供者
Terraform 采用去中心化架构。Terraform CLI 包含 Terraform 的核心功能,并提供与特定云提供商无关的所有功能,而 Terraform 提供者则为 Terraform CLI 和云提供商之间提供接口。这种去中心化的方式使得公共云供应商能够提供他们自己的 Terraform 提供者,从而使他们的客户可以使用 Terraform 来管理云中的基础设施。由于 Terraform 的流行,现在每个公共云提供商都必须提供 Terraform 提供者。
本章我们将与 Azure 进行交互,并使用 Azure Terraform 提供者来进行操作。
要访问本节的资源,请在终端中 cd 到以下目录:
$ cd ~/modern-devops/ch8/terraform-exercise/
在继续配置提供者之前,我们需要了解 Terraform 如何与 Azure API 进行身份验证和授权。
与 Azure 的身份验证和授权
与 Azure 进行身份验证和授权的最简单方法是使用 Azure CLI 登录到你的帐户。当你在 Terraform 文件中使用 Azure 提供程序时,它将自动充当你的帐户并执行所需的操作。现在,这听起来有点危险。管理员通常有很多访问权限,而有一个作为管理员的工具可能不是一个好主意。如果你想将 Terraform 插入到 CI/CD 流水线中怎么办?其实,还有另一种方法——通过使用 Azure 服务主体。Azure 服务主体允许你在不使用指定用户帐户的情况下访问所需功能。你可以对服务主体应用 最小权限原则,只提供必要的访问权限。
在配置服务主体之前,让我们在机器上安装 Azure CLI。为此,请运行以下命令:
$ curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
上述命令将下载一个 shell 脚本,并使用 bash 执行它。脚本将自动下载并配置 Azure CLI。要确认 Azure CLI 是否安装成功,请运行以下命令:
$ az --version
azure-cli 2.49.0
我们已经看到 Azure CLI 在系统上正确安装。现在,让我们继续配置服务主体。
要配置 Azure 服务主体,请按照以下步骤操作。
使用以下命令登录 Azure,并按照命令提示的步骤操作。你必须浏览到指定的 URL 并输入给定的代码。登录后,你将获得一个包含一些详细信息的 JSON 响应,内容可能如下所示:
$ az login
To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter
the code XXXXXXXXX to authenticate:
[
{
"id": "00000000-0000-0000-0000-0000000000000",
...
}
]
记下id属性,它是订阅 ID,如果你有多个订阅,可以使用以下命令将其设置为正确的订阅:
$ export SUBSCRIPTION_ID="<SUBSCRIPTION_ID>"
$ az account set --subscription="$SUBSCRIPTION_ID"
使用以下命令创建一个contributor角色,允许 Terraform 管理订阅的基础设施。
提示
在授予服务主体访问权限时,遵循最小权限原则。不要因为未来可能需要某些权限就提前授予它们。如果未来需要任何访问权限,你可以稍后再授予。
我们使用 contributor 权限是为了简化,但也可以使用更细粒度的权限,这种权限应该被使用:
$ az ad sp create-for-rbac --role="Contributor" \
--scopes="/subscriptions/$SUBSCRIPTION_ID"
Creating 'Contributor' role assignment under scope '/subscriptions/<SUBSCRIPTION_ID>'
The output includes credentials that you must protect. Ensure you do not include these
credentials in your code or check the credentials into your source control (for more
information, see https://aka.ms/azadsp-cli):
{
"appId": "00000000-0000-0000-0000-0000000000000",
"displayName": "azure-cli-2023-07-02-09-13-40",
"password": "00000000000.xx-00000000000000000",
"tenant": "00000000-0000-0000-0000-0000000000000"
}
我们已经成功创建了appId、password和tenant。我们将需要这些信息来配置 Terraform 使用服务主体。在下一节中,我们将根据这些详细信息定义 Azure Terraform 提供程序。
使用 Azure Terraform 提供程序
在定义 Azure Terraform 提供程序之前,让我们了解什么构成了 Terraform 根模块。Terraform 根模块仅是你文件系统中的一个工作目录,包含一个或多个 .tf 文件,这些文件帮助你定义配置,并且通常是在这里运行 Terraform 命令。
Terraform 会扫描所有 .tf 文件,将它们合并,并在内部作为一个整体处理。因此,你可以根据需要将一个或多个 .tf 文件拆分开来。虽然没有为 .tf 文件命名的标准,但大多数约定使用 main.tf 作为主 Terraform 文件,其中定义资源,使用 vars.tf 文件来定义变量,使用 outputs.tf 文件来定义输出。
在本讨论中,让我们在工作目录中创建一个 main.tf 文件,并添加如下的 provider 配置:
terraform {
required_providers {
azurerm = {
source = "azurerm"
version = "=3.55.0"
}
}
}
provider "azurerm" {
subscription_id = var.subscription_id
client_id = var.client_id
client_secret = var.client_secret
tenant_id = var.tenant_id
features {}
}
上面的文件包含两个块。terraform 块包含 required_providers 块,声明了 azurerm 提供程序的 version 约束。provider 块声明了一个 azurerm 提供程序,它需要四个参数。
提示
始终约束提供程序版本,因为提供程序的发布没有通知,如果你不包含版本号,在你的机器上可用的功能可能在其他人的机器或 CI/CD 流水线中无法正常工作。使用版本约束可以避免破坏性变更,并保持对版本的控制。
你可能已经注意到,我们在前面的文件中声明了几个变量,而不是直接输入值。这背后有两个主要原因——我们希望使模板尽可能通用,以促进复用。所以,假设我们希望在另一个订阅中应用相似的配置,或者使用另一个服务主体,我们应该能够通过修改变量值来改变它。其次,我们不希望将 client_id 和 client_secret 存储在源代码管理中。这是一个不好的做法,因为我们将服务主体暴露给了不需要知道的人。
提示
永远不要将敏感数据存储在源代码控制中。相反,使用 tfvars 文件来管理敏感信息,并将其存储在像 HashiCorp Vault 这样的秘密管理系统中。
好的,既然我们已经定义了提供程序资源,并且属性值来自变量,接下来的步骤是声明变量。现在让我们看看如何操作。
Terraform 变量
要声明变量,我们需要创建一个 vars.tf 文件,并包含以下数据:
variable "subscription_id" {
type = string
description = "The azure subscription id"
}
variable "app_id" {
type = string
description = "The azure service principal appId"
}
variable "password" {
type = string
description = "The azure service principal password"
sensitive = true
}
variable "tenant" {
type = string
description = "The azure tenant id"
}
所以,我们在这里使用 variable 块定义了四个变量。变量块通常包含 type 和 description。type 属性定义了我们声明的变量的数据类型,默认为 string 数据类型。它可以是诸如 string、number、bool 这样的基本数据类型,或者是诸如 list、set、map、object 或 tuple 这样的复杂数据结构。稍后在练习中我们会详细讨论这些类型。description 属性提供了有关变量的更多信息,用户可以参考它以便更好地理解。
提示
始终从一开始就设置 description 属性,因为它对用户友好并促进模板的复用。
client_secret 变量还包含一个名为 sensitive 的第三属性,一个设置为 true 的布尔属性。当 sensitive 属性为 true 时,Terraform CLI 不会在屏幕输出中显示它。对于像密码和机密这样的敏感变量,强烈建议使用此属性。
提示
始终将敏感变量声明为 sensitive。这是因为如果在您的 CI/CD 管道中使用 Terraform,非特权用户可能通过查看日志访问敏感信息。
除其他三者外,名为 default 的属性将帮助您指定变量的默认值。默认值可帮助您为变量提供最佳可能的值,用户如有需要可以覆盖它们。
提示
在可能的情况下始终使用默认值,因为它们允许您向用户提供关于企业标准的软指导,并节省他们的时间。
接下来的步骤将是提供变量值。让我们来看看这一点。
提供变量值
在 Terraform 中提供变量值有几种方法:
-
-var标志及variable_name=variable_value字符串以提供这些值。 -
.tfvars(如果您喜欢 HCL)或.tfvars.json(如果您喜欢 JSON),通过命令行使用-var-file标志。 -
在 Terraform 工作区中使用
terraform.tfvars或以.auto.tfvars扩展名结束。Terraform 将自动扫描这些文件并从中获取值。 -
使用
TF_VAR_<var-name>结构包含变量值。 -
默认:当您在任何其他方式中不为变量提供值时运行 Terraform 计划时,Terraform CLI 将提示输入这些值,并且您必须手动输入它们。
如果使用多种方法提供同一变量的值,则在前述列表中的第一种方法对特定变量具有最高优先级。它会覆盖后面列出的任何定义。
我们将使用 terraform.tfvars 文件进行此活动,并为变量提供值。
将以下数据添加到 terraform.tfvars 文件中:
subscription_id = "<SUBSCRIPTION_ID>"
app_id = "<SERVICE_PRINCIPAL_APP_ID>"
password = "<SERVICE_PRINCIPAL_PASSWORD>"
tenant = "<TENANT_ID>"
如果要将 Terraform 配置提交到源代码控制中,请将该文件添加到忽略列表以避免意外提交。
如果使用 Git,请将以下内容添加到 .gitignore 文件中即可:
*.tfvars
.terraform*
现在,让我们继续查看 Terraform 工作流程,以进一步进行。
Terraform 工作流程
Terraform 工作流通常包括以下步骤:
-
init:在您的构建过程中多次初始化 Terraforminit命令,因为它不会更改您的工作区或状态。 -
plan:此命令会在请求的资源上运行一个预估计划。此命令通常与云提供商连接,然后检查 Terraform 管理的对象是否存在于云提供商中,以及它们是否与 Terraform 模板中定义的配置一致。然后,它会显示计划输出中的差异,管理员可以审查并在不满意时更改配置。如果满意,他们可以应用计划,将更改提交到云平台。plan命令不会对当前基础设施进行任何更改。 -
apply:此命令将增量配置应用到云平台。当你单独使用apply时,它会首先运行plan命令并要求确认。如果提供计划,它会直接应用该计划。你也可以在不运行计划的情况下使用apply,通过使用-auto-approve标志来自动批准。 -
destroy:destroy命令销毁 Terraform 管理的整个基础设施。因此,它并不是一个非常受欢迎的命令,在生产环境中很少使用。不过,这并不意味着destroy命令没有用处。假设你正在为临时目的搭建开发环境,之后不再需要它,那么通过此命令销毁你创建的所有内容只需要几分钟。
要访问本节的资源,cd进入以下目录:
$ cd ~/modern-devops/ch8/terraform-exercise
现在,让我们通过实际操作来详细了解这些内容。
terraform init
要初始化 Terraform 工作区,请运行以下命令:
$ terraform init
Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/azurerm versions matching "3.63.0"...
- Installing hashicorp/azurerm v3.63.0...
- Installed hashicorp/azurerm v3.63.0 (signed by HashiCorp)
Terraform has created a lock file, .terraform.lock.hcl, to record the provider selections
it made previously. 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!
由于 Terraform 工作区已被初始化,我们可以创建一个Azure 资源组来开始与云进行交互。
创建第一个资源 – Azure 资源组
我们必须在main.tf文件中使用azurerm_resource_group资源来创建 Azure 资源组。请将以下内容添加到main.tf文件中:
resource "azurerm_resource_group" "rg" {
name = var.rg_name
location = var.rg_location
}
由于我们使用了两个变量,因此我们需要声明它们,请将以下内容添加到vars.tf文件中:
variable "rg_name" {
type = string
description = "The resource group name"
}
variable "rg_location" {
type = string
description = "The resource group location"
}
接下来,我们需要将资源组名称和位置添加到terraform.tfvars文件中。因此,请将以下内容添加到terraform.tfvars文件中:
rg_name=terraform-exercise
rg_location="West Europe"
现在,我们已经准备好执行计划了,但在此之前,让我们使用terraform fmt将我们的文件格式化为标准格式。
terraform fmt
terraform fmt命令将.tf文件格式化为标准格式。使用以下命令格式化你的文件:
$ terraform fmt
terraform.tfvars
vars.tf
该命令列出了它已格式化的文件。下一步是验证你的配置。
terraform validate
terraform validate命令验证当前配置,并检查是否有任何语法错误。要验证配置,请运行以下命令:
$ terraform validate
Success! The configuration is valid.
成功输出表示我们的配置有效。如果有任何错误,它会在验证输出中突出显示。
提示
在每次执行 Terraform 计划之前,请始终运行fmt和validate。这可以节省大量规划时间,并帮助你保持配置的良好状态。
由于配置有效,我们可以准备运行一个计划。
terraform plan
要运行 Terraform 计划,请使用以下命令:
$ terraform plan
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:
# azurerm_resource_group.rg will be created
+ resource "azurerm_resource_group" "rg" {
+ id = (known after apply)
+ location = "westeurope"
+ name = "terraform-exercise"
}
Plan: 1 to add, 0 to change, 0 to destroy.
Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to
take exactly these actions if you run terraform apply now.
plan输出告诉我们,如果我们立即运行terraform apply,它将创建一个名为terraform_exercise的单一资源组。它还输出了一条提示,说明由于我们没有保存此计划,后续的应用操作无法保证执行相同的操作。同时,事情可能已发生变化;因此,Terraform 将在应用时重新运行plan并提示我们输入yes。因此,如果不想遇到意外,应该将计划保存到文件中。
提示
始终将terraform plan输出保存到文件,并使用该文件来应用更改。这样可以避免最后一分钟的意外,避免背景中可能发生的变化导致apply没有按预期执行,尤其是在您的计划作为流程的一部分进行审查时。
所以,我们先使用以下命令将计划保存到文件中:
$ terraform plan -out rg_terraform_exercise.tfplan
这次,计划被保存为名为rg_terraform_exercise.tfplan的文件。我们可以使用这个文件来随后应用更改。
terraform apply
要使用plan文件应用更改,请运行以下命令:
$ terraform apply "rg_terraform_exercise.tfplan"
azurerm_resource_group.rg: Creating...
azurerm_resource_group.rg: Creation complete after 2s [id=/subscriptions/id/
resourceGroups/terraform-exercise]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
就是这样!Terraform 已应用配置。让我们使用 Azure CLI 验证资源组是否已创建。
运行以下命令列出您订阅中的所有资源组:
$ az group list
...
"id": "/subscriptions/id/resourceGroups/terraform-exercise",
"location": "westeurope",
"name": "terraform-exercise",
...
我们看到资源组已创建并出现在列表中。
有时apply可能部分成功。在这种情况下,Terraform 会自动标记它认为未成功创建的资源。这些资源将在下一次运行时自动重新创建。如果您希望手动标记某个资源以供重新创建,可以使用terraform的taint命令:
$ terraform taint <resource>
假设我们希望销毁资源组,因为我们不再需要它。我们可以使用terraform destroy来完成。
terraform destroy
要销毁资源组,我们可以先运行一个推测性的计划。始终建议运行推测性计划,以确认我们需要销毁的内容是否包含在输出中,以免稍后出现意外。Terraform 就像 Linux 一样,没有撤销按钮。
要运行一个推测性的销毁计划,请使用以下命令:
$ terraform plan -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.rg will be destroyed
- resource "azurerm_resource_group" "rg" {
- id = "/subscriptions/id/resourceGroups/terraform-exercise" -> null
- location = "westeurope" -> null
- name = "terraform-exercise" -> null
- tags = {} -> null
}
Plan: 0 to add, 0 to change, 1 to destroy.
如我们所见,由于资源组是 Terraform 管理的唯一资源,它已列出该资源作为将要销毁的资源。销毁资源有两种方式:使用terraform destroy单独运行,或使用out参数保存推测性计划后,运行terraform apply来应用销毁计划。
目前我们先使用第一种方法。
运行以下命令销毁所有由 Terraform 管理的资源:
$ terraform destroy
Terraform will perform the following actions:
# azurerm_resource_group.rg will be destroyed
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:
现在,这次,Terraform 重新运行plan并提示输入一个值。它只会接受yes,然后按Enter键确认:
Enter a value: yes
azurerm_resource_group.rg: Destroying... [id=/subscriptions/id/resourceGroups/terraform-
exercise]
azurerm_resource_group.rg: Still destroying... [id=/subscriptions/id/resourceGroups/
terraform-exercise, 10s elapsed]
azurerm_resource_group.rg: Destruction complete after 1m20s
现在它已经销毁了资源组。
我们已经看过一个基础的根模块,并探讨了 Terraform 的工作流程。基础根模块帮助我们创建和管理资源,但缺少一个非常重要的特性——可重用性。Terraform 为我们提供了模块,允许我们对常见模板进行重用。在下一部分,我们将看看它。
Terraform 模块
Terraform 模块是可重用、可重复的模板。它们允许在基础设施提供过程中进行抽象,这在您的使用场景超出一些概念验证时非常需要。HashiCorp 将模块视为由专家设计,这些专家了解企业标准,并被希望在项目中应用企业标准基础设施的开发人员使用。这样,整个组织的事情都能保持标准化。这为开发人员节省时间,避免重复劳动。模块可以版本控制并通过 模块仓库 或版本控制系统分发。这同时为基础设施管理员提供了丰富的权限和控制。
在上一部分我们创建了资源组,接下来让我们将其模块化。在下一个练习中,为了访问该部分的资源,请执行以下操作:
$ cd ~/modern-devops/ch8/terraform-modules/
在此目录中,我们有以下目录结构:
.
├── main.tf
├── modules
│ └── resource_group
│ ├── main.tf
│ └── vars.tf
├── terraform.tfvars
└── vars.tf
如我们所见,根目录中我们仍然有 main.tf、terraform.tfvars 和 vars.tf 文件。然而,我们增加了一个 modules 目录,其中包含一个 resource_group 子目录,里面有一个 main.tf 文件和一个 vars.tf 文件。让我们来看一下这两个文件。
modules/resource_group/main.tf 文件如下所示:
resource "azurerm_resource_group" "rg" {
name = var.name
location = var.location
}
它只包含一个 azurerm_resource_group 资源,其名称和位置来自以下 modules/resource_group/vars.tf 文件中定义的 name 和 location 变量:
variable "name" {
type = string
description = "The resource group name"
}
variable "location" {
type = string
description = "The resource group location"
}
在根模块中,也就是当前目录,我们已修改 main.tf 文件,使其如下所示:
terraform {
required_providers {
...
}
}
provider "azurerm" {
...
}
module "rg" {
source = "./modules/resource_group"
name = var.rg_name
location = var.rg_location
}
如我们所见,我们没有直接在此文件中定义资源,而是定义了一个名为 rg 的模块,其 source 是 ./modules/resource_group。请注意,我们从根级别定义的变量(即 var.rg_name 和 var.rg_location)传递给模块中定义的变量,即 name 和 location 的值。
现在,让我们继续看看当我们初始化并应用这个配置时会发生什么。
运行以下命令来初始化 Terraform 工作区:
$ terraform init
Initializing the backend...
Initializing modules...
- rg in modules/resource_group
Initializing provider plugins...
...
Terraform has been successfully initialized!
如我们所见,Terraform 在初始化时已检测到新模块并对其进行了初始化。
提示
每当定义新模块时,您必须始终重新初始化 Terraform。
现在,让我们继续运行以下命令来进行计划:
$ terraform plan
Terraform will perform the following actions:
# module.rg.azurerm_resource_group.rg will be created
+ resource "azurerm_resource_group" "rg" {
+ id = (known after apply)
+ location = "westeurope"
+ name = "terraform-exercise"
}
Plan: 1 to add, 0 to change, 0 to destroy.
如我们所见,它将创建资源组。然而,这现在是模块 module.rg.azurerm_resource_group.rg 的一部分。要应用计划,让我们运行以下命令:
$ terraform apply
module.rg.azurerm_resource_group.rg: Creating...
module.rg.azurerm_resource_group.rg: Creation complete after 4s [id=/subscriptions/id/
resourceGroups/terraform-exercise]
资源组已创建!要销毁该资源组,让我们运行以下命令:
$ terraform destroy
通过使用模块,您可以简化基础设施的创建和管理,增强团队之间的协作,并建立一种一致的方法来以可扩展和可维护的方式部署资源。
提示
使用 Terraform 模块封装并重用基础设施配置,促进模块化和可重用性。
直到现在,我们已经看到 Terraform 创建和销毁资源,但 Terraform 如何知道它之前创建了什么以及需要销毁什么呢?嗯,它使用状态文件来解决这个问题。我们来看看。
管理 Terraform 状态
Terraform 使用状态文件来跟踪已部署的内容及其管理的资源。状态文件至关重要,因为它记录了 Terraform 维护的所有基础设施。如果丢失了状态文件,Terraform 将无法追踪已执行的操作,并且会将资源当作新的、需要重新创建的资源。因此,您应该像代码一样保护状态文件。
Terraform 将状态存储在后端。默认情况下,Terraform 将状态文件存储为 terraform.tfstate,并放置在 workspace 目录中,这称为本地后端。然而,这并不是管理状态的最佳方式。您不应将状态存储在本地系统中,原因有几个:
-
如果状态文件存储在某人的本地目录中,多个管理员无法共同操作同一基础设施。
-
本地工作站没有备份;因此,即使只有一个管理员在执行任务,丢失状态文件的风险仍然很高。
您可能会争辩说,我们可以通过将状态文件与 .tf 文件一起检查到源代码管理中来解决这些问题。不要这样做!状态文件是纯文本的,如果您的基础设施配置包含敏感信息,如密码,任何人都可以看到它。因此,您需要安全存储状态文件。此外,将状态文件存储在源代码管理中并不能提供状态锁定功能,如果多人同时修改状态文件,会导致冲突。
提示
永远不要将状态文件存储在源代码管理中。使用 .gitignore 文件条目跳过 terraform.tfstate 文件。
存储 Terraform 状态的最佳位置是远程云存储。Terraform 提供了远程后端来远程存储状态。您可以使用多种类型的远程后端。当编写本书时,plan 和 apply 在后端运行,只有 Terraform Cloud 和 Enterprise 支持这一功能。
提示
在选择状态存储方案时,您应该优先选择支持状态锁定的存储。这将允许多人操作资源而不互相干扰,一旦状态文件被锁定,其他人将无法获取它,直到锁定被释放。
由于我们使用的是 Azure,因此可以使用 Azure 存储来保存状态。其优点有三:
-
您的状态文件是集中管理的。您可以让多个管理员一起工作并管理相同的基础设施。
-
存储在休眠状态下是加密的。
-
你将获得自动备份、冗余和高可用性。
要访问本节的资源,cd到以下目录:
$ cd ~/modern-devops/ch8/terraform-backend/
现在,让我们使用azurerm后端,并使用 Azure 存储来持久化我们的 Terraform 状态。
使用 Azure 存储后端
由于如果我们使用 Terraform 来构建存储其状态的后端,我们将陷入“鸡和蛋”的局面,所以我们必须在不使用 Terraform 的情况下配置这部分内容。
因此,让我们使用az命令在一个 Terraform 不会管理的不同资源组中配置存储帐户。
创建 Azure 存储资源
让我们从定义一些变量开始:
-
$RESOURCE_GROUP=tfstate -
$STORAGE_ACCOUNT_NAME=tfstate$RANDOM -
$CONTAINER_NAME=tfstate
首先,使用以下命令创建资源组:
$ az group create --name $RESOURCE_GROUP --location westeurope
现在,让我们继续使用以下命令在资源组内创建一个存储帐户:
$ az storage account create --resource-group $RESOURCE_GROUP \
--name $STORAGE_ACCOUNT_NAME --sku Standard_LRS \
--encryption-services BLOB
下一步是使用以下命令获取帐户密钥:
$ ACCOUNT_KEY=$(az storage account keys list \
--resource-group tfstate --account-name $STORAGE_ACCOUNT_NAME \
--query '[0].value' -o tsv)
现在,我们可以使用以下命令创建一个 Blob 存储容器:
$ az storage container create --name $CONTAINER_NAME \
--account-name $STORAGE_ACCOUNT_NAME --account-key $ACCOUNT_KEY
如果我们收到created响应,则表示存储帐户已创建并准备就绪。现在,我们可以继续在 Terraform 中定义后端配置文件。
在 Terraform 中创建后端配置
在我们创建后端之前,我们需要STORAGE_ACCOUNT_NAME的值。要获取此值,请运行以下命令:
$ echo $STORAGE_ACCOUNT_NAME
tfstate28099
要在 Terraform 中创建后端配置,创建一个名为backend.tf的文件,并放在工作区内:
terraform {
backend "azurerm" {
resource_group_name = "tfstate"
storage_account_name = "tfstate28099"
container_name = "tfstate"
key = "example.tfstate"
}
}
在后端配置中,我们定义了resource_group_name后端,其中存在 Blob 存储实例 —— storage_account_name,container_name和key。key属性指定我们将用于定义此配置状态的文件名。你可能会使用 Terraform 管理多个项目,它们都需要单独的状态文件。因此,key属性定义了我们将为项目使用的状态文件的名称。这使得多个 Terraform 项目可以使用相同的 Azure Blob 存储来存储状态。
提示
始终使用项目的名称作为key的名称。例如,如果你的项目名称是foo,则将key命名为foo.tfstate。这样可以防止与其他项目发生潜在冲突,并且还可以让你快速找到状态文件。
要使用新的后端配置初始化 Terraform 工作区,请运行以下命令:
$ terraform init
Initializing the backend...
Backend configuration changed!
Terraform has detected that the configuration specified for the backend has changed.
Terraform will now check for existing state in the backends.
Successfully configured the backend azurerm! Terraform will automatically use this backend
unless the backend configuration changes.
当我们初始化时,Terraform 会检测到后端已更改,并检查现有后端中是否有任何内容。如果它找到内容,它会询问是否要将当前状态迁移到新后端。如果没有,它会自动切换到新后端,就像我们在这里看到的那样。
现在,让我们继续使用terraform plan命令来运行计划:
$ terraform plan
Acquiring state lock. This may take a few moments...
Terraform will perform the following actions:
# azurerm_resource_group.rg will be created
+ resource "azurerm_resource_group" "rg" {
...
}
Plan: 1 to add, 0 to change, 0 to destroy.
如我们所见,terraform plan 告诉我们,它将创建一个名为 terraform-exercise 的新资源组。让我们应用这个配置,这次使用auto-approve标志,这样计划就不会再次运行,Terraform 会立即应用更改,使用以下命令:
$ terraform apply -auto-approve
Acquiring state lock. This may take a few moments...
azurerm_resource_group.rg: Creating...
azurerm_resource_group.rg: Creation complete after 2s [id=/subscriptions/id/
resourceGroups/terraform-exercise]
Releasing state lock. This may take a few moments...
我们现在已经成功创建了资源。
现在,让我们去 Azure Blob 存储查看是否有一个 tfstate 文件,如下图所示:
图 8.2 – Terraform 状态
如我们所见,在 Blob 容器中有一个名为example.tfstate的文件。这就是远程存储的工作方式,现在任何有权限访问 Blob 存储实例的人都可以使用 Terraform 配置并进行更改。
到目前为止,我们一直在使用默认工作区管理资源,但如果有多个环境需要使用相同的配置进行控制怎么办?好吧,Terraform 为这些场景提供了工作区。
Terraform 工作区
软件开发需要多个环境。你在工作区内开发软件,将其部署到开发环境,进行单元测试,然后将经过测试的代码提升到测试环境。你的 QA 团队将在测试环境中对代码进行广泛的测试,一旦所有测试用例通过,你就可以将代码推广到生产环境。
这意味着你必须在所有环境中保持类似的基础设施。借助像 Terraform 这样的 IaC 工具,基础设施以代码的形式呈现,我们必须管理我们的代码以适应多个环境。但 Terraform 不仅仅是代码;它还包含状态文件,我们必须为每个环境维护状态文件。
假设你想创建三个资源组,分别是 terraform-exercise-dev、terraform-exercise-test 和 terraform-exercise-prod。每个资源组将包含一组相似的基础设施,具有类似的属性。例如,每个资源组都包括一个 Ubuntu 虚拟 机(VM)。
解决此问题的一种简单方法是创建如下结构:
├── dev
│ ├── backend.tf
│ ├── main.tf
│ ├── terraform.tfvars
│ └── vars.tf
├── prod
│ ├── backend.tf
│ ├── main.tf
│ ├── terraform.tfvars
│ └── vars.tf
└── test
├── backend.tf
├── main.tf
├── terraform.tfvars
└── vars.tf
你能看到重复吗?相同的文件多次出现,所有文件都包含相同的配置。唯一可能改变的是每个环境的 terraform.tfvars 文件。
所以,这听起来并不是一个很好的解决方法,这就是为什么 Terraform 为此提供了工作区。
Terraform 工作区仅仅是独立的状态文件。所以,你有一个配置和多个状态文件,每个环境都有一个。听起来很简单,对吧?让我们看一看。
使用 Terraform 工作区表示相同配置的另一种方式如下:
├── backend.tf
├── main.tf
├── terraform.tfvars
└── vars.tf
现在,这看起来很简单。它只包含一组文件。让我们逐一查看它们,以便更好地理解它们。
要访问本节的资源,请cd到以下位置:
$ cd ~/modern-devops/ch8/terraform-workspaces/
main.tf文件包含一个resource_group资源,其名称包括环境后缀,以及我们需要在该资源组内创建的其他资源,如 VNet、子网和 VM,类似以下内容:
...
resource "azurerm_resource_group" "main" {
name = "${var.rg_prefix}-${terraform.workspace}"
location = var.rg_location
}
resource "azurerm_virtual_network" "main" {
...
}
resource "azurerm_subnet" "internal" {
...
}
resource "azurerm_network_interface" "main" {
...
}
resource "azurerm_virtual_machine" "main" {
...
}
...
要访问工作区的名称,Terraform 提供了terraform.workspace变量,我们已使用该变量定义resource_group名称。所以,模板现在已准备好接受任何环境的配置,并且我们将为每个环境创建一个独立的资源组。
同时,更新backend.tf文件,添加我们在上一节中创建的tfstate容器名称,并使用以下命令初始化 Terraform 工作区:
$ terraform init
现在,一旦 Terraform 初始化完成,让我们使用以下命令创建一个dev工作区:
$ terraform workspace new dev
Created and switched to workspace "dev"!
你现在处于一个新的、空的工作区。工作区会隔离其状态,因此如果你运行terraform plan,Terraform 将不会看到此配置的任何现有状态。
所以,既然我们处于一个名为dev的新空工作区,让我们运行一个计划。
使用以下命令在dev环境中运行计划:
$ terraform plan -out dev.tfplan
Acquiring state lock. This may take a few moments...
Terraform will perform the following actions:
+ resource "azurerm_network_interface" "main" {
...
}
+ resource "azurerm_resource_group" "main" {
+ id = (known after apply)
+ location = "westeurope"
+ name = "terraform-ws-dev"
}
+ resource "azurerm_subnet" "internal" {
...
}
+ resource "azurerm_virtual_machine" "main" {
...
}
+ resource "azurerm_virtual_network" "main" {
...
}
Plan: 5 to add, 0 to change, 0 to destroy.
现在,让我们继续使用以下命令应用该计划:
$ terraform apply "dev.tfplan"
Acquiring state lock. This may take a few moments...
azurerm_resource_group.main: Creating...
azurerm_virtual_network.main: Creating...
azurerm_subnet.internal: Creating...
azurerm_network_interface.main: Creating...
azurerm_virtual_machine.main: Creating...
Apply complete! Resources: 5 added, 0 changed, 0 destroyed.
Releasing state lock. This may take a few moments...
由于dev计划已被应用,资源已创建在dev资源组中,我们来创建一个测试用的工作区:
$ terraform workspace new test
由于新工作区已创建,让我们使用以下命令在测试工作区运行计划,并将其保存到test.tfplan文件中:
$ terraform plan -out test.tfplan
...
+ resource "azurerm_resource_group" "main" {
+ id = (known after apply)
+ location = "westeurope"
+ name = "terraform-ws-test"
}
...
如我们所见,资源将被创建在terraform-ws-test资源组中。所以,让我们继续使用以下命令应用计划:
$ terraform apply test.tfplan
test计划也已经应用。现在让我们继续检查已创建的资源。
提示
Terraform 工作区非常适合为不同的环境(如开发、预生产和生产)维护独立的基础设施配置。这有助于防止意外的配置更改,并确保一致的设置。
检查资源
让我们使用az命令列出资源组。如我们所知,我们的资源组有一个前缀terraform-ws。因此,使用以下命令列出所有包含该前缀的资源组:
$ az group list | grep name | grep terraform-ws
"name": "terraform-ws-dev",
"name": "terraform-ws-test",
如我们所见,我们有两个资源组,terraform-ws-dev和terraform-ws-test。所以,两个资源组已成功创建。
你也可以在 Azure 门户中验证这一点,如下图所示:
图 8.3 – 资源组
现在,让我们使用 Azure 门户,通过点击terraform-ws-dev来检查terraform-ws-dev资源组中的资源:
图 8.4 – Terraform 开发资源组
我们在资源组中有一个虚拟网络、一个网络接口、一个操作系统磁盘和一台虚拟机。我们应该期待在terraform-ws-test资源组中有相同名称的类似资源。让我们来看看:
图 8.5 – Terraform 测试资源组
如我们所见,我们在 terraform-ws-test 资源组中也有类似的资源。
我们通过一个配置完成了所有这些操作,但由于它们是两组资源,每个工作空间应该有两个状态文件。让我们来看一下。
检查状态文件
如果我们使用本地后端来存储状态文件,我们将得到以下结构:
|-- terraform.tfstate.d
|-- dev
| `-- terraform.tfstate
`-- test
`-- terraform.tfstate
所以,Terraform 创建了一个名为 terrafom.tfstate.d 的目录;在这个目录下,它为每个工作空间创建了相应的子目录。在这些子目录中,它将每个工作空间的状态文件保存为 terraform.tfstate。
但由于我们使用的是远程后端,并且使用 Azure Blob 存储来存储它,让我们通过 Azure 控制台检查其中的文件:
图 8.6 – Terraform 工作空间状态
如我们所见,有两个状态文件,每个环境一个。因此,状态文件的后缀分别是 env:dev 或 env:test 字符串。这就是在 Azure Blob 存储中管理工作空间的方式。远程后端用于维护状态文件的结构取决于提供商插件,因此,管理多个后端的状态可能会有不同的方式。但是,Terraform CLI 会以相同的方式解释工作空间,无论后端如何,因此,从 CLI 角度来看,最终用户没有变化。
清理
现在,让我们继续清理这两个资源组,以避免不必要的费用。
由于我们已经在测试工作空间内,让我们运行以下命令销毁测试工作空间中的资源:
$ terraform destroy --auto-approve
现在,让我们使用以下命令切换到 dev 工作空间:
$ terraform workspace select dev
Switched to workspace "dev".
由于我们在 dev 工作空间中,使用以下命令销毁 dev 工作空间中的所有资源:
$ terraform destroy --auto-approve
稍后,我们应该能看到两个资源组已经消失。接下来,让我们在下一节中了解一些 Terraform 的高级概念。
Terraform 输出、状态、控制台和图表
虽然我们知道 Terraform 使用状态文件来管理资源,但让我们来看一些高级命令,帮助我们更好地理解和掌握 Terraform 状态的概念。
要访问本节中的资源,cd 到以下路径:
$ cd ~/modern-devops/ch8/terraform-workspaces/
现在,让我们来看一下我们的第一个命令——terraform output。
terraform 输出
到目前为止,我们已经看过了变量,但还没有讨论输出。Terraform 输出是 Terraform 配置的返回值,允许用户将配置导出给用户或任何可能使用当前模块的模块。
让我们继续上一个例子,在 outputs.tf 文件中添加一个输出变量,用来导出附加到虚拟机的网络接口的私有 IP:
output "vm_ip_addr" {
value = azurerm_network_interface.main.private_ip_address
}
现在,让我们继续应用配置:
$ terraform apply --auto-approve
...
Outputs:
vm_ip_addr = "10.0.2.4"
在 Terraform 应用配置后,它会在控制台结果的末尾显示输出。你可以随时运行以下命令来检查输出:
$ terraform output
vm_ip_addr = "10.0.2.4"
输出像其他内容一样存储在状态文件中,因此让我们来看一下如何使用 CLI 管理 Terraform 状态。
管理 Terraform 状态
Terraform 将其管理的配置存储在状态文件中,因此提供了一个用于高级状态管理的命令。terraform state命令帮助你管理当前配置的状态。尽管状态文件是纯文本格式的,你可以手动修改它,但推荐使用terraform state命令。
但在我们深入讨论细节之前,我们必须了解为什么要这么做。事情可能不会总是按计划进行,因此状态文件可能包含损坏的数据。你可能还想在应用某个资源后查看该资源的特定属性。状态文件可能需要进行调查,以便进行特定基础设施配置问题的根本原因分析。让我们看看最常见的使用场景。
查看当前状态
要查看当前状态,我们可以运行以下命令:
$ terraform show
这将输出所有 Terraform 创建和管理的资源,包括输出内容。当然,对于一些人来说,这可能会显得信息过载,我们可能只想查看 Terraform 管理的资源列表。
列出当前状态中的资源
要列出 Terraform 状态文件中的资源,可以运行以下命令:
$ terraform state list
azurerm_network_interface.main
azurerm_resource_group.main
azurerm_subnet.internal
azurerm_virtual_machine.main
azurerm_virtual_network.main
如我们所见,Terraform 管理着五个资源。你可能希望将某个资源从 Terraform 状态中移除。也许有人手动移除了某个资源,因为它不再需要,但它并未从 Terraform 配置中移除。
从状态中移除资源
要手动从 Terraform 状态文件中移除状态,必须使用terraform state rm <resource>命令。例如,要从 Terraform 状态中移除 Azure 虚拟机资源,可以运行以下命令:
$ terraform state rm azurerm_virtual_machine.main
Acquiring state lock. This may take a few moments...
Removed azurerm_virtual_machine.main
Successfully removed 1 resource instance(s).
Releasing state lock. This may take a few moments...
请记住,这仅仅是从状态文件中移除了该资源,实际在 Azure 上存在的资源并未受到影响。
可能会有某些情况,其中有人在 Azure 中手动创建了虚拟机,而我们现在希望 Terraform 来管理它。这种情况大多发生在棕地项目中。在这种情况下,我们必须在 Terraform 中声明相同的配置,然后将现有资源导入到 Terraform 状态中。为此,我们可以使用terraform import命令。
将现有资源导入 Terraform 状态
你可以使用terraform import命令将现有资源导入到 Terraform 状态中。terraform import命令的结构如下:
terraform import <resource> <resource_id>
例如,要将httpd虚拟机重新导入状态,可以运行以下命令:
$ terraform import azurerm_virtual_machine.main \
"/subscriptions/<SUBSCRIPTION_ID>/resourceGroups\
/terraform-ws-dev/providers/Microsoft.Compute/virtualMachines/httpd"
Acquiring state lock. This may take a few moments...
azurerm_virtual_machine.main: Importing from ID "/subscriptions/id/resourceGroups/
terraform-ws-dev/providers/Microsoft.Compute/virtualMachines/httpd"...
azurerm_virtual_machine.main: Import prepared!
Prepared azurerm_virtual_machine for import
azurerm_virtual_machine.main: Refreshing state... [id=/subscriptions/1de491b5-f572-
459b-a568-c4a35d5ac7a9/resourceGroups/terraform-ws-dev/providers/Microsoft.Compute/
virtualMachines/httpd]
Import successful!
要检查资源是否已导入状态,我们可以再次列出资源,使用以下命令:
$ terraform state list | grep azurerm_virtual_machine
azurerm_virtual_machine.main
如我们所见,VM 已经存在于状态文件中。如果我们想进一步了解资源,可以使用 terraform console。
terraform console
terraform console 命令提供了一个交互式控制台,用于调查状态文件、动态构建路径并在使用资源之前评估表达式。这是一个强大的工具,大多数高级 Terraform 用户都在使用它。例如,我们启动控制台并查看我们刚刚导入的 VM 资源配置。
使用以下命令启动控制台并获取 VM 的资源组和 id 值:
$ terraform console
Acquiring state lock. This may take a few moments...
> azurerm_virtual_machine.main.resource_group_name
"terraform-ws-dev"
> azurerm_virtual_machine.main.id
"/subscriptions/id/resourceGroups/terraform-ws-dev/providers/Microsoft.Compute/
virtualMachines/httpd"
> exit
Releasing state lock. This may take a few moments...
如我们所见,VM 位于正确的资源组中,我们确认导入操作是正确的。
Terraform 依赖关系和图形
Terraform 使用依赖模型来管理资源的创建和销毁顺序。有两种依赖关系——隐式和显式。直到现在,我们一直在使用隐式依赖关系,其中 VM 依赖于网络接口,网络接口依赖于子网,子网依赖于虚拟网络,所有这些资源都依赖于资源组。这些依赖关系通常发生在我们将一个资源的输出作为另一个资源的输入时。
然而,有时我们希望显式定义资源之间的依赖关系,尤其是当没有方法定义隐式依赖时。你可以使用 depends_on 属性来进行这种操作。
提示
除非必要,否则避免使用显式依赖关系,因为 Terraform 使用并行处理来管理资源。如果不需要显式依赖关系,它将加速 Terraform 运行,因为它可以并行处理多个资源。
为了可视化资源之间的依赖关系,我们可以从状态文件导出图形,并使用像Graphviz这样的工具将其转换为 PNG 文件。
运行以下命令导出依赖图:
$ terraform graph > vm.dot
然后我们可以使用 Graphviz 工具处理图形文件。在 Ubuntu 上安装该工具,运行以下命令:
$ sudo apt install graphviz -y
现在运行以下命令将图形文件转换为 PNG 文件:
$ cat vm.dot | dot -T png -o vm.png
图形可以在 github.com/PacktPublishing/Modern-DevOps-Practices-2e/blob/main/ch8/terraform-graph.png 处查看。现在,让我们继续查看如何清理资源。
清理资源
如我们所知,我们运行以下命令来清理资源:
$ terraform destroy --auto-approve
它将清除资源组中的资源,并在此之后删除资源组。
虽然使用 terraform destroy 可以轻松删除不需要的资源,但最好只在 dev 环境中使用它,绝不要在生产环境中使用。相反,你可以从配置中删除不需要的资源,然后运行 terraform apply。
总结
在本章中,我们讨论了 Terraform 的核心内容,并从实践角度理解了一些最常见的命令和功能。我们从理解 IaC 开始,介绍了 Terraform 作为 IaC 工具,安装了 Terraform,了解了 Terraform 提供程序,并使用 Azure Terraform 提供程序管理 Azure 中的基础设施。
接着,我们看了 Terraform 变量以及多种为变量提供值的方式。我们讨论了核心的 Terraform 工作流程以及你将用来管理基础设施的几个命令。然后,我们看了 Terraform 模块,接着看了 Terraform 状态,作为一个重要组成部分,帮助 Terraform 跟踪它所管理的基础设施。
我们了解了本地和远程状态存储,并使用 Azure Blob 存储作为远程状态后端。接着,我们讨论了 Terraform 工作空间,以及它们如何通过实践操作使我们能够使用相同的 Terraform 配置构建多个环境。
接着,我们看了一些关于 Terraform 状态的高级操作,使用了outputs、state和console命令。最后,我们查看了 Terraform 如何管理依赖关系,并使用graph命令查看了依赖图。
在下一章中,我们将深入讨论使用 Ansible 的配置管理。
问题
-
为什么我们应该限制提供程序版本?
-
你应该始终在 Terraform 计划之前使用
fmt和validate函数。(对/错) -
Terraform
plan命令做什么?(选择两个)A. 刷新当前状态与现有基础设施状态
B. 获取当前配置与预期配置之间的差异
C. 将配置应用于云端
D. 销毁云端的配置
-
terraform apply命令做什么?(选择三个)A. 刷新当前状态与现有基础设施
B. 获取当前配置与预期配置之间的差异
C. 将配置应用于云端
D. 销毁云端的配置
-
为什么你绝不应该将状态文件存储在源代码管理中?(选择两个)
A. 状态文件是纯文本的,因此你暴露了敏感信息给没有权限的用户。
B. 源代码管理不支持状态锁定,因此可能会导致用户之间的潜在冲突。
C. 多个管理员不能同时在同一配置上工作。
-
以下哪些是有效的 Terraform 远程后端?(选择五个)
A. S3
B. Azure Blob 存储
C. Artifactory
D. Git
E. HTTP
F. Terraform 企业版
-
哪个命令将在下次
apply中标记一个资源以供重建? -
如果你使用工作空间,本地后端中的状态文件存储在哪里?
-
我们应该使用什么命令将 Terraform 资源从状态中移除?
-
我们应该使用什么命令将现有的云资源导入到状态中?
答案
-
因为 Terraform 提供程序是单独发布到 Terraform CLI 的,不同版本可能会破坏现有配置
-
对
-
A, B
-
A, B, C
-
A, B
-
A, B, C, E, F
-
taint命令 -
terraform.tfstate.d -
terraform staterm <resource> -
terraform import <``resource> <id>
第九章:使用 Ansible 进行配置管理
在上一章中,我们介绍了基础设施即代码(IaC)与 Terraform,以及其核心概念、IaC 工作流、状态和调试技巧。现在,我们将深入探讨使用 Ansible 进行配置管理(CM)和配置即代码(CaC)。Ansible 是一种配置管理工具,帮助你将配置定义为幂等的代码块。
在本章中,我们将涵盖以下主要主题:
-
配置管理简介
-
设置 Ansible
-
Ansible 剧本简介
-
Ansible 剧本实战
-
设计可重用性
技术要求
你需要一个有效的 Azure 订阅才能进行本章的练习。目前,Azure 提供 30 天的免费试用,赠送 200 美元的免费信用额度,你可以在 azure.microsoft.com/en-in/free 注册。
你还需要克隆以下 GitHub 仓库以完成部分练习:
github.com/PacktPublishing/Modern-DevOps-Practices-2e
运行以下命令将仓库克隆到你的主目录,并cd进入ch9目录以访问所需资源:
$ git clone https://github.com/PacktPublishing/Modern-DevOps-Practices-2e.git \
modern-devops
$ cd modern-devops/ch9
你还需要在系统中安装 Terraform。有关安装和设置 Terraform 的更多详细信息,请参考 第八章,使用 Terraform 实现基础设施即代码(IaC)。
配置管理简介
在技术和系统管理领域,配置管理(CM)可以比作指挥家领导乐团的角色。想象一下,你在指导一群演奏不同乐器的音乐家。你的责任是确保每个人都能和谐地同步,遵循正确的乐谱,在恰当的时刻演奏各自的部分。
在技术和系统管理的背景下,配置管理(CM)是指巧妙地协调和监督计算机系统和软件的创建、更新和维护的实践,就像指挥家指挥乐队演奏精彩的音乐一样。
下面是其功能的分解:
-
标准化:就像音乐家使用相同的音符和音阶一样,配置管理(CM)确保组织内的所有计算机和软件都遵循标准化的配置。这种一致性减少了错误并增强了系统的可靠性。
-
自动化:在乐队中,音乐家不会在演出时手动调试他们的乐器。同样,配置管理工具自动化计算机系统的配置和维护,持续应用配置而无需人工干预。
-
版本控制:音乐家遵循特定的乐谱,如果发生更改,每个人都会收到更新的乐谱。配置管理保持系统配置的版本历史,简化了更改跟踪、回滚到先前的版本,并确保全局一致性。
-
效率:就像指挥家同步每个乐器的节奏一样,配置管理优化系统性能和资源分配。它确保软件和系统高效运行,并根据需要进行扩展。
-
合规性和安全性:类似于指挥家执行表演指导,配置管理(CM)确保遵循安全政策和最佳实践。它在维护一个安全且合规的 IT 环境中发挥着至关重要的作用。
-
故障排除:当表演过程中出现问题时,指挥家迅速识别并解决问题。配置管理工具帮助排查和修复与 IT 系统配置相关的问题。
为了更好地理解配置管理,让我们首先看一下传统的托管和管理应用程序的方式。我们首先从物理基础设施中创建一个虚拟机(VM),然后手动登录到虚拟机中。接着我们可以运行一组脚本或手动进行设置。至少,直到现在,我们一直是这样做的,甚至在本书中也是如此。
这种方法存在一些问题。让我们来看一些:
-
如果我们手动设置服务器,过程不可重复。例如,如果我们需要构建另一台具有类似配置的服务器,就必须重复整个过程来构建新的服务器。
-
即使我们使用脚本,这些脚本本身也不是幂等的。这意味着它们无法识别并仅在需要时应用增量配置。
-
典型的生产环境由多个服务器组成;因此,手动设置一切是一个劳动密集型任务,并且增加了繁重的工作量。软件工程师应该专注于自动化那些导致繁琐工作的过程。
-
虽然你可以将脚本存储在源代码管理中,但它们是命令式的。我们始终鼓励使用声明式的方式来管理事物。
现代的配置管理工具如 Ansible 通过提供以下好处解决了所有这些问题:
-
它们通过一组声明式代码片段来管理配置
-
你可以将代码存储在版本控制中
-
你可以从单个控制节点将代码应用到多个服务器
-
由于它们是幂等的,它们只应用增量配置
-
这是一个可重复的过程;你可以使用变量和模板将相同的配置应用于多个环境
-
它们提供部署编排,主要用于 CI/CD 流水线中
尽管市场上有许多提供配置管理的工具,如Ansible、Puppet、Chef和SaltStack,但 Ansible 是最受欢迎和最简单的工具。它更高效,且由于其简洁性,花费的时间比其他工具少。
这是一个开源的配置管理工具,使用 Python 构建,并由红帽公司拥有。它提供以下功能:
-
它帮助你自动化常规任务,如操作系统升级、补丁和备份,同时创建所有操作系统级别的配置,如用户、组、权限等。
-
配置使用简单的 YAML 语法编写
-
它通过 安全外壳(SSH)与管理节点通信并发送命令
-
命令在每个节点内按顺序以幂等方式执行
-
它通过并行连接节点来节省时间
让我们深入探讨使用 Ansible 作为配置管理和自动化工具的优点。以下是一些令人信服的因素:
-
简洁与易用性:Ansible 拥有简单的、易于理解的 YAML 语法,即使对于编码经验有限的人也能轻松掌握和使用。
-
无代理方式:Ansible 通过 SSH 或 WinRM 通信,避免在管理节点上安装代理,减少了开销和安全问题,关于这一点,我们将在讨论 Ansible 架构时进一步探讨。
-
幂等操作:Ansible 确保即使配置被重复应用,也能实现期望的系统状态,从而最小化意外更改的风险。
-
广泛采用:Ansible 拥有一个蓬勃发展的活跃用户社区,提供大量的文档、模块和针对各种使用案例的 playbook。
-
跨平台兼容性:Ansible 可以处理多种环境,管理不同的操作系统、云服务商、网络设备和基础设施组件,且只需一个工具。
-
无缝集成:Ansible 可以与其他工具无缝集成,包括 版本控制系统(VCS)、监控解决方案和 CI/CD 管道。
-
可扩展性:Ansible 能够轻松扩展,处理从小型到大型的各种环境,适用于企业和初创公司。
-
版本控制:基础设施配置存储在纯文本文件中,简化了变更管理、历史跟踪,并通过 Git 或类似的版本控制系统(VCS)进行协作。
-
自动化常规任务:Ansible 自动化软件安装、配置更新和补丁管理等重复性工作,为战略任务腾出时间。
-
安全性与合规性:使用 Ansible 的 基于角色的访问控制(RBAC)和集成的安全模块,可以在整个基础设施中一致地实施安全策略和合规标准。
-
回滚与恢复:Ansible 允许在出现问题时轻松回滚到之前的配置,减少停机时间并最小化变更的影响。
-
模块化与可重用性:Ansible 鼓励创建模块化、可重用的 playbook 和角色,推动一种有序高效的自动化方法。
-
支持性社区:受益于强大的 Ansible 社区,提供支持、文档以及贡献的角色和模块库。
-
成本效益:Ansible 是开源并且免费使用,相比其他自动化工具,大大降低了许可费用。
-
编排和工作流自动化:除了配置管理(CM)之外,Ansible 还可以编排复杂的工作流,包括应用部署和基础设施配置。
-
不可变基础设施:Ansible 支持不可变基础设施的概念,即更改时是通过重建组件而不是在原地修改它们。这使得部署更加可预测和可靠。
-
实时反馈:Ansible 提供实时反馈和报告,简化了自动化任务的监控和故障排除。
这些优势使得 Ansible 成为广泛 IT 环境和行业中配置管理、自动化和编排的热门选择。
Ansible 具有简单的架构。它有一个控制节点,负责管理多个受管节点。你所需要的只是一个控制节点服务器来安装 Ansible,以及用于通过控制节点管理的节点(也称为受管节点)。这些受管节点应该允许来自 Ansible 控制节点的 SSH 连接——如下图所示:
图 9.1 – Ansible 架构
现在,让我们继续看看如何使用 Ansible 安装和配置所需的配置。接下来的部分将介绍如何安装 Ansible。
配置 Ansible
我们需要在控制节点上安装并设置 Ansible,但在此之前,我们将需要启动三台服务器来开始活动——一台 Ansible 控制节点和两台受管节点。
配置库存
目的是建立一个包含Apache和MySQL的两层架构。所以,让我们使用 Terraform 启动三台服务器。
首先,让我们cd进入 Terraform 模板所在的目录,然后编辑 terraform.tfvars 文件,填写所需的详细信息。(有关如何获取属性的更多详细信息,请参考第八章,基础设施即代码 (IaC) 与 Terraform)
$ cd ~/modern-devops/ch9/setup-ansible-terraform
$ vim terraform.tfvars
然后,使用以下命令通过 Terraform 启动服务器:
$ terraform init
$ terraform plan -out ansible.tfplan
$ terraform apply ansible.tfplan
一旦 terraform apply 命令成功完成,我们将看到三台服务器——ansible-control-node、web 和 db,以及在 ansible-exercise 资源组内创建的相关资源。
terraform apply的输出还提供了 Ansible 控制节点和web虚拟机的公网 IP 地址。你应该在输出中看到我们获得的公网 IP 地址。
注意
Azure 可能需要一些时间来报告输出,如果在 terraform apply 时没有获得 IP 地址,你可以之后运行 terraform output 来获取详细信息。
Ansible 需要控制节点通过 SSH 与受管节点连接。现在,让我们继续并看看如何与我们的受管节点(也称为库存服务器)进行通信。
将 Ansible 控制节点与库存服务器连接
当我们使用 Terraform 提供基础设施时,我们已经在控制节点和受控节点之间设置了无密码 SSH。让我们看看我们是如何做到这一点的,以便更好地理解它。
我们在该子网内创建了control-node、web和db。如果我们查看 VM 资源配置,还可以看到custom_data字段,可用于将初始化脚本传递给 VM,如下所示:
resource "azurerm_virtual_machine" "control_node" {
name = "ansible-control-node"
...
os_profile {
...
custom_data = base64encode(data.template_file.control_node_init.rendered)
}
}
resource "azurerm_virtual_machine" "web" {
name = "web"
...
os_profile {
...
custom_data = base64encode(data.template_file.managed_nodes_init.rendered)
}
}
resource "azurerm_virtual_machine" "db" {
name = "db"
...
os_profile {
...
custom_data = base64encode(data.template_file.managed_nodes_init.rendered)
}
}
如我们所见,control_node虚拟机引用了data.template_file.control_node_init资源,而web和db节点引用了data.template_file.managed_nodes_init资源。这些是可以用于模板文件的template_file资源。让我们看一下资源如下:
data "template_file" "managed_nodes_init" {
template = file("managed-nodes-user-data.sh")
vars = {
admin_password = var.admin_password
}
}
data "template_file" "control_node_init" {
template = file("control-node-user-data.sh")
vars = {
admin_password = var.admin_password
}
}
正如我们所看到的,managed_nodes_init资源指向managed-nodes-user-data.sh文件,并向该文件传递一个admin_password变量。同样,control_node_init资源指向control-node-user-data.sh文件。让我们先看看managed-nodes-user-data.sh文件:
#!/bin/sh
sudo useradd -m ansible
echo 'ansible ALL=(ALL) NOPASSWD:ALL' | sudo tee -a /etc/sudoers
sudo su - ansible << EOF
ssh-keygen -t rsa -N '' -f ~/.ssh/id_rsa
printf "${admin_password}\n${admin_password}" | sudo passwd ansible
EOF
如我们所见,这是一个执行以下操作的 shell 脚本:
-
创建一个
ansible用户。 -
将用户添加到
sudoers列表。 -
为无密码认证生成
ssh密钥对。 -
设置
ansible用户的密码。
由于我们已经生成了ssh密钥对,因此我们需要在控制节点上进行相同的操作,并进行一些额外的配置。让我们看一下control-node-user-data.sh脚本,如下所示:
#!/bin/sh
sudo useradd -m ansible
echo 'ansible ALL=(ALL) NOPASSWD:ALL' | sudo tee -a /etc/sudoers
sudo su - ansible << EOF
ssh-keygen -t rsa -N '' -f ~/.ssh/id_rsa
sleep 120
ssh-keyscan -H web >> ~/.ssh/known_hosts
ssh-keyscan -H db >> ~/.ssh/known_hosts
sudo apt update -y && sudo apt install -y sshpass
echo "${admin_password}" | sshpass ssh-copy-id ansible@web
echo "${admin_password}" | sshpass ssh-copy-id ansible@db
EOF
该脚本执行以下操作:
-
创建一个
ansible用户 -
将用户添加到
sudoers列表 -
为无密码认证生成
ssh密钥对 -
将
web和db虚拟机添加到known_hosts文件中,以确保我们信任这两个主机 -
安装
sshpass实用程序以允许发送ssh公钥到web和db虚拟机 -
将
ssh公钥复制到web和db虚拟机以实现无密码连接
当 VM 创建时,这些文件会自动执行;因此,无密码 SSH 应已经起作用。因此,让我们使用上一步中获取的 IP 地址使用ansible-control-node。我们将使用在terraform.tfvars文件中配置的用户名和密码:
$ ssh ssh_admin@104.46.61.213
一旦你在控制节点服务器上,请切换用户到ansible,并尝试使用以下命令连接到web服务器:
$ sudo su - ansible
$ ssh web
如果你登陆到web服务器,无密码认证是正常工作的。
重复相同的步骤检查您是否可以与db服务器连接。
退出提示,直到您在控制节点。
现在,因为我们在控制节点上,让我们安装 Ansible。
在控制节点安装 Ansible
Ansible 需要一个 Linux/Unix 机器(最好是),并且应安装 Python 2.x或3.x。
由于 Ansible 控制节点运行在 Ubuntu 上,Ansible 提供了apt命令。
使用以下命令在服务器上安装 Ansible:
$ sudo apt update
$ sudo apt install software-properties-common -y
$ sudo apt-add-repository --yes --update ppa:ansible/ansible
$ sudo apt install ansible -y
要检查 Ansible 是否已成功安装,请运行以下命令:
$ ansible --version
ansible 2.9.27
正如我们所见,ansible 2.9.27已成功安装在您的控制节点上。
Ansible 使用清单文件来管理节点。因此,下一步是设置清单文件。
设置清单文件
在 Ansible 中的清单文件是一个文件,允许您根据角色对受管节点进行分组。例如,您可以定义诸如webserver和dbserver之类的角色,并将相关服务器分组。您可以使用 IP 地址、主机名或别名进行定义。
提示
始终使用别名,因为它们为 IP 地址和主机名更改提供了空间。
您可以使用分配给它们的角色在主机或一组主机上运行 Ansible 命令。可以有一个特定角色的服务器没有限制。如果您的服务器使用非标准 SSH 端口,您还可以在清单文件中使用该端口。
Ansible 清单文件的默认位置是/etc/ansible/hosts。如果查看/etc/ansible目录的所有权,则其所有者是root用户。出于安全目的,我们希望使用我们为此创建的ansible用户。因此,我们必须更改/etc/ansible目录及其子目录和文件的所有权为ansible。请使用以下命令执行此操作:
$ sudo chown -R ansible:ansible /etc/ansible
然后,我们可以将用户切换到ansible,并使用以下命令将包含所需文件的 Git 存储库克隆到控制服务器:
$ sudo su - ansible
$ git clone https://github.com/PacktPublishing/Modern-DevOps-Practices-2e.git \
modern-devops
$ cd ~/modern-devops/ch9/ansible-exercise
在我们的场景中,我们有一个名为web的 Web 服务器和一个名为db的数据库服务器。因此,如果您检查存储库中名为hosts的主机文件,您将看到以下内容:
[webservers]
web ansible_host=web
[dbservers]
db ansible_host=db
[all:vars]
ansible_python_interpreter=/usr/bin/python3
[all:vars]部分包含适用于所有组的变量。在这里,我们明确地将ansible_python_interpreter定义为python3,以便 Ansible 使用python3而不是python2。由于我们使用的是 Ubuntu,默认安装了python3,而python2已弃用。
我们还看到,我们没有直接使用web,而是指定了一个ansible_host部分。它将web定义为一个别名,指向一个名为web的主机。如果需要,您也可以使用 IP 地址而不是主机名。
提示
始终根据执行的功能对清单进行分组。这有助于我们对大量具有相似角色的机器应用相似的配置。
由于我们希望将配置与代码保持一致,因此我们希望保留在 Git 存储库本身内。因此,我们必须告诉 Ansible 清单文件位于非标准位置。为此,我们将创建一个 Ansible 配置文件。
设置 Ansible 配置文件
Ansible 配置文件定义了特定于我们设置的全局属性。以下是指定 Ansible 配置文件的方法,第一种方法会覆盖下一个——设置不会合并,所以请记住这一点:
-
通过设置环境变量
ANSIBLE_CONFIG,指向 Ansible 配置文件 -
通过在当前目录中创建
ansible.cfg文件 -
通过在当前用户的主目录中创建
ansible.cfg文件 -
通过在
/etc/ansible目录中创建ansible.cfg文件
提示
如果你管理多个应用,每个应用都有自己的 Git 仓库,那么在每个仓库中都有一个本地的 ansible.cfg 文件将有助于保持应用的去中心化。它还将启用 GitOps 并使 Git 成为单一的可信源。
所以,如果你检查当前目录中的 ansible.cfg 文件,你会看到以下内容:
[defaults]
inventory = ./hosts
host_key_checking = False
现在,为了检查我们的清单文件是否正确,让我们使用以下命令列出我们的清单:
$ ansible-inventory --list -y
all:
children:
dbservers:
hosts:
db:
ansible_host: db
ansible_python_interpreter: /usr/bin/python3
ungrouped: {}
webservers:
hosts:
web:
ansible_host: web
ansible_python_interpreter: /usr/bin/python3
我们看到有两个组——dbservers 包含 db,webservers 包含 web,每个组都使用 python3 作为 ansible_python_interpreter。
如果我们想查看所有主机,我们可以使用以下命令:
$ ansible --list-hosts all
hosts (2):
web
db
如果我们想列出所有具有 webservers 角色的主机,我们可以使用以下命令:
$ ansible --list-hosts webservers
hosts (1):
web
现在,让我们使用以下命令来检查 Ansible 是否能够连接到这些服务器:
$ ansible all -m ping
web | SUCCESS => {
"changed": false,
"ping": "pong"
}
db | SUCCESS => {
"changed": false,
"ping": "pong"
}
如我们所见,对于两个服务器,我们都得到了成功的响应。因此,我们已经完全设置好,可以开始定义配置。Ansible 提供了任务和模块来提供配置管理。让我们在下一节中详细了解这些内容。
Ansible 任务和模块
Ansible 任务是执行 Ansible 命令的基本构建块。Ansible 任务的结构如下所示:
$ ansible <options> <inventory>
Ansible 模块是用于特定功能的可重用代码,例如运行 shell 命令或创建和管理用户。你可以通过 Ansible 任务与 Ansible 模块一起使用来管理受管节点中的配置。例如,以下命令将在每个受管服务器上运行 uname 命令:
$ ansible -m shell -a "uname" all
db | CHANGED | rc=0 >>
Linux
web | CHANGED | rc=0 >>
Linux
因此,我们从 db 服务器和 web 服务器得到了回复,每个服务器提供了返回码 0 和输出 Linux。如果你查看命令,你会发现我们提供了以下标志:
-
-m:模块的名称(这里是shell模块) -
-a:模块的参数(在此例中为uname)
命令最后会指定我们想在哪个地方运行这个任务。由于我们指定了 all,因此任务将在所有服务器上运行。我们也可以将任务运行在单个服务器、一组服务器、角色或多个角色上,或者使用通配符选择我们想要的组合。
任务有三种可能的状态——SUCCESS、CHANGED 和 FAILURE。SUCCESS 状态表示任务成功完成,Ansible 没有进行任何操作。CHANGED 状态表示 Ansible 为了应用预期的配置,必须更改现有的配置,而 FAILURE 表示在执行任务时发生了错误。
Ansible 模块是可重用的脚本,我们可以用它们来定义服务器内的配置。每个模块针对配置管理(CM)的特定方面。模块在 Ansible 任务和剧本中都可以使用。有许多模块可供使用,它们可以在docs.ansible.com/ansible/latest/collections/index_module.html找到。你可以根据自己的需求和使用场景挑选并使用模块。
提示
由于 Ansible 是幂等的,始终使用特定于任务的模块,避免使用command和shell模块。例如,使用apt模块安装软件包,而不是使用command模块运行apt install <package> -y。如果你的剧本看起来像代码,那么你可能做错了根本性的事情。
当我们在设置服务器时有一系列步骤要跟随时,任务就没有意义。因此,Ansible 为这个活动提供了剧本。我们将在下一部分了解这一点。
Ansible 剧本介绍
想象一下你是指挥家,正在带领一个交响乐团。在这种情境下,Ansible 剧本就像是你的乐谱,指导每个音乐家在技术世界中创造一场和谐的自动化交响乐。
在技术和自动化领域,Ansible 剧本提供了以下功能:
-
自动化的乐谱:就像指挥家使用带有记谱符号的乐谱来指导每个乐器一样,Ansible 剧本包含了一系列指令和操作,用于编排特定的 IT 任务和配置,从软件部署到系统配置等。
-
和谐的引导:Ansible 剧本采取了类似的方式。你声明所需的 IT 状态,而 Ansible 扮演指挥的角色,确保所有必要的步骤都得到执行,就像指定“我希望演奏一场完美的音乐会”,而 Ansible 就是协调整个过程。
-
任务与重用性:Ansible 剧本被组织成任务和角色,就像乐谱和乐器一样。这些任务可以在多个剧本中重用,促进一致性并节省时间。
-
乐器选择与指挥:就像指挥家选择哪些乐器在何时演奏一样,剧本指定了哪些服务器或机器(清单)应执行任务。你可以指挥特定的服务器组或单独的机器。
-
和谐的执行:Ansible 能够巧妙地协调多台机器上的任务,就像指挥家协调不同音乐家之间的配合,创造出一首美丽的乐章。
-
精细调控的表现:如果在演奏过程中出现意外挑战,指挥家会调整并引导音乐家以确保演出完美无缺。同样,Ansible 剧本也包含了错误处理策略,以应对自动化过程中的意外问题。
Ansible 剧本是由任务集合组成的,这些任务会在被管理的节点中生成所需的配置。它们具有以下特点:
-
它们通过使用声明性步骤帮助管理多个远程服务器上的配置
-
它们使用一系列顺序的幂等步骤,符合预期配置的步骤不会再次应用。
-
剧本中的任务可以是同步的也可以是异步的
-
它们通过允许将步骤存储在简单的 YAML 文件中并保存在源代码控制中,从而实现 GitOps,提供 CaC。
Ansible 剧本由多个 play 组成,每个 play 映射到一组 hosts,使用 role,并包含实现目标所需的一系列 tasks,类似于以下图示:
图 9.2 – 剧本
以下的 ping.yaml 文件是一个简单的剧本示例,它会对所有服务器进行 ping 测试:
---
- hosts: all
tasks:
- name: Ping all servers
action: ping
YAML 文件包含了一个任务列表,如列表指令所示。每个任务包含一个 hosts 属性,定义了我们希望应用任务的角色。tasks 部分包含一个任务列表,每个任务都有 name 和 action 属性。在之前的示例中,我们有一个单一的任务,它会对所有服务器进行 ping 测试。
检查剧本语法
在应用剧本之前检查语法是一种最佳实践。要检查剧本的语法,请运行以下命令:
$ ansible-playbook ping.yaml --syntax-check
playbook: ping.yaml
语法是正确的,因为我们得到了包含剧本名称的响应。现在,让我们继续应用这个剧本。
应用第一个剧本
要应用剧本,请运行以下命令:
$ ansible-playbook ping.yaml
PLAY [all] *******************************************
TASK [Gathering Facts] *******************************
ok: [db]
ok: [web]
TASK [Ping all servers] ******************************
ok: [db]
ok: [web]
PLAY RECAP *******************************************
db : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
web : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
执行剧本有三个要素:
-
收集信息:Ansible 检查所有属于该角色的主机,登录到每个实例,并收集每个主机在执行 play 中的任务时使用的信息。
-
运行任务:然后,它运行每个剧本中定义的任务。
-
任务回顾:Ansible 会提供已执行任务的回顾,以及它在哪些主机上执行这些任务。这包括所有成功和失败的响应列表。
在我们研究了一个基础的剧本示例之后,我们必须了解如何有效地使用 Ansible 剧本。在接下来的部分中,让我们通过一个更好的示例来查看 Ansible 剧本的实际应用。
Ansible 剧本实际应用
让我们使用 Ansible 设置一个连接 MySQL 后端的 Apache 服务器,简而言之,这是一个 Linux、Apache、MySQL 和 PHP(LAMP)栈。
以下目录包含本节练习所需的所有资源:
$ cd ~/modern-devops/ch9/lamp-stack
我们创建了以下自定义的 index.php 页面,用于测试与 MySQL 数据库的连接,并显示是否能够连接:
...
<?php
mysqli_connect('db', 'testuser', 'Password@1') or die('Could not connect the database :
Username or password incorrect');
echo 'Database Connected successfully';
?>
...
我们根据 CM 所遵循的逻辑步骤创建了多个 Ansible 剧本。
在每次配置开始时更新软件包和仓库是一种很好的实践。因此,我们需要从这一步开始编写剧本。
更新软件包和仓库
由于我们使用的是 Ubuntu,我们可以使用apt模块来更新软件包。我们必须更新软件包和仓库,以确保所有的apt仓库都有最新的软件包索引,并避免在安装软件包时出现任何问题。以下剧本apt-update.yaml执行了更新:
---
- hosts: webservers:dbservers
become: true
tasks:
- name: Update apt packages
apt: update_cache=yes cache_valid_time=3600
YAML 文件以播放列表开始,在这种情况下包含一个播放。hosts属性定义了一个冒号分隔的roles/hosts清单,用于应用此剧本。在这种情况下,我们指定了webservers和dbservers。become属性指定是否要作为root用户执行播放。因此,由于我们将become设置为true,Ansible 将在所有播放任务中使用sudo权限。该播放包含一个任务——更新 apt 软件包。该任务使用apt模块,包含update_cache=yes。它将在所有具有webservers和dbservers角色的节点上运行apt update操作。下一步是安装软件包和服务。
安装应用程序软件包和服务
我们将使用apt模块在 Ubuntu 上安装软件包,并使用service模块启动并启用服务。
我们从使用以下install-webserver.yaml剧本在 Web 服务器上安装 Apache 开始:
---
- hosts: webservers
become: true
tasks:
- name: Install packages
apt:
name:
- apache2
- php
- libapache2-mod-php
- php-mysql
update_cache: yes
cache_valid_time: 3600
state: present
- name: Start and Enable Apache service
service: name=apache2 state=started enabled=yes
由于这个配置是为webservers准备的,我们在hosts属性中指定了这一点。tasks部分定义了两个任务——安装软件包和启动并启用 Apache 服务。安装软件包任务使用apt模块安装apache2、php、libapache2-mod-php和php-mysql。启动并启用 Apache 服务任务将启动并启用apache2服务。
类似地,我们将使用以下install-dbserver.yaml剧本安装并设置 MySQL 服务:
---
- hosts: dbservers
become: true
tasks:
- name: Install packages
apt:
name:
- python-pymysql
- mysql-server
update_cache: yes
cache_valid_time: 3600
state: present
- name: Start and enable MySQL service
service:
name: mysql
state: started
enabled: true
这个剧本将运行两个任务——安装软件包和启动并启用 MySQL 服务。安装软件包任务将使用apt模块安装python-mysql和mysql-server软件包。启动并启用 MySQL 服务任务将启动并启用 MySQL 服务。
配置应用程序
链中的下一步是配置应用程序。为此有两个剧本。第一个将配置webservers上的 Apache,第二个将配置dbservers上的 MySQL。
以下setup-webservers.yaml剧本将配置webservers:
---
- hosts: webservers
become: true
tasks:
- name: Delete index.html file
file:
path: /var/www/html/index.html
state: absent
- name: Upload application file
copy:
src: index.php
dest: /var/www/html
mode: 0755
notify:
- Restart Apache
handlers:
- name: Restart Apache
service: name=apache2 state=restarted
这个剧本会在所有具有webservers角色的节点上运行,剧本中有三个任务。删除 index.html 文件任务使用file模块从 Web 服务器中删除/var/www/html/index.html文件。因为我们使用的是index.php作为主页,而不是index.html。上传应用文件任务然后使用copy模块将index.php文件从 Ansible 控制节点复制到 Web 服务器上的/var/www/html目标路径,模式为0755。上传应用文件任务还包含一个notify动作,如果该任务的状态为CHANGED,它会调用重启 Apache处理程序。剧本中的handlers部分定义了监听通知事件的处理程序。在此场景中,如果上传应用文件任务发生变化,重启 Apache处理程序会被触发,并重启apache2服务。
我们将使用以下setup-dbservers.yaml剧本来配置dbservers上的 MySQL:
---
- hosts: dbservers
become: true
vars:
mysql_root_password: "Password@1"
tasks:
- name: Set the root password
copy:
src: client.my.cnf
dest: "/root/.my.cnf"
mode: 0600
notify:
- Restart MySQL
- name: Create a test user
mysql_user:
name: testuser
password: "Password@1"
login_user: root
login_password: "{{ mysql_root_password }}"
state: present
priv: '*.*:ALL,GRANT'
host: '%'
- name: Remove all anonymous user accounts
mysql_user:
name: ''
host_all: yes
state: absent
login_user: root
login_password: "{{ mysql_root_password }}"
notify:
- Restart MySQL
- name: Remove the MySQL test database
mysql_db:
name: test
state: absent
login_user: root
login_password: "{{ mysql_root_password }}"
notify:
- Restart MySQL
- name: Change bind address
lineinfile:
path: /etc/mysql/mysql.conf.d/mysqld.cnf
regexp: ^bind-address
line: 'bind-address = 0.0.0.0'
notify:
- Restart MySQL
handlers:
- name: Restart MySQL
service: name=mysql state=restarted
这个剧本稍微复杂一些,但我们可以将其分解成几部分,便于理解。
这个剧本中有一个vars部分,定义了mysql_root_password变量。在执行 MySQL 任务时我们需要这个密码。第一个任务是设置 root 密码。设置 root 密码的最佳方式是通过在 MySQL 中定义一个包含 root 凭证的/root/.my.cnf文件。我们将以下client.my.cnf文件通过copy模块复制到/root/.my.cnf:
[client]
user=root
password=Password@1
然后,创建测试用户任务使用mysql_user模块创建一个名为testuser的用户。该任务需要提供login_user和login_password属性的值,我们分别提供root和{{ mysql_root_password }}。接着,它会删除所有匿名用户,并删除测试数据库。然后,它使用lineinfile模块将绑定地址更改为0.0.0.0。lineinfile模块是一个强大的模块,通过先使用正则表达式在文件中查找内容,然后用行属性的值替换这些行来操作文件。所有这些任务会通知重启 MySQL处理程序,该处理程序会重启 MySQL 数据库服务。
结合剧本
由于我们已经编写了多个剧本,我们需要按顺序执行它们。在安装软件包和服务之前无法配置服务,而且在安装软件包之后再运行apt更新也没有意义。因此,我们可以创建一个包含多个剧本的剧本。
为此,我们创建了一个 YAML 文件playbook.yaml,其内容如下:
---
- import_playbook: apt-update.yaml
- import_playbook: install-webserver.yaml
- import_playbook: install-dbserver.yaml
- import_playbook: setup-webservers.yaml
- import_playbook: setup-dbservers.yaml
这个 YAML 文件包含了一系列的剧本,每个剧本都包含一个import_playbook语句。剧本按照文件中指定的顺序执行。现在,让我们继续执行这个剧本。
执行剧本
执行剧本很简单。我们将使用 ansible-playbook 命令后跟剧本的 YAML 文件。由于我们将剧本合并到 playbook.yaml 文件中,以下命令将执行剧本:
$ ansible-playbook playbook.yaml
PLAY [webservers:dbservers] **************************
TASK [Gathering Facts] *******************************
ok: [web]
ok: [db]
TASK [Update apt packages] ***************************
ok: [web]
ok: [db]
PLAY [webservers] ************************************
TASK [Gathering Facts] *******************************
ok: [web]
TASK [Install packages] ******************************
changed: [web]
TASK [Start and Enable Apache service] ***************
ok: [web]
PLAY [dbservers] *************************************
TASK [Gathering Facts] *******************************
ok: [db]
TASK [Install packages] ******************************
changed: [db]
TASK [Start and enable MySQL service] ****************
ok: [db]
PLAY [webservers] ************************************
TASK [Gathering Facts] *******************************
ok: [web]
TASK [Delete index.html file] ************************
changed: [web]
TASK [Upload application file] ***********************
changed: [web]
RUNNING HANDLER [Restart Apache] *********************
changed: [web]
PLAY [dbservers] *************************************
TASK [Gathering Facts] *******************************
ok: [db]
TASK [Set the root password] *************************
changed: [db]
TASK [Update the cnf file] ***************************
changed: [db]
TASK [Create a test user] ****************************
changed: [db]
TASK [Remove all anonymous user accounts] ************
ok: [db]
TASK [Remove the MySQL test database] ***************
ok: [db]
TASK [Change bind address] **************************
changed: [db]
RUNNING HANDLER [Restart MySQL] **********************
changed: [db]
PLAY RECAP *******************************************
db: ok=13 changed=6 unreachable=0 failed=0
skipped=0 rescued=0 ignored=0
web: ok=9 changed=4 unreachable=0 failed=0
skipped=0 rescued=0 ignored=0
如我们所见,配置同时应用于 webservers 和 dbservers,所以让我们向 web 服务器发送一个 curl 命令来查看我们得到的结果:
$ curl web
<html>
<head>
<title>PHP to MQSQL</title>
</head>
<body>Database Connected successfully</body>
</html>
如我们所见,数据库已成功连接!这证明设置已成功。
我们采用的问题解决方法并不是最好的,有几个原因。首先,剧本中有几个部分我们硬编码了值。虽然我们在一些剧本中使用了变量,但也在剧本内为变量赋值。这样做使得剧本不能成为重用的候选项。设计软件的最佳方式是始终考虑到可重用性。因此,我们有多种方法可以重新设计剧本,以促进可重用性。
设计以支持可重用性
Ansible 提供了用于将 Ansible 剧本转化为可重用模板的变量。你可以使用Jinja2标记在适当的位置替换变量,我们在上一个剧本中已经使用过它。现在,让我们来看一下 Ansible 变量、它们的类型以及如何使用它们。
Ansible 变量
Ansible 变量与其他任何变量一样,用于管理托管节点之间的差异。你可以使用相似的剧本来处理多个服务器,但有时配置上会有一些差异。Ansible 变量帮助你为剧本创建模板,使得它们可以在多个相似的系统中重用。你可以在多个地方定义变量:
-
在 Ansible 剧本的
vars部分内 -
在你的清单中
-
在可重用的文件或角色中
-
通过命令行传递变量
-
通过将任务的返回值赋给变量来注册变量
Ansible 变量名可以包含字母、数字和下划线。由于 Ansible 在后台使用 Python,变量名不能是 Python 的关键字。此外,变量名不能以数字开头,但可以以下划线开头。
你可以使用简单的键值对在 YAML 文件中定义变量,并遵循标准的 YAML 语法。
变量大致可以分为三种类型——简单变量、列表变量和字典变量。
简单变量
{{ var_name }}。你应该始终引用 Jinja 表达式,因为如果没有这些引号,YAML 文件将无法解析。
以下是一个简单变量声明的示例:
mysql_root_password: bar
这是你应该如何引用它的方式:
- name: Remove the MySQL test database
mysql_db:
name: test
state: absent
login_user: root
login_password: "{{ mysql_root_password }}"
现在,让我们来看一下列表变量。
列表变量
列表变量保存一个值的列表,你可以通过索引引用它们。你还可以在循环中使用列表变量。要定义一个列表变量,你可以使用标准的 YAML 列表语法,如以下示例所示:
region:
- europe-west1
- europe-west2
- europe-west3
要访问变量,我们可以使用索引格式,如以下示例所示:
region: " {{ region[0] }} "
Ansible 还支持更复杂的字典变量。让我们来看一下。
字典变量
字典变量 保存复杂的 键值对 组合,类似于 Python 中的字典。你可以使用标准的 YAML 语法定义字典变量,如下所示:
foo:
bar: one
baz: two
有两种方式来引用这些变量的值。例如,在点符号中,我们可以这样写:
bar: {{ foo.bar }}
在括号符号中,我们可以使用以下表达式表示相同的内容:
bar: {{ foo[bar] }}
我们可以像在 Python 中一样使用点符号或括号符号。
提示
尽管点符号和括号符号表示相同的内容,但括号符号更好。使用点符号时,某些键可能与 Python 字典的方法和属性冲突。
现在,让我们看看如何获取变量值。
获取变量值
虽然你可以手动定义变量并提供其值,但有时我们需要动态生成的值;例如,如果我们需要知道 Ansible 执行 playbook 的服务器主机名,或想要在变量中使用任务返回的特定值。Ansible 在收集事实阶段提供了一组变量和系统元数据,以满足前述需求。这有助于确定哪些变量可用以及如何使用它们。让我们了解如何收集这些信息。
使用 Ansible facts 查找元数据
Ansible facts 是与受管节点相关的元数据。Ansible 在 收集事实 阶段获取事实,我们可以直接在 playbook 中使用 facts 变量。我们可以使用 setup 模块作为 Ansible 任务来确定事实。例如,你可以运行以下命令以获取所有具有 webservers 角色的节点的 Ansible facts:
$ ansible -m setup webservers
web | SUCCESS => {
"ansible_facts": {
"ansible_all_ipv4_addresses": [
"10.0.2.5"
],
...
"ansible_hostname": "web",
如我们所见,我们得到了带有多个变量的 ansible_facts,这些变量与库存项相关。由于这里只有一台服务器,我们得到了 web 服务器的详细信息。在这一部分中,我们有一个名为 web 的 ansible_hostname 属性。如果需要,我们可以在 playbook 中使用该 ansible_hostname 属性。
有时,我们想要将某个任务的输出源到特定变量,以便在 playbook 的后续任务中使用该变量。让我们看看如何做到这一点。
注册变量
如果你的 playbook 中的某个任务需要前面任务的结果值,我们可以使用 register 属性。
以下目录包含本节练习的所有资源:
$ cd ~/modern-devops/ch9/vars-exercise
让我们看看以下示例 register.yaml 文件:
- hosts: webservers
tasks:
- name: Get free space
command: free -m
register: free_space
ignore_errors: true
- name: Print the free space from the previous task
debug:
msg: "{{ free_space }}"
该 playbook 包含两个任务。第一个任务使用 command 模块执行命令 free -m,并将结果注册到 free_space 变量中。随后的任务使用前一个任务的输出,利用 debug 模块将 free_space 作为消息打印到控制台。
让我们运行 playbook 来亲自看看:
$ ansible-playbook register.yaml
PLAY [webservers] ************************************
TASK [Gathering Facts] *******************************
ok: [web]
TASK [Get free space] ********************************
changed: [web]
TASK [Print the free space from the previous task] ***
ok: [web] => {
"msg": {
"stdout": " total used
free shared buff/cache available\nMem: 3.3G
170M 2.6G 2.2M 642M 3.0G\nSwap:
0B 0B 0B",
}
PLAY RECAP ****************************************
web: ok=3 changed=1 unreachable=0 failed=0 skipped=0
rescued=0 ignored=0
现在我们已经理解了变量,让我们看看其他方面,这些方面将帮助我们改进最后的 playbook。
Jinja2 模板
Ansible 允许使用动态 Jinja2 模板对文件进行模板化。你可以在文件中使用 Python 语法,开始符号是 {{,结束符号是 }}。这将允许你在运行时替换变量,并对变量进行复杂的计算。
为了进一步理解这一点,让我们修改 index.php 文件,在执行期间动态提供 MySQL 的用户名和密码:
...
<?php
mysqli_connect('db', '{{ mysql_user }}', '{{ mysql_password }}')
or die('Could not connect the database : Username or password
incorrect');
echo 'Database Connected successfully';
?>
...
正如我们所看到的,除了硬编码用户名和密码外,我们还可以使用模板在运行时替代变量值。这将使文件更加可重用,并且适应多个环境。Ansible 提供了一个关于代码重用的重要功能——Ansible 角色。让我们在下一节中进一步了解这一点。
Ansible 角色
好吧,最后的 playbook 看起来有些杂乱。它包含了很多文件,而且没有一个是可重用的。我们编写的代码只能以特定的方式设置配置。这对于小型团队和有限的配置管理可能有效,但对于大多数企业来说,情况并不像看起来那么简单。
Ansible 角色有助于标准化 Ansible 配置并促进重用。通过角色,你可以使用标准的目录结构自动加载 var 文件、handlers、tasks 和其他 Ansible 工件,这些目录相对于你的 playbook 进行组织。目录结构如下:
<playbook>.yaml
roles/
<role>/
tasks/
handlers/
library/
files/
templates/
vars/
defaults/
meta/
roles 目录包含多个子目录,每个子目录代表一个角色。每个角色目录包含多个标准子目录:
-
tasks: 该目录包含任务的 YAML 文件列表。它应该包含一个名为main.yaml(或main.yml或main)的文件,其中列出了所有任务或从目录中的其他文件导入任务。 -
handlers: 该目录包含与角色相关的处理程序列表,保存在一个名为main.yaml的文件中。 -
library: 该目录包含可以与角色一起使用的 Python 模块。 -
files: 该目录包含配置所需的所有文件。 -
templates: 该目录包含角色部署的 Jinja2 模板。 -
vars: 该目录包含一个main.yaml文件,文件中列出了与角色相关的变量。 -
defaults: 该目录包含一个main.yaml文件,文件中列出了与角色相关的默认变量,这些变量可以通过任何包含清单变量的其他变量轻松覆盖。 -
meta: 该目录包含与角色相关的元数据和依赖项,保存在main.yaml文件中。
一些最佳实践涉及通过文件夹结构来管理 Ansible 配置。接下来,我们来看看其中的一些实践。
提示
在选择 vars 和 defaults 目录时,基本的规则是将不会改变的变量放入 vars 目录。将可能改变的变量放入 defaults 目录。
因此,我们将尽可能使用defaults目录。关于角色,也有一些最佳实践我们应该遵循。让我们来看一下其中的一些。
提示
在设计角色时,考虑特定服务的完整生命周期,而不是构建整个堆栈——换句话说,不要使用lamp作为角色,而是使用apache和mysql角色。
我们将为我们的使用创建三个角色——common、apache和mysql。
提示
使用特定的角色,例如apache或mysql,而不是使用webserver或dbserver。典型的企业会有多种 Web 服务器和数据库技术的组合,因此,为角色使用通用名称会导致混淆。
以下目录包含本节练习的所有资源:
$ cd ~/modern-devops/ch9/lamp-stack-roles
以下是我们将遵循的目录结构:
├── ansible.cfg
├── hosts
├── output.log
├── playbook.yaml
我们将创建三个角色——apache、mysql和common。首先,让我们看看apache角色的目录结构:
└── roles
├── apache
│ ├── defaults
│ │ └── main.yaml
│ ├── handlers
│ │ └── main.yaml
│ ├── tasks
│ │ ├── install-apache.yaml
│ │ ├── main.yaml
│ │ └── setup-apache.yaml
│ └── templates
│ └── index.php.j2
还有一个适用于所有场景的common角色。以下目录结构定义了这一点:
├── common
│ └── tasks
│ └── main.yaml
最后,让我们通过以下目录结构定义mysql角色:
└── mysql
├── defaults
│ └── main.yaml
├── files
├── handlers
│ └── main.yaml
├── tasks
│ ├── install-mysql.yaml
│ ├── main.yaml
│ └── setup-mysql.yaml
└── templates
└── client.my.cnf.j2
apache目录包含以下内容:
-
我们使用了在上一个练习中创建的相同的
index.php文件,将其转换为一个名为index.php.j2的 Jinja2 模板,并将其复制到roles/apache/templates目录。 -
handlers目录包含一个main.yaml文件,该文件包含RestartApache处理程序。 -
tasks目录包含一个install-apache.yaml文件,其中包括安装 Apache 所需的所有任务。setup-apache.yaml文件包含设置 Apache 的任务列表,类似于我们在上一个练习中所做的。main.yaml文件包含两个文件中的任务,使用诸如以下的include指令:
---
- include: install-apache.yaml
- include: setup-apache.yaml
defaults目录包含main.yaml文件,该文件包含mysql_username和mysql_password变量及其默认值。
提示
尽可能少使用变量,并尝试为其设置默认值。以最小化自定义配置的方式为变量设置默认值。
mysql目录包含以下内容:
- 我们修改了
client.my.cnf并将其转换为一个j2文件。j2文件是一个 Jinja2 模板文件,我们将在角色中通过template模块在Set the root password任务中使用。该文件位于templates目录内:
[client]
user=root
password={{ mysql_root_password }}
如我们所见,我们通过 Jinja2 表达式提供密码。当我们通过 playbook 运行mysql角色时,mysql_root_password的值将会替换password部分。
-
handlers目录包含RestartMySQL处理程序。 -
tasks目录包含三个文件。install-mysql.yaml文件包含安装mysql的任务,setup-mysql.yaml文件包含设置mysql的任务。main.yaml文件使用include任务指令将这两个文件合并,如下所示:
---
- include: install-mysql.yaml
- include: setup-mysql.yaml
defaults目录包含一个main.yaml文件,其中列出了我们将在角色中使用的变量。在这种情况下,它只包含mysql_root_password的值。
common 目录包含一个名为 tasks 的子目录,其中有一个 main.yaml 文件,文件内有一个任务来执行 apt update 操作。
主目录包含 ansible.cfg、hosts 和 playbook.yaml 文件。虽然 hosts 和 ansible.cfg 文件与上一个练习相同,但 playbook.yaml 文件如下所示:
---
- hosts: webservers
become: true
roles:
- common
- apache
- hosts: dbservers
become: true
roles:
- common
- mysql
现在,playbook 已经变得简洁,包含了许多可重用的元素。它由两个 play 组成。第一个 play 会以 root 用户身份在所有 Web 服务器上运行,并应用 common 和 apache 角色。第二个 play 会以 root 用户身份在所有具有 dbservers 角色的节点上运行,并使用 common 和 mysql 角色。
提示
始终保持角色松散耦合。在前面的示例中,apache 角色与 mysql 没有依赖关系,反之亦然。这将使我们能够轻松地重用配置。
现在,让我们继续执行 playbook:
$ ansible-playbook playbook.yaml
PLAY [webservers]
...
PLAY [dbservers]
...
PLAY RECAP
db: ok=10 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
web: ok=7 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
正如我们所见,配置没有变化。我们以更好的方式应用了相同的配置。如果我们想与团队中的其他人共享我们的配置,我们可以共享 roles 目录,他们可以在自己的 playbook 中应用该角色。
在某些情况下,我们可能需要为 roles 部分中定义的变量使用不同的值。你可以通过使用 extra-vars 标志,在 playbook 中覆盖变量值,如下所示:
$ ansible-playbook playbook.yaml --extra-vars "mysql_user=foo mysql_password=bar@123"
当我们使用上述命令应用 playbook 时,我们会看到用户现在变成了 foo,并且在 Apache 和 MySQL 配置中密码更改为 bar@123:
...
PLAY RECAP
db: ok=9 changed=1 unreachable=0 failed= skipped=0 rescued=0 ignored=0
web: ok=7 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
因此,如果我们运行 curl 命令到 Web 主机,我们会得到与之前相同的响应:
...
<body>Database Connected successfully</body>
...
我们的设置已经正确地与角色一起工作。我们通过遵循所有最佳实践并使用可重用的角色和模板设置了 Ansible playbook。这就是设计强大 Ansible playbook 的正确方法。
总结
在本章中,我们从实际操作的角度讨论了 Ansible 及其核心功能。我们首先理解了 CaC,了解了 Ansible 和 Ansible 架构,安装了 Ansible,理解了 Ansible 模块、任务和 playbooks,并应用了第一个 Ansible 配置。然后,我们通过 Ansible 变量、Jinja2 模板和角色促进了可重用性,并根据可重用性重新组织了我们的配置。我们还沿途探讨了几项最佳实践。
在下一章中,我们将结合 Terraform 和 Ansible 来启动一些有用的内容,并了解 HashiCorp 的 Packer 来创建不可变基础设施。
问题
-
最佳实践是尽量避免使用
command和shell模块。(对/错) -
别名有助于保持清单的通用性。(对/错)
-
ansible-playbook命令的作用是什么?A. 它在清单上运行临时任务。
B. 它在清单上运行一系列任务。
C. 它应用与 Playbook 配置的任务和剧本。
D. 它会销毁从托管节点获取的配置。
-
以下哪种技术有助于在 Ansible 配置中建立可重用性?(选择三个)
A. 使用变量。
B. 使用 Jinja2 模板。
C. 使用角色。
D. 使用任务。
-
在命名角色时,我们应该考虑什么?(选择两个)
A. 尽可能精确地命名角色。
B. 在思考角色时,考虑服务而不是整个堆栈。
C. 为角色使用通用名称。
-
如果变量的值可能会更改,应该在角色中的哪个目录中定义变量?
A.
defaultsB.
vars -
当与处理程序关联的任务的输出是什么时,处理程序会触发?
A.
SUCCESSB.
CHANGEDC.
FAILED -
SUCCESS状态表示任务未检测到任何更改的配置吗?(是/否) -
库存管理的最佳实践是什么?(选择三个)
A. 对每个环境使用单独的清单。
B. 按功能分组清单。
C. 使用别名。
D. 将清单文件保存在中央位置。
答案
-
True
-
True
-
C
-
A, B, 和 C
-
A, B
-
A
-
B
-
True
-
A, B, 和 C
Terraform与Ansible在DevOps中的实践指南
932

被折叠的 条评论
为什么被折叠?



