Ansible-学习指南第二版-全-

Ansible 学习指南第二版(全)

原文:annas-archive.org/md5/8d4b06f2ca98c3efdf577fe8a213a97b

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Ansible 是一个开源的编排工具,已经经历了显著的增长,现在成为一个综合的编排和配置管理解决方案,由红帽公司拥有。本书将指导你使用核心模块、供应商提供的模块以及社区 Ansible 模块来编写 playbook,以部署各种系统,从简单的 LAMP 堆栈到高可用的公共云基础设施。

到本书结束时,你将掌握以下技能:

  • 对 Ansible 及其各种支持工具的坚实基础知识

  • 能够编写自定义 playbook 来配置 Linux 和 Windows 服务器

  • 能够使用代码定义高可用的云基础设施,从而能够轻松地将你的基础设施配置与代码库一起分发

  • 理解如何使用 Ansible Galaxy,使用社区贡献的角色,并创建和贡献你自己的角色

  • 能够通过 GitHub Actions 和 Azure DevOps 运行你的 Ansible playbook

  • 能够部署和配置 Ansible AWX,这是一个基于 Web 的 Ansible 界面

  • 从多个使用案例中获得的各种技能,展示如何将 Ansible 集成到你的日常任务和项目中

你将扎实地理解如何将 Ansible 融入到你作为系统管理员、开发者或 DevOps 实践者的日常职责中。

本书适用人群

本书是为以下角色的人编写的,他们希望通过利用 Ansible 的功能来简化工作流程:

  • 系统管理员:本书将帮助你自动化重复性任务,并确保在你的系统中实现一致的配置,如果你负责管理和维护服务器、网络和其他基础设施组件。

  • 开发者:作为开发者,你可以通过本书学习如何使用 Ansible 来配置和管理开发环境、部署应用程序,并将基础设施即代码实践融入到你的开发工作流中。

  • DevOps 实践者:如果你是一个负责弥合开发和运维之间差距的 DevOps 实践者,本书将为你提供使用 Ansible 创建高效、可重复和可扩展部署流程的工具和知识。

本书开始时无需具备 Ansible 的先前经验。

本书内容涵盖的主题

第一章安装和运行 Ansible,讨论了 Ansible 开发的背景和所解决的问题。在介绍背景后,我们将介绍如何在 macOS 和 Linux 上安装 Ansible。我们还将讨论为什么没有原生的 Windows 安装程序,并介绍如何在 Windows 子系统 for Linux 上安装 Ansible。

第二章探索 Ansible Galaxy,讨论了 Ansible Galaxy,这是一个社区和供应商贡献的角色的在线库。在本章中,我们将发现一些最好的角色,如何使用它们,以及如何创建自己的角色并将其托管在 Ansible Galaxy 上。

第三章Ansible 命令,解释了如何在编写和执行更高级的 playbook 之前,检查 Ansible 命令。在这里,我们将介绍构成 Ansible 的工具的使用。

第四章部署 LAMP 堆栈,讨论了使用 Ansible 自带的各种核心模块部署完整的 LAMP 堆栈。我们将以本地运行的 Ubuntu 机器为目标。

第五章部署 WordPress,扩展了我们在上一章中部署的 LAMP 堆栈 playbook,作为我们的基础。我们将使用 Ansible 下载、安装并配置 WordPress —— 一个流行的内容管理系统(CMS)。

第六章目标多发行版,解释了如何调整上一章的 playbook,以便它可以同时在 Debian(我们到目前为止的目标)和基于 Red Hat 的 Linux 发行版上运行。

第七章Ansible Windows 模块,探索了支持并与基于 Windows 的服务器交互的 Ansible 模块的不断增长的集合。

第八章Ansible 网络模块,讨论了通过 Ansible Galaxy 提供的来自不同供应商的网络模块。由于它们的要求,我们将仅讨论这些模块的功能。

第九章迁移到云端,讨论了如何从使用本地虚拟机迁移到使用 Ansible 在 Microsoft Azure 中部署网络和计算资源。然后,我们将使用前几章的 playbook 来安装和配置 LAMP 堆栈和 WordPress。

第十章构建云网络,由于我们刚刚在 Microsoft Azure 上启动了虚拟机,本章转向亚马逊 Web 服务(AWS);然而,在启动任何计算实例之前,我们必须创建一个可以托管这些实例的网络。

第十一章高度可用的云部署,继续我们的亚马逊 Web 服务(AWS)部署。我们将开始在我们在上一章创建的网络中部署计算和存储服务,到本章结束时,我们将拥有一个高度可用的 WordPress 安装。

第十二章构建 VMware 部署,讨论了允许与典型 VMware 安装的各个组件交互的模块。

第十三章扫描你的 Ansible 剧本,提供了运行两个第三方工具——Checkov 和 KICS——的实际示例。这些工具旨在扫描你的 Ansible 剧本代码,查找常见错误和潜在的安全问题。

第十四章使用 Ansible 加固你的服务器,解释了如何安装和执行 OpenSCAP。我们还将自动生成修复 Ansible 剧本和 Bash 脚本,以解决扫描中发现的任何问题。我们还将探讨如何对使用前面章节中的剧本部署的资源运行 WPScan 和 OWASP ZAP 扫描。

第十五章在 GitHub Actions 和 Azure DevOps 中使用 Ansible,将探讨如何在这两个 CI/CD 平台上运行我们的 Ansible 剧本。由于这两个平台都没有原生的 Ansible 支持,我们将讨论如何安装和运行 Ansible,以便最大程度地利用这些平台。

第十六章介绍 Ansible AWX 和 Red Hat Ansible 自动化平台,探讨了两个基于 Web 的界面:我们将讨论商业版的 Red Hat Ansible 自动化平台,之后将深入探讨如何部署和配置开源的 Ansible AWX。

第十七章使用 Ansible 的下一步,讨论了如何将 Ansible 集成到我们的日常工作流中,从与协作服务的交互到使用内置调试器排查剧本问题。我们还将查看一些我在多个组织中使用 Ansible 的真实案例。

为了最大程度地从本书中获益

为了最大程度地从本书中获益,我假设你具备以下条件:

  • 在 Linux 系统和 macOS 上使用命令行的经验

  • 具备在 Linux 服务器上安装和配置服务的基本理解

  • 具备 Git、YAML 和虚拟化等服务与语言的工作知识

    本书中涉及的软件/硬件 操作系统要求
    Ansible 通过 Linux 子系统在 macOS、Linux 或 Windows 上运行
    Canonical Multipass macOS、Linux 或 Windows
    各种公共云提供商的 CLI 工具 通过 Linux 子系统在 macOS、Linux 或 Windows 上运行

如果你使用的是本书的数字版本,我们建议你亲自输入代码,或通过本书 GitHub 仓库访问代码(下节中会提供链接)。这样做可以帮助你避免因复制粘贴代码而引发的潜在错误。

下载示例代码文件

你可以从 GitHub 下载本书的示例代码文件,链接:github.com/PacktPublishing/Learn-Ansible-Second-Edition。如果代码有更新,将会在 GitHub 仓库中更新。

我们还有其他代码包,来自我们丰富的书籍和视频目录,您可以在github.com/PacktPublishing/查看。快来看看吧!

使用的约定

本书中使用了许多文本约定。

文本中的代码:表示文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账号。以下是一个示例:“如您所见,它调用了一个名为{{ apache_packages }}的变量,该变量在roles/apache/defaults/main.yml中定义,如下所示。”

一段代码会设置如下:

- name: "Install apache packages"
  ansible.builtin.apt:
    state: "present"
    pkg: "{{ apache_packages }}"

当我们希望引起您对某个代码块中特定部分的注意时,相关的行或项会以粗体显示:

- name: "Install apache packages"
  ansible.builtin.apt:
    state: "present"
    pkg: "{{ apache_packages }}"

任何命令行输入或输出的格式如下所示:

$ ansible-playbook -i hosts site.yml

粗体:表示新术语、重要词汇或您在屏幕上看到的单词。例如,菜单或对话框中的词汇会以粗体显示。以下是一个示例:“您可以保持其他选项为默认设置,然后点击表单底部的创建仓库按钮。”

提示或重要说明

显示如下。

联系我们

我们始终欢迎读者的反馈。

一般反馈:如果您对本书的任何内容有疑问,请通过电子邮件联系我们:customercare@packtpub.com,并在邮件主题中注明书名。

勘误表:虽然我们已经尽力确保内容的准确性,但错误难免发生。如果您发现本书中有错误,我们将非常感谢您向我们报告。请访问www.packtpub.com/support/errata并填写表格。

盗版:如果您在互联网上发现任何我们作品的非法复制形式,烦请提供该材料的所在位置或网站名称。请通过版权@packt.com 与我们联系,并附上该材料的链接。

如果您有兴趣成为作者:如果您在某个主题上有专长,并且有兴趣撰写或参与编写书籍,请访问 authors.packtpub.com。

分享您的想法

一旦您读完了《学习 Ansible》,我们很希望听到您的想法!请点击这里直接进入本书的亚马逊评价页面并分享您的反馈

您的评价对我们和技术社区非常重要,并且帮助我们确保提供优质内容。

下载本书的免费 PDF 副本

感谢您购买本书!

您喜欢随时随地阅读,但无法随身携带纸质书籍吗?

您购买的电子书与您选择的设备不兼容吗?

不用担心,现在购买每本 Packt 书籍时,您将免费获得该书的无 DRM PDF 版本。

在任何地方,任何设备上阅读。搜索、复制并粘贴你最喜爱的技术书籍中的代码,直接将其应用到你的项目中。

好处不止于此,你还可以获得独家折扣、新闻简报,并且每天都有精彩的免费内容发送到你的邮箱。

按照以下简单步骤,获取更多好处:

  1. 扫描二维码或访问下面的链接

packt.link/free-ebook/9781835088913

  1. 提交你的购买凭证

  2. 就这些!我们将直接把你的免费 PDF 和其他福利发送到你的邮箱。

第一部分:介绍、安装和运行 Ansible

在这一部分,我们将深入探讨 Ansible 的世界,探索其基本概念。你将学习如何在不同的操作系统上安装 Ansible,并熟悉 Ansible 命令和 Playbook 的基本结构。到这一部分结束时,你将具备坚实的基础,能够在后续深入学习 Ansible 自动化任务时有更好的基础。

本部分包含以下章节:

  • 第一章**, 安装与运行 Ansible

  • 第二章**, 探索 Ansible Galaxy

  • 第三章**, Ansible 命令

第一章:安装和运行 Ansible

欢迎来到我们的第一章,《Learn Ansible》第二版的第一章。在本章中,我们将介绍一些主题,帮助你了解Ansible,这些内容将让你熟悉 Ansible 的基本概念,并展示一些不同的使用场景。

到本章结束时,你将亲自操作 Ansible,并涵盖以下内容:

  • 谁是 Ansible 背后的团队?

  • Ansible 与其他工具的区别

  • Ansible 解决的问题

  • 如何在 macOS 和 Linux 上安装 Ansible

  • 在 Windows 11 上使用 Windows 子系统运行 Ansible

  • 启动测试虚拟机

  • Playbook 介绍

在我们开始讨论 Ansible 之前,让我们简要介绍一下我的背景,讲讲我如何开始写 Ansible 相关书籍,以及你需要在系统上安装和运行 Ansible 的前提条件。

技术要求

本章后续内容中,我们将安装 Ansible,因此你需要一台能够运行它的机器。关于这些要求,我将在本章下半部分详细说明。我们还将使用Multipass在本地启动虚拟机。接下来的一节将指导你安装 Multipass,并下载一个 Ubuntu 镜像作为虚拟机的基础,这个镜像的下载量大约是几百 MB。你可以在 github.com/PacktPublishing/Learn-Ansible-Second-Edition/tree/main/Chapter01 找到本章中使用的所有代码。

我的故事:第一部分

自上世纪 90 年代末以来,我一直在与服务器打交道,主要是为网页提供服务的服务器,那个时代的技术和现在完全不同。这里简要回顾了我运营服务器的最初几年,以便你了解我当时是如何操作我的早期服务器的。

和大多数人一样,我从一个共享主机账户开始,当时网站因论坛而逐渐超出了共享主机的承载能力,这也是该站点受欢迎的部分原因。我搬到了专用服务器,以为可以展示未来的系统管理员能力,但我错了。

我获得的服务器是一台 Cobalt RaQ 3;这是一款 1U 服务器设备,超越了当时的技术水平。然而,我没有对机器的 root 级别访问权限,所有需要做的操作都必须通过基于 Web 的控制面板来完成。最终,我获得了访问权限,可以通过 Telnet 访问服务器;现在我知道这并不好,但那是早期阶段,SSH 被认为是前沿技术。我开始通过在 Web 控制面板中进行更改并查看服务器上配置文件的变化,自学如何成为一名系统管理员。

一段时间后,我更换了服务器,这次我决定放弃任何基于网页的控制面板,转而使用我在 Cobalt RaQ 上学到的知识,通过我整理的笔记来配置我的第一台真正的LinuxApacheMySQLPHP(简称LAMP)服务器。我创建了运行手册,包含了安装和配置所需软件的单行命令,还有大量草稿帮助我排查问题并保持服务器运行。

当我为另一个项目得到了第二台服务器时,我意识到那时候正是整理笔记的好时机,这样我就可以在需要部署服务器时直接复制粘贴这些内容;这时机恰到好处,因为就在整理这些笔记几个月后,我的第一台服务器出现故障——我的主机提供商道歉并更换了一台更新操作系统的高配置全新机器。

所以,我拿起了包含笔记的 Microsoft Word 文件,复制并粘贴每条指令,根据需要安装在升级操作系统上的内容进行了调整。几个小时后,我的服务器启动并运行,数据也成功恢复。

我学到的一个重要教训是,除了“没有什么比备份过多更为重要”这一点之外,就是不要用 Microsoft Word 来存储这类笔记;Linux 命令行并不在乎你的笔记是否有漂亮的格式、标题和用于打印的 Courier 字体。它在乎的是正确的语法,而 Word 很“贴心”地为我所有的笔记做了自动更正和格式化,意味着我不仅要在部署新服务器和恢复每天备份的同时,还得调试我已经做好的笔记。

因此,我将服务器上的历史文件复制了一份,并将我的笔记转录为纯文本。这些笔记为接下来的几年提供了基础,我开始将其中的一些部分脚本化,主要是那些不需要用户输入的部分。

这些命令片段、单行指令和脚本最初是通过 Red Hat Linux 6 进行调整的;注意操作系统名称后面没有附加“Enterprise”这个词,一直到 CentOS 3 和 4 版本。

当我换了职位之后,事情变得复杂了;我不再从一个网络托管公司消费服务,而是开始为其中一家工作。突然之间,我开始为客户构建服务器,而这些客户的需求可能与我自己的项目不同——每台服务器都是不同的。

从这里开始,我开始使用 Kickstart 脚本、PXE 启动服务器、影像服务器上的金盘映像、虚拟机,以及开始要求提供有关系统构建信息的 bash 脚本。我也从只需要担心维护自己的服务器,变成了必须登录到成百上千台不同的物理和虚拟服务器,从属于我工作的公司,到客户的机器。

在接下来的几年里,我的单一文本文件迅速变成了一个复杂的笔记、脚本、预编译二进制文件和信息电子表格的集合,这些内容只有我能理解;如果说实话,我最终自己成了一个显著的单点故障。

尽管我已开始通过 bash 脚本和将命令串联在一起来自动化我日常工作的许多部分,但我发现我的日子仍然充满了手动执行所有这些任务,并且需要在服务台处理客户报告的问题和查询。

我的故事是许多人典型的经历,虽然使用的操作系统可能被认为是古老的。使用图形界面(GUI)并转向命令行,同时保留常用命令的便签,这是我在与其他系统管理员甚至现代 DevOps 从业者合作时,常常听到的一个典型场景。

现在你已经了解了我的背景,接下来我们来谈谈 Ansible。

Ansible 的故事

让我们快速了解一下是谁开发了 Ansible,以及它究竟是什么。

Ansible 是什么?

在讨论 Ansible 的起源之前,我们应该简要讨论一下它名字的由来。“Ansible”这一术语是科幻小说作家乌苏拉·K·勒古恩所创造;它首次出现在她 1966 年出版的小说《洛卡农的世界》中。在故事中,Ansible 是一种虚构的设备,能够比光速更快地发送和接收消息。

注意

1974 年,乌苏拉·K·勒古恩的小说《被剥夺的: 一个模糊的乌托邦》出版。这本书通过探索使得这种设备成为可能的(虚构的)数学理论,展示了 Ansible 技术的发展。

这个术语此后被许多其他著名作家在同一类型的作品中使用,描述能够跨星际距离传递信息的通信设备,正如你将在本书的过程中发现的那样,这也是对 Ansible 软件本身的恰当描述。

Ansible,软件

Ansible 最初由 Michael DeHaan 开发,他也是Cobbler的作者,Cobbler 是在 DeHaan 为 Red Hat 工作期间开发的。

注意

Cobbler 是一个 Linux 安装服务器,可以帮助你在网络中快速部署服务器;它可以处理 DNS、DHCP、包更新和分发、虚拟机部署、物理主机的电源管理,以及将新部署的服务器,无论是物理的还是虚拟的,交给配置管理系统。

DeHaan 离开了 Red Hat,转而为像Puppet这样的公司工作,这非常合适,因为许多 Cobbler 的用户会在服务器配置完毕后,使用 Puppet 服务器来管理这些服务器,我也是其中之一。

离开 Puppet 几年后,DeHaan 于 2012 年 2 月 23 日首次向 Ansible 项目提交了公共代码。最初的README文件给出了一个相当简单的描述,为 Ansible 最终会成为什么打下了基础:

Ansible 是一个极简的 Python API,用于通过 SSH 执行“远程操作”。就像我共同编写的 Func 想要避免使用 SSH 并拥有自己的守护进程基础设施一样,Ansible 则志在与之完全不同,更加简洁,但仍能够随着时间推移更加模块化地发展。

自从首次提交以来,截止本文写作时,已有超过 53,000 次提交,来自 5,000 位贡献者,且该项目在 GitHub 上的星标超过 58,000。

到 2013 年,项目已经发展壮大。Ansible, Inc. 成立,旨在为依赖该项目管理基础设施和服务器配置(无论是物理的、虚拟的,还是托管在公共云上的用户)提供商业支持。

在 Ansible, Inc. 成立后,该公司获得了 600 万美元的 A 轮融资,推出了商业版 Ansible Tower,作为一个基于 Web 的前端,最终用户可以通过它访问基于角色的 Ansible 服务。

然后,在 2015 年 10 月,Red Hat 宣布以 1.5 亿美元收购 Ansible。

在公告中,当时 Red Hat 的管理副总裁 Joe Fitzgerald 说道,“Ansible 是 IT 自动化和 DevOps 的明显领导者,帮助 Red Hat 在实现创建 无摩擦 IT 目标上迈出了重要一步。”

在本书中,你会发现原始 README 文件中的声明以及 Red Hat 在收购 Ansible 时的声明依然有效。

在我们开始动手安装 Ansible(稍后我们会在本章进行安装)之前,我们应该先了解它的一些核心概念。

Ansible 与其他工具

如果你对比第一次提交时的设计原则和当前版本的设计,你会发现,尽管有一些增加和调整,核心原则基本上保持不变:

  • 无代理:所有操作都应该通过 SSH 守护进程来管理,Windows 机器则使用 WinRM 协议或 API 调用——不应该依赖于自定义代理或需要在目标主机上打开或交互的其他端口。运行 Ansible 的机器应该能够从网络上直接访问目标资源。

  • 最小化:你应该能够在不安装任何新软件的情况下管理新的远程机器;每个 Linux 目标主机通常至少会安装 SSH 和 Python,这些是运行 Ansible 所需的最小安装环境。

  • 描述性:你应该能够用机器和人类都能读懂的语言描述你的基础设施、栈或任务。

  • 简单:设置过程和学习曲线应该简单直观。

  • 易于使用:它应该是最易接入的 IT 自动化系统。

其中一些原则使得 Ansible 与其他工具有很大的不同。让我们来看看 Ansible 和其他工具(如 Puppet 和 Chef)之间的根本区别。

声明式与命令式

当我开始使用 Ansible 时,我已经实现了 Puppet 来帮助管理我所管理的机器上的堆栈。随着配置变得越来越复杂,Puppet 代码变得非常复杂。这时,我开始寻找替代方案,一些方案解决了我面临的一些问题。

Puppet 使用一种自定义声明式语言来描述配置。然后,Puppet 将此配置打包为一个清单,代理程序会在每台服务器上应用此清单。

使用声明式语言意味着 Puppet、Chef 和其他配置工具,如CFEngine,都采用最终一致性的原则,这意味着最终,在代理程序运行几次之后,你期望的配置会到位。

另一方面,Ansible 是一种命令式语言,它不仅定义了你期望的最终状态并让工具决定如何到达该状态,还定义了执行任务的顺序,以达到你定义的状态。

我使用的示例如下。我们有一个配置,其中需要将以下状态应用到服务器:

  1. 创建一个名为Team的组。

  2. 创建用户Alice并将其添加到Team组。

  3. 创建一个用户Bob,并将其添加到Team组。

  4. 给用户Alice提升权限。

这看起来可能很简单;然而,当你使用声明式语言执行这些任务时,你可能会发现发生了以下情况:

图 1.1 – 声明式运行时发生情况的概览

图 1.1 – 声明式运行时发生情况的概览

那么,发生了什么呢?我们的工具在Alice无法创建时执行了任务,因为第一个任务运行时Team组不存在。

然而,由于Team组在创建用户Bob之前已经存在,Bob的用户成功创建,没有任何错误,最后一个任务,即为用户Alice添加提升权限失败,因为系统上不存在名为Alice的用户,无法将提升的权限应用到该用户。

Team存在的情况下,用户Alice被创建,并且由于Alice存在,该用户获得了提升的权限。

运行 3期间没有需要更改的内容,因为一切如预期;也就是说,状态是一致的。

每次随后的运行都会继续,直到配置或主机本身发生变化。例如,如果Bob惹恼了Alice,并且她使用了提升的权限将用户Bob从主机上删除。那么,当代理程序随后运行时,Bob将会被重新创建,因为这是我们期望的配置,不管Alice认为Bob应该拥有怎样的权限。

如果我们使用命令式语言运行相同的任务,那么应该发生以下情况:

图 1.2 – 命令式运行时发生情况的概览

图 1.2 – 命令式运行时发生情况的概览

任务会按照我们定义的顺序执行,这意味着首先会创建Team组,然后添加AliceBob用户,最后将提升的权限应用于Alice用户。

如你所见,两种方法都能达到最终配置,并强制执行我们期望的状态。使用声明性语言的工具可以声明依赖关系,这意味着我们可以解决在运行任务时遇到的问题。

然而,这个例子只有四个步骤;如果你有几百个步骤,其中涉及在公共云平台上启动服务器,然后安装需要多个前提条件的软件会发生什么呢?

这是我在开始使用 Ansible 之前所面临的情况。Puppet 在强制执行我期望的最终配置方面非常出色;然而,要实现这一点,我不得不在我的清单中加入大量的逻辑,以达到预期的状态。在 Puppet 中,这些逻辑是通过一个函数实现的,允许我作为最终用户定义我的依赖关系。

在我们使用的例子中,我必须定义用户只能在创建组的代码块运行并且资源存在后才可以创建。

我的代码变得越复杂,我与声明性工具希望运行的方式斗争得越多,每次执行所需的时间也就越长,因为该工具必须考虑我的逻辑,而我的逻辑有时并不完全正确。

这变得越来越让人烦恼,因为每次成功运行的时间接近 40 分钟。如果我遇到依赖问题,我不得不从头开始,每次失败和更改时都要确保自己解决了问题,而不是因为事情开始变得一致,这通常意味着必须重新部署资源,而不是运行后续的代码。这使得开发变得非常耗时,尤其是在调试代码时,有时还需要进行反复试验。

当你在计时并且必须满足客户的最后期限时,陷入这种境地并不理想。

配置与编排

Ansible 与其他常被拿来比较的工具之间的另一个关键区别是,大多数这些工具起初是为了部署和管理配置状态而设计的系统。

它们通常需要在每个主机上安装一个代理;该代理会发现它所安装的主机上的一些信息,然后回调到中央服务器说:“你好,我是服务器 XYZ。请给我我的配置好吗?”然后,服务器决定服务器的配置是什么,并将其发送给代理,代理再应用它。通常,这种交换每 15 到 30 分钟进行一次——如果你需要在服务器上强制执行某个配置,这非常有用。

然而,Ansible 的设计方式使其能够作为一个编排工具运行;例如,你可以使用它在 VMware 环境中启动一个服务器,服务器启动后,它可以连接到新启动的机器并安装 LAMP 堆栈。然后,它不再需要连接该主机,这意味着我们剩下的只是服务器、LAMP 堆栈,除此之外,可能只有几个文件中的注释,说明 Ansible 添加了一些配置行,这应该是唯一的迹象,表明 Ansible 被用来配置该主机。

查看一些代码

在我们完成本章这一部分并进入安装 Ansible 之前,让我们快速看一下实际代码的例子。以下 bash 脚本使用 yum 包管理器安装多个 RPM:

#!/bin/sh
LIST_OF_APPS="dstat lsof mailx rsync tree vim-enhanced git whois"
yum install -y $LIST_OF_APPS

以下是一个 Puppet 类,它完成与之前的 bash 脚本相同的任务:

class common::apps {
  package {
    [
      'dstat',
      'lsof',
      'mailx',
      'rsync',
      'tree',
      'vim-enhanced',
      'git',
      'whois',
    ]:
    ensure => installed,
  }
}

接下来,我们使用 SaltStack 完成相同的任务:

common.packages:
  pkg.installed:
    - pkgs:
      - dstat
      - lsof
      - mailx
      - rsync
      - tree
      - vim-enhanced
      - git
      - whois

最后,我们再次完成相同的任务,这次使用的是 Ansible:

- name: "Install packages we need"
  ansible.builtin.yum:
    name:
      - "dstat"
      - "lsof"
      - "mailx"
      - "rsync"
      - "tree"
      - "vim-enhanced"
      - "git"
      - "whois"
      - "iptables-services"
    state: "present"

即使不深入细节,你也应该能够大致理解这三个例子各自的作用。虽然不完全是基础设施管理,但这三者都是基础设施即代码的有效示例。

这是你以与开发人员管理其应用程序源代码完全相同的方式来管理管理你基础设施的代码。你使用源代码管理,存储在一个中央可用的仓库中,在那里你可以与同事合作,分支并使用拉取请求来检查你的更改,并且在可能的情况下,编写并执行单元测试,以确保对基础设施的更改在部署到生产之前是成功且没有错误的。这应该尽可能自动化。任何在上述任务中的人工干预都可能是故障点,你应该努力将任务自动化。

这种基础设施管理方法有一些优势,其中之一是,作为系统管理员,你正在使用与开发同事相同的流程和工具,这意味着适用于他们的任何流程也适用于你。这使得工作体验更加一致,并且让你接触到你可能还没有接触过或使用过的工具。

其次,更重要的是,它使你能够共享你的工作。在这种方法之前,这类工作对于其他人来说似乎是一门只有系统管理员才能掌握的黑暗艺术。将这项工作公开,允许你的同事审查并评论你的配置,同时你也可以对他们的配置进行相同的操作。此外,你还可以分享你的工作,供他人将其元素整合到他们的项目中。

我的故事:第二部分

在我们完成本章这一部分之前,我想讲完我的旅程故事。正如本章前面提到的,我从一堆脚本和运行手册转向了 Puppet,这非常好,直到我的需求开始超出仅仅管理服务器配置和维护服务器状态的范围。

我需要开始管理公共云中的基础设施。当时,使用 Puppet 时,这个需求让我感到越来越沮丧。因为 Puppet 对我需要使用的基础设施 API 的支持不够完善。我确信现在好多了,但同时我发现自己不得不在清单文件中编写过多的逻辑,来控制每个任务执行的顺序。

大约是在 2014 年 12 月,我决定开始了解 Ansible。我记得这个时间点,因为我写了一篇名为 与 Ansible 的第一步 的博客文章;自那以后,我想我再也没有回头看过。我后来将 Ansible 介绍给了几位同事和客户,并写了相关书籍,包括你现在正在阅读的本书的第一版。

到目前为止,在本章中,我们已经回顾了我个人与 Ansible 以及一些与 Ansible 相比的其他工具的历史,讨论了这些工具之间的区别,以及 Ansible 的起源。

现在我们将通过查看如何安装 Ansible 并运行我们的第一个 Ansible playbook 来开始你的 Ansible 之旅,目标是运行在本地虚拟机上。

安装和运行 Ansible

让我们直接开始安装 Ansible。在本书中,我假设你正在使用 macOS 主机或运行 Ubuntu LTS 版本的 Linux 主机。虽然我们会讲解如何在 Windows 11 上使用 Windows 子系统运行 Ansible,但本书不支持将 Windows 作为主机来使用。

在 macOS 上安装

你可以通过几种不同的方式在 macOS 主机上安装 Ansible。我将在这里讲解两种方法。由于我们讨论的是两种不同的安装方式,我建议在选择适合你本地机器的安装方法之前,先阅读本节以及优缺点小节。

使用 Homebrew 安装

第一种安装方法是使用一个叫做 Homebrew 的包管理器。

注意

Homebrew 是 macOS 的一个包管理器,可以用来安装命令行工具和桌面应用程序。它自称是 “macOS 缺失的包管理器”,通常是在我做干净安装或新电脑时安装的第一个工具。

要通过 Homebrew 安装 Ansible,你首先需要安装 Homebrew。为此,运行以下命令:

$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

在安装过程的每一步,安装程序都会告诉你它将做什么,并提示你提供完成安装所需的任何额外信息。

安装完成后,或者如果你已经安装了 Homebrew,运行以下命令来更新你的软件包列表;如果有更新,你也可以进行升级:

$ brew update
$ brew upgrade

最后,根据你安装的版本新旧,或者你上次使用它的时间,你可能需要运行以下命令来检查你的 Homebrew 安装是否最优化:

$ brew doctor

现在我们已经安装并更新了 Homebrew,并准备好使用brew,我们可以运行以下命令来检查 Homebrew 中有哪些 Ansible 包:

$ brew search ansible

如你从以下截图中的结果看到的,搜索结果中返回了几个包:

图 1.3 – 使用 brew 命令搜索 Ansible

图 1.3 – 使用 brew 命令搜索 Ansible

我们需要的是 Ansible 包;你可以通过运行以下命令来获取有关该包的更多信息:

$ brew info ansible

你可以在以下截图中看到命令的结果:

图 1.4 – 查看我们将要安装的 Ansible 包的信息

图 1.4 – 查看我们将要安装的 Ansible 包的信息

如你所见,命令返回了将要安装的包的版本信息,并列出了所有依赖项的完整列表;在之前的屏幕中,所有依赖项旁边都有绿色勾选,因为我已经安装了它们——你的显示可能会有所不同。

它还给出了 Homebrew 公式的 URL,用于安装该包。在我们的案例中,你可以在github.com/Homebrew/homebrew-core/blob/master/Formula/ansible.rb查看公式详情。

要使用 Homebrew 安装 Ansible,我们必须运行以下命令:

$ brew install ansible

这将下载并安装所有依赖项,然后是 Ansible 包本身。

根据你的机器上安装的依赖项数量,这可能需要几分钟时间。

安装完成后,你应该能看到类似以下截图的内容:

图 1.5 – 使用 Homebrew 安装 Ansible

图 1.5 – 使用 Homebrew 安装 Ansible

如你从前面的截图中看到的,Homebrew 的输出非常详细,既提供了它正在执行的操作反馈,又给出了如何使用其安装的包的详细信息。

我们将要查看的第二种安装方法是一种更传统的方法。

使用 pip 安装

第二种方法,pip,是一种更传统的安装和配置 Python 包的方法。

注意

pip是 Python 软件的包管理器。它是pip install packages的递归缩写。它是从Python Package IndexPyPI)安装包的一个很好的前端工具。

大多数现代 macOS 安装默认会安装 pip;根据你机器上安装的内容,你可能需要检查安装了哪个 pip 二进制文件。

为此,请运行以下命令:

$ pip --version
$ pip3 --version

其中一个或两个应该返回版本号,并提供 pip 二进制文件的路径。

根据你安装的 pip 版本,你可能需要修改以下 pip 命令,这是我们需要运行的命令来安装 Ansible:

$ pip install ansible

该命令将下载并安装运行 Ansible 所需的所有先决条件。虽然它和 Homebrew 一样详细,但它的输出包含的是它已完成的操作信息,而不是下一步的提示:

图 1.6 – 使用 Pip 安装 Ansible

图 1.6 – 使用 Pip 安装 Ansible

从少量的输出中可以看到,许多依赖项已经满足。

优缺点

那么,现在我们已经覆盖了在 macOS 上安装 Ansible 的一些不同方法,哪种方法最好呢?实际上没有真正的答案,因为这取决于个人偏好。这两种方法都会安装 Ansible 的最新版本。然而,Homebrew 往往比当前版本滞后一个或两个星期。

如果你已经使用 Homebrew 安装了很多软件包,那么你应该已经习惯了运行以下命令:

$ brew update
$ brew upgrade

只需偶尔运行这些命令来更新已安装的软件包到最新版本。如果你已经在做这件事,那么使用 Homebrew 来管理你的 Ansible 安装是有意义的。

如果你不是 Homebrew 用户,并且希望确保立即安装最新版本的 Ansible,可以使用 pip 命令来安装 Ansible。升级到最新版本的 Ansible,只需运行以下命令:

$ pip install ansible --upgrade

如果需要,你可以使用 Homebrew 和 pip 安装 Ansible 的旧版本。

要使用 Homebrew 执行此操作,你需要通过运行以下命令删除当前版本:

$ brew uninstall ansible

然后,你可以通过运行以下命令安装旧版本的软件包:

$ brew install ansible@2.0

虽然这会安装旧版本,但你对安装哪个版本没有太多选择。如果你确实需要一个精确的版本,可以使用 pip 命令来安装它。例如,要安装 Ansible 2.3.1.0,你需要运行:

$ pip install ansible==2.3.1.0

需要注意的是,你通常不需要这样做,而且我也不推荐这么做。

然而,我发现,在少数情况下,我不得不降级以帮助调试通过升级到较高版本的 Ansible 导致的我几年前最后一次修改的 playbook 中出现的怪异问题。

如前所述,我大部分时间都在 macOS 机器前度过,无论是在工作中还是在家里,那么我使用的是哪种方法呢?

主要是因为我使用 Homebrew,因为我还安装了其他几个工具。然而,如果我需要回滚到以前的版本,我会使用 pip,然后在问题解决后再返回使用 Homebrew。

在 Linux 上安装

在 Ubuntu 上安装 Ansible 有几种不同的方法。但我这里只会介绍其中一种。虽然有适用于 Ubuntu 的包可以通过 apt 安装,但它们通常会很快过时,并且通常会滞后于当前发布版本。

如果你希望使用 apt 包管理器进行安装,可以运行以下命令:

$ apt install ansible

注意

.``deb 文件。

因此,我们将使用 pip。首先要做的是安装 pip,可以通过运行以下命令来完成:

$ sudo -H apt-get update
$ sudo -H apt-get install python3-pip

第一个 apt-get 命令会下载所有更新文件,确保你的 Ubuntu 安装中的软件包列表是最新的,第二个命令安装 python3-pip 包及其依赖。

一旦安装了 pip,安装 Ansible 的方法和在 macOS 上类似。运行以下命令:

$ sudo -H pip install ansible

这将下载并安装 Ansible 及其所需的组件,如下图所示:

图 1.7 – 在 Ubuntu 上使用 pip 安装 Ansible

图 1.7 – 在 Ubuntu 上使用 pip 安装 Ansible

安装完成后,你可以使用以下命令进行升级:

$ sudo -H pip install ansible --upgrade

同样,降级 Ansible 使用相同的命令:

$ sudo -H pip install ansible==2.3.1.0

上述命令应适用于大多数 Linux 发行版,如 Rocky Linux、Red Hat Enterprise Linux、Debian 和 Linux Mint 等。

很多这些发行版都有自己的包管理器,你也可以使用它们来安装 Ansible;例如,在基于 Red Hat 的发行版(如 Red Hat Enterprise Linux 或 Rocky Linux)上,你也可以运行:

$ dnf install ansible-core

请参考文档获取有关安装你选择的 Linux 发行版的更多细节。

在 Windows 11 上安装

我们将要介绍的最后一个平台是 Windows 11,嗯,也不完全是。虽然技术上可以在 Windows 11 上本地运行 Ansible,但我不建议尝试,因为这是一个典型的“只是因为你能,不代表你应该”的例子,安装和配置所有依赖项的过程非常麻烦,而且维护起来更加复杂。

幸运的是,微软——作为一名长期的 Linux 系统管理员,写下这些话时依然感觉很奇怪——对在 Windows 11 中无缝运行 Linux 系统提供了出色的原生支持。

打开 Microsoft Store,搜索 Ubuntu;你应该看到类似下图的界面:

图 1.8 – 在 Microsoft Store 中找到 Ubuntu

图 1.8 – 在 Microsoft Store 中找到 Ubuntu

点击 获取 按钮下载 Ubuntu。下载完成后,我们就可以在 Windows 11 主机上运行 Ubuntu,但我们仍需要一个可以运行它的环境。要运行它,我们需要启用 Windows 子系统 Linux。

要启用此功能,请打开 PowerShell 窗口,在 Windows 搜索栏中输入 PowerShell 并打开 Windows PowerShell 应用程序;当你进入终端提示符后,运行以下命令:

$ wsl --install

按照屏幕上的提示操作,安装完成后,重启你的 Windows 11 主机。

重启后,重新登录时,你应该会看到类似以下的提示:

图 1.9 – 完成在 Windows 11 上安装 Ubuntu

图 1.9 – 完成在 Windows 11 上安装 Ubuntu

一旦安装完成,我喜欢退出默认的 Windows 子系统 Linux 终端,改用 Microsoft Terminal,你可以在 Microsoft Store 中免费下载该终端。

一旦你打开了首选的终端模拟器,并且在 Windows 子系统中的 Ubuntu 安装的提示符下,可以运行我们在 Linux 上安装 Ansible 时运行的相同命令,命令如下:

$ sudo -H apt-get update
$ sudo -H apt-get install python3-pip
$ sudo -H pip install ansible

在运行这些命令后,你应该会看到如下截图的输出:

图 1.10 – 在 Windows 11 上的 Ubuntu 中安装 Ansible

图 1.10 – 在 Windows 11 上的 Ubuntu 中安装 Ansible

如你所见,一切工作方式就像你在运行一台 Ubuntu 机器,允许你以完全相同的方式运行和维护 Ansible 安装。

注意

Windows 子系统(WSL) 并非在虚拟机中运行。它是一个完全本地化的 Linux 环境,直接集成在 Windows 11 中。它面向需要在工具链中运行 Linux 工具的开发者。尽管整体上对 Linux 命令的支持非常好,我建议阅读微软编写并维护的常见问题解答,以了解子系统的限制和特性。常见问题解答可以在 learn.microsoft.com/en-us/windows/wsl/faq 找到。

如前所述,虽然这是在 Windows 系统上运行 Ansible 控制节点的可行方式,但我们在未来章节中介绍的其他一些工具可能无法与 Windows 配合使用。因此,尽管你可以按照 Ubuntu 的说明进行操作,但某些部分可能无法正常工作。

启动虚拟机

为了启动一个虚拟机来运行我们的第一组 Ansible 命令,我们将使用一个名为 Multipass 的工具。这个工具允许你在本地主机上运行 Ubuntu 虚拟机。它支持 macOS、Linux 和 Windows。

要在 macOS 上安装 Multipass,我们可以使用 Homebrew 并运行以下命令:

$ brew install multipass

要在 Ubuntu 上安装,你可以运行以下命令:

$ snap install multipass

最后,对于 Windows 11 用户,你需要先从 www.virtualbox.org/wiki/Downloads 下载并安装 VirtualBox Windows 可执行文件,然后从 https://multipass.run/install 下载并安装 Multipass。我建议在安装之前,先阅读 Windows 上的安装说明,说明文档可以在以下网址找到:https://multipass.run/docs/installing-on-windows。

注意

虽然你可以在 Windows 子系统中的 Ubuntu 上运行相同的命令,但你需要将所有 multipass 命令的引用替换为 multipass.exe,以便调用 Windows 版本的 Multipass。

接下来,查看与本书相关的 GitHub 仓库,并在 Chapter01 文件夹中打开终端。如果你使用的是 Windows 11,必须打开 Ubuntu 终端,而不是 Windows 终端。

重要

在开始之前,快速提醒一句:Chapter01文件夹中包含一个 OpenSSH 密钥对,将用于访问本地机器。重要的是不要在本地机器上将这个密钥对用于除本例之外的其他任何地方,因为该密钥对是公开的,这样做不安全。

Chapter01文件夹中,你会看到几个文件。我们在启动虚拟机时要使用的文件名为vmadmin,并将 OpenSSH 密钥的公共部分附加到该用户上,这意味着在执行 Ansible 时,我们可以使用 OpenSSH 密钥的私有部分进行身份验证,以vmadmin用户身份登录。

我们将要运行的命令来启动虚拟机,该虚拟机名为ansiblevm,命令如下:

$ multipass launch -n ansiblevm --cloud-init cloud-init.yaml

一旦虚拟机启动(初次运行该命令时可能需要一段时间,因为它会下载虚拟机镜像),你需要运行以下命令来获取新创建的ansiblevm虚拟机的一些信息:

$ multipass info ansiblevm

以下屏幕显示了我启动并查看虚拟机信息的过程:

图 1.11 – 启动我们的虚拟机

图 1.11 – 启动我们的虚拟机

现在虚拟机已经启动并且基本信息已经检查过,你需要记下 IP 地址,在我这里是192.168.64.7。当你在主机上启动虚拟机时,IP 地址会有所不同。

在运行第一个 Ansible playbook 之前,你必须复制hosts-simple.examplehosts.example文件,并通过运行以下命令去掉文件名中的.example

$ cp -pr hosts-simple.example hosts-simple
$ cp -pr hosts.example hosts

一旦你复制了文件,打开新创建的文件,并将其中写着paste_your_ip_here的文本替换为ansiblevm虚拟机的 IP 地址;在我这里,hosts-simple文件的内容从以下内容变为:

paste_your_ip_here.nip.io ansible_user=vmadmin ansible_private_key_file=./example_key

阅读内容:

192.168.64.7.nip.io ansible_user=vmadmin ansible_private_key_file=./example_key

一旦你修改了hosts-simplehosts文件,你就可以开始运行第一个 Ansible Playbook 了。

playbook 简介

在 IT 领域,playbook通常是一组在某些事件发生时由某人执行的指令;这个定义有点模糊,我知道,但请继续听下去。这些指令包括从构建和配置新的服务器实例到部署代码更新,以及处理出现的问题等。

在传统意义上,playbook 通常是用户要遵循的脚本或指令集合,虽然它们的目的是在系统之间引入一致性和规范性,但即使出于最好的意图,这种情况也很少发生。

这就是 Ansible 的作用所在。通过使用 Ansible playbook,你告诉它对这些主机组应用这些更改和命令,而不必登录并手动执行 playbook。

在运行 playbook 之前,让我们讨论如何为 Ansible 提供目标主机的列表。为此,我们将使用 ansible.builtin.setup 模块。这个模块会连接到主机,然后尽可能多地获取关于该主机的信息。

主机清单

为了提供主机列表,我们需要提供一个清单列表。这是以主机文件的形式提供的。

在最简单的形式下,我们的主机文件可以像我们的 hosts-simple 文件一样,包含一行内容:

192.168.64.7.nip.io ansible_user=vmadmin ansible_private_key_file=./example_key

这告诉 Ansible 我们要联系的主机是 192.168.64.7.nip.io(请记住,你的 IP 地址会不同),并使用用户名 vmadmin。如果我们没有提供用户名,它会回退到你在 Ansible 控制主机上登录的用户,在我的例子中是 russ 用户,但 ansiblevm 上并不存在该用户。命令的最后部分告诉 Ansible 使用名为 example_key 的私有 OpenSSH 密钥文件,我们在启动虚拟机时将其公钥部分安装到了 vmadmin 用户下。

注意

我们使用的是 nip.io,这是一个免费服务,提供任何包含 IP 地址的主机名的通配符 DNS 记录。这意味着我们的域名 192.168.64.7.nip.io 在进行 DNS 查询时将解析为 192.168.64.7

要运行 ansible.builtin.setup 模块,我们需要从存储更新后的 hosts-simpleexample_key 文件的 Chapter01 文件夹中运行以下命令,并确保更新 IP 地址为你自己的:

$ ansible -i hosts-simple 192.168.64.7.nip.io -m ansible.builtin.setup

如果一切正常,你应该看到大量输出,其中包含关于你的主机的相当详细且低级别的信息。你应该看到类似以下内容:

图 1.12 – 我运行 ansible.builtin.setup 模块时输出的开始部分

图 1.12 – 我运行 ansible.builtin.setup 模块时输出的开始部分

如你从前面的截图中看到的,Ansible 很快就发现了我们 Vagrant box 上的很多信息。截图显示了机器上配置的 IP 地址和 IPv6 地址。它记录了时间和日期,如果你滚动浏览输出,你会看到返回了很多详细的主机信息。

让我们回到我们执行的命令:

$ ansible -i hosts-simple 192.168.64.7.nip.io -m ansible.builtin.setup

如你所见,我们使用 -i 标志加载 hosts-simple 文件。我们也可以使用 --inventory=hosts-simple,它会加载我们的清单文件。命令的下一部分是目标主机。在我们的例子中,这就是 192.168.50.4.nip.io。命令的最后部分,-m,告诉 Ansible 使用 setup 模块。我们也可以使用 --module-name= ansible.builtin.setup

这意味着如果我们不使用简写命令,完整的命令应该是:

$ ansible --inventory=hosts-simple simple 192.168.64.7.nip.io --module-name=ansible.builtin.setup

如前所述,hosts-simple 文件是我们能做到的最简单形式。以下是一个更常见的主机清单文件:

ansiblevm ansible_host=192.168.64.7.nip.io
[ansible_hosts]
ansiblevm
[ansible_hosts:vars]
ansible_connection=ssh
ansible_user=vmadmin
ansible_private_key_file=./example_key
host_key_checking=False

这是名为 hosts 的文件内容;如你所见,里面有更多的内容,所以我们从头到尾快速讲解一遍。

第一行定义了我们的单一主机。与简单示例不同,我们将目标主机命名为 ansiblevm,并将其归入名为 ansible_hosts 的组中,因此我们正在向 Ansible 提供它可以 SSH 连接的位置。这意味着现在我们可以使用名称 ansiblevm 来引用 192.168.64.7.nip.io。这意味着我们的命令现在看起来像这样:

$ ansible -i hosts ansiblevm -m ansible.builtin.setup

在文件的下一部分,我们创建了一个名为 ansible_hosts 的主机组,并将我们的单一主机 ansiblevm 添加到该组中。这意味着我们也可以运行:

$ ansible -i hosts ansible_hosts -m ansible.builtin.setup

如果我们组中不止一个主机,前面的命令将会遍历所有主机。hosts 文件的最后部分为 boxes 组中的所有主机设置了一些常见的配置选项。在这种情况下,我们告诉 Ansible 所有组中的主机都使用 SSH,用户是 vmadmin,应使用 ./example_key 作为私钥,并且在连接时不检查主机密钥。

我们将在后面的章节中重新访问清单主机文件。从现在开始,我们将使用 hosts 文件来定位 ansible_hosts 组。

Playbooks

在前一节中,运行 ansible 命令让我们调用了一个单独的模块。

在本节中,我们将介绍调用多个模块。以下是我们在前一节中调用的 ansible.builtin.setup 模块,然后使用 ansible.builtin.debug 模块将消息打印到屏幕:

---
- name: "A simple playbook"
  hosts: ansible_hosts
  gather_facts: true
  become: true
  become_method: "ansible.builtin.sudo"
  tasks:
    - name: "Output some information on our host"
      ansible.builtin.debug:
        msg: "I am connecting to {{ ansible_nodename }} which is running {{ ansible_distribution }} {{ ansible_distribution_version }}"

在我们拆解配置之前,让我们先看看运行 playbook 的结果。为此,请使用以下命令:

$ ansible-playbook -i hosts playbook01.yml

这将连接到我们的主机,收集系统信息,然后只返回我们需要的信息,以消息形式显示:

图 1.13 – 运行 ansible-playbook01.yml 的输出

图 1.13 – 运行 ansible-playbook01.yml 的输出

你会注意到,playbook 是用 YAML 编写的,YAML 是 YAML Ain’t Markup Language 的递归缩写。YAML 旨在作为一种人类可读的数据序列化标准,所有编程语言都可以使用。它通常用于帮助定义配置。

缩进在 YAML 中非常重要,因为它用于嵌套和定义文件的区域。让我们更详细地看看我们的 playbook:

---

虽然这些行看起来没什么特别的,但它们作为文档分隔符使用,因为 Ansible 会将所有 YAML 文件合并为一个文件。Ansible 必须知道一个文档的结束位置和另一个文档的开始位置。

接下来,我们看一下 playbook 的配置。如你所见,这就是缩进开始发挥作用的地方:

- name: "A simple playbook"
  hosts: ansible_hosts
  gather_facts: true
  become: true
  become_method: "ansible.builtin.sudo"

- 告诉 Ansible 这是一个部分的开始。从这里开始,使用键值对。具体如下:

  • name:这为 playbook 运行提供一个名称。

  • hosts:这告诉 Ansible 在 playbook 中需要针对的主机或主机组。必须在像我们在前一部分中介绍的主机清单中定义这些主机。

  • gather_facts:这告诉 Ansible 在首次连接到主机时运行 ansible.builtin.setup 模块。然后,这些信息将在运行过程中提供给 playbook。

  • become:这是因为我们以普通用户身份连接到主机,在这种情况下是 vmadmin 用户。Ansible 可能没有足够的访问权限来执行一些我们告诉它的命令,因此这个指令告诉 Ansible 以 root 用户身份执行所有命令。

  • become_method:这告诉 Ansible 如何成为 root 用户;在我们的案例中,我们通过在启动虚拟机时运行的 cloud-init 脚本配置了无密码的 sudo,因此我们使用 ansible.builtin.sudo

  • tasks:这些是我们可以告诉 Ansible 在连接到目标主机时执行的任务。

你会注意到,从这里开始,我们再次移动了缩进。这定义了配置的另一个部分。这一次是针对任务的:

    - name: "Output some information on our host"
      ansible.builtin.debug:
        msg: "I am connecting to {{ ansible_nodename }} which is running {{ ansible_distribution }} {{ ansible_distribution_version }}"

如我们所见,我们唯一运行的任务是 ansible.builtin.debug 模块。该模块允许我们在运行 playbook 时,在 Ansible playbook 执行流中显示输出。

你可能已经注意到,大括号之间的信息由 ansible.builtin.setup 模块中的键组成。在这里,我们告诉 Ansible 在使用这些键时替换为每个键的值。我们将在我们的 playbook 中经常使用这一点。我们还将定义我们自己的键值,作为 playbook 运行的一部分。

让我们通过添加另一个任务来扩展我们的 playbook。以下内容可以在 playbook02.yml 中找到:

---
- name: "Update all packages"
  hosts: "ansible_hosts"
  gather_facts: true
  become: true
  become_method: "ansible.builtin.sudo"
  tasks:
    - name: "Output some information on our host"
      ansible.builtin.debug:
        msg: "I am connecting to {{ ansible_nodename }} which is running {{ ansible_distribution }} {{ ansible_distribution_version }}"
    - name: "Update all packages to the latest version"
      ansible.builtin.apt:
        name: "*"
        state: "latest"
        update_cache: true

如你所见,我们添加了第二个任务,调用了 ansible.builtin.apt 模块。这个模块旨在帮助我们与 Ubuntu 和其他基于 Debian 的操作系统使用的包管理器 apt 进行交互。我们在这里设置了三个关键值:

  • name:这是一个通配符。它告诉 Ansible 使用所有已安装的包,而不仅仅是一个指定的包。例如,我们可以在这里使用类似 apache2 的内容来定位 Apache。

  • state:在这里,我们告诉 Ansible 确保我们在 name 键中定义的包是 latest 版本。由于我们已经命名了所有已安装的包,这将更新我们安装的所有内容。

  • update_cache:由于我们下载的虚拟机镜像是为了优化体积而设计的,因此不包含有关可用包的信息;通过将 update_cache 设置为 true,这将下载所有包及其版本信息的列表。

使用以下命令运行 playbook:

$ ansible-playbook -i hosts playbook02.yml

这将给我们带来以下结果:

图 1.14 – 运行 ansible-playbook02.yml 的输出

图 1.14 – 运行 ansible-playbook02.yml 的输出

ansible.builtin.apt 任务已在主机上标记为已更改。这意味着软件包已被更新。

重新运行相同的命令显示以下结果:

图 1.15 – 重新运行 ansible-playbook02.yml 的输出

图 1.15 – 重新运行 ansible-playbook02.yml 的输出

如你所见,ansible.builtin.apt 任务现在在我们的主机上显示为 ok。这是因为当前没有软件包需要更新。

在我们结束这次对 playbook 的简短回顾之前,让我们做一些更有趣的事情。

该 playbook playbook03.yml 为我们的虚拟机添加了 NTP 安装、配置和启动功能。它还使用模板将自定义的 NTP 配置文件添加到虚拟机中。

vars 部分允许我们配置自己的键值对。在这种情况下,我们提供了一组 NTP 服务器,稍后将在 playbook 中使用:

  vars:
    ntp_servers:
      - "0.uk.pool.ntp.org"
      - "1.uk.pool.ntp.org"
      - "2.uk.pool.ntp.org"
      - "3.uk.pool.ntp.org"

我们实际上为相同的键提供了四个不同的值。这些将在模板任务中使用。我们也可以这样写:

  vars:
    ntp_servers: [ "0.uk.pool.ntp.org", "1\. uk.pool.ntp.org", "2\. uk.pool.ntp.org", "3\. uk.pool.ntp.org" ]

然而,这部分内容稍微有些难以阅读。下一个新增部分是 handlers处理器是一个被赋予名称并在 playbook 执行结束时根据任务变更情况被调用的任务:

  handlers:
    - name: "Restart ntp"
      ansible.builtin.service:
        name: "ntp"
        state: "restarted"

在我们的案例中,重启 ntp 处理器使用 ansible.builtin.service 模块来重启 ntp。接下来,我们有两个新任务,首先是使用 ansible.builtin.apt 安装 NTP 服务以及 sntpntp-doc 软件包:

    - name: "Install packages"
      ansible.builtin.apt:
        state: "present"
        pkg:
          - "ntp"
          - "sntp"
          - "ntp-doc"

由于我们需要安装三个软件包,我们需要一种方法将三个不同的软件包名称传递给 ansible.builtin.apt 模块,以便我们无需为每个软件包安装创建三个不同的任务。为此,我们使用 pkg 选项,而不是只能定义一个软件包安装的 name 选项。我们没有使用 latest,而是使用了 present;这意味着如果软件包已经安装,它们不会被更新。

对 playbook 的最后一个添加是以下任务:

    - name: "Configure NTP"
      ansible.builtin.template:
        src: "./ntp.conf.j2"
        dest: "/etc/ntp.conf"
        mode: "0644"
      notify: "Restart ntp"

该任务使用 ansible.builtin.template 模块。从我们的 Ansible 控制器读取模板文件,处理它并将处理后的模板上传到主机。一旦上传,我们告诉 Ansible 如果我们上传的配置文件有任何更改,则通知 restart ntp 处理器。

在这种情况下,模板文件是与 playbook 位于同一文件夹中的 ntp.conf.j2 文件,如 src 选项中定义的那样。该文件内容如下:

# {{ ansible_managed }}
driftfile /var/lib/ntp/drift
restrict default nomodify notrap nopeer noquery
restrict 127.0.0.1
restrict ::1
{% for item in ntp_servers %}
server {{ item }} iburst
{% endfor %}
includefile /etc/ntp/crypto/pw
keys /etc/ntp/keys
disable monitor

文件的大部分内容是标准的 NTP 配置文件,附加了少量 Ansible 部分。第一个新增部分是第一行:

# {{ ansible_managed }}

如果这一行不存在,每次我们运行 Ansible 时,文件将被上传,这会被视为更改,并且 restart ntp 处理器会被调用,这意味着即使没有更改,NTP 也会被重启。

下一部分循环遍历我们在 playbook 的 vars 部分定义的 ntp_servers 值:

{% for item in ntp_servers %}
server {{ item }} iburst
{% endfor %}

对于每个值,添加一行,包含单词 server,值或 {{ item }},然后是 iburst

现在我们知道了在 playbook 中添加的内容,并大致了解将要执行的额外任务,让我们使用以下命令运行它:

$ ansible-playbook -i hosts playbook03.yml

以下屏幕仅显示额外的任务,而不是完整的输出,因为我们知道它将只是标记为 ok

图 1.16 – 运行 ansible-playbook03.yml 的输出

图 1.16 – 运行 ansible-playbook03.yml 的输出

这次我们有三项更改任务。再次运行 playbook 后显示如下:

图 1.17 – 重新运行 ansible-playbook03.yml 的输出

图 1.17 – 重新运行 ansible-playbook03.yml 的输出

正如预期的那样,没有更改,因为我们没有更改 playbook 或虚拟机上的任何内容,Ansible 报告一切为 ok。此外,由于没有检测到 NTP 配置文件的更改,因此没有调用重新启动 NTP 的 Handler,因此它不会出现在输出中。

在我们结束之前,让我们通过运行以下命令启动第二台虚拟机:

$ multipass launch -n ansiblevm2 --cloud-init cloud-init.yaml

一旦第二台虚拟机启动,运行以下命令以获取有关新虚拟机的一些信息:

$ multipass info ansiblevm2

现在我们知道了 IP 地址,可以向 hosts 文件添加两行。首先,为了定义新主机,请在原始主机定义下方添加以下代码(更新为使用正确的 IP 地址):

ansiblevm2 ansible_host=192.168.64.8.nip.io

然后,将 ansiblevm2 添加到 ansible_hosts 组中:

[ansible_hosts]
ansiblevm
ansiblevm2

然后,使用以下命令重新运行 playbook:

$ ansible-playbook -i hosts playbook03.yml

如你所见,执行的是相同的命令,但现在我们针对的是两台虚拟机,原始虚拟机没有变化,所有更改都应用到新部署的主机:

图 1.18 – 针对两台虚拟机重新运行 ansible-playbook03.yml 的输出

图 1.18 – 针对两台虚拟机重新运行 ansible-playbook03.yml 的输出

如果你重新运行命令,你会看到一切现在都显示为 ok,因为没有进一步的更改。

在我们继续总结之前,让我们整理一下两台虚拟机,并将它们删除,因为我们不会再需要它们。为此,请运行以下命令:

$ multipass delete --purge ansiblevm ansiblevm2

正如你可能已经猜到的,这将删除虚拟机,然后清除配置和文件。

总结

在本章中,我们通过在本地安装 Ansible 并使用 Vagrant 启动虚拟机与其交互,迈出了使用 Ansible 的第一步。我们了解了基本的主机清单文件,并使用 Ansible 命令对我们的虚拟机执行了一个任务。

然后我们查看了 playbook,从一个基本的 playbook 开始,该 playbook 返回了目标的相关信息,然后进入一个更新所有已安装操作系统包的 playbook,接着安装并配置 NTP 服务。

本章结束时,我们启动了第二台虚拟机,并迅速将其配置到与第一台虚拟机相同的水平。

在下一章中,我们将探讨 Ansible Galaxy,并讨论 Ansible 如何打包和维护其社区模块。

进一步阅读

在本章中,我们提到了 Puppet 和 SaltStack:

  • Puppet 是一个配置管理工具,采用服务器/代理配置运行。它有两个版本——开源版本和由 Puppet 公司支持的企业版本。它是一个声明式系统,与 Ruby 紧密相关。欲了解更多关于 Puppet 的信息,请参见 www.puppet.com/

  • SaltStack 是另一种配置管理工具。它具有高度可扩展性,虽然它与 Ansible 采用相似的设计方法,但它类似于 Puppet,采用服务器/代理模式。您可以在 www.vmware.com/support/acquisitions/saltstack.html 获取更多关于 SaltStack 的信息。

  • 我还提到了我的个人博客,您可以在 www.russ.foo/ 找到。

我们使用了以下 Ansible 模块,您可以通过以下链接了解每个模块的更多信息:

第二章:探索 Ansible Galaxy

欢迎来到我们的第二章。在这一章中,我们将学习 ansible-galaxy 命令;我们将介绍该命令提供的功能,并讨论它在过去几年中对 Ansible 发展的重要性。

Ansible Galaxy 是一个由社区贡献角色的在线仓库;我们将探索一些最优秀的角色,学习如何使用它们,以及如何创建你自己的角色并将其托管在 Ansible Galaxy 上。

到本章结束时,我们将涵盖以下主题:

  • Ansible 发布生命周期

  • Ansible Galaxy 介绍

  • 什么是角色?

  • 发布并使用 Ansible Galaxy 角色

  • Ansible 集合

  • Ansible Galaxy 命令

在我们开始探索 Ansible Galaxy 之前,让我们讨论一下 Ansible 核心的发布生命周期,以及它在过去几年中的变化,因为这些变化使得它成为 Ansible 生态系统中的一个重要组成部分。

技术要求

在本章中,我们将再次使用Multipass,这是我们在第一章《安装与运行 Ansible》中介绍的工具,以及本书配套的 GitHub 仓库,地址是 github.com/PacktPublishing/Learn-Ansible-Second-Edition

Ansible 发布生命周期

在上一章安装 Ansible 时,细心的你可能注意到通过 sudo -H pip install ansible 命令安装了几个不同的 Ansible 包。

以下是使用 pip 安装 Ansible 时输出的经过编辑的版本:

 $ sudo -H pip install ansible
Collecting ansible
  Downloading ansible-8.2.0-py3-none-any.whl (45.1 MB)
Collecting ansible-core~=2.15.2
  Downloading ansible_core-2.15.2-py3-none-any.whl (2.2 MB)
Installing collected packages: resolvelib, packaging, ansible-core, ansible
Successfully installed ansible-8.2.0 ansible-core-2.15.2 packaging-23.1 resolvelib-1.0.1

如你所见,安装了两个主要的 Ansible 包:ansible-8.2.0ansible-core-2.15.2。在我们讨论这两个包的区别之前,让我们快速回顾一下,直到 Ansible 的 2.9 版本,Ansible 是如何进行维护和打包的。

2.10 版本之前的每个 Ansible 版本,都包含了许多内置在发布中的模块;当 Ansible 还很新,用户基础和功能集中在少数任务上时,管理和维护这些模块的发布作为 Ansible 代码库的一部分非常容易,而该代码库由 Ansible 团队在官方 GitHub 仓库中维护,地址是 github.com/ansible/ansible

第一章《安装与运行 Ansible》结束时,我们总共使用了五个模块,所有这些模块都是 Ansible 内置的,它们分别是:

  • ansible.builtin.setup:一个发现目标主机信息并在 Playbook 执行过程中使其可用的模块

  • ansible.builtin.service:一个管理目标主机上服务状态的模块

  • ansible.builtin.debug:该模块允许你在 Playbook 执行过程中打印语句

  • ansible.builtin.apt:这个模块使用 apt 包管理器管理目标主机上的软件包

  • ansible.builtin.template:这个模块为 Ansible 带来了模板功能,使你能够将文件输出到目标主机

那只不过是一个执行单一任务的 playbook;再加上模块的数量,就能让你大致了解情况——目前在 Amazon AWS 命名空间中有 95 个模块,在 Microsoft Azure 命名空间中有 282 个模块——这意味着超过 370 个模块涵盖了两个不同命名空间的基本功能。在撰写本文时,已有超过 40 个不同的命名空间。

你可能会问,“等一下,什么是命名空间?” 这些模块现在被分组到不同的集合中,每个集合都有自己的命名空间;以下是一些示例:

  • ansible.builtin:正如你可能已经猜到的,这个命名空间中的模块提供了一些 Ansible 核心功能

  • amazon.aws:这些是官方的 Amazon Web Services 模块

  • azure.azcollection:这里你会找到官方的 Microsoft Azure 模块

  • kubernetes.core:如果你想使用 Kubernetes,这些模块将会是你所需要的

再次,你可能会想,“这有用的信息,但这和任何事情有什么关系?”;好吧,Ansible 过去每年有几个主要版本发布,而随着模块数量的逐渐增加,发布过程变得越来越难以管理,因为团队不仅需要关注核心的 Ansible 代码库,还需要关注与之捆绑的模块及其插件。

每个命名空间可能都有自己的开发团队,团队成员包括 Ansible 核心贡献者、社区成员,某些命名空间中还有像 Amazon 和 Microsoft 这样的巨大企业。因此,协调一次 Ansible 发布变得异常困难,无论是在物流还是时间安排上。Ansible 支持的一些技术变化非常快速;例如,Amazon Web Services 和 Microsoft Azure 几乎每周都会推出新特性并为现有服务添加功能。Ansible 如果等到可能需要六个月才提供更新,来解决兼容性问题,显然没有意义。这就是为什么 Ansible 团队决定将现在被称为 Ansible Core 的发布过程与 Ansible 的其他部分解耦开来;Ansible Core 是运行 Ansible 所需要的工具,目前已经包括了超过 85 个命名空间集合,其中包含超过 1,000 个模块和插件。

发布生命周期

发布周期从引入新的主要版本 ansible-core 开始,例如 ansible-core 2.11。之后,最新版本的 ansible-core 和它之前的两个版本 ansible-base 2.10Ansible 2.9 会继续得到积极维护。开发工作转向 ansible-core,并持续进行。在此阶段,Ansible 社区包中的集合不再进行新增或更新。

随后,Ansible 社区包的候选版本被引入。经过测试后,如果有必要,会推出更多的候选版本。

一旦最终确定,新的主要版本的 ansible-core 会基于 ansible-core 2.11,例如 Ansible 4.0.0。在此发布之后,只有最新版本的 Ansible 社区 包会继续进行积极维护。

然后,焦点转向 ansible-core 版本,例如 2.11.1 和唯一支持的 4.1.0 版本。

随着周期的推进,ansible-core 会进入功能冻结阶段。接下来是 ansible-core 的候选版本发布,该版本进行测试。如有需要,会推出更多的候选版本。最后,后续的主要版本将发布,标志着新周期的开始。

以下图表提供了周期的概览:

图 2.1 – Ansible 发布周期概览

图 2.1 – Ansible 发布周期概览

如你所见,这种方法使得 Ansible 团队在发布计划上更加灵活,并允许在两个不同版本之间进行更多的并行工作。

现在我们已经了解了 Ansible 如何管理发布周期,以及如何打包模块,接下来我们来看看 Ansible Galaxy,它可以用来分发集合和角色。

Ansible Galaxy 介绍

大多数人第一次接触 Ansible Galaxy 时,都会访问 galaxy.ansible.com/ 网站。该网站是社区贡献的角色和模块的聚集地:

图 2.2 – Ansible Galaxy 首页

图 2.2 – Ansible Galaxy 首页

在本书的其余部分,我们将编写与 Ansible Core 模块交互的自定义角色,以供我们在 playbook 中使用。

Ansible Galaxy 上发布了超过 15,000 个角色,这些角色涵盖了许多任务,并支持几乎所有 Ansible 支持的操作系统。

然后,我们有了 ansible-galaxy 命令;这是与 Ansible Galaxy 网站交互的一种方式,可以在命令行中操作,并且能够启动角色,稍后我们将介绍;我们还可以用它下载、搜索并发布我们在 Ansible Galaxy 上的自定义角色。

最后,Red Hat 已经开源了 Ansible Galaxy 的代码,这意味着如果你需要在公司防火墙后分发你的角色,你也可以运行自己托管的版本。

在我们讨论发布角色和使用来自 Ansible Galaxy 的角色之前,先来讨论一下什么是角色。

什么是角色?

在本书的其余部分,我们将构建我们自己的自定义角色,因此这里只是简单介绍什么是角色。

第一章安装和运行 Ansible 中,我们的最终 playbook 包含了一些变量、一个处理器、四个任务和一个模板。

除了模板文件外,所有的代码都被硬编码到我们的剧本文件中,虽然这样在使用少量任务和变量时易于阅读,但这种做法并不使代码具有很好的重用性。此外,在后面的章节中,我们可能在一次剧本执行中需要执行超过 50 个任务,这将导致文件变得相当庞大且难以管理。

为了避免这个问题,Ansible 引入了角色(roles)的概念;它们允许你以逻辑上有意义的方式组织你的 Ansible 代码,例如将执行单一任务的任务组合在一起,在第一章中,安装与运行 Ansible部分就是安装和配置 NTPD 服务。

这也意味着你可以通过复制角色文件夹来将角色添加到另一个剧本中,发布它,然后从 Ansible Galaxy 拉取它。

那么,让我们来看看如何基于我们在第一章中运行的最终剧本中的任务、处理程序、变量和模板来创建一个基本的角色,安装与运行 Ansible部分。

首先,我们需要创建 Ansible 推荐的角色文件夹和文件结构;幸运的是,ansible-galaxy命令已经为我们解决了这个问题;只需在存储你的剧本的文件夹中运行以下命令,它将启动文件夹和文件结构,这被 Red Hat 视为最佳实践:

$ ansible-galaxy role init roles/learnansible-example-role

上述命令将创建一个名为roles的文件夹(如果该文件夹不存在的话),并在roles文件夹中添加一个名为learnansible-example-role的文件夹。

learnansible-example-role文件夹包含了所有最佳实践的文件夹布局和所需文件,目的是使你能够在 Ansible Galaxy 上发布一个角色。

这些如下:

  • README.md:这个文件包含了一个大纲,你可以在其中填写角色的信息;你可以根据需要使用模板中的内容。请注意,如果你决定将角色发布到 Ansible Galaxy,它的内容将会显示在上面,因此尽量使其描述尽可能详细。

  • defaults/main.yml:这个 YAML 文件通常包含你的角色的任何默认值。

  • files/:这个空文件夹存放在剧本执行过程中需要复制到目标主机上的任何文件。

  • handlers/main.yml:正如你从这个文件夹的名称可能已经猜到的,这个 YAML 文件是用来定义你的角色需要的任何处理程序(handlers)的地方。

  • meta/main.yml:这个 YAML 文件与README.md文件类似,只有在角色发布到 Ansible Galaxy 后才会使用;在这里,你可以提供你的详细信息、添加任何标签,并定义支持的平台以及你的角色支持的最低 Ansible 版本。

  • tasks/main.yml:这是我们在接下来的章节中大部分时间都会使用的文件;它定义了所有角色的任务。

  • templates/:这是另一个空文件夹,这次它用来存储你的模板文件。

  • tests/inventorytest.yml:这里有一个包含两个文件的文件夹,一个是库存文件,一个是测试 playbook;它用于对你的角色进行测试。

  • vars/main.yml:最后,这个 YAML 文件包含了你可能需要使用的任何变量,如果需要的话,这些变量将覆盖defaults/main.yml文件中的内容。

为了填充角色,我将最终 playbook 中的代码拆分到上述各种文件中;我对 playbook 本身所做的唯一修改是删除了以下任务,因为我们不需要它:

    - name: "Output some information on our host"
      ansible.builtin.debug:
        msg: "I am connecting to {{ ansible_nodename }} which is running {{ ansible_distribution }} {{ ansible_distribution_version }}"

这使得roles/learnansible-example-role/tasks/main.yml文件看起来像下面的代码:

# tasks file for roles/learnansible-example-role
- name: "Update all packages to the latest version"
  ansible.builtin.apt:
    name: "*"
    state: "latest"
    update_cache: true
  tags:
    - "skip_ansible_lint"
- name: "Install packages"
  ansible.builtin.apt:
    state: "present"
    pkg:
      - "ntp"
      - "sntp"
      - "ntp-doc"
- name: "Configure NTP"
  ansible.builtin.template:
    src: "./ntp.conf.j2"
    dest: "/etc/ntp.conf"
    mode: "0644"
  notify: "Restart ntp"

请注意,正如在第一章中提到的,安装和运行 Ansible,我们在文件顶部有---,表示main.yml是一个独立的文件。由于它位于tasks文件夹中,我们不需要像在原始 playbook 中那样使用 tasks 来定义它包含任务。

这个模式由roles/learnansible-example-role/handlers/main.yml文件遵循,其内容如下:

# handlers file for roles/learnansible-example-role
- name: "Restart ntp"
  ansible.builtin.service:
    name: "ntp"
    state: "restarted"

此外,接下来是roles/learnansible-example-role/vars/main.yml文件,其内容如下:

---
# vars file for roles/learnansible-example-role
ntp_servers:
  - "0.uk.pool.ntp.org"
  - "1.uk.pool.ntp.org"
  - "2.uk.pool.ntp.org"
  - "3.uk.pool.ntp.org"

roles/learnansible-example-role/vars/ntp.conf.j2文件与我们在第一章中使用的模板文件完全相同,安装和 运行 Ansible

除了README.md文件外,唯一的新增文件是roles/learnansible-example-role/meta/main.yml。如前所述,这个文件包含了将角色发布到 Ansible Galaxy 所需的所有信息;在我们的示例中,其内容如下:

galaxy_info:
  role_name: "ansible_role_learnansible_example"
  namespace: "russmckendrick"
  author: "Russ McKendrick"
  description: "Example role to accompany Learn Ansible (Second Edition)"
  issue_tracker_url: "https://github.com/russmckendrick/ansible-role-learnansible-example/issues"
  license: "license (BSD-3-Clause)"
  min_ansible_version: "2.9"
  platforms:
    - name: "Ubuntu"
      versions:
        - "jammy"
  galaxy_tags:
    - "ntp"
    - "time"
    - "example"
dependencies: []

我们将在本章的下一节中重新访问这个文件,当我们将角色发布到 Ansible Galaxy 时。

现在我们已经准备好了运行角色所需的一切,我们需要一个 playbook 来调用它;在本书附带的仓库的Chapter02文件夹中,你将找到前面提到的角色文件夹,以及一个名为playbook01.yml的 playbook,其内容如下:

- name: "Run the role locally"
  hosts: "ansible_hosts"
  gather_facts: true
  become: true
  become_method: "ansible.builtin.sudo"
  roles:
    - learnansible_example_role

正如你所见,playbook 的开头与我们在第一章中运行的完全相同,安装和运行 Ansible。然而,它缺少varshandlerstasks部分,而我们只使用了一个包含单个角色的roles部分,这个角色位于roles/learnansible-example-role

Chapter02文件夹中,你将找到所有启动本地虚拟机所需的文件,这些文件使用 Multipass。

重要提示

运行以下命令时,创建hosts.example文件的副本并将其命名为hosts;复制完成后,更新新创建的文件,填入新启动虚拟机的 IP 地址,正如我们在第一章中所做的,安装和 运行 Ansible

要启动虚拟机,获取其 IP 地址并使用以下命令运行 playbook:

$ multipass launch -n ansiblevm --cloud-init cloud-init.yaml
$ multipass info ansiblevm
$ ansible-playbook -i hosts playbook01.yml

这应该会给你类似以下的输出:

图 2.3 – 使用新创建的角色运行 playbook

图 2.3 – 使用新创建的角色运行 playbook

你可以通过以下两个命令停止并删除虚拟机:

$ multipass stop ansiblevm
$ multipass delete --purge ansiblevm

现在我们知道什么是角色,并且定义了一个基本的角色,让我们看看如何将这个简单的角色发布到 Ansible Galaxy,然后在 Ansible playbook 中使用它—以及其他一些角色。

发布到并使用 Ansible Galaxy 角色

现在我们知道什么是角色,并且看到了如何使用角色让我们的 Ansible playbook 更加简洁且可重复使用,接下来我们应该看看如何将角色发布到 Ansible Galaxy,并在我们的 playbook 中从那里使用它们。

将你的角色发布到 Ansible Galaxy

在将角色发布到 Ansible Galaxy 时,你需要满足两个主要的前提条件:一个有效的 GitHub 账户,这将用于认证到 Ansible Galaxy,以及一个包含角色代码的公开 GitHub 仓库。

在这个示例中,我将使用我自己的 GitHub 账户;你可以在 github.com/russmckendrick/ 找到我。我将使用一个可以在 github.com/russmckendrick/ansible-role-learnansible-example/ 找到的仓库。

要发布你的角色,你需要执行以下步骤:

  1. 访问 Ansible Galaxy 网站,网址是 galaxy.ansible.com/,然后点击 GitHub 图标进行登录:

图 2.4 – 登录到 Ansible Galaxy

图 2.4 – 登录到 Ansible Galaxy

  1. 登录后,点击左侧菜单中的 我的内容 菜单项,该图标是一个带有项目符号的列表图标,如下所示:

图 2.5 – 进入我的内容页面

图 2.5 – 进入我的内容页面

  1. 进入 我的内容 页面后,点击 + 添加内容 按钮;在这里,你将看到两个选项:从 GitHub 导入角色上传 新集合

图 2.6 – 添加内容选项

图 2.6 – 添加内容选项

  1. 点击 从 GitHub 导入角色 按钮,你将看到你的仓库列表;选择包含你想要发布的角色的仓库并点击 确定 按钮:

图 2.7 – 选择包含你想要发布的角色的仓库

图 2.7 – 选择包含你想要发布的角色的仓库

  1. 几分钟后,你的角色将被发布,且你将返回到 我的内容 页面,此时应该会列出你新发布的角色:

图 2.8 – 返回到我的内容页面

图 2.8 – 返回到我的内容页面

  1. 点击新发布的角色名称,这将把你带到 Ansible Galaxy 角色页面:

图 2.9 – 新发布的 Ansible Galaxy 角色页面

图 2.9 – 新发布的 Ansible Galaxy 角色页面

你可以在 https://galaxy.ansible.com/russmckendrick/ansible_role_learnansible_example 找到我在本次演示中发布的角色副本。

如你所见,查看 meta/main.yml 文件中的详细信息,并点击 README.md 文件。

现在角色已经发布,我们如何在 Ansible playbook 中使用它呢?让我们来看看。

使用 Ansible Galaxy 中的角色

在我们在 playbook 中使用角色之前,首先需要做的事情是下载角色;有几种方法可以做到这一点;首先,你可以使用在 Ansible Galaxy 页面上给出的命令来下载角色。

运行以下命令将把角色下载到你的 Ansible 配置目录:

$ ansible-galaxy install russmckendrick.ansible_role_learnansible_example

Ansible 配置目录通常是用户主文件夹中的一个隐藏文件夹。该文件夹的缩写是 ~/.ansible,或者在我的情况下,文件夹的完整路径是 /Users/russ.mckendrick/.ansible,如下所示的 Shell 输出:

图 2.10 – 从 Ansible Galaxy 下载角色

图 2.10 – 从 Ansible Galaxy 下载角色

下载角色的第二种方式是创建一个 requirements.yml 文件;此文件应包含你希望下载的角色列表,例如,仓库中 Chapter02 文件夹中的 requirements.yml 文件,它伴随本书并看起来像以下内容:

- src: "itnok.update_ubuntu"
- src: "geerlingguy.nginx"
- src: "russmckendrick.ansible_role_learnansible_example"

如你所见,这里定义了三个角色;要安装这三个角色,你可以运行以下命令:

$ ansible-galaxy install -r requirements.yml

我们将下载的另外两个角色如下:

  • itnok.update_ubuntu:此角色管理 Ubuntu 主机上的更新

  • geerlingguy.nginx:此角色帮助你在多个 Linux 发行版上下载、安装和配置 NGINX

你可以在本章末尾的进一步阅读部分找到角色的相关链接。

这将只下载缺失的角色;当我运行命令时,得到以下输出:

图 2.11 – 从 Ansible Galaxy 下载缺失的角色

图 2.11 – 从 Ansible Galaxy 下载缺失的角色

如你所见,russmckendrick.ansible_role_learnansible_example 已经存在于我的机器上,因此跳过了下载它。

Ansible playbook 文件 playbook02.yml,可以在 Chapter02 文件夹中找到,使用以下代码调用 requirements.yml 文件中定义的三个角色:

---
- name: "Run the remote roles"
  hosts: "ansible_hosts"
  gather_facts: true
  become: true
  become_method: "ansible.builtin.sudo"
  roles:
    - "itnok.update_ubuntu"
    - "geerlingguy.nginx"
    - "russmckendrick.ansible_role_learnansible_example"

和以前一样,你可以使用 Multipass 启动虚拟机(确保更新 hosts 文件中的 IP 地址),并使用以下命令运行 playbook:

$ multipass launch -n ansiblevm --cloud-init cloud-init.yaml
$ multipass info ansiblevm
$ ansible-playbook -i hosts playbook02.yml

如你在以下屏幕中的 playbook 摘要中所见,这次发生了更多的事情:

图 2.12 – 运行 playbook,使用从 Ansible Galaxy 下载的角色

图 2.12 – 运行 playbook,使用从 Ansible Galaxy 下载的角色

另外两个角色对操作系统进行了比我们的角色更彻底的更新,安装了 NGINX 包并启动了该服务;这意味着,如果你将 multipass info ansiblevm 命令返回的 IP 地址放入浏览器中,你将看到默认的 NGINX 页面。

再次强调,准备好后,你可以使用以下两个命令停止并删除虚拟机:

$ multipass stop ansiblevm
$ multipass delete --purge ansiblevm

既然你已经理解了什么是 Ansible 角色,如何将其发布到 Ansible Galaxy,以及如何将我们自己发布的角色和社区角色结合到 Ansible playbook 中,那么 Ansible Galaxy 还能做些什么呢?

Ansible 集合

在本章开始时,我们讨论了 Ansible 开发团队如何将 Ansible 模块与 Ansible Core 解耦,以及这如何影响发布生命周期。

所有这些模块、插件和其他支持代码都可以在 Ansible Galaxy 上找到;例如,来自 Amazon 命名空间的 AWS 集合可以在 galaxy.ansible.com/amazon/aws 找到;你可以使用以下命令安装该集合:

$ ansible-galaxy collection install amazon.aws

运行此命令将下载并安装集合到 ~/.ansible/collections/ansible_collections/amazon/aws,如下所示的终端输出:

图 2.13 – 安装 amazon.aws 集合

图 2.13 – 安装 amazon.aws 集合

然而,单单安装集合并不意味着你可以在 playbooks 中使用它;例如,Amazon AWS 模块需要安装一些额外的 Python 库,通常每个集合都会附带一个 requirements.txt 文件,列出需要在你的系统上安装的 Python 库,以便集合的模块和插件能够正常工作。

要安装这些库,你应该使用 pip 来安装它们:

$ pip install -r ~/.ansible/collections/ansible_collections/amazon/aws/requirements.txt

一旦安装完成,你就可以使用构成该集合的模块和插件。

Ansible Galaxy 命令

在完成关于 Ansible Galaxy 的这一章之前,让我们快速讨论一些其他有用的命令。

ansible-galaxy 命令具有一些你所期望的基本功能,例如以下内容:

$ ansible-galaxy --version
$ ansible-galaxy --help

这将分别显示命令的版本详情和基本帮助选项。

ansible-galaxy 命令分为两部分,我们已经涉及过了。

首先,有 ansible-galaxy collection;从这里,你可以添加以下命令:

  • download:检索集合及其依赖项,如 tarballs,这是 Linux 机器上的离线安装归档格式。

  • init:设置一个带有基础结构的新集合。

  • build:构建一个适合发布到 Ansible Galaxy 的 Ansible 集合工件。

  • publish:将集合工件发布到 Ansible Galaxy。

  • install:从指定的文件、URL 或直接从 Ansible Galaxy 添加集合。

  • list:显示集合路径中每个集合的名称和版本。

  • verify:对已安装的集合进行校验和对比,与服务器上的校验和进行对比;任何依赖项不会被验证。

其次是 ansible-galaxy role,正如您已经猜到的,这些命令是用于处理角色的:

  • init:设置一个具有基础结构的新角色

  • remove:从指定的角色路径中删除角色

  • delete:从 Galaxy 中删除角色。请注意,这不会影响或修改实际的 GitHub 仓库

  • list:显示角色路径中每个角色的名称和版本

  • search:使用标签、平台、作者和关键词查询 Galaxy 数据库

  • import:将一个角色导入到 Galaxy 服务器

  • setup:监督 Galaxy 与指定源之间的连接

  • info:获取某个特定角色的详细信息

  • install:从指定的文件、URL 或直接从 Ansible Galaxy 添加角色

如需获得更多关于这些命令的帮助,您可以在命令末尾添加--help。例如,您可以运行以下命令:

$ ansible-galaxy role search --help

这将为您提供如何搜索 Ansible Galaxy 的详细信息;例如,要搜索我的角色,您需要运行以下命令:

$ ansible-galaxy role search --author russmckendrick

这将返回我已发布到 Ansible Galaxy 的所有角色的列表,或者您也可以运行以下命令:

$ ansible-galaxy role list

这将列出您在角色路径(即 ~/.ansible/roles/)中安装的所有角色。您可以从那里运行以下类似的命令:

$ ansible-galaxy role info russmckendrick.ansible_role_learnansible_example

这将帮助您获取有关您已安装角色的详细信息,这也标志着我们对 ansible-galaxy 命令的介绍结束。

总结

在本章中,我们深入探讨了 Ansible Galaxy、该网站以及命令行工具。我们还讨论了 Ansible 的开发和发布周期,并了解了 Ansible 角色的概念。

我相信您会同意,Ansible Galaxy 提供了宝贵的社区服务,因为它允许用户共享日常任务的角色,并为用户提供了一种通过发布角色为 Ansible 社区做出贡献的方式。

然而,请小心使用。记得在生产环境中使用 Ansible Galaxy 的角色之前,检查代码并阅读 bug 跟踪器;毕竟,许多角色需要提升权限才能成功执行其任务。

正如本章所述,我们将在接下来的内容中创建自己的 Ansible 角色,并且随着 Ansible playbook 越来越复杂,会提供更多关于创建和使用角色的提示和建议。

在下一章中,我们将探讨更多的 Ansible 命令和工具,这些工具是 Ansible Core 的一部分。

进一步阅读

您可以在以下网站找到更多关于我们从 Ansible Galaxy 安装的两个额外角色以及官方文档的详细信息:

第三章:Ansible 命令

在开始编写和执行更复杂的 playbook 之前,我们将先了解 Ansible 的其他内建命令。在这里,我们将介绍构成 Ansible 的命令。在本章末,我们将安装一个第三方工具来可视化我们的主机清单。

本章将涉及以下主题:

  • 内建命令

  • 第三方命令

内建命令

当我们安装 Ansible 时,安装了几种不同的命令。它们如下所示:

  • ansible

  • ansible-config

  • ansible-console

  • ansible-doc

  • ansible-galaxy

  • ansible-inventory

  • ansible-playbook

  • ansible-pull

  • ansible-vault

我们已经在 第二章探索 Ansible Galaxy 中介绍了 ansible-galaxy 命令。在本书的剩余章节中,我们将继续深入探讨 ansible-playbook,因此本章将不再详细介绍该命令。让我们从列表的顶部开始,回顾一个我们已经使用过的命令。

Ansible

现在,你可能会认为 ansible 是我们在本书中最常用的命令,但事实并非如此。

ansible 命令仅用于对单个主机或一组主机执行临时命令。在 第一章安装与运行 Ansible 中,我们创建了一个目标为单个本地虚拟机的主机清单文件。

在本章的这一部分,我们将查看我在云服务提供商上运行的四个不同主机;我的主机文件如下所示:

ansible01 ansible_host=139.162.233.174
ansible02 ansible_host=139.162.233.227
ansible03 ansible_host=139.144.132.49
ansible04 ansible_host=139.144.132.71
[london]
ansible01
ansible02
[nyc]
ansible03
ansible04
[demohosts:children]
london
nyc
[demohosts:vars]
ansible_connection=ssh
ansible_user=root
ansible_private_key_file=~/.ssh/id_rsa
host_key_checking=False

如你所见,我有四个主机——ansible01 > ansible04。前两个主机在名为 london 的组中,后两个主机在名为 nyc 的组中。然后我将这两个组组合在一起,创建了一个名为 demohosts 的新组,并使用该组应用一些基于我启动的主机的基本配置。

使用 ping 模块,我可以通过运行以下命令来检查与主机的连接性。首先,让我们检查 london 中的两个主机:

$ ansible -I hosts london -m ping

这将返回以下结果:

图 3.1 – 执行 Ansible ping,目标为 london 主机

图 3.1 – 执行 Ansible ping,目标为 london 主机

现在,让我们运行相同的命令,但这次是针对 nyc 主机:

$ ansible -i hosts nyc -m ping

这给我们以下输出:

图 3.2 – 执行 Ansible ping,目标为 nyc 主机

图 3.2 – 执行 Ansible ping,目标为 nyc 主机

如你所见,我的四个主机都返回了 pong

我也可以通过添加 all 而不是特定主机组,来一次性针对所有四个主机:

$ ansible -i hosts all -m ping

现在,我们可以通过 Ansible 访问我们的主机,可以对其进行操作并运行一些临时命令;让我们从一些基础命令开始:

$ ansible -i hosts london -a "ping -c 3 google.com"

此命令将连接到 london 主机并运行 ping -c 3 google.com 命令;这将从主机对 google.com 域进行 ping 并返回结果:

图 3.3 – 对 google.com 执行 ping 命令

图 3.3 – 对 google.com 执行 ping 命令

我们还可以通过 ansible 命令运行单个模块;我们在第一章中曾经做过这种操作,安装和运行 Ansible,使用的是 setup 模块。然而,一个更好的例子是通过运行以下命令来更新所有主机上的所有已安装软件包:

$ ansible -i hosts all -m ansible.builtin.apt -a "name=* state=latest update_cache=ye"

如你所见,我们使用的是 ansible.builtin.apt 模块,我们在第一章中已经定义了该模块,安装和运行 Ansible

- ansible.builtin.apt:
    name:"*"
    state:"latest"
    update_cache:"true"

我传入了相同的选项,但这次我没有使用 YAML,而是将其格式化为键值对,这通常是你在命令行中传递给任何命令的格式:

图 3.4 – 使用 ansible.builtin.apt 模块更新所有软件包

图 3.4 – 使用 ansible.builtin.apt 模块更新所有软件包

如你所见,运行 Ansible 时的输出非常详细,并提供了反馈,准确地告诉我们在临时执行过程中做了什么。

让我们重新对所有主机运行命令,但这次只针对一个软件包,比如 ntp

$ ansible -i hosts all -m ansible.builtin.apt -a "pkg=ntp state=latest"

运行一次命令将会在我们的四个主机上安装该软件包:

图 3.5 – 使用 ansible.builtin.apt 模块安装 ntp 软件包

图 3.5 – 使用 ansible.builtin.apt 模块安装 ntp 软件包

现在,让我们重新运行该命令:

$ ansible -i hosts all -m ansible.builtin.apt -a "pkg=ntp state=latest"

运行一次命令将会在我们的四个主机上安装该软件包,并给出以下结果:

图 3.6 – 重新运行 ansible.builtin.apt 模块安装 ntp 软件包

图 3.6 – 重新运行 ansible.builtin.apt 模块安装 ntp 软件包

如你所见,主机返回了 SUCCESS 状态并且没有显示变化,这正是我们期望看到的。

那么,为什么要这样做呢?我们运行的两个命令有什么区别?

首先,让我们回顾一下在确认主机可用后,使用 Ansible ping 命令最初运行的两个命令:

$ ansible -i hosts london -a "ping -c 3 google.com"
$ ansible -i hosts all -m ansible.builtin.apt -a "name=* state=latest update_cache=true"

虽然看起来第一个命令并没有运行模块,但实际上它是运行了的。ansible 命令的默认模块叫做 raw,它会在每个目标主机上运行原始命令。命令中的 -a 部分将参数传递给模块。raw 模块接受原始命令,这正是我们在第二个命令中所做的。

如前所述,你可能已经注意到,当我们将命令传递给 ansible 命令和将其作为 YAML 剧本的一部分使用时,语法略有不同。我们这里只是将键值对直接传递给模块。

那么,为什么要像这样使用 Ansible 呢?嗯,它非常适合以极其受控的方式直接在非 Ansible 管理的主机上运行命令。

Ansible 使用 SSH 连接到主机,执行命令并告知你结果。只需小心——很容易变得过于自信,执行一些像下面这样的命令:

$ ansible -I hosts all -a "reboot now"

如果连接主机的用户具有执行命令的权限,它将执行你提供的命令。运行上面的命令将重启主机清单文件中的所有服务器:

图 3.7 – 使用单个命令重启所有四个主机

图 3.7 – 使用单个命令重启所有四个主机

所有主机的状态为 UNREACHABLE,因为 reboot 命令在返回 SUCCESS 状态之前终止了我们的 SSH 会话。不过,你可以通过运行 uptime 命令看到每台主机已经重启:

$ ansible -i hosts all -a "uptime"

以下截图显示了前面命令的输出:

图 3.8 – 检查四个主机的运行时间

图 3.8 – 检查四个主机的运行时间

重要

如前所述,并根据经验(这是个长故事),在使用 Ansible 管理主机时,使用临时命令要格外小心——它是一个强大但愚笨的工具,它会假设你知道运行这些命令对主机的后果。

这就是我们对 ansible 命令的介绍;接下来我们来看下一个命令,ansible-config

ansible-config 命令

ansible-config 命令用于管理 Ansible 配置文件。Ansible 附带了合理的默认设置,因此在这些设置之外几乎无需进行配置。你可以通过运行以下命令来查看当前配置:

$ ansible-config dump

从以下输出可以看到,所有绿色文本是默认配置,任何橙色的配置值是已更改的值:

图 3.9 – 将完整的 Ansible 配置输出到屏幕

图 3.9 – 将完整的 Ansible 配置输出到屏幕

运行以下命令将列出 Ansible 中每个配置选项的详细信息,包括该选项的作用、当前状态、引入时间、类型等:

$ ansible-config list

以下截图显示了前面命令的输出:

图 3.10 – 查看 Ansible 配置选项的详细信息

图 3.10 – 查看 Ansible 配置选项的详细信息

如果你有一个配置文件,比如在 ~/.ansible.cfg,你可以使用 -c--config 标志来加载它:

$ ansible-config view –-config "~/.ansible.cfg

上述命令将为你提供自定义配置文件的概览,并显示未在自定义配置文件中定义的 Ansible 默认值。

ansible-console 命令

Ansible 内置了一个控制台。它不是我在日常使用 Ansible 时经常用到的工具。要启动控制台,我们需要运行以下其中一个命令:

$ ansible-console -i hosts
$ ansible-console -i hosts london
$ ansible-console -i hosts nyc

三个命令中的第一个针对所有主机,而接下来的两个仅针对指定的组:

图 3.11 – 建立控制台连接

图 3.11 – 建立控制台连接

一旦连接成功,你将看到我已连接到 london 主机组,该组中有两台主机。在这里,你可以输入一个模块名称,例如 ping

图 3.12 – 从 Ansible 运行 ping

图 3.12 – 从 Ansible 运行 ping

另外,你可以使用 raw 模块;例如,你可以通过输入 ansible.builtin.raw uptime 来检查 uptime 命令:

图 3.13 – 使用 raw 模块运行 uptime 命令

图 3.13 – 使用 raw 模块运行 uptime 命令

你也可以使用与运行 ansible 命令时相同的语法来传递键值对——例如,在控制台提示符下运行以下命令:

ansible.builtin.apt pkg=ntp state=latest update_cache=true

它应该会给你类似以下的输出:

图 3.14 – 使用 ansible.builtin.apt 模块检查是否安装了 ntp 包

图 3.14 – 使用 ansible.builtin.apt 模块检查是否安装了 ntp 包

你可能已经注意到,这次我们运行的命令语法与我们在本章前面使用 ansible 命令运行同一个模块时略有不同。

那个命令如下:

$ ansible -i hosts london -m ansible.builtin.apt -a"pkg=ntp state=latest update_cache=true"

而这次,我们只运行了以下命令:

ansible.builtin.apt pkg=ntp state=latest update_cache=true

之所以如此,是因为当我们使用 ansible 命令调用模块时,我们是在本地机器的命令行上操作,因此需要通过 -m 标志传入模块名称,然后使用 -a 标志定义属性。之后,我们必须将键值对放入引号中,以避免破坏命令流,因为命令行中空格作为分隔符。

当我们运行 Ansible 控制台时,我们实际上已经执行了 ansible -i hosts london 命令的一部分,完全离开了本地命令行,直接与 Ansible 进行交互。

要离开控制台,输入 exit 以返回常规命令行 shell。

如本节开始时所提到的,ansible-console 命令是我不常使用的——主要是因为我在本章开始时讨论 ansible 命令时给出的警告。

使用 ansible-console 命令连接多个主机时,你必须 100% 确信你输入的命令是正确的。例如,虽然我只连接了两台主机,但我的 hosts 文件可能包含了 200 台主机。现在,假设我输入了错误的命令——如果它同时在 200 台主机上执行,可能会导致一些不想要的事情,比如同时重启所有主机。

要退出 ansible-console 会话,只需输入 exit 并按 Enter 键。

如你可能已经猜到的,这种情况发生在我身上。虽然不是 200 台主机,但也很可能是——所以请 小心。

ansible-inventory 命令

使用ansible-inventory命令可以为你提供主机清单文件的详细信息。这对于理解你的主机是如何分组的非常有帮助。例如,假设我运行以下命令:

$ ansible-inventory -i hosts–-graph

在与我在本节中一直使用的hosts清单文件相同的文件夹中,返回以下内容:

图 3.15 – 获取清单主机文件的概览

图 3.15 – 获取清单主机文件的概览

如你所见,它显示了从all开始的主机组,然后是主要的主机组(demohosts),接着是子组(londonnyc),最后是主机本身(ansible01 > ansible04)。

如果你想查看单个主机的配置,可以使用此命令:

$ ansible-inventory -i hosts –-host=ansible01

以下截图展示了前面命令的输出:

图 3.16 – 查看单个主机

图 3.16 – 查看单个主机

你可能已经注意到,它显示了主机继承的配置,这些配置来自我们为清单文件中所有主机设置的demohost主机组。你可以通过运行以下命令来查看每个主机和主机组的所有信息:

$ ansible-inventory -i hosts –-list

如果你有一个大型或复杂的主机清单文件,并且只需要某一台主机的信息,或者你接手了一个主机清单并希望更好地了解清单的结构,那么这个命令会很有帮助。我们将在本章后面讨论一个第三方工具,它提供更多显示选项。

什么是 ansible-pull?

ansible-console命令一样,ansible-pull不是我经常使用的命令;我可以用一只手来数出在过去几年中我使用它的次数。

ansible-pull是一个命令,允许目标机器从指定的源(例如 Git 仓库)拉取配置并应用于本地。这与典型的 Ansible 推送模型相反,后者是中央控制节点将配置推送到受管理节点。

ansible-pull命令的工作方式如下:

  1. 目标机器(即运行ansible-pull的机器)获取指定的仓库。

  2. 一旦仓库被拉取,目标机器会查找剧本。默认情况下,它会查找名为localhost.yml的剧本,但如果需要,你也可以指定其他剧本文件——请注意,这个文件不包括在示例文件中。

  3. 目标机器随后对自身运行该剧本。

ansible-pull有一些使用场景:

  • ansible-pull允许节点通过拉取配置来进行自我配置。

  • ansible-pull可以通过定时任务安排在特定的时间间隔内运行,确保主机在有连接时能够自我更新。

  • ansible-pull用来拉取并应用配置到本地开发环境,确保与生产配置的一致性。

运行 ansible-pull 有一些前提条件,其中最重要的是运行 ansible-pull 的主机必须有一个有效且活动的 Ansible 安装,并且安装任何执行剧本所需的其他依赖项。

总结来说,ansible-pull 提供了一种颠倒传统 Ansible 模型的方法,允许主机根据需要拉取配置,而不是像我们在第一章《安装和运行 Ansible》和第二章《探索 Ansible Galaxy》中所做的那样,由中央主机将配置推送给它们。本书的其余部分,我们将采用更传统的 Ansible 部署方式,将配置推送到目标主机。

然而,值得注意的是,如果由于某种原因你无法采用这种方法,那么你确实有一个替代方案,那就是 ansible-pull

使用 ansible-vault 命令

在 Ansible 中,可以从文件或剧本本身加载变量;我们将在下一章更详细地介绍这个内容。这些文件可能包含敏感信息,如密码和 API 密钥。以下是一个示例:

secret:"mypassword"
secret-api-key:"myprivateapikey"

正如你所看到的,我们有两个敏感信息以明文形式显示。这在文件位于本地机器时是可以的——嗯,差不多可以。但是,如果我们想将文件提交到源代码管理系统并与同事共享呢?

即使仓库 是私有的,我们也不应将这些信息以明文形式存储。

Ansible 引入了 Ansible Vault 来帮助解决这个问题。通过使用 ansible-vault 命令,我们可以加密文件或仅加密变量,然后在 Ansible 执行时,它可以在内存中解密,内容可以作为执行的一部分被读取。

注意

在本章剩余部分,我将设置 Vault 密码为 password,以便你在运行 ansible-vault 命令时可以针对 Chapter03/vault 文件夹中的文件进行操作。

要加密一个文件,我们需要运行以下命令,并提供一个密码,该密码将在提示时用于解密文件:

$ ansible-vault encrypt secrets.yml

以下截图显示了前述命令的输出:

图 3.17 – 使用 ansible-vault 加密整个文件

图 3.17 – 使用 ansible-vault 加密整个文件

从输出中可以看出,你将被要求确认密码。加密后,你的文件看起来会像这样:

$ANSIBLE_VAULT;1.1;AES256
62373138643038636664363166646637333131386431366137643630326433303231336331303262
3661383061313436653464663039626338376233646630310a306437666462313439636634646633
39653435333433326361306531393832613038353665333866383161313239343134376632316263
3736633665303161630a393265633066663631336239613938363130306262613633333030336430
66343833376532313866363838653464383065633737613735323739303232383031326262376366
61663136623431306363666330373831336230323132336263626237366539326162373564353937
303465306233633633303533633232623233

正如你所看到的,细节是使用文本编码的。这确保了我们的 secrets.yml 文件在提交到源代码管理系统(如 Git)时仍然可以正常工作。

你可以通过运行以下命令来查看文件内容:

$ ansible-vault view secrets.yml

这将要求你输入密码,并将文件内容打印到屏幕上:

图 3.18 – 使用 ansible-vault 加密整个文件

图 3.18 – 使用 ansible-vault 加密整个文件

你可以通过运行以下命令来解密磁盘上的文件:

$ ansible-vault decrypt secrets.yml

这将恢复文件到其未加密的原始状态。

重要提示

使用 ansible-vault decrypt 命令时,请不要将解密后的文件提交或检查到你的源代码控制系统中!

自 Ansible 2 版本初期发布以来,现已可以加密文件中的单个变量。让我们将更多变量添加到文件中:

username:"russmckendrick"
password:"mypassword"
secretapikey:"myprivateapikey"
packages:
  - apache2
  - ntp
  - git

如果我们不需要不断查看或解密文件以检查其变量名和整体内容,那就太好了。

让我们通过运行以下命令来加密密码内容:

$ ansible-vault encrypt_string'mypassword'–-name'password'

这将加密字符串 mypassword 并给它一个变量名 password

图 3.19 – 使用 ansible-vault 加密单个字符串

图 3.19 – 使用 ansible-vault 加密单个字符串

然后,我们可以将输出复制并粘贴到我们的文件中,并对 secretapikey 重复此过程:

$ ansible-vault encrypt_string 'myprivateapikey' –-name 'secretapikey'

这样,我们生成了两个秘密变量,并用它们替换了变量文件中未加密的内容。

注意

为了便于阅读,我对输出进行了简化 – 完整的文件可以在本书 GitHub 仓库中的 Chapter03/vault 文件夹找到。

我们的变量文件应该最终看起来像这样:

username:"russmckendrick"
password: !vault |
          $ANSIBLE_VAULT;1.1;AES256
30393463363733386333636536663832383565346335393030643435316132363437643261383837
          3035
secretapikey: !vault |
          $ANSIBLE_VAULT;1.1;AES256
38663133393834646638663632353634343638626237333438336131653862373761666539326263
          3934
packages:
  - apache2
  - ntp
  - git

如你所见,这样更容易阅读,且与加密文件同样安全。

到目前为止,一切顺利,但如何在 Ansible playbook 中使用 Ansible Vault 加密的数据呢?

在我们学习如何做之前,让我们先看看当你没有告诉 ansible-playbook 命令你正在使用 Ansible Vault 时会发生什么。运行以下 playbook,你会看到它加载了 myvars.yml 文件,并使用 ansible.builtin.debug 模块将变量内容打印到屏幕上:

---
- name: "Print some secrets"
  hosts: "localhost"
  vars_files:
    - "myvars.yml"
  tasks:
    - name: "Print the vault content"
      ansible.builtin.debug:
        msg:
          - "The username is {{ username }} and password is {{ password }}, also the API key is {{ secretapikey }}"
          - "I am going to install {{ packages }}"

我们可以使用以下命令运行 playbook;请注意,由于它只是本地运行,因此我们不会传递库存文件。这是它会给你警告的地方:

$ ansible-playbook playbook01.yml

这将导致终端输出中显示错误消息:

图 3.20 – 运行  命令时出现错误

图 3.20 – 运行 ansible-playbook 命令时出现错误

如你所见,它在抱怨它在其中一个文件中发现了 Vault 加密数据,但我们没有提供解锁它的秘密。

我们可以通过在 ansible-playbook 运行时将 Vault 密码放入文本文件中,并让 ansible-playbook 命令读取该文件的内容,来传递 Vault 密码。

如本节开始时提到的,我一直在使用密码 password 对我的 Vault 进行编码。我们将密码放入文件中,然后用它来解锁我们的 Vault:

$ echo "password" > /tmp/vault-file

运行以下命令将读取 /tmp/vault-file 的内容并解密数据:

$ ansible-playbook --vault-id /tmp/vault-file playbook01.yml

如你所见,以下是播放书执行后的结果,输出现在符合我们的预期:

图 3.21 – 运行 ansible-playbook 并通过文件传递 Vault 密码

图 3.21 – 运行 ansible-playbook 并通过文件传递 Vault 密码

如果您更喜欢在提示时输入密码,可以使用以下命令:

$ ansible-playbook --vault-id @prompt playbook01.yml

以下输出显示了提示:

图 3.22 – 运行 ansible-playbook 并通过提示输入密码

图 3.22 – 运行 ansible-playbook 并通过提示输入密码

你可能会问,为什么有两个不同的选项?当提示时,直接运行命令并输入密码似乎就足够了。

然而,在使用我们将在第十五章中介绍的服务时,使用 Ansible 与 GitHub Actions 和 Azure DevOps,命令需要完全无人值守地运行,因为在运行时不会有活动终端让您输入密码。

另一个在第十五章使用 Ansible 与 GitHub Actions 和 Azure DevOps,和第十六章介绍 Ansible AWX 和 Red Hat Ansible 自动化平台中会看到的优点是,通过抽象化运行时输入凭据的需求,完全可以让某人运行一个管道,而不需要知道或访问任何存储在剧本中的机密或解锁它们的凭据。

第三方命令

在我们结束查看各种 Ansible 命令之前,让我们来看一个命令,它不是 Ansible 本身的一部分,而是一个第三方开源项目。

ansible-inventory-grapher 命令

ansible-inventory-grapher命令,由 Will Thames 编写,使用 Graphviz 库可视化您的主机清单。我们需要做的第一件事是安装 Graphviz。要使用 Homebrew 在 macOS 上安装它,请运行以下命令:

$ brew install graphviz

要在 Ubuntu 上安装 Graphviz,请使用以下命令:

$ sudo apt-get install graphviz

安装后,您可以使用pip安装ansible-inventory-grapher

$ pip install ansible-inventory-grapher

现在我们已经安装好了所有内容,可以使用本章早些时候使用的hosts文件生成图表:

$ ansible-inventory-grapher -i hosts demohosts

这将生成如下所示的内容:

图 3.23 – 对我们的主机文件运行 ansible-inventory-grapher

图 3.23 – 对我们的主机文件运行 ansible-inventory-grapher

这是图形的原始输出。如您所见,它类似并使用了与 HTML 相同的一些语法。我们可以使用dot命令来呈现它,dot命令作为 Graphviz 的一部分一起提供。dot命令从图形中创建层次结构图。要执行此操作,请运行以下命令:

$ ansible-inventory-grapher -i hosts demohosts | dot -Tpng > hosts.png

这将生成一个名为hosts.png的 PNG 文件,其中包含您可以在此处看到的主机清单文件的可视化:

图 3.24 – 通过 Graphviz 传递我们 ansible-inventory-grapher 输出的结果

图 3.24 – 通过 Graphviz 传递我们 ansible-inventory-grapher 输出的结果

如你所见,这是 Ansible 目标主机的一个很好的展示;它非常适合用在文档中,同时也能让你了解复杂的清单文件是如何结构化的。

总结

在本章中,我们简要介绍了作为标准 Ansible 安装一部分的几个支持工具,以及一个旨在与 Ansible 配合使用的有用第三方工具。

我们将在后续章节中使用这些命令,以及我们有意跳过的 ansible-playbook

在下一章,我们将编写一个更复杂的 playbook,安装一个基本的 LAMP 堆栈到本地虚拟机上。

深入阅读

你可以在以下网址找到本章涉及的每个工具的文档:

第二部分:部署应用程序

现在你已经理解了 Ansible 的基础知识,接下来是时候将这些知识付诸实践了。在这一部分,我们将重点讲解如何使用 Ansible playbook 部署应用程序。从设置 LAMP 堆栈到部署 WordPress,再到针对多个发行版的操作,你将获得实际操作经验,自动化应用程序的部署。我们还将探索 Ansible 如何管理基于 Windows 的服务器,扩展你的自动化能力。

这一部分包含以下章节:

  • 第四章, 部署 LAMP 堆栈

  • 第五章, 部署 WordPress

  • 第六章, 目标多个发行版

  • 第七章Ansible Windows 模块

第四章:部署 LAMP 堆栈

本章将介绍如何使用 Ansible 提供的各种核心模块来部署完整的 LAMP 堆栈。我们将以在 第一章 中首次使用的本地 Multipass 虚拟机为目标,安装和运行 Ansible

我们将讨论以下内容:

  • 剧本布局 – 我们的剧本将如何构建

  • Linux – 准备 Linux 服务器

  • Apache – 安装和配置 Apache

  • MariaDB – 安装和配置 MariaDB

  • PHP – 安装和配置 PHP

本章将涵盖以下主题:

  • 剧本结构

  • LAMP 堆栈

  • LAMP 剧本

在开始编写剧本之前,我们将在简要讨论本章所需内容后,讨论我们将使用的结构。

技术要求

我们将再次使用在前几章中启动的本地 Multipass 虚拟机。由于我们将把 LAMP 堆栈的所有元素安装到虚拟机上,因此你的 Multipass 虚拟机需要能够从互联网下载软件包;总共有大约 500 MB 的软件包和配置需要下载。

你可以在本书随附的仓库中找到剧本的完整副本,链接地址为 github.com/PacktPublishing/Learn-Ansible-Second-Edition/tree/main/Chapter04

剧本结构

第一章安装和运行 Ansible 中,我们运行的剧本尽可能简单。它们都在一个文件中,附带一个主机清单文件,并且如果需要,还有一个模板文件。然后在 第二章探索 Ansible Galaxy 中,我们扩展了我们的剧本文件,开始使用角色,而不是将所有的任务、处理器和变量放在一个文件中。

从下面的布局可以看到,包含了几个文件夹和文件:

图 4.1 – 我们将使用的剧本文件夹结构

图 4.1 – 我们将使用的剧本文件夹结构

虽然仓库中有结构的副本,但我们还是先创建结构,并在创建过程中讨论每个项目。我们需要创建的第一个文件夹是顶层文件夹。这个文件夹将包含我们的剧本文件夹和文件:

$ mkdir Chapter04
$ cd Chapter04

我们将创建的下一个文件夹名为 group_vars。它将包含在我们的剧本中使用的变量文件。现在,我们将创建一个名为 common.yml 的变量文件:

$ mkdir group_vars
$ touch group_vars/common.yml

接下来,我们将创建两个文件 – 我们的主机清单文件,将命名为 hosts,以及我们的主剧本,通常命名为 site.yml

$ touch production
$ touch site.yml

我们将手动创建的最终文件夹叫做roles。在这里,我们将使用我们在第二章《探索 Ansible Galaxy》中学到的ansible-galaxy命令来创建一个名为common的角色。为此,我们使用以下命令:

$ mkdir roles
$ ansible-galaxy role init roles/common

这应该会创建开始编写common角色所需的所有文件。

cloud-init.yamlexample_keyexample_key.pubhosts.example 文件均直接来自第一章《安装与运行 Ansible》和第二章《探索 Ansible Galaxy》,因此我们在本章中不再覆盖它们。

注意

虽然我们将在本节和接下来的各节中逐个处理每个文件,但整个剧本的完整副本可以在随附的 GitHub 仓库中找到。

让我们看看剧本中的四个角色,并安装和配置我们的 LAMP 堆栈。

LAMP 堆栈

LAMP 堆栈是一个用来描述一体化 Web 和数据库服务器的术语。通常,组件如下:

  • Linux是底层操作系统;在我们的案例中,我们将使用 Ubuntu 22.04。

  • Apache是堆栈中的 Web 服务器元素。

  • MariaDB是我们将用作堆栈数据库组件的数据库;通常,它基于MySQL,也可以使用 MySQL。

  • PHP是 Web 服务器用于生成内容的动态语言。

LAMP堆栈的常见变种被称为LEMP;它用NGINX代替了Apache,NGINX 的发音为engine-x,因此是E而不是N

我们将创建角色来处理这些组件;它们如下所示:

  • common:该角色将为我们的 Ubuntu 服务器做准备,安装我们所需的任何支持包和服务。

  • apache:该角色将安装 Apache Web 服务器并配置默认虚拟主机。

  • mariadb:该角色不仅会安装 MariaDB,还会确保安装安全,并创建默认数据库和用户,还可以选择下载并导入用于测试的数据库。

  • php:该角色将安装 PHP 并配置一组常见的 PHP 模块,如果我们将选项设置为一个用 PHP 编写的数据库管理工具,我们可以通过浏览器与我们的测试数据库进行交互。

让我们首先来看一下common角色。

公共角色

在本章的前一部分中,我们使用ansible-galaxy role init命令创建了common角色。它创建了几个文件夹和文件;正如在第二章《探索 Ansible Galaxy》中讨论的那样,我们不会在这里深入探讨,而是直接进入角色本身。

让我们开始通过添加一些任务来进行。

更新已安装的软件包。

首先,让我们通过将以下内容添加到roles/common/tasks/main.yml文件的开头来更新我们的服务器:

- name: "Update apt cache and upgrade packages"
  ansible.builtin.apt:
    name: "*"
    state: "latest"
    update_cache: true

你会注意到与我们上次使用ansible.builtin.apt模块更新所有已安装的软件包时的情况有所不同。

现在我们通过name键启动任务;当 Playbook 运行时,这将打印出我们赋值给name键的内容,这样可以让我们更清楚地了解 Playbook 运行过程中发生的事情,而不仅仅是打印执行的模块名称。

安装常用软件包

现在我们已经更新了已安装的软件包,让我们安装所有 Playbook 将要针对的 Linux 服务器上需要安装的软件包:

- name: "Install common packages"
  ansible.builtin.apt:
    state: "present"
    pkg: "{{ common_packages }}"

如你所见,我们再次使用了ansible.builtin.apt模块,并且为任务添加了一个描述性名称。我们没有在任务中提供一个包列表,而是使用了一个名为common_packages的变量,该变量在roles/common/defaults/main.yml文件中定义如下:

common_packages:
  - "ntp"
  - "sntp"
  - "ntp-doc"
  - "vim"
  - "git"
  - "unzip"

如你所见,我们安装了ntpsntpntp-doc;我们将很快配置ntp。接下来,我们安装了vimgitunzip,因为它们在服务器上安装总是非常有用。

你可能还注意到,我们通过{{ common_packages }}将包列表传递给ansible.builtin.apt模块的pkg键,这样模块会遍历我们传入的包列表,并一次性安装它们,而无需调用模块逐个安装每个包。

配置网络时间协议(NTP)

接下来,我们从templates文件夹中复制ntp.conf文件,像前几章一样添加 NTP 服务器列表,并告知 Ansible 每当配置文件更改时重新启动 NTP。

创建密钥、组和用户

roles/common/defaults/main.yml文件中,定义了以下变量:

users:
  - {
      name: "lamp",
      group: "lamp",
      state: "present",
      key: "/tmp/id_ssh_lamp_rsa",
    }

这与我们迄今为止使用的变量略有不同,因为这是一个名为users的单一变量,它由一个项组成,而该项包含namegroupstatekey键值对。

因为我们使用了项,我们需要改变在任务中使用变量的方式,第一个在roles/common/tasks/main.yml中的任务是创建 OpenSSH 密钥对;如果尚未存在密钥,我们需要将其保存在key键值对中定义的路径下:

- name: "Generate a ssh keypair"
  community.crypto.openssh_keypair:
    path: "{{ item.key }}"
  with_items: "{{ users }}"
  delegate_to: "localhost"
  become: false

在执行任务时,你可以看到我们使用了community.crypto.openssh_keypair模块,在其中我们只传递了一个值,即我们希望存储 OpenSSH 密钥的文件路径。

如你所见,我们使用了{{ item.key }}变量来输入路径,但这里并没有定义该变量的名称是users;相反,我们使用了with_items选项,并在这里传入了{{ users }}变量。

虽然在这个例子中我们只传递了一个项目,但你也可以采用这种方法来多次执行单个任务——例如,如果我们的变量看起来像这样:

users:
  - {
      name: "lamp",
      group: "lamp",
      state: "present",
      key: "/tmp/id_ssh_lamp_rsa",
    }
  - {
      name: "user2",
      group: "lamp",
      state: "present",
      key: "/tmp/id_ssh_user2_rsa",
    }

然后,当任务执行时,它会创建两个 OpenSSH 密钥,随后的任务(稍后我们会讲解)会创建一个名为lamp的组,然后创建两个用户,lampuser2

回到当前任务–你会注意到我们定义了另外两个选项,delegate_tobecome

如果我们在没有定义delegate_to的情况下运行community.crypto.openssh_keypair模块,那么该模块将在远程主机上执行,而这并不是我们希望发生的情况,因为我们希望 OpenSSH 密钥的私钥和公钥部分保存在本地机器上。因此,通过在delegate_to选项中使用localhost作为值,我们告诉 Ansible 在本地执行此任务。

下一个选项become告诉 Ansible 不要使用sudo命令提升为超级用户,这也是我们在主site.yml剧本文件顶部定义的所有主机的默认行为–这是因为我们希望community.crypto.openssh_keypair模块作为你登录的用户运行,而不是本地机器的 root 用户。

该任务的逻辑(不包括delegate_tobecome选项,因为我们希望剩余的任务在目标机器上执行)会延续到角色中的剩余任务,首先通过执行ansible.builtin.group模块来创建组:

- name: "Add group for our users"
  ansible.builtin.group:
    name: "{{ item.group }}"
    state: "{{ item.state }}"
  with_items: "{{ users }}"

一旦组创建完成,我们可以通过ansible.builtin.user来添加用户,或者如果我们在users变量中定义了多个项,则可以添加多个用户:

- name: "Add users to our group"
  ansible.builtin.user:
    name: "{{ item.name }}"
    group: "{{ item.group }}"
    comment: "{{ item.name }}"
    state: "{{ item.state }}"
  with_items: "{{ users }}"

角色中的最后一个任务将我们之前生成的 OpenSSH 密钥的公钥部分,添加到先前任务中创建的用户(或用户们)中,使用ansible.builtin.authorized_key模块。

- name: "Add keys to our users"
  ansible.posix.authorized_key:
    user: "{{ item.name }}"
    key: "{{ lookup('file', item.key + '.pub') }}"
  with_items: "{{ users }}"

你可能已经注意到我们传递给key选项的值对我们来说是新的;它使用lookup插件读取路径item.key下的文件内容,并在文件名末尾附加.pub,这意味着在我们的情况下,它读取/tmp/id_ssh_lamp_rsa.pub文件的内容。这个文件是 OpenSSH 密钥对的公钥部分,它是在我们之前执行"生成 ssh 密钥对"任务时创建的。

lookup插件设计为在本地执行,因此在这种情况下,我们不需要使用delegate_tobecome选项,因为我们希望任务在目标主机上执行,因为用户已经在该主机上创建,但我们希望将本地主机上的/tmp/id_ssh_lamp_rsa.pub文件的内容填充到远程主机的/home/lamp/.ssh/authorized_key文件中。

这就完成了common角色中的任务;在我们继续下一个角色(安装和配置apache)之前,你需要知道一件事。

"生成 SSH 密钥对"任务在执行时不会覆盖任何现有的密钥对,这意味着当你第一次运行此角色并且/tmp/id_ssh_lamp_rsa/tmp/id_ssh_lamp_rsa.pub这两个文件不存在时,密钥对将被创建;在随后的 Playbook 执行中,由于这些文件已经存在,任务将返回community.crypto.openssh_keypair模块来创建密钥对。

Apache 角色

一旦common角色在我们的远程主机上运行完毕,我们就准备好安装和配置 Apache Web 服务器了。

安装 Apache 软件包

roles/apache/tasks/main.yml中的第一个任务安装运行 Apache Web 服务器所需的软件包;它使用ansible.builtin.apt模块,内容如下:

- name: "Install apache packages"
  ansible.builtin.apt:
    state: "present"
    pkg: "{{ apache_packages }}"

如你所见,它调用了一个名为{{ apache_packages }}的变量,该变量在roles/apache/defaults/main.yml中定义,如下所示:

apache_packages:
  - "apache2"
  - "apache2-ssl-dev"
  - "ca-certificates"
  - "openssl"

正如我们在走过common角色时所学到的,这将安装在变量中定义的四个软件包。

一旦安装了 Apache(这是一个单独的任务),我们就可以继续配置 Apache 安装了。

配置 Apache

配置 Apache 时的第一个任务是将common角色运行时创建的用户添加到 Apache 组中;为此,我们运行以下任务:

- name: "Add user to apache group"
  ansible.builtin.user:
    name: "{{ item.name }}"
    groups: "{{ apache_group }}"
    append: true
  with_items: "{{ users }}"

该任务取用了前一个角色中的{{ users }}变量,并循环遍历变量中定义的项目,将用户添加到roles/apache/defaults/main.yml文件中{{ apache_group }}变量定义的组中。以下是配置 Apache 时使用的变量的完整列表,这些变量将被我们在接下来的任务中使用:

apache_group: "www-data"
web_root: "web"
document_root: "/home/{{ users.0.name }}/{{ web_root }}"
index_file: index.html
vhost_path: "/etc/apache2/sites-enabled/"
vhost_default_file: "000-default.conf"
vhost_our_file: "vhost.conf"

你可能已经注意到,document_root变量的值与我们之前使用的有所不同;稍后我们会进一步讨论这一点。

下一个任务将在用户目录中创建一个文件夹,我们将使用它来存储通过 Apache 提供的文件:

- name: "Create the document root for our website"
  ansible.builtin.file:
    dest: "{{ document_root }}"
    state: directory
    mode: "0755"
    owner: "{{ users.0.name }}"
    group: "{{ apache_group }}"

如你所见,我们像为document_root变量值那样使用{{ users.0.name }};这是为什么呢?

正如我们所知,common角色只创建了一个用户;我们不能简单地使用{{ users.name }},因为name键存在于变量中的某一项内,所以使用{{ users.name }}会导致错误,提示找不到该变量。

因此,我们可以通过使用其在列表中的位置来引用列表中的第一个项,因为 Ansible 从零开始计数,所以它的位置是0而不是1

使用我们在commonapache角色的默认值中定义的值,该任务将在/home/lamp/web/创建一个文件夹;lamp用户将拥有该文件夹,并且该文件夹会分配给www-data组,这是 Apache 进程运行时所使用的组。

下一个任务将确保在/home/lamp/文件夹上设置正确的读取、写入和执行权限:

- name: "Set the permissions on the user folder"
  ansible.builtin.file:
    dest: /home/{{ users.0.name }}/
    state: directory
    mode: "0755"
    owner: "{{ users.0.name }}"

该任务完成了配置我们网页所需的文件夹结构;现在,是时候配置 Apache 了。

我们需要做的第一件事是删除默认的虚拟主机配置文件;为此,我们将执行以下任务:

- name: "Remove the apache default vhost config"
  ansible.builtin.file:
    path: "{{ vhost_path }}{{ vhost_default_file }}"
    state: absent
  notify: "Restart apache2"

这使用 ansible.builtin.file 模块将 {{ vhost_default_file }}{{ vhost_path }} 文件夹中的文件状态设置为 absent,意味着如果该文件存在,将把它删除。

它还使用 notify 来调用 "Restart apache2" 处理程序,该处理程序在 roles/apache/handlers/main.yml 文件中定义,任务如下所示:

- name: "Restart apache2"
  ansible.builtin.service:
    name: "apache2"
    state: "restarted"
    enabled: true

删除默认文件后,我们可以添加我们的虚拟主机配置文件。

该虚拟主机配置文件的模板可以在 roles/apache/templates/vhost.conf.j2 找到,内容如下:

# {{ ansible_managed }}
<VirtualHost *:80>
  ServerName {{ ansible_hostname }}
  DocumentRoot {{ document_root }}
  DirectoryIndex {{ index_file }}
  <Directory {{ document_root }}>
    AllowOverride All
    Require all granted
  </Directory>
</VirtualHost>

加载后,该配置文件会在有人访问网站 URL 时,提供 {{ document_root }} 文件夹中的内容。

部署该模板文件到远程主机的任务如下所示:

- name: "Copy the our vhost.conf to the sites-enabled folder"
  ansible.builtin.template:
    src: vhost.conf.j2
    dest: "{{ vhost_path }}{{ vhost_our_file }}"
    mode: "0644"
  notify: "Restart apache2"

如您所见,如果文件有任何更改,它还会调用 "Restart apache2" 处理程序。

现在 Apache 已经配置好,剩下一个最终任务。

可选地复制一个 index.html 文件

该角色中的最终任务使用以下variables块:

html_deploy: true
html_heading: "Success !!!"
html_body: |
  This HTML page has been deployed using Ansible to <b>{{ ansible_host }}</b>.<br>
  The user is <b>{{ users.0.name }}</b> who is in the <b>{{ apache_group }}</b> group.<br>
  The weboot is <b>{{ document_root }}</b>, the default index file is <b>{{ index_file }}</b>.<br>

如您所见,它包含了一个标题和一些 HTML 代码,供后续任务使用:

- name: "Copy the test HTML page to the document root"
  ansible.builtin.template:
    src: index.html.j2
    dest: "{{ document_root }}/index.html"
    mode: "0644"
    owner: "{{ users.0.name }}"
    group: "{{ apache_group }}"
  when: html_deploy

这使用一个可以在 roles/apache/templates/index.html.j2 找到的模板,内容如下所示:

<!--{{ ansible_managed }}-->
<!doctype html>
<title>{{ html_heading }}</title>
<style>
  body { text-align: center; padding: 150px; }
  h1 { font-size: 50px; }
  body { font: 20px Helvetica, sans-serif; color: #333; }
  article { display: block; text-align: left; width: 650px; margin: 0 auto; }
</style>
<article>
    <h1>{{ html_heading }}</h1>
    <div>
        <p>{{ html_body }}</p>
    </div>
</article>

然而,只有在 html_deploy 变量设置为 true 时,才会调用该任务;这由任务结尾的以下语句管理:

when: html_deploy

因此,如果出于任何原因,html_deploy 变量不等于 true,那么在执行 playbook 时该任务将被跳过。

这就是我们安装和配置 Apache 所需要做的全部工作;现在让我们来看看安装 LAMP 中的 M,并审视安装和配置 MariaDB 的角色。

MariaDB 角色

在本章介绍的四个角色中,MariaDB 角色是最复杂的,因为它安装 MariaDB、配置它,并可选地下载并导入一个示例数据库。

让我们从安装开始讲解。

安装 MariaDB

您可能已经开始发现角色中的趋势:任务通常从安装几个软件包开始,而 MariaDB 也不例外。

来自 roles/mariadb/tasks/main.yml 的任务如下:

- name: "Install mariadb packages"
  ansible.builtin.apt:
    state: "present"
    pkg: "{{ mariadb_packages }}"

roles/mariadb/defaults/main.yml 中的 mariadb_packages 变量如下所示:

mariadb_packages:
  - "mariadb-server"
  - "mariadb-client"
  - "python3-pymysql"

如您所见,我们安装了 MariaDB 客户端和服务器。同时,我们安装了 python3-pymysql 包;这是与 MariaDB 交互的任务所必需的,一旦 MariaDB 安装完成才能正常工作。如果没有它,Ansible 将无法与我们的 MariaDB 服务器建立连接并进行交互。

一旦软件包安装完成,我们需要通过以下任务启动 MariaDB 服务器:

- name: "Start mariadb"
  ansible.builtin.service:
    name: mariadb
    state: started
    enabled: true

你可能在想,为什么我们没有像之前的任务一样使用处理程序(handler)?实际上,处理程序只有在剧本执行完成后,Ansible 知道所有需要重启的服务时才会被调用。

然而,在这种情况下,我们需要与 MariaDB 服务进行交互,以便在剧本运行时能够进行配置,因此我们没有使用处理程序,而是像处理程序一样在任务中启动该服务。

现在 MariaDB 已经安装并启动,我们可以开始配置。

配置 MariaDB

在我们开始任务之前,快速查看一下 roles/mariadb/defaults/main.yml 中的变量,这些变量将用于配置我们的 MariaDB 服务器:

mariadb_root_username: "root"
mariadb_root_password: "Pa55W0rd123"
mariadb_hosts:
  - "127.0.0.1"
  - "::1"
  - "{{ ansible_nodename }}"
  - "%"
  - "localhost"

现在我们知道将要使用哪些变量,是时候开始处理配置了,由于 MariaDB 在安装后立即启动时的默认配置方式,这个过程有点复杂。

默认情况下,MariaDB 启动时没有设置密码,这意味着任何人都可以作为 root 用户连接到数据库,这是不理想的,因此我们需要做的第一件事就是通过设置 root 密码来保护我们的安装。

听起来还挺简单的,你可能在心里这么想。

从技术上讲,是的;然而,如果剧本第二次运行时,也就是密码已经设置的情况下,我们接下来定义的任务将会出错,因为我们需要配置任务不要使用密码。一旦密码设置完成,服务器只会接受使用已设置密码的连接。

我们还需要考虑,一旦配置了密码,每次需要连接 MariaDB 服务器时,都需要使用该密码——所以我们需要一种简便的方式来确保密码设置完成后能够顺利连接。

幸运的是,MariaDB 和 MySQL 都内置了一个功能,可以让你将凭证存储在服务器上的一个文件中;这个文件应该放在你登录用户的主目录下。一旦文件放置好,每次你尝试以该用户连接数据库时,数据库客户端都会读取该文件并自动连接,而无需你输入凭证——这个文件应该被命名为 ~/.my.cnf~/ 部分是指用户的主文件夹)。

对于我们的场景来说,这种方式有效,因为我们可以检查 ~/.my.cnf 文件是否存在,如果文件不存在,就可以安全地假设密码尚未配置。

检查文件是否存在的任务如下:

- name: "Check to see if the ~/.my.cnf file exists"
  ansible.builtin.stat:
    path: ~/.my.cnf
  register: mycnf

这使用了 ansible.builtin.stat 模块来检查文件的存在性,然后使用 register 选项注册一个名为 mycnf 的运行时变量。

现在我们有了一个动态注册的变量,它包含了有关远程主机文件系统中 ~/.my.cnf 文件是否存在的详细信息,我们可以根据这个信息来更改密码,或者如果 ~/.my.cnf 文件存在,则跳过任务。

Ansible 有几个内置模块可与 MySQL 和 MariaDB 进行交互;我们在这里使用的是 ansible.builtin.mysql_user

- name: "Change mysql root password if we need to"
  community.mysql.mysql_user:
    name: "{{ mariadb_root_username }}"
    host: "{{ item }}"
    password: "{{ mariadb_root_password }}"
    check_implicit_admin: "yes"
    priv: "*.*:ALL,GRANT"
    login_user: "{{ mariadb_root_username }}"
    login_unix_socket: /var/run/mysqld/mysqld.sock
  with_items: "{{ mariadb_hosts }}"
  when: not mycnf.stat.exists

在任务中,我们指示 Ansible 将 {{ mariadb_root_username }} 变量中定义的用户的密码设置为 {{ mariadb_root_password }} 变量中存储的密码,从而授予该用户对所有数据库的完全管理员访问权限,这些数据库由 {{ mariadb_hosts }} 定义,并通过 with_items 函数进行循环。

当登录执行此操作时,Ansible 应使用 {{ mariadb_root_username }} 用户名并通过 Unix 套接字连接,该套接字位于 /var/run/mysqld/mysqld.sock;这意味着我们不需要建立网络连接来与数据库交互,因为如果这样做,Ansible 将无法连接,因为它无法发送空密码。

最后,只有在 mycnf.stat.exists 变量等于 false 时,才运行此任务。

现在我们已经设置了实际密码并确保了 MariaDB 安装的安全性,我们需要创建 ~/.my.cnf 文件以继续配置。

为了完成此操作,我们将再次使用模板,模板文件位于 roles/mariadb/templates/my.cnf.j2。该模板如下所示:

# {{ ansible_managed }}
[client]
user='{{ mariadb_root_username }}'
password='{{ mariadb_root_password }}'

如您所见,它包含连接到数据库服务器所需的用户名和密码。

由于文件包含凭证,当任务在服务器上创建文件时,我们需要确保该文件只能由 root 用户读取和写入,通过设置文件创建时的读取、写入和执行权限:

- name: "Set up ~/.my.cnf file"
  ansible.builtin.template:
    src: "my.cnf.j2"
    dest: "~/.my.cnf"
    mode: "0600"

现在我们在远程主机上有了 ~/.my.cnf 文件,我们可以继续进行 MariaDB 安装的安全配置;接下来的任务是移除 anonymous 用户,再次循环遍历该用户可能关联的主机:

- name: "Delete anonymous MySQL user"
  community.mysql.mysql_user:
    user: ""
    host: "{{ item }}"
    state: absent
  with_items: "{{ mariadb_hosts }}"

最终的任务是对我们的 MariaDB 安装进行安全处理,它会移除默认的 test 数据库:

- name: "Remove the MySQL test database"
  community.mysql.mysql_db:
    db: "test"
    state: "absent"

角色中的其余任务,如在 apache 角色中复制 index.html 文件,是可选的,我们现在来回顾这些任务。

下载和导入示例数据库

roles/mariadb/defaults/main.yml 中还有一块变量,它们用于下载和导入示例数据库。mariadb_sample_database 变量中包含许多键值,首先是启用选项的标志、要下载的文件的 URL,以及保存路径:

mariadb_sample_database:
  create_database: true
  source_url: "https://github.com/russmckendrick/test_db/archive/master.zip"
  path: "/tmp/test_db-master"

接下来,我们有示例数据库的名称以及用于新数据库的用户名和密码:

  db_name: "employees"
  db_user: "employees"
  db_password: "employees"

最后,是需要导入的文件列表。前两个文件包含架构:

  dump_files:
    - "employees.sql"
    - "show_elapsed.sql"

剩余的文件包含实际需要加载的数据:

    - "load_departments.dump"
    - "load_employees.dump"
    - "load_dept_emp.dump"
    - "load_dept_manager.dump"
    - "load_titles.dump"
    - "load_salaries1.dump"
    - "load_salaries2.dump"
    - "load_salaries3.dump"

现在我们知道了定义的变量,我们可以处理剩余的任务,第一个任务是下载并解压包含示例数据库文件的 ZIP 文件:

- name: "Download and unarchive the sample database data"
  ansible.builtin.unarchive:
    src: "{{ mariadb_sample_database.source_url }}"
    dest: /tmp
    remote_src: "yes"
  when: mariadb_sample_database.create_database

如你所见,ansible.builtin.unarchive 模块允许你下载并解压文件,这意味着我们可以在一个任务中完成所有操作。另外,只有当 mariadb_sample_database.create_databasetrue 时,when 任务才会执行。我们将在剩下的任务中继续这样做,甚至会在角色的最后扩展 when 条件。

下一步任务是创建示例数据库:

- name: "Create the sample database"
  community.mysql.mysql_db:
    db: "{{ mariadb_sample_database.db_name }}"
    state: present
  when: mariadb_sample_database.create_database

一旦数据库创建完成,我们可以运行一个任务,创建用户并分配权限,使其能够访问我们刚刚添加的数据库:

- name: "Create the user for the sample database"
  community.mysql.mysql_user:
    name: "{{ mariadb_sample_database.db_user }}"
    password: "{{ mariadb_sample_database.db_password }}"
    priv: "{{ mariadb_sample_database.db_name }}.*:ALL"
    state: present
  with_items: "{{ mariadb_hosts }}"
  when: mariadb_sample_database.create_database

现在我们只剩下最后两个任务,这里需要在我们的 playbook 中加入更多逻辑,确保我们只导入一次示例数据;如果没有这些逻辑,当 playbook 被重新运行时,可能会遇到各种问题,甚至有数据被覆盖或重复插入的风险,如果导入任务再次运行的话。

由于数据库存储在主机的文件系统中,我们可以使用与检查~/.my.cnf文件存在性时相同的逻辑,不过这次我们检查的是数据库文件:

- name: "Check to see if we need to import the sample database dumps"
  ansible.builtin.stat:
    path: /var/lib/mysql/{{ mariadb_sample_database.db_name }}/{{ mariadb_sample_database.db_name }}.frm
  register: db_imported
  when: mariadb_sample_database.create_database

我们注册了一个名为 db_imported 的变量,接下来会用它与下一个任务的 when 条件配合使用;这个任务会遍历 mariadb_sample_database.dump_files 并导入数据库:

- name: "Import the sample database"
  community.mysql.mysql_db:
    name: "{{ mariadb_sample_database.db_name }}"
    state: import
    target: "{{ mariadb_sample_database.path }}/{{ item }}"
  with_items: "{{ mariadb_sample_database.dump_files }}"
  when: db_imported is defined and not db_imported.stat.exists

我们稍微改变了 when 条件;这里不再引用 mariadb_sample_database.create_database,而是只使用 db_imported

第一个部分确保在我们决定不导入数据库时,playbook 不会出错,方法是将 mariadb_sample_database.create_database 设置为 false,因为只有当 mariadb_sample_database.create_database 设置为 true 时,db_imported 才能被定义,因为设置 db_imported 变量的任务只有在满足该条件时才会执行。

正如你所看到的,我们使用了 and,从而为 when 语句添加了第二个条件;这意味着该任务只有在同时满足 db_imported is definednot db_imported.stat.exists 时才会执行。

最后一个任务将带我们完成 MariaDB 角色的工作,接下来我们只剩一个角色要处理——PHP 角色。

PHP 角色

我们的这个最终角色安装 PHP,选项上可以附带一个 PHP 信息文件,并且安装一个用 PHP 编写的数据库管理界面,叫做 Adminer,这样我们就可以访问前一个角色中使用的数据库服务器。

安装 PHP 包

对你来说,PHP 角色中执行的第一个任务是安装我们运行 PHP 所需的包,应该不是什么惊讶的事。

所有包的完整列表在roles/php/default/main.yml文件中定义,内容如下:

php_packages:
  - "php"
  - "php-cli"
  - "php-curl"
  - "php-gd"
  - "php-intl"
  - "php-mbstring"
  - "php-mysql"
  - "php-soap"
  - "php-xml"
  - "php-xmlrpc"
  - "php-zip"
  - "libapache2-mod-php"

这个任务本身看起来很熟悉:

- name: "Install php packages"
  ansible.builtin.apt:
    state: "present"
    pkg: "{{ php_packages }}"
  notify: "Restart apache2"

需要注意的是,PHP 安装完毕后,我们重启 Apache,因为我们以 Apache 模块的形式运行 PHP。所以,一旦安装完成,Apache 需要重启以加载模块,并在 Apache 网络服务器上启用 PHP。

就是这样。PHP 已经安装,Apache 请求重启;从这里开始的一切都是可选的。

复制 PHP 信息文件

下一个任务是一个简单的任务,如果 php_info 变量设置为 true,则将 roles/php/files/info.php 复制到服务器的网页根目录:

- name: "Copy the PHP info to the document root"
  ansible.builtin.copy:
    src: info.php
    dest: "{{ document_root }}/info.php"
    mode: "0755"
    owner: "{{ users.0.name }}"
    group: "{{ apache_group }}"
  when: php_info

唯一的区别是我们通过这个任务将文件从本地主机复制到远程主机——这次我们不使用 ansible.builtin.template 模块,而是使用 ansible.builtin.copy 模块。因为 info.php 只有三行代码,这些代码不需要根据环境或我们设置的任何变量进行更新。

安装和配置 Adminer

roles/php/default/main.yml 文件中的其余任务的变量如下所示:

adminer:
  install: true
  path: "/usr/share/adminer"
  download: "https://github.com/vrana/adminer/releases/download/v4.8.1/adminer-4.8.1-mysql.php"

它们定义了从哪里下载文件,以及将文件下载到远程主机的哪个位置,这就是三项任务中的第一项,因为它会在远程虚拟机的文件系统上创建一个文件夹,以便我们下载 Adminer:

- name: "Create the document root for adminer"
  ansible.builtin.file:
    dest: "{{ adminer.path }}"
    state: directory
    mode: "0755"
  when: adminer.install

一旦我们创建了下载目标文件夹,就可以下载 Adminer 本身:

- name: "Download adminer"
  ansible.builtin.get_url:
    url: "{{ adminer.download }}"
    dest: "{{ adminer.path }}/index.php"
    mode: "0755"
  when: adminer.install

正如您可能从下载 URL 和目标位置中发现的,Adminer 是一个单独的 PHP 文件,我们将其保存为 index.php。那么,如何通过我们的 Apache web 服务器访问 Adminer 呢?

好的,为了做到这一点,我们需要再复制一个虚拟主机配置文件:

- name: "Copy the adminer.conf to sites-enabled folder"
  ansible.builtin.template:
    src: adminer.conf.j2
    dest: "{{ vhost_path }}adminer.conf"
    mode: "0755"
  when: adminer.install
  notify: "Restart apache2"

如您所见,它渲染并将 roles/php/templates/adminer.conf.j2 复制到 adminer.conf,即远程主机上的 site-enabled 文件夹,并指示 Apache 服务重新启动以加载新增的配置。

adminer.conf.j2 文件包含以下内容:

# {{ ansible_managed }}
Alias /adminer "{{ adminer.path }}"
  <Directory "{{ adminer.path }}">
    DirectoryIndex index.php
    AllowOverride All
    Require all granted
  </Directory>

这告诉 Apache,当有人访问 http://someurl/adminer/ 时,应该提供 Adminer 的 index.php 文件。

完成这个任务后,我们已经完成了安装和配置 LAMP 堆栈的四个角色的演示,现在是时候回顾并执行 playbook 本身了。

LAMP playbook

正如我们在本章开始时讨论 playbook 结构时提到的,主 playbook 文件名为 site.yml,其内容如下所示:

---
- name: "Install LAMP stack"
  hosts: ansible_hosts
  gather_facts: true
  become: true
  become_method: ansible.builtin.sudo
  vars_files:
    - group_vars/common.yml
  roles:
    - common
    - apache
    - mariadb
    - php

如您所见,它调用了我们已经走过的四个角色,并且加载了来自 group_vars/common.ymlvariables 文件;这个文件包含了 html_body 的重写配置,该配置在 roles/apache/defaults/main.yml 中配置,内容如下所示:

html_body: |
  This HTML page has been deployed using Ansible to <b>{{ ansible_nodename }}</b>.<br>
  The user is <b>{{ users.0.name }}</b> who is in the <b>{{ apache_group }}</b> group.<br>
  The weboot is <b>{{ document_root }}</b>, the default index file is <b>{{ index_file }}</b>.<br><br>
  You can access a <a href="/info.php">PHP Info file</a> or <a href="/adminer/">Adminer</a>.

这意味着当我们运行 playbook 时,index.hml 页面将包含指向 info.php/adminer URL 的链接,以便轻松访问附加内容。

注意

本书附带的 GitHub 仓库中的 Chapter04 文件夹包含了示例的 hosts 文件和密钥,用于使用 Multipass 启动本地虚拟机。如果您在跟随教程,请参考 第一章安装和运行 Ansible,了解如何启动虚拟机并准备自己的 hosts 文件。

所以,不再拖延,让我们运行 playbook:

$ ansible-playbook -i hosts site.yml

在第一次运行时,这应该会给我们输出如下内容:

PLAY [ansible_hosts]
TASK [Gathering Facts]
ok: [ansiblevm]
TASK [roles/common : update apt cache and upgrade packages]
ok: [ansiblevm]
…. lots of other output here ….
RUNNING HANDLER [roles/apache : restart apache2]
changed: [ansiblevm]
PLAY RECAP
ansiblevm : ok=34    changed=26    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

如你所见,Playbook 已对目标虚拟机进行了 26 次更改。

让我们再运行一次 playbook:

$ ansible-playbook -i hosts site.yml

然后,在 play recap 中,你应该会看到一些任务被跳过:

PLAY RECAP
ansiblevm         : ok=30    changed=0    unreachable=0    failed=0    skipped=2    rescued=0    ignored=0

如预期的那样,其中一项任务是更新数据库用户的 root 密码:

TASK [roles/mariadb : change mysql root password if we need to]
skipping: [ansiblevm] => (item=127.0.0.1)
skipping: [ansiblevm] => (item=::1)
skipping: [ansiblevm] => (item=ansiblevm)
skipping: [ansiblevm] => (item=%)
skipping: [ansiblevm] => (item=localhost)
skipping: [ansiblevm]

被跳过的第二个任务是导入数据库文件:

TASK [roles/mariadb : import the sample database]
skipping: [ansiblevm] => (item=employees.sql)
skipping: [ansiblevm] => (item=show_elapsed.sql)
skipping: [ansiblevm] => (item=load_departments.dump)
skipping: [ansiblevm] => (item=load_employees.dump)
skipping: [ansiblevm] => (item=load_dept_emp.dump)
skipping: [ansiblevm] => (item=load_dept_manager.dump)
skipping: [ansiblevm] => (item=load_titles.dump)
skipping: [ansiblevm] => (item=load_salaries1.dump)
skipping: [ansiblevm] => (item=load_salaries2.dump)
skipping: [ansiblevm] => (item=load_salaries3.dump)
skipping: [ansiblevm]

这两项都是预期的,因为我们是如此配置任务以便在后续的 Playbook 运行中响应。

现在,如果你打开浏览器,输入 http:// 然后是你的 Ansible 主机名称(对我来说是 192.168.64.20.nip.io;我怀疑你的地址会有所不同,所以这个链接可能无法使用),你应该会看到 Ansible 生成的 index.html 页面:

图 4.2 – 成功!!!– 查看 index.html 页面

图 4.2 – 成功!!!– 查看 index.html 页面

点击 PHP 信息文件的链接应会将你带到类似于 192.168.64.20.nip.io/info.php 的页面,该页面将显示你的 PHP 安装信息:

图 4.3 – 查看 PHP 信息页面

图 4.3 – 查看 PHP 信息页面

最终点击的链接是 Adminer 的链接;点击它会将你带到 192.168.64.20.nip.io/adminer/,并提示你登录:

图 4.4 – Adminer 登录页面

图 4.4 – Adminer 登录页面

要登录,请使用以下凭据:

  • root

  • Pa55W0rd123

  • employees

登录后,你将直接进入 employees 数据库的概览页面:

图 4.5 – 员工数据库概览

图 4.5 – 员工数据库概览

随意点击,完成后,确保终止 Multipass 虚拟机;如何终止的说明可以在 第一章 安装和运行 Ansible 章节的结尾找到。

总结

本章中,我们编写了一个 playbook,该 playbook 在我们的 Multipass 虚拟机上安装了 LAMP 堆栈。我们创建了四个角色,每个角色代表堆栈中的一个元素,在每个角色中,我们构建了一些逻辑,可以覆盖这些逻辑以部署额外的元素,例如测试 HTML 和 PHP 页面,还构建了创建包含超过 40,000 条记录的测试数据库的选项。

到目前为止,我们已经安装了一些基本的软件包。在下一章中,我们将编写一个 playbook,该 playbook 安装、配置并维护 WordPress 安装。

更新后的 playbook 将重用我们在本章中覆盖的一些元素,并进行一些改进,因为我们在本章中涉及的一些元素有些过于简单。最大的变化是,我们将不再使用硬编码的数据库实例密码。

深入阅读

你可以在以下网址找到本章中涉及的第三方工具的项目页面:

第五章:部署 WordPress

在上一章中,我们构建了一个安装和配置基本 LAMP 堆栈 的 playbook。在本章中,我们将在那里使用的技术基础上构建一个 playbook,用于安装 LEMP 堆栈,正如你可能记得的,它用 NGINX 替代了 Apache,然后安装 WordPress。

完成本章后,你应该能够做到以下几点:

  • 准备我们的初始 playbook

  • 下载并安装 WordPress CLI

  • 安装和配置 WordPress

  • 登录到你的 WordPress 安装

本章涉及以下主题:

  • 预安装任务

  • stack_install 角色

  • stack_config 角色

  • wordpress 角色

  • 运行 WordPress playbook

在我们开始之前,简要了解一下 WordPress;你可能在过去的 24 小时内访问过一个由 WordPress 提供支持的网站。

它是一个开源的 内容管理系统 (CMS),由 PHP 和 MySQL 提供支持,根据 2023 年 8 月 Colorlib 发布的统计数据,全球约有 8.1 亿个网站使用它,占所有网站的约 43%。

技术要求

就像在 第四章 部署 LAMP 堆栈 中一样,我们将继续使用之前在整个标题中使用的本地 Multipass 虚拟机。同样,在启动虚拟机并部署 WordPress 时,将下载额外的软件包。

你可以在本书附带的仓库中找到完整的 playbook 副本,地址是 github.com/PacktPublishing/Learn-Ansible-Second-Edition/tree/main/Chapter05

预安装任务

第四章 部署 LAMP 堆栈 中所提到的,LEMP 堆栈由以下元素组成:

  • Linux:在我们的案例中,这将是 Ubuntu Multipass 虚拟机

  • NGINX:如果你记得,它的发音是 engine-x,这意味着在 LEMP 中应该有一个 E,而不是 N(否则也无法作为缩写来发音)

  • MariaDB:正如我们已经看到的,这将是数据库组件

  • PHP:我们将再次使用 PHP 8 来进行配置

在安装 WordPress 之前,我们需要先安装和配置这些组件。而且,由于这个 playbook 最终将在公开的云服务器上执行,我们必须考虑一些关于 NGINX 配置的最佳实践。

然而,在我们开始查看 playbook 之前,让我们先设置 playbook 的初始结构:

$ mkdir Chapter05 Chapter05/group_vars Chapter05/roles
$ touch Chapter05/group_vars/common.yml Chapter05/hosts Chapter05/site.yml
$ cd Chapter05

这为我们提供了基本布局。接下来,我们必须从前面的章节中复制 cloud-init.yamlexample_keyexample_key.pubhosts.example 文件,这样当我们运行 playbook 时,我们就能拥有所有必要的文件来通过 Multipass 启动虚拟机。

现在我们已经配置好了基础设置,可以开始编写 playbook 来部署和配置我们的初始软件栈。

stack_install 角色

我们将从使用 ansible-galaxyrole init 创建一个名为 stack_install 的角色开始:

$ ansible-galaxy role init roles/stack_install

这将安装我们的初始软件栈。安装完成后,我们将其交给第二个角色,第二个角色将配置软件栈,之后第三个角色开始安装 WordPress。

那么,我们需要哪些软件包呢?WordPress 有以下要求:

  • PHP 7.4 或更高版本

  • MySQL 5.7 或更高版本,或 MariaDB 10.4 或更高版本

  • 带有 mod_rewrite 模块的 Nginx 或 Apache

  • 支持 HTTPS

从上一章我们知道,安装的 PHP 和 MariaDB 版本满足此要求,只剩下 NGINX,我们可以从主要的 NGINX 仓库下载并安装以获取最新的版本。

启用 NGINX 仓库

在我们查看启用主线 NGINX 仓库所需的任务和变量之前,让我们从 roles/stack_install/tasks/main.yml 文件开始,执行一个更新操作系统和可用软件包缓存的任务:

- name: "Update apt-cache and upgrade packages"
  ansible.builtin.apt:
    name: "*"
    state: "latest"
    update_cache: true

接下来的任务将会定义启用仓库的操作,最后我们将安装软件包。

接下来,在 roles/stack_install/default/main.yml 文件中,我们需要设置一些包含仓库信息的变量,并将它们与默认的 Ubuntu 仓库一起添加。

这些变量从一个包含仓库签名密钥 URL 的变量开始,该仓库将被启用:

repo_keys_url:
  - "http://nginx.org/keys/nginx_signing.key"

然后我们将添加以下仓库 URL:

repo_packages:
  - "deb http://nginx.org/packages/mainline/ubuntu/ {{ ansible_distribution_release }} nginx"
  - "deb-src http://nginx.org/packages/mainline/ubuntu/ {{ ansible_distribution_release }} nginx"

你可能已经注意到,我们正在使用 ansible_distribution_release 事实动态运行 URL,以正确地填入 Ubuntu 发行版的版本号。

现在,回到 roles/stack_install/tasks/main.yml 文件以及调用这些变量的两个任务——它们看起来如下,首先是添加签名密钥:

- name: "Add the apt keys from a URL"
  ansible.builtin.apt_key:
    url: "{{ item }}"
    state: "present"
  with_items: "{{ repo_keys_url }}"

如你所见,我们正在使用 with_items,所以,如果需要,你可以定义多个 URL 并添加额外的签名密钥。

这种方法被延续到了下一个任务,在那里我们将添加多个仓库:

- name: "Install the repo packages"
  ansible.builtin.apt_repository:
    repo: "{{ item }}"
    state: "present"
    update_cache: true
  with_items: "{{ repo_packages }}"

roles/stack_install/tasks/main.yml 文件中的最后一个任务是安装所有软件包的任务:

- name: "Update cache and install the stack packages"
  ansible.builtin.apt:
    state: "present"
    update_cache: true
    pkg: "{{ system_packages + extra_packages + stack_packages }}"

你会注意到,我没有将所有的软件包定义为一个变量,而是将它们分成了三个,并且在调用变量时通过 + 将它们合并。

那么,这三个变量包含了什么?为什么我们不将它们定义为一个变量?

回到 roles/stack_install/default/main.yml 文件,你可以看到 system_packages 定义如下:

system_packages:
  - "software-properties-common"
  - "python3-pymysql"
  - "acl"

紧接着,extra_packages 变量包含以下软件包列表:

extra_packages:
  - "vim"
  - "git"
  - "unzip"

最后,我们列出了组成我们软件栈主体的软件包:

stack_packages:
  - "nginx"
  - "mariadb-server"
  - "mariadb-client"
  - "php-cli"
  - "php-curl"
  - "php-fpm"
  - "php-gd"
  - "php-intl"
  - "php-mbstring"
  - "php-mysql"
  - "php-soap"
  - "php-xml"
  - "php-xmlrpc"
  - "php-zip"

由于我们为软件包定义了三个变量,这意味着如果需要,我们可以在剧本的其他地方覆盖它们。

例如,假设我们需要在虚拟机上安装 Amazon Web Services 命令行工具。

这将允许我们将数据(例如图片)推送到 Amazon S3 存储桶,或清除 CloudFront 内容分发网络的缓存。

我们可以不通过单一变量覆盖一长串软件包,而是将extra_packages变量添加到group_vars/common.yml中,并将其追加到软件包列表的末尾,这样它将变成如下所示:

extra_packages:
  - "vim"
  - "git"
  - "unzip"
  - "awscli"

如你所见,这比重复列出我们要安装的所有软件包要高效得多。

使用+来合并所有内容的另一个优点是,我们只需要调用一个ansible.builtin.apt任务来安装我们在接下来的角色中需要的所有内容,我们现在就来深入了解这个角色。

stack_config 角色

现在我们已经安装好了基础软件栈,我们需要对其进行配置,首先通过运行以下命令创建角色:

$ ansible-galaxy role init roles/stack_config

这为我们提供了stack_config角色所需的基本文件结构。有了这些结构后,我们现在可以开始配置角色本身——在这个角色中,我们需要执行以下操作:

  • 为我们的 WordPress 安装添加一个系统用户来运行

  • 根据 WordPress 文档中的最佳实践来配置 NGINX

  • 配置 PHP-FPM 以作为我们之前创建的 WordPress 用户运行

由于我们需要一个用户让 WordPress 运行,因此我们应当从这里开始。

WordPress 系统用户

应放置在roles/stackconfig/defaults/main.yml中的 WordPress 系统用户的默认值如下:

wordpress_system:
  user: "wordpress"
  group: "php-fpm"
  comment: "wordpress system user"
  home: "/var/www/wordpress"
  state: "present"

我们将其称为系统用户,因为我们稍后将在 WordPress 本身内创建一个用户。该用户的详细信息也将在 Ansible 中定义,因此我们不希望将这两个不同的用户搞混。

使用这些变量的两个任务可以在roles/stack_config/tasks/main.yml中找到,它们应该如下所示:

- name: "add the wordpress group"
  ansible.builtin.group:
    name: "{{ wordpress_system.group }}"
    state: "{{ wordpress_system.state }}"

前面的任务确保该组存在,接下来的任务将添加一个操作系统级别的用户,该用户会被加入到刚刚创建的组中:

- name: "Add the wordpress user"
  ansible.builtin.user:
    name: "{{ wordpress_system.user }}"
    group: "{{ wordpress_system.group }}"
    comment: "{{ wordpress_system.comment }}"
    home: "{{ wordpress_system.home }}"
    state: "{{ wordpress_system.state }}"

如你所见,我们这次没有为用户添加密钥,因为我们不想登录到用户账户去操作文件或执行其他操作。这些都应该在 WordPress 本身内完成,或通过 Ansible 来执行。

NGINX 配置

我们将使用多个模板文件来配置我们的 NGINX。第一个模板叫做roles/stack_config/templates/nginx-nginx.conf.j2,它将替换通过软件包安装部署的主要 NGINX 配置:

# {{ ansible_managed }}
user  nginx;
worker_processes  {{ ansible_processor_count }};
error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;
events {
    worker_connections  1024;
}
http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
    access_log  /var/log/nginx/access.log  main;
    sendfile        on;
    keepalive_timeout  65;
    client_max_body_size 20m;
    include /etc/nginx/conf.d/*.conf;
}

文件内容与将被替换的文件相同,唯一的不同之处在于我们正在更新worker_processes,使其使用在运行 setup 模块时 Ansible 检测到的处理器数量,而不是硬编码的值。

部署配置文件的任务正如你所预期的那样,它应该放在roles/stack_config/tasks/main.yml中:

- name: "Copy the nginx.conf to /etc/nginx/"
  ansible.builtin.template:
    src: nginx-nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    mode: "0644"
  notify: "Restart nginx"

如你所见,我们正在通知重启nginx处理器,该处理器存储在roles/stack_config/handlers/main.yml文件中:

- name: "Restart nginx"
  ansible.builtin.service:
    name: nginx
    state: restarted
    enabled: true

接下来,我们有默认的站点模板roles/stack_config/templates/nginx-confd-default.conf.j2

# {{ ansible_managed }}
upstream {{ php.upstream }} {
        server {{ php.ip }}:{{ php.port }};
}
server {
     listen       80;
     server_name  {{ ansible_nodename }};
     root         {{ wordpress_system.home }};
     index        index.php index.html index.htm;
    include global/restrictions.conf;
    include global/wordpress_shared.conf;
}

为了帮助确定模板文件将放置在目标主机上的位置,我将它们命名为文件名中包含完整路径。在这种情况下,文件名是nginx-confd-default.conf.j2,它将被部署到/etc/nginx/conf.d/default.conf;执行此操作的任务如下:

- name: "Copy the default.conf to /etc/nginx/conf.d/"
  ansible.builtin.template:
    src: nginx-confd-default.conf.j2
    dest: /etc/nginx/conf.d/default.conf
    mode: "0644"
  notify: "Restart nginx"

接下来我们要部署的两个文件将放到一个不存在的文件夹中。所以,我们首先需要创建目标文件夹。为此,我们需要在roles/stack_config/tasks/main.yml中添加以下内容:

- name: "Create the global directory in /etc/nginx/"
  ansible.builtin.file:
    dest: /etc/nginx/global/
    state: directory
    mode: "0644"

由于我们在nginx-global-restrictions.conf文件中没有进行任何替换,所以在这里我们使用ansible.builtin.copy模块,而不是ansible.builtin.template;该文件存储在roles/stack_config/files/中,复制它的任务如下:

- name: "Copy the restrictions.conf to /etc/nginx/global/"
  ansible.builtin.copy:
    src: nginx-global-restrictions.conf
    dest: /etc/nginx/global/restrictions.conf
    mode: "0644"
  notify: "Restart nginx"

这个文件中有一些合理的默认设置,比如拒绝访问作为 WordPress 安装一部分的文件:

location ~* /(wp-config.php|readme.html|license.txt|nginx.conf) {
    deny all;
}

另一个导入包含的配置是拒绝访问/wp-content/及其子文件夹中的.php文件:

location ~* ^/wp-content/.*.(php|phps)$ {
    deny all;
}

nginx-global-restrictions.conf文件中有其他一些配置;请参阅随书附带的仓库以获取完整配置,因为这里有太多的代码片段,我们无法一一讲解。

对于 NGINX 配置的下一个和最后一个块,可以说情况是一样的;请查看仓库,了解由以下任务部署的更多配置信息:

- name: "Copy the wordpress_shared.conf to /etc/nginx/global/"
  ansible.builtin.template:
    src: nginx-global-wordpress_shared.conf.j2
    dest: /etc/nginx/global/wordpress_shared.conf
    mode: "0644"
  notify: "Restart nginx"

当我们查看默认站点模板roles/stack_config/templates/nginx-confd-default.conf.j2时,你可能已经注意到使用了几个我们尚未定义的变量;它们是php.ipphp.port

正如你可能从变量标记中已经猜到的,这些与 PHP 的配置有关,因此,让我们来看看配置部署中 PHP 和 PHP-FPM 的部分。

PHP 和 PHP-FPM 配置

正如我们在上一节中看到的,roles/stack_config/defaults/main.yml中为 PHP 定义了几个变量,它们如下:

php:
  ip: "127.0.0.1"
  port: "9000"
  upstream: "php"
  ini:
    - { regexp: "^;date.timezone =", replace: "date.timezone = Europe/London" }
    - { regexp: "^expose_php = On", replace: "expose_php = Off" }
    - {
        regexp: "^upload_max_filesize = 2M",
        replace: "upload_max_filesize = 20M",
      }

然后,我们有一些变量定义了一些关于各种文件路径和服务名称的信息:

php_fpm_path: "/etc/php/8.1/fpm/pool.d/www.conf"
php_ini_path: "/etc/php/8.1/fpm/php.ini"
php_service_name: "php8.1-fpm"

我们将运行的两个任务中的第一个任务部署 PHP-FPM 配置;这是模板roles/stack_config/templates/php-fpmd-www.conf.j2的样子:

; {{ ansible_managed }}
[{{ wordpress_system.user }}]
user = {{ wordpress_system.user }}
group = {{ wordpress_system.group }}
listen = {{ php.ip }}:{{ php.port }}
listen.allowed_clients = {{ php.ip }}
pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 35
php_admin_value[error_log] = /var/log/php-fpm/{{ wordpress_system.user }}-error.log
php_admin_flag[log_errors] = on
php_value[session.save_handler] = files
php_value[session.save_path]    = /var/lib/php/fpm/session
php_value[soap.wsdl_cache_dir]  = /var/lib/php/fpm/wsdlcache

如你所见,我们在这个文件中有一些替换。从顶部开始,在方括号之间,我们定义了 PHP-FPM 池的名称,并使用wordpress_system.user变量的内容来进行此操作。

接下来,我们设置了希望我们的池运行的用户和组;在这里,我们使用wordpress_system.userwordpress_system.group

最后,我们通过使用php.ipphp.port变量来设置我们希望 PHP-FPM 池监听的 IP 地址和端口。

roles/stack_config/tasks/main.yml中用于部署模板的任务如下所示:

- name: "Copy the www.conf to /etc/php-fpm.d/"
  ansible.builtin.template:
    src: php-fpmd-www.conf.j2
    dest: "{{ php_fpm_path }}"
    mode: "0644"
  notify: "Restart php-fpm"

roles/stack_config/handlers/main.yml中,用于重启 PHP-FPM 的处理程序与我们在本书中之前定义的处理程序非常相似:

- name: "Restart php-fpm"
  ansible.builtin.service:
    name: "{{ php_service_name }}"
    state: restarted
    enabled: true

roles/stack_config/tasks/main.yml中的下一个任务使用了ansible.builtin.lineinfile模块:

- name: "Configure php.ini settings"
  ansible.builtin.lineinfile:
    dest: "{{ php_ini_path }}"
    regexp: "{{ item.regexp }}"
    line: "{{ item.replace }}"
    backup: "true"
    backrefs: "true"
  with_items: "{{ php.ini }}"
  notify: "Restart php-fpm"

我们正在读取php.ini文件,并通过查找regexp键定义的值来循环处理它。一旦找到该值,我们就用replace键的内容替换它。如果文件有变化,我们会先备份文件,以防万一。

此外,我们使用了backrefs来确保如果文件中没有匹配的regex,则文件保持不变;如果不使用它们,restart php-fpm处理程序将在每次运行剧本时被调用,而我们并不希望 PHP-FPM 在没有必要的情况下被重启。

启动 NGINX 和 PHP-FPM

现在,我们已经安装并配置好了 NGINX 和 PHP-FPM,我们需要启动这两个服务,而不是等到剧本运行结束再启动。

如果我们现在不设置密码,我们接下来的安装 WordPress 的角色会失败。roles/stackconfig/tasks/main.yml中的两个任务的第一个看起来像这样:

- name: "Start php-fpm"
  ansible.builtin.service:
    name: "{{ php_service_name }}"
    state: "started"

第二个任务看起来几乎是一样的:

- name: "Start nginx"
  ansible.builtin.service:
    name: "nginx"
    state: "started"

如果你看一下这两个任务,它们与我们之前定义的两个处理程序是一样的。

然而,如果你仔细看,会发现尽管我们使用了ansible.builtin.service模块,我们只将state设置为started而不是restarted,并且我们没有配置enabled,这个配置用于设置服务在启动时自启。

你可能注意到的另一件事是使用了php_service_name变量;为了说明我们为什么使用它,你需要等到第六章,标题为针对多种发行版时再进一步了解。

我们需要配置的软件堆栈的最后一个组件是 MariaDB,因此在继续进行 WordPress 安装和配置之前,让我们先回顾一下它。

MariaDB 配置

MariaDB 的配置将与第四章中的配置非常相似,标题为部署 LAMP 堆栈,只不过少了几个步骤,因此我在这里不再详细讲解。

这一部分角色的默认变量在roles/stack_config/defaults/main.yml中的定义如下:

mariadb:
  bind: "127.0.0.1"
  server_config: "/etc/my.cnf.d/mariadb-server.cnf"
  username: "root"
  password: "Pa55W0rd123"
  hosts:
    - "127.0.0.1"
    - "::1"
    - "{{ ansible_nodename }}"
    - "localhost"

正如你所看到的,我们现在使用了一个嵌套变量,并且去除了之前在第四章中定义的主机通配符%,该章标题为部署 LAMP 堆栈

我们的第一个任务是启动 MariaDB,以便我们能够与之交互:

- name: "Start mariadb"
  ansible.builtin.service:
    name: "mariadb"
    state: "started"
    enabled: true

检查~/.my.cnf文件是否存在:

- name: "Check to see if the ~/.my.cnf file exists"
  ansible.builtin.stat:
    path: "~/.my.cnf"
  register: mycnf

设置密码:

- name: "Change mysql root password if we need to"
  community.mysql.mysql_user:
    name: "{{ mariadb.username }}"
    host: "{{ item }}"
    password: "{{ mariadb.password }}"
    check_implicit_admin: "true"
    priv: "*.*:ALL,GRANT"
    login_user: "{{ mariadb.username }}"
    login_unix_socket: /var/run/mysqld/mysqld.sock
  with_items: "{{ mariadb.hosts }}"
  when: not mycnf.stat.exists

创建 ~/``my.cnf 文件:

- name: "Set up .my.cnf file"
  ansible.builtin.template:
    src: "my.cnf.j2"
    dest: "~/.my.cnf"
    mode: "0644"

然后,删除匿名用户:

- name: "Delete anonymous MySQL user"
  community.mysql.mysql_user:
    user: ""
    host: "{{ item }}"
    state: "absent"
  with_items: "{{ mariadb.hosts }}"

现在,我们已经来到了最后一个任务,即删除测试数据库:

- name: "Remove the MySQL test database"
  community.mysql.mysql_db:
    db: "test"
    state: "absent"

现在,所有安装和运行 WordPress 所需的配置已经完成,我们可以开始安装 WordPress 了。

WordPress 角色

现在,我们已经完成了准备目标虚拟机的角色,可以开始实际的 WordPress 安装了;这将分为几个不同的部分,首先是下载 wp-cli 并设置数据库。

在我们继续之前,我们需要创建角色:

$ ansible-galaxy role init roles/wordpress

现在我们已经有了空的角色文件,可以开始在文件中填充任务和变量。

一些事实

在安装 WordPress 之前,我们必须使用 ansible.builtin.set_fact 模块设置一些事实。接下来的任务是 roles/wordpress/tasks/main.yml 文件中的第一个任务,它使用 Ansible 初次连接主机时收集的信息设置两个变量:

- name: "Set a fact for the wordpress domain"
  ansible.builtin.set_fact:
    wordpress_domain: "{{ ansible_ssh_host }}"
    os_family: "{{ ansible_distribution }} {{ ansible_distribution_version }}"

我们将使用这两个变量,当我们使用 WordPress CLI 安装 WordPress 时,接下来会下载并安装该工具。

WordPress CLI 安装

WordPress CLI(wp-cli)是一个用于管理 WordPress 安装的命令行工具;我们将在整个角色中使用它,因此我们角色的第一件事应该是下载它。为此,我们需要在 roles/wordpress/defaults/main.yml 中下载以下变量:

wp_cli:
  download: "https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar"
  path: "/usr/local/bin/wp"

回到 roles/wordpress/tasks/main.yml 文件,在接下来的任务中,我们使用这两个变量,它们会下载 wp-cli 并将其放置在我们的主机上:

- name: "Download wp-cli"
  ansible.builtin.get_url:
    url: "{{ wp_cli.download }}"
    dest: "{{ wp_cli.path }}"
    mode: "0755"

现在,我们的主机上已经安装了 wp-cli 并且拥有正确的执行权限。

在开始使用 wp-cli 之前,我们还需要做一件准备工作:创建数据库和用户,这些将在我们安装 WordPress 时使用。

创建 WordPress 数据库

角色的下一部分创建我们 WordPress 安装将使用的数据库;和本章中的其他任务一样,它使用了一个嵌套变量,该变量可以在 roles/wordpress/defaults/main.yml 中找到:

wp_database:
  name: "wordpress"
  username: "wordpress"
  password: "W04DPr3S5"

roles/wordpress/tasks/main.yml 中创建数据库的任务如下:

- name: "Create the wordpress database"
  community.mysql.mysql_db:
    db: "{{ wp_database.name }}"
    state: "present"

现在数据库已经创建完毕,我们可以添加用户:

- name: "Create the user for the wordpress database"
  community.mysql.mysql_user:
    name: "{{ wp_database.username }}"
    password: "{{ wp_database.password }}"
    priv: "{{ wp_database.name }}.*:ALL"
    state: "present"
  with_items: "{{ mariadb.hosts }}"

请注意,我们正在使用来自上一个角色的 mariadb.hosts 变量。现在数据库已经创建,我们可以开始下载和安装 WordPress。

下载、配置并安装 WordPress

现在,我们已经为安装 WordPress 做好了一切准备,可以开始了,首先在 roles/wordpress/defaults/main.yml 中设置一些默认变量:

wordpress:
  domain: "http://{{ wordpress_domain }}/"
  title: "WordPress installed by Ansible on {{ os_family }}"
  username: "ansible"
  password: "password"
  email: "test@example.com"
  plugins:
    - "jetpack"
    - "wp-super-cache"
    - "wordpress-seo"
    - "wordfence"
    - "nginx-helper"

现在我们已经有了变量,如果需要的话可以开始下载。为了判断是否需要下载 WordPress,我们应该检查是否已有 WordPress 安装。执行此操作的任务位于 roles/wordpress/tasks/main.yml 文件中,内容如下:

- name: "Are the wordpress files already there?"
  ansible.builtin.stat:
    path: "{{ wordpress_system.home }}/index.php"
  register: wp_installed

如你所见,第一个任务使用 ansible.builtin.stat 模块来检查系统用户主目录中是否存在 index.php 文件,在我们的情况下,这也是 Web 根目录。

如果这是首次对主机运行此 playbook,那么我们需要下载 WordPress:

- name: "Download wordpresss"
  ansible.builtin.command: "{{ wp_cli.path }} core download"
  args:
    chdir: "{{ wordpress_system.home }}"
  become_user: "{{ wordpress_system.user }}"
  become: true
  when: not wp_installed.stat.exists

此任务使用 ansible.builtin.shell 模块发出以下命令:

$ su wordpress -
$ cd /var/www/wordpress
$ /usr/local/bin/wp core download

在继续下一个任务之前,我们需要处理一些参数,具体如下:

  • argschdir:你可以通过 argsansible.builtin.shell 模块传递额外的参数。在这里,我们传递了 chdir,它指示 Ansible 在运行我们提供的 shell 命令之前切换到我们指定的目录。

  • become_user:这是我们希望以其身份运行命令的用户。如果不使用此标志,命令将以 root 用户身份运行。

  • become:该指令告诉 Ansible 以定义的用户身份执行任务。

Playbook 中的下一个任务会设置用户主目录的正确权限:

- name: "Set the correct permissions on the homedir"
  ansible.builtin.file:
    path: "{{ wordpress_system.home }}"
    mode: "0755"
  when: not wp_installed.stat.exists

现在 WordPress 已经下载完成,我们可以开始安装。首先,我们需要检查是否已经完成安装:

- name: "Is wordpress already configured?"
  ansible.builtin.stat:
    path: "{{ wordpress_system.home }}/wp-config.php"
  register: wp_configured

如果没有 wp-config.php 文件,则将执行以下任务:

- name: "Sort the basic wordpress configuration"
  ansible.builtin.command: "{{ wp_cli.path }} core config --dbhost={{ mariadb.bind }} --dbname={{ wp_database.name }} --dbuser={{ wp_database.username }} --dbpass={{ wp_database.password }}"
  args:
    chdir: "{{ wordpress_system.home }}"
  become_user: "{{ wordpress_system.user }}"
  become: true
  when: not wp_configured.stat.exists

这就像你登录并运行以下命令:

$ su wordpress -
$ cd /var/www/wordpress
$ /usr/local/bin/wp core config \
--dbhost=127.0.0.1\
--dbname=wordpress\
--dbuser=wordpress \
--dbpass=W04DPr3S5

如你所见,我们正在使用 Ansible 执行命令,就好像我们打开了一个本地终端。

现在我们已经创建了 wp-config.php 文件,并设置了数据库凭证,我们可以安装 WordPress。

首先,我们需要检查 WordPress 是否已经安装:

- name: "Do we need to install wordpress?"
  ansible.builtin.command: "{{ wp_cli.path }} core is-installed"
  args:
    chdir: "{{ wordpress_system.home }}"
  become_user: "{{ wordpress_system.user }}"
  become: true
  ignore_errors: true
  register: wp_installed

如你所见,存在 ignore_errors 选项,如果 WordPress 未安装,此命令将给出错误。我们正是利用这一点来注册结果,正如接下来的任务所示:

- name: "Install wordpress if needed"
  ansible.builtin.command: "{{ wp_cli.path }} core install --url='{{ wordpress.domain }}' --title='{{ wordpress.title }}' --admin_user={{ wordpress.username }} --admin_password={{ wordpress.password }} --admin_email={{ wordpress.email }}"
  args:
    chdir: "{{ wordpress_system.home }}"
  become_user: "{{ wordpress_system.user }}"
  become: true
  when: wp_installed.rc == 1

只有当上一个任务返回错误时,此任务才会执行,如果 WordPress 未安装,便会发生这种情况。

现在我们的主 WordPress 网站已经安装完成,我们可以继续安装插件。

WordPress 插件安装

我们的 WordPress 安装的最后部分是下载并安装我们在 wordpress.plugins 变量中定义的插件。

根据之前的任务,我们会在任务中嵌入一点逻辑。首先,我们运行以下任务来查看所有插件是否已安装:

- name: "Do we need to install the plugins?"
  ansible.builtin.command: "{{ wp_cli.path }} plugin is-installed {{ item }}"
  args:
    chdir: "{{ wordpress_system.home }}"
  become_user: "{{ wordpress_system.user }}"
  become: true
  with_items: "{{ wordpress.plugins }}"
  ignore_errors: true
  register: wp_plugin_installed

如果插件没有安装,这个任务应该失败,因此我们在其中加入了 ignore_errors

如你所见,我们正在注册整个任务的结果,因为,如果你记得的话,我们正在安装多个插件,比如 wp_plugin_installed

接下来的两个任务会获取 wp_plugin_installed 的结果,并使用 ansible.builtin.set_fact 模块根据结果设置一个事实:

- name: "Set a fact if we don't need to install the plugins"
  ansible.builtin.set_fact:
    wp_plugin_installed_skip: true
  when: wp_plugin_installed.failed is undefined

前面的任务是在我们不需要安装任何插件的情况下设置的,下面的任务则是在我们需要安装至少一个插件时使用的:

- name: "Set a fact if we need to install the plugins"
  ansible.builtin.set_fact:
    wp_plugin_installed_skip: false
  when: wp_plugin_installed.failed is defined

如你所见,我们将 wp_plugin_installed_skip 设置为 truefalse:如果该值设置为 false,则下一任务将循环安装插件:

- name: "Install the plugins if we need to or ignore if not"
  ansible.builtin.command: "{{ wp_cli.path }} plugin install {{ item }} --activate"
  args:
    chdir: "{{ wordpress_system.home }}"
  become_user: "{{ wordpress_system.user }}"
  become: true
  with_items: "{{ wordpress.plugins }}"
  when: not wp_plugin_installed_skip

现在我们已经定义了插件任务,可以尝试运行我们的 playbook。

运行 WordPress playbook

要运行 playbook 并安装 WordPress,我们需要完成对文件的查看;site.yml 应该如下所示:

---
- name: "Install and configure WordPress and supporting software"
  hosts: "ansible_hosts"
  gather_facts: true
  become: true
  become_method: "ansible.builtin.sudo"
  vars_files:
    - "group_vars/common.yml"
  roles:
    - "stack_install"
    - "stack_config"
    - "wordpress"

事情处理完毕后,我们可以运行 playbook。

注意

本书附带的 GitHub 仓库中的 Chapter05 文件夹包含了示例 hosts 文件以及使用 Multipass 启动本地虚拟机的密钥。如果你在跟随操作,请参考 第一章安装和运行 Ansible;这部分详细介绍了如何启动虚拟机并准备你的 hosts 文件。

如我们所知,要运行 playbook,我们需要在 Multipass 虚拟机启动并运行后执行以下命令:

$ ansible-playbook -i hosts site.yml

让我们从添加 NGINX 仓库开始,重点介绍一些亮点,而不是从头到尾浏览整个输出:

TASK [roles/stack_install : add the apt keys from a URL] **
changed: [ansiblevm] => (item=http://nginx.org/keys/nginx_signing.key)
TASK [roles/stack_install : install the repo packages] ****
changed: [ansiblevm] => (item=deb http://nginx.org/packages/mainline/ubuntu/ jammy nginx)
changed: [ansiblevm] => (item=deb-src http://nginx.org/packages/mainline/ubuntu/ jammy nginx)

如你所见,Ubuntu 发行版的名称已被添加—在这个示例中是 jammy

在修改 php.ini 文件时,我们定义的三个更改中只有两个需要应用,因为 expose_php 已经设置为 Off

TASK [roles/stack_config : configure php.ini] *************
changed: [ansiblevm] => (item={'regexp': '^;date.timezone =', 'replace': 'date.timezone = Europe/London'})
ok: [ansiblevm] => (item={'regexp': '^expose_php = On', 'replace': 'expose_php = Off'})
changed: [ansiblevm] => (item={'regexp': '^upload_max_filesize = 2M', 'replace': 'upload_max_filesize = 20M'})

记住,当安装和配置 WordPress 时,我们为某些检查设置了 ignore_errors 标志;这就是原因:

TASK [roles/wordpress : do we need to install wordpress?] *
fatal: [ansiblevm]: FAILED! => {"changed": true, "cmd": "/usr/local/bin/wp core is-installed", "delta": "0:00:00.142910", "end": "2023-09-17 12:28:16.500304", "msg": "non-zero return code", "rc": 1, "start": "2023-09-17 12:28:16.357394", "stderr": "PHP Warning:  Undefined array key \"HTTP_HOST\" in /var/www/wordpress/wp-includes/functions.php on line 6135\nWarning: Undefined array key \"HTTP_HOST\" in /var/www/wordpress/wp-includes/functions.php on line 6135\nPHP Warning:  Undefined array key \"HTTP_HOST\" in /var/www/wordpress/wp-includes/functions.php on line 6135\nWarning: Undefined array key \"HTTP_HOST\" in /var/www/wordpress/wp-includes/functions.php on line 6135", "stderr_lines": ["PHP Warning:  Undefined array key \"HTTP_HOST\" in /var/www/wordpress/wp-includes/functions.php on line 6135", "Warning: Undefined array key \"HTTP_HOST\" in /var/www/wordpress/wp-includes/functions.php on line 6135", "PHP Warning:  Undefined array key \"HTTP_HOST\" in /var/www/wordpress/wp-includes/functions.php on line 6135", "Warning: Undefined array key \"HTTP_HOST\" in /var/www/wordpress/wp-includes/functions.php on line 6135"], "stdout": "", "stdout_lines": []}
...ignoring
TASK [roles/wordpress : install wordpress if needed] ******
changed: [ansiblevm]

如你所见,错误被忽略了,安装 WordPress 的任务被触发了。插件也发生了同样的情况:

TASK [roles/wordpress : set a fact if we don't need to install the plugins] **************************************
skipping: [ansiblevm]
TASK [roles/wordpress : set a fact if we need to install the plugins] **********************************************
ok: [ansiblevm]

第一次执行时,回顾大致如下:

PLAY RECAP ************************************************
ansiblevm                  : ok=39   changed=28   unreachable=0    failed=0    skipped=1    rescued=0    ignored=2

在任务执行过程中,重新运行 playbook 立即展示了我们所添加的逻辑如何生效,这导致许多后续任务被完全跳过:

TASK [roles/wordpress : are the wordpress files already there?] ***************************************************
ok: [ansiblevm]
TASK [roles/wordpress : download wordpresss] **************
skipping: [ansiblevm]

请注意,这次对插件的检查没有导致错误:

TASK [roles/wordpress : do we need to install the plugins?]
changed: [ansiblevm] => (item=jetpack)
changed: [ansiblevm] => (item=wp-super-cache)
changed: [ansiblevm] => (item=wordpress-seo)
changed: [ansiblevm] => (item=wordfence)
changed: [ansiblevm] => (item=nginx-helper)
TASK [roles/wordpress : set a fact if we don't need to install the plugins] **************************************
ok: [ansiblevm]
TASK [roles/wordpress : set a fact if we need to install the plugins] **********************************************
skipping: [ansiblevm]
TASK [roles/wordpress : install the plugins if we need to or ignore if not] *****************************************
skipping: [ansiblevm] => (item=jetpack)
skipping: [ansiblevm] => (item=wp-super-cache)
skipping: [ansiblevm] => (item=wordpress-seo)
skipping: [ansiblevm] => (item=wordfence)
skipping: [ansiblevm] => (item=nginx-helper)

现在 WordPress 已经安装,我们应该能够通过访问在 hosts 文件中定义的主机,在浏览器中打开它,在我的案例中是 http://192.168.64.26.nip.io/;你的地址会有所不同。

你将看到默认的 WordPress 网站:

图 5.1 – 我们刚安装的 WordPress 网站

图 5.1 – 我们刚安装的 WordPress 网站

如你所见,网站左上角的描述写着 由 Ansible 在 Ubuntu 22.04 上安装的 WordPress,这就是我们在安装 WordPress 时设置的内容。

此外,如果你通过在 URL 末尾添加 /wp-admin/ 访问 WordPress 管理区域,例如 http://192.168.64.26.nip.io/wp-admin/,你应该能够使用我们定义的用户名和密码登录 WordPress:

图 5.2 – WordPress 管理员登录页面

图 5.2 – WordPress 管理员登录页面

登录后,你应该会看到一些关于我们在运行 playbook 时安装的插件需要配置的消息:

图 5.3 – 初次登录 WordPress 时的提示

图 5.3 – 初次登录 WordPress 时的提示

随意玩转 WordPress 安装,甚至如果你有兴趣,可以尝试破坏它——如果需要,你可以删除并重新启动 Multipass 虚拟机,然后快速重新运行 playbook 来重新安装 WordPress。

摘要

在本章中,我们重复使用了前一章中介绍的许多相同的原则,并继续部署一个完整的应用程序。好的一点是,这个过程既可以重复执行,也只需一个命令。

到目前为止,我们一直在针对 Ubuntu 虚拟机。如果我们将 playbook 运行在一个基于 Red-Hat 的虚拟机上,playbook 会报错,因为命令和路径不同。

下一章将使用相同的 playbook 来支持多个操作系统。

进一步阅读

你可以通过以下链接了解我们在本章中介绍的技术的更多信息:

我们安装的插件的项目页面可以通过以下链接找到:

第六章:针对多个发行版

到目前为止,在之前的章节中,我们在运行剧本时一直针对的是单一操作系统——Ubuntu。

本章将探讨如何在相同的角色和剧本中使用多个 Linux 发行版。

我们将以 第五章 中创建的 WordPress 剧本和角色为基础,部署 WordPress,并进行以下操作:

  • 发现我们两个目标操作系统之间的区别

  • 查看并实现我们的 WordPress 角色,使它们能够在两个目标操作系统上运行

  • 讨论并应用针对多个发行版的最佳实践

本章涵盖以下内容:

  • Debian 和 Red Hat

  • 多发行版考虑

  • 适应角色

  • 运行剧本

技术要求

鉴于我们将启动两个不同的操作系统,我们将改变以往章节中的方法,而是在云服务提供商中启动一对虚拟机,而不是在本地机器上启动两台不同的虚拟机。

这么做的主要原因是 Multipass 只真正支持 Ubuntu 系统,因为它是由 Ubuntu 的创建者和维护者 Canonical 开发的,目的是为用户提供一种快速、简便和一致的方式,在多个主机平台上启动 Ubuntu 虚拟机。

由于我们将在 第九章 中讨论自动化云部署,迁移到云端,所以本章不会使用 Ansible 来部署云资源。

对于本章,我建议使用像 DigitalOcean (www.digitalocean.com/) 或 Linode (www.linode.com/) 这样的云服务提供商,它们都支持本章将要介绍的操作系统,并且其虚拟机费用从每月约 5 美元起。

重要说明

本章不会介绍如何启动虚拟机;如果你正在跟随本书进行操作,请查看你所选择的云服务提供商的文档。此外,完整的工作代码可以在 GitHub 仓库中查看:github.com/PacktPublishing/Learn-Ansible-Second-Edition

Debian 和 Red Hat

这是 Linux 操作系统世界可能变得稍微混乱的地方。尽管我们启动了 Ubuntu 22.04 和 Rocky Linux 9 的虚拟机来运行我们的剧本,但我们将在剧本代码中引用 Debian 和 Red Hat。

为什么会这样呢?原因在于 Linux 发行版的血统。Ubuntu 是 Debian 操作系统的后代,继承了其包管理系统及许多其他特性。同样,Rocky Linux 是 Red Hat 的后代,旨在成为与 Red Hat 企业版 Linux (RHEL) 完全兼容的发行版。

因此,当我们在剧本中提到 Debian 和 Red Hat 时,我们指的是我们的两个操作系统——Ubuntu 和 Rocky Linux——所基于的基本平台。

在实际操作中,剧本代码通常会检查底层的发行版类型,以决定如何执行特定任务。例如,在基于 Debian 的系统(如 Ubuntu)上安装软件包的命令,可能与在基于 Red Hat 的系统(如 Rocky Linux)上的命令不同。

基于 Debian 的系统使用 Debian 包管理系统,以dpkg作为核心工具,并且通常使用apt、`apt-get,或在某些情况下,两者都使用,以便更友好地进行用户交互。

基于 Red Hat 的系统采用 RPM 包管理系统,使用rpm作为核心工具,通常辅以yum或其后继者dnf,以便为包管理提供更友好的界面。

还有其他差异,例如 Debian 和 Red Hat 基于系统的目录结构和配置文件位置不同,这可能会影响我们在剧本角色中必须考虑的系统管理。

目前为止,最重要且最相关的差异是许可证问题。

Debian 以严格遵循自由软件原则而闻名。相比之下,基于 Red Hat 的系统可能会包含更多的专有或闭源软件,尤其是在 Red Hat 企业版 Linux 的商业企业发行版中。

2023 年 6 月,Red Hat 在修改其条款时达到了高潮,停止了 RHEL 源代码的公开发布,并限制了仅供客户访问。

这一变动影响了下游项目,这些项目依赖 RHEL 源代码创建兼容的发行版,例如 Rocky Linux。该变更意味着,只有受合同限制并禁止共享代码的客户才能访问 RHEL 源代码,这符合 GPL 许可证的条款,后者要求仅为二进制用户提供源代码,二进制用户实际上是付费客户。

截至目前,这一变化的后果仍在发酵,局势仍在平稳下来,尽管像 Rocky Linux 这样的发行版似乎已经找到合规的方式;有关更多信息,请参阅本章末尾的进一步阅读部分。

因此,回到我们的剧本,通过在代码中引用 Debian 和 Red Hat(或两者),我们可以创建更具适应性的角色,能够一致地处理不同的 Linux 发行版及其衍生版。

多发行版的考虑

查看在三个角色(stack_installstack_configwordpress)中使用的每个 Ansible 内置模块,我们正在使用一些在我们新引入的 Rocky Linux 环境中无法正常工作的模块。

让我们快速浏览每个模块,考虑在针对两个不同发行版时需要改变或考虑的事项。

堆栈安装角色

这个角色使用了以下内置模块:

  • ansible.builtin.apt

  • ansible.builtin.apt_key

  • ansible.builtin.apt_repository

我们使用这些模块来更新我们的操作系统,添加 NGINX 主线仓库,并安装我们的 WordPress 安装所需的所有软件包。

由于这些模块都涉及软件包管理,我们将无法重用这些任务,这意味着我们需要将角色分为两部分:一部分处理基于 Debian 的系统,另一部分处理基于 Red Hat 的系统。

此外,由于两个发行版之间软件包名称的微妙差异,我们将无法重用变量。

这意味着我们对这个角色的最佳方法是根据 Ansible 所针对的发行版使用两组不同的任务集。幸运的是,有内置的 Ansible 模块使得这种方法变得简单。在审查了剩余两个角色中的模块后,我们将在下一节中介绍它们。

Stack Config 角色

与前一个角色稍有不同的是,这里我们不需要将任务分为两部分;大多数任务将在我们的两个 Linux 发行版上运行。

这意味着使用以下模块的任务不需要任何更改:

  • ansible.builtin.group: 创建组在两个发行版中都是相同的

  • ansible.builtin.user: 创建用户在两个发行版中都是相同的

  • ansible.builtin.template: 这仅将文件渲染并复制到目标主机

  • ansible.builtin.file: 这仅将文件复制到目标主机

  • ansible.builtin.copy: 这仅在目标主机上复制文件

  • ansible.builtin.lineinfile: 这仅在目标主机的文件中搜索文本,并在需要时更新它

  • ansible.builtin.service: 这在两个发行版上都受支持

  • ansible.builtin.stat: 仅检查主机文件系统上文件的存在

  • ansible.builtin.mysql_user: 由于它与数据库服务交互,因此与发行版无关

  • ansible.builtin.mysql_db: 与前一个任务一样,它与数据库服务交互

这个列表基本属实;但是,两个发行版之间的文件路径将会有所改变。

此外,正如我们在 第五章 中已经提到的,即 部署 WordPress,当我们查看 Stack Config 角色的变量时,我们正在引用包含我们想要加载到 playbook 运行中的变量的文件,因此我们需要加载额外一组变量用于分发以及标准变量。

我们将需要执行一些额外的任务来添加第二个发行版。一些 Red Hat 发行版默认启用防火墙和启用 SELinux,因此我们需要在最后执行一些仅适用于 Red Hat 的任务。

SELinux,或者全称为安全增强型 Linux,是 Linux 内核的安全模块,提供支持访问控制安全策略的机制。

然而,我们可以将这些任务保留在 main.yml 文件中,而不是通过调用任务时使用条件来加载不同的任务集。

WordPress 角色

由于前两个角色已经安装并配置了我们运行 WordPress 安装所需的一切,因此这个角色完全与发行版无关,我们不需要对角色中的任务做任何更改。如果你还记得,在第五章部署 WordPress时,我们运行配置 WordPress 的命令时,设置了以下事实:

- name: "Set a fact for the wordpress domain"
  ansible.builtin.set_fact:
    wordpress_domain: "{{ ansible_ssh_host }}"
    os_family: "{{ ansible_distribution }} {{ ansible_distribution_version }}"

这使用了 Ansible 在首次连接主机时收集的事实来确定我们连接的分发版和版本;我们将在本节中深入探讨 Stack 安装和配置角色中列出的变更。

调整角色

那么,我们如何将逻辑构建到角色中,以便只在不同操作系统上执行某些部分呢?正如我们所知,软件包名称会不同。我们如何为每个操作系统定义不同的变量集?

操作系统系列

我们在第一章中看过 ansible.builtin.setup 模块,安装和运行 Ansible;该模块收集了关于目标主机的事实。

其中一个事实是 ansible_os_family;它告诉我们正在运行的操作系统类型。

为了演示这一点,我启动了两台主机,一台运行 Ubuntu 22.04,另一台运行 Rocky Linux 9 作为其操作系统。我创建了如下所示的清单文件:

RedHat ansible_host=178.79.178.78.nip.io
Debian ansible_host=176.58.114.60.nip.io
[ansible_hosts]
RedHat
Debian
[ansible_hosts:vars]
ansible_connection=ssh
ansible_user=root
ansible_private_key_file=~/.ssh/id_rsa
host_key_checking=False

重要提示

上述清单文件仅用于说明目的;如果你在跟随本教程,你需要更新它,以考虑主机 IP 地址、用户名和私钥文件的位置。

主机已启动并运行后,我们可以使用以下命令单独对每台主机进行操作:

$ ansible -i hosts RedHat -m ansible.builtin.setup | grep ansible_os_family
$ ansible -i hosts Debian -m ansible.builtin.setup | grep ansible_os_family

运行这两个命令应该会显示如下所示的终端输出:

图 6.1 – 检查 ansible_os_family 的值

图 6.1 – 检查 ansible_os_family 的值

正如你所看到的,每个主机都正确返回了操作系统系列。

我们可以进一步更新我们的命令,如下所示:

$ ansible -i hosts RedHat -m ansible.builtin.setup | grep ansible_distribution
$ ansible -i hosts Debian -m ansible.builtin.setup | grep ansible_distribution

这将产生以下输出:

图 6.2 – 检查 ansible_distribution 的值

图 6.2 – 检查 ansible_distribution 的值

如你所见,这提供了关于操作系统本身的更多详细信息,而不仅仅是 Linux 的版本;它是基于 RedHatDebian

最后,我们运行以下命令:

$ ansible -i hosts ansible_hosts -m ansible.builtin.setup | grep ansible_os_family

这将同时在同一个 Ansible 运行中针对两台主机,并返回如下所示的终端输出:

图 6.3 – 在单次运行中检查 ansible_distribution 的值

图 6.3 – 在单次运行中检查 ansible_distribution 的值

现在,我们可以识别每台主机上正在使用的操作系统,可以开始调整角色,以考虑我们在本章前一部分讨论的更改。

Stack Install 角色

我们将要查看的角色的第一部分是roles/stack_install/tasks/main.yml文件的内容。角色的先前版本包含了所有的任务,用于为我们的 Ubuntu 服务器安装仓库和包;所有这些任务应该被移动到名为roles/stack_install/tasks/Debian.yml的文件中,并创建一个名为roles/stack_install/tasks/RedHat.yml的新文件;最后,我们应该更新roles/stack_install/tasks/main.yml,使其包含以下内容。

这里是为我们目标操作系统加载变量文件的三个任务:

- name: "Include the operating system specific variables"
  ansible.builtin.include_vars: "{{ ansible_os_family }}.yml"

如你所见,这使用了ansible.builtin.include_vars模块,从 roles 文件夹内的变量路径加载变量,该路径是roles/stack_install/vars/

然后,它加载一个名为RedHat.ymlDebian.yml的文件;这两个文件名是通过任务中的{{ ansible_os_family }}变量动态生成的,这意味着将加载与目标操作系统相关的变量。

如果你查看 GitHub 上的仓库,你会注意到,尽管差异微妙,但在system_packagesextra_packagesstack_packages包列表中列出的包是有差别的。

下一个任务在调用ansible.builtin.import_tasks模块时使用了when条件,首先针对基于 Debian 的系统:

- name: "Install the stack on Debian based systems"
  ansible.builtin.import_tasks: "Debian.yml"
  when: ansible_os_family == 'Debian'

对于我们来说,这意味着当 Ansible 剧本针对基于 Debian 的主机时,它将加载来自roles/stack_install/tasks/Debian.yml的任务,这些任务与我们在第五章中详细讨论的内容相同,部署 WordPress,并将它们应用到主机上。

下一个任务执行相同的功能,但这次是针对基于 Red Hat 的主机,使用的是roles/stack_install/tasks/RedHat.yml文件中列出的任务:

- name: "Install the stack on RedHat based systems"
  ansible.builtin.import_tasks: "RedHat.yml"
  when: ansible_os_family == 'RedHat'

roles/stack_install/tasks/RedHat.yml文件包含三项任务,这些任务与Debian.yml中的任务几乎相同。

我们通过运行更新所有已安装包的任务来开始角色:

- name: "Update all of the installed packages"
  ansible.builtin.dnf:
    name: "*"
    state: "latest"
    update_cache: true

如你所见,这使用了ansible.builtin.dnf模块,而不是ansible.builtin.apt模块。

接下来,我们有一个任务来安装 NGINX 主线仓库:

- name: "Add the NGINX mainline repo"
  ansible.builtin.yum_repository:
    name: "{{ nginx_repo.name }}"
    description: "{{ nginx_repo.description }}"
    baseurl: "{{ nginx_repo.baseurl }}"
    gpgcheck: "{{ nginx_repo.gpgcheck }}"
    enabled: "{{ nginx_repo.enabled }}"

尽管这使用了ansible.builtin.yum_repository模块,但一旦添加了新的仓库,DNF 将会自动识别它。这也是我们需要运行的唯一任务来添加仓库,并且在 Debian 系统上添加仓库与在 Red Hat 系统上添加仓库是非常不同的。

针对基于 Red Hat 的系统的最终任务是安装所有包,包括我们刚刚启用的主线仓库中的 NGINX 包,再次通过调用ansible.builtin.dnf模块:

- name: "Update cache and install the stack packages"
  ansible.builtin.dnf:
    state: "present"
    update_cache: true
    pkg: "{{ system_packages + extra_packages + stack_packages }}"

如你所见,通过对任务调用逻辑进行一些调整,将角色更新为支持 Debian 和 Red Hat 发行版是相对轻松的。

对于我们接下来需要更改的角色,即堆栈配置角色,我们将采取一种略有不同的方法来考虑不同操作系统发行版。

堆栈配置角色

除了开始时的一个任务和结束时的几个任务外,这个角色的主体部分保持不变。

roles/stack_config/default/main.yml文件中,默认变量文件有了一些更改;首先,添加了以下变量:

selinux:
  http_permissive: true
firewall_comands:
  - "firewall-cmd --zone=public --add-port=80/tcp --permanent"
  - "firewall-cmd --zone=public --add-port=80/tcp"

正如你从它们的名字中猜到的,这些涉及到 SELinux 和防火墙。

下一个更改是将mysql_socket_pathphp_fpm_pathphp_ini_pathphp_service_name变量移动到roles/stack_config/vars/Debian.ymlroles/stack_config/vars/RedHat.yml中的发行版特定文件中。

正如我们之前讨论的,两个发行版之间的一个关键区别是我们在堆栈安装角色中安装的核心文件和服务配置文件的路径。

roles/stack_config/vars/Debian.yml文件中,我们有以下内容:

mysql_socket_path: "/var/run/mysqld/mysqld.sock"
php_fpm_path: "/etc/php/8.1/fpm/pool.d/www.conf"
php_ini_path: "/etc/php/8.1/fpm/php.ini"
php_service_name: "php8.1-fpm"

然而,对于roles/stack_config/vars/RedHat.yml文件,我们需要定义以下内容:

mysql_socket_path: "/var/lib/mysql/mysql.sock"
php_fpm_path: "/etc/php-fpm.d/www.conf"
php_ini_path: /etc/php.ini
php_service_name: "php-fpm"

如你所见,乍一看,它们看起来有点相似,但路径和文件名是不同的。

这些文件由一个任务调用,这与我们在堆栈安装角色开始时使用的相同:

- name: Include the operating system specific variables
  ansible.builtin.include_vars: "{{ ansible_os_family }}.yml"

从这里开始,我们在第五章中涵盖的所有原始任务都被调用并执行,最后是删除测试 MySQL 数据库的任务。

从这里开始,在该角色中,我们有任务来考虑配置基于 Red Hat 的主机所需的额外步骤,首先是配置 SELinux;对于我们的角色,我们需要启用允许 Web 服务器运行的策略,而在许多 Red Hat 发行版中,默认情况下会阻止该策略。

执行此任务的代码如下:

- name: "Set the selinux allowing httpd_t to be permissive is required"
  community.general.selinux_permissive:
    name: httpd_t
    permissive: true
  when: selinux.http_permissive and ansible_os_family == 'RedHat'

如你所见,这里的when条件确保任务只有在selinux.http_permissive变量设置为trueansible_os_family等于RedHat时才会执行。

虽然我们的 Debian 系统将满足selinux.http_permissive条件,但在这些主机上该任务将被跳过,因为它不满足第二个条件。

最后,我们有配置firewalld服务的任务,这是大多数现代 Red Hat 发行版上的默认防火墙。

重要提示

尽管我们在这一部分使用了firewall-cmd,但也有一个 Ansible 模块支持 firewalld 服务,名为ansible.posix.firewalld。由于这是我们在标题中唯一面向 Red Hat 操作系统的实例,我们改用了ansible.builtin.command,以展示如何根据输出命令满足更复杂的条件。

就像我们在其他章节中包含的某些角色一样,配置防火墙是我们只需要执行一次的任务。我们要做的第一件事是检查 ~/firewall-configured 文件是否存在,并记录结果:

- name: "Check to see if the ~/firewall-configured file exists"
  ansible.builtin.stat:
    path: "~/firewall-configured"
  register: firewall_configured

接下来,我们需要检查 firewalld 是否在运行,但只有在 RedHat 发行版上才需要这样做。为此,我们需要运行 firewall-cmd --state shell 命令,并将输出结果注册到 fireweall_status 变量中:

- name: "Check if firewalld is running"
  ansible.builtin.command: firewall-cmd --state
  register: fireweall_status
  when: ansible_os_family == 'RedHat'

现在,由于剩余任务也可能在 Debian 系统上执行,因此我们需要考虑这一点,因为我们现在有一个包含我们运行命令的 stdout 的变量 fireweall_status,它在 Debian 系统上不会存在,导致错误并停止 playbook 执行:

- name: "Set a fact so the playbook can continue if running on a Debian based system"
  ansible.builtin.set_fact:
    fireweall_status:
      stdout: notrunning
  when: ansible_os_family == 'Debian'

正如前面的任务所示,如果 ansible_os_familyDebian,我们将 fireweall_status.stdout 变量设置为 notrunning

现在我们已经拥有了做出是否运行配置防火墙命令的决策所需的所有信息,以下条件需要满足:

  • firewall-cmd --state 命令返回 running

  • 操作系统是 RedHat

  • ~/firewall-configured 文件不存在

如果满足以下任务中定义的三个条件,则会执行配置防火墙、开放端口80并允许流量的命令:

- name: "Run the firewall-cmd commands if the firewall-cmd --state command returns running"
  ansible.builtin.command: "{{ item }}"
  with_items: "{{ firewall_comands }}"
  when: fireweall_status.stdout == "running" and ansible_os_family == 'RedHat' and not firewall_configured.stat.exists

最后一个任务创建 ~/firewall-configured 文件,以确保命令不会再次执行:

- name: "Create the ~/firewall-configured file"
  ansible.builtin.file:
    path: ~/firewall-configured
    state: touch
    mode: "0644"
  when: not firewall_configured.stat.exists

这在两个发行版上都执行,因为无论在 Debian 系统上如何设置,我们都不希望运行命令;而在 Red Hat 系统上,这将意味着 playbook 的后续执行将无法满足执行防火墙服务配置命令的三个条件。

WordPress 角色

如前所述,我们不需要对这个角色进行任何更改。

运行 playbook

我们的 site.yml 文件没有任何更改,这意味着我们需要运行以下命令来运行 playbook:

$ ansible-playbook -i hosts site.yml

输出太多,无法一一涵盖,但我会包括一些 playbook 执行的亮点,从收集事实开始:

TASK [Gathering Facts] ************************************
ok: [Debian]
ok: [RedHat]

现在 Ansible 知道了我们的两个主机,它开始执行任务;以下是来自 Stack Install 角色的更新任务:

TASK [roles/stack_install : update apt-cache and upgrade packages] *************
skipping: [RedHat]
changed: [Debian]

如你所见,这个是 apt,而 dnf 的形式如下:

TASK [roles/stack_install : update all of the installed packages] **************
skipping: [Debian]
changed: [RedHat]

现在,进入 Stack Config 角色,这是在两个发行版上运行任务的地方:

TASK [roles/stack_config : add the wordpress group] *******
changed: [RedHat]
changed: [Debian]

要仅更新 Red Hat 发行版上的防火墙,我们需要执行以下操作:

TASK [roles/stack_config : run the firewall-cmd commands if the firewall-cmd --state command returns running] ***
skipping: [Debian] => (item=firewall-cmd --zone=public --add-port=80/tcp --permanent)
skipping: [Debian] => (item=firewall-cmd --zone=public --add-port=80/tcp)
skipping: [Debian]
changed: [RedHat] => (item=firewall-cmd --zone=public --add-port=80/tcp --permanent)
changed: [RedHat] => (item=firewall-cmd --zone=public –
-add-port=80/tcp)

最后,我们完成了 playbook 的运行:

PLAY RECAP ************************************************
Debian                     : ok=44   changed=29   unreachable=0    failed=0    skipped=7    rescued=0    ignored=2
RedHat                     : ok=45   changed=34   unreachable=0    failed=0    skipped=6    rescued=0    ignored=2

这意味着我现在应该有两个 WordPress 安装:

图 6.4 – WordPress 在 Ubuntu 22.04 上运行

图 6.4 – WordPress 在 Ubuntu 22.04 上运行

图 6.5 – WordPress 在 Rocky Linux 9.2 上运行

图 6.5 – WordPress 在 Rocky Linux 9.2 上运行

虽然前面的屏幕并不是最令人兴奋的网站,但正如你所看到的,我们已经在两种不同的操作系统上成功运行 WordPress。

在这一点上,如果你一直在跟着做,不要忘记删除任何你已部署用来运行 playbooks 的资源。

摘要

在本章中,我们修改了我们在第五章部署 WordPress中编写的 WordPress 安装 playbook,以便针对多个操作系统进行目标定位。我们通过使用 Ansible 内置的审计模块来确定 playbook 正在运行的操作系统,并且仅运行适用于这两个目标发行版的任务来实现这一点。

尽管我们所采用的方法针对多个 Linux 发行版是一种应用,但我相信你已经有了一些想法,可以在你的项目中使用我们使用的一些逻辑,比如基于虚拟机主机上的角色引导不同软件等。

此外,通过这种方法将你的角色发布到 Ansible Galaxy 时也是有益的,正如在第二章探索 Ansible Galaxy中所讨论的,通过使操作系统无关。

到目前为止,你可能已经注意到我们一直在针对 Linux 虚拟机进行目标定位;在下一章中,我们将探讨 Ansible 对基于 Windows 的操作系统的支持。

进一步阅读

第七章:Ansible Windows 模块

在本章中,我们将探讨内置的 Ansible 模块集,这些模块支持并与基于 Windows 的服务器进行交互;对于几乎完全依赖 macOS 和 Linux 背景的人来说,使用一个在 Windows 上不受原生支持的工具来管理 Windows 似乎有些奇怪。

在本章结束时,我相信您会同意,通过查看可用的选项,Ansible 的开发人员已经使得使用 Playbook 管理 Windows Server 工作负载尽可能简单和熟悉。

在本章中,我们将学习如何执行以下操作:

  • 在 Microsoft Azure 中启动 Windows 服务器实例

  • 启用 Windows 中的功能

  • 创建用户

  • 使用 Chocolatey 安装第三方软件包

本章涵盖以下主题:

  • 在 Azure 中启动 Windows 服务器

  • Ansible 准备工作

  • Windows Playbook 角色

  • 运行 Playbook

技术要求

与其在本地运行完整的 Windows Server 2022 虚拟机(VM),本章中我们将介绍如何安全地启动和配置托管在 Microsoft Azure 中的 Windows Server 2022 VM。如果您正在跟随学习,请确保已激活 Microsoft Azure 订阅并安装了 Azure 命令行界面CLI)。

有关如何安装和配置 Azure CLI 的详细信息,请参阅文档:learn.microsoft.com/en-us/cli/azure/install-azure-cli/。如果您在 Windows 主机上进行学习,请在您的 Windows Subsystem for Linux 安装中安装 Azure CLI,同时也安装 Ansible。

在 Azure 中启动 Windows 服务器

我们将不会像在 第九章 中所做的那样,使用 Ansible 部署 Azure 资源;相反,我们将使用 Azure CLI 来启动我们的 VM。

注意

由于本章中的某些命令会很长,我会用反斜杠来分隔它们。在 Linux 命令行中,反斜杠(\)后跟换行符表示命令的延续。这使您可以将单个命令分割成多行,以提高可读性。

首先切换到您所检出的存储库的 Chapter07 文件夹,并运行以下命令:

$ MYIP=$(curl https://api.ipify.org 2>/dev/null)
$ VMPASSWORD=$(openssl rand -base64 24)
$ echo $VMPASSWORD > VMPASSWORD

前两个命令在您的命令行上设置了两个变量;第一个使用 $MYIP 变量与当前网络会话的公共 IP 地址。

第二个命令使用 openssl 命令生成一个随机密码,并将其分配给名为 $VMPASSWORD 的变量。

第三个命令将 $VMPASSWORD 的内容复制到一个名为 VMPASSWORD 的文件中;此命令必须在与主机清单文件相同的文件夹中执行,因为它将在我们稍后将讨论的主机清单文件中调用。

注意

我将遵循 Azure 云采纳框架的资源命名建议,并在英国南部区域启动资源。

既然我们知道了 IP 地址并且有了密码,我们就可以开始使用 Azure CLI 启动资源。首先,我们需要确保已登录,可以运行以下命令:

$ az login

登录后,我们可以通过执行以下命令来创建一个Azure 资源组

$ az group create \
    --name rg-ansible-windows-server-uks \
    --location uksouth

Azure 资源组是我们将部署 Azure 资源的逻辑容器,首先将部署一个Azure 虚拟网络

以下命令将创建一个地址空间为10.0.0.0/24、单一子网为10.0.0.0/27的 Azure 虚拟网络;我们将在这里启动我们的 Windows Server:

$ az network vnet create \
    --resource-group rg-ansible-windows-server-uks \
    --name vnet-ansible-windows-server-uks \
    --address-prefix 10.0.0.0/24 \
    --subnet-name sub-vms \
    --subnet-prefix 10.0.0.0/27

现在,我们需要创建一个网络安全组,并在虚拟机启动后将其分配给网络接口。

我们需要这样做,因为我们将为虚拟机分配一个公共 IP 地址,我们不希望将三个管理端口直接暴露到互联网;相反,我们希望将它们限制为仅对我们开放:

$ az network nsg create \
    --resource-group rg-ansible-windows-server-uks \
    --name nsg-ansible-windows-server-uks

我们现在已经创建了一个空的网络安全组。接下来,我们添加一些规则,从允许所有人访问端口80以允许 HTTP 流量的规则开始:

$ az network nsg rule create \
   --resource-group rg-ansible-windows-server-uks \
   --nsg-name nsg-ansible-windows-server-uks \
   --name allowHTTP \
   --protocol tcp \
   --priority 100 \
   --destination-port-range 80 \
   --access allow

接下来,我们有一个规则,打开端口3389远程桌面使用该端口让你与主机建立会话;我们只希望该端口对我们开放,所以这里的命令如下:

$ az network nsg rule create \
    --resource-group rg-ansible-windows-server-uks \
    --nsg-name nsg-ansible-windows-server-uks \
    --name allowRDP \
    --protocol tcp \
    --priority 1000 \
    --destination-port-range 3389 \
    --source-address-prefix $MYIP/32 \
    --access allow

请注意,我们正在传递在启动资源时注册的$MYIP变量。这将传递你的 IP 地址,如你所见,我们随后在末尾附加了/32;这是无类域间路由CIDR)表示法,表示单个 IP 地址。

现在我们已经有了远程桌面规则,这也是我们作为终端用户连接到虚拟机的方式,我们需要打开Windows 远程管理WinRM)端口,这是 Ansible 将连接到机器的方式:

$ az network nsg rule create \
    --resource-group rg-ansible-windows-server-uks \
    --nsg-name nsg-ansible-windows-server-uks \
    --name allowWinRM \
    --protocol tcp \
    --priority 1050 \
    --destination-port-range 5985-5986 \
    --source-address-prefix $MYIP/32 \
    --access allow

我们需要运行的下一个命令是启动虚拟机(VM)本身并配置它,以便使用我们刚刚启动的核心网络组件:

$ az vm create \
     --resource-group rg-ansible-windows-server-uks \
     --name vm-ansible-windows-server-uks \
     --computer-name ansibleWindows \
     --image Win2022Datacenter \
     --admin-username azureuser \
     --admin-password $VMPASSWORD \
     --vnet-name vnet-ansible-windows-server-uks \
     --subnet sub-vms \
     --nsg nsg-ansible-windows-server-uks \
     --public-ip-sku Standard \
     --public-ip-address-allocation static

如你所见,我们正在指示 Azure CLI 启动一个虚拟机,使用rg-ansible-windows-server-uks资源组,并使用我们通过之前的命令启动的所有网络资源。

你可能会想,太好了,接下来可以继续看 Ansible 了。然而,在使用 Ansible 连接到虚拟机之前,我们还需要运行一个命令——原因是尽管我们已经有了 Windows 2022 服务器,但默认情况下 WinRM 协议并未启用。

启用此功能的命令如下:

$ az vm extension set \
    --resource-group rg-ansible-windows-server-uks \
    --vm-name vm-ansible-windows-server-uks \
    --name CustomScriptExtension \
    --publisher Microsoft.Compute \
    --version 1.10 \
    --settings "{'fileUrls': ['https://raw.githubusercontent.com/PacktPublishing/Learn-Ansible-Second-Edition/main/Scripts/ConfigureRemotingForAnsible.ps1'],'commandToExecute': 'powershell -ExecutionPolicy Unrestricted -File ConfigureRemotingForAnsible.ps1'}"

这会在我们刚刚部署的 Azure 虚拟机上启用一个虚拟机扩展。虚拟机扩展有多种类型;我们使用的是自定义脚本扩展。此扩展从传递给它的 URL 下载脚本,然后执行命令;在我们的案例中,我们正在从与本书一起提供的 GitHub 仓库下载配置 WinRM 的脚本。

你可以看到将从以下 URL 下载的脚本:raw.githubusercontent.com/PacktPublishing/Learn-Ansible-Second-Edition/main/Scripts/ConfigureRemotingForAnsible.ps1

脚本下载后运行的命令如下:

$ powershell -ExecutionPolicy Unrestricted -File ConfigureRemotingForAnsible.ps1

虚拟机扩展执行了上述命令,因此我们不需要直接运行它。

在 Azure 门户中,资源组的资源可视化工具应该显示如下所示的概览:

图 7.1 – 在 Azure 资源可视化工具中查看我们的资源

图 7.1 – 在 Azure 资源可视化工具中查看我们的资源

完成后,我们的 Windows Server 虚拟机已经准备好接受 Ansible 的操作。

Ansible 准备工作

如前一节所述,Ansible 将使用 WinRM 与我们的 Windows 主机进行交互。

信息

WinRM 提供了一种类似简单对象访问协议SOAP)的协议,叫做WS-Management。与提供交互式 Shell 来管理主机的安全外壳SSH)不同,WinRM 接受已执行的脚本,结果会返回给你。

Ansible 要求我们安装一些 Python 模块,以便它能够使用该协议;这些模块需要单独安装,因为它们通常不会与 Ansible 一起安装。

如果你使用的是 Ubuntu,运行以下命令来安装该模块:

$ sudo -H pip install pywinrm

在 macOS 上,运行以下命令:

$ pip install pywinrm

安装完成后,我们需要更新环境文件,指示 Ansible 使用 WinRM 协议,而不是 SSH。

我们更新后的hosts文件如下所示,它是来自附带仓库中Chapter07文件夹的hosts.example文件的副本。如果你在跟随练习,你需要更新你的文件,将 IP 地址更改为与你的 Azure 虚拟机匹配,一旦虚拟机启动:

WindowsServer ansible_host=123.123.123.123.nip.io
[ansible_hosts]
WindowsServer
[ansible_hosts:vars]
ansible_connection=winrm
ansible_user="azureuser"
ansible_password="{{ lookup('ansible.builtin.file', 'VMPASSWORD') }}"
ansible_winrm_server_cert_validation=ignore

文件的开始部分与我们迄今为止所熟悉的类似,包含了主机的名称和虚拟机的可解析主机名,再次使用了Nip.io服务(nip.io/)。

接下来,我们将命名的主机放入ansible_hosts组中,然后为该组定义一系列设置。

这些设置中的第一个指示 Ansible 通过将winrm设置为ansible_connection键的值来使用 WinRM。

接下来,我们设置 ansible_user 键;该值为 azureuser,我们在启动 Azure 虚拟机时定义的值;同时还设置了 ansible_password 键。

如果你还记得,在本章开始时,我们运行了以下命令:

$ echo $VMPASSWORD > VMPASSWORD

这将我们生成的随机密码,即 $VMPASSWORD,放入一个名为 VMPASSWORD 的文件中;这意味着当我们定义 ansible_password 键时,可以使用查找值,使用 {{ lookup('ansible.builtin.file', 'VMPASSWORD') }} 来读取 VMPASSWORD 文件的内容,而无需将密码硬编码到我们的环境文件中。

最后,我们告诉 Ansible 忽略任何证书错误,将 ansible_winrm_server_cert_validation 键设置为 false;我们需要这样做,因为 WinRM 已配置为使用自签名证书,这会导致证书错误,因为我们的本地机器不知道信任该证书。

现在我们已经启动了 Windows 并配置了 Ansible,可以开始与其交互了。

ping 模块

并不是所有 Ansible 模块都能与 Windows 主机配合使用,ansible.builtin.ping 就是其中之一。

如果你运行以下命令:

$ ansible WindowsServer -i hosts -m ansible.builtin.ping

然后你会看到一个详细的错误,包含以下警告:

[WARNING]: No python interpreters found for host WindowsServer (tried ['python3.11', 'python3.10',
'python3.9', 'python3.8', 'python3.7', 'python3.6', 'python3.5', '/usr/bin/python3',
'/usr/libexec/platform-python', 'python2.7', '/usr/bin/python', 'python'])

幸运的是,Windows 提供了一个名为 ansible.windows.win_ping 的模块,所以让我们更新命令以运行这个模块:

$ ansible WindowsServer -i hosts -m ansible.windows.win_ping

这将返回你期望接收到的 ping 结果:

WindowsServer | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

接下来我们将查看的模块不需要做任何更改,就像我们在 Linux 主机上运行时一样。

setup 模块

如之前所述,我们需要运行该模块并定位到我们的主机,因此命令如下:

$ ansible WindowsServer -i hosts -m ansible.builtin.setup

这将返回主机的信息,就像在对我们的 Linux 主机执行相同模块时一样,以下截图中可以看到部分输出:

图 7.2 – 来自 setup 模块的一些输出

图 7.2 – 来自 setup 模块的一些输出

这是为数不多的可以同时在 Windows 和 Linux 主机上运行的模块之一。

现在我们已确认主机可以访问,让我们看看需要在 playbook 中做出的更改。

Windows Playbook 角色

整个 playbook 可以在与本书一起提供的存储库中的 Chapter 07 文件夹中找到,因此我将不再在本章中介绍如何创建角色,因为我们在前几章已经详细讲解过了。

启用 Windows 功能

两个角色涉及如何启用功能;第一个角色,名为 iis,在我们的 Windows Server 上启用 Internet Information Services (IIS)。

信息

IIS 是 Windows Server 默认随附的 Web 服务器,支持以下协议:HTTP、HTTPS 和 HTTP/2,以及 FTP、FTPS、SMTP 和 NNTP。它于 1995 年作为 Windows NT 的一部分首次发布。

roles/iis/defaults/main.yml 中有一些默认变量;这些定义了需要复制到服务器上的位置,并包括我们将复制到主机的 HTML 文件内容:

document_root: 'C:\inetpub\wwwroot\'
html_file: "ansible.html"
html_heading: "Success !!!"
html_body: |
  This HTML page has been deployed using Ansible to a <b>{{ ansible_distribution }}</b> host.<br><br>
  The weboot is <b>{{ document_root }}</b> this file is called <b>{{ html_file }}</b>.<br>

然后在 roles/iis/tasks/main.yml 中有两个任务。第一个任务是 魔法发生的地方

- name: "Enable IIS"
  ansible.windows.win_feature:
    name:
      - "Web-Server"
      - "Web-Common-Http"
    state: "present"

我说 魔法发生的地方 是因为作为一名 Linux 系统管理员,我很少接触 Windows 主机。

尽管如此,正如您从前面的任务中看到的那样,Ansible 给了我们一个类似 Linux 的体验,这意味着我不需要卷起袖子过多地深入 Windows 的内部。

该任务使用 ansible.windows.win_feature 模块来启用 Web-ServerWeb-Common-Http 功能;由于我们坚持使用默认的开箱即用设置,因此除了将一个 HTML 文件复制到文档根目录之外,不需要进行任何额外的配置:

- name: "Create an html file from a template"
  ansible.windows.win_template:
    src: "index.html.j2"
    dest: "{{ document_root }}{{ html_file }}"

如您所见,我们正在使用一个 Jinja2 模板文件,其中的简化版本如下所示:

<!--{{ ansible_managed }}-->
<title>{{ html_heading }}</title>
<article>
    <h1>{{ html_heading }}</h1>
    <div>
        <p>{{ html_body }}</p>
    </div>
</article>

但是,我们并没有使用 ansible.builtin.template,而是使用了 ansible.windows.win_template,这是 Windows 模块版本,正如您已经猜到的那样。

假设我们使用的是 ansible.builtin.template 版本,我们将遇到与运行 ansible.builtin.ping 模块时相同的错误,并且会抱怨 Python 没有安装。

下一个角色扩展了 iis 文件并启用了 .Net;该角色名为 dotnet

同样,roles/dotnet/defaults/main.yml 中也有一些默认变量:

aspx_document_root: 'C:\inetpub\wwwroot\ansible\'
aspx_file: "default.aspx"
aspx_heading: "Success !!!"
aspx_body: |
  This HTML page has been deployed using Ansible to a <b>{{ ansible_distribution }}</b> host.<br><br>
  The weboot is <b>{{ aspx_document_root }}</b> this file is called <b>{{ aspx_file }}</b>.<br><br>
  The output below is from ASP.NET<br><br>
  Hello from <%= Environment.MachineName %> at <%= DateTime.UtcNow %><br><br>

如您所见,这一次,正文中包含了一些内联代码。

然而,您可能还没有注意到我们在变量中定义路径时的微妙差异。对于我们的 Windows 工作负载的两个任务,路径变量已如下所示定义:

document_root: 'C:\inetpub\wwwroot\'
aspx_document_root: 'C:\inetpub\wwwroot\ansible\'

但是,如果我们查看在 第五章《部署 WordPress》中如何定义路径,就会发现有一个至关重要的区别:

wordpress_system:
    home: "/var/www/wordpress"

区别并不是我们使用了 wordpress_system.home 作为变量;它比这更微妙。

如果您注意到 Windows 工作负载的路径使用单引号,而 Linux 的路径使用双引号,给自己点个赞。

在 Ansible 中,包围字符串的单引号(')被视为字面量,这样可以确保特殊字符不会被解释或展开,这使得它们非常适合 Windows 路径。

双引号(")允许进行字符串插值,这意味着嵌入的 Jinja2 模板表达式或特殊字符将被展开。它们还支持转义序列,例如用于换行的\n,因为许多转义序列,如我们路径中的\,可能会导致问题。

如果我们需要使用双引号,因为我们需要传递某些需要展开的内容,那么您可以像这样使用双斜杠(\\):

document_root: "C:\\inetpub\\wwwroot\\"
aspx_document_root: "C:\\inetpub\\wwwroot\\ansible\\"

然而,这可能会让路径的阅读变得混乱,所以我在示例中使用了单引号——现在回到角色。

roles/dotnet/tasks/main.yml 中的四个任务中,第一个任务启用了 .Net

- name: "Enable .NET"
  ansible.windows.win_feature:
    name:
      - "Net-Framework-Features"
      - "Web-Asp-Net45"
      - "Web-Net-Ext45"
    state: "present"
  notify: "Restart IIS"

如果检测到任何更改,我们还通过处理程序触发 IIS 的重启;这使用了 ansible.windows.win_service

- name: "Restart IIS"
  ansible.windows.win_service:
    name: "w3svc"
    state: "restarted"

下一个任务是如果文件夹不存在,则创建一个文件夹:

- name: "Create the folder for our asp.net app"
  ansible.windows.win_file:
    path: "{{ aspx_document_root }}"
    state: "directory"

再次调用了我们之前使用过的一个现有模块,这次是 ansible.windows.win_file。接下来,我们将文件复制到我们刚创建的文件夹:

- name: "Create an aspx file from a template"
  ansible.windows.win_template:
    src: "default.aspx.j2"
    dest: "{{ aspx_document_root }}{{ aspx_file }}"

角色中的最后一个任务配置 IIS,以便它知道我们现在正在运行一个应用程序:

- name: "Ensure the default web application exists"
  community.windows.win_iis_webapplication:
    name: "Default"
    state: "present"
    physical_path: "{{ aspx_document_root }}"
    application_pool: "DefaultAppPool"
    site: "Default Web Site"

在运行 playbook 之前,还有一些角色需要覆盖;让我们来看下一个角色。

创建用户

该角色为我们创建一个用户,以便我们能够连接到实例。可以在 roles/user/defaults/main.yml 中找到的默认设置如下:

ansible:
  username: "ansible"
  password: "{{ lookup('password', 'group_vars/generated_password chars=ascii_letters,digits length=30') }}"
  groups:
    - "Users"
    - "Administrators"

如你所见,在这里我们定义了一个名为 ansible 的用户,并设置了一个 30 个字符的随机 password,如果不存在,Ansible 会使用查找插件创建该密码。ansible 用户将是 UsersAdministrators 组的成员。

roles/user/tasks/main.yml 中有一个任务,使用 ansible.windows.win_user 模块,任务如下所示:

- name: "Ensure that the ansible created users are present"
  ansible.windows.win_user:
    name: "{{ ansible.username }}"
    fullname: "{{ ansible.username | capitalize }}"
    password: "{{ ansible.password }}"
    state: "present"
    groups: "{{ ansible.groups }}"

和所有 Windows 模块一样,语法类似于 Linux 等效模块,因此你应该知道每个键的含义。如前一个任务所示,我们使用 Jinja2 转换将 ansible.username 变量的首字母大写。

使用 Chocolatey 安装应用程序

下一个角色叫做 choco,它使用 Chocolatey 在机器上安装一些软件。

信息

Chocolatey 是 Windows 对 macOS Homebrew 的回应——一个简化软件安装的包管理器。就像我们之前使用 Homebrew,Chocolatey 将典型的 Windows 安装封装成整洁的 PowerShell 命令,使其成为 Ansible 等自动化工具的完美搭档。

roles/choco/defaults/main.yml 中,我们有一个包含我们想要安装的软件包列表的变量:

apps:
  - "notepadplusplus.install"
  - "putty.install"
  - "googlechrome"

正如你可能已经猜到的,这是安装应用程序的任务:

- name: "Install software using chocolatey"
  chocolatey.chocolatey.win_chocolatey:
    name: "{{ apps }}"
    state: "present"

再次,该模块接受类似于我们之前使用的包管理模块 ansible.builtin.aptansible.builtin.dnf 的输入。这意味着,Ansible 在多个操作系统之间,甚至不仅仅是不同的 Linux 发行版之间,使用一致的逻辑来处理类似的任务。

信息角色

最终的角色叫做 info;它的唯一作用是在 playbook 执行完成后输出信息。该角色在 roles/info/tasks/main.yml 中定义了一个任务:

- name: "Print out information on the host"
  ansible.builtin.debug:
    msg: "You can connect to '{{ ansible_host }}' using the username of '{{ ansible.username }}' with a password of '{{ ansible.password }}'."

如你所见,这将提供我们所需的主机名,以创建远程桌面会话,同时确认我们应该使用的用户名和密码。

这就是我们在运行 playbook 时将调用的角色概览,现在我们可以开始执行了。

运行 Playbook

site.yml 在顶部缺少一些设置,因为我们正在针对 Windows 主机:

---
- name: "Install IIS, .NET, create user, install chocolatey and display info"
  hosts: "ansible_hosts"
  gather_facts: true
  vars_files:
    - "group_vars/common.yml"
  roles:
    - "iis"
    - "dotnet"
    - "user"
    - "choco"
    - "info"

如你所见,不需要设置 becomebecome_method 键,因为连接到主机后,我们不需要更改用户。

除此之外,文件的其余部分都符合预期,运行剧本的方式也是如此:

$ ansible-playbook -i hosts site.yml

由于后台有很多操作,运行时需要一点时间,你将会在第一次运行剧本时看到输出中的信息:

图 7.3 – 审查剧本输出

图 7.3 – 审查剧本输出

正如你从之前的输出中看到的,我使用的主机是 20.50.120.120.nip.io(这台主机早已终止,但如果你跟着操作,可以将其替换为你自己的主机)。

要查看我们上传的静态 HTML 和 .Net 页面,你可以访问20.50.120.120.nip.io/ansible.html20.50.120.120.nip.io/ansible/default.aspx,记得更新主机以反映你自己的主机地址。

你还可以使用输出中给出的凭据打开远程桌面会话;下面的截图展示了我们创建的用户会话,并使用 Google Chrome 打开页面,旁边是 Notepad++,这两个应用程序都是我们通过剧本安装的:

图 7.4 – 远程桌面会话

图 7.4 – 远程桌面会话

完成后,你可以运行以下 Azure CLI 命令来终止我们创建的所有资源:

$ az group delete \
    --name rg-ansible-windows-server-uks

请再次检查确保所有内容已经按照预期被删除,以确保不会收到任何意外账单。

总结

如本章开始时提到的那样,在 Windows 上使用我们认为的传统 Linux 工具(如 Ansible)总是有点奇怪。然而,我相信你会同意,这种体验尽可能地像 Linux。

当我第一次开始实验 Windows 模块时,我很惊讶于我居然能够在 Azure 中启动一台 Windows Server 并部署一个简单的 Web 应用程序,而无需远程桌面连接到目标实例。

随着每次新版本的发布,Ansible 对基于 Windows 的主机支持越来越好,使得从剧本中管理混合工作负载变得更加轻松。

在下一章,我们将研究 Ansible 中可用的网络模块。

深入阅读

你可以在chocolatey.org/上找到更多关于优秀的 Chocolatey 的信息。

第三部分:网络与云自动化

Ansible 的强大功能不仅限于管理服务器,它还可以自动化网络设备和云基础设施。在这里,我们将探索 Ansible 的网络模块,并讨论如何以编程方式与网络设备进行交互。接下来,我们将把注意力转向云计算,你将了解如何在流行的云平台如 Microsoft Azure 和 Amazon Web Services 上配置和管理资源。到本部分结束时,你将掌握使用 Ansible 自动化复杂云部署的技能。

本部分包括以下章节:

  • 第八章Ansible 网络模块

  • 第九章迁移到云端

  • 第十章构建云网络

  • 第十一章高度可用的云部署

  • 第十二章构建 VMware 部署

第八章:Ansible 网络模块

欢迎来到第八章;本章将探讨 Ansible 的一个常被忽视的使用案例,并深入了解 Ansible 庞大的网络模块社区。

本章将提供一个概述,而不是深入探讨每个集合——因为那可能需要一本完整的书——重点介绍这些模块的功能和灵活性。

本章讨论以下内容:

  • 制造商和设备支持

制造商和设备支持

到目前为止,我们一直在关注与服务器交互的模块。在我们的案例中,这些模块大多数是本地运行的。在接下来的章节中,我们将更多地与远程云托管的服务器进行通信。但是,在与远程服务器交互之前,我们应该先了解核心网络模块。

这些模块的设计旨在与各种网络设备交互并管理其配置,从传统的机架顶部交换机和完全虚拟化的网络基础设施,到防火墙和负载均衡器。Ansible 支持许多设备,从开源虚拟设备到硬件解决方案,其中一些设备的起始价格可能超过 50 万美元,具体取决于你的配置。

那么,这些集合和模块有什么共同点呢?

这些模块的共同点是,它们都与传统上配置复杂的设备进行交互,而这些设备在大多数部署和环境中,既是核心也是关键元素;毕竟,所有与它们连接的设备都需要某种程度的网络连接性。

这些模块为许多设备提供了一个标准接口,即 Ansible,避免了工程师直接访问这些设备的需要。相反,他们可以运行 Ansible playbook,这些 playbook 由经验丰富的网络工程师创建角色,以受控且一致的方式配置设备,只需更改几个变量。

使用 Ansible 管理这些关键核心基础设施的唯一缺点是,运行 Ansible 的主机需要能够访问设备上的管理接口或 API,这有时会引发一些安全问题。因此,Ansible 提供的关于如何管理网络设备的指南需要经过深思熟虑。

这些集合

以下是按命名空间顺序列出的集合;每个条目末尾列出了[命名空间.集合名称]

Apstra 可扩展操作系统 (EOS) [arista.eos]

有超过 30 个模块可以让你管理运行 EOS 的设备。这些模块可以让你操作访问控制列表ACLs)接口,配置边界网关协议BGP)设置,在设备上运行任意命令,管理主机名、接口配置、日志记录等。此外,还有一个模块允许你从每个设备收集事实数据。

此外,还提供了用于命令行和 HTTP API 交互的插件。

Check Point [check_point.mgmt]

Check Point 管理的 Ansible 集合包含许多模块;截至写作时,已有超过 250 个模块。

每个模块管理你 Check Point 设备的不同方面,如访问层、规则、管理员或通过 Web 服务 API 管理 Check Point 防火墙上的网络流量源。它们提供的功能包括从获取事实和添加或管理对象,到工作流功能,如批准和发布 Check Point 防火墙上的会话。

Cisco

考虑到 Cisco 设备类型和类别的数量,Cisco 命名空间中有多个集合。

Cisco 应用程序中心基础设施(ACI)[cisco.aci]

150 多个 ACI 模块用于管理 Cisco ACI 的各个方面,这是 Cisco 下一代 API 驱动网络堆栈的预期功能。

有多个模块可用于管理 Cisco ACI 的不同方面,如 AAAA 记录(这些是存储 IPv6 地址的地址记录)、角色、用户、证书、访问 SPAN 配置、桥接域BDs)和子网、BGP 路由反射器等。还有用于管理云应用程序配置文件和云 AWS 提供商配置的模块。

Cisco 自适应安全设备(ASA)[cisco.asa]

通过五个 ASA 模块,你可以管理访问控制列表、运行命令并管理物理和虚拟 Cisco ASA 设备的配置。

Cisco DNA Center (DNAC) [cisco.dnac]

Cisco DNAC 的 Ansible 集合包括近 400 个模块,用于管理 Cisco DNAC 部署的不同方面。这些模块涵盖了从获取接入点配置详情、管理应用策略、将设备分配到站点、导入认证证书、运行合规性检查,到管理配置模板等多个功能。

Cisco IOS 和 IOS XR [cisco.ios 和 cisco.iosxr]

这两个集合包含用于管理 Cisco IOS 和 IOS XR 驱动的设备的模块。你可以使用它们收集设备信息,并配置用户、接口、日志、横幅等。

身份服务引擎(ISE)[cisco.ise]

该集合管理你的 ISE;它包含多个模块,用于管理设置和配置,例如处理 ACI 绑定和设置、管理 Active Directory 设置、处理允许的协议、管理 ANC 端点和策略、管理备份配置和计划、处理证书等。

Cisco Meraki [cisco.meraki]

在这里,我们有近 500 个模块,用于管理 Meraki 部署的不同元素,如管理的身份、设备详情、摄像头设置、蜂窝网关配置和传感器关系。每个模块都旨在获取信息或修改设置,帮助你通过自动化管理 Cisco Meraki 设备。

Cisco 网络服务编排器(NSO)[cisco.nso]

一些模块允许你与 Cisco NSO 管理的设备进行交互。你可以执行 NSO 操作、查询你的安装数据,并验证你的配置,同时进行服务同步和配置。

Cisco 网络操作系统软件 (NX-OS) [cisco.nxos]

如你所想,管理运行 Cisco NXOS 的设备的模块非常多;有超过 80 个模块,涵盖了诸如管理 AAA 服务器配置、ACL、BGP 配置、执行任意命令、管理接口以及处理 Cisco NX-OS 设备上的各种其他配置和设置等功能。

Cisco 统一计算系统 (UCS) [cisco.ucs]

尽管这些模块不严格属于网络设备,但管理 Cisco 统一计算、存储和网络系统的模块包括一个允许你管理 DNS 服务器、IP 地址池、局域网连接策略、MAC 地址池、QoS 设置、VLAN 和 vNIC 的模块。其余模块允许你以编程方式管理刀片和机箱中的计算和存储。

F5 BIG-IP 命令式 [F5Networks.F5_Modules]

有 160 个模块,所有模块都以 BIG-IP 为前缀,允许你管理 F5 BIG-IP 应用交付控制器的各个方面。

Fortinet

Fortinet 命名空间中只有两个集合,但正如你从每个集合中的模块数量中看到的,它们功能非常丰富。

Fortinet FortiManager [fortinet.fortimanager]

总共有超过 1,100 个模块(没错,你没看错),包括配置防病毒配置文件和选项、管理 AP 本地配置文件和命令列表、配置自定义应用程序签名和防火墙应用程序组、管理互联网服务应用程序等。

Fortinet FortiOS v6 (fortinet.fortios)

尽管这个集合的模块比 FortiManager 集合少,但仍有超过 650 个模块用于配置防病毒设置、应用程序控制列表、身份验证方案和证书设置。

Free Range Routing (FRR) [Frr.Frr]

这里只有两个模块:一个允许你配置 BGP,另一个让你收集运行 FRR 的设备的事实数据。

Juniper Networks Junos [junipernetworks.junos]

总共有 40 个模块使你能够在播放本中与运行 Junos 的 Juniper 设备进行交互。这些模块包括标准的命令、配置和事实收集模块,也包括允许你安装包并将文件复制到设备上的模块。

Open vSwitch [Openvswitch.Openvswitch]

该命名空间中的四个模块允许你管理 OVS 虚拟交换机上的绑定、桥接、端口和数据库。

VyOS [vyos.vyos]

VyOS 集合包括用于管理 VyOS 设备上各种配置和资源的模块。 其中一些模块包括管理多行横幅、配置 BGP 全局设置和地址族设置、运行命令、管理防火墙设置、接口配置、日志记录、NTP、OSPF、SNMP、静态路由、系统命令、用户管理和 VLAN 配置等。

社区网络集合 [Community.Network]

此集合是所有没有专用命名空间或开发团队的其他网络模块的集合;模块前缀现在在方括号中。

A10 Networks [a10]

A10 模块支持 A10 Networks 的 AX、SoftAX、Thunder 和 vThunder 设备。 这些都是提供负载均衡的应用交付平台。

Cisco AireOS [aireos]

两个 AireOS 模块允许您与运行 AireOS 的 Cisco 无线局域网控制器进行交互。 其中一个模块将使您能够直接在设备上运行命令,另一个用于管理配置。

APCON [apcon]

一个模块允许您在 APCON 设备上运行命令。

Aruba 移动控制器 [aruba]

只有两个 Aruba 模块。 这些模块允许您管理 Hewlett Packard 的 Aruba 移动控制器的配置并执行命令。

Avi Networks [avi]

总共有 65 个 Avi 模块,允许您与 Avi 应用服务平台的所有方面进行交互,包括负载均衡和 Web 应用防火墙 (WAF) 功能。

Big Cloud Fabric 和 Big Switch Network [bcf + bigmon]

有三个 Big Switch Network 模块。Big Cloud Fabric (BCF) 允许您创建和删除 BCF 交换机。其余两个模块使您能够创建 Big Monitoring Fabric (Big Mon) 服务链和策略。

Huawei Cloud Engine [ce]

超过 75 个 Cloud Engine 模块使您能够管理来自华为的这些强大交换机的所有方面,包括 BGP、访问控制列表、MTU、静态路由、VXLAN 和 SNMP 配置。

Lenovo CNOS [cnos]

有近 30 个模块,允许您管理运行 Lenovo CNOS 操作系统的设备;它们使您能够配置从 BGP 和端口聚合到 VLAG、VLAN 以及在需要时恢复出厂设置的设备。

Arista Cloud Vision [cv]

一个模块允许您使用配置文件配置 Arista Cloud Vision 服务器端口。

illumos [dladm + flowadm + ipadm]

illumos 是 Open Solaris 操作系统的一个分支。其强大的网络功能使其成为作为自建路由器或防火墙部署的完美候选。 这些模块使您能够管理接口、NetFlow 和隧道。此外,由于 illumos 是 Open Solaris 的一个分支,您的剧本应该也适用于基于 Open Solaris 的操作系统。

Ubiquiti EdgeOS [edgeos + edgeswitch]

EdgeOS 模块使您能够管理配置、执行临时命令并收集运行 EdgeOS 设备(如 Ubiquiti Edge 路由器)上的事实。

也有一些模块用于 Edge 交换机。

联想企业网络操作系统 [enos]

有三个模块用于 Lenovo ENOS。像其他设备一样,它们允许你收集信息、执行命令并管理配置。

Ericsson [eccli]

这个单一模块允许你在运行爱立信命令行界面的设备上执行命令。

ExtremeXOS [exos + nos + slxos]

这六个模块允许你与 Extreme Networks 交换机上的 ExtremeXOS、Extreme Networks SLX-OS 和 Extreme Networks NOS 软件进行交互。

Cisco Firepower 威胁防御 [ftd]

一些模块允许你配置并上传/下载文件到 Cisco Firepower 威胁防御设备。

Itential 自动化平台 [iap]

一些模块允许你与托管在 Itential 自动化平台上的工作流进行交互,以及为混合云网络提供低代码自动化和编排。

Ruckus ICX 7000 [icx]

这些模块允许你配置 Ruckus ICX 7000 系列校园交换机。

Ingate 会话边界控制器 [ig]

虽然这些主要用于SIP,或者说它的全名是会话启动协议服务,但也有一些模块帮助配置网络元素。

NVIDIA 网络命令行工具 [nclu]

一个单一模块允许你在兼容设备上使用 NVIDIA 网络命令行工具管理网络接口。

诺基亚 NetAct [netact]

单个模块允许你上传并应用由诺基亚 NetAct 驱动的核心和无线网络。

Citrix Netscaler [netscaler]

这些模块旨在管理和配置 Netscaler 设备的各个方面。它们涵盖的功能包括内容交换、全球服务器负载均衡GSLB)、负载均衡、发出 Nitro API 请求、保存配置,以及管理服务器配置、服务、服务组和 SSL 证书密钥。

诺基亚 Nuage 网络虚拟化服务平台(VSP)[nuage]

有一个单一模块允许你管理诺基亚 Nuage 网络 VSP 上的企业。

OpenSwitch [opx]

一个单一模块,通过利用 CPS API 在运行 OpenSwitch 的网络设备上对 YANG 对象执行指定操作。

Ordnance 虚拟路由器 [ordnance]

有两个模块:一个用于管理配置,另一个用于收集 Ordnance 虚拟路由器的信息。

Pluribus Networks Netvisor OS [pn]

这 40 个模块允许你管理你的Pluribus NetworksPN)Netvisor OS 驱动的设备,从创建集群和路由器到在白盒交换机上执行命令。

诺基亚网络服务路由器操作系统 [sros]

有三个模块可以让你对诺基亚网络的 SROS 设备执行命令、配置以及回滚更改。

Radware [vidrect]

少量模块允许你通过 vDirect 服务器管理 Radware 设备。

Ansible Net Common [ansible.netcommon]

最终的集合是一组模块,可以视为帮助支持本章中所有设备的工具。这些模块能够 ping 目标并使用自定义提示和答案运行通用命令。

总结

我怀疑你们中的大多数人可能没听说过本章列出的许多设备,对于你们听说过的设备——比如思科的设备——你们可能也没有直接接触过它们,所有的配置工作都交给了网络管理员。

当我们在第十五章,“使用 GitHub Actions 和 Azure DevOps 调用 Ansible”以及第十六章,“介绍 Ansible AWX 和 Red Hat Ansible 自动化平台”中讨论通过 CI/CD 触发 Ansible 时,我们将了解一些部署选项,这些选项可能有助于缓解我们在章节开头提到的一些问题,比如关于运行 Ansible playbook 的主机需要与潜在的核心基础设施保持视距的问题。

在我们进入这些章节之前,我们将探讨如何将工作负载迁移到云端,这一过程将在下一章开始。

进一步阅读

第九章:向云端迁移

本章将从使用本地虚拟机转向使用 Ansible 启动与公共云提供商交互的实例。

本章中,我们将使用 Microsoft Azure,选择该云服务提供商是因为它允许我们启动虚拟机并与其进行交互,而无需过多的配置工作。

我们将继续研究如何调整我们的 WordPress 剧本以便与新启动的 Microsoft Azure 实例进行交互。

在本章中,我们将涵盖以下主题:

  • Microsoft Azure 介绍

  • 在 Microsoft Azure 启动实例

  • 启动 WordPress

技术要求

在本章中,我们将启动一个公共云实例,因此如果你跟随本书操作,你将需要一个 Microsoft Azure 账户。与其他章节一样,完整的剧本版本可以在章节文件夹中找到,地址为github.com/PacktPublishing/Learn-Ansible-Second-Edition/tree/main/Chapter09

Microsoft Azure 介绍

2008 年,微软通过推出 Windows Azure,迈出了其进入云计算领域的第一步,Windows Azure 是一种基于云的数据中心服务。这次发布标志着许多人所认为的传统软件公司历史的一个转折点,宣告着微软在战略上转向云计算。

Windows Azure 是在内部项目Project Red Dog下开发的,代表了微软对日益增长的可扩展、可访问和灵活计算资源需求的回应。

Windows Azure 最初推出时包含五个核心组件,每个组件都旨在提供云计算领域内不同的功能:

  • Microsoft SQL 数据服务:此组件提供了 Microsoft SQL 数据库的云版本,简化了在云环境中托管和管理数据库的复杂性。

  • Microsoft .NET 服务:作为一种平台即服务PaaS)产品,它使开发人员能够在 Microsoft 管理的运行时环境中部署基于 .NET 的应用程序,从而简化了开发过程。

  • Microsoft SharePointMicrosoft Dynamics:这些软件即服务SaaS)产品提供了公司知名的企业内联网和客户关系管理CRM)产品的云版本,提升了协作与客户互动。

  • Windows Azure(IaaS):一种基础设施即服务IaaS)解决方案,允许用户创建并控制虚拟机、存储和网络服务,处理各种计算工作负载。

之前的四个定义摘自我写的旧书《基础设施即代码 初学者指南》。

Windows Azure 架构的核心是 Red Dog 操作系统,它是一个特别修改过的 Windows NT 版本。该系统被设计为包含一个云层,确保数据中心服务的顺利交付。

到 2014 年,随着服务范围的扩大和对基于 Linux 的工作负载的重视,Microsoft 将该服务更名为 Microsoft Azure。这一变化突显了该平台超越 Windows 为中心的解决方案的演变。

快进到 2020 年,显然 Microsoft Azure 已经采取了更加包容的方法,超过一半的虚拟机核心和大量 Azure Marketplace 镜像都是基于 Linux 的。

这一转变展示了 Microsoft 更广泛采用 Linux 和开源技术,这些技术在写作时仍然是其当前云服务产品的重要组成部分。

在 Microsoft Azure 中启动实例

如果你跟着第七章《Ansible Windows 模块》一起操作,你应该已经使用 Azure CLI 在 Microsoft Azure 中启动了一个虚拟机。

提醒

有关如何安装和配置 Azure CLI 的说明,请参阅learn.microsoft.com/en-us/cli/azure/install-azure-cli/中的文档。记住,如果你是在 Windows 主机上操作,确保在安装 Ansible 的同一位置,在 Windows Subsystem for Linux 中安装 Azure CLI。

在讨论启动 Windows 虚拟机时,我们做了以下工作:

  • 我们创建了一个资源组,以便将所有虚拟机工作负载的资源收集在一起。

  • 然后我们创建了一个虚拟网络和子网,并将其附加到机器的网络接口上。

  • 然后我们创建了一个网络安全组来保护我们的虚拟机。

  • 一旦我们有了基础,我们启动了一个 Windows 虚拟机,并将一个公共 IP 地址直接附加到网络接口上。

  • 最后,我们部署了一个虚拟机扩展,在我们的 Windows 主机上执行 PowerShell 脚本,以启用 WinRM 协议,允许我们使用 Ansible 连接并与主机进行交互。

本章将使用 Ansible 和 Azure 模块集重复、调整并补充这些步骤。

为 Microsoft Azure 准备 Ansible

在我们深入探讨 Ansible 角色之前,该角色将启动我们的资源,我们需要做一些准备工作;首先,让我们通过运行以下命令来确保安装了 Azure 集合:

$ ansible-galaxy collection install azure.azcollection

接下来,我们必须安装允许 Azure 集合与 Azure API 交互的 Python 模块。为此,我们需要运行以下命令:

$ pip3 install -r ~/.ansible/collections/ansible_collections/azure/azcollection/requirements-azure.txt

在安装了必要的支持 Python 模块后,下一步是确保你已使用 Azure CLI 登录到你的 Microsoft Azure 帐户。为此,请运行以下命令并按照屏幕上的提示进行操作:

$ az login

如果你使用的帐户可以访问多个 Azure 订阅,请确保选择了你打算启动资源的订阅。

为此,你可以列出所有订阅,并在需要时通过运行以下命令切换到正确的订阅:

$ az account list --output table
$ az account set --subscription <subscription_id>

确保您将<subscription_id>替换为从az account list命令获取的正确订阅 ID。

注意

使用az account set命令只会影响当前会话;如果关闭终端窗口并重新打开新会话,您必须确保再次更改订阅。

审查变量

我们将在多个角色中使用几个变量来部署 Azure 资源并配置 WordPress。我们首先要查看的变量可以在group_vars/common.yml文件中找到。

首先,我们有一些debug_output,它会输出在 playbook 运行期间注册的变量内容;将其设置为true可以帮助我们在开发角色时拉取已启动的 Azure 资源信息。

第二个功能标志是generate_key;如果设置为true,那么 Ansible 将在~/.ssh/id_rsa目录下创建一个公私钥对(如果该对不存在的话)。

Playbook 在启动虚拟机时会使用此位置的密钥,因此必须确保该密钥存在,否则 Ansible 无法连接到新启动的虚拟机。

这两个变量如下所示:

debug_output: false
genterate_key: false

接下来,在group_vars/common.yml文件中,我们定义了一些关于我们的app工作负载的信息;这些信息包含了应用程序的细节以及一些 Azure 相关的内容,如工作负载将要启动的 Azure 区域(locationlocation_short),以及我们的 WordPress 网站可访问的名称(public_dns_name):

app:
  name: "learnansible-wordpress"
  shortname: "ansiblewp"
  location: "westeurope"
  location_short: "euw"
  env: "prod"
  public_dns_name: "learnansible"

最后一组变量定义在group_vars/common.yml文件中,用于应用到每个 Azure 资源的标签,这些资源将由 Ansible 启动:

common_tags:
  "project": "{{ app.name }}"
  "environment": "{{ app.env }}"
  "deployed_by": "ansible"

我们将使用的下一组变量可以在roles/azure/defaults/main.yml中找到,这些变量用于部署我们的资源。

第一组变量定义了一个快速的 Azure 服务名称字典,方便我们在命名资源时使用:

dict:
  ansible_warning: "Resource managed by Ansible"
  load_balancer: "lb"
  network_interface: "nic"
  nsg: "nsg"
  private_endpoint: "pe"
  public_ip: "pip"
  resource_group: "rg"
  subnet: "snet"
  virtual_machine: "vm"
  virtualnetwork: "vnet"

接下来,我们定义资源名称——根据第七章《Ansible Windows 模块》中的内容,我将资源名称尽可能接近云采用框架的推荐做法:

load_balancer_name: "{{ dict.load_balancer }}-{{ app.name }}-{{app.env}}-{{ app.location_short }}"
load_balancer_public_ip_name: "{{ dict.public_ip }}-{{ load_balancer_name }}"
nsg_name: "{{ dict.nsg }}-{{ app.name }}-{{app.env}}-{{ app.location_short }}"
resource_group_name: "{{ dict.resource_group }}-{{ app.name }}-{{app.env}}-{{ app.location_short }}"
virtual_network_name: "{{ dict.virtualnetwork }}-{{ app.name }}-{{app.env}}-{{ app.location_short }}"
vm_name: "{{ dict.virtual_machine }}-admin-{{ app.name }}-{{app.env}}-{{ app.location_short }}"
vnet_name: "{{ dict.virtualnetwork }}-{{ app.name }}-{{app.env}}-{{ app.location_short }}"

现在所有的命名工作完成后,我们可以开始定义网络相关的变量:

vnet_config:
  cidr_block: "10.0.0.0/24"
  subnets:
    - {
        name: "{{ dict.subnet }}-vms-{{ app.name }}-{{app.env}}-{{ app.location_short }}",
        subnet: «10.0.0.0/27»,
        private: true,
        service_endpoints: «Microsoft.Storage»,
      }

接下来,在网络配置中,我们有两个 IP 地址列表——一个是固定 IP 地址,另一个是在 playbook 运行时发现的 IP 地址:

trusted_ips:
  - ""
dynamic_ips:
  - "{{ your_public_ip }}"

下一组变量使用先前的 IP 地址列表,在创建两个网络安全组规则时使用这些列表:

nsg_rules:
  - name: "allowHTTP"
    description: "{{ dict.ansible_warning }}"
    protocol: «Tcp»
    destination_port_range: «80»
    source_address_prefix: "*"
    access: "Allow"
    priority: "100"
    direction: "Inbound"
  - name: "allowSSH"
    description: "{{ dict.ansible_warning }}"
    protocol: "Tcp"
    destination_port_range: «{{ load_balancer.ssh_port }}»
    source_address_prefix: "{{ trusted_ips|select() + dynamic_ips | unique }}"
    access: "Allow"
    priority: "150"
    direction: "Inbound"

如您所见,第一个规则allowHTTP打开了80端口给全世界;但是allowSSH则限制了 SSH 端口,仅允许我们两个列表中的 IP 地址访问。为此,我们从trusted_ips变量中获取 IP 地址列表,追加dynamic_ips的内容,然后仅显示列表中的唯一条目,以去除任何重复项。

最后一组网络变量定义了启动 Azure 负载均衡器所需的基本设置:

load_balancer:
  ssh_port: "22"
  ssh_port_backend: "22"
  http_port: "80"
  http_port_backend: "80"

现在我们有了虚拟机配置:

vm_config:
  admin_username: "adminuser"
  ssh_password_enabled: false
  vm_size: "Standard_B1ms"
  image:
    publisher: "Canonical"
    offer: "0001-com-ubuntu-server-jammy"
    sku: "22_04-LTS"
    version: "latest"
  disk:
    managed_disk_type: "Premium_LRS"
    caching: "ReadWrite"
  key:
    path: "/home/adminuser/.ssh/authorized_keys"
    data: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"

最后,我们将放置定义新启动的虚拟机的位置和主机组的两个变量:

location: "{{ app.location }}"
hosts_group: "vmgroup"

现在我们已经涵盖了启动 Azure 资源所需的所有变量,我们可以开始执行实际工作的任务,所有这些任务都可以在 roles/azure/tasks/main.yml 中找到。

资源组任务

我们将要看的第一个任务是创建资源组,这是所有其他 Azure 资源的存放地点:

- name: "Create the resource group"
  azure.azcollection.azure_rm_resourcegroup:
    name: "{{ resource_group_name }}"
    location: "{{ location }}"
    tags: "{{ common_tags }}"
  register: "resource_group_output"

正如您所见,这里没有太多内容;它使用我们定义的 namelocationtags 变量,并使用 azure.collection.azure_rm_resourcegroup 模块创建资源组。然后将任务输出注册为变量,允许我们在后续任务中重用输出。

如果 debug_output 设置为 true,则下一个任务将在屏幕上打印 resource_group_output 注册变量的内容;如果设置为 false,则跳过该任务:

- name: "Debug - Resource Group result"
  ansible.builtin.debug:
    var: "resource_group_output"
  when: debug_output

这是 Azure 角色中常见的模式,因此我们不会再次涵盖这个任务。假设如果任务注册了其输出,那么后续将有支持调试任务。现在我们有了我们的资源组,我们可以开始配置网络了。

网络任务

第一个任务启动了虚拟网络,并将其放置在我们刚刚创建的资源组中:

- name: "Create the virtual network"
  azure.azcollection.azure_rm_virtualnetwork:
    resource_group: "{{ resource_group_output.state.name }}"
    name: "{{ virtual_network_name }}"
    address_prefixes: "{{ vnet_config.cidr_block }}"
    tags: "{{ common_tags }}"
  register: "virtual_network_output"

正如您所见,当引用资源组名称时,我们使用前一个任务的注册输出,使用 {{ resource_group_output.state.name }}。同样,这将贯穿剩余的任务。

注意,我们在创建虚拟网络时未定义子网;这是因为我们仅添加了一个子网,但使用 azure.collection.azure_rm_subnet 模块添加子网是最佳实践,因为这种方法可以通过 with_items 语句循环添加子网:

- name: "Add the subnets to the virtual network"
  azure.azcollection.azure_rm_subnet:
    resource_group: "{{ resource_group_output.state.name }}"
    name: "{{ item.name }}"
    address_prefix: "{{ item.subnet }}"
    virtual_network: "{{ virtual_network_output.state.name }}"
    service_endpoints:
      - service: "{{ item.service_endpoints }}"
  with_items: "{{ vnet_config.subnets }}"
  register: "subnet_output"

现在虚拟网络已经填充了子网,我们可以继续创建网络安全组。

您可能还记得,当我们查看变量时,我们使用了一个名为 your_public_ip 的变量,所以我们的下一个任务是使用 community.general.ipify_facts 模块来发现运行 Ansible 的主机的外部 IP 地址:

- name: "Find out your current public IP address using https://ipify.org/"
  community.general.ipify_facts:
  register: public_ip_output

正如您所见,这里没有太多内容,但我们没有注册一个名为 your_public_ip 的变量;这是作为一个单独的任务完成的,使用 ansible.builtin.set_fact 模块:

- name: "Register your public ip as a fact"
  ansible.builtin.set_fact:
    your_public_ip: "{{ public_ip_output.ansible_facts.ipify_public_ip }}"

现在我们知道了 IP 地址,我们可以创建网络安全组了:

- name: "Create the network security group"
  azure.azcollection.azure_rm_securitygroup:
    resource_group: "{{ resource_group_output.state.name }}"
    name: "{{ nsg_name }}"
    rules: "{{ nsg_rules }}"
    tags: "{{ common_tags }}"
  register: "nsg_output"

到目前为止,一切顺利;我们需要做的下一个网络配置是启动 Azure 负载均衡器。这是我们在第七章Ansible Windows 模块中启动的资源的第一个偏离,原因是什么呢?

虽然 Microsoft 允许你直接将公共 IP 地址分配给 Azure 中虚拟机的网络接口,但通常不推荐这样做,并且这不是最佳实践——使用像 Azure 负载均衡器这样的网络资源来路由和分发流量到一个或多个主机被认为更为安全,因为这样你就把虚拟机和公共互联网之间增加了一层防护。

此外,即使像我们这样运行单台虚拟机,通过负载均衡器传递流量也能让你进行基本的健康检查,查看负载均衡器发送流量的端口是否健康。

启动 Azure 负载均衡器时,我们需要执行的第一个任务是创建一个公共 IP 地址资源,这个资源将在启动负载均衡器时附加到负载均衡器上:

- name: "Create the public IP address needed for the load balancer"
  azure.azcollection.azure_rm_publicipaddress:
    resource_group: "{{ resource_group_output.state.name }}"
    allocation_method: "Static"
    name: "{{ load_balancer_public_ip_name }}"
    sku: "standard"
    domain_name: "{{ app.public_dns_name }}"
    tags: "{{ common_tags }}"
  register: "public_ip_output"

现在公共 IP 地址已经定义,我们可以继续配置 Azure 负载均衡器本身。

由于任务内容较多,我会在执行过程中稍作拆解:

- name: "Create load balancer using the public IP we created"
  azure.azcollection.azure_rm_loadbalancer:
    resource_group: "{{ resource_group_output.state.name }}"
    name: "{{ load_balancer_name }}"
    sku: "Standard"

任务的下一个环节是定义负载均衡器的前端。这里是我们将刚才创建的公共 IP 地址附加到负载均衡器的位置:

    frontend_ip_configurations:
      - name: "{{ load_balancer_name }}-frontend-ip-config"
        public_ip_address: "{{ public_ip_output.state.name }}"

接下来,我们定义后端池。这是我们的虚拟机将被放置并接收流量的池。如果我们有多个虚拟机,它们都将被指派到这个池中:

    backend_address_pools:
      - name: "{{ load_balancer_name }}-backend-address-pool"

现在我们有了健康探针,它会探测后端池上的 HTTP 端口,以确保虚拟机准备好接收端口 80 上的流量,方法是检查该端口是否开放:

    probes:
      - name: "{{ load_balancer_name }}-http-probe"
        port: «{{ load_balancer.http_port_backend }}»
        fail_count: "3"
        protocol: "Tcp"

对于我们的 WordPress 工作负载,我们希望暴露 HTTP 端口。为此,我们将创建一个负载均衡规则,允许你在后端池中的一个或多个虚拟机之间建立一对多的关系。此规则会将 HTTP 端口暴露在负载均衡器上,并将流量发送到后端虚拟机的 HTTP 端口。如果我们有多个虚拟机,流量将均匀分布到后端所有主机的 HTTP 端口上:

    load_balancing_rules:
      - name: "{{ load_balancer_name }}-rule-http"
        frontend_ip_configuration: "{{ load_balancer_name }}-frontend-ip-config"
        backend_address_pool: "{{ load_balancer_name }}-backend-address-pool"
        frontend_port: «{{ load_balancer.http_port }}»
        backend_port: "{{ load_balancer.http_port_backend }}"
        probe: "{{ load_balancer_name }}-http-probe"

虽然负载均衡规则将来自前端单一端口的流量分发到后端池中的多个虚拟机,但入站 NAT网络地址转换)规则则是按一对一的方式分发流量,这使得它非常适合像 SSH 这样的服务,这些服务不适合在多个主机之间分配:

    inbound_nat_rules:
      - name: "{{ load_balancer_name }}-nat-ssh"
        frontend_ip_configuration: "{{ load_balancer_name }}-frontend-ip-config"
        backend_port: "{{ load_balancer.ssh_port }}"
        frontend_port: "{{ load_balancer.ssh_port }}"
        protocol: "Tcp"

如果我们有多台机器,我们将添加更多的规则,这些规则将不同的端口映射到后端虚拟机的端口 22。通常,我会使用高端口号,例如 2220 > 2229,这样就不会与其他服务冲突——2220 将流量发送到第一台机器的端口 222221 会将流量发送到第二台机器,依此类推。

然而,在这个例子中,我们只有一台主机,因此我将端口 22 映射到端口 22

最后,我们将标记资源并注册输出:

    tags: "{{ common_tags }}"
  register: "load_balancer_output"

现在我们有了负载均衡器,我们需要创建一个网络接口,该接口将被放置在后端池中,并附加到我们的虚拟机上。

对于那些已经查看过 Ansible Azure 集合的人,你们可能注意到有一个名为 azure.azcollection.azure_rm_networkinterface 的模块,用于管理网络接口。因此,你可能会假设我们正在研究的任务使用了该模块。嗯,你的猜测是错的。

尽管预编写的模块与其交互的 API 端点功能相当完善,但它缺少我们部署所需的一个关键功能:将网络接口分配给 NAT 规则的能力。

然而,一切并非失去希望,仍然有解决方法。

有一个 Azure 模块,其唯一目的是直接与 Azure 资源管理器 API 进行交互,名为 azure.collection.azure_rm_resource,通过使用该模块,我们可以直接从 Ansible 中向 Microsoft.Network/networkInterfaces 端点发出 API 调用。

能够对任何 Azure 资源管理器 API 执行此操作是非常强大的,因为它意味着一旦 Microsoft 发布新功能,我们就能立刻使用它,而且无需等待 Ansible Azure 集合开发者编写、测试并发布该模块。

然而,这种方法有一个缺点:使用这种方法会给你的 playbook 增加额外的复杂性。

以下 URL 是 REST API 文档的链接,其中涵盖了网络接口的创建:learn.microsoft.com/en-us/rest/api/virtualnetwork/network-interfaces/create-or-update?view=rest-virtualnetwork-2023-05-01&tabs=HTTP

正如我们通过执行任务所看到的,我们正在做的事情的总体思路是构建我们希望访问的 API 的 URL,然后构建 REST 文档中详细说明的请求体。

首先,让我们看看生成 URL 的任务部分:

- name: "Create the network interface for the wordpress vm"
  azure.azcollection.azure_rm_resource:
    api_version: "2023-05-01"
    resource_group: "{{ resource_group_output.state.name }}"
    provider: "network"
    resource_type: "networkinterfaces"
    resource_name: "{{ dict.network_interface }}-{{ vm_name }}"
    idempotency: true

前述信息构建了文档中给出的 URL,具体如下:

PUT https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/networkInterfaces/{networkInterfaceName}?api-version=2023-05-01

让我们看看这是如何生成的:

  • {subscriptionId} 是由模块自动生成的,我们无需提供此信息。

  • {resourceGroupName} 是通过提供 resource_group 键添加的,和其他任务一样,我们使用的是资源组名称,这个名称是我们在资源组任务中注册变量时的输出。

  • 由我们通过填写 providerresource_type 键提供的提供者。别担心——URL 对大小写不敏感,模块会为我们自动添加 Microsoft. 部分。

  • {networkInterfaceName}resource_name 键。

  • 最后,API 版本通过填写 api_version 键来提供。

“头部”的最后部分并不构成 URL 的一部分,而是指示 Ansible 执行一个GET请求,然后将将要发布的主体与GET请求返回的结果进行比较,如果有任何问题,它将在主体发布之前报错。

现在我们已经有了 Azure 资源管理器 API 端点的 URL,我们需要填充请求的主体。

对于我们的情况,代码如下所示:

    body:
      location: "{{ location }}"
      properties:
        enableAcceleratedNetworking: false
        primary: true
        networksecuritygroup:
          id: "{{ nsg_output.state.id }}"
        configurations:
          - name: "{{ vm_name }}-ipcfg"
            properties:
              subnet:
                id: "{{ subnet_output.results[0].state.id }}"
              loadBalancerBackendAddressPools:
                - id: "{{ load_balancer_output.state.backend_address_pools[0].id }}"
              loadBalancerInboundNatRules:
                - id: "{{ load_balancer_output.state.inbound_nat_rules[0].id }}"
      tags: "{{ common_tags }}"

当模块运行时,properties将作为 JSON 格式渲染,并与locationtags一起发布在请求的主体中,任务的最后部分是注册输出:

  register: "network_interface_output"

现在我们已经配置了所有基本的 Azure 设置和资源,我们可以启动虚拟机。由于我们将使用 SSH 连接到虚拟机并引导安装 WordPress,我们需要确保生成了有效的 SSH 密钥。

由于我们将连接到远程虚拟机,因此我们希望像在本地部署的主机上那样传送测试密钥。如果你的本地机器上~/.ssh/id_rsa没有密钥,则在group_vars/common.yml文件中将genterate_key变量设置为true(默认为false),然后 Ansible 会为你生成密钥。

如果该位置已有密钥,不用担心;Ansible 只有在密钥不存在时才会创建一个密钥。

- name: "Check user has a key, if not create one for {{ ansible_user_id }}"
  ansible.builtin.user:
    name: "{{ ansible_user_id }}"
    generate_ssh_key: true
    ssh_key_file: "~/.ssh/id_rsa"
  when: genterate_key

接下来,我们有一个启动虚拟机的任务。它使用我们已经部署和配置的所有资源,因此我将不再详细说明:

- name: Create the admin virtual machine
  azure.azcollection.azure_rm_virtualmachine:
    resource_group: "{{ resource_group_output.state.name }}"
    name: "{{ vm_name }}"
    admin_username: "{{ vm_config.admin_username }}"
    ssh_public_keys:
      - path: "{{ vm_config.key.path }}"
        key_data: "{{ vm_config.key.data }}"
    ssh_password_enabled: "{{ vm_config.ssh_password_enabled }}"
    vm_size: "{{ vm_config.vm_size }}"
    managed_disk_type: "{{ vm_config.disk.managed_disk_type }}"
    network_interfaces: "{{ network_interface_output.response.name }}"
    image:
      offer: "{{ vm_config.image.offer }}"
      publisher: "{{ vm_config.image.publisher }}"
      sku: "{{ vm_config.image.sku }}"
      version: «{{ vm_config.image.version }}»
    tags: «{{ common_tags }}»
  register: «vm_output»

和我们在此角色中运行的大多数任务一样,紧接着的将是一个调试任务。

你可能会想,“这就是角色的结束,对吧?” 但我们还有两个任务需要处理。

这两个最后任务中的第一个任务获取主机的信息,例如公共 IP 地址和 SSH 端口,然后将其添加到定义为hosts_group变量的主机组中。

这意味着在我们的主机清单文件中没有硬编码的 IP 地址或连接。注册主机的任务如下所示:

- name: Add the Virtual Machine to the host group
  ansible.builtin.add_host:
    groups: "{{ hosts_group }}"
    hostname: "{{ public_ip_output.state.ip_address }}-{{ load_balancer.ssh_port }}"
    ansible_host: "{{ public_ip_output.state.ip_address }}"
    ansible_port: «{{ load_balancer.ssh_port }}»

那么,这个任务可能是什么呢?我们已经配置了网络,虚拟机已经启动,并且我们已经注册了主机,因此我们应该准备好开始引导 WordPress 了。

这就是问题所在;我们可能已经准备好了,但我们刚刚启动的主机可能还没有准备好,因为虚拟机启动可能需要一两分钟。如果我们在虚拟机尚未完成启动时立即尝试 SSH 连接主机,playbook 将出错并停止运行。

幸运的是,Ansible 为这种场景开发了一个模块,ansible.builtin.wait_for

- name: "Wait for the virtual machine to be ready"
  ansible.builtin.wait_for:
    host: "{{ public_ip_output.state.ip_address }}"
    port: «{{ load_balancer.ssh_port }}»
    delay: 10
    timeout: 300

这将等待10秒钟,然后尝试 SSH 连接主机,最多持续 5 分钟(300秒);当 SSH 可访问时,Ansible playbook 将继续执行下一个角色集,在我们的情况下是引导 WordPress。

引导 WordPress

你不会感到惊讶,WordPress 角色的大部分内容保持不变,因此我们不会在此覆盖这些部分,而是回顾一些小的更改。

站点和主机环境文件

site.yml现在分为两个部分;第一个部分在本地运行,并与 Azure 资源管理器 API 交互,以启动和配置 Azure 资源:

- name: "Deploy and configure the Azure Environment"
  hosts: localhost
  connection: local
  gather_facts: true
  vars_files:
    - group_vars/common.yml
  roles:
    - "azure"

第二部分针对vmgroup主机组,类似于我们在前几章中使用过的内容:

- name: "Install and configure Wordpress"
  hosts: vmgroup
  gather_facts: true
  become: true
  become_method: "ansible.builtin.sudo"
  vars_files:
    - group_vars/common.yml
  roles:
    - "secrets"
    - "stack_install"
    - "stack_config"
    - "wordpress"

hosts文件看起来和我们在前几章中使用的hosts文件一样;它只是缺少了我们显式定义目标主机的行,而是仅由主机组定义组成。

你可能已经注意到我们正在添加一个新角色,其他的角色大部分保持不变;这个角色被命名为secrets,让我们来看看它的功能。

secrets角色

这个角色的唯一目的是为 WordPress 和数据库生成安全密码。它的任务被委派给本地机器,因为它在group_vars/secrets.yml创建了一个变量文件,并将其加载到剧本运行中。

首先,它检查group_vars/secrets.yml是否已经存在,如果存在,我们不想更改文件的内容:

- name: "Check if the file secrets.yml exists"
  ansible.builtin.stat:
    path: "group_vars/secrets.yml"
  register: secrets_file
  delegate_to: "localhost"
  become: false

如果没有文件,则它及其内容将从模板文件生成:

- name: "Generate the secrets.yml file using a template file if not exists"
  ansible.builtin.template:
    src: "secrets.yml.j2"
    dest: "group_vars/secrets.yml"
  when: secrets_file.stat.exists == false
  delegate_to: "localhost"
  become: false

位于roles/secrets/templates/secrets.yml.j2的模板文件如下所示:

db_password: "{{ lookup('community.general.random_string', length=20, upper=true, special=false, numbers=true) }}" wp_password: "{{ lookup('community.general.random_string', length=20, upper=true, special=true, override_special="@-&*", min_special=2, numbers=true) }}"

如你所见,它使用community.general.random_string模块根据一些合理的规则生成随机字符串,我们将把它用作密码。

其他更改

角色的大多数更改都涉及到变量;例如,在roles/wordpress/defaults/main.yml中我们有如下内容:

wordpress:
  domain: "http://{{ app.public_dns_name }}.{{ app.location }}.cloudapp.azure.com/"
  password: "{{ wp_password }}"

这使用了我们在 Azure 负载均衡器公共 IP 地址上配置的公共 URL,以及刚刚运行的secrets角色中的密码变量。

角色中的其他部分保持与我们在第五章中相同,即部署 WordPress

运行剧本

运行剧本使用我们在本书中一直运行的相同命令:

$ ansible-playbook -i hosts site.yml

剧本将执行,完成后你应该能看到类似以下屏幕上的输出:

图 9.1 – 在终端中运行剧本

图 9.1 – 在终端中运行剧本

访问 Azure 门户网站portal.azure.com/并查看 Ansible 创建的资源组,应该能看到类似以下内容:

图 9.2 – 在 Azure 门户中查看资源

图 9.2 – 在 Azure 门户中查看资源

在这里,你应该能够输入分配给公共 IP 地址的 DNS 名称;例如,在我的例子中是http://learnansible.westeurope.cloudapp.azure.com/。你的情况可能不同,你应该能看到新引导的 WordPress 站点。

就像我们在第七章中启动 Azure 资源一样,Ansible Windows 模块,要终止资源,我们需要删除资源组,这将删除其中包含的所有资源。

要使用 Ansible 实现这一点,有一个小型的、独立的 playbook,名为destroy.yml,可以通过运行以下命令来执行:

$ ansible-playbook -i hosts destory.yml

这将需要几分钟才能完成,但它将删除在site.yml playbook 中部署的所有资源,包括 Azure 中的资源以及group_vars/secrets.yml文件中的内容,为下次运行主site.yml playbook 时提供一个干净的起点。

摘要

在本章中,我们使用 Azure Ansible 模块在公共云中启动了我们的第一个实例;正如你所看到的,这个过程相对简单,我们成功地在 Microsoft Azure 上安全地启动了网络和计算资源,为后续在其上安装 WordPress 做好准备,且无需对我们在第五章中涉及的部署 WordPress角色做出重大更改。

在下一章中,我们将扩展本章中涉及的一些技术,并回到网络方面,但与上一章中我们介绍网络设备不同,这次我们将关注公共云中的网络。

第十章:构建云网络

现在我们已经在 Microsoft Azure 启动了服务器,我们将开始在 Amazon Web ServicesAWS)内启动服务。

在启动虚拟机实例之前,我们必须创建一个网络来托管它们。这被称为虚拟私有云VPC),我们需要在 playbook 中将几个不同的元素整合起来,以创建一个 VPC,然后我们可以将其用于我们的实例。

在本章中,我们将执行以下操作:

  • 介绍 AWS

  • 介绍我们想要实现的目标及其原因

  • 创建 VPC、子网和路由(网络和路由)

  • 创建安全组(防火墙)

当我们启动和管理更多具有复杂依赖关系的动态资源时,我们将研究更高级的 Ansible 技巧。

本章涵盖以下主题:

  • AWS 简介

  • Amazon VPC 概述

  • 创建访问密钥和密钥对

  • 为目标 AWS 准备 Ansible

  • AWS playbook

  • 运行 playbook

技术要求

本章将使用 AWS;您需要管理员访问权限来创建角色,以允许 Ansible 与您的账户交互。与其他章节一样,您可以在附带的 GitHub 仓库的 Chapter10 文件夹中找到完整的 playbooks,网址为 github.com/PacktPublishing/Learn-Ansible-Second-Edition/tree/main/Chapter10

AWS 简介

AWS 自 2002 年以来就存在;它最初提供了一些不相关的服务。直到 2006 年初,它才重新推出。重新推出的 AWS 将三项服务整合在一起:

  • Amazon 弹性计算云Amazon EC2):这是 AWS 的计算服务

  • Amazon 简单存储服务Amazon S3):亚马逊的可扩展对象存储服务

  • Amazon 简单队列服务Amazon SQS):此服务提供消息队列,主要用于 Web 应用程序

自 2006 年以来,AWS 从三个独立的服务发展到了超过 160 个,涵盖了 15 个主要领域,具体包括:

  • 计算

  • 存储

  • 数据库

  • 网络和内容交付

  • 机器学习分析安全、身份和合规性

  • 物联网

在 2023 年 10 月的财报电话会议中,透露 AWS 在 2023 年第三季度的收入为 230.6 亿美元,这是一个最初提供共享空闲计算时间的服务所取得的成就。

在撰写本文时,AWS 覆盖了 32 个地理区域,托管了 102 个可用区 (aws.amazon.com/about-aws/global-infrastructure/)。

那么,是什么让 AWS 如此成功呢?不仅仅是它的覆盖面,还有它推出服务的方式。AWS CEO Andy Jassy 曾被引用说:

我们的使命是让任何开发者或任何公司能够在我们的基础设施技术平台上构建所有他们的技术应用。”

作为个人用户,你可以使用与大型跨国公司以及 Amazon 自身一样的 API、服务、区域、工具和定价模型,就像它们消费自己的服务一样。这让你可以自由从小规模开始并实现大规模扩展。例如,Amazon EC2 实例的费用从每月约 4.50 美元的 t2.nano(1 vCPU 和 0.5GB)起,一直到每月超过 19,000 美元的 x1e.32xlarge(128 vCPU,3,904 GB RAM 和两块 1920 GB SSD 存储);正如你所见,几乎所有工作负载都有适合的实例类型。

实例和大多数服务按使用量计费,从 EC2 实例按秒计费,到存储按 GB 每月计费。

Amazon VPC 概述

在本章中,我们将专注于启动 Amazon Virtual Private CloudAmazon VPC);这是将托管我们将在 第十一章 中启动的计算和其他 Amazon 服务的网络层,高度可用云部署

我们将把 VPC 部署到 EU-West #1(爱尔兰) 区域;我们将跨越所有三个可用区来部署我们的 EC2 实例以及 应用负载均衡器。我们同样会使用这三个可用区来部署我们的 Amazon Relational Database ServiceRDS)实例,还会在两个可用区部署 Amazon Elastic File SystemAmazon EFS)卷。

这意味着我们的 Ansible playbook 需要创建/配置以下内容:

  • 一个 Amazon VPC

  • 三个 EC2 实例的子网

  • 三个 Amazon RDS 实例的子网

  • 三个 Amazon EFS 卷的子网

  • 三个应用负载均衡器的子网

  • 一个互联网网关

我们还需要配置以下内容:

  • 一条路由,允许通过互联网网关访问

  • 一个安全组,允许所有人访问应用负载均衡器的 80 端口(HTTP)和 443 端口(HTTPS)

  • 一个安全组,允许可信源通过 22 端口(SSH)访问 EC2 实例

  • 一个安全组,允许从应用负载均衡器访问 EC2 实例的 80 端口(HTTP)

  • 一个安全组,允许从 EC2 实例访问 Amazon RDS 实例的 3306 端口(MySQL)

  • 一个安全组,允许从 EC2 实例访问 Amazon EFS 卷的 2049 端口(NFS)

这将为我们提供主要的网络,允许对除应用负载均衡器外的所有内容进行限制性访问,而应用负载均衡器是我们希望公开可用的。

在创建一个部署网络的 Ansible playbook 之前,我们需要获取一个 AWS API 访问密钥和秘密密钥。

创建访问密钥和秘密密钥

为你的 AWS 用户创建访问密钥和秘密密钥,以便为 Ansible 提供对你的 AWS 账户的完全访问权限是完全可能的。

因此,我们将创建一个 Ansible 用户,它仅有权限访问 AWS 中我们知道 Ansible 在本章中将需要交互的部分。我们将给予 Ansible 对以下服务的完全访问权限:

  • 亚马逊 VPC

  • 亚马逊 EC2

  • 亚马逊 RDS

  • 亚马逊 EFS

为此,按照以下步骤操作:

  1. 登录到 AWS 控制台,可以在console.aws.amazon.com/找到。

  2. 登录后,在搜索框中点击IAM,然后点击IAM “管理 AWS 资源访问”结果。

  3. IAM页面,点击左侧菜单中的用户组;我们将创建一个分配权限的组,然后创建一个用户并将其添加到我们的组中。

  4. 一旦进入Ansible

  5. 现在,在附加权限策略 - 可选部分,选择AmazonEC2FullAccessAmazonVPCFullAccessAmazonRDSFullAccessAmazonElasticFileSystemFullAccess;选择所有四个后,点击页面底部的创建组按钮。

  6. 现在我们已经有了 Ansible 组,点击左侧菜单中的用户

  7. 一旦进入这里的LearnAnsible

  8. 保留提供用户访问 AWS 管理控制台 - 可选选项未勾选,因为我们将创建一个编程用户。

  9. 点击我们之前创建的Ansible组,然后点击下一步,这将带你到审核和 创建页面。

  10. 审核详细信息后,你需要点击LearnAnsible用户。

  11. 最后一步是为我们的用户获取访问密钥。为此,点击LearnAnsible用户,选择安全凭证标签;然后向下滚动到访问密钥,点击创建访问 密钥按钮。

  12. For use with Learn Ansible列表中选择描述标签的值,然后点击创建 访问密钥

  13. 获取访问密钥页面是唯一可以访问秘密访问密钥的地方,因此我建议下载 CSV 文件。下载后,点击完成

重要提示

你刚下载的 CSV 文件包含凭证,允许拥有它们的人在你的 AWS 账户中启动资源;请勿分享它们并确保其安全,以免被滥用,如果落入错误之手,可能导致巨额且意外的 AWS 账单。

现在我们有了一个拥有权限的用户的访问密钥 ID 和秘密访问密钥,我们需要使用 Ansible 启动我们的 VPC;我们可以开始准备 Ansible 并审查剧本。

为目标 AWS 准备 Ansible

我们首先需要讨论如何安全地将访问密钥 ID 和秘密访问密钥传递给 Ansible。由于我将把最终的播放书分享在 GitHub 的公共仓库中,所以我希望将我的 AWS 密钥从公开世界中保持私密,因为泄露了可能会导致费用增加!通常,如果是私人仓库,我会使用 Ansible Vault 或其他一些秘密管理工具来加密密钥,并将它们与其他可能敏感的数据(例如部署密钥)一起包含。

在这种情况下,我不想在仓库中包含任何加密信息,因为这意味着人们需要解密它,编辑值,然后重新加密。幸运的是,AWS 模块允许你在 Ansible 控制器上设置两个环境变量;这些变量将在播放书执行过程中被读取。

要设置这些变量,请运行以下命令,确保在=后替换内容为你的访问密钥和秘密密钥(以下列出的信息仅为占位符值):

$ export AWS_ACCESS_KEY=AKIAI5KECPOTNTTVM3EDA
$ export AWS_SECRET_KEY=Y4B7FFiSWl0Am3VIFc07lgnc/TAtK5+RpxzIGTr

设置后,你可以通过运行以下命令查看内容:

$ echo $AWS_ACCESS_KEY

现在我们可以安全地将凭证传递给 Ansible,我们可以安装 AWS Ansible 模块所需的 Python 模块,以便与 AWS API 进行交互。

重要提示

你必须为每个终端会话设置环境变量,因为每次关闭终端时这些变量都会丢失。

要安装 Python 模块,请运行以下命令:

$ pip3 install botocore boto3

现在我们已经配置了基本设置,可以审查我们的播放书。

AWS 播放书

如本章开头所提到的,我们将在可能的情况下使用一些更高级的技术来部署 AWS 中的资源;我尽量让资源的部署尽可能动态化,这在很大程度上取决于我们如何定义变量,而这正是我们将开始审查播放书的地方。

播放书变量

我们定义的大部分变量可以在group_vars/common.yml中找到,正如你从以下内容所见,它们看起来很像我们在第九章中描述的变量,迁移到云端

debug_output: false
app:
  name: "learnansible"
  region: "eu-west-1"
  env: "prod"

如你所见,我们有相同的debug_output特性标志和一组变量,用于描述我们的应用程序及其将要启动的 AWS 区域。

接下来,我们来看一下资源名称:

vpc_name: "{{ app.name }}-{{ app.env }}-{{ playbook_dict.vpc }}"
internet_gateway_name: "{{ app.name }}-{{ app.env }}-{{ playbook_dict.internet_gateway }}"
internet_gateway_route_name: "{{ internet_gateway_name }}-{{ playbook_dict.route }}"

到目前为止没有什么特别的地方,但这里我们会看到我们方法上的第一次区别:

vpc:
  cidr_block: "10.0.0.0/23"
  dns_hostnames: true
  dns_support: true
  subnet_size: "27"
  subnets:
    - name: "ec2"
      role: "{{ subnet_role_compute }}"
    - name: "rds"
      role: "{{ subnet_role_database }}"
    - name: "efs"
      role: "{{ subnet_role_storage }}"
    - name: "dmz"
      role: "{{ subnet_role_public }}"

乍一看,这与我们在 Microsoft Azure 中所做的似乎没有太大区别。

然而,你可能已经注意到子网没有列出 IP 地址 CIDR 范围,只列出了一些关于子网的详细信息,包括角色字典:

subnet_role_compute: "compute"
subnet_role_database: "database"
subnet_role_storage: "storage"
subnet_role_public: "public"

当我们开始创建子网的任务时,我们会看到为什么子网的 CIDR 范围缺失。

接下来,我们有创建安全组的变量;总的来说,我们将配置四个安全组,因此为了节省空间,这里仅展示其中一个小组:

security_groups:
  - name: "{{ app.name }}-rds-{{ playbook_dict.security_group }}"
    description: "opens port 3306 to the ec2 instances"
    id_var_name: "rds_group_id"
    rules:
      - proto: "tcp"
        from_port: "3306"
        to_port: "3306"
        group_id: "{{ ec2_group_id | default('') }}"
        rule_desc: "allow {{ ec2_group_id | default('') }} access to port 3306"

请参阅 GitHub 仓库以获取四个安全组的完整配置;目前要特别强调的只有一件事,那就是:在引用 {{ ec2_group_id | default('') }} 时,我们将默认值设置为无(即 '' 部分)。我们将在讨论安全角色时解释为什么要这样做。

最后一组变量是字典(playbook_dict)和一个变量,它使用 app.region 设置 region 的值;如果你想查看所有内容,请参见 GitHub。

VPC 角色

在我们进入令人兴奋的任务之前,我们需要创建 VPC。roles/vpc/tasks/main.yml 中的任务如下所示:

- name: "Create VPC"
  amazon.aws.ec2_vpc_net:
    name: "{{ vpc_name }}"
    region: "{{ region }}"
    cidr_block: "{{ vpc.cidr_block }}"
    dns_hostnames: "{{ vpc.dns_hostnames }}"
    dns_support: "{{ vpc.dns_support }}"
    state: "{{ state }}"
    tags:
      Name: "{{ vpc_name }}"
      projectName: "{{ app.name }}"
      environment: "{{ app.env }}"
      deployedBy: "{{ playbook_dict.deployedBy }}"
      description: "{{ playbook_dict.ansible_warning }}"
  register: vpc_output

任务基本上和你预期的一样,只是标签设置稍微更符合我们在 第九章,《迁移到云端》中定义的标签。还有一个调试语句,如果将 debug_output 设置为 true,它会打印创建 VPC 的结果:

- name: "Debug - VPC result"
  ansible.builtin.debug:
    var: "vpc_output"
  when: debug_output

从现在开始,可以安全地假设所有注册的输出将由 ansible.builtin.debug 任务跟随。现在我们的 VPC 已经启动,我们可以开始将内容放入其中,从子网开始,这里将更有趣。

子网角色

如 AWS 概述中所述,AWS 共有 32 个地理区域,并且在写本文时有 102 个可用区。与 Microsoft Azure 不同,AWS 需要每个可用区都有一个子网,而不是一个跨越所有可用区的子网。

eu-west-1 区域是我们将要针对的区域,由三个可用区组成,并且我们为四种不同角色设置了子网,这意味着我们总共需要 12 个子网,但我们的 playbook 很可能会针对一个仅有两个可用区的区域,或者在某些情况下,可能有更多的可用区。

因此,我们的第一个任务是获取目标区域中可用区的信息:

- name: "Get some information on the available zones"
  amazon.aws.aws_az_info:
    region: "{{ region }}"
  register: zones_output

现在我们知道了一些区域信息,我们可以利用这些信息来创建子网:

- name: "Create all subnets"
  ansible.builtin.include_tasks: create_subnet.yml
  loop: "{{ vpc.subnets }}"
  loop_control:
    loop_var: subnet_item
    index_var: subnet_index
  vars:
    subnet_name: "{{ subnet_item.name }}"
    subnet_role: "{{ subnet_item.role }}"
    az_zones_from_main: "{{ zones_output }}"
  register: subnet_output

这个任务与我们之前在本书中使用的任务非常不同,所以让我们更深入地了解一下发生了什么。

在这里,我们使用循环来自动创建多个子网。每次循环处理 vpc.subnets 列表中的一个子网,正如我们之前所看到的,列表包含每个子网的配置详细信息。

当循环运行时,它将当前子网的详细信息分配给 subnet_item 变量,并将该子网在列表中的索引分配给 subnet_index。然后,这些变量被用来定制每个子网的创建过程。

该任务包括并执行在 create_subnet.yml(我们接下来将讨论)中为每个子网定义的步骤,使用该子网的具体详细信息(如名称和角色)。

你可能已经注意到,我们仍然没有传入任何子网的 CIDR 范围;这些都在create_subnet.yml任务中处理,我们为每个子网类型循环遍历;这也是发生第二个循环的地方:

- name: "Create subnet in the availability zone"
  amazon.aws.ec2_vpc_subnet:
    region: "{{ region }}"
    state: "{{ state }}"
    vpc_id: "{{ vpc_output.vpc.id }}"
    cidr: "{{ vpc_output.vpc.cidr_block | ansible.utils.ipsubnet(vpc.subnet_size, az_loop_index + (subnet_index * az_zones_from_main.availability_zones|length)) }}"
    az: "{{ az_item.zone_name }}"
    tags:
      Name: "{{ subnet_name }}-{{ playbook_dict.subnet }}-{{ az_item.zone_id }}"
      projectName: "{{ app.name }}"
      environment: "{{ app.env }}"
      deployedBy: "{{ playbook_dict.deployedBy }}"
      description: "{{ playbook_dict.ansible_warning }}"
      role: "{{ subnet_role }}"
  loop: "{{ az_zones_from_main.availability_zones }}"
  loop_control:
    loop_var: az_item
    index_var: az_loop_index

请跟着我,因为这里可能有点混乱;对于我们从主循环中执行的四个循环中的每一个,我们都将获取可用区的信息,然后对它们进行循环,为我们当前正在循环的角色创建一个子网。

那么,子网的 CIDR 范围是什么呢?

你可能已经注意到,在你期望看到 CIDR 范围的地方,我们有这个表达式:

vpc_output.vpc.cidr_block | ansible.utils.ipsubnet(vpc.subnet_size, az_loop_index + (subnet_index * az_zones_from_main.availability_zones|length))

我们在表达式中有以下组件:

  • vpc_output.vpc.cidr_block:这是 VPC 的 CIDR 块,在此 CIDR 块内将创建子网。对于我们的示例,它是10.0.0.0/22

  • vpc.subnet_size:这指定了每个子网的大小。我们使用/27,表示一个包含 32 个 IP 地址的子网。

  • az_zones_from_main.availability_zones|length:这是可用的可用区的总数。我们目标的区域有3个可用区。

  • az_loop_index:这是在可用区上循环时的当前索引。

  • subnet_index:这是当前处理的子网的索引。

这意味着,对于我们的表达式,我们将得到以下结果。第一个子网,标记为az1,将具有以下内容:

  • az_loop_index = 0

  • subnet_index = 0

所以,公式将是0+(0*3)=0,这意味着我们将得到以下内容:

cidr = "{{ vpc_output.vpc.cidr_block  | ansible.utils.ipsubnet(27, 0) }}"

假设vpc_output.vpc.cidr_block10.0.0.0/22,我们可以得到第一个/27,它将是10.0.0.0/27

对于第二个可用区(az2),循环将如下所示:

  • az_loop_index = 1

  • subnet_index = 0

1+(0*3)=1意味着我们将得到10.0.0.32/27,因为下一个子网块从前一个子网的下一个 32 个 IP 地址间隔开始。

第三个可用区(az3)将是2+(0*3)=2,CIDR 块将是10.0.0.64/27

下一个子网角色,即 RDS 角色,将为az1提供以下内容:

  • az_loop_index = 0

  • subnet_index = 1

公式将是0+(1*3)=3,给我们一个 CIDR 块10.0.0.96/27

这个模式将跟随序列,RDS 的下一个子网az2将位于10.0.0.128/27,而az3将位于10.0.0.160/27,以此类推。

该表达式确保在 VPC 中创建的每个子网都被分配一个唯一且不重叠的 CIDR 块,按定义的子网大小适当地分段,并分布在不同的可用区中。

采用这种方法不仅简化了子网创建的管理,而且在编写角色时也确保了效率,因为这意味着我们不必硬编码任务来考虑区域之间的变化或我们在变量中定义的子网数量。

该角色中的其余任务构建了一个包含我们已定义的每个角色子网 ID 的列表。以下是其中一个任务的示例:

- name: "Gather information about the compute subnets"
  amazon.aws.ec2_vpc_subnet_info:
    region: "{{ region }}"
    filters:
      "tag:role": "{{ subnet_role_compute }}"
      "tag:environment": "{{ app.env }}"
      "tag:projectName": "{{ app.name }}"
  register: subnets_compute_output

这将获取被分配了subnet_role_compute角色的三个子网的信息。在仓库中可以找到更多这样的数据收集任务;这些任务涵盖了subnet_role_databasesubnet_role_storagesubnet_role_public角色。

最后,角色中的最后一个任务打印我们使用前一组任务收集到的子网 ID;这与我们到目前为止在剧本中使用的调试语句略有不同,因为我们调用ansible.builtin.debug模块时,使用的是msg函数,而不是var函数。

网关角色

与之前的角色相比,网关角色相对简单。相比之下,它部署了一个互联网网关。然后,它创建了一条路由,将所有目标为互联网的流量(使用0.0.0.0/0表示,CIDR 表示所有网络流量)发送到我们新启动的互联网网关。

创建互联网网关的任务如下所示:

- name: "Create an Internet Gateway"
  amazon.aws.ec2_vpc_igw:
    region: "{{ region }}"
    state: "{{ state }}"
    vpc_id: "{{ vpc_output.vpc.id }}"
    tags:
      "Name": "{{ internet_gateway_name }}"
      "projectName": "{{ app.name }}"
      "environment": "{{ app.env }}"
      "deployedBy": "{{ playbook_dict.deployedBy }}"
      "description": "{{ playbook_dict.ansible_warning }}"
      "role": "igw"
  register: internet_gateway_output

根据其余任务,接着是一个调试任务,然后是创建路由表的任务,接着将路由表与我们新创建的互联网网关以及我们在子网角色中定义并收集的计算和公网子网相关联:

- name: "Create a route table so the internet gateway can be used by the public subnets"
  amazon.aws.ec2_vpc_route_table:
    region: "{{ region }}"
    state: "{{ state }}"
    vpc_id: "{{ vpc_output.vpc.id }}"
    subnets: "{{ subnet_compute_ids + subnet_public_ids }}"
    routes:
      - dest: "0.0.0.0/0"
        gateway_id: "{{ internet_gateway_output.gateway_id }}"
    resource_tags:
      "Name": "{{ internet_gateway_route_name }}"
      "projectName": "{{ app.name }}"
      "environment": "{{ app.env }}"
      "deployedBy": "{{ playbook_dict.deployedBy }}"
      "description": "{{ playbook_dict.ansible_warning }}"
      "role": "route"
  register: internet_gateway_route_output

接着,我们执行一个调试任务来完成这个角色,然后我们进入剧本的最后一个角色:安全组的角色。

安全组的角色

虽然我认为这个角色没有子网角色那么复杂,但我们在任务中加入了比一些更简单的任务更多的逻辑,这些任务是我们到目前为止在书中运行的。

如果你还记得,在本章早些时候,当我们介绍剧本使用的变量时,我们给出了以下安全组部署的示例:

  - proto: "tcp"
    from_port: "3306"
    to_port: "3306"
    group_id: "{{ ec2_group_id | default('') }}"
    rule_desc: "allow {{ ec2_group_id | default('') }} access to port 3306"

上述规则根据rule_desc,为所有附加了 EC2 安全组的设备打开端口3306,正如我们将在第十一章《高可用云部署》中看到的那样,这些设备将是运行我们工作负载的 EC2 实例,高可用云部署

你可能会想,“现在明白了。” 然而,这是我们必须绕过的逻辑中的一个小缺陷。ec2_group_id引用了一个组 ID,而在我们第一次运行剧本时,这个组 ID 并不存在。那么,我们如何创建组并用引用尚不存在的组的规则填充它们呢?

正如我们已经看到的那样,循环遍历我们在变量中定义的资源更为高效。它减少了角色级别的硬编码逻辑,使得该角色在项目和剧本之间更具可重用性。

在我们查看创建组的逻辑之前,我们需要收集一项信息:运行 Ansible 的资源的公网 IP 地址。为此,我们调用以下任务:

- name: "Find out your current public IP address using https://ipify.org/"
  community.general.ipify_facts:
  register: public_ip_output

然后我们设置一个名为your_public_ip的事实变量,在需要的地方可以在规则中引用它:

- name: "Set your public ip as a fact"
  ansible.builtin.set_fact:
    your_public_ip: "{{ public_ip_output.ansible_facts.ipify_public_ip }}/32"

现在我们已经有了那段信息,我们可以回到问题,讨论如何引用尚未启动的资源的 ID。

为了创建安全组,我们将使用amazon.aws.ec2_security_group模块。该模块有一个名为purge_rules的标志,默认设置为true;在默认状态下,当我们的剧本找到并需要更新现有安全组时,它会删除组中的所有规则,然后仅添加剧本中定义的规则,以保持一致的状态。

尽管这是一个有效的用例,但在我们的示例中,通过将purge_rules设置为false,我们可以创建一些未填充的安全组:

- name: "Create the base security groups"
  amazon.aws.ec2_security_group:
    region: "{{ region }}"
    state: "{{ state }}"
    vpc_id: "{{ vpc_output.vpc.id }}"
    name: "{{ item.name }}"
    description: "{{ item.description }}"
    purge_rules: false
    tags:
      "Name": "{{ item.name }}"
      "projectName": "{{ app.name }}"
      "environment": "{{ app.env }}"
      "deployedBy": "{{ playbook_dict.deployedBy }}"
      "role": "securitygroup"
  loop: "{{ security_groups }}"
  register: base_security_groups_output

这将遍历并创建基础的、未填充的安全组,如果它们不存在。如果它们已经存在,则不会对其进行任何更改。

因此,既然我们已经创建了安全组,或者它们已经存在,我们就有了根据前面任务的输出动态定义某些事实所需的信息:

- name: "Set the fact for the security group ids"
  ansible.builtin.set_fact:
    "{{ item.id_var_name }}": "{{ base_security_groups_output.results | selectattr('item.name', 'equalto', item.name) | map(attribute='group_id') | first }}"
  loop: "{{ security_groups }}"
  when: base_security_groups_output.results | selectattr('item.name', 'equalto', item.name) | map(attribute='group_id') | list | length > 0

该任务使用ansible.builtin.set_fact模块,允许在运行时创建或更新新变量。该任务旨在提取第一个任务中创建的每个安全组的唯一 ID,并将其分配给特定的变量名。

我们使用了两个表达式来完成此操作。第一个如下:

"{{ item.id_var_name }}": "{{ base_security_groups_output.results | selectattr('item.name', 'equalto', item.name) | map(attribute='group_id') | first }}"

这是用来根据第二个表达式创建的循环动态生成变量集的。以下是第一个表达式的细节:

  • base_security_groups_output.results:这指的是前一个任务创建安全组时的结果列表。该列表中的每个结果包含有关某个安全组的数据。

  • selectattr('item.name', 'equalto', item.name)selectattr过滤器用于在结果列表中进行搜索。它查找item.name属性等于当前循环中item.name的结果。换句话说,它过滤结果以找到我们当前感兴趣的特定安全组。

  • map(attribute='group_id')map过滤器用于转换过滤后的结果列表。它仅提取每个结果中的group_id属性,这是安全组的 ID。

  • first:由于前一步仍可能返回一个列表(尽管只有一个元素),first过滤器仅获取该列表中的第一个元素,这应该是安全组的唯一 ID。

该表达式的结果是与循环中的当前项匹配的安全组 ID,并将其分配给一个根据item.id_var_name命名的变量。

第二个表达式位于when条件中,作为循环的一部分运行:

when: base_security_groups_output.results | selectattr('item.name', 'equalto', item.name) | map(attribute='group_id') | list | length > 0

该表达式确定是否应该对循环中的特定项执行任务。它遵循与第一个表达式类似的逻辑:

  • 它从相同的过滤过程开始,查找与当前 item.name 匹配的安全组。

  • 在提取 group_id 后,它通过 list 过滤器确保输出被视为列表。

  • length > 0:此部分检查列表的长度(项目数)是否大于 0。这意味着必须至少存在一个具有指定名称的安全组。如果列表为空,则没有找到匹配的安全组,任务将被跳过。

理论上,我们现在应该已经填充了包含安全组 ID 的变量,这意味着我们现在可以添加规则:

- name: "Provision security group rules"
  amazon.aws.ec2_security_group:
    region: "{{ region }}"
    state: "{{ state }}"
    vpc_id: "{{ vpc_output.vpc.id }}"
    name: "{{ item.name }}"
    description: "{{ item.description }}"
    purge_rules: false
    rules: "{{ item.rules }}"
  loop: "{{ security_groups }}"
  register: security_groups_with_rules_output

这将遍历已创建的安全组,并为每个安全组填充规则,使用我们在上一任务中动态定义的变量中的组 ID。

运行 playbook

如前所述,我们已经通过 playbook 代码逐步处理了,在运行 playbook 之前,必须通过运行以下命令在您的终端会话中设置 AWS_ACCESS_KEYAWS_SECRET_KEY 环境变量,确保更新为您在 AWS 控制台中创建 Ansible 用户时记下的值:

$ export AWS_ACCESS_KEY=AKIAI5KECPOTNTTVM3EDA
$ export AWS_SECRET_KEY=Y4B7FFiSWl0Am3VIFc07lgnc/TAtK5+RpxzIGTr

设置环境变量后,您可以运行如下非常熟悉的 playbook 代码:

$ ansible-playbook -i hosts site.yml

完成后,您应该看到类似以下的终端输出:

图 10.1 – 在终端中运行 playbook

图 10.1 – 在终端中运行 playbook

访问 VPC 并在 console.aws.amazon.com/ 上查看资源图,应该会显示类似以下的资源图:

图 10.2 – 查看资源图

图 10.2 – 查看资源图

通过访问 安全组,您还应该看到我们创建的安全组列表:

图 10.3 – 审查安全组

图 10.3 – 审查安全组

我在仓库中添加了第二个 playbook,它销毁了运行 site.yml playbook 时创建的所有资源,名为 destroy.yml。您可以使用以下命令运行它:

$ ansible-playbook -i hosts destroy.yml

我在这里不会详细讲解 playbook 的内容,但如果您查看代码,会发现它本质上是按相反的顺序运行本章中我们讨论过的角色中的相同任务,将状态设置为 absent 而不是 present

总结

在本章中,我们迈出了使用 Ansible 启动公共云资源的下一步。我们通过创建 VPC、设置应用程序所需的子网、配置互联网网关并设置实例通过它路由外出流量,为自动化一个复杂环境奠定了基础。

我们配置了四个安全组,其中三个包含动态内容,用于保护启动到我们 VPC 中的服务。

在下一章,我们将以本章所奠定的基础为基础,并启动一组与 VPC 配套的更复杂的服务。

进一步阅读

第十一章:高可用云部署

继续我们的 AWS 部署,我们将开始将服务部署到我们在上一章创建的网络中,到本章结束时,我们将拥有一个高可用的 WordPress 安装。

基于我们在上一章创建的角色,我们将执行以下操作:

  • 启动和配置应用负载均衡器

  • 启动和配置 Amazon 关系型数据库服务RDS)(数据库)

  • 启动和配置 Amazon 弹性文件系统EFS)(共享存储)

  • 启动 弹性计算云EC2)实例并从中创建 Amazon 机器镜像AMI)(部署 WordPress 代码)

  • 启动和配置一个启动模板,使用新创建的 AMI 和自动扩展组(高可用性)

本章涵盖以下主题:

  • 规划部署

  • 操作手册

  • 运行操作手册

  • 终止所有资源

技术要求

和上一章一样,我们将使用 AWS;你需要我们在上一章创建的访问密钥和秘密密钥来启动所需的资源,以实现我们的高可用 WordPress 安装。请注意,我们将启动会产生费用的资源。同样,你可以在附带的 GitHub 仓库中的 Chapter11 文件夹找到完整的操作手册,地址为 github.com/PacktPublishing/Learn-Ansible-Second-Edition/tree/main/Chapter11/

规划部署

在深入研究操作手册之前,我们应该先了解我们要实现的目标。如前所述,我们将在我们的 AWS 虚拟私有云VPC)角色上构建,添加实例和存储;我们的最终部署将如以下图所示:

图 11.1 – 我们将要启动的概览

图 11.1 – 我们将要启动的概览

在图中,我们有以下内容:

  • 2 x EC2 实例 (t2.micro),部署在不同的可用区

  • 1 x RDS 实例 (t2.micro)

  • 1 x EFS 存储,跨越三个可用区

在我们讨论部署本身之前,根据这里的图示和规格,运行此部署的成本是多少?

部署成本估算

在 EU-West-1 区域运行此部署的成本如下:

实例类型 # 数量 实例费用 每月总费用
EC2 实例 (t2.micro) x2 $9.20 | $18.40
RDS 实例 (t2.micro) x1 $13.14 | $13.14
应用负载均衡器 x1 $24.24 | $24.24
EFS 5GB $0.88 | $4.40
总计 $61.83

表 11.1 – 部署运行成本

还有一些其他的次要费用,比如带宽和存储包含我们软件堆栈的 AMI。我们也可以考虑通过增加冗余来提高这些费用,例如将我们的 RDS 实例更新为多 AZ 的 RDS 主实例和备份实例部署,并增加 EC2 实例的数量。

然而,这为我们的部署引入了额外的复杂性,因为我们将花费余下的章节来介绍 playbook,playbook 将负责部署资源。我现在希望将这个 playbook 保持尽可能简单。

WordPress 的考虑因素和高可用性

到目前为止,我们一直在单台服务器上启动 WordPress,这是可以的。但因为我们希望尽可能消除部署中的单点故障,我们必须认真思考如何初始配置和启动我们的部署。

首先,让我们讨论一下我们需要启动部署的顺序。我们需要处理元素的主要顺序如下:

  • VPC、子网、互联网网关、路由和安全组:这些都是启动我们部署所需要的。

  • 应用弹性负载均衡器:我们将在安装过程中使用弹性负载均衡器的公共主机名,因此在我们开始安装之前,需要先启动它。

  • RDS 数据库实例:我们的数据库实例必须在我们启动安装之前可用,因为我们需要创建 WordPress 数据库并引导安装过程。

  • EFS 存储:我们需要一些存储来在接下来启动的 EC2 实例之间共享。

到目前为止,一切顺利;然而,这也是我们必须开始考虑 WordPress 的时候。

正如一些有经验的人所知道的那样,当前版本的 WordPress 并不是为了在多个服务器上分布而设计的。我们可以应用很多技巧和变通方法,使 WordPress 在这种部署方式中正常工作;然而,本章的重点并不是部署 WordPress 的细节问题。而是使用 Ansible 来部署一个多层次的 Web 应用程序。

因此,我们将选择最基础的多实例 WordPress 选项,将代码和内容部署到 EFS 卷上。这意味着我们需要做的就是安装 LEMP 堆栈。需要注意的是,这个选项在大规模部署时可能会更具性能优势,但它能够满足我们的需求。

现在,回到任务列表。当涉及到启动我们的实例时,我们需要执行以下操作:

  1. 启动一个临时的运行 Ubuntu 的 EC2 实例,以便重用现有的 playbook 中的部分内容。

  2. 更新操作系统并安装软件堆栈、支持工具和配置,以便我们安装并运行 WordPress。

  3. 挂载 EFS 卷,设置正确的权限,并配置其在实例启动时自动挂载。

  4. 引导启动 WordPress 本身。

  5. 从临时实例创建一个 AMI,然后终止该临时实例,因为它现在不再需要。

  6. 创建一个启动模板,使用我们刚刚创建的 AMI。

  7. 创建一个自动伸缩组并附加启动配置;它还应该将我们的 WordPress 实例注册到 Elastic Load Balancer。

后续的 Playbook 运行将更新操作系统和非 WordPress 配置,应当在现有实例已启动并运行的情况下重复这一过程;一旦 AMI 创建完成,它应与当前实例一同部署,并且在新实例注册到 Elastic Load Balancer 并接收流量后,旧实例将被终止。

这将允许我们在不造成停机的情况下更新操作系统的软件包和配置,如果一切按计划进行的话!

现在我们大概了解了要实现的目标,让我们开始编写我们的 Playbook。

Playbook

我们将使用在 第十章 中查看的 Playbook 作为起点,构建云网络,因为所有角色都与我们的部署相关,而且它已经具备了我们所需的 Playbook 结构。

我们还将使用角色来部署和配置 WordPress 及我们在 第九章 中使用的支持软件栈,迁移到云端,并做一些调整,因为我们面向的是 AWS 而不是 Microsoft Azure;当我们遇到这些调整时,我会告诉你。

与之前的章节不同,我们首先查看 site.yml 文件,以了解我们将执行角色的顺序。

文件中有三个阶段,从部署和配置我们基础 AWS 资源的阶段开始:

- name: "Deploy and configure the AWS Environment"
  hosts: localhost
  connection: local
  gather_facts: true
  vars:
    state: "present"
  vars_files:
    - group_vars/common.yml
  roles:
    - vpc
    - subnets
    - gateway
    - securitygroups
    - elb
    - efs
    - rds
    - ec2tmp
    - endpoints

如你所见,这与来自 第十章site.yml 文件相同,构建云网络,只是从 securitygroups 角色开始,列表中添加了额外的角色。

当我们的 Playbook 执行到第二阶段时:

- name: "Install and configure Wordpress"
  hosts: vmgroup
  gather_facts: true
  become: true
  become_method: "ansible.builtin.sudo"
  vars_files:
    - group_vars/common.yml
    - group_vars/generated_aws_endpoints.yml
  roles:
    - stack_install
    - stack_config
    - wordpress

一个名为group_vars/generated_aws_endpoints.yml的文件会被生成,并且应该有一个临时虚拟机实例正在运行,这意味着可以通过 SSH 访问运行 Playbook 的主机。

一旦这一阶段完成,我们的临时虚拟机实例应该已经安装了我们的软件栈。如果这是首次运行 Playbook,WordPress 会被全新安装;如果 Playbook 检测到已有的 WordPress 安装并保持不变,除非 Playbook 内的插件配置有任何更改。

最终阶段是执行:

- name: "Create AMI and update the Auto Scaling Group"
  hosts: localhost
  connection: local
  gather_facts: true
  vars:
    state: "present"
  vars_files:
    - group_vars/common.yml
  roles:
    - ec2ami
    - autoscaling

这一阶段从临时虚拟机实例创建一个 AMI,终止临时实例(因为我们不再需要它),创建一个新的启动模板版本,然后创建/更新自动伸缩组,以便在 EC2 实例上部署新版本。

听起来很简单?好吧,让我们来看看。

变量

开箱即用时,有一个名为group_vars/common.yml的单一变量文件,包含了部署我们环境所需的所有静态变量。

在 Playbook 运行过程中,group_vars文件夹内将创建一些附加文件;它们将包含一些动态生成的资源,如密码、资源名称/端点和其他信息。

我们将在查看创建和与之交互的任务时更详细地讨论这些文件;现在,我们将查看group_vars/common.yml中定义的静态变量,从基础应用配置开始。

应用程序和资源配置

我们从配置中开始,提供了启用/禁用调试的选项。默认情况下,设置为false;然而,在运行 Playbook 时,我建议将其切换为true并查看输出:

debug_output: false

接下来,我们有应用程序名称、区域和环境参考:

app:
  name: "learnansible"
  region: "eu-west-1"
  env: "prod"

下一个块的变量定义了 WordPress 数据库的详细信息;由于我们将使用 Amazon RDS 服务,我们只使用文件后面定义的变量,因此只需要在一个地方更新信息:

wp_database:
  name: "{{ rds.db_name }}"
  username: "{{ rds.db_username }}"
  password: "{{ rds.db_password }}"

下一个块是用于配置 WordPress 本身的各种变量:

wordpress:
  domain: "http://{{ aws_endpoints.elb }}/"
  title: "WordPress installed by Ansible on {{ os_family }}"
  username: "ansible"
  password: "{{ rds.db_password }}"
  email: "test@test.com"
  plugins:
    - "jetpack"
    - "wp-super-cache"
    - "wordpress-seo"
    - "wordfence"
    - "nginx-helper"

除了使用aws_endpoints.lb变量(在 Elastic Load Balancer 启动之前无法知道)之外,自我们在第九章《迁移到云端》一文中最后一次定义这些变量以来,没有什么重大变化。为了方便使用,我们重新利用了将在文件后面动态生成的密码作为 WordPress 管理员密码。

堆栈配置

下一部分覆盖了roles/stack_install角色中的默认设置:

stack_packages:
  - "nginx"
  - "mariadb-client"
  - "php-cli"
  - "php-curl"
  - "php-fpm"
  - "php-gd"
  - "php-intl"
  - "php-mbstring"
  - "php-mysql"
  - "php-soap"
  - "php-xml"
  - "php-xmlrpc"
  - "php-zip"
  - "nfs-common" # Added for AWS
  - "nfs4-acl-tools" # Added for AWS
  - "autofs"  # Added for AWS
  - "rpcbind"  # Added for AWS

我们已从软件包列表中移除了mariadb-server,因为我们不再需要安装或配置本地数据库服务器,并且在最后添加了四个软件包(全部标注为# Added for AWS)。这些软件包安装了使用 NFS 协议挂载 EFS 文件系统所需的软件,这样我们顺利进入下一个块:

nfs:
  mount_point: "/var/www/"
  mount_options: "nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2"
  state: "mounted"
  fstype: "nfs4"

如你所见,这里定义了一些关于 EFS 文件系统应该挂载的位置、使用的选项以及它的文件系统类型的基本信息。

资源名称

下一部分构建了我们要部署的资源名称;这里没有什么特别的内容——它只是这样定义的,以便我们无需手动更新多个地方的重复信息:

vpc_name: "{{ app.name }}-{{ app.env }}-{{ playbook_dict.vpc }}"
internet_gateway_name: "{{ app.name }}-{{ app.env }}-{{ playbook_dict.internet_gateway }}"
internet_gateway_route_name: "{{ internet_gateway_name }}-{{ playbook_dict.route }}"
elb_target_group_name: "{{ app.name }}-{{ app.env }}-{{ playbook_dict.elb_target_group }}"
elb_name: "{{ app.name }}-{{ app.env }}-{{ playbook_dict.elb }}"
efs_name: "{{ app.name }}-{{ app.env }}-{{ playbook_dict.efs }}"
rds_name: "{{ app.name }}-{{ app.env }}-{{ playbook_dict.rds }}"
ec2_tmp_name: "{{ app.name }}-tmp-{{ playbook_dict.ec2 }}"
ami_name: "{{ app.name }}-{{ app.env }}-{{ playbook_dict.ami }}"
ec2_name: "{{ app.name }}-{{ app.env }}-{{ playbook_dict.ec2 }}"
launch_template_name: "{{ app.name }}-{{ app.env }}-{{ playbook_dict.lt }}"
asg_name: "{{ app.name }}-{{ app.env }}-{{ playbook_dict.asg }}"

我们不会在这里覆盖完整的playbook_dict块,因为没有太多内容可看,不过提醒一下,这就是它开始的样子:

playbook_dict:
  deployedBy: "Ansible"
  ansible_warning: "Resource managed by Ansible"
  vpc: "vpc"

它继续定义服务名称。接下来的部分是我们开始定义用于 AWS 资源部署的变量。

EC2 配置

ec2变量分为几个不同的层。用于自动扩展组、AMI 和 SSH 密钥对的层遵循一些通用设置:

ec2:
  instance_type: "t2.micro"
  public_ip: true
  ssh_port: "22"

这些变量跨实例使用,除了public_ip引用,它仅在启动临时虚拟机实例以引导 WordPress 时使用。

下一层定义了关于自动扩展组和启动模板的一些细节;它们有助于定义启动多少实例、如何更新实例以及负载均衡器如何检查它们是否健康:

  asg:
    min_size: 1
    max_size: 3
    desired_capacity: 2
    health_check_type: "EC2"
    replace_batch_size: 1
    health_check_period: 300
    replace_all_instances: true
    wait_for_instances: true
    wait_timeout: 900
    disable_api_termination: true

接下来,我们定义了我们将使用的基本 AMI 的详细信息;如你所见,我们使用的是由 Canonical 发布和维护的 Ubuntu 22.04:

  ami:
    owners: "099720109477"
    filters:
      name: "ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"
      virtualization_type: "hvm"

最后,我们有一些关于上传到 AWS 并在启动虚拟机实例时使用的密钥对的详细信息:

  keypair:
    name: "ssh_keypair"
    key_material: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"

接下来是启动 RDS 服务时使用的变量。

RDS 配置

这些都是标准配置,除了rds.db_password变量:

rds:
  db_username: "{{ app.name }}"
  db_password: "{{ lookup('password', 'group_vars/generated_rds_passwordfile chars=ascii_letters,digits length=30') }}"
  db_name: "{{ app.name }}"
  instance_type: "db.t2.micro"
  engine: "mysql"
  engine_version: "8.0"
  allocated_storage: "5"

如你所见,我们使用查找模块将一个随机密码添加到group_vars/generated_rds_passwordfile文件中;我们指示该模块生成一个由字母和数字组成的 30 字符随机密码。

EFS 配置

在这里,我们定义了用于告诉 Ansible 在创建 EFS 资源时等待多长时间的变量:

efs:
  wait: "yes"
  wait_time: "1200"

VPC 和子网配置

这一块与第十章《构建云网络》中的内容保持一致。

安全组配置

这个部分与第十章《构建云网络》中的内容大部分相同,唯一的不同是我们现在将 SSH 端口定义为ec2.ssh_port。我已更新 EC2 组,使用此引用,而不是将端口 22 硬编码到此块中。唯一的其他新增项是:

elb_seach_string: "elb"
ec2_seach_string: "ec2"
rds_seach_string: "rds"
efs_seach_string: "efs"

这些将在整个 Playbook 中使用,当我们查询 AWS API 以获取有关安全组的信息时。

最后的块

根据第十章《构建云网络》,其中包含以下内容:

region: "{{ app.region }}"

这就结束了我们对group_vars/common.yml文件的快速浏览;如你所见,无论在结构还是内容上,我们都遵循了过去几章的相同模式,按逻辑将变量分组,并尽量在整个过程中重复使用引用,以避免重复信息。

Playbook 角色

现在我们已经介绍了变量,我们可以按site.yml文件中出现的顺序处理这些角色。

VPC、子网、网关和安全组角色

这些角色与第十章《构建云网络》中的内容没有变化;它们只是直接放置并按预期工作。Playbook 中这一部分的剩余角色将在引用子网、安全组和 VPC 时,引用这些角色的输出。

应用程序弹性负载均衡器(ELB)角色

在此角色中,我们将部署两个资源,第一个是目标组。在我们启动自动扩展虚拟机实例时,这个目标组将被使用——我们将实例附加到目标组上。然后,目标组被附加到我们将在此角色中启动的应用弹性负载均衡器。

该任务本身是相对静态的,如以下任务代码所示:

- name: "Provision the target group"
  community.aws.elb_target_group:
    name: "{{ elb_target_group_name }}"
    region: "{{ region }}"
    state: "{{ state }}"
    protocol: "http"
    port: "80"
    deregistration_delay_timeout: "15"
    vpc_id: "{{ vpc_output.vpc.id }}"
    modify_targets: "false"
    tags:
      "Name": "{{ elb_target_group_name }}"
      "projectName": "{{ app.name }}"
      "environment": "{{ app.env }}"
      "deployedBy": "{{ playbook_dict.deployedBy }}"
      "description": "{{ playbook_dict.ansible_warning }}"
      "role": "target-group"
  register: elb_target_group_output

我们只是引用变量,唯一的动态内容是 VPC 的 ID,这是通过vpc_output变量引用的,该变量在 VPC 角色中启动 VPC 时已注册。

由于我们在此角色中注册了一些输出,我们将继续在后面添加一个调试任务;在这种情况下,任务如下所示:

- name: "Debug: ELB Target Group Output"
  ansible.builtin.debug:
    var: "elb_target_group_output"
  when: debug_output

正如我们在第十章《构建云网络》中已经讨论过的,我们在概述 Playbook 时不会重复这些任务,除非我们做的是不同的事情——所以,从现在开始,如果我们正在注册一个输出,请假设会紧接着进行一个调试任务。

在创建 ELB 之前,我们还需要一个信息,那就是安全组的 ID。

为了获取这一信息,我们可以遍历security_groups_with_rules_output变量,并在group_name包含elb_seach_string变量内容时使用set_fact来设置group_id

- name: Extract ELB Group ID
  ansible.builtin.set_fact:
    elb_group_id: "{{ item.group_id }}"
  loop: "{{ security_groups_with_rules_output.results }}"
  when: item.group_name is search(elb_seach_string)

每当我们需要安全组的 ID 时,我们将使用相同的模式,但更新设置的事实名称和相应的搜索引导变量。

以下任务配置应用弹性负载均衡器,用于将 HTTP 请求分发到我们的自动扩展管理虚拟机实例,以便为我们的 WordPress 网站提供服务:

- name: "Provision an application elastic load balancer"
  amazon.aws.elb_application_lb:
    region: "{{ region }}"
    name: "{{ elb_name }}"
    state: "{{ state }}"
    security_groups: "{{ elb_group_id }}"
    subnets: "{{ subnet_public_ids }}"
    listeners:
      - Protocol: "HTTP"
        Port: "80"
        DefaultActions:
          - Type: "forward"
            TargetGroupArn: "{{ elb_target_group_output.target_group_arn }}"
    tags:
      "Name": "{{ elb_name }}"
      "projectName": "{{ app.name }}"
      "environment": "{{ app.env }}"
      "deployedBy": "{{ playbook_dict.deployedBy }}"
      "description": "{{ playbook_dict.ansible_warning }}"
      "role": "load-balancer"
  register: loadbalancer_output

如你所见,我们将应用弹性负载均衡器附加到subnet_public_ids中列出的子网,并将安全组附加到之前任务中注册的elb_group_id事实。

然后,我们配置一个80端口的监听器,以接受 HTTP 流量并将其转发到我们在角色开始时启动的目标组——这就完成了应用弹性负载均衡器的角色。

弹性文件系统(EFS)角色

该角色从设置efs_group_id的任务开始,使用efs_seach_string变量。一旦我们知道应用到 EFS 服务的安全组 ID,就可以继续执行下一个任务。

该任务使用模板生成一个文件,并将其放入group_vars文件夹中:

- name: "Generate the efs targets vars file"
  ansible.builtin.template:
    src: "targets.j2"
    dest: "group_vars/generated_efs_targets.yml"
    mode: "0644"

用于填充group_vars/generated_efs_targets.yml文件的模板文件如下所示:

efs_targets:
{% for item in subnet_storage_ids %}
      - subnet_id: "{{ item }}"
        security_groups: [ "{{ efs_group_id }}" ]
{% endfor %}

在这里,我们使用 Jinja2 的for循环遍历subnet_storage_ids的内容,这将创建一个类似以下内容的文件:

efs_targets:
      - subnet_id: "subnet01_id"
        security_groups: [ "efs_group_id" ]
      - subnet_id: "subnet02_id"
        security_groups: [ "efs_group_id" ]
      - subnet_id: "subnet03_id"
        security_groups: [ "efs_group_id" ]

这意味着当我们创建 EFS 文件系统时,它将在我们选择的区域的所有可用区中可用。

好的,一旦我们加载了刚才加载的文件内容,它就会完成,这在下一个任务中实现,正如你在这里看到的:

- name: "Include the efs targets vars file"
  ansible.builtin.include_vars: "group_vars/generated_efs_targets.yml"

现在我们已经做好了创建 EFS 文件系统的所有准备工作,这可以通过以下任务来完成:

- name: "Create the EFS File System"
  community.aws.efs:
    name: "{{ efs_name }}"
    region: "{{ region }}"
    state: "{{ state }}"
    tags:
      "Name": "{{ efs_name }}"
      "projectName": "{{ app.name }}"
      "environment": "{{ app.env }}"
      "deployedBy": "{{ playbook_dict.deployedBy }}"
      "description": "{{ playbook_dict.ansible_warning }}"
      "role": "efs"
    targets: "{{ efs_targets }}"
    wait: "{{ efs.wait }}"
    wait_timeout: "{{ efs.wait_time }}"
  register: efs_output

创建文件系统可能需要几分钟,我们必须等到此任务成功完成后才能继续,这就是为什么我们要使用等待标志。如果不等待,我们会增加文件系统在虚拟机启动时未准备好,无法挂载的风险,这将导致 Playbook 执行失败。

说到需要一些时间的任务,下一个角色处理的是启动 Amazon RDS 实例,我们将把它作为 WordPress 网站的数据库。这个任务最多可能需要 10 分钟才能完成。

Amazon RDS 角色

该角色主要由两部分组成;第一部分执行的任务类似于我们在前一个角色中创建 EFS 目标时所做的任务。

RDS 服务的不同之处在于,当我们部署服务时,不需要手动传递子网,而是可以在 AWS 端原生创建一个组,然后在启动 RDS 实例时引用它。

创建 RDS 子网组的任务如下所示:

- name: "Add RDS subnet group"
  amazon.aws.rds_subnet_group:
    name: "{{ rds_name }}"
    region: "{{ region }}"
    state: "{{ state }}"
    description: "{{ dict.ansible_warning }}"
    subnets: "{{ subnet_database_ids }}"
    tags:
      "Name": "{{ rds_name }}"
      "projectName": "{{ app.name }}"
      "environment": "{{ app.env }}"
      "deployedBy": "{{ playbook_dict.deployedBy }}"
      "description": "{{ playbook_dict.ansible_warning }}"
      "role": "rds"
  register: rds_subnet_group_output

创建子网组后,我们需要使用 rds_seach_string 变量找到安全组 ID,并设置一个名为 rds_group_id 的事实。

现在我们已经拥有了启动 RDS 实例所需的所有信息,执行这个任务看起来如下:

- name: "Create the RDS instance"
  amazon.aws.rds_instance:
    id: "{{ rds_name }}"
    region: "{{ region }}"
    state: "{{ state }}"
    db_instance_class: "{{ rds.instance_type }}"
    engine: "{{ rds.engine }}"
    engine_version: "{{ rds.engine_version }}"
    allocated_storage: "{{ rds.allocated_storage }}"
    username: "{{ rds.db_username }}"
    password: "{{ rds.db_password }}"
    db_name: "{{ rds.db_name }}"
    db_subnet_group_name: "{{ rds_subnet_group_output.subnet_group.name }}"
    vpc_security_group_ids: ["{{ rds_group_id }}"]
    tags:
      "Name": "{{ rds_name }}"
      "projectName": "{{ app.name }}"
      "environment": "{{ app.env }}"
      "deployedBy": "{{ playbook_dict.deployedBy }}"
      "description": "{{ playbook_dict.ansible_warning }}"
      "role": "rds"
  register: rds_instance_output

正如上一任务结束时提到的,这个过程可能需要一些时间,通常超过 10 分钟,因此当我们运行 Playbook 时,这个任务看起来会好像已经停止了。

所以请不要担心——它正在后台忙碌地工作。

一旦此角色执行完成,我们将拥有启动 EC2 实例、进行软件配置并安装 WordPress 所需的所有核心 AWS 资源。

临时 EC2 实例角色

在我们开始执行启动临时实例的任务之前,先详细了解一下我们为什么需要临时 EC2 实例。

正如我们在简介中提到的,这个实例将运行 Ubuntu,我们将用稍微修改过的 stack_installstack_configwordpress 角色来操作它,这些角色最早是在 第五章 部署 WordPress 中本地运行过的,后来又在 第九章 迁移到云 中针对单一云实例运行过。

我们将对角色进行的修改之一是安装挂载 EFS 所需的软件,接下来我们将使用它来存储 WordPress 代码和支持文件,以便在共享文件系统中存储所有 WordPress 所需的文件,并可以在多个虚拟机实例上挂载。

第二个变化是,我们不再在本地实例上安装数据库服务器,而是使用 Amazon RDS 数据库服务来支持 WordPress,这意味着我们可以拥有多个 WordPress 实例,它们都能够连接到一个单一的远程数据库。

很好,你可能会想,这样说不错,但这并没有解释为什么这是一个临时实例。

好的,一旦一切安装、挂载、配置完毕并且 WordPress 已经启动,我们将创建自己的Amazon 机器镜像AMI),并终止临时的 EC2 实例。终止后,我们将使用该 AMI 并配置我们的自动扩展组以使用新创建的镜像,如果这是我们第一次运行操作手册,系统将触发新主机的部署;如果我们已经有运行 WordPress 安装的虚拟机实例,它将启动更多实例并终止旧的实例。

当这些虚拟机实例使用我们自定义的 AMI 启动时,它们将已经安装并配置好 NGINX 和 PHP,准备好为 WordPress 提供服务,而且包含我们 WordPress 文件的 EFS 将被挂载,这意味着我们的服务器一旦部署就能立即运行。

所有这些意味着,如果我们因为任何原因遇到流量激增,我们的 WordPress 安装应该能够良好地扩展,而且我们所有的虚拟机实例都将运行一个已知的、良好的配置;事实上,它将与为我们的 WordPress 网站提供服务的其他主机使用相同的配置。

同样重要的是,由于我们不依赖本地虚拟机实例的文件系统中的任何内容,我们在流量激增后能够自动缩放,通过自动终止主机来减少实例数量,而不会有数据丢失或可用性问题的风险。

如果这个方法计划得当——理论上,我们甚至不需要通过 SSH 访问由自动扩展组启动的主机,因为我们不需要手动管理它们,我们可以将它们视为短期实例,完全不需要关心它们是否正在运行或已经终止——只需要确保有足够的实例来交付我们的应用。

所以,现在我们知道了为什么要采用这种方法,让我们回到操作手册,看看需要完成哪些任务,以使这个临时的 EC2 实例能够启动并运行,直到我们可以通过 SSH 连接它并安装我们的软件和 WordPress。

第一个任务是使用我们在本章之前介绍的变量,获取所有 Ubuntu AMI 的列表:

- name: "Gather information about AMIs with the specified filters"
  amazon.aws.ec2_ami_info:
    region: "{{ region }}"
    owners: "{{ ec2.ami.owners }}"
    filters:
      name: "{{ ec2.ami.filters.name }}"
      virtualization-type: "{{ ec2.ami.filters.virtualization_type }}"
  register: ubuntu_ami_info

返回的 AMI 列表将包含我们所选 Ubuntu 版本的所有不同 AMI 版本;我们只需要知道由 Canonical(Ubuntu 的发布者和维护者)发布的最新版本的 ID,这样我们就能确保使用的是包含最新补丁和修复的最新镜像。

幸运的是,列表中返回的每个 AMI 都有一个名为 creation_date 的键,值为 AMI 发布的日期和时间。正如你可能猜到的,这意味着我们可以运行以下任务来获取最新版本 AMI 的 ID:

- name: "Filter the list of AMIs to find the latest one"
  ansible.builtin.set_fact:
    ami: "{{ ubuntu_ami_info.images | sort(attribute='creation_date') | last }}"

如你所见,上一任务获取了列表的内容,这个列表被定义为 ubuntu_ami_info.images,并根据 creation_date 排序,然后取列表中最后一个 AMI 的 ID,因为默认情况下,它们是按升序排序的。

既然我们已经知道了最新的 Ubuntu AMI 的 ID,我们可以继续进行更多的准备工作,为启动我们的 EC2 实例做准备。

现在,我们需要在 AWS 端创建一个 SSH 密钥对。这个密钥对将包含我们用来访问启动后的 EC2 实例的 SSH 密钥的公钥部分 —— 配置此任务的步骤如下,它使用我们在本章之前提到的变量来获取公钥部分的内容:

- name: "Create a SSH Key Pair"
  amazon.aws.ec2_key:
    region: "{{ region }}"
    state: "{{ state }}"
    name: "{{ ec2.keypair.name }}"
    key_material: "{{ ec2.keypair.key_material }}"
    tags:
      "Name": "{{ ec2.keypair.name }}"
      "projectName": "{{ app.name }}"
      "environment": "{{ app.env }}"
      "deployedBy": "{{ playbook_dict.deployedBy }}"
      "description": "{{ playbook_dict.ansible_warning }}"
      "role": "ssh_keypair"
  register: keypair_output

最后,在我们启动 EC2 实例之前,我们需要安全组的 ID,这个安全组允许我们运行 Ansible SSH 的主机通过公共 IP 地址访问 EC2 实例。为此,我们使用 ec2_seach_string 变量查找正确的组 ID,并设置一个名为 ec2_group_id 的事实。

现在,我们已准备好通过以下任务启动 EC2 实例:

- name: "Create the temporary ec2 instance"
  amazon.aws.ec2_instance:
    name: "{{ ec2_tmp_name }}"
    region: "{{ region }}"
    state: "{{ state }}"
    vpc_subnet_id: "{{ subnet_compute_ids[0] }}"
    instance_type: "{{ ec2.instance_type }}"
    security_group: "{{ ec2_group_id }}"
    key_name: "{{ ec2.keypair.name }}"
    network:
      assign_public_ip: "{{ ec2.public_ip }}"
    image_id: "{{ ami.image_id }}"
    tags:
      Name: "{{ ec2_tmp_name }}"
      Description: "{{ dict.ansible_warning }}"
      Project: "{{ app.name }}"
      Environment: "{{ app.env }}"
      Deployed_by: "Ansible"
      Role: "tmp"
  register: ec2_tmp_instance_output

在前一个任务中指出的唯一事项是,当我们为 vpc_subnet_id 添加值时,只能传入单个 ID。由于我们不需要这个虚拟机实例具有高可用性,因此这不是问题,所以我们使用子网 ID 列表中的第一个 ID,即 {{ subnet_compute_ids[0] }}

当在 AWS 中启动 EC2 实例时,它会经历几个阶段,默认情况下,amazon.aws.ec2_instance 模块会创建实例,但不会等待状态从 creating 变为 running

下一步任务会轮询 AWS API,等待我们的 EC2 实例的状态变为 running

- name: "Get information about the temporary EC2 instance to see if it is running"
  amazon.aws.ec2_instance_info:
    region: "{{ region }}"
    filters:
      instance-id: "{{ ec2_tmp_instance_output.instances[0].instance_id }}"
  register: ec2_tmp_instance_state
  delay: 5
  retries: 50
  until: ec2_tmp_instance_state.instances[0].state.name == "running"

如你所见,前一个任务获取了我们新创建的 EC2 实例的 ID,并每隔 5 秒轮询 AWS API,最多轮询 50 次,直到 ec2_tmp_instance_state.instances[0].state.name 的值变为 running

你可能会觉得,这样做有点过于繁琐,99%的情况下,你是对的 —— 状态通常在检查几次后就会改变。不过,也有偶尔 AWS 系统会比较“慢”,在测试中,我曾看到状态变化最多需要检查 15 次,或者大约一分钟的时间。因此,我们需要在 Playbook 中考虑到这个延迟,因为如果不考虑,可能会导致 Playbook 执行失败。

下一步任务是获取当前正在运行的 EC2 实例的详细信息,包括 DNS 名称和 IP 地址,并将其添加到名为 vmgroup 的主机组中:

- name: "Add the temporary EC2 instance to the vmgroup"
  ansible.builtin.add_host:
    name: "{{ ec2_tmp_instance_output.instances[0].public_dns_name }}"
    ansible_ssh_host: "{{ ec2_tmp_instance_output.instances[0].public_ip_address }}"
    groups: "vmgroup"

在交给下一个角色之前,我们应该再进行一次检查。

有时,Ansible Playbook 执行任务太快,以至于即使我们的 EC2 实例状态显示为 running,也不意味着主机已经完全启动,SSH 已启动并可访问:

- name: "Wait for the temporary EC2 instance to be ready to accept SSH connections"
  ansible.builtin.wait_for:
    host: "{{ ec2_tmp_instance_output.instances[0].public_ip_address }}"
    port: "{{ ec2.ssh_port }}"
    delay: 10
    timeout: 300

现在,我们已经确认 EC2 主机可以通过 SSH 连接到运行 Ansible 的机器,接下来可以继续执行 site.yml 文件中这一部分的最后一个角色。

端点角色

这个角色有一个任务,它会创建一个文件 generated_aws_endpoints.yml,其中包含我们创建的 EFS、RDS 和 ELB 资源的 AWS 端点名称:

- name: "Generate the aws endpoints file"
  ansible.builtin.template:
    src: "endponts.j2"
    dest: "group_vars/generated_aws_endpoints.yml"
    mode: "0644"

endponts.j2 模板文件如下所示:

aws_endpoints:
  efs: "{{ efs_output.efs.filesystem_address.split(':')[0] }}"
  rds: "{{ rds_instance_output.endpoint.address }}"
  elb: "{{ loadbalancer_output.dns_name }}"

无论是 RDS 还是 ELB 端点都很直接;对于 EFS,你可能会注意到最后有点不同——那是做什么用的?

efs_output.efs 变量下注册的输出中没有任何内容仅包含 EFS 端点的地址。我们使用的 filesystem_address 包含文件系统挂载的信息,这通过在我们需要的 DNS 地址末尾附加 :/ 来表示。

为了解决这个问题,我们使用了 split 函数,传递 : 作为分隔符,然后取第一个部分(定义为 0),意味着我们得到的是 : 之前的所有内容,这就是我们要找的 DNS 名称。

现在,我们已经有了填充好的 group_vars/generated_aws_endpoints.yml 文件,可以将它作为变量文件加载到 site.yml 文件的第二部分,这样我们就不需要从 EC2 实例与 AWS 进行交互了。

现在,既然我们的 EC2 实例已经启动并运行,让我们安装、配置软件堆栈,并引导 WordPress。

堆栈安装角色

这个角色中的任务与我们之前执行 Playbook 时没有变化,因为我们所做的所有更改都在我们传递的 stack_packages 变量中。

提醒一下,这个角色执行以下操作:

  • 更新 APT 缓存并确保已安装的软件包运行的是最新版本——由于我们使用的是最新的 AMI,因此更新的数量不应该太多。

  • 导入我们将启用的附加仓库的 APT 密钥

  • 安装包含附加仓库详细信息的包并启用它们

  • 安装 system_packagesextra_packagesstack_packages 变量中列出的包——system_packagesextra_packages 包含我们一直使用的默认值,因为我们通过 group_vars/common.yml 文件传递了更新后的 stack_packages 变量,这会覆盖之前章节中仍在 roles/stack_install/defaults/main.yml 文件中定义的默认值。

这为我们提供了安装在 EC2 实例上的所有基础软件。

堆栈配置角色

与之前的角色不同,这个角色做了一些修改,首先是增加了额外的任务。

三个任务被添加到roles/stack_config/tasks/main.yml的顶部,第一个任务是对上一节site.yml文件最后部分中的角色检查的延续:

- name: "Check that the EFS volume is ready"
  ansible.builtin.wait_for:
    host: "{{ aws_endpoints.efs }}"
    port: "2049"
    delay: 10
    timeout: 300

如你所见,这检查了端口2049是否在aws_endpoints.efs定义的端点处可访问;之所以这么做,是因为虽然 EFS 服务已经准备就绪,但 DNS 记录更新并在 VPC 内可访问可能需要一点时间。由于我们接下来将尝试挂载 EFS 文件系统,因此必须确保在继续之前它是可访问的。

接下来的任务是确保 RPC Bind 服务正常运行;我们需要挂载 EFS 文件系统:

- name: "ensure rpcbind service is running"
  ansible.builtin.service:
    name: "rpcbind"
    state: "started"
    enabled: true

最终的附加任务是挂载 EFS,并确保将其添加到文件系统配置中,以确保从现在开始,EFS 会在 EC2 实例启动时挂载:

- name: "mount the EFS volume"
  ansible.posix.mount:
    src: "{{ aws_endpoints.efs }}:/"
    path: "{{ nfs.mount_point }}"
    opts: "{{ nfs.mount_options }}"
    state: "{{ nfs.state }}"
    fstype: "{{ nfs.fstype }}"

如你在本章开始时看到的,我们将 EFS 挂载在/var/www/;我们确保在执行接下来的两个任务之前进行此操作,以确保我们的 WordPress users主目录在共享目录中创建。

这两个任务从上次安装 WordPress 以来没有变化,wordpress_system.home的值也是不变的,即/var/www/wordpress

现在我们已经创建了 WordPress 用户和组,我们可以继续执行剩下的任务:

  • 使用一些合理的默认值更新/etc/nginx/nginx.conf

  • /etc/nginx/conf.d/default.conf中创建我们默认主机的配置。

  • 创建/etc/nginx/global目录,并将restrictions.confwordpress_shared.conf文件复制到该目录。

接下来的任务更多是一个生活质量的提升,涉及到我们的 Playbook 如何处理 PHP。这个 Playbook 的设计目的是通过使用基础的 Ubuntu 镜像,每次从头启动并保持我们的 WordPress 安装最新,而不是管理现有的配置。PHP 的版本在我们 WordPress 安装的生命周期中可能会发生变化。

到目前为止,每当执行stack_config角色时,它都使用了以下变量:

php_fpm_path: "/etc/php/8.1/fpm/pool.d/www.conf"
php_ini_path: "/etc/php/8.1/fpm/php.ini"
php_service_name: "php8.1-fpm"

如你所见,8.1是一个硬编码值。虽然我们可以在配置的其他地方以变量级别覆盖这些变量,但最好在运行时找出安装的 PHP 版本并引用它。

为此,我们可以按照以下方式更新这些值:

php_fpm_path: "/etc/php/{{ php_version }}/fpm/pool.d/www.conf"
php_ini_path: "/etc/php/{{ php_version }}/fpm/php.ini"
php_service_name: "php{{ php_version }}-fpm"

这意味着我们现在必须找到一种方法,将php_version变量填充为相关的 PHP 版本。

为此,我们可以运行php -v命令,它会返回有关安装的 PHP 版本的大量信息。然后,我们使用head和几个cut命令在 Linux 命令行中,使用ansible.builtin.shell而不是 Ansible 内置函数:

- name: "Get the PHP version"
  ansible.builtin.shell:
    cmd: "php -v | head -n 1 | cut -d ' ' -f 2 | cut -c 1-3"
  register: php_version_output

这是我们要求 Ansible 执行的命令的详细分解:

  • php -v:此命令运行时,会输出主机上安装的 PHP 版本信息;该输出通常是多行文本,包括 PHP 版本及其编译方式的附加信息。

  • |:这个符号被称为管道(pipe)。它将左侧命令的输出(在此例中为 php -v)作为输入传递给右侧的命令。它是一种在程序之间传递数据的方式。

  • head -n 1:此命令处理来自上一个命令的输入;head 命令输出接收到的文件或数据的第一部分。-n 1 是一个选项,告诉 head 只输出第一行。所以,在我们的例子中,head -n 1 会从 php -v 的多行输出中只返回第一行。

  • |:另一个管道,仍然是将其左侧命令 head -n 1 的输出传递给右侧的命令。

  • cut -d ' ' -f 2:此命令用于切割每行输入的部分。-d ' ' 是一个选项,其中 -d 表示分隔符,' '(空格)是使用的分隔符。此命令告诉 cut 根据空格将每行分割成多个部分。-f 2 表示第二个字段。此选项告诉 cut 命令选择标准格式下 PHP 版本输出的第二个字段,该字段应为版本号。

  • |:再次出现管道,将输出结果(现在仅包含版本号)传递给下一个命令。

  • cut -c 1-3:此命令进一步处理版本号。-c 1-3 告诉 cut 仅返回字符串中位置为 13 的字符。对于典型的 PHP 版本,如 8.2.1,结果将是 8.2,这正是我们继续执行其他任务所需的版本号。

然后我们可以将输出注册为 php_version_output,并将 php_version 变量设置为事实(fact):

- name: "Set the PHP version"
  ansible.builtin.set_fact:
    php_version: "{{ php_version_output.stdout }}"

现在我们已经获取了 PHP 版本,可以继续进行其他 PHP 任务,这些任务包括将 www.conf 文件复制到 /etc/php/{{ php_version }}/fpm/pool.d/www.conf,并更新 /etc/php/{{ php_version }}/fpm/php.ini 中的 PHP.ini 文件。

有了这些文件,我们启动 PHP-FPM 和 NGINX 服务,并确保它们设置为开机自启。

角色中的最后一个任务是创建 ~/.my.cnf 文件,并将我们的 Amazon RDS 实例信息填充进去。所有其他 MariaDB 任务都已被注释掉,因为我们不再安装本地数据库服务器,因此不需要运行这些配置本地数据库的任务。

WordPress 角色

该角色中只有两个任务被注释掉。创建数据库和数据库用户的任务不需要,因为当 Amazon RDS 实例启动时,数据库和用户已经为我们创建好了,意味着这两个任务是多余的。

其他任务保持不变;欲了解更多详情,请参见第五章部署 WordPress

EC2 AMI 角色

现在我们的软件栈已安装并配置完毕,WordPress 也已设置好,是时候从临时实例创建 AMI 了。

我们首先需要做的是获取临时 EC2 实例的详细信息;由于我们的主机组包含实例的 DNS 名称,我们可以使用以下方式:

- name: "Find out some facts about the instance we have been using"
  amazon.aws.ec2_instance_info:
    region: "{{ region }}"
    filters:
      dns-name: "{{ groups['vmgroup'] }}"
  register: our_instance

现在我们已经将要从中创建 AMI 的实例信息注册为our_instance,可以继续进行 AMI 创建:

- name: "Create the AMI"
  amazon.aws.ec2_ami:
    region: "{{ region }}"
    state: "{{ state }}"
    instance_id: "{{ our_instance.instances[0].instance_id }}"
    wait: "yes"
    name: "{{ ami_name }}-{{ ansible_date_time.date }}_{{ ansible_date_time.hour }}{{ ansible_date_time.minute }}"
    tags:
      "Name": "{{ ami_name }}-{{ ansible_date_time.date }}_{{ ansible_date_time.hour }}{{ ansible_date_time.minute }}"
      "buildDate": "{{ ansible_date_time.date }} {{ ansible_date_time.time }}"
      "projectName": "{{ app.name }}"
      "environment": "{{ app.env }}"
      "deployedBy": "{{ playbook_dict.deployedBy }}"
      "description": "{{ playbook_dict.ansible_warning }}"
      "role": "{{ playbook_dict.ami }}"
  register: ami_output

这里有几点需要注意。如你所见,我们正在使用ansible_date_time来生成date并获取当前时间的hourminute。我们将其用于为 AMI 提供唯一名称,并添加一个名为buildDate的标签。

我们使用日期和时间的原因是可能会在同一天内创建多个 AMI,因此需要确保通过名称能够轻松识别它们。

一旦 AMI 创建完成,我们不再需要临时实例,因此可以终止它:

- name: "Remove any temporary  instances which are running"
  amazon.aws.ec2_instance:
    region: "{{ region }}"
    state: "absent"
    name: "{{ ec2_tmp_name }}"
    filters:
      instance-state-name: "running"
      "tag:Name": "{{ ec2_tmp_name }}"
      "tag:Role": "tmp"
      "tag:Project": "{{ app.name }}"

一旦 EC2 实例被终止,角色中还有一项任务:

- name: "Wait for 2 minutes before continuing"
  ansible.builtin.pause:
    minutes: 2

这正是它所说的功能:它将暂停 Playbook 执行 2 分钟。

我添加这一点是因为曾经有过 AMI 已创建并显示为可用的情况,但由于某些原因,在我们查询 Amazon API 以查找 AMI 时,它需要稍微等待一段时间才能出现在结果中,因此为了避免下一角色开始时可能发生的错误,我发现最好等待一到两分钟。

自动扩展角色

我们已经进入 Playbook 的最后一个角色;在这个角色中,我们将创建部署 EC2 实例所需的所有资源,使用新创建的 AMI 并将其注册到 ELB 上,以便访问我们的 WordPress 站点。

我们首先需要做的是从 API 获取我们所有 AMI 的列表:

- name: "Search for all of our AMIs"
  amazon.aws.ec2_ami_info:
    region: "{{ region }}"
    filters:
      name: "{{ ami_name }}-*"
  register: ami_find

现在我们已有 AMI 列表,我们需要筛选出最新的一个。为此,我们使用与启动临时 EC2 实例时相同的逻辑:

- name: "Find the last one we built"
  ansible.builtin.set_fact:
    ami_sort_filter: "{{ ami_find.images | sort(attribute='creation_date') | last }}"

现在我们已经将 AMI 列表筛选到最新的那个,我们需要设置两个事实,一个是 AMI 的名称,另一个是 AMI 的 ID:

- name: "Grab AMI ID and name of the most recent result"
  ansible.builtin.set_fact:
    our_ami_id: "{{ ami_sort_filter.image_id }}"
    our_ami_name: "{{ ami_sort_filter.name }}"

在我们开始创建/更新资源之前,需要的最后一条信息是我们用于 EC2 实例的安全组 ID。

如之前所述,我们使用ec2_seach_string变量来查找正确的组 ID,并设置一个名为ec2_group_id的事实。

接下来,我们需要创建或更新启动模板(如果已有模板)。

启动模板包含我们将在自动扩展组中启动的实例的基本配置:

- name: "Create the launch template"
  community.aws.ec2_launch_template:
    region: "{{ region }}"
    state: "{{ state }}"
    name: "{{ launch_template_name }}"
    version_description: "{{ our_ami_name }}"
    image_id: "{{ our_ami_id }}"
    security_group_ids: ["{{ ec2_group_id.security_groups[0].group_id }}"]
    instance_type: "{{ ec2.instance_type }}"
    disable_api_termination: "{{ ec2.asg.disable_api_termination }}"
    tags:
      "Name": "{{ ec2_name }}"
      "projectName": "{{ app.name }}"
      "environment": "{{ app.env }}"
      "deployedBy": "{{ playbook_dict.deployedBy }}"
      "description": "{{ playbook_dict.ansible_warning }}"
      "role": "launchTemplate"

在此任务中,我们创建了启动模板,并发布了一个以 AMI 名称命名的版本,以便我们可以快速识别它;然后,我们附加了相应的 AMI ID 和安全组 ID,并设置了要启动实例的规格。

在启动模板准备好之后,我们需要从 AWS API 获取更多信息,然后才能创建自动扩展组。

首先,我们需要在 ELB 角色中创建的目标组的 ID:

- name: "Find out the target group ARN"
  community.aws.elb_target_group_info:
    region: "{{ region }}"
    names:
      - "{{ elb_target_group_name }}"
  register: elb_target_group_output

然后,我们需要获取子网的 ID,这些子网将用于部署作为自动扩展组一部分的 EC2 实例,以下任务会收集子网的信息:

- name: "Get information on the ec2 subnets"
  amazon.aws.ec2_vpc_subnet_info:
    region: "{{ region }}"
    filters:
      tag:role: "*{{ subnet_role_compute }}*"
  register: ec2_subnet_output

现在我们已经有了子网的信息,需要仅提取每个子网的 ID 并创建一个列表:

- name: "Create a list of subnet IDs"
  ansible.builtin.set_fact:
    subnet_ec2_ids: "{{ subnet_ec2_ids | default([]) + [item.subnet_id] }}"
  loop: "{{ ec2_subnet_output.subnets }}"

这是我们需要的最后一部分信息,现在可以继续创建或更新自动扩展组:

- name: "Create/update the auto-scaling group using the launch template we just created"
  amazon.aws.autoscaling_group:
    region: "{{ region }}"
    state: "{{ state }}"
    name: "{{ asg_name }}"
    target_group_arns: ["{{ elb_target_group_output.target_groups[0].target_group_arn }}"]
    launch_template:
      launch_template_name: "{{ launch_template_name }}"
    min_size: "{{ ec2.asg.min_size }}"
    max_size: "{{ ec2.asg.max_size }}"
    desired_capacity: "{{ ec2.asg.desired_capacity }}"
    health_check_period: "{{ ec2.asg.health_check_period }}"
    health_check_type: "{{ ec2.asg.health_check_type }}"
    replace_all_instances: "{{ ec2.asg.replace_all_instances }}"
    replace_batch_size: "{{ ec2.asg.replace_batch_size }}"
    vpc_zone_identifier: "{{ subnet_ec2_ids }}"
    wait_for_instances: "{{ ec2.asg.wait_for_instances }}"
    wait_timeout: "{{ ec2.asg.wait_timeout }}"
    tags:
      - key: "Name"
        value: "{{ ec2_name }}"
        propagate_at_launch: true
      - key: "Project"
        value: "{{ app.name }}"
        propagate_at_launch: true
      - key: "Environment"
        value: "{{ app.env }}"
        propagate_at_launch: true
      - key: "Deployed_by"
        value: "Ansible"
        propagate_at_launch: true
  register: ec2_asg_output

这是我们将要启动的最终资源,发生的事情相当多,因此我们需要更详细地了解。

首先,我们有 AWS 相关模块中的基本配置标准,这些模块在整个 Playbook 中都有调用;在这里,我们设置了资源的名称、区域和状态,对于本 Playbook,状态将设置为 present

接下来,我们必须提供目标组的 target_group_arns 键,指定负载均衡器的目标组 ARN,我们将其设置为 elb_target_group_output 中的第一个目标组 ARN,然后 launch_template 键通过其名称引用启动模板,设置为 launch_template_name 的值。

现在我们已经有了大小和容量设置;min_sizemax_sizedesired_capacity 键分别使用 ec2.asg.min_sizeec2.asg.max_sizeec2.asg.desired_capacity 变量进行设置,这些变量定义了自动扩展组的最小、最大和期望实例数。

接下来是健康检查配置,设置 health_check_periodhealth_check_type 键,以控制如何检查 自动扩展组 (ASG) 中实例的健康状况。

现在我们有了实例替换设置。replace_all_instancesreplace_batch_size 键分别指示是否替换所有实例,并提供替换实例的批量大小。

然后,我们设置了网络配置,设置 vpc_zone_identifier 来使用存储在 subnet_ec2_ids 中的子网 ID 列表,将 ASG 中的实例分布到这些子网和可用区中。

接下来是等待设置,控制任务是否应该等待实例的状态为 running,并指定等待该条件满足的最长时间。

最后,你会注意到,我们在这里标记的方式与 Playbook 的其余部分有所不同;该任务定义了几个标签(NameProjectEnvironmentDeployed_by)以及相应的值,所有标签都标记为在启动时传播,这意味着由自动扩展组启动的 EC2 实例在启动时会继承这些标签。

这就是我们对 Playbook 的演练总结。如你所见,我们扩展了原始的 AWS 网络 Playbook,从第十章,“构建云网络”,将更多的服务整合进来,并结合了我们在第五章,“部署 WordPress”中讨论的 WordPress 角色——现在剩下的就是运行 Playbook 了。

运行 Playbook

现在我们有了部署资源到 AWS 所需的所有角色,我们可以运行 Playbook。首先,我们需要通过运行以下命令,并使用你自己的凭证设置环境变量,告诉 Ansible 我们的访问密钥和密钥:

$ export AWS_ACCESS_KEY=AKIAI5KECPOTNTTVM3EDA
$ export AWS_SECRET_KEY=Y4B7FFiSWl0Am3VIFc07lgnc/TAtK5+RpxzIGTr

设置好环境变量后,通过以下命令启动 Ansible 运行:

$ ansible-playbook -i hosts site.yml

与前几章不同,我们这里只关注部署资源时发生的重点,而不是只看 Playbook 运行结束时的结果。

Playbook 运行亮点

这不是完整的 Playbook 输出,在运行 Playbook 时,我没有启用调试,因此所有这些任务都将被跳过。

我们从 VPC 开始:

PLAY [Deploy and configure the AWS Environment] ***********
TASK [Gathering Facts] ************************************
ok: [localhost]
TASK [roles/vpc : Create VPC] *****************************
changed: [localhost]

一旦我们收集到我们选择区域的可用区信息,我们就有地方放置子网了:

TASK [roles/subnets : Get some information on the available zones] *************
ok: [localhost]

一旦我们获得了这些信息,Playbook 会循环执行并包括create_subnet.yml任务:

TASK [roles/subnets : Create all subnets] *****************
included: create_subnet.yml for localhost => (item={'name': 'ec2', 'role': 'compute'})
included: create_subnet.yml for localhost => (item={'name': 'rds', 'role': 'database'})
included: create_subnet.yml for localhost => (item={'name': 'efs', 'role': 'storage'})
included: create_subnet.yml for localhost => (item={'name': 'dmz', 'role': 'public'})

接着,我们获取每个包含的四个任务运行的结果,第一个结果如下所示:

TASK [roles/subnets : Create subnet in the availability zone] *****************************************************
changed: [localhost] => (item={'state': 'available', 'opt_in_status': 'opt-in-not-required', 'messages': [], 'region_name': 'eu-west-1', 'zone_name': 'eu-west-1a', 'zone_id': 'euw1-az1', 'group_name': 'eu-west-1', 'network_border_group': 'eu-west-1', 'zone_type': 'availability-zone'})
changed: [localhost] => (item={'state': 'available', 'opt_in_status': 'opt-in-not-required', 'messages': [], 'region_name': 'eu-west-1', 'zone_name': 'eu-west-1b', 'zone_id': 'euw1-az2', 'group_name': 'eu-west-1', 'network_border_group': 'eu-west-1', 'zone_type': 'availability-zone'})
changed: [localhost] => (item={'state': 'available', 'opt_in_status': 'opt-in-not-required', 'messages': [], 'region_name': 'eu-west-1', 'zone_name': 'eu-west-1c', 'zone_id': 'euw1-az3', 'group_name': 'eu-west-1', 'network_border_group': 'eu-west-1', 'zone_type': 'availability-zone'})

如你所见,为eu-west-1区域的每个可用区创建了一个子网——这一过程会重复三次。所有子网添加完成后,我们会获取更多关于已创建内容的信息。

接下来,运行 Internet Gateway 角色:

TASK [roles/gateway : Create an Internet Gateway] *********
changed: [localhost]
TASK [roles/gateway : Create a route table so the internet gateway can be used by the public subnets] ****************
changed: [localhost]

如你所记得,在那个角色中并没有发生太多事情,不像下一个角色,它会添加网络安全组,我们首先通过获取你当前的公共 IP 地址开始:

TASK [roles/securitygroups : Find out your current public IP address using https://ipify.org/] **********************
ok: [localhost]
TASK [roles/securitygroups : Set your public ip as a fact]*
ok: [localhost]

如你所记得,我们将两个组分两部分创建——首先,我们创建基础组:

TASK [roles/securitygroups : Create the base security groups] ***************************************************
changed: [localhost] => (item={'name': 'learnansible-elb-security-group', 'description': 'opens port 80 and 443 to the world', 'id_var_name': 'elb_group_id', 'rules': [{'proto': 'tcp', 'from_port': '80', 'to_port': '80', 'cidr_ip': '0.0.0.0/0', 'rule_desc': 'allow all on port 80'}, {'proto': 'tcp', 'from_port': '443', 'to_port': '443', 'cidr_ip': '0.0.0.0/0', 'rule_desc': 'allow all on port 443'}]})
changed: [localhost] => (item={'name': 'learnansible-ec2-security-group', 'description': 'opens port 22 to a trusted IP and port 80 to the elb group', 'id_var_name': 'ec2_group_id', 'rules': [{'proto': 'tcp', 'from_port': '22', 'to_port': '22', 'cidr_ip': '86.177.22.88/32', 'rule_desc': 'allow 86.177.22.88/32 access to port 22'}, {'proto': 'tcp', 'from_port': '80', 'to_port': '80', 'group_id': '', 'rule_desc': 'allow access to port 80 from ELB'}]})
changed: [localhost] => (item={'name': 'learnansible-rds-security-group', 'description': 'opens port 3306 to the ec2 instances', 'id_var_name': 'rds_group_id', 'rules': [{'proto': 'tcp', 'from_port': '3306', 'to_port': '3306', 'group_id': '', 'rule_desc': 'allow  access to port 3306'}]})
changed: [localhost] => (item={'name': 'learnansible-efs-security-group', 'description': 'opens port 2049 to the ec2 instances', 'id_var_name': 'efs_group_id', 'rules': [{'proto': 'tcp', 'from_port': '2049', 'to_port': '2049', 'group_id': '', 'rule_desc': 'allow  access to port 2049'}]})

然后,我们获取刚刚启动的基础的相关信息并将其设置为事实:

TASK [roles/securitygroups : Set the fact for the security group ids] ************************************************
ok: [localhost] => (item={'name': 'learnansible-elb-security-group', 'description': 'opens port 80 and 443 to the world', 'id_var_name': 'elb_group_id', 'rules': [{'proto': 'tcp', 'from_port': '80', 'to_port': '80', 'cidr_ip': '0.0.0.0/0', 'rule_desc': 'allow all on port 80'}, {'proto': 'tcp', 'from_port': '443', 'to_port': '443', 'cidr_ip': '0.0.0.0/0', 'rule_desc': 'allow all on port 443'}]})
ok: [localhost] => (item={'name': 'learnansible-ec2-security-group', 'description': 'opens port 22 to a trusted IP and port 80 to the elb group', 'id_var_name': 'ec2_group_id', 'rules': [{'proto': 'tcp', 'from_port': '22', 'to_port': '22', 'cidr_ip': '86.177.22.88/32', 'rule_desc': 'allow 86.177.22.88/32 access to port 22'}, {'proto': 'tcp', 'from_port': '80', 'to_port': '80', 'group_id': '', 'rule_desc': 'allow access to port 80 from ELB'}]})
ok: [localhost] => (item={'name': 'learnansible-rds-security-group', 'description': 'opens port 3306 to the ec2 instances', 'id_var_name': 'rds_group_id', 'rules': [{'proto': 'tcp', 'from_port': '3306', 'to_port': '3306', 'group_id': '', 'rule_desc': 'allow  access to port 3306'}]})
ok: [localhost] => (item={'name': 'learnansible-efs-security-group', 'description': 'opens port 2049 to the ec2 instances', 'id_var_name': 'efs_group_id', 'rules': [{'proto': 'tcp', 'from_port': '2049', 'to_port': '2049', 'group_id': '', 'rule_desc': 'allow  access to port 2049'}]})

最后,我们添加规则;你会从输出中看到,我们传入了我们所创建的组的 ID,以便将它们作为规则的一部分使用:

TASK [roles/securitygroups : Provision security group rules] ****************************************************
changed: [localhost] => (item={'name': 'learnansible-elb-security-group', 'description': 'opens port 80 and 443 to the world', 'id_var_name': 'elb_group_id', 'rules': [{'proto': 'tcp', 'from_port': '80', 'to_port': '80', 'cidr_ip': '0.0.0.0/0', 'rule_desc': 'allow all on port 80'}, {'proto': 'tcp', 'from_port': '443', 'to_port': '443', 'cidr_ip': '0.0.0.0/0', 'rule_desc': 'allow all on port 443'}]})
changed: [localhost] => (item={'name': 'learnansible-ec2-security-group', 'description': 'opens port 22 to a trusted IP and port 80 to the elb group', 'id_var_name': 'ec2_group_id', 'rules': [{'proto': 'tcp', 'from_port': '22', 'to_port': '22', 'cidr_ip': '86.177.22.88/32', 'rule_desc': 'allow 86.177.22.88/32 access to port 22'}, {'proto': 'tcp', 'from_port': '80', 'to_port': '80', 'group_id': 'sg-04f31e782e30e1f0a', 'rule_desc': 'allow access to port 80 from ELB'}]})
changed: [localhost] => (item={'name': 'learnansible-rds-security-group', 'description': 'opens port 3306 to the ec2 instances', 'id_var_name': 'rds_group_id', 'rules': [{'proto': 'tcp', 'from_port': '3306', 'to_port': '3306', 'group_id': 'sg-05bffd3eb96602519', 'rule_desc': 'allow sg-05bffd3eb96602519 access to port 3306'}]})
changed: [localhost] => (item={'name': 'learnansible-efs-security-group', 'description': 'opens port 2049 to the ec2 instances', 'id_var_name': 'efs_group_id', 'rules': [{'proto': 'tcp', 'from_port': '2049', 'to_port': '2049', 'group_id': 'sg-05bffd3eb96602519', 'rule_desc': 'allow sg-05bffd3eb96602519 access to port 2049'}]})

现在,规则配置完毕后,我们可以开始部署一些使用这些规则的资源,从目标组和 ELB 开始:

TASK [roles/elb : Provision the target group] *************
changed: [localhost]
TASK [roles/elb : Provision an application elastic load balancer] *************************************************
changed: [localhost]

然后是 EFS:

TASK [roles/efs : Generate the efs targets vars file] *****
changed: [localhost]
TASK [roles/efs : Include the efs targets vars file] ******
ok: [localhost]
TASK [roles/efs : Create the EFS File System] *************
changed: [localhost]

现在是 RDS:

TASK [roles/rds : Add RDS subnet group] *******************
changed: [localhost]
TASK [roles/rds : Create the RDS instance] ****************
changed: [localhost]

现在是时候创建临时的 EC2 实例了。首先,我们找到要使用的 AMI:

TASK [roles/ec2tmp : Gather information about AMIs with the specified filters] ****************************************
ok: [localhost]
TASK [roles/ec2tmp : filter the list of AMIs to find the latest one] ***********************************************
ok: [localhost]

接着,我们创建 SSH 密钥对:

TASK [roles/ec2tmp : Create an SSH Key Pair] **************
changed: [localhost]

然后,我们创建 EC2 实例本身:

TASK [roles/ec2tmp : Create the temporary ec2 instance] ***
changed: [localhost]

在实例配置好后,我们需要等待它的状态变为 running

TASK [roles/ec2tmp : Get information about the temporary EC2 instance to see if it is running] ***
FAILED - RETRYING: [localhost]: Get information about the temporary EC2 instance to see if it is running (50 retries left).
. . . .
FAILED - RETRYING: [localhost]: Get information about the temporary EC2 instance to see if it is running (46 retries left).
ok: [localhost]

现在实例正在运行,我们将新启动的 EC2 实例添加到我们的主机组:

TASK [roles/ec2tmp : Add the temporary EC2 instance to the vmgroup] **************************************************
changed: [localhost]
TASK [roles/ec2tmp : Wait for the temporary EC2 instance to be ready to accept SSH connections] ***********************
ok: [localhost]

在我们继续连接 EC2 主机以安装和配置软件栈及 WordPress 之前,我们生成端点变量文件:

TASK [roles/endpoints : Generate the aws endpoints file] **
changed: [localhost]

这标志着 site.yml 文件的第一部分结束,我们现在可以通过 SSH 登录到临时 EC2 主机并安装一切:

PLAY [Install and configure Wordpress] ********************
TASK [Gathering Facts] ************************************
ok: [ec2-18-203-221-2.eu-west-1.compute.amazonaws.com]

然后,我们继续执行安装,正如我们之前讨论的,这几乎与我们在 第五章中讨论的 部署 WordPress,以及 第九章中讨论的 迁移到云端 相同 —— 除了这些任务,它们会挂载 EFS 文件系统:

TASK [roles/stack_config : Check that the EFS volume is ready] ****************************************************
ok: [ec2-18-203-221-2.eu-west-1.compute.amazonaws.com]
TASK [roles/stack_config : ensure rpcbind service is running] **************************************************
ok: [ec2-18-203-221-2.eu-west-1.compute.amazonaws.com]
TASK [roles/stack_config : mount the EFS volume] **********
changed: [ec2-18-203-221-2.eu-west-1.compute.amazonaws.com]

这些任务获取 PHP 版本并将其设置为事实:

TASK [roles/stack_config : Get the PHP version] ***********
changed: [ec2-18-203-221-2.eu-west-1.compute.amazonaws.com]
TASK [roles/stack_config : Set the PHP version] ***********
ok: [ec2-18-203-221-2.eu-west-1.compute.amazonaws.com]

一旦完成,NGINX 和 PHP-FPM 会被重启:

RUNNING HANDLER [roles/stack_config : restart nginx] ******
changed: [ec2-18-203-221-2.eu-west-1.compute.amazonaws.com]
RUNNING HANDLER [roles/stack_config : restart php-fpm] ****
changed: [ec2-18-203-221-2.eu-west-1.compute.amazonaws.com]

这标志着引导临时 EC2 实例的任务结束。我们现在可以回到本地机器并运行 sites.yml 文件的最后部分。

首先,我们创建 AMI 并终止临时 EC2 实例:

TASK [roles/ec2ami : find out some facts about the instance we have been using] ***************************************
ok: [localhost]
TASK [roles/ec2ami : create the AMI] **********************
changed: [localhost]
TASK [roles/ec2ami : remove any temporary instances which are running] **********************************************
changed: [localhost]

然后,我们等待两分钟:

TASK [roles/ec2ami : wait for 2 minutes before continuing]
Pausing for 120 seconds
(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort)
ok: [localhost]

现在,我们获取刚才创建的 AMI 的详细信息:

TASK [roles/autoscaling : Search for all of our AMIs] *****
ok: [localhost]
TASK [roles/autoscaling : Find the last one we built] *****
ok: [localhost]
TASK [roles/autoscaling : Grab AMI ID and name of the most recent result] ********************************************
ok: [localhost]

一旦我们有了这些详细信息,我们就创建(或如果我们已经运行过 Playbook,则更新)启动模板:

TASK [roles/autoscaling : Create the launch template] *****
changed: [localhost]

现在,我们收集创建/更新自动扩展组所需的信息:

TASK [roles/autoscaling : find out the target group ARN] **
ok: [localhost]
TASK [roles/autoscaling : get information on the ec2 subnets] **************************************************
ok: [localhost]

然后我们创建自动扩展组将使用的子网列表:

TASK [roles/autoscaling : create a list of subnet IDs] ****
ok: [localhost] => (item={'availability_zone': 'eu-west-1c', 'availability_zone_id': 'euw1-az3', 'available_ip_address_count': 27, 'cidr_block': '10.0.0.64/27', 'default_for_az': False, 'map_public_ip_on_launch': False, 'map_customer_owned_ip_on_launch': False, 'state': 'available', 'subnet_id': 'subnet-091ea1834c5fc8e48', 'vpc_id': 'vpc-008808ff628883751', 'owner_id': '687011238589', 'assign_ipv6_address_on_creation': False, 'ipv6_cidr_block_association_set': [], 'tags': {'role': 'compute', 'deployedBy': 'Ansible', 'Name': 'ec2-subnet-euw1-az3', 'environment': 'prod', 'description': 'Resource managed by Ansible', 'projectName': 'learnansible'}, 'subnet_arn': 'arn:aws:ec2:eu-west-1:687011238589:subnet/subnet-091ea1834c5fc8e48', 'enable_dns64': False, 'ipv6_native': False, 'private_dns_name_options_on_launch': {'hostname_type': 'ip-name', 'enable_resource_name_dns_a_record': False, 'enable_resource_name_dns_aaaa_record': False}, 'id': 'subnet-091ea1834c5fc8e48'})

前面的输出会重复两次,以适应我们将要使用的其他两个子网;然后,我们最终创建/更新自动扩展组:

TASK [roles/autoscaling : Create/update the auto-scaling group using the launch template we just created] **********
changed: [localhost]

现在,我们已经来到了我们的 Playbook 执行的尾声,接下来是回顾:

PLAY RECAP ************************************************
ec2-18-203-221-2.eu-west-1.compute.amazonaws.com :
ok=37   changed=28   unreachable=0    failed=0    skipped=1    rescued=0    ignored=2
localhost :
ok=56   changed=23   unreachable=0    failed=0    skipped=30   rescued=0    ignored=0

当我运行 Playbook 时,第一次运行花费了超过 20 分钟,随后的运行大约需要 10 分钟才能完成。

所以,凭借一条命令和大约 20 分钟的时间,我们就能得到一个高可用的原生 WordPress 安装。如果你从 AWS 控制台获取到弹性负载均衡器的公共 URL,或者通过检查 group_vars/generated_aws_endpoints.yml 文件中的 elb 键值,你应该能够看到你的网站。

终止所有资源

在我们完成这一章之前,我们需要看看如何终止这些资源;为此,你可以运行以下命令:

$ ansible-playbook -i hosts destroy.yml

这会按照我们启动资源的相反顺序删除一切,从自动扩展组开始:

PLAY [Destroy the AWS Environment created by the site.yml playbook] ************
TASK [Gathering Facts] ************************************
ok: [localhost]
TASK [Delete the Auto Scaling Group] **********************
changed: [localhost]
TASK [Delete the Launch Template] *************************
changed: [localhost]

由于可能有多个 AMI,我们收集一些信息,然后循环删除所有返回的内容:

TASK [Get information about the AMIs] *********************
ok: [localhost]
TASK [Delete the AMI(s)] **********************************
changed: [localhost] => (item={'architecture': 'x86_64', 'creation_date': '2024-01-12T09:44:07.000Z', 'image_id': 'ami-0ddfeb5a1fb64c23a', 'image_location': '687011238589/learnansible-prod-ami-2024-01-12_0944', 'image_type': 'machine', 'public': False, 'tags': {'Name': 'learnansible-prod-ami-2024-01-12_0944', 'deployedBy': 'Ansible', 'environment': 'prod', 'buildDate': '2024-01-12 09:44:06', 'description': 'Resource managed by Ansible', 'projectName': 'learnansible', 'role': 'ami'}, 'virtualization_type': 'hvm', 'source_instance_id': 'i-050689909fa289998'})

接着,我们删除更多一次性资源:

TASK [Create a SSH Key Pair] ******************************
changed: [localhost]
TASK [Delete the group_vars/generated_aws_endpoints.yml file] *****************************************************
changed: [localhost]
TASK [Delete the RDS database] ****************************
changed: [localhost]
TASK [Delete RDS subnet group] ****************************
changed: [localhost]
TASK [Delete the group_vars/generated_rds_passwordfile file] *****************************************************
changed: [localhost]
TASK [Delete the EFS File System] *************************
changed: [localhost]
TASK [Delete the group_vars/generated_efs_targets.yml file]
changed: [localhost]
TASK [Delete the application elastic load balancer]********
changed: [localhost]
TASK [Delete the target group] *********************************************************************
changed: [localhost]

由于安全组相互引用,我们需要按相反顺序创建它们的列表,以便能够尝试删除下一个要删除的组所引用的组:

TASK [Create a reversed list of the security group names] *
ok: [localhost]
TASK [Delete the security groups] *************************
changed: [localhost] => (item=learnansible-efs-security-group)
changed: [localhost] => (item=learnansible-rds-security-group)
changed: [localhost] => (item=learnansible-ec2-security-group)
FAILED - RETRYING: [localhost]: Delete the security groups (50 retries left).
. . . . .
FAILED - RETRYING: [localhost]: Delete the security groups (46 retries left).
changed: [localhost] => (item=learnansible-elb-security-group)

你可能注意到它在接近尾声时失败了;这是因为 AWS API 有点跟不上,Playbook 运行的速度超过了它返回的结果。

我们检查了更多任务:

TASK [Get information about the VPC] **********************
ok: [localhost]
TASK [Get information about the Route Table] **************
ok: [localhost]
TASK [Delete the Route Table] *****************************
changed: [localhost] => (item={'associations': [{'main': False, 'route_table_association_id': 'rtbassoc-0738bb9e5aaf44848', 'route_table_id': 'rtb-04bc7177949ad2c92', 'subnet_id': 'subnet-07c28d376283741f6', 'association_state'
TASK [Delete the Internet Gateway]*************************
changed: [localhost]

接下来,我们来处理子网:

TASK [Get information on the subnets] **************************************************************
ok: [localhost]
TASK [Delete the subnets] *********************************
changed: [localhost] => (item={'availability_zone': 'eu-west-1c', 'availability_zone_id': 'euw1-az3', 'available_ip_address_count': 27, 'cidr_block': '10.0.0.64/27', 'default_for_az': False, 'map_public_ip_on_launch': False, 'map_customer_owned_ip_on_launch': False, 'state': 'available', 'subnet_id': 'subnet-091ea1834c5fc8e48', 'vpc_id': 'vpc-008808ff628883751', 'id': 'subnet-091ea1834c5fc8e48'})
. . . . .
changed: [localhost] => (item={'availability_zone': 'eu-west-1b', 'availability_zone_id': 'euw1-az2', 'available_ip_address_count': 27, 'cidr_block': '10.0.0.128/27', 'default_for_az': False, 'map_public_ip_on_launch': False, 'map_customer_owned_ip_on_launch': False, 'state': 'available', 'subnet_id': 'subnet-0fd4610392872d442', 'vpc_id': 'vpc-008808ff628883751', 'id': 'subnet-0fd4610392872d442'})

最后,我们回顾一下 VPC:

TASK [Delete the VPC] *************************************
changed: [localhost]
PLAY RECAP ************************************************
localhost :
ok=23   changed=17   unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

一旦操作手册运行完毕,我建议您登录 AWS 控制台,仔细检查确保一切已正确删除,以避免产生任何意外费用。

总结

在本章中,我们通过创建并启动一个高度可用的 WordPress 安装,将我们的 AWS 部署提升到了一个新水平。通过利用 AWS 提供的各种服务,我们消除了关于实例可用性和可用区使用的任何单点故障。

我们还在我们的操作手册中加入了逻辑,使得使用相同的命令可以启动新的部署或更新现有的操作系统,通过滚动部署包含更新包的新实例 AMI,确保在部署过程中零停机时间。

虽然 WordPress 的部署尽可能简化,但在使用更复杂的应用程序时,部署生产就绪的镜像将保持类似的方式。

在下一章中,我们将讨论如何从公共云迁移到私有云,以及 Ansible 如何与 VMware 互动。

第十二章:12

扩展 VMware 部署

现在我们已经了解如何在 AWS 中启动网络和服务,接下来我们将讨论如何在VMware环境中部署类似的设置,并深入探讨核心 VMware 模块。

本章将涵盖以下主题:

  • VMware 介绍

  • VMware REST 模块

技术要求

本章将讨论 VMware 产品家族的各种组件,以及如何使用 Ansible 与它们交互。尽管本章中会有示例的 Playbook 任务,但这些任务可能需要根据您的安装情况进行调整。因此,不建议在未先审查完整文档的情况下使用本章中的任何示例。

VMware 介绍

VMware 拥有超过 25 年的历史,从一个隐形的初创公司发展到今天,经历了显著的变化。到 2023 年 8 月,VMware 的收入已超过 130 亿美元,VMware 的产品组合已发展到涵盖大约 30 种产品,最为人熟知的是其虚拟机监控程序(Hypervisor),并且它是大多数企业的标准配置,使管理员能够在各种基于 x86 的硬件配置上快速部署虚拟机。

然而,最近的变化发生在博通于 2023 年底收购 VMware 之后,变化十分显著。

这次收购极大简化了 VMware 的产品组合,这是受到客户和合作伙伴反馈的影响,使各种规模的用户都能从 VMware 解决方案中获得更多价值。两个显著的产品包括 VMware Cloud Foundation 和 VMware vSphere Foundation,每个都有高级附加产品。

博通实施的首项重大变化是将 VMware 转变为基于订阅的模式。这与云计算消费的行业标准一致,旨在通过逐步淘汰永久许可并用订阅或期限许可证替代,来为客户提供持续的创新、更快的价值实现时间和可预测的投资,从而支持客户和合作伙伴在数字化转型中的成功。

在更广泛的行业中,关于博通收购 VMware 后的战略存在一些担忧。有人猜测博通可能会专注于保留仅有的最大和最盈利的 VMware 客户和合作伙伴。此策略可能导致 VMware 产品组合的重组,以更好地与博通的业务目标对接,可能包括资产处置和更简化的产品范围。

在写作时(2024 年初),这些变化对 VMware 现有客户群和合作伙伴生态系统的影响尚不清楚,更多细节预计将在全年逐步浮现,因为博通将继续实施其针对 VMware 的战略长期计划。

VMware REST 模块

如前所述,VMware 产品系列中大约有 30 个产品,而 Ansible 拥有可以与其中许多产品交互的模块。

然而,由于产品简化,我们将集中讨论 vmware.vmware_rest 命名空间中的模块,而不会查看 community.vmware 中的任何模块,因为这些模块将在 2025 年某个时候失去所有支持。

这两组模块的区别在于,正如其名称所示,vmware.vmware_rest 模块使用 VMware REST API 来管理资源,而 community.vmware 中的模块则使用 Python 库与各种 VMware 端点进行交互以执行任务。

vmware.vmware_rest 命名空间中的模块分为三个领域:

  • Appliance:这些模块管理你的 vCenter Appliance,它们是组成 vCenter 部署的基础资源。

  • Content:内容库模块允许你管理定义和管理库中项、订阅、发布和存储的服务。

  • vCenter:这些模块允许你管理运行在 vCenter 部署上的工作负载,例如虚拟机。

让我们先来看一下 VMware REST appliance 模块。

VMware REST appliance 模块

在撰写本文时,已有超过 60 个模块;这些模块被划分到各自清晰标记的区域。

访问模块

首先,我们有访问模块:

  • appliance_access_consolecli:此模块允许你启用或禁用基于控制台的 CLI(TTY1)。

  • appliance_access_consolecli_info:这个模块返回基于控制台的 CLI(TTY1)的当前状态;它要么是启用,要么是禁用。

  • appliance_access_dcui:使用此模块,你可以配置 Direct Console User InterfaceDCUI TTY2)的状态;同样,你只有两个选项:启用或禁用。

  • appliance_access_dcui_info:正如你可能已经猜到的,这个模块返回 DCUI TTY2 状态的启用或禁用。

  • appliance_access_shell:同样,这个模块的功能很简单,就是改变 BASH 的启用状态。启用后,你可以在 CLI 中访问 BASH shell。

  • appliance_access_shell_info:此模块仅返回 BASH 访问状态;它要么是启用,要么是禁用。

  • appliance_access_ssh:此模块设置基于 SSH 控制的 CLI 的启用状态。

  • appliance_access_ssh_info:这个模块返回基于 SSH 控制的 CLI 的启用状态。

如前所述,这些模块中的每一个要么允许你设置访问系统的状态,要么返回当前配置的状态:

- name: "Enable SSH access"
  vmware.vmware_rest.appliance_access_ssh:
    enabled: true
  register: access_ssh_result

每个非信息类模块都有一个值为 enabled,其值可以是 truefalse,如前所示。

健康信息模块

下一个模块组仅返回关于系统健康状态的信息:

  • appliance_health_applmgmt_info

  • appliance_health_database_info

  • appliance_health_databasestorage_info

  • appliance_health_load_info

  • appliance_health_mem_info

  • appliance_health_softwarepackages_info

  • appliance_health_storage_info

  • appliance_health_swap_info

  • appliance_health_system_info

您可以像这样调用其中一个模块:

- name: "Get the system health status"
  vmware.vmware_rest.appliance_health_system_info:
  register: health_system_result

这将返回您查询的任何服务的当前健康状态。

Infraprofile 模块

在这里,我们只有两个模块:

  • appliance_infraprofile_configs: 此模块导出所选配置文件

  • appliance_infraprofile_configs_info: 此模块列出所有已注册的配置文件

appliance_infraprofile_configs模块的唯一有效状态是export:

- name: "Export the ApplianceManagement profile"
  vmware.vmware_rest.appliance_infraprofile_configs:
    state: "export"
    profiles:
    - "ApplianceManagement"
  register: infraprofile_configs_result

这里的输出是包含所选配置文件的 JSON。在上述示例中,这是ApplianceManagement

本地帐户模块

在这里,我们有三个模块:

  • appliance_localaccounts_globalpolicy

  • appliance_localaccounts_globalpolicy_info

  • appliance_localaccounts_info

这些模块允许您设置和查询全局策略,并返回所有或只返回一个本地帐户的信息。

监控模块

虽然这里只有两个模块,但是当您组合它们时它们可以非常强大:

  • appliance_monitoring_info: 此模块返回一组监视器

  • appliance_monitoring_query: 此模块允许您查询监视器

这里是一个示例查询:

- name: "Query the monitoring backend"
  vmware.vmware_rest.appliance_monitoring_query:
    start_time: "2024-01-01 09:00:00+00:00"
    end_time: "2024-01-01 10:00:00+00:00"
    names:
    - "mem.total"
    interval: "MINUTES5"
    function: "AVG"
  register: mem_total_result

如您所见,在前述任务中,我们正在查询 2024 年 1 月 1 日上午 9 点至 10 点之间的总内存量,耗时 5 分钟。

网络模块

这是事情开始变得有点复杂的地方; 每个模块都有一个info等效项,其中突出显示:

  • appliance_networking(以及信息): 此模块重置和重新启动所有接口的网络配置。它还更新 DHCP 分配的 DHCP IP 地址的租约。

  • appliance_networking_dns_domains(以及信息): 此模块用于管理 DNS 搜索域。

  • appliance_networking_dns_hostname(以及信息): 此模块配置完全合格的域名FQDN)主机名。

  • appliance_networking_dns_servers(以及信息): 此模块可以管理 DNS 服务器配置。

  • appliance_networking_firewall_inbound(以及信息): 此模块设置防火墙规则的有序列表。

  • appliance_networking_interfaces_info: 此模块获取单个网络接口的信息。

  • appliance_networking_interfaces_ipv4(以及信息): 此模块管理指定网络接口的 IPv4 网络配置。

  • appliance_networking_interfaces_ipv6(以及信息): 此模块管理指定网络接口的 IPv6 网络配置。

  • appliance_networking_noproxy(以及信息): 此模块配置不应应用代理配置的服务器。

  • Appliance_networking_proxy(以及信息): 此模块配置用于指定协议的代理服务器。

时间和日期模块

下列模块在某种程度上影响时间和日期设置:

  • appliance_ntp(以及信息): 此模块管理 NTP 服务器配置。

  • appliance_system_time_info: 此模块获取系统时间

  • appliance_system_time_timezone(以及信息): 此模块设置时区

  • appliance_timesync module(附加信息):此模块配置时间同步模式

其余模块

其余模块涵盖设备配置和管理:

  • appliance_services(附加信息):您可以使用此模块重启给定的服务

  • appliance_shutdown(附加信息):此模块允许您取消挂起的关机操作

  • appliance_system_globalfips(附加信息):使用此模块,您可以启用或禁用设备的全局 FIPS 模式

  • appliance_system_storage(附加信息):此模块将所有分区调整为磁盘大小的 100%

  • appliance_system_version_info:此模块获取版本信息

  • appliance_update_info:此模块获取设备更新的状态

  • appliance_vmon_service(附加信息):此模块列出 vmon 管理的服务的详细信息

这部分内容到此结束。接下来,我们将查看内容模块。

VMware REST 内容模块

有少数几个模块允许您管理和收集有关内容库的信息:

  • content_configuration(附加信息):此模块用于更新配置

  • content_library_item_info:此模块在提供标识符时返回 {@link ItemModel}

  • content_locallibrary(附加信息):此模块用于创建一个新的本地库

  • content_subscribedlibrary(附加信息):此模块用于创建一个新的订阅

vCenter 模块

在这里,您将看到更有趣的内容。使用这些模块,您可以启动、配置并管理虚拟机的整个生命周期。在查看虚拟机之前,我们将先看看一些支持 vCenter 的模块。

支持的 vCenter 模块

这些支持模块允许您管理托管在 vCenter 中的资源池、数据中心、文件夹和数据存储:

  • vcenter_cluster_info:此模块检索与 {@``param.name cluster_name} 相对应的集群信息

  • vcenter_datacenter(附加信息):此模块向您的 vCenter 库中添加一个新的数据中心

  • vcenter_datastore_info:此模块获取有关数据存储的信息,使用 {@``param.name datastore_name}

  • vcenter_folder_info:此模块检索最多 1,000 个文件夹的详细信息,这些文件夹符合 {@link FilterSpec} 并且当前用户有权限查看

  • vcenter_host(附加信息):此模块可用于将新的独立主机添加到您的 vCenter

  • vcenter_network_info:此模块返回关于 vCenter 中前 1,000 个可见网络的信息,具体取决于您的权限,符合 {@link FilterSpec}

  • vcenter_ovf_libraryitem:此模块用于从虚拟机或虚拟设备创建内容库中的项目

  • vcenter_resourcepool(附加信息):此模块用于部署资源池

  • vcenter_storage_policies_info:此模块获取 vCenter 中可用的存储策略信息;最多返回 1,024 条结果

虚拟机模块

最后一组模块涉及创建和管理虚拟机及其关联资源。我们先来看看主要的模块 vcenter_vm

vcenter_vm 模块用于创建虚拟机。例如,一个基本的任务如下所示:

- name: "Create a Virtual Machine"
  vmware.vmware_rest.vcenter_vm:
    placement:
      cluster: "{{ lookup('vmware.vmware_rest.cluster_moid', '/learnansible_dc/host/learnansible_cluster') }}"
folder: "{{ lookup('vmware.vmware_rest.folder_moid', '/learnansible_dc/vm') }}"
      resource_pool: "{{ lookup('vmware.vmware_rest.resource_pool_moid', '/learnansible_dc/host/learnansible_cluster/Resources') }}"
    name: "LearnAnsibleVM"
    guest_OS: "UBUNTU_64"
    hardware_version: "VMX_11"
    memory:
      hot_add_enabled: true
      size_MiB: 4000
  register: LearnAnsibleVM_output

如你所见,我们使用了不同的查找模块来查找集群、数据存储、文件夹和资源池的 ID——如果我们有这些信息,可以直接提供这些 ID。

一旦虚拟机创建完成,我们可以使用其余的模块进一步配置虚拟机或管理其状态:

  • vcenter_vm_guest_customization:该模块对虚拟机应用客制化设置,例如运行脚本。

  • vcenter_vm_guest_filesystem_directories:使用该模块,你可以在虚拟机操作系统中创建一个目录。

  • vcenter_vm_guest_identity_info:该模块获取关于虚拟机的身份信息。

  • vcenter_vm_guest_localfilesystem_info:该模块获取虚拟机操作系统中本地文件系统的详细信息。

  • vcenter_vm_guest_networking_info:该模块获取虚拟机操作系统中网络配置的详细信息。

  • vcenter_vm_guest_networking_interfaces_info:该模块显示虚拟机操作系统中的网络接口信息。

  • vcenter_vm_guest_networking_routes_info:该模块显示虚拟机操作系统中的网络路由信息。

  • vcenter_vm_guest_operations_info:该模块获取关于虚拟机操作系统状态的信息。

  • vcenter_vm_guest_power(附加信息):该模块请求从虚拟机操作系统内部进行软关机、待机(挂起)或软重启。

  • vcenter_vm_hardware:该模块用于更新所请求虚拟机的硬件设置。

  • vcenter_vm_hardware_adapter_sata(附加信息):该模块配置一个虚拟 SATA 适配器。

  • vcenter_vm_hardware_adapter_scsi(附加信息):该模块添加一个虚拟 SCSI 适配器。

  • vcenter_vm_hardware_boot(附加信息):该模块用于管理虚拟机与启动相关的设置。

  • vcenter_vm_hardware_boot_device(附加信息):该模块可以设置将作为启动驱动器使用的虚拟设备。

  • vcenter_vm_hardware_cdrom(附加信息):该模块将一个虚拟 CD-ROM 附加到虚拟机。

  • vcenter_vm_hardware_cpu(附加信息):该模块管理虚拟机的 CPU 设置。

  • vcenter_vm_hardware_disk(附加信息):该模块将虚拟磁盘连接到虚拟机。

  • vcenter_vm_hardware_ethernet(附加信息):该模块将虚拟以太网适配器连接到虚拟机。

  • vcenter_vm_hardware_floppy(附加信息):该模块向虚拟机添加一个虚拟软盘驱动器。

  • vcenter_vm_hardware_info:该模块获取虚拟机的虚拟硬件设置信息。

  • vcenter_vm_hardware_memory(附加信息):该模块配置虚拟机的内存设置。

  • vcenter_vm_hardware_parallel(附加信息):此模块添加虚拟并行端口。

  • vcenter_vm_hardware_serial(附加信息):此模块添加虚拟串行端口。

  • vcenter_vm_info:此模块返回关于虚拟机的信息。

  • vcenter_vm_libraryitem_info:此模块检索与你的虚拟机相关联的库项信息。

  • vcenter_vm_power(附加信息):此模块对虚拟机执行启动、硬关机、硬重置或硬暂停操作——也就是说,它按下虚拟机前面的电源按钮。

  • vcenter_vm_storage_policy(附加信息):此模块更新虚拟机虚拟硬盘的存储策略。

  • vcenter_vm_storage_policy_compliance:此模块更新并收集关于虚拟机存储策略合规性的相关信息。

  • vcenter_vm_tools(附加信息):此模块用于管理 VMware Tools 的配置。

  • vcenter_vm_tools_installer(附加信息):此模块将 VMware Tools 安装程序作为 CD-ROM 挂载,使其在虚拟机操作系统中可用。

  • vcenter_vmtemplate_libraryitems(附加信息):此模块创建并返回内容库中项目信息。

如你所见,vmware.vmware_rest 集合提供了全面的虚拟机资源管理支持,更棒的是,这些模块都是设计来使用官方 REST API,这意味着无论你是使用命令行界面、Web 界面还是 Ansible,你都可以安全地混合搭配管理 VMware 资源的方式。所有内容都是通过相同的 REST API 进行管理。

总结

如你所见,从这一长串模块中,你可以使用 Ansible 完成作为 VMware 管理员每天需要执行的大部分管理和配置任务。

再加上我们在第八章中查看的模块,Ansible 网络模块,用于管理网络设备,以及支持如 NetApp 存储设备等硬件的模块。

通过这样做,你可以构建跨越物理设备、VMware 元素和你本地企业级虚拟化基础设施中运行的虚拟机的复杂剧本。

正如本章开始时提到的,写作时 VMware 正处于动荡之中。本章旨在展示可能的操作方法,而不是为你提供一个实际的 VMware 资源管理实践指南。有关 vmware.vmware_rest 集合当前状态的更多详情,请访问 galaxy.ansible.com/ui/repo/published/vmware/vmware_rest/

在下一章,我们将讨论如何通过扫描常见问题和潜在安全问题,确保我们的剧本遵循最佳实践。

第四部分:Ansible 工作流

在本书的最后部分,您将学习高级 Ansible 工作流和最佳实践,包括安全实践、剧本扫描、服务器加固、CI/CD 集成、Ansible AWX 和 Red Hat Ansible 自动化平台。最终,您将掌握在实际场景中有效利用 Ansible 所需的知识。

本部分包含以下章节:

  • 第十三章扫描您的 Ansible 剧本

  • 第十四章使用 Ansible 强化您的服务器

  • 第十五章将 Ansible 与 GitHub Actions 和 Azure DevOps 结合使用

  • 第十六章介绍 Ansible AWX 和 Red Hat Ansible 自动化平台

  • 第十七章与 Ansible 的下一步

第十三章:扫描你的 Ansible 剧本

本章中,你将学习如何使用两个第三方工具扫描你的 Ansible 剧本:Checkov 和 KICS。它们都是开源工具,可以帮助你识别和修复 Ansible 代码中的常见配置问题,如语法错误、配置错误、硬编码的密钥和部署问题,这些都可能导致潜在的安全漏洞。

到本章结束时,你将完成以下任务:

  • 在我们的 Ansible 剧本上安装并运行 Checkov 和 KICS 扫描

  • 审查扫描过程中生成的结果和报告

  • 修复扫描过程中检测到的任何问题

本章涉及以下主题:

  • 为什么要扫描你的剧本?

  • Docker 概述与安装

  • 探索 Checkov

  • 探索 KICS

技术要求

我们将不再在本地安装工具,而是使用 Docker 来执行扫描;本章稍后会有关于如何安装 Docker 的详细内容。此外,我们将扫描我们在 第十一章 中编写的剧本的一个变种,高可用云部署;该剧本可以在 github.com/PacktPublishing/Learn-Ansible-Second-Edition/tree/main/Chapter13 的代码库中找到。

为什么要扫描你的剧本?

虽然在前几章中我们采取了合理的方式来部署云资源,但我们所设置的许多保护措施都是我通过经验和一些常识积累而来的。

例如,在微软 Azure 或亚马逊 Web 服务(AWS)中启动虚拟机资源时,我们一直将 SSH 或 RDP 服务限制为主机的公共 IP 地址,该主机运行着 Ansible;直到现在,这个主机通常是你的本地机器,而不是像使用 0.0.0.0/0 这样的源地址将 SSH 或 RDP 开放给全世界,这个地址的 CIDR 表示法是 允许所有

对我们一直在处理的工作负载来说,这不是一个问题;将虚拟机直接暴露在互联网上,并且其管理端口对所有人开放并不是最佳实践,因为这将使你容易受到暴力破解攻击。如果攻击成功,不仅该机器可能会被攻破,还可能成为进入你网络其他部分以及其他相关资源(如数据库和存储)的跳板。

我将前面的示例归类为常识,但随着我们使用剧本(playbooks)发布越来越多的云服务,如何确保我们遵循最佳实践,特别是对于那些我们可能没有太多经验的服务,除了让它们上线之外?我们如何在资源部署之前设置一些保护措施,防止做出错误的操作?

这就是本章将要介绍的两款工具发挥作用的地方;它们的设计目的是扫描您的剧本,查看配置,并将其与最佳实践策略进行比较。最终,在第十五章使用 Ansible 与 GitHub Actions 和 Azure DevOps,我们将把其中一个工具构建到我们的部署流水线中,但目前,我们将查看这些工具并使用 Docker 在本地运行它们。

Docker 概述与安装

Docker,作为一个让容器流行的平台,既是开源解决方案也是商业解决方案,使您能够将应用程序的所有元素,包括库和其他依赖项,与您自己的代码一起打包到一个易于分发的包中;这意味着我们不需要为本章运行的工具下载和安装所有的前置条件,也不需要从源代码编译工具来获得适用于我们系统的可执行文件。

要按照本章的示例操作,您必须在主机上安装Docker Desktop

在 macOS 上安装 Docker Desktop

要在 macOS 上安装 Docker Desktop,请按照以下三个步骤操作:

  1. 选择适合您 Mac 架构的安装程序:

    1. 对于 ARM64(Apple Silicon),请使用 desktop.docker.com/mac/main/arm64/Docker.dmg

    2. 对于 AMD64(Intel Macs),请使用 desktop.docker.com/mac/main/amd64/Docker.dmg

  2. 下载完成后,双击打开Docker.dmg文件。在弹出的窗口中,将 Docker 图标拖入您的应用程序文件夹以安装 Docker Desktop。它将被安装在/Applications/Docker.app

  3. 要启动 Docker,请前往应用程序文件夹,双击Docker;这将启动Docker Desktop

当您首次启动 Docker Desktop 时,它将引导您完成剩余的安装步骤,并在完成后在后台运行。

在 Windows 上安装 Docker Desktop

要在 Windows 上安装 Docker Desktop,请按照以下说明操作:

  1. 从此链接下载 Windows 版本的 Docker Desktop 安装程序:desktop.docker.com/win/main/amd64/Docker%20Desktop%20Installer.exe

  2. 运行下载的C:\Program Files\Docker\Docker

  3. 在安装过程中,您可能会被提示选择是否使用WSL 2(Windows Subsystem for Linux 2)Hyper-V作为后端。请选择使用 WSL 2 而非 Hyper-V选项,因为我们在本书中使用的是这一选项来运行 Ansible。

  4. 按照安装向导提供的屏幕指示授权安装程序并完成安装过程。

  5. 安装完成后,点击关闭以完成设置。

从这里,您可以从开始菜单打开 Docker Desktop,它将会在后台运行。

在 Linux 上安装 Docker Desktop

如果你在运行 Linux 桌面系统,指令会根据你的 Linux 发行版有所不同;有关详细说明,请参见 docs.docker.com/desktop/linux/install/

现在,安装了 Docker Desktop 后,我们可以开始了解我们将要使用的两个工具中的第一个。

探索 Checkov

Checkov 是一个开源的静态代码分析工具,由 Prisma Cloud 维护,专为 基础设施即代码 (IaC) 设计。

它帮助开发人员和 DevOps 团队在部署到云环境之前识别文件中的错误配置。通过扫描 Terraform、CloudFormation、Kubernetes 以及其他工具(包括 Ansible)的代码,Checkov 会检查最佳实践和合规性指南,确保你的基础设施部署在发布之前是安全、高效且符合行业标准的。

重要提示

你可能已经注意到,前面的描述中提到了 Ansible 作为“其他工具”;这是因为在撰写本文时(2024 年初),Ansible 的支持才刚刚被引入。因此,虽然我们会在本章中查看 Checkov,但我们不会深入讲解 Checkov 或第二个工具 Kics。

在运行扫描之前,我们需要一个 playbook;打开终端并通过运行以下命令检查 GitHub 仓库:

$ git clone git@github.com:PacktPublishing/Learn-Ansible-Second-Edition.git

该仓库包含来自 第十一章 的最终 playbook 代码,高度可用 云部署

既然我们已经检查了代码,接下来可以下载 Checkov 容器镜像。为此,我们需要通过运行以下命令从 Docker Hub 拉取它:

$ docker image pull bridgecrew/checkov:latest

这将从 hub.docker.com/r/bridgecrew/checkov 下载镜像,下载完成后,我们现在可以扫描我们的 playbook 代码了。

要运行扫描,请执行以下命令:

$ cd Learn-Ansible-Second-Edition/Chapter13
$ docker container run --rm --tty --volume ./:/ansible --workdir /ansible bridgecrew/checkov --directory /ansible

在查看结果之前,让我们简要分析一下刚才执行的命令:

  • docker container run 执行一个新的 Docker 容器。

  • --rm 指示 Docker 在容器退出后自动移除容器。

  • --tty 分配一个伪终端,使扫描输出对我们的会话可读。

  • --volume ./:/ansible 将当前目录(定义为 ./)挂载到容器内的 /ansible 路径。

  • --workdir /ansible 将容器内的工作目录设置为 /ansible

  • bridgecrew/checkov 指定了我们刚从 Docker Hub 拉取的 Checkov Docker 镜像。

  • --directory /ansible 指示 Checkov 扫描 /ansible 目录中的文件;这不是 Docker 命令的一部分,而是向 Checkov 二进制文件发送指令,Checkov 是我们容器运行扫描的默认入口。如果我们在本地安装了 Checkov,那么这相当于运行 checkov --directory /ansible 命令。

现在我们已经拆解了用于运行扫描的命令,接下来我们可以查看扫描本身的输出,从概览开始:

ansible scan results:
Passed checks: 5, Failed checks: 3, Skipped checks: 0

如你所见,我们的通过检查多于失败检查,这是个不错的开始;输出的下一部分详细列出了检查内容,从以下的通过检查开始:

Check: CKV_ANSIBLE_2: "Ensure that certificate validation isn't disabled with get_url"
     PASSED for resource: tasks.ansible.builtin.get_url.download wp-cli
     File: /roles/wordpress/tasks/main.yml:11-17

我们的第一次检查是查看我们是否指示 ansible.builtin.get_url 模块在连接到 HTTPS 网站下载内容时绕过证书验证。

接下来的四次检查是针对我们在 playbook 中使用 ansible.builtin.apt 模块的两次情况:

Check: CKV_ANSIBLE_5: "Ensure that packages with untrusted or missing signatures are not used"
     PASSED for resource: tasks.ansible.builtin.apt.update apt-cache and upgrade packages
     File: /roles/stack-install/tasks/main.yml:5-13
Check: CKV_ANSIBLE_5: "Ensure that packages with untrusted or missing signatures are not used"
     PASSED for resource: tasks.ansible.builtin.apt.update cache and install the stack packages
     File: /roles/stack-install/tasks/main.yml:27-33

第一对检查确保我们没有安装任何未正确签名的软件包。第二对检查也检查相同的内容:

Check: CKV_ANSIBLE_6: "Ensure that the force parameter is not used, as it disables signature validation and allows packages to be downgraded which can leave the system in a broken or inconsistent state"
     PASSED for resource: tasks.ansible.builtin.apt.update apt-cache and upgrade packages
     File: /roles/stack-install/tasks/main.yml:5-13
Check: CKV_ANSIBLE_6: "Ensure that the force parameter is not used, as it disables signature validation and allows packages to be downgraded which can leave the system in a broken or inconsistent state"
     PASSED for resource: tasks.ansible.builtin.apt.update cache and install the stack packages
     File: /roles/stack-install/tasks/main.yml:27-33

然而,这次检查确保我们没有使用 force 参数,正如你从描述中看到的,这会禁用签名检查,并且如果出现问题,还可能导致我们的 APT 数据库处于某种不稳定状态。

接下来,我们进入失败的部分;第一个失败是我们在讲解为什么要使用本章所介绍的工具时提到的例子:

Check: CKV_AWS_88: "EC2 instance should not have public IP."
     FAILED for resource: tasks.amazon.aws.ec2_instance.Create the temporary ec2 instance
     File: /roles/ec2tmp/tasks/main.yml:53-75
      Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/public-policies/public-12
           62 |     network:
           63 |       assign_public_ip: "{{ ec2.public_ip }}"

那么,问题出在哪里?正如你可能还记得的 第十一章,《高可用云部署》中提到的,我们启动的实例是临时的,只能在 playbook 运行时访问。然而,Checkov 并不知道这一点,所以它正确地指出了这一点,并且正如你所看到的,提供了通过指南 URL 解释原因,对于此检查,指南 URL 是 docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/public-policies/public-12

继续查看扫描中的下一个失败,我们看到如下内容:

Check: CKV_AWS_135: "Ensure that EC2 is EBS optimized"
     FAILED for resource: tasks.amazon.aws.ec2_instance.Create the temporary ec2 instance
     File: /roles/ec2tmp/tasks/main.yml:53-75
     Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/aws-general-policies/ensure-that-ec2-is-ebs-optimized

在这种情况下,Checkov 认为在我们启动临时 EC2 实例时,amazon.aws.ec2_instance 块中缺少一个参数。建议将参数 ebs_optimized 设置为 true,而不是保持默认的 false 值。

扫描输出中的最后一个失败如下:

Check: CKV2_ANSIBLE_2: "Ensure that HTTPS url is used with get_url"
     FAILED for resource: tasks.ansible.builtin.get_url.download wp-cli
    File: /roles/wordpress/tasks/main.yml:11-17
           11 | - name: "download wp-cli"
           12 |   ansible.builtin.get_url:
           13 |     url: "{{ wp_cli.download }}"
           14 |     dest: "{{ wp_cli.path }}"

由于 Checkov 是进行静态代码分析,它并没有设计用来检查变量的内容。因为我们提供的策略是检查是否使用了安全的 URL(也就是任务中的 url 部分是 https://domain.com/),它失败了,因为它只看到 {{ wp_cli.download }} 变量名,而不是变量的内容。

如果你在跟踪的话,那就意味着三次失败检查中有两次是误报;对于第一次失败,我们可以接受风险,因为我们知道该机器只是临时的,并且我们已经将 EC2 实例限制为可信的 IP 地址。

对于第三次失败,我们可以确认 {{ wp_cli.download }} 变量的内容是一个安全的 URL,因为它是 https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar

第二个失败是唯一需要我们查看的;让我们从 Amazon.aws.ec2_instance 任务开始看。

在这里,我们需要添加两件事;第一件事是一个注释,指示 Checkov 我们接受 CKV_AWS_88 策略所突出显示的风险,然后我们需要将 ebs_optimized 设置为 true

以下代码展示了我对 roles/ec2tmp/tasks/main.yml 所做的更新;name 参数下方的内容保持不变:

- name: "Create the temporary ec2 instance"
  amazon.aws.ec2_instance:
    # checkov:skip=CKV_AWS_88:"While a public IP address is assigned to the instance, it is locked down by the security group and the instance is temporary."
    ebs_optimized: true
    name: "{{ ec2_tmp_name }}"

如你所见,指示 Checkov 跳过检查非常简单;注释分为四个部分:

  • # 是在 YAML 文件中开始注释的标准语法

  • checkov: 指示 Checkov 关注注释的内容

  • skip=CKV_AWS_88: 指示 Checkov 在运行时跳过 CKV_AWS_88 检查

  • "虽然实例分配了公共 IP 地址,但它已通过安全组进行限制,并且实例是临时的。" 是我们运行扫描时将在输出中显示的抑制注释

更新任务中的下一行实现了我们将 ebs_optimized 参数设置为 true 的建议。

现在,我们进入第二个任务,需要更新的任务位于 roles/wordpress/tasks/main.yml 中。这里,我们只需要添加一个注释,让 Checkov 跳过 CKV2_ANSIBLE_2

- name: "download wp-cli"
  ansible.builtin.get_url:
    # checkov:skip=CKV2_ANSIBLE_2:"The URL passed in the variable is secured with SSL/TLS protocol."
    url: "{{ wp_cli.download }}"
    dest: "{{ wp_cli.path }}"

如果你跟着操作,仓库中有一个名为 checkov 的分支;应用前述详细更改后,你可以通过运行以下命令切换到该分支:

$ git switch chapter13-checkov

然后,我们可以使用以下命令重新运行扫描:

$ docker container run --rm --tty --volume ./:/ansible --workdir /ansible bridgecrew/checkov --directory /ansible

我看到我的更改已经抑制并解决了三个失败:

ansible scan results:
Passed checks: 6, Failed checks: 0, Skipped checks: 2

我们已经通过了 CKV_AWS_135

Check: CKV_AWS_135: "Ensure that EC2 is EBS optimized"
     PASSED for resource: tasks.amazon.aws.ec2_instance.Create the temporary ec2 instance
      File: /roles/ec2tmp/tasks/main.yml:53-77
      Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/aws-general-policies/ensure-that-ec2-is-ebs-optimized

现在,我们也看到了显示的两个假阳性:

Check: CKV_AWS_88: "EC2 instance should not have public IP."
     SKIPPED for resource: tasks.amazon.aws.ec2_instance.Create the temporary ec2 instance
     Suppress comment: "While a public IP address is assigned to the instance, it is locked down by the security group and the instance is temporary."
     File: /roles/ec2tmp/tasks/main.yml:53-77
     Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/public-policies/public-12

对于第二个问题,我们有如下内容:

Check: CKV2_ANSIBLE_2: "Ensure that HTTPS url is used with get_url"
     SKIPPED for resource: tasks.ansible.builtin.get_url.download wp-cli
     Suppress comment: "The URL passed in the variable is secured with SSL/TLS protocol."
     File: /roles/wordpress/tasks/main.yml:11-18

如你所见,我们的注释对所有人都是可见的。

那么,回到本节开头的提问,既然 Checkov 并未完全覆盖 Ansible,为什么我们还要介绍这个工具呢?从我们对 playbook 执行扫描的输出中可以看到,尽管现在覆盖面不大,但每次新版本发布时都会增加更多的 Ansible 策略。因此,随着时间的推移,覆盖面应该只会变得更加健全,并且希望我们能将这个有前景的工具与我们将要看的第二个工具对接:KICS,或者其完整名称是 保持基础设施即代码的安全

探索 KICS

KICS 是另一个静态代码分析工具,像 Checkov 一样,它是开源的。它旨在帮助你发现基础设施即代码(IaC)中的常见配置错误、潜在合规问题,甚至安全漏洞。它支持 Kubernetes、Docker、AWS CloudFormation、Terram,当然还有 Ansible,在本章中我们将重点讨论 Ansible。

KICS 设计易于安装、理解并集成到 CI/CD 流水线中。它包括超过 2400 条可定制规则,并且具备可扩展性,允许轻松添加对新的 IaC 工具的支持,并更新现有集成。

KICS 由 Checkmarx 的软件应用安全测试专家维护和支持,这意味着 KICS 具有很好的背景。

运行扫描

让我们直接开始。如果你还没有,使用以下命令查看示例仓库:

$ git clone https://github.com/russmckendrick/Learn-Ansible-Second-Edition-Scan.git

现在,我们可以通过使用以下命令从 Docker Hub (hub.docker.com/r/checkmarx/kics) 拉取最新的容器镜像:

$ docker image pull checkmarx/kics:latest

切换到包含我们 Ansible playbook 的文件夹:

$ cd Learn-Ansible-Second-Edition-Scan

然后运行扫描:

$ docker container run --rm --tty --volume ./:/ansible checkmarx/kics scan --path /ansible/

如你所见,docker 命令遵循了我们在运行 Checkov 时讨论的相同模式,直到我们将选项传递给 KICS 二进制文件;在这里,我们指示 KICS 对 --path /ansible/ 进行 scan,这是我们通过 --volume 选项将主机机上的目录挂载到容器中的路径。

审查结果

现在,让我们来看一下扫描的结果;KICS 展示了它的输出,这与 Checkov 略有不同,因为初始输出旨在提供关于扫描本身的实时信息:

Scanning with Keeping Infrastructure as Code Secure v1.7.12
Preparing Scan Assets: Done
Executing queries: [------------------------------] 100.00%
Files scanned: 33
Parsed files: 32
Queries loaded: 292
Queries failed to execute: 0

现在,让我们逐步分析各种结果,并按严重性级别进行分组。

信息和低严重性结果

第一个结果突出了我们创建(使用模板)或复制的文件可能存在的风险文件权限:

Risky File Permissions, Severity: INFO, Results: 5
Description: Some modules could end up creating new files on disk with permissions that might be too open or unpredictable
Platform: Ansible
Learn more about this vulnerability: https://docs.kics.io/latest/queries/ansible-queries/common/88841d5c-d22d-4b7e-a6a0-89ca50e44b9f

然后,它列出了所有受影响的文件;以下是前几个文件的片段:

[1]: ../../ansible/destroy.yml:52
     051:     - name: "Delete the group_vars/generated_aws_endpoints.yml file"
     052:       ansible.builtin.file:
     053:         path: "group_vars/generated_aws_endpoints.yml"

这是另一个:

[2]: ../../ansible/destroy.yml:81
     080:     - name: "Delete the group_vars/generated_efs_targets.yml file"
     081:       ansible.builtin.file:
     082:         path: "group_vars/generated_efs_targets.yml"

继续下一个问题,我们看到以下内容:

Unpinned Package Version, Severity: LOW, Results: 1
Description: Setting state to latest performs an update and installs additional packages possibly resulting in performance degradation or loss of service
Platform: Ansible
Learn more about this vulnerability: https://docs.kics.io/latest/queries/ansible-queries/common/c05e2c20-0a2c-4686-b1f8-5f0a5612d4e8

再次,下面是它发现问题的一个示例:

[1]: ../../ansible/roles/stack_install/tasks/main.yml:8
      007:     name: "*"
      008:     state: "latest"
      009:     update_cache: true

下一个也是最后一个低评分的结果如下:

EFS Without Tags, Severity: LOW, Results: 1
Description: Amazon Elastic Filesystem should have filesystem tags associated
Platform: Ansible
Learn more about this vulnerability: https://docs.kics.io/latest/queries/ansible-queries/aws/b8a9852c-9943-4973-b8d5-77dae9352851

详细信息如下:

[1]: ../../ansible/destroy.yml:75
      074:     - name: "Delete the EFS File System"
      075:       community.aws.efs:
      076:         name: "{{ efs_name }}"

在继续到得分中等的结果之前,让我们快速回顾一下低评分的结果。

所以,第一个结果是“一些模块可能会在磁盘上创建新的文件,权限可能过于开放或不可预测”。它指出了我们 playbook 中的 11 个地方可能会出现这个问题,因此我们应该着手解决这些问题。

首先,如果你运行了完整的扫描,你会注意到三个结果来自 destroy.yml 文件。

由于这些任务涉及删除文件,我们不关心文件权限。因此,我们不应在单独的任务中添加权限,而应该指示 KICS 不要在整个文件上执行检查。

要做到这一点,我们需要在文件的最顶部添加以下注释:

# kics-scan disable=88841d5c-d22d-4b7e-a6a0-89ca50e44b9f

接下来,我们在 roles/efs/tasks/main.yml 中看到了 ansible.builtin.template。与其跳过测试,我通过 mode 键添加了权限:

- name: "Generate the efs targets vars file"
  ansible.builtin.template:
    src: "targets.j2"
    dest: "group_vars/generated_efs_targets.yml"
    mode: "0644"

最终结果是 ansible.builtin.get_url 模块,该模块由任务使用,下载 wp-cli,文件位于 roles/wordpress/tasks/main.yml

审查代码时,它看起来如下:

- name: "Download wp-cli"
  ansible.builtin.get_url:
    url: "{{ wp_cli.download }}"
    dest: "{{ wp_cli.path }}"

紧接着是以下内容:

- name: "Update permissions of wp-cli to allow anyone to execute it"
  ansible.builtin.file:
    path: "{{ wp_cli.path }}"
    mode: "0755"

在这里,KICS 强调我们可以将模式作为 ansible.builtin.get_url 的一部分进行设置,这意味着我们不需要单独处理它,因此我在下载任务中添加了以下内容:

    mode: "0755"

然后,我删除了第二个任务。这解决了 KICS 报告的文件权限问题。

下一个低评分显示,“将状态设置为最新执行更新并安装附加包,可能导致性能下降或服务丢失”

这出现在roles/stack-install/tasks/main.yml中,其中任务使用ansible.builtin.apt来更新已安装的镜像,因为这个任务仅在我们启动临时 EC2 实例时调用,而且我们已经在主剧本中考虑了 PHP 版本的变化。我认为可以将其视为假阳性,因此我们可以通过在roles/stack-install/tasks/main.yml文件的最顶部添加以下内容来告诉 KICS 不要对该文件运行测试:

# kics-scan disable=c05e2c20-0a2c-4686-b1f8-5f0a5612d4e8

这就剩下destroy.yml,因此缺少标签确实很重要。

我们排除该检查的执行。为此,我们需要将其附加到我们已添加的注释末尾,这意味着destroy.yml文件末尾的注释现在是这样:

# kics-scan disable=88841d5c-d22d-4b7e-a6a0-89ca50e44b9f,b8a9852c-9943-4973-b8d5-77dae9352851

在附加 ID 时,请确保用逗号将它们分开;否则,KICS 会将它们视为一个字符串。最后,我们来看看高严重性结果。

高严重性结果

幸运的是,这里我们只有两个问题,分布在四个任务中,从以下任务开始:

EFS Not Encrypted, Severity: HIGH, Results: 2
Description: Elastic File System (EFS) must be encrypted
Platform: Ansible
Learn more about this vulnerability: https://docs.kics.io/latest/queries/ansible-queries/aws/727c4fd4-d604-4df6-a179-7713d3c85e20

这就是这两个任务:

[1]: ../../ansible/roles/efs/tasks/main.yml:25
     024: - name: "Create the EFS File System"
     025:   community.aws.efs:

第二个问题出现在destroy.yml文件中:

[2]: ../../ansible/destroy.yml:77
     076:     - name: "Delete the EFS File System"
     077:       community.aws.efs:

我想你应该能猜到我们如何解决第二个问题;让我们让它忽略destroy.yml中的测试:

# kics-scan disable=88841d5c-d22d-4b7e-a6a0-89ca50e44b9f,b8a9852c-9943-4973-b8d5-77dae9352851,050f085f-a8db-4072-9010-2cca235cc02f,727c4fd4-d604-4df6-a179-7713d3c85e20

对于roles/efs/tasks/main.yml,推荐启用加密,所以我们采取这个建议:

- name: "Create the EFS File System"
  community.aws.efs:
    encrypt: true
    name: "{{ efs_name }}"

如你从前面的代码片段中看到的,我们已经添加了encrypt参数并将其设置为true

KICS 指出的下一个问题也与 EFS 文件系统加密有关:

EFS Without KMS, Severity: HIGH, Results: 2
Description: Amazon Elastic Filesystem should have filesystem encryption enabled using KMS CMK customer-managed keys instead of AWS managed-keys
Platform: Ansible
Learn more about this vulnerability: https://docs.kics.io/latest/queries/ansible-queries/aws/bd77554e-f138-40c5-91b2-2a09f878608e

结果与前一个问题相同,因此我们将把 ID 附加到destroy.yml文件顶部的禁用检查列表中。

鉴于这是一个演示环境,我愿意接受不使用客户管理的密钥库来存储我自己管理的加密密钥的潜在风险;因此,在这种情况下,我将添加以下内容:

# kics-scan disable=bd77554e-f138-40c5-91b2-2a09f878608e

我将在roles/efs/tasks/main.yml文件的最顶部进行此操作。如果这是一个固定的生产环境,那么我会添加一个角色来启动和维护 AWS Key Management Service(aws.amazon.com/kms/)作为部署的一部分。

结果总结

规则的最后部分概述了我们已覆盖的所有内容,对于没有应用任何修复的初始扫描,结果如下:

Results Summary:
HIGH: 4
MEDIUM: 0
LOW: 2
INFO: 5
TOTAL: 11

重新运行扫描

如之前所述,存在一个包含我们在上一节中讨论和实施的所有更新文件的分支;要切换到它,运行以下命令:

$ git switch chapter13-kics

然后,你可以使用以下命令重新运行扫描:

$ docker container run --rm --tty --volume ./:/ansible checkmarx/kics scan --path /ansible/

现在应该返回一个健康的结果:

Scanning with Keeping Infrastructure as Code Secure v1.7.13
Preparing Scan Assets: Done
Executing queries: [------------------------------] 100.00%
Results Summary:
HIGH: 0
MEDIUM: 0
LOW: 0
INFO: 0
TOTAL: 0

如你所见,现在没有报告任何问题。

输出文件

在我们结束这一章之前,还有一件事情我们应该快速讨论一下 KICS:它能够以各种文件格式输出报告的能力。

如果你重新对mainkics分支运行扫描,但使用以下命令,你会注意到在你的存储库文件夹中出现了一个名为results.html的文件:

$ docker container run --rm --tty --volume ./:/ansible checkmarx/kics scan --path /ansible/ --report-formats "html" --output-path /ansible/

正如你所看到的,我们正在传入两个新标志;第一个,--report-formats,告诉 KICS 输出一个html文件作为报告,第二个,--output-path,让 KICS 知道在哪里保存报告文件;在我们的情况下,由于我们在需要持久化的容器中运行 KICS,所以这个位置必须是容器内的一个位置,一旦容器运行结束,容器将自动删除以及任何写入的文件。

当对不包含任何修复的主分支运行命令时,我们应用了报告的标题,看起来如下所示:

图 13.1 – 查看显示问题的报告

图 13.1 – 查看显示问题的报告

然后,对 KICS 分支的重新运行扫描会更新为以下内容:

图 13.2 – 健康的清单

图 13.2 – 健康的清单

你还可以输出 PDF、JSON 和其他标准报告格式。如你所见,这比阅读我们在前一节中介绍的命令行报告的输出要容易理解些。

当我们进入第十五章使用 Ansible 与 GitHub Actions 和 Azure DevOps,我们将利用这些报告,并将结果作为我们的流水线运行的一部分发布。

摘要

在本章中,我们涵盖了两个可以添加到我们工作流程中的工具,并手动针对我们在第十一章开发的 playbook 运行了扫描,高可用云部署。正如本章所述,Checkov 对 Ansible 的支持相对较新,因此与 KICS 相比覆盖范围不同。然而,我相信你会同意这两个工具都表现不错。

重要提示

不过,有一个大问题;即使覆盖级别不同,这两个工具的结果也略有不同,因此你不应完全依赖它们来完全保护你的部署。把它们看作是值得信赖的同事,检查你的代码中显而易见的问题,而不是专注于安全的云平台架构师,他对你的工作负载有详细了解,严格规定你在安全地部署基础设施时应该采取的措施。

如前一节结尾所述,我们将在第十五章中重新讨论 KICS,使用 Ansible 与 GitHub Actions 和 Azure DevOps。在进入这一章之前,既然我们已经了解了如何审查和保护我们的 playbook 代码,现在可以看看如何通过快速应用安全最佳实践,来保护我们目标主机操作系统的工作负载,使用 Ansible 来实现这一点。

进一步阅读

有关工具及其维护者的更多信息,请参阅以下链接:

第十四章:使用 Ansible 加固你的服务器

使用像 Ansible 这样的编排和配置工具的一个优势是,它可以在多个主机上以可重复的任务生成并部署一组复杂的配置。在本章中,我们将介绍一个工具,它使用 Ansible 扫描你的主机,动态生成修复剧本,然后为你执行它。

我们还将介绍如何运行两个不同的安全工具,它们扫描我们在前几章中使用的 WordPress 安装。

本章涉及以下主题:

  • 扫描工具

  • 剧本

技术要求

在我们完成云端的探险后,我们将回到本地机器并使用 Multipass 启动一个 Ubuntu 22.04 虚拟机;由于我们将运行一个需要更多磁盘空间的工作负载,因此在启动虚拟机时,我们会调整虚拟机的规格,以增加磁盘空间和内存。

由于我们将在虚拟机上安装许多不同的软件,你的 Multipass 虚拟机需要能够从互联网下载包;大约需要下载 3GB 的各种包和配置文件。

你可以在github.com/PacktPublishing/Learn-Ansible-Second-Edition/tree/main/Chapter14的仓库中找到本章附带的完整剧本副本。

扫描工具

在我们深入剧本之前,让我们快速了解一下我们将要运行的三个工具,从其中一个做得最多的工具开始,OpenSCAP

OpenSCAP

首先,我们将查看 Red Hat 的一个工具,叫做 OpenSCAP。在继续之前,下一节将包含许多缩写。

那么,什么是 SCAP 呢?安全内容自动化协议SCAP)是一个开放标准,它包含多个组件,每个组件本身也是开放标准,构建一个框架,允许你自动评估和修复你的主机,以符合国家标准与技术研究院NIST)的特别 出版物 800-53

该出版物是对所有美国联邦 IT 系统应用的控制的目录,除了由国家安全局NSA)维护的系统。这些控制的实施旨在帮助执行 2002 年《联邦信息安全管理法》(FISMA)在美国联邦部门中的应用。

SCAP 由以下组件组成:

  • 资产识别AID)是用于资产识别的数据模型。

  • 资产报告格式ARF)是一个中立于供应商和技术的数据模型,用于在不同的报告应用程序和服务之间传输资产信息。

  • 通用配置枚举CCE)是一个标准数据库,提供常见软件的推荐配置。每个推荐都有一个唯一的标识符。写作时,数据库已经有十多年未更新。

  • 通用配置评分系统CCSS)是 CCE 的延续。它用于为各种软件和硬件配置生成评分,适用于所有类型的部署。

  • 通用平台枚举CPE)用于识别组织基础设施中的硬件资产、操作系统和软件。一旦识别出这些数据,就可以用于在其他数据库中搜索,进行资产的威胁评估。

  • 通用弱点枚举CWE)是一个通用语言,用于处理和讨论系统架构、设计和代码中可能导致漏洞的弱点原因。

  • 通用漏洞和曝光CVE)是一个公开承认的漏洞数据库。大多数系统管理员和 IT 专业人员都会在某个时刻接触到 CVE 数据库。每个漏洞都有一个唯一的 ID;例如,大多数人都知道 CVE-2014-0160,也就是Heartbleed。Heartbleed 漏洞是 OpenSSL(一个加密软件库)中的一个严重安全缺陷,攻击者可以利用该漏洞通过 OpenSSL 对传输层安全性TLS)/数据报传输层安全性DTLS)心跳扩展的错误,窃取受影响系统内存中的敏感信息,如密码和私钥。

  • 通用漏洞评分系统CVSS)是一种方法,用于捕捉漏洞的特征并生成标准化的数值评分,随后可用于描述漏洞的影响,例如低、中、高和严重。

  • 可扩展配置清单描述格式XCCDF)是一种用于描述安全清单的 XML 格式。它也可以用于配置和基准测试,并为 SCAP 的各个部分提供统一的语言。

  • 开放清单互动语言OCIL)是一个框架,用于向最终用户提问并以标准化方式处理响应的过程。

  • 开放漏洞和评估语言OVAL)定义为 XML,旨在标准化通过 NIST、MITRE 公司、美国计算机紧急响应小组US-CERT)以及美国国土安全部DHS)提供的所有工具和服务之间的安全内容传输。

  • 安全自动化数据的信任模型TMSAD)是一个 XML 文档,旨在定义一个可以应用于 SCAP 各组件间交换数据的共同信任模型。

正如你可以想象的那样,数千人的工作年限已经投入到生产 SCAP 及其组件,以构建它的基础。有些项目自 90 年代中期以来就以某种形式存在,因此它们已经相当成熟,并被认为是关于安全最佳实践的事实标准;然而,我相信你会觉得这一切听起来很复杂——毕竟,这些标准是由学者、安全专家和政府部门定义并维护的。

这就是 OpenSCAP 的作用所在。OpenSCAP 项目由 Red Hat 维护,并且通过 NIST 认证以支持 SCAP 标准,它允许你使用命令行客户端应用我们所讨论的所有最佳实践。

OpenSCAP 中的自动修复脚本仍在进行中,并且存在一些已知问题,我们将在本章的后半部分解决这些问题。因此,您的输出可能与本章所述有所不同。

OpenSCAP 像许多 Red Hat 项目一样,支持 Ansible,当前版本引入了自动生成 Ansible 演练手册的功能,用于修复在 OpenSCAP 扫描过程中发现的不合规项。

接下来我们将查看的两个工具将扫描我们的 WordPress 网站,首先是 WPScan。

WPScan

我们将运行的第二个工具叫做WPScan,我们将使用它来扫描我们的 WordPress 网站。WPScan 是一个命令行工具,可以对 WordPress 安装进行各种安全评估和漏洞测试。它可以检测常见的配置错误、过时的主题、弱密码和其他潜在风险。WPScan 易于安装——特别是我们将使用容器版本并通过 Docker 运行它,而这也是我们将要使用的第三个也是最后一个工具,OWASP ZAP。

OWASP ZAP

SQL 注入、跨站脚本攻击、身份验证漏洞和不安全的反序列化等 Web 漏洞可能威胁到我们 WordPress 网站的安全性和质量。为了帮助识别和优先处理这些漏洞,我们可以使用OWASP ZAP。这个工具是我们在本章中讨论的第三个也是最后一个工具,它生成报告、警报和图表,帮助我们可视化并处理发现的问题。此外,OWASP ZAP 易于使用且安装简单,是提升我们网站安全性和整体质量的宝贵资源。

演练手册

我们将把演练手册拆分为几个不同的角色,用于运行本章中将要运行的各种扫描工具——正如从site.yml文件中看到的,我们正在为包含任务的角色添加一些条件。文件的开始部分与我们之前运行的其他演练手册文件相似:

- name: "Scan our WordPress Ansible Playbook and stack"
  hosts: ansible_hosts
  gather_facts: true
  become: true
  become_method: "ansible.builtin.sudo"
  vars_files:
    - 'group_vars/common.yml'

如前所述,角色是这个演练手册与我们到目前为止运行的前几个演练手册的区别所在。

正如从以下源代码中可以看到的,我们在定义角色的同时也定义了标签:

  roles:
    - { role: 'common', tags: ['openscap','scan'] }
    - { role: 'docker', tags: ['docker','scan'] }

如您所见,我们正在使用 openscapscandocker 标签,后面跟着 wordpress,这些角色直接来自于 第五章部署 WordPress

    - { role: 'stack_install', tags: ['wordpress'] }
    - { role: 'stack_config', tags: ['wordpress'] }
    - { role: 'wordpress', tags: ['wordpress'] }

最后,我们有运行 scansopenscap 的角色:

    - { role: 'scan', tags: ['scan'] }
    - { role: 'openscap', tags: ['openscap'] }

那么,这意味着什么呢?嗯,在本章的后续内容中,当我们运行 playbook 时,我们只会运行特定的角色;例如,要运行 OpenSCAP,我们将使用以下命令:

$ ansible-playbook -i hosts site.yml --tags "openscap" --extra-vars "scap_options_remediation=true"
$ ansible-playbook -i hosts site.yml --tags "openscap"

运行第一个命令时,它将只运行 commonopenscap 角色,并运行修复 Ansible Playbook 和 bash 脚本,这两个都会在初次扫描期间自动生成——它还会下载结果副本、实施指南、playbook 副本和 bash 脚本副本。

两个命令中的第二个命令将重新扫描主机并再次下载结果副本。

一旦我们完成了 OpenSCAP 的运行,我们将重新部署主机并运行以下命令:

$ ansible-playbook -i hosts site.yml --tags "wordpress"

这,正如你猜到的,运行三个 wordpress 角色。然后,安装了 WordPress 后,我们可以运行以下命令:

$ ansible-playbook -i hosts site.yml --tags "scan"

这将执行 commondockerscan 角色。

我们还可以运行这些命令,只运行 scan 角色运行的两个扫描工具中的一个:

$ ansible-playbook -i hosts site.yml --tags "scan" --extra-vars "scan_types=zap"
$ ansible-playbook -i hosts site.yml --tags "scan" --extra-vars "scan_types=wpscan"

但我们现在有些超前了;让我们先处理前面的角色,然后再考虑运行 playbook。

公共角色

这个角色在 roles/common/tasks/main.yml 中包含一个单独的任务,其唯一任务是设置一个包含当前日期和时间的事实:

- name: "Set a fact for the date"
  ansible.builtin.set_fact:
    the_date: "{{ lookup('pipe', 'date +%Y-%m-%d-%H%M') }}"

你可能会想,“这看起来有点基础。” 然而,正如我们将多次在这个 playbook 的角色中使用 the_date 变量,我们只希望它在第一次生成时就好,因为它将用于创建文件和文件夹名称,这些名称会在后续任务中被调用。

如果我们使用 {{ lookup('pipe', 'date +%Y-%m-%d-%H%M') }} 来动态插入日期作为其他变量和任务的一部分,我们需要小心。因为 playbook 的某些部分可能需要几分钟才能完成运行。

例如,我们可能会在 playbook 中某个位置创建一个名为 myfile-2024-02-16-1300.yml 的文件。然而,如果我们动态设置日期和时间,而几项任务后它花了五分钟才到达那个任务,那么我们可能会引用一个名为 myfile-2024-02-16-1305.yml 的文件。这样会导致错误,因为该文件并不存在。因此,我们应该只在 playbook 运行时使用一次日期和时间查找。

Docker 角色

这个角色包含了在目标主机上安装和配置 Docker 所需的所有任务和变量,类似于 第四章部署 LAMP 堆栈,和 第五章部署 WordPress 中讨论的角色;这个角色使用 ansible.builtin.aptansible.builtin.apt_keyansible.builtin.apt_repository 模块来完成以下操作:

  1. 下载并安装 Docker 运行所需的前置条件。

  2. 添加官方 Docker 高级包装工具APT)仓库的GNU 隐私保护GPG)密钥。

  3. 配置官方 Docker APT 仓库。

  4. 安装 Docker 本身及 Docker 命令行工具。

  5. 确保 Docker 正在运行并设置为开机启动。

要查看此角色的完整任务和变量列表,请参见以下内容:

接下来,我们有安装 WordPress 的角色。

WordPress 角色

如你在本章 Playbook 部分开始时从site.yml文件中看到的那样,这里我们只是在重用我们在第五章中详细讨论的角色,部署 WordPress。如果你想回顾这些内容,可以访问github.com/PacktPublishing/Learn-Ansible-Second-Edition/tree/main/Chapter05/roles

扫描角色

如前所述,我们将使用 Docker 来运行 WPScan 和 OWASP ZAP;这使我们能够重用相同的任务。让我们来看一下roles/scan/tasks/main.yml;首先,我们需要拉取 Docker 镜像或镜像:

- name: "Pull the Docker image for the scanning tool"
  community.docker.docker_image:
    name: "{{ item.image }}"
    source: "{{ item.source }}"
  loop: "{{ scan }}"
  when: "item.name in scan_types"
  loop_control:
    label: "{{ item.name }}"

我们稍微做了一些调整,使用loop而不是with_items;这样可以更好地控制循环中的行为。在此任务中,我们使用label来显示当前正在处理的扫描工具。

你可能还会注意到我们使用了when条件;这使我们可以通过传递scan_types变量中的扫描名称来运行两个扫描任务中的一个或两个。当我们稍后查看变量时,你会看到默认情况下,我们传递了两个扫描工具的名称。

looploop_controlwhen模式将在该角色的所有任务中重复使用。我们有一个任务将在虚拟机上创建一个文件夹;我们将在运行时将此文件夹挂载到容器中,以便保存扫描输出的副本:

- name: "Create the folder which we will mount inside the container"
  ansible.builtin.file:
    path: "{{ item.log.remote_folder }}"
    state: "directory"
    mode: "0777"
  loop: "{{ scan }}"
  when: "item.name in scan_types"
  loop_control:
    label: "{{ item.name }}"

现在,容器镜像和文件夹已经创建,我们可以运行扫描:

- name: "Run the scan"
  community.docker.docker_container:
    detach: "{{ item.detach }}"
    auto_remove: "{{ item.auto_remove }}"
    name: "{{ item.name }}"
    volumes: "{{ item.log.remote_folder }}:{{ item.container_folder }}"
    image: "{{ item.image }}"
    command: "{{ item.command }}"
  register: docker_scan
  ignore_errors: true
  no_log: true
  loop: "{{ scan }}"
  when: "item.name in scan_types"
  loop_control:
    label: "{{ item.name }}"

如你所见,所有内容都作为变量传递给容器;这就是我们如何通过一个通用任务运行两个非常不同的工具,更多细节稍后会在查看变量时解释。

你还会注意到我们在任务末尾添加了一些选项;它们如下:

  • register:在这里,我们只是注册任务的输出——没有什么特别的。

  • ignore_errors:这告诉 Ansible 在检测到错误时继续运行;在我们的情况下,我们运行的容器会故意触发错误代码,因为它们被设计为停止并且在扫描未通过之前不继续执行任何后续任务。

  • no_log:这会抑制输出——因为我们在运行扫描时保存了输出,所以我们不需要在终端打印输出。

当我们注册输出时,接下来的任务是调试行。这与其他章节中的调试任务遵循相同的模式,因此我们将转向下载报告副本的任务:

- name: "Download the report"
  ansible.builtin.fetch:
    src: "{{ item.log.remote_folder }}{{ item.log.file }}"
    dest: "{{ item.log.local_folder }}"
    flat: true
    mode: "0644"
  loop: "{{ scan }}"
  when: "item.name in scan_types"
  loop_control:
    label: "{{ item.name }}"

这使用了 ansible.builtin.fetch 模块,并将 flat 选项设置为 true。这个选项会复制文件而不是完整的目录路径。最后一个任务移除了容器,这意味着当我们下次运行扫描时,它将从头开始并启动一个新的容器,而不是重用我们刚刚使用完的那个:

- name: "Remove the scan container"
  community.docker.docker_container:
    name: "{{ item.name }}"
    state: "absent"
  loop: "{{ scan }}"
  when: "item.name in scan_types"
  loop_control:
    label: "{{ item.name }}"

现在我们知道了任务的样子,让我们看看变量,这些变量可以在 roles/scan/defaults/main.yml 中找到。第一个变量设置我们要运行的扫描,正如前面提到的,这里给出了两个扫描的名称:

scan_types:
  - "{{ common_scan_settings.dict.wpscan }}"
  - "{{ common_scan_settings.dict.zap }}"

接下来,在 roles/scan/defaults/main.yml 中,我们有一个可能在两个扫描工具中都常用的变量块:

common_scan_settings:
  detach: false
  auto_remove: false
  source: "pull"
  local_folder: "output/"
  report_name: "{{ the_date }}-results-"
  dict:
    wpscan: "wpscan"
    zap: "zap"

最后,我们有主要的 scan 变量,这是我们一直在循环使用的那个,它以 WPScan 开头:

scan:
  - name: "{{ common_scan_settings.dict.wpscan }}"
    image: "wpscanteam/wpscan:latest"
    source: "{{ common_scan_settings.source }}"
    detach: "{{ common_scan_settings.detach }}"
    auto_remove: "{{ common_scan_settings.auto_remove }}"
    container_folder: "/tmp/{{ common_scan_settings.dict.wpscan }}/"
    command: "--url http://{{ ansible_host }} --enumerate u --plugins-detection mixed --format cli-no-color --output /tmp/{{ common_scan_settings.dict.wpscan }}/{{ common_scan_settings.report_name }}{{ common_scan_settings.dict.wpscan }}.txt"
    log:
      remote_folder: "/tmp/{{ common_scan_settings.dict.wpscan }}/"
      local_folder: "{{ common_scan_settings.local_folder }}"
      file: "{{ common_scan_settings.report_name }}{{ common_scan_settings.dict.wpscan }}.txt"

接下来的代码块是 OSWAP ZAP 的部分:

  - name: "{{ common_scan_settings.dict.zap}}"
    image: "ghcr.io/zaproxy/zaproxy:stable"
    source: "{{ common_scan_settings.source }}"
    detach: "{{ common_scan_settings.detach }}"
    auto_remove: "{{ common_scan_settings.auto_remove }}"
    container_folder: "/zap/wrk/"
    command: "zap-baseline.py -t http://{{ ansible_host }} -g gen.conf -r {{ common_scan_settings.report_name }}{{ common_scan_settings.dict.zap }}.html"
    log:
      remote_folder: "/tmp/{{ common_scan_settings.dict.zap }}/"
      local_folder: "{{ common_scan_settings.local_folder }}"
      file: "{{ common_scan_settings.report_name }}{{ common_scan_settings.dict.zap }}.html"

正如你所看到的,我们传入不同的容器镜像和运行扫描的命令,同时使用相同的变量。因此,我们可以保持角色中的任务完全中立,意味着我们无需考虑任何与运行的工具相关的自定义内容。

这就结束了扫描角色,剩下的正如你从章节开头工具说明的长度中已经猜到的那样,是 Playbook 中最复杂的角色:OpenSCAP。

OpenSCAP 角色

编写 Playbook 时,了解你正在自动化的工具如何工作至关重要;鉴于 OpenSCAP 有点复杂,让我们回顾一下手动运行扫描并使用自动生成的 Ansible Playbook 和 shell 脚本来修复它发现的问题所需的步骤。

注意

虽然接下来是运行 OpenSCAP 的命令,但你无需跟着执行;这些命令是为了说明我们在 Playbook 角色中需要遵循的过程。

首先,我们需要下载并安装 OpenSCAP 本身,以及我们还需要的一些工具:

$ sudo apt-get install unzip curl libopenscap8

接下来,我们需要下载实际内容——这些定义涵盖了多个不同的操作系统和不同级别的合规性。该内容的 GitHub 仓库可以在 github.com/ComplianceAsCode/content 找到,写作时当前版本是 0.1.71。

获取 zip 文件的发布 URL,该文件包含我们需要的文件,从发布页面下载并在主机上解压:

$ wget https://github.com/ComplianceAsCode/content/releases/download/v0.1.71/scap-security-guide-0.1.71.zip
unzip scap-security-guide-0.1.71.zip

现在我们已经安装了 OpenSCAP 和定义文件,我们可以获取一些关于我们 Ubuntu 22.04 操作系统的可用信息:

$ sudo oscap info --fetch-remote-resources scap-security-guide-0.1.71/ssg-ubuntu2204-ds.xml

这将为我们提供我们想要使用的配置文件的名称;在我们的例子中,它是 xccdf_org.ssgproject.content_profile_cis_level1_server。一旦我们有了这个,我们就可以运行扫描本身:

$ oscap xccdf eval --profile xccdf_org.ssgproject.content_profile_cis_level1_server  --results-arf result.xml --report report.html scap-security-guide-0.1.71/ssg-ubuntu2204-ds.xml

这将生成两个输出文件:一个 HTML 格式的报告,包含需要修复的所有内容,易于阅读;另一个是 XML 文件,包含相同的信息,以便 OpenSCAP 可以读取。

然后我们可以获取 XML 文件,并通过运行以下命令生成一个更详细的指南,告诉我们如何解决扫描中发现的问题:

$ sudo oscap xccdf generate guide  --profile xccdf_org.ssgproject.content_profile_cis_level1_server scap-security-guide-0.1.71/ssg-ubuntu2204-ds.xml  > guide.html

然而,由于本书是关于 Ansible 的,最好有一个 Playbook 来修复尽可能多的问题,运行以下命令将为我们提供这个功能:

$ sudo oscap xccdf generate fix --fetch-remote-resources --fix-type ansible --result-id "" result.xml > playbook.yml

最后,并非所有问题都能通过 Playbook 方法解决,因此拥有一个 Bash 脚本来修复任何无法通过运行 Playbook 解决的问题也是一个好主意,因为这将减少我们需要手动处理的工作量:

$ sudo oscap xccdf generate fix --fetch-remote-resources --fix-type bash --result-id "" result.xml > bash.sh

现在我们有了 Playbook 和 Bash 脚本;我们需要运行它们,将 Playbook 复制到本地机器,并使用以下命令运行:

$ ansible-playbook -i hosts --become -become-method=sudo output/ansiblevm-playbook.yml

然后我们回到虚拟机,使用以下命令运行 Bash 脚本:

$ sudo bash bash.sh

你会看到很多输出,但如果一切按计划进行,当你重新运行扫描时,你应该会看到许多问题被报告。

注意

仓库中的代码包含了一些变量和任务,这些任务属于我们这里不涉及的功能,因为我们从 GitHub 下载的内容可能会占用你硬盘上的大量空间。这些任务的目的是删除不需要的文件。

现在我们已经有了需要自动化的步骤的概念,让我们直接开始吧。

首先,让我们看一下变量,这些变量可以在 roles/openscap/default/main.yml 中找到,并且我们将在任务中使用这些变量。

从以下选项开始,如果设置为 true,将执行修复 Playbook 和 Bash 脚本:

scap_options_remediation: false

接下来,我们有运行 OpenSCAP 所需的包和 OpenSCAP 本身:

scap_packages:
  - "unzip"
  - "curl"
  - "libopenscap8"

然后,我们有了从 GitHub 下载内容的信息;请注意,我们传递的是 API URL,而不是直接下载链接(稍后在本章会详细说明为什么这样做):

openscap_download:
  openscap_github_release_api_url: "https://api.github.com/repos/ComplianceAsCode/content/releases/latest"
  dest: "/tmp/scap-security-guide"

现在我们有了一长串文件名和我们需要使用的配置文件的详细信息:

openscap_scan:
  ssg_file_name: "{{openscap_download.dest}}/ssg-{{ ansible_facts.distribution | lower }}{{ ansible_facts.distribution_version | replace('.','') }}-ds.xml"
  profile_search: "cis_level1_server"
  output_dir: "/tmp/"
  output_file_xml: "{{ inventory_hostname }}-result.xml"
  output_file_html: "{{ inventory_hostname }}-report.html"
  output_file_guide: "{{ inventory_hostname }}-guide.html"
  output_file_playbook: "{{ inventory_hostname }}-playbook.yml"
  output_file_bash: "{{ inventory_hostname }}-bash.sh"
  local_output_dir: "output/{{ the_date }}-openscap-results"

请注意,我们尽量不硬编码任何值;例如,在引用操作系统时,我们使用 {{ ansible_facts.distribution | lower }}{{ ansible_facts.distribution_version | replace('.','') }},在我们的例子中,它给我们的是 ubuntu2204。这意味着如果 OpenSCAP 支持它,我们可以在其他 Ubuntu 发行版上运行我们的 Playbook,而无需做任何更改。

使用这些变量的任务可以在 roles/openscap/tasks/main.yml 中找到;我们从两个任务开始安装 OpenSCAP,第一个任务确保 APT 缓存和我们的操作系统都是最新的:

- name: "Update apt cache and upgrade packages"
  ansible.builtin.apt:
    name: "*"
    state: "latest"
    update_cache: "yes"

安装 OpenSCAP 本身及我们需要的其他软件包的任务:

- name: "Install common packages"
  ansible.builtin.apt:
    state: "present"
    pkg: "{{ scap_packages }}"

现在,我们创建一个目录,用于存储我们从 GitHub 下载的 OpenSCAP 内容:

- name: "Create the directory to store the scap security guide content"
  ansible.builtin.file:
    path: "{{ openscap_download.dest }}"
    state: "directory"
    mode: "0755"

在准备好目标文件夹后,我们现在可以下载内容并解压:

- name: "Download the latest scap security guide content"
  ansible.builtin.unarchive:
    src: "{{ lookup('url', '{{ openscap_download.openscap_github_release_api_url }}', split_lines=false) | from_json | json_query('assets[?content_type==`application/zip`].browser_download_url') | last }}"
    dest: "{{ openscap_download.dest }}"
    creates: "{{ openscap_download.dest }}/README.md"
    list_files: true
    remote_src: true
  register: scap_download_result

从表面上看,虽然它看起来有点复杂,但实际上有很多内容在运行;让我们拆解一下如何获取值以填充 src 键。

我们使用 Ansible 的 lookup 插件从 GitHub API 获取并处理数据,从而获取 OpenSCAP 内容 GitHub 仓库的最新发布信息:

  • {{ lookup('url', '{{ openscap_download.openscap_github_release_api_url }}', split_lines=false) }}: 这里使用了 lookup 插件与 url 查找类型,它从由 openscap_download.openscap_github_release_api_url 变量指定的给定 URL 获取数据,该变量指向 GitHub 仓库最新发布的 API 端点(api.github.com/repos/ComplianceAsCode/content/releases/latest)。split_lines=false 参数确保获取的内容不会按行拆分,保持其 JSON 结构。

  • | from_json: 这部分代码将 lookup 插件的输出(预期为 JSON 字符串)转换为 Ansible 数据结构(例如字典或列表),以便进一步处理。

  • | json_query('assets[?content_type==`application/zip`].browser_download_url'): 这使用 json_query 过滤器和 JMESPath 表达式来查询转换后的 JSON 数据。'assets[?content_type==`application/zip`].browser_download_url' 查询会查找 assets 数组中 content_typeapplication/zip 的项,然后提取 browser_download_url。这个 URL 通常用于直接从浏览器下载资产。

  • | last: 最后,使用 last 过滤器从 json_query 过滤器返回的 URL 列表中获取最后一个 URL。这样做是因为可能有多个 application/zip 内容类型的资产,但我们只关心最新或最后一个列出的。

这意味着我们不需要在 Playbook 中硬编码最新发布的版本号,这一点很有帮助,因为 OpenSCAP content 仓库至少每隔几周就会更新一次。

我们传递给 ansible.builtin.unarchive 模块的其他选项如下:

  • dest: 指定要将归档文件解压到的目标机器上的目标目录。

  • creates: 该参数作为条件检查,用于在特定文件存在时防止重新下载和解压归档文件。

  • list_files: 当设置为 true 时,此选项列出归档文件中的所有文件;我们将使用此列表将文件复制到目标文件夹。

  • remote_src: 设置为 true 表示源归档文件位于远程服务器上,而不是在运行 Ansible 的控制机器上;这在直接从 URL 下载内容时是必需的。

接下来的两个任务将文件移动到 openscap_download.dest 的根目录,因为它们会被解压到一个包含版本号的文件夹中——我们不希望使用该文件夹,因为它在不同的执行之间可能会发生变化:

- name: "Move scap security guide content to the correct location"
  ansible.builtin.shell: "mv {{ openscap_download.dest }}/{{ scap_download_result.files[0] }}/* {{ openscap_download.dest }}"
  when: scap_download_result.changed
- name: "Remove the downloaded scap security guide content"
  ansible.builtin.file:
    path: "{{ openscap_download.dest }}/{{ scap_download_result.files[0] }}"
    state: "absent"
  when: scap_download_result.changed

请注意,我们只会在下载文件的任务发生变化时才运行这些任务。

在我们可以运行 OpenSCAP 扫描之前,我们需要的最后一项信息是使用哪个配置文件。为了获取这个信息,我们需要运行命令打印出适用于我们操作系统的配置文件信息:

- name: "Get information of the SCAP profiles available for the target system"
  ansible.builtin.command: "oscap info –profiles –fetch-remote-resources {{ openscap_scan.ssg_file_name }}"
  register: scap_info

现在我们已经有了注册为 scap_info 的可用配置文件信息,我们可以根据 openscap_scan.profile_search 的内容过滤此列表并设置一个事实:

- name: "Extract profile name based on our selection criteria"
  ansible.builtin.set_fact:
    profile_name: "{{ scap_info.stdout_lines | select('search', openscap_scan.profile_search) | map('regex_replace', '^(.*?):.*$', '\\1') | first }}"

设置好事实后,我们可以运行扫描任务:

- name: "Run OpenSCAP scan"
  ansible.builtin.command: "oscap xccdf eval --profile {{ profile_name }} --results-arf {{ openscap_scan.output_dir }}{{ openscap_scan.output_file_xml }} --report {{ openscap_scan.output_dir }}{{ openscap_scan.output_file_html }} {{ openscap_scan.ssg_file_name }}"
  ignore_errors: true
  no_log: true
  register: scap_scan

如你所见,我们通过使用 no_log: true 来抑制输出;这是因为在此阶段我们不需要查看输出,并且可以忽略错误,就像我们在之前的角色中运行 WPScan 和 OSWAP ZAP 时一样。

现在我们已经得到了扫描的输出,我们需要在我们的 Ansible 主机上创建一个文件夹,将输出文件复制到以下位置:

- name: "Ensure the local output directory exists"
  ansible.builtin.file:
    path: "{{ openscap_scan.local_output_dir }}"
    state: directory
    mode: "0755"
  delegate_to: "localhost"
  become: false

如你所见,我们正在使用 delegate_to 来确保 Ansible 在 localhost 上运行任务,并且我们告诉它不要以特权用户身份运行。

现在我们可以 fetch output.xmlreport.html 文件:

- name: "Copy the SCAP report and results file to local machine"
  ansible.builtin.fetch:
    src: "{{ item }}"
    dest: "{{ openscap_scan.local_output_dir }}/"
    flat: true
    mode: "0644"
  with_items:
    - "{{ openscap_scan.output_dir }}{{ openscap_scan.output_file_xml }}"
    - "{{ openscap_scan.output_dir }}{{ openscap_scan.output_file_html }}"

接下来,我们需要生成指南和修复文件:

- name: "generate SCAP guide"
  ansible.builtin.command: "oscap xccdf generate guide --profile {{ profile_name }} {{ openscap_scan.ssg_file_name }}"
  ignore_errors: true
  register: scap_guide

你可能已经注意到,我们在这里没有保存文件;我们只是注册了输出。这是因为指南的所有内容都在命令执行时输出到屏幕上,所以我们与其将输出直接写入虚拟机上的文件再复制,不如直接捕获输出,然后在本地机器上创建一个包含这些内容的文件,基本上是将内容从远程主机复制粘贴到本地:

- name: "Copy SCAP guide to local machine"
  ansible.builtin.copy:
    content: "{{ scap_guide.stdout }}"
    dest: "{{ openscap_scan.local_output_dir }}/{{ openscap_scan.output_file_guide }}"
    mode: "0644"
  when: scap_guide is defined
  delegate_to: "localhost"
  become: false

这对于修复的 Ansible Playbook 也是如此:

- name: "Generate SCAP fix playbook"
  ansible.builtin.command: "oscap xccdf generate fix --fetch-remote-resources --fix-type ansible --result-id '' {{ openscap_scan.output_dir }}{{ openscap_scan.output_file_xml }}"
  ignore_errors: true
  register: scap_playbook
- name: "Copy SCAP playbook to local machine"
  ansible.builtin.copy:
    content: "{{ scap_playbook.stdout }}"
    dest: "{{ openscap_scan.local_output_dir }}/{{ openscap_scan.output_file_playbook }}"
    mode: "0644"
  when: scap_playbook is defined
  delegate_to: "localhost"
  become: false

接着,对于修复的 Bash 脚本:

- name: "Generate SCAP fix bash script"
  ansible.builtin.command: "oscap xccdf generate fix --fetch-remote-resources --fix-type bash --result-id '' {{ openscap_scan.output_dir }}{{ openscap_scan.output_file_xml }}"
  ignore_errors: true
  register: scap_bash_script
- name: "Copy SCAP bash script to local machine"
  ansible.builtin.copy:
    content: "{{ scap_bash_script.stdout }}"
    dest: "{{ openscap_scan.local_output_dir }}/{{ openscap_scan.output_file_bash }}"
    mode: "0644"
  when: scap_bash_script is defined
  delegate_to: "localhost"
  become: false

角色中的其余任务处理修复工作,从 playbook 开始:

- name: "Run the remediation playbook"
  ansible.builtin.command: "ansible-playbook -i {{ inventory_file }} --become --become-method sudo {{ openscap_scan.local_output_dir }}/{{ openscap_scan.output_file_playbook }}"
  when: scap_options_remediation
  delegate_to: "localhost"
  become: false
  register: remediation_playbook

然后,由于我们从未在目标虚拟机上保留过 bash 脚本的副本,我们需要将它复制回去:

- name: "Copy the remediation bash script to the target machine"
  ansible.builtin.copy:
    src: "{{ openscap_scan.local_output_dir }}/{{ openscap_scan.output_file_bash }}"
    dest: "{{ openscap_scan.output_dir }}"
    mode: "0755"
  when: scap_options_remediation

文件复制完成后,我们可以运行脚本:

- name: "Run the remediation bash script"
  ansible.builtin.command: "bash {{ openscap_scan.output_dir }}{{ openscap_scan.output_file_bash }}"
  when: scap_options_remediation
  register: remediation_bash_script

完成了该任务后,角色就完成了,现在我们已经准备好运行我们的 playbook。

运行 playbook

第一章《安装与运行 Ansible》中,我们介绍了 Multipass 的安装和使用;从那时起,我们一直使用相同的命令启动本地虚拟机。在本章中,由于需要更多的磁盘空间和 RAM,我们将在启动虚拟机时添加一些额外的选项:

$ multipass launch -n ansiblevm --cloud-init cloud-init.yaml --disk 10G --memory 4G

一旦虚拟机启动,你可以通过运行以下命令获取主机的 IP 地址:

$ multipass info ansiblevm

一旦你获得了 IP 地址,创建一个 hosts.example 的副本,命名为 hosts 并更新 IP 地址,正如我们在之前的章节中所做的那样。将 hosts 库文件准备好后,我们可以开始运行剧本,首先进行 OpenSCAP 扫描:

$ ansible-playbook -i hosts site.yml --tags "openscap" --extra-vars "scap_options_remediation=true"

如你所见,我们使用了 openscap 标签,并将 scap_options_remediation 变量设置为 true;如果你还记得,默认情况下该变量为 false,意味着修复任务将在此剧本运行时执行。

完成后,你将在本地机器的输出文件夹中找到几个文件;如果你没有跟着操作,你可以在 github.com/PacktPublishing/Learn-Ansible-Second-Edition/tree/main/Chapter14/examples/01-scap_options_remediation_true 找到输出的副本。

从以下屏幕可以看到,在初次运行时,我们有 98 个失败的结果:

图 14.1 – 初始结果

图 14.1 – 初始结果

由于我们在剧本运行过程中执行了修复任务,所以我们知道分数应该已经提高,因此让我们重新运行剧本——这次完全跳过修复任务:

$ ansible-playbook -i hosts site.yml --tags "openscap"

完成后,你应该会得到一个结果文件夹;同样,你可以在 github.com/PacktPublishing/Learn-Ansible-Second-Edition/tree/main/Chapter14/examples/02-scap_options_remediation_false 查看结果:

图 14.2 – 更新后的结果

图 14.2 – 更新后的结果

如你所见,这显著提高了得分,这次我们只有六个失败。

接下来,我们需要安装 WordPress;让我们从头开始。为了重新开始,运行以下命令终止虚拟机,并用新虚拟机替换它:

$ multipass stop ansiblevm
$ multipass delete --purge ansiblevm
$ multipass launch -n ansiblevm --cloud-init cloud-init.yaml --disk 10G --memory 4G
$ multipass info ansiblevm

更新 hosts 文件中的新 IP 地址,然后运行以下命令安装 WordPress:

$ ansible-playbook -i hosts site.yml --tags "wordpress"

安装 WordPress 后,你可以使用以下命令运行 WPScan 和 OSWAP ZAP 扫描:

$ ansible-playbook -i hosts site.yml --tags "scan"

完成后,你将在输出文件夹中找到扫描结果;你可以在github.com/PacktPublishing/Learn-Ansible-Second-Edition/tree/main/Chapter14/examples找到结果的示例。该文件夹还包含了本章到目前为止每次运行 playbook 的完整输出。

同样,如本章开始时提到的,你可以使用以下命令独立运行每一个扫描:

$ ansible-playbook -i hosts site.yml --tags "scan" --extra-vars "scan_types=zap"
$ ansible-playbook -i hosts site.yml --tags "scan" --extra-vars "scan_types=wpscan"

一旦你完成了运行 playbook 的操作,可以通过运行以下命令来删除虚拟机:

$ multipass stop ansiblevm
$ multipass delete --purge ansiblevm

虚拟机已清理完毕,这也就结束了我们使用 Ansible 扫描和加固服务器的过程。

在我们进入下一章之前,我建议你查看修复 playbook,它是在我们首次运行 OpenSCAP 时生成的。

它可以在github.com/PacktPublishing/Learn-Ansible-Second-Edition/blob/main/Chapter14/examples/01-scap_options_remediation_true/ansiblevm-playbook.yml找到,正如你所看到的,它包含了超过 4,600 行代码!

总结

在这一章中,我们生成了一个 playbook,用来修复在扫描中发现的任何 CIS 1 级不合规错误。它不仅很酷,而且如果你想象一下自己正在管理几十台需要合规并且都需要完整审计历史的服务器,它也非常方便。

现在你已经有了一个 playbook 的基础,可以用它每天针对这些主机进行操作、审计它们,并将结果保存在主机本身之外。此外,根据你的配置,如果需要,你也可以自动解决扫描中发现的任何不合规问题。

我们还对我们的 WordPress 安装进行了扫描,并将结果存储在主机本身之外——尽管 WPScan 和 OWASP ZAP 的扫描没有包括任何修复措施,但你可以快速查看结果,并在部署时更新你的 WordPress 部署脚本以修复所发现的问题。

到目前为止,我们一直在从本地机器运行 Ansible Playbook;在下一章,我们将从本地机器运行 Ansible 代码转移到云端,并了解如何使用 Azure DevOps Pipelines 和 GitHub Actions 来执行我们的 playbook。

第十五章:使用 Ansible 与 GitHub Actions 和 Azure DevOps

本章将开始在云中运行 Ansible,而不是像以前那样在本地机器上运行。

首先,本章将介绍我在日常工作中经常使用的两个服务:

  • 运行 GitHub Actions

  • 在 Azure DevOps 中运行流水线

在继续之前,我们将检查一些旨在从中央位置执行 Ansible 的工具,参见 第十六章介绍 Ansible AWX 和 Red Hat Ansible 自动化平台

我们将要了解的这两项服务都没有所谓的原生 Ansible 支持;然而,它们都提供可以使用 YAML 配置的临时计算资源,而你可以将这些配置文件与 Playbook 代码一起部署。

本章将介绍一个更复杂的 Playbook,涉及 GitHub ActionsAzure DevOps。我们还将讨论在远离本地机器运行 Ansible 时需要考虑的一些事项。

所以,既然不再讨论这些内容,让我们直接深入,看看 GitHub Actions。

技术要求

如果你正在跟随我们将要逐步讲解的示例代码,那么你将需要一个 GitHub 和 Azure DevOps 账号,同时还需要一个 Azure 账号,因为我们将在本章中启动一个运行在 Azure 上的 WordPress 实例。

你可以在本书的 GitHub 仓库中找到本章所附的完整 Playbook、GitHub Action 配置和 Azure DevOps Pipeline 代码,地址是 github.com/PacktPublishing/Learn-Ansible-Second-Edition/tree/main/Chapter15

GitHub Actions

GitHub Actions 是一个全面的 持续集成CI)和 持续交付CD)平台,集成于 GitHub。它使你能够自动化构建、测试和部署流水线,同时托管你的代码和 GitHub 强大的代码管理工具。使用 GitHub Actions,你可以定义自定义工作流,自动构建和测试每一个对你仓库发出的 Pull Request,或将已合并的 Pull Request 部署到生产环境。

GitHub Actions 不仅仅提供 DevOps 功能,它与 GitHub 紧密集成。这使得你可以根据其他仓库事件来运行工作流。例如,你可以设置一个工作流,当你的仓库中新建一个 Issue 时,自动添加相关标签。

使用 GitHub Actions,你可以完全掌控。你可以利用 GitHub 提供的 Linux、Windows 和 macOS 虚拟机来运行你的工作流。你还可以完全控制,操作自己托管的 Runner,在你自己的数据中心或云基础设施中运行。

我们将创建一个 GitHub Action 工作流,利用 GitHub 托管的 Linux 代理。

准备工作

在开始编写 GitHub Action 工作流代码之前,我们需要配置一些东西:

  1. 创建一个 GitHub 仓库来托管我们的代码和工作流。

  2. 生成一个 SSH 密钥对;这将在工作流运行时用于从 GitHub 托管的计算资源访问我们托管在 Azure 上的虚拟机实例。

  3. 配置一些仓库密钥,这些密钥将在我们的工作流中使用;它们将存储诸如 Azure 凭证和我们创建的 SSH 密钥对等信息。

  4. 将文件从github.com/PacktPublishing/Learn-Ansible-Second-Edition/tree/main/Chapter15复制到你的新仓库,并运行工作流。

让我们更详细地看一下这些步骤。

创建一个仓库

让我们从在 GitHub 上创建一个仓库开始,我们将使用它来托管我们的代码和工作流。

首先,你需要登录 GitHub。登录后,进入Repositories(仓库),然后点击New(新建)按钮;这会带你进入创建新仓库页面,在此页面上你需要更新以下内容:

  • 所有者:在这里,你需要为仓库选择一个所有者。通常情况下,这是你的 GitHub 用户;然而,如果你属于某个组织,你可能有选项将仓库创建在该组织下。如果选择这样做,请确保你有权限这样做,因为我们将启动临时计算资源,而这可能不被组织管理员允许。

  • 仓库名称:我建议使用一些描述性的名称,例如Learn-Ansible-Second-Edition-Chapter15

  • 描述:虽然这不是必需的,但最好加上描述;例如,我们可以添加跟随 第十五章 学习学习 Ansible》一书的内容。

  • 公开私有:我建议将仓库的可见性设置为私有

你可以保持其他选项不变,然后点击表单底部的创建仓库按钮。一旦仓库创建成功,你将看到如下页面:

图 15.1 – 我们的新仓库

图 15.1 – 我们的新仓库

接下来,我们进入下一步。

生成 SSH 密钥对和 Azure 服务主体

在将密钥添加到我们新创建的仓库之前,我们需要生成一个 SSH 密钥对和一个 Azure 服务主体。

信息

如果你在 Windows 机器上进行操作,记得在 Windows 子系统 Linux(WSL)中运行这些命令。

为此,打开终端并运行以下命令:

$ ssh-keygen -t rsa -C "learnansible" -f ./id_rsa

当提示输入密码时,直接按Enter键;我们不需要设置密码。这将生成两个文件:一个名为id_rsa,它包含我们密钥的私有部分——请保持其私密性——另一个名为id_rsa.pub。顾名思义,它包含我们 SSH 密钥的公有部分。

接下来,我们需要生成一个 Azure 服务主体并授予权限给我们的 Azure 订阅。

第七章Ansible Windows 模块,和第九章迁移到云中,我们使用 Azure 命令行工具登录 Azure,并使用我们的 Azure 凭据。然而,当通过 GitHub Actions 等服务与 Azure 交互时,我们不想使用我们的凭据,因为它们将通过多因素身份验证进行锁定,而且你也不想泄露自己的凭据。

为了解决这个问题,我们可以创建一个服务主体,并授予它对 Azure 订阅的权限,以便它可以从 GitHub Action 启动资源。

要创建服务主体,你需要通过运行以下命令使用 Azure CLI 登录 Azure:

$ az login

如果你已经登录,请运行以下命令:

$ az account list

两个命令将返回你的账户可以访问的订阅 ID 列表。请记下该 ID,我们稍后会用到它。

这是你可以预期看到的输出示例;这是 Azure CLI 发出的 API 请求返回的 JSON:

{
  "environmentName": "AzureCloud",
  "id": "e80d5ad9-e2c5-4ade-a866-bcfbae2b8aea",
  "isDefault": true,
  "name": "My Subscription",
  "state": "Enabled",
  "tenantId": "c5df827f-a940-4d7c-b313-426cb3c6b1fe",
  "user": {
    "name": "account@russ.foo",
    "type": "user"
  }
}

我们需要的信息标记为 id,与我们希望授予服务主体访问权限的订阅相关。使用前面的示例,我需要运行的命令如下:

$ az ad sp create-for-rbac –name sp-learn-ansible –role contributor –scopes /subscriptions/e80d5ad9-e2c5-4ade-a866-bcfbae2b8aea

当你运行此命令时,请将范围中的订阅 ID 替换为你自己的。

你得到的输出将类似于这样;请记下它,因为你将无法再次检索密码:

Creating 'contributor' role assignment under scope '/subscriptions/e80d5ad9-e2c5-4ade-a866-bcfbae2b8aea'
The output includes credentials that you must protect. Be sure that 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": "2616e3df-826d-4d9b-9152-3de141465a69",
  "displayName": "sp-learn-ansible",
  "password": "Y4j8Q~gVO*NoTaREalPa55w0rdpP-pdaw",
  "tenant": "c5df827f-a940-4d7c-b313-426cb3c6b1fe"
}

此外,正如我相信你已经猜到的那样,前面示例中的所有信息都不是有效数据,因此请在下一部分使用你自己的值。

GitHub 个人访问令牌

还有一组凭据需要生成;因为我们的 GitHub 仓库设置为私有,我们需要能够进行身份验证,以便在工作流运行期间检查代码并将日志写回仓库。为此,我们需要生成一个个人访问令牌。

GitHub 个人访问令牌是一种安全的、可撤销的、可定制的凭据,允许你使用 GitHub 进行身份验证,并访问其 API 或命令行工具,而无需使用主账户密码。

由于 GitHub 正在从经典令牌转向细粒度令牌(本文撰写时),所以在此不记录过程,最新的文档副本可以在docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens找到。

对于我们的目的,你需要为你的令牌命名,只选择你的仓库,并授予它以下访问权限:

  • 内容:只读

  • 元数据:只读;此选项将在权限设置后自动选中

一旦你获得了令牌,请将其保存在安全的地方;它将不会再显示。

向仓库中添加机密

通过选择 设置 | 机密和变量 | Actions 返回 GitHub 仓库。点击 新建仓库机密 按钮,为以下表格中列出的每个机密创建新机密。请确保按照以下命名约定命名每个机密,因为我们的工作流代码通过名称引用这些机密:

机密名称 机密内容
ARM_CLIENT_ID 这是您创建服务主体时的 appId 值。在这个示例中,这将是 2616e3df-826d-4d9b-9152-3de141465a69
ARM_CLIENT_SECRET 这是您创建服务主体时给定的 password 值。在这个示例中,这将是 Y4j8Q~gVO*NoTaREalPa55w0rdpP-pdaw
ARM_SUBSCRIPTION_ID 这是您的 Azure 订阅 ID;使用您授予服务主体访问权限的订阅 ID。在这个示例中,这将是 e80d5ad9-e2c5-4ade-a866-bcfbae2b8aea
ARM_TENANT_ID 这是您在创建服务主体时列出的 tenant 值的 ID。在这个示例中,这将是 c5df827f-a940-4d7c-b313-426cb3c6b1fe
SSH_PRIVATE_KEY 打开 id_rsa 文件并复制粘贴其内容。
SSH_PUBLIC_KEY 打开 id_rsa.pub 文件并复制粘贴其内容。
GH_PAT 这应该包含您的 GitHub 个人访问令牌。

表 15.1 – GitHub Actions 所需的信息

一旦它们都已添加,您的 Actions 机密和变量 页面应该如下所示:

图 15.2 – 所有仓库机密已添加

图 15.2 – 所有仓库机密已添加

现在我们已经配置了 GitHub Action 的所有基本设置,让我们来看一下工作流本身。

理解 GitHub Action 工作流

工作流文件位于 .github/workflows/action.yml 文件中,该文件包含了 YAML 代码,定义了工作流执行期间将执行的作业、步骤和任务。对于我们的情况,工作流将执行以下两个作业,每个作业由多个步骤组成:

  • 扫描 Ansible Playbook:

    1. 检出代码。

    2. 创建一个文件夹以存储扫描结果。

    3. 对检出的代码运行 KICS 扫描。

    4. 上传结果副本到 GitHub。

现在,如果 KICS 检测到我们的 playbook 有问题,它将报告错误,并且工作流将在这里停止——如果 KICS 扫描看起来一切正常,工作流将继续运行以下任务:

  • 安装并运行 Ansible Playbook:
  1. 检查是否有缓存版本的 Ansible 模块和 Python 包可用。

  2. 如果没有缓存,下载并安装 Ansible Azure 模块和支持的 Python 包。

  3. 检出代码。

  4. 使用 Azure CLI 和我们创建的服务主体登录 Azure。

  5. 设置 SSH 密钥。

  6. 运行 Ansible Playbook,并记录 Playbook 输出,以便我们可以将副本存储在工作流日志中,与扫描结果一起。

  7. 上传 Playbook 执行摘要。

现在我们知道工作流将执行什么,让我们深入代码部分。我们从一些基本配置开始:

  1. 第一行禁用了 KICS 检查——尽管工作流不是我们 Playbook 的一部分,它存储在仓库中,并将作为工作流执行的一部分被扫描:

    # kics-scan disable=555ab8f9-2001-455e-a077-f2d0f41e2fb9
    name: "Ansible Playbook Run"
    env:
      FAIL_ON: "medium"
      RESULTS_DIR: "results-dir"
    

    我们还设置了工作流的名称,这就是它在 GitHub Web 界面中显示的方式,然后最终设置了一些在工作流执行过程中会使用的变量。

  2. 接下来,我们有定义应运行的工作流的配置;根据我们的需求,我们将在每次代码提交到主分支时运行工作流:

    On:
      push:
        branches:
          - main
    
  3. 接下来,我们必须定义我们的第一个作业,即扫描 Playbook 代码的作业:

    jobs:
      scan_ansible_playbook:
        name: "Scan Ansible Playbook"
        runs-on: ubuntu-latest
        defaults:
          run:
            shell: bash
    

    正如你所看到的,我们将其定义为scan_ansible_playbook,它运行在 GitHub 提供的最新版本的 Ubuntu 镜像上,任务的默认操作是运行 bash。定义好作业后,我们可以继续进行下一步。

  4. 我们从检查代码并创建一个存储扫描结果的目录开始:

        steps:
          - name: "Checkout the code"
            uses: "actions/checkout@v4"
            with:
              token: "${{secrets.GH_PAT}}"
    
  5. 该步骤下载了托管工作流的仓库的副本;如你所见,我们正在使用${{secrets.GH_PAT}}。稍后我们将讨论秘密变量。现在,我们必须创建该文件夹:

          - name: "Create the folder for storing the scan results"
            run: mkdir -p ${{env.RESULTS_DIR}}
    

    该步骤创建了一个目录,其名称引用为RESULTS_DIR环境变量,我们在工作流文件的顶部部分中定义了该变量。

  6. 引用环境变量时,我们使用${{env.VARIABLE_NAME}}格式。所以,在我们的例子中,我们使用${{env.RESULTS_DIR}}。在下一步中,我们有一个专门用于运行 KICS 的任务,由 Checkmarx 管理和维护:

          - name: "Run kics Scan"
            uses: "checkmarx/kics-github-action@v1.7.0"
            with:
              path: "./"
              output_path: "${{env.RESULTS_DIR}}"
              output_formats: "json,sarif"
              fail_on: "${{ env.FAIL_ON }}"
              enable_jobs_summary: true
    

    正如你所看到的,我们指示任务输出 JSON 和 SARIF 文件,以及我们在上一步中创建的${{env.RESULTS_DIR}}目录,并且如果扫描结果中包含任何在${{ env.FAIL_ON }}中定义的严重性,工作流会失败。我们在工作流文件开始时将其设置为medium

  7. 现在我们完成了扫描,可以查看安装并运行 Ansible 的工作流代码。这被称为run_ansible_playbook

      run_ansible_playbook:
        name: "Install Ansible and run Playbook"
        runs-on: ubuntu-latest
        needs: scan_ansible_playbook
        defaults:
          run:
            shell: bash
    

    正如你所看到的,这个作业的定义和第一个作业一样,唯一的区别是:我们添加了一个needs行,值为scan_ansible_playbook。这指示该作业仅在scan_ansible_playbook完成并成功时才会运行。

  8. 该任务检查三个文件夹是否存在;如果存在,将使用这些文件夹的缓存版本,这意味着一旦工作流运行过一次,后续的执行会更快,因为我们不必每次都安装 Ansible Galaxy 模块及其依赖项:

        steps:
          - name: "Cache Ansible collections and Python packages"
            uses: actions/cache@v4
            with:
              path: |
                ~/.ansible/collections
                ~/.cache/pip
                /home/runner/.local/lib/python3.10/site-packages
              key: ${{ runner.os }}-ansible-collections-and-python-packages
              restore-keys: |
                ${{ runner.os }}-ansible-collections-and-python-packages
    
  9. 接下来,我们有检查出我们仓库的步骤:

          - name: "Checkout the code"
            id: "checkout"
            uses: "actions/checkout@v4"
    

    你可能会问:“为什么我们需要再次检出代码?我们在上一个作业中已经做过了。”这是个很好的问题。

    答案是,运行作业的计算资源在上一个作业完成时被终止,所有数据都丢失。当当前作业启动时,启动了一个新的资源,并从完全崭新的安装开始。

  10. 工作流中的下一步使用 Azure/login@2 任务来安装 Azure CLI(如果尚未安装),然后使用我们在本章前面定义为存储库机密的服务主体信息进行登录:

          - name: "Login to Azure using a service principal"
            uses: "Azure/login@v2"
            with:
              creds: '{"clientId":"${{secrets.ARM_CLIENT_ID }}","clientSecret":"${{secrets.ARM_CLIENT_SECRET }}","subscriptionId":"${{secrets.ARM_SUBSCRIPTION_ID }}","tenantId":"${{secrets.ARM_TENANT_ID }}"}'
    

    我们需要使用 ${{ secrets.SECRET_NAME }} 格式来嵌入机密。在这里,我们使用的是:

    • ${{``secrets.ARM_CLIENT_ID }}

    • ${{``secrets.ARM_CLIENT_SECRET}}

    • ${{ secrets.ARM_SUBSCRIPTION_ID }}

    • ${{ secrets.ARM_TENANT_ID }}

    由于这些都被定义为机密,因此这些值永远不会出现在任何 Pipeline 运行日志中。

    这意味着,虽然我们知道这些值,但有权限运行工作流的其他人永远不需要被告知我们的服务主体凭据,因为他们可以使用这些机密。如果他们检查任何日志或尝试输出它们,由于工作流的执行,它们将被自动屏蔽,因此也永远不会意外暴露。

  11. 在运行 Ansible 之前的最后一步是将 SSH 密钥对添加到我们的主机并进行配置:

          - name: "Setup SSH key for Ansible"
            id: "add-ssh-key"
            run: |
              mkdir ~/.ssh
              chmod 700 ~/.ssh/
              echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
              chmod 600 ~/.ssh/id_rsa
              echo "${{ secrets.SSH_PUBLIC_KEY }}" > ~/.ssh/id_rsa.pub
              chmod 644 ~/.ssh/id_rsa.pub
              cat  ~/.ssh/id_rsa.pub
    
  12. SSH 密钥对是我们所需的最后一部分。现在,我们可以运行 Ansible 了:

          - name: "Run the playbook (with ansible-playbook)"
            id: "ansible-playbook-run"
            continue-on-error: true
            run: |
              ansible-playbook -i inv site.yml 2>&1 | tee ansible_output.log
              echo "summary<<EOF" >> $GITHUB_OUTPUT
              echo "## Ansible Playbook Output" >> $GITHUB_OUTPUT
              echo "<details><summary>Click to expand</summary>" >> $GITHUB_OUTPUT
              echo "" >> $GITHUB_OUTPUT
              echo "\`\`\`" >> $GITHUB_OUTPUT
              cat ansible_output.log >> $GITHUB_OUTPUT
              echo "\`\`\`" >> $GITHUB_OUTPUT
              echo "</details>" >> $GITHUB_OUTPUT
              echo "EOF" >> $GITHUB_OUTPUT
            env:
              ANSIBLE_HOST_KEY_CHECKING: "False"
    

正如你所看到的,运行 Ansible 在这里比在本地机器上运行时稍微复杂一些。我们运行 Ansible playbook 的原因是捕获其输出并格式化输出,以便在 GitHub Actions 作业日志中显示。

下面是发生的事情的详细说明:

  • 运行 playbook(使用 ansible-playbook),以便在工作流的执行日志中提供更清晰的说明。

  • ansible-playbook-run,这样我们就可以在后续步骤中引用此步骤的输出。

  • continue-on-error 设置为 true,我们允许即使此步骤遇到错误,工作流仍然继续。这对于确保工作流可以继续执行其他步骤(例如提供诊断信息或执行清理操作)非常有用,即使 Ansible playbook 失败。

  • 运行:这个关键字启动了一个多行脚本块,该脚本将在作业的 shell 中执行。脚本执行以下操作:

    ansible-playbook -i inv site.yml 2>&1 | tee ansible_output.log
    

    此命令运行在 site.yml 中定义的 Ansible playbook,并使用清单文件 inv2>&1 部分将 stderr 重定向到 stdout,因此 ansible-playbook 命令的标准输出和错误都被管道传输到 tee 命令。tee ansible_output.log 将输出写入 ansible_output.log 文件并在工作流日志中显示,便于实时监控。

    后续的 echo 命令和 cat 会将格式化的 Ansible 输出摘要追加到特殊的 GITHUB_OUTPUT 环境变量中。正如你可能已经注意到的,我们主要使用 Markdown 格式化文本。

  • env 部分定义了此步骤的环境变量。ANSIBLE_HOST_KEY_CHECKING: "False" 禁用了 Ansible 的 SSH 主机密钥检查。此选项通常在自动化环境中使用,以避免人工干预。

我们工作流的最后一步将前一步的输出输出到 $GITHUB_STEP_SUMMARY。这是一个特殊的变量,用于 GitHub Actions 工作流记录工作流执行日志中的步骤结果:

      - name: "Publish Ansible Playbook run to Task Summary"
        env:
          SUMMARY: ${{ steps.ansible-playbook-run.outputs.summary }}
        run: |
          echo "$SUMMARY" >> $GITHUB_STEP_SUMMARY

虽然这完成了我们的工作流代码审查,但还有一个任务在后台执行,我们无需定义。如你所记得,在 run_ansible_playbook 任务的第一步中,我们有一个步骤是查找与工作流相关的任何缓存。通过定义这个步骤,工作流结束时会有一个后部署任务,如果缓存不存在,它会创建缓存。

现在我们理解了工作流代码,让我们检查一下我们新创建的仓库副本。复制示例仓库中的代码,然后提交更改。

提交代码

如前所述,在运行工作流之前,我们需要检查出在本章开始时创建的空仓库。具体操作方式取决于你如何与 GitHub 互动。我使用命令行,但你可能使用 GitHub Desktop 应用程序或 Visual Studio Code 等 IDE。

信息

欲了解更多 GitHub 桌面应用程序的信息,请参见 desktop.github.com/。有关如何配置 SSH 连接到 GitHub 的详细信息,请参见 docs.github.com/en/authentication/connecting-to-github-with-ssh.

如果你想在命令行中跟随操作,你必须更新仓库名称以反映你自己的名称,并确保你可以通过 SSH 访问 GitHub 仓库:

$ git clone https://github.com/PacktPublishing/Learn-Ansible-Second-Edition.git
$ cd Learn-Ansible-Second-Edition-Chapter15

一旦进入文件夹,我将 github.com/PacktPublishing/Learn-Ansible-Second-Edition/tree/main/Chapter15 中的内容复制过来,并确保也复制了 .github 文件夹,因为它包含我们想要执行的工作流。

复制完成后,我运行了以下命令来添加新文件并创建第一次提交,然后推送:

$ git add .
$ git commit -m "first commit"
$ git push

如果一切按计划进行,当你访问仓库并点击 Actions 标签时,你应该能看到类似这样的内容:

图 15.3 – 我们的第一次提交正在运行 GitHub Action

图 15.3 – 我们的第一次提交正在运行 GitHub Action

点击提交的名称应该能显示工作流的进度:

图 15.4 – 查看工作流进度

图 15.4 – 查看工作流进度

点击正在运行的任务——在我的示例中,这是 安装 Ansible 并运行 Playbook 任务。这将显示它的实时进度:

图 15.5 – 查看实时输出

图 15.5 – 查看实时输出

如果一切按计划进行,Ansible playbook 将会运行,Azure 资源将被部署,我们应该能够看到正在运行的 WordPress 实例。

点击页面顶部的摘要链接将显示完整的输出。在这里,我们将看到在工作流运行过程中记录的任何警告或信息,然后是 KICS 结果:

图 15.6 – KICS 扫描结果

图 15.6 – KICS 扫描结果

您还可以展开Ansible Playbook 输出区域并查看日志:

图 15.7 – Ansible Playbook 输出

图 15.7 – Ansible Playbook 输出

在我们删除 Azure 资源之前,让我们看看当扫描失败时会发生什么。为此,打开 roles/azure/tasks/main.yml 并删除以下内容(大约在第 61 行):

    security_group: "{{ nsg_output.state.name }}"

删除后,检查更新的代码。这将触发新的工作流运行:

图 15.8 – 触发第二次工作流运行

图 15.8 – 触发第二次工作流运行

由于我们删除的那一行将触发中等严重性的规则,因此我们的工作流运行应当失败,如此处所示:

图 15.9 – 由于我们的修改,第二次工作流运行失败

图 15.9 – 由于我们的修改,第二次工作流运行失败

完成测试后,我建议登录 Azure 手动删除包含我们刚刚启动的资源的资源组。

如您所见,尽管在部署过程中您需要考虑一些因素——例如确保所有连接和步骤都已到位,以便安全地与云服务提供商交互——但运行 Playbook 的基本思路和方法与在本地机器上运行时几乎相同。

对于我们将要查看的下一个工具 Azure DevOps,也可以说同样的话。

Azure DevOps

我们用于 GitHub Actions 的描述同样适用于 Azure DevOps Pipelines 和代码库,这是我们将在本节中使用的 Azure DevOps 服务中的两个。再次强调,我们将使用平台提供的计算资源来运行 Ansible Playbook,很多方法将是相同的。因此,我们将跳过老的内容,从准备一个 Azure DevOps 项目开始,以托管我们的代码并运行 Playbook。

创建和配置我们的项目

首先,您需要创建一个 Azure DevOps 项目。就像我们的 GitHub 仓库一样,我将其命名为 Learn-Ansible-Second-Edition-Chapter15

图 15.10 – 我们新创建的 Azure DevOps 项目

图 15.10 – 我们新创建的 Azure DevOps 项目

在检查代码并添加我们的管道之前,我们需要配置一些内容;首先是创建与 Azure 本身的服务连接。为此,请点击页面左下角的项目设置按钮。

一旦打开项目设置,在左侧菜单下选择管道,点击服务连接,然后点击创建服务连接按钮。

选择Azure 资源管理器,然后点击下一步;从这里,选择服务主体(手动),再点击一次下一步

我们采取这种方法,而不是其他任何会自动为我们创建服务主体的方法,因为我们已经在GitHub Actions部分记录了服务主体的详细信息。

以下表格包含你需要输入的信息:

选项 内容
订阅 Id 这是你的 Azure 订阅 ID;使用你授予服务主体访问权限的那个订阅 ID。在这个示例中,它是e80d5ad9-e2c5-4ade-a866-bcfbae2b8aea
订阅名称 输入你的 Azure 订阅名称。由于我们将在管道代码中引用订阅 ID,因此可以设置为任何你喜欢的值。
服务 主体 Id 这是你在创建服务主体时得到的appId值。在这个示例中,它是2616e3df-826d-4d9b-9152-3de141465a69
服务 主体密钥 这是你在创建服务主体时得到的password值。在这个示例中,它是Y4j8Q~gVO*NoTaREalPa55w0rdpP-pdaw
租户 ID 这是你在创建服务主体时列出的tenant值的 ID。在这个示例中,它是c5df827f-a940-4d7c-b313-426cb3c6b1fe
服务 连接名称 在此输入azConnection,这是我们在管道代码中引用连接的方式。
安全性 确保选择了授予所有管道访问权限

表格 15.2 – Azure DevOps 中管道所需的信息

输入这些信息后,点击验证并保存按钮。这将检查你输入的详细信息是否正确,并保存服务连接。

接下来,我们需要从 Visual Studio Marketplace 安装几个扩展,以便发布我们的 KICS 报告和 Playbook 运行概况:

为了在你的 Azure DevOps 组织中启用这些扩展,请遵循上述 URL,并按照点击免费获取按钮后的说明进行操作。

最后的配置步骤是添加管道变量组和安全文件。为此,点击左侧菜单中的管道,然后点击。进入页面后,点击+ 变量组按钮。

将变量组命名为playbook并输入以下变量:

名称
breakSeverity MEDIUM
SSH_PUBLIC_KEY 在此粘贴id_rsa.pub文件的内容
subscriptionName azConnection – 这是我们在本节开始时创建的连接名称

表 15.3 – 变量组所需的信息

填写完前面的信息后,点击id_rsa文件。

我们现在已经准备好了所有基本配置,可以上传我们的代码了。

克隆仓库并上传代码

接下来,我们必须克隆仓库并上传我们的代码,包括我们将在下一节中讨论的azure-pipelines.yml文件。为此,请点击左侧菜单中的Repos;你将看到几种克隆仓库的方法。

我选择再次使用 SSH 克隆;如果你跟着操作,更新git clone命令以反映你的仓库:

$ git clone git@ssh.dev.azure.com:v3/russmckendrick/Learn-Ansible-Second-Edition-Chapter15/Learn-Ansible-Second-Edition-Chapter15
$ cd Learn-Ansible-Second-Edition-Chapter15

然后,我从github.com/PacktPublishing/Learn-Ansible-Second-Edition/tree/main/Chapter15复制了文件。这一次,我没有担心复制.github目录,因为它不是必需的。将文件复制到本地克隆的文件夹后,我运行了以下命令,添加了新文件并创建了第一次提交,然后推送:

$ git add .
$ git commit -m "first commit"
$ git push

与我们第一次将代码提交到 GitHub 时不同,由于我们尚未配置管道,因此什么也不会发生。

Azure DevOps 管道

我们的管道定义在azure-pipelines.yml文件中,该文件位于我们仓库文件的根目录。在使用该文件创建管道之前,让我们快速回顾一下内容。

信息

从结构上看,我们的azure-pipelines.yml文件与我们之前在 GitHub Actions 中讨论的非常相似;事实上,你几乎可以认为它们是可以互换和兼容的——但它们并不完全相同,所以请小心不要混淆这两者。

我们的管道文件从一个基本配置开始,指示管道何时触发,加载哪个变量组,以及使用哪个底层镜像。在顶部,包含一个 KICS 排除规则,这是我们在第十三章中讨论过的内容,扫描你的 Ansible Playbooks

# kics-scan disable=3e2d3b2f-c22a-4df1-9cc6-a7a0aebb0c99
trigger:
  - main
variables:
  - group: playbook
pool:
  vmImage: ubuntu-latest

完成基本配置后,我们可以开始各个阶段:

  1. 我们的第一次运行是对代码进行 KICS 扫描:

      - stage: "scan"
        displayName: "KICS - Scan Ansible Playbook"
    
  2. 该阶段由一个作业组成:

       jobs:
          - job: "kics_scan"
            displayName: "Run KICS Scan"
            pool:
              vmImage: "ubuntu-latest"
            container: checkmarx/kics:debian
    
  3. 正如你可能已经注意到的,这里我们使用了checkmarx/kics:debian容器镜像来部署 KICS。这将启动容器并在其中执行以下步骤。我们的步骤包含两个任务——第一个任务创建输出文件夹,检出代码并运行扫描:

            steps:
              - script: |
                  mkdir -p $(System.DefaultWorkingDirectory)/output
                  /app/bin/kics scan --ci -p ${PWD} -o ${PWD} --report-formats "all" --ignore-on-exit results
                  mv results* $(System.DefaultWorkingDirectory)/output
                  ls -lhat $(System.DefaultWorkingDirectory)/output
    
  4. 第二个任务发布输出目录的内容,该目录包含所有扫描结果,作为构建工件:

              - task: PublishBuildArtifacts@1
                inputs:
                  pathToPublish: $(System.DefaultWorkingDirectory)/output
                  artifactName: CodeAnalysisLogs
    
  5. 文件发布后,我们不再需要在此阶段生成的资源,因此可以进入第二阶段:

      - stage: "scan_parse"
        displayName: "KICS - Parse Scan Resaults"
        jobs:
          - job: "kics_scan_parse_result"
            displayName: "Check KICS Scan Resaults"
            pool:
              vmImage: "ubuntu-latest"
            steps:
    
  6. 如你所见,这个阶段解析了我们的扫描结果;我们运行的第一个任务是下载我们在上一阶段上传的工件副本:

              - task: DownloadPipelineArtifact@2
                displayName: "Download the Security Scan Artifact Result"
                inputs:
                  artifact: CodeAnalysisLogs
    

    现在我们已经得到了结果文件,接下来需要审查它们,以判断是否需要运行 Ansible Playbook。这个任务运行一个 bash 脚本,读取 JSON 结果并设置一些管道变量,以控制接下来的步骤。

  7. 我们用一些配置来启动任务:

              - task: Bash@3
                name: "setvar"
                displayName: "Check for issues in the scan result"
                inputs:
                    failOnStderr: true
                    targetType: "inline"
                    script: |
    
  8. 现在,我们有了脚本本身,脚本首先通过设置一些本地变量并使用 echo 命令将一些结果输出到屏幕。这些结果会出现在我们的管道运行中:

                      resultsFilePath="$(Pipeline.Workspace)/results.json"
                      BREAK=$(breakSeverity)
                      echo "Checking for severity level: $BREAK"
                      noIssues=$(jq --arg BREAK "$BREAK" '.severity_counters[$BREAK] // 0' $resultsFilePath)
                      echo "Number of issues found: $noIssues"
    

    然后,我们创建一个,这样当我们查看管道输出时,以下信息会被最小化,从而使其更容易阅读。

  9. 在组内,我们有一个 if 语句,表示如果检测到的问题少于(-lt1(也就是零个问题),则输出变量 OK_TO_DEPLOY 被设置为 true

                      echo "##[group]Checking the scan output"
                      if [ "$noIssues" -lt 1 ]; then
                          echo "##vso[task.setvariable variable=OK_TO_DEPLOY;isOutput=true]true"
                          echo "##vso[task.logissue type=warning]No issue found. Progressing with pipeline."
    
  10. 如果此条件不满足——也就是说,存在一个或多个问题——那么 OK_TO_DEPLOY 被设置为 false 并且记录一个错误:

                      else
                          echo "##vso[task.setvariable variable=OK_TO_DEPLOY;isOutput=true]false"
                          echo "##vso[task.logissue type=error]Pipeline failed due to $noIssues issue(s) found."
                      fi
                      echo "##[endgroup]"
    
  11. 记录错误会停止管道的其余部分运行。下一个也是最后一个阶段运行 Ansible Playbook。它依赖于前一个阶段成功执行,并且 OK_TO_DEPLOY 被设置为 true

      - stage: "run_ansible"
        displayName: "Run Ansible"
        condition: |
          and
            (
              succeeded(),
              eq(dependencies.scan_parse.outputs['kics_scan_parse_result.setvar.OK_TO_DEPLOY'], 'true')
            )
        jobs:
          - job: "ansible_install"
            displayName: "Ansible"
            steps:
    
  12. 第一个任务是登录到 Azure 并将服务主体的详细信息设置为环境变量,以便在后续任务中使用:

              - task: AzureCLI@2
                displayName: 'Azure CLI'
                inputs:
                  azureSubscription: '$(subscriptionName)'
                  addSpnToEnvironment: true
                  scriptType: 'bash'
                  scriptLocation: 'inlineScript'
                  inlineScript: |
                    echo "##vso[task.setvariable variable=ARM_SUBSCRIPTION_ID]$(az account show --query="id" -o tsv)"
                    echo "##vso[task.setvariable variable=ARM_CLIENT_ID]${servicePrincipalId}"
                    echo "##vso[task.setvariable variable=ARM_CLIENT_SECRET]${servicePrincipalKey}"
                    echo "##vso[task.setvariable variable=ARM_TENANT_ID]${tenantId}"
    
  13. 接下来,我们需要将 SSH 密钥添加到环境中。这里使用的是我们之前上传的安全文件:

              - task: InstallSSHKey@0
                displayName: "Add SSH Key"
                inputs:
                  sshKeySecureFile: "id_rsa"
                  knownHostsEntry: "azure.devops"
    
  14. 现在,我们需要添加 SSH 密钥的公共部分,安装运行 Ansible Playbook 所需的工具,然后实际运行它,记得添加服务主体的详细信息:

              - task: Bash@3
                name: "ansible"
                displayName: "Run Ansible"
                env:
                  AZURE_CLIENT_ID: $(ARM_CLIENT_ID)
                  AZURE_SECRET: $(ARM_CLIENT_SECRET)
                  AZURE_TENANT: $(ARM_TENANT_ID)
                  AZURE_SUBSCRIPTION_ID: $(ARM_SUBSCRIPTION_ID)
                  ANSIBLE_HOST_KEY_CHECKING: "False"
                inputs:
                    targetType: "inline"
                    script: |
    
  15. 环境准备好后,我们可以运行脚本,脚本首先会添加 id_rsa.pub 文件并设置正确的权限:

                      echo "##[group]Add SSH key"
                          echo "$(SSH_PUBLIC_KEY)" > ~/.ssh/id_rsa.pub
                          chmod 644 ~/.ssh/id_rsa.pub
                      echo "##[endgroup]"
    
  16. 脚本的下一部分安装 Azure Ansible 集合(来自 Ansible Galaxy)并安装相关要求。我们在这里使用 --force 确保从 Ansible Galaxy 拉取到最新的所有集合:

                      echo "##[group]Install the Azure Ansible Collection"
                          ansible-galaxy collection install --force azure.azcollection
                          pip3 install -r ~/.ansible/collections/ansible_collections/azure/azcollection/requirements-azure.txt
                      echo "##[endgroup]"
    
  17. 安装好这些后,我们可以运行 playbook;我们采取与运行 GitHub Action 类似的方法来运行 playbook:

                      echo "##[group]Run the Ansible Playbook"
                          ansible-playbook -i inv site.yml 2>&1 | tee $(System.DefaultWorkingDirectory)/ansible_output.log
                      echo "##[endgroup]"
    
  18. 脚本的最后部分将我们的 Ansible 输出转换成一个名为 summary.md 的 Markdown 文件:

                      echo "##[group]Create the mardown file for the Ansible Playbook Output"
                          mkdir -p $(System.DefaultWorkingDirectory)/markdown
                          echo "# Ansible Playbook Output" > $(System.DefaultWorkingDirectory)/markdown/summary.md
                          echo "<details><summary>Click to expand</summary>" >> $(System.DefaultWorkingDirectory)/markdown/summary.md
                          echo "" >> $(System.DefaultWorkingDirectory)/markdown/summary.md
                          echo "\`\`\`" >> $(System.DefaultWorkingDirectory)/markdown/summary.md
                          cat $(System.DefaultWorkingDirectory)/ansible_output.log >> $(System.DefaultWorkingDirectory)/markdown/summary.md
                          echo "\`\`\`" >> $(System.DefaultWorkingDirectory)/markdown/summary.md                      echo "</details>" >> $(System.DefaultWorkingDirectory)/markdown/summary.md
                      echo "##[endgroup]"
    
  19. 管道的最后一个任务是将 markdown/summary.md 文件的副本上传到我们的管道:

              - task: PublishMarkdownReports@1
                name: "upload_ansible_output"
                displayName: "Upload Ansible Output"
                inputs:
                  contentPath: "$(Build.SourcesDirectory)/markdown"
                  indexFile: "summary.md"
    

到此,我们的管道已经完成。那么,既然我们知道它的功能,接下来我们将其添加到我们的 Azure DevOps 项目中,并首次运行它。

如果你点击 azure-pipelines.yml 文件,将会加载并提供 运行保存 选项。我们将点击 运行

屏幕上会显示如下内容:

图 15.11 – 第一次运行管道

图 15.11 – 第一次运行管道

然而,事情并非如它看起来的那样!如果点击第一个阶段,您将看到以下内容。管道需要访问我们创建的变量组的权限:

图 15.12 – 授予变量组的权限

图 15.12 – 授予变量组的权限

点击查看并按照屏幕上的说明授予权限。KICS 扫描将会运行,阶段将完成。然后,它将进入解析扫描结果阶段,该阶段也应完成。

如果返回到摘要,您会看到需要更多权限,这次是为了访问我们上传的安全文件:

图 15.13 – 授予安全文件的权限

图 15.13 – 授予安全文件的权限

再次点击查看并按照屏幕上的说明授予权限。这应该是需要授予的最后一个权限。从现在开始,当我们运行管道时,权限已经被授予。

如果点击运行 Ansible阶段,您可以跟踪 Playbook 的运行。如果一切按计划进行,返回摘要页面应该会显示如下内容:

图 15.14 – 一切正常!!!

图 15.14 – 一切正常!!

点击Markdown 报告将显示 Playbook 运行的结果:

图 15.15 – Markdown 报告

图 15.15 – Markdown 报告

点击扫描将显示 KICS 扫描的结果:

图 15.16 – 扫描报告

图 15.16 – 扫描报告

就像 GitHub Actions 一样,我们来看看当扫描失败时会发生什么。再次打开roles/azure/tasks/main.yml,删除如下所示的行(它大约在 61 行附近):

    security_group: "{{ nsg_output.state.name }}"

删除后,提交更新后的代码。这将触发新的工作流运行:

图 15.17 – 管道出现错误

图 15.17 – 管道出现错误

如您所见,我们收到一条消息,说明管道因发现 1 个问题而失败,并且由于未满足运行条件,运行 Ansible 阶段被跳过。

测试完成后,登录 Azure 并手动删除包含我们刚刚启动的资源的资源组。

摘要

在本章中,我们学习了如何使用 GitHub 和 Azure DevOps 提供的计算资源来运行我们的 Ansible Playbooks。我们发现,这非常适合运行我们的 Playbook 代码,因为我们可以将定义计算资源配置的代码与 Playbook 代码一起部署。

我们还学到,通过使用内置工具,我们可以安全地配置环境,以避免将服务主体凭证等机密信息暴露给其他有权限运行 Playbook 的用户。

唯一的缺点是我们必须创建执行 Playbook 的逻辑。如果有一个工具可以从单一用户界面集中运行我们的 Playbooks,岂不是很棒吗?好消息是,在下一章,我们将详细讲解这一点——所以,如果你喜欢我们目前采用的方法,请继续阅读。

进一步阅读

要了解本章涵盖的更多主题,请查看以下资源:

第十六章:介绍 Ansible AWX 和 Red Hat Ansible Automation Platform

本章将探讨两种 Ansible 的图形化界面:商业版 Red Hat Ansible Automation Platform 和开源版 Ansible AWX —— 或者更完整的名称,Ansible Web eXecutable

本章将重点介绍开源的 Ansible AWX,因为它是免费的,并且除了运行该工具所需的资源外,不需要预付费用或合同。

我们将讨论如何安装 Ansible AWX 以及为什么要使用它。毕竟,我们已经在使用 Ansible 的过程中走了 16 章,还没有需要使用图形界面——那么,为什么现在要用呢?

本章结束时,我们将完成以下任务:

  • 讨论了 Red Hat Ansible Automation Platform 与 Ansible AWX 的对比

  • 安装并配置了 Ansible AWX

  • 使用 Ansible AWX 部署了我们的 Microsoft Azure 云应用

技术要求

尽管本章我们仅部署 Ansible AWX,但它的要求相对复杂。因此,我将提供在 Microsoft Azure 上使用 AKS 服务部署 Kubernetes 集群的说明,而不是在本地运行。

如果你正在跟随本书,你需要拥有 Microsoft Azure 账户并安装 Azure CLI。有关更多信息,请参见 第九章迁移到 云端

Red Hat Ansible Automation Platform 与 AWX 的对比

Red Hat Ansible Automation Platform 和 Ansible AWX 是 Red Hat 提供的两个强大工具,用于管理和简化 Ansible 部署。两者都提供了基于 Web 的界面,简化了 Ansible playbook 的执行和管理,使用户能够更容易地利用 Ansible 的自动化功能,而不需要深入的命令行知识。

Red Hat Ansible Automation Platform,前身为 Ansible Tower,是一款综合性的企业级解决方案,超越了 Ansible Tower 的功能。它集成了多种组件,创建了一个紧密结合且广泛扩展的自动化环境。Red Hat Ansible Automation Platform 的一些关键特性如下:

  • 集中控制:Red Hat Ansible Automation Platform 提供了一个统一的基于 Web 的仪表板,用于从一个中心位置定义、调度和监控自动化任务。

  • 基于角色的访问控制RBAC):通过精细化的访问控制,确保用户能够访问合适的自动化资源,从而增强安全性和控制性。

  • 工作流管理:创建复杂的工作流,结合多个 playbook、作业模板和库存源,并支持依赖关系、条件判断和审批流程。

  • 可扩展性和灵活性:自动化可以根据大型企业的需求进行扩展,支持多样化的基础设施,包括云平台、容器和网络设备。

  • 内容集合:访问经过精心策划的预包装模块和插件,以加速自动化项目的实施。

  • 自动化中心:这个集中式的资源库托管了经过认证、合作伙伴支持和社区驱动的内容。它促进了合作,并使高质量资源更加易于访问。

  • 自动化分析:利用复杂的分析工具来审视不同集群和实例的性能、利用率以及各种关键绩效指标(KPI)。

  • 与 Red Hat 生态系统的集成:与其他 Red Hat 产品(如 Red Hat Insights 和 Red Hat Satellite)无缝集成,促进了一个统一的环境。

另一方面,Ansible AWX 是 Red Hat Ansible 自动化平台的开源上游项目。它提供了该平台的许多核心功能,但遵循社区驱动的开发模型,发布频率较高。虽然 Ansible AWX 为自动化提供了坚实的基础,但 Red Hat Ansible 自动化平台可能需要一些企业特定的功能和集成。

选择 Red Hat Ansible 自动化平台还是 Ansible AWX 取决于你们组织的需求和要求。Red Hat Ansible 自动化平台非常适合寻求稳健、功能丰富且拥有商业支持与无缝集成 Red Hat 生态系统的企业。它提供了先进的功能,旨在处理跨不同环境的复杂自动化需求。

另一方面,Ansible AWX 是适合那些偏好开源解决方案并且能够接受社区驱动支持的组织的合适选择。它为自动化提供了坚实的基础,并且得益于更频繁的更新和社区贡献。

Red Hat Ansible 自动化平台和 Ansible AWX 都允许组织在大规模上实现自动化,减少手动工作,并提高 IT 运维的一致性和可靠性。它们提供了用户友好的界面,并使团队协作更加高效,提高了效率和合规性。

Ansible AWX

说安装 Ansible AWX 很复杂,简直是轻描淡写。自从 Red Hat 首次开源该项目以来,部署它一直都非常困难。

幸运的是,第一次发布已经容器化,它从最初在少量容器中运行,逐步过渡到能够在 Kubernetes 集群中运行,并由 AWX Operator 管理。

信息

Kubernetes Operator 使用自定义资源来自动化 Kubernetes 集群中应用程序和组件的管理。它扩展了集群的行为,而无需修改 Kubernetes 本身的代码。Operator 可以处理各种任务,例如部署、备份、升级和服务发现,从而减少了人工干预,提高了系统的可靠性。

我们首先在 Microsoft Azure 上启动自己的 Kubernetes 并配置本地机器,这样我们就可以部署并配置 AWX Operator。

部署和配置 Ansible AWX Operator

我们需要做的第一件事是部署 Kubernetes 集群。为此,我们将使用 Azure CLI 启动一个 AKS 集群。首先,我们需要在命令行中设置一些变量,以定义资源名称、选择要部署的 Azure 区域以及所需的计算节点数量:

$ AKSLOCATION=uksouth
$ AKSRG=rg-awx-cluster
$ AKSCLUSTER=aks-awx-cluster
$ AKSNUMNODES=2

接下来,让我们创建要将集群部署到的 Azure 资源组;这将使我们在完成后更容易删除,因为我们需要删除该资源组及其内容:

$ az group create --name $AKSRG --location $AKSLOCATION

在资源组创建完成后,我们现在可以启动 AKS 集群:

$ az aks create \
     --resource-group $AKSRG \
     --name $AKSCLUSTER \
     --node-count $AKSNUMNODES \
     --generate-ssh-keys

这将花费大约 5 分钟来部署。如果你没有在本地机器上安装 kubectl 命令,你可以运行以下命令,让 Azure CLI 为你安装它:

$ az aks install-cli

最后,安装了 kubectl 后,你可以通过运行以下命令配置凭证和上下文:

$ az aks get-credentials --resource-group $AKSRG --name $AKSCLUSTER

现在我们的集群已经启动并可用,我们必须使用 Helm 安装并配置 AWX Operator。

信息

Helm 是一个软件包管理器,通过将应用程序打包成 charts 并定义必要的资源和配置,简化了 Kubernetes 部署。更多详细信息和安装说明,请参见 helm.sh/

首先,我们需要启用 AWX 仓库并将其拉取到本地机器:

$ helm repo add awx-operator https://ansible.github.io/awx-operator/
$ helm repo update

现在,我们需要将 AWX Operator 部署到集群中:

$ helm install -n awx --create-namespace awx awx-operator/awx-operator --version 2.12.1

部署过程需要一两分钟时间。

请注意

你可能注意到前面的命令指定了一个明确的版本号,因为当前版本存在一些已知的 bug,而这个版本是我们正在使用的版本的一个重大更新。

你可以运行以下命令来检查部署的状态:

$ kubectl get pods -n awx

一切准备就绪后,你应该看到如下屏幕:

图 16.1 – 部署 AWX Operator

图 16.1 – 部署 AWX Operator

随着 AWX Operator 已在集群中部署,我们可以要求该 Operator 现在部署 AWX 本身。为此,运行以下命令:

$ kubectl apply -f https://raw.githubusercontent.com/PacktPublishing/Learn-Ansible-Second-Edition/main/Chapter16/awx/ansible-awx.yaml

此命令只是将以下 YAML 配置传递给 Operator,指示它如何部署我们的 AWX 安装:

---
apiVersion: awx.ansible.com/v1beta1
kind: AWX
metadata:
  name: ansible-awx
  namespace: awx
spec:
  service_type: loadbalancer

如你所见,部署过程并不复杂,因此请不要将其视为一个生产环境下的 AWX 实例。我们只是指示 AWX Operator 部署 AWX 并通过负载均衡器公开服务,以便我们能够连接到它。

现在,我们等待;我们的 AWX 安装需要 15 到 20 分钟来部署应用程序并进行自引导。

你可以通过运行以下代码来检查容器和负载均衡器服务的状态:

$ kubectl get pods -n awx
$ kubectl get svc ansible-awx-service -n awx

一旦基础设置完成,你应该能看到如下图所示的内容。这些是服务 AWX 应用程序的容器。正如你所看到的,它们分别用于数据库、任务运行器和 web 界面:

图 16.2 – 检查 AWX 部署状态

图 16.2 – 检查我们 AWX 部署的状态

一旦你的部署显示出与前述相同的输出,最后一步就是获取管理员密码。为此,运行以下命令——该密钥的名称始终为 ansible-awx-admin-password

$ kubectl get secret -n awx ansible-awx-admin-password -o jsonpath="{.data.password}" | base64 –decode

这将从 Kubernetes 密钥存储中提取 base64 编码的密钥并为你解码——它应该看起来像这样:

图 16.3 – 获取管理员密码

图 16.3 – 获取管理员密码

如你在前面的输出中所见,末尾有一个 % 图标——这不是密码的一部分,你只需要前面的内容。

请记录下密码和之前命令中的 EXTERNAL-IP 值,这些信息会告诉你在哪里登录以及使用什么凭证。在之前的部署(已经被终止)中,这些信息如下:

访问 URL 后,你应该会看到一个登录页面,界面如下:

图 16.4 – 获取管理员密码

图 16.4 – 获取管理员密码

登录后,你将进入空的 AWX 实例:

图 16.5 – 获取管理员密码

图 16.5 – 获取管理员密码

现在,让我们设置我们的 playbook。

设置我们的 playbook

在运行我们的 playbook 之前,我们必须将其导入到 Ansible AWX 并配置支持的凭证,如我们的 Azure 服务主体。我们将从一个项目开始。

添加新项目

首先,我们需要添加一个新项目,在这里告诉 Ansible AWX 我们的 playbook 存放的代码仓库。如前所述,我们将使用一个托管代码的 GitHub 仓库。要添加新项目,点击左侧菜单中的 资源 下的 Projects,然后点击 添加 按钮。

在这里,你需要提供几个信息;请填写以下内容:

  • Azure WordPress

  • 在 Azure 部署 WordPress

  • Default

  • 执行环境:选择 AWX EE(最新)

  • GIT

当你选择 源代码控制类型 时,将会出现第二部分,询问你的源代码托管位置的详细信息:

  • https://github.com/PacktPublishing/Learn-Ansible-Second-Edition.git

  • 源代码控制分支/标签/提交:留空

  • 源代码控制引用规范:留空

  • 源代码控制凭证:留空

  • Clean

输入这些信息后,点击 保存。现在,如果你返回 项目 页面,你应该会看到 Ansible 已经下载了 playbook 的源代码:

图 16.6 – 添加项目并从 GitHub 下载代码

图 16.6 – 添加项目并从 GitHub 下载代码

添加凭证

接下来,我们必须告诉 Ansible AWX 在访问 Azure 环境时使用哪些凭证;为添加这些凭证,点击 凭证。此选项也可以在左侧菜单的 资源 部分找到。点击 添加,并输入以下内容:

  • Azure

  • Azure 凭证

  • Default

  • 凭证类型:选择 Microsoft Azure 资源管理器

如前所述,这将打开一个单独的部分;在此,您需要输入我们在 第十五章 中创建的服务主体的详细信息,章节名为 使用 Ansible 与 GitHub Actions 和 Azure DevOps

  • e80d5ad9-e2c5-4ade-a866-bcfbae2b8aea

  • 用户名:留空

  • 密码:留空

  • 创建服务主体时返回的 appId 值;在前一章的示例中,这个值是 2616e3df-826d-4d9b-9152-3de141465a69

  • 创建服务主体时返回的 password 值;在前一章的示例中,这个值是 Y4j8Q~gVO*NoTaREalPa55w0rdpP-pdaw

  • tenant ID;在前一章的示例中,这个值是 c5df827f-a940-4d7c-b313-426cb3c6b1fe

填写完表单后,点击 保存。保存后,您会注意到 客户端密钥 值被标记为 已加密

图 16.7 – 将我们的服务主体添加到 Ansible AWX

图 16.7 – 将我们的服务主体添加到 Ansible AWX

当您在 Ansible AWX 中保存敏感信息时,它会被加密,您只有 替换还原 的选项。在任何时候,您都无法再次查看这些信息。

接下来,我们需要创建一个凭证,包含我们在第十五章中生成的 SSH 密钥的私有部分,章节名为 使用 Ansible 与 GitHub Actions 和 Azure DevOps。为此,请再次点击 添加,但这次输入以下内容:

  • AzureVM

  • Azure 虚拟机的私有 SSH 密钥

  • Default

  • 凭证类型:选择 机器

在附加信息框中,输入以下信息:

  • azureadmin

  • 密码:留空

  • SSH 私钥:复制并粘贴私钥的内容,或者上传私钥文件

  • 剩余选项:留空

填写完毕后,点击 保存。返回到 凭证 屏幕后,再次点击 添加,并输入以下内容:

  • Ansible Galaxy

  • 默认组织的 Ansible Galaxy 凭证

  • Default

  • 凭证类型:选择 Ansible Galaxy/Automation Hub API Token

然后,输入以下信息:

  • https://galaxy.ansible.com

  • 剩余选项:留空

再次点击 保存。现在,是时候添加我们的最后一组凭证了:

  • WordPress Vault

  • WordPress 秘密的 Vault 密码

  • Default

  • 凭证类型:选择 Vault

类型详细信息 部分,输入以下内容:

  • Chapter16 剧本中的 group_vars/common.yml。因此,您必须在此输入密码 wibble —— 如果不输入该密码,示例剧本将失败。

  • Vault 标识符:留空。

那是我们的最后一个凭据。所以,让我们继续进行下一个配置步骤。

添加库存

现在我们已经完成了所有凭据设置,我们需要在 Ansible AWX 中重新创建 production 库存文件的内容。提醒一下,我们一直在使用的库存文件如下所示(注释除外):

[local]
localhost ansible_connection=local
[vmgroup]
[azure_vms:children]
vmgroup
[azure_vms:vars]
ansible_ssh_user=adminuser
ansible_ssh_private_key_file=~/.ssh/id_rsa
host_key_checking=False

要添加库存,请点击左侧菜单中的库存添加按钮现在会弹出一个下拉列表;我们需要从列表中选择添加库存

在打开的表单中,输入以下内容:

  • Azure 库存

  • Azure 库存

  • 默认

  • 实例组:我们稍后将添加这些

  • 标签:保持空白

  • 变量:输入这里列出的值:

    ansible_ssh_user: "adminuser"
    ansible_ssh_private_key_file: "~/.ssh/id_rsa"
    host_key_checking: false
    

输入后,点击保存;这将创建库存。现在,我们可以添加我们需要的两个组。为此,点击,可以在库存详细信息上方的按钮行中找到:

图 16.8 – 将库存添加到 Ansible AWX

图 16.8 – 将库存添加到 Ansible AWX

点击添加并输入以下详细信息:

  • vmgroup

  • vmgroup

  • 变量:保持空白

然后,点击保存,重复该过程,使用以下详细信息添加第二个组:

  • azure_vms

  • azure_vms

  • 变量:保持空白

再次点击保存;现在,您应该已经列出了两个组。

现在我们已经有了项目、库存和访问 Azure 环境的一些凭据,我们需要添加启动和配置集群以及终止它的模板。

添加模板

让我们来看看如何添加模板。

信息

我们将向我们的剧本传递一个运行时变量,该变量将包含 SSH 密钥的公钥部分 —— 我们已经在本章前面将私钥部分作为凭据添加 —— 并将被命名为ssh_key_public。请确保在填写这些细节时,您已经拥有公钥。

点击左侧菜单中的模板,然后在添加按钮的下拉菜单中选择作业模板。这是我们遇到的最复杂的表单;然而,当我们填写细节时,部分内容将自动填充。让我们开始吧:

  • 启动 WordPress

  • 在 Azure 中启动 WordPress

  • 作业类型:选择运行

  • 库存:选择Azure 库存

  • 项目:选择Azure WordPress

  • 执行环境:选择AWX EE(最新)

  • 剧本:从下拉列表中选择Chapter16/site.yml

  • 凭据:选择以下内容:

    • 机器AzureVM

    • Microsoft Azure 资源 管理器Azure

    • 保管库WordPress 保管库

  • ssh_key_public变量在此处;这里显示的是要输入的简化版本:

    ---
    ssh_key_public: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDCGosD5doqnJgOLpkztaDvIZFaCKoChm9yyU6FPaci9fZR60SCXbOu1zeMmyJouFH7xVBv7xw5HBk0FDNLXrssR5B7YHiti8= youremail@example.com"
    
  • 剩余选项:保持空白

点击保存;您将被带到模板的概览页面:

图 16.9 – 完整的模板

图 16.9 – 完整的模板

添加后,我们需要使用以下详细信息重复该过程,针对终止我们部署的剧本:

  • 终止 WordPress

  • 终止 WordPress 在 Azure 中

  • 作业类型: 选择 运行

  • 库存: 选择 Azure 库存

  • 项目: 选择 Azure WordPress

  • 执行环境: 选择 AWX EE(最新)

  • Playbook: 从下拉列表中选择 Chapter16/destroy.yml

  • 凭据: 选择以下内容:

    • Microsoft Azure 资源 管理器: Azure
  • 剩余选项: 留空

填写完这些细节后,点击 保存

我们已经准备好运行我们的 playbook,接下来就执行它吧。

运行我们的 playbook

回到 模板 页面,你应该能看到我们配置的两个模板:

图 16.10 – 我们的两个模板

图 16.10 – 我们的两个模板

要从此页面运行 playbook,请点击 Launch WordPress 模板上的 火箭 图标;这将启动 playbook 运行,并将你带到一个作业页面,你可以在这里查看 playbook 作业的状态:

图 16.11 – 使用 Ansible AWX 在 Azure 中启动 WordPress

图 16.11 – 使用 Ansible AWX 在 Azure 中启动 WordPress

如果一切按计划进行,大约 5 分钟后,你应该能收到确认,表示 playbook 已完成且资源已启动:

图 16.12 – Ansible AWX 已完成运行 playbook

图 16.12 – Ansible AWX 已完成运行 playbook

在这里,你可以重新运行启动 playbook,它应该会像在本地机器上重新运行时一样,识别到新部署的资源。

考虑到我们已经启动的 Azure 资源数量,在我们审查 playbook 代码的更改之前,应该先终止 WordPress 资源。点击 Terminate WordPress 模板旁边的 火箭 图标,销毁我们刚刚启动的资源。

终止 Kubernetes 集群

在终止 Azure AKS 资源之前,我建议你先点击并探索一下 Ansible AWX 界面。完成后,你可以删除 Azure 资源,并通过运行以下命令清理本地配置:

$ AKSRG=rg-awx-cluster
$ AKSCLUSTER=aks-awx-cluster
$ az aks delete --resource-group $AKSRG --name $AKSCLUSTER
$ az group delete --name $AKSRG
$ kubectl config delete-cluster $AKSCLUSTER
$ kubectl config delete-context $AKSCLUSTER

集群将需要大约 5 分钟时间来移除。为了安全起见,请在完成之前不要关闭任何窗口。

信息

和往常一样,请再次确认你的云资源已经终止 – 你不希望产生任何意外费用。

现在我们已经终止了所有可能产生费用的资源,让我们来讨论一下在 Playbook 中需要考虑的一些事项。

Playbook 注意事项

虽然我们已经轻轻触及了一些需要对 playbook 进行更改的地方,以便它能在 Ansible AWX 中运行,现在让我们深入探讨一下。

对现有 playbook 的更改

当我们在本地运行代码时,为了保持 playbook 简单,我们创建了一个名为 secrets.yml 的文件,并从中加载变量。现在我们在共享环境中运行 Ansible,我们应该将 Ansible 执行环境视为临时性的,这意味着我们不能依赖这种方法。

我使用 Ansible Vault 对密码进行了加密,并将其随代码一起传输以解决这个问题。为此,我运行了以下命令:

$ ansible-vault encrypt_string 'SomeP4ssw0rd4MySQL' --name 'db_password'
$ ansible-vault encrypt_string 'aP455w0rd4W0rDPR355' --name 'wp_password'

在被提示输入 Vault 密码时,我输入了 wibble 作为密码,然后我们在 Ansible AWX 中添加凭据时设置了 Vault 密码。你可以在 group_vars/common.yml 文件中查看前面命令的结果。

回到我们从本地机器运行 playbook 时的代码,在 第九章迁移到云端,包含公钥数据的变量看起来像这样:

vm_config:
  key:
    path: "/home/adminuser/.ssh/authorized_keys"
    data: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"

正如你所看到的,我们通过读取 ~/.ssh/id_rsa.pub 文件的内容来填充 vm_config.key.data 变量。然而,当我们将 playbook 移到 Ansible AWX 时,这个文件不再存在。

因此,我们添加了 ssh_key_public 变量,其中包含我们在添加机器凭证时上传的私钥的公钥部分,这样就可以在 Azure 上启动资源。这意味着代码需要更新为如下所示:

vm_config:
  key:
    path: "/home/adminuser/.ssh/authorized_keys"
    data: "{{ ssh_key_public }}"

就变更而言,没有什么太戏剧性的内容,希望也不是什么出乎意料的事情。

Ansible Galaxy 集合

你可能没有注意到,但我们不需要考虑与 Azure 交互的模块,这是我们在 第九章迁移到云端 中讨论的初步内容之一。

Ansible AWX 不支持这些以及其他我们需要的模块集合,无法让我们的 playbook 直接运行,那么我们的 playbook 是如何在没有报错的情况下工作的呢?

当我们第一次添加项目时,我们配置它使用支持本书的 GitHub 仓库,里面包含了我们到目前为止讨论的所有代码。该仓库可以在 github.com/packtPublishing/Learn-Ansible-Second-Edition/ 找到。

我们只是指示 Ansible AWX 使用 Chapter16 文件夹中的 site.ymldestory.yml 文件,但在后台,Ansible AWX 还使用了 requirements.yml 文件,该文件位于仓库根目录中的 collections 文件夹中。

该文件包含以下代码:

---
collections:
  - name: "azure.azcollection"
    source: "https://galaxy.ansible.com"
  - name: "community.general"
    source: "https://galaxy.ansible.com"
  - name: "community.mysql"
    source: "https://galaxy.ansible.com"

正如你所看到的,这让 Ansible AWX 知道它需要从 Ansible Galaxy 下载 azure.azcollectioncommunity.generalcommunity.mysql 集合,并在后台安装它们的先决条件。

我们所需要做的就是创建 Ansible Galaxy 凭证,并将其附加到我们的默认组织上。这意味着每当 Ansible AWX 遇到一个 collections/requirements.yml 文件时,它将使用提供的凭证向 Ansible Galaxy 进行身份验证,在我们的情况下,由于我们没有拉取私有集合,所以使用的是匿名凭证。

我们还可以做一些事情,比如将集合固定在特定版本上,或者添加角色:

collections:
  - name: "azure.azcollection"
    source: "https://galaxy.ansible.com"
    version: 2.0.0
roles:
  - name: "russmckendrick.learnansible_example"
    source: "https://galaxy.ansible.com"

如果你是自托管 Ansible Galaxy 的安装版本,或者甚至提供包含角色和集合的 Git 仓库链接,你也可以提供不同的 URL。

这意味着,Ansible AWX 可以像从本地机器运行 Ansible 一样灵活。

在我们讨论完 Ansible AWX 之前,让我们看看运行它的优缺点。

Ansible AWX 的优缺点

我相信你会同意,基于我们在 Ansible AWX 上的使用经验,它看起来是一个很棒的工具。然而,运行它也有一些优点和缺点。

开源

Ansible AWX 是一个开源项目,这意味着任何人都可以免费使用、修改和贡献。这与专有解决方案相比,可以显著降低成本。然而,它的企业级功能较为有限。

Ansible AWX 提供了广泛的功能,然而,一些 Red Hat Ansible Automation Platform 中的高级企业特定功能,如高级报告、服务级别协议SLA)管理,以及更全面的集成,可能是必需的。

社区驱动的开发

作为开源项目,Ansible AWX 拥有一个强大的开发者和用户社区,积极地为其开发做出贡献,提供支持并分享最佳实践。

然而,作为一个开源项目,Ansible AWX 依赖于社区支持,而非官方商业支持。社区通常很活跃且乐于助人,但没有保证的响应时间,甚至无法确保有人能够在 Red Hat Ansible Automation Platform 的商业支持之外提供帮助。

频繁的更新和改进

Ansible AWX 的发布周期比 Red Hat Automation Platform 更频繁。这意味着你可以更快地获得新功能、bug 修复和改进。

Ansible AWX 的频繁发布周期意味着你可能需要更频繁地更新,以访问最新的功能和 bug 修复。升级 Ansible AWX 可能需要更多的工作来确保兼容性和稳定性,尤其是在生产环境中。

更新和 Ansible AWX 一直是一个挑战;它们通常更像是迁移而非就地更新。

以我们快速部署 Ansible AWX 为例,我们需要一种方法来升级它。我们必须在 Kubernetes 集群外部部署一个外部数据库服务器,以创建一个更接近生产环境的环境——这个数据库服务器将存储并持久化我们所有的数据和配置。

更新 Ansible AWX,我们需要销毁集群中的所有资源(数据库除外),更新 AWX Operator,然后重新部署运行最新版本的 Ansible AWX —— 这将连接到我们的外部数据库,并运行所有必要的数据库迁移脚本,以更新我们的架构和数据,使其与新版本兼容。

扎实的基础

Ansible AWX 提供了强大的功能,用于管理和执行 Ansible Playbook,使其成为那些开始自动化之旅或有更简单自动化需求的组织的稳固选择。

灵活性与定制化

虽然 Ansible AWX 可以与各种工具和系统集成,但它可能与 Red Hat 自动化平台相比,具有不同程度的即插即用集成和认证内容。Red Hat 自动化平台旨在与其他 Red Hat 产品无缝协作,并且支持更广泛的集成生态系统。

Ansible AWX 在管理大规模部署或复杂企业环境时也可能存在限制。可能需要额外的设置、配置和资源来有效地处理高流量的自动化任务。

摘要

本章介绍了 Ansible AWX,并简要提到了 Red Hat 自动化平台,这两个强大的图形界面用于管理和简化 Ansible 部署。

我们了解了它们的不同之处、所提供的优势,以及如何在 Microsoft Azure 上的 Kubernetes 集群中安装和配置 Ansible AWX。我们成功地运行了 Playbook,通过设置项目、凭证、库存和模板,使用 Ansible AWX 启动和终止在 Azure 上运行的 WordPress。

在这个过程中,我们发现了必要的 Playbook 考虑事项和修改,例如使用 Ansible Vault 保护敏感信息、处理 SSH 密钥,并利用 Ansible Galaxy 集合。

虽然 Ansible AWX 提供了许多优点,包括其开源特性、社区驱动开发和扎实的基础,但在企业环境中仍然需要意识到其潜在的限制,以及与平台更新相关的挑战。

唯一我们没有讨论的是运行商业支持的企业级 Red Hat 自动化平台的成本。Red Hat 并未在其官网公开发布这些费用。你需要联系其合作伙伴或直接联系 Red Hat 获取详情。

在我们的下一章,也是最后一章中,我们将探讨如何将 Ansible 集成到日常工作流程中,如何在运行时调试 Playbook,以及我如何在实际应用中使用 Ansible 的一些例子。

进一步阅读

要了解更多本章中涉及的主题,请查看以下资源:

第十七章:Ansible 的后续步骤

在本章的最后一部分,我们将讨论如何将 Ansible 集成到你的日常工作流程中。我们将讨论持续集成工具、监控工具和故障排除。

我们将讨论以下主题:

  • 集成第三方服务

  • 如何使用 Ansible 在问题发生时进行故障排除

  • 一些实际案例

让我们直接进入主题,看看如何将我们的剧本与第三方服务进行集成。

技术要求

本章与之前的章节有所不同。虽然本章和 GitHub 库中给出了代码示例,但它们并不是完整的可运行示例。相反,我们将讨论如何将它们集成到你的项目中,使它们更像是可能的艺术,而不是完全形成的示例。

集成第三方服务

虽然你可能自己运行剧本,但保持剧本运行日志或更新其他团队成员或部门的结果是个好主意。Ansible 有多个模块可以帮助你与第三方服务合作,提供实时通知。

让我们从 Slack 开始。

Slack

Slack 迅速成为不同 IT 部门团队协作服务的首选。Slack 的一个主要优势是通过其应用目录支持第三方应用;Ansible 通过community.general.slack模块支持 Slack 的传入 Webhooks。

记住,如果你还没有安装community.general集合,你可以运行以下命令来安装它:

$ ansible-galaxy collection install community.general

在我们查看 Ansible 代码之前,先快速讨论一下如何创建一个 Slack 应用并启用 webhooks。

首先,你需要创建自己的 Slack 应用;你可以访问api.slack.com/apps/new来完成这一步。进入后,点击创建应用按钮,并选择从零开始选项。接下来,你需要填写应用名称选择一个工作区来开发你的应用,对于我们大多数人来说,这将是你的主工作区,正如以下截图所示:

图 17.1 – 创建 Slack 应用

图 17.1 – 创建 Slack 应用

一旦 Slack 应用创建完成,你将进入新的应用设置页面。在左侧菜单中,你应该看到传入 Webhooks的选项。进入该页面并将启用传入 Webhooks开关切换为开启。这将扩展选项,并允许你添加新的 Webhook工作区

在这里,你需要选择你希望你的 Slack 应用发布到哪里;如以下截图所示,我选择了#general频道:

图 17.2 – 选择发布位置

图 17.2 – 选择发布位置

一旦选择了,你将被带回到你的应用的Incoming Webhooks页面;在这里,你将获得一个 Webhook URL,它应该看起来像以下内容,你需要记录并妥善保管(以下的链接已被撤销):

https://hooks.slack.com/services/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

现在我们已经准备好与 Slack 交互,我们可以查看代码。正如本章开始时提到的,我只会深入讲解其中的一些代码,因为很多内容你可能已经很熟悉了。

我们只需要添加一个变量,它是用于识别和验证我们创建的 Webhook 的 token:token 是 Webhook URL 中https://hooks.slack.com/services/后面的所有内容,因此在我的例子中,我将该变量放在group_vars/common.yml中,如下所示:

slack:
  token: "TBCRVDMGA/B06RCMPD6R4/YBTo7ZXZHrRg57fvJXr1sg43"

由于这个 token 应该被视为机密,我建议也使用 Ansible Vault 来加密其值,因此你可以运行以下命令来实现这一点:

$ ansible-vault encrypt_string 'TBCRVDMGA/B06RCMPD6R4/YBTo7ZXZHrRg57fvJXr1sg43' --name 'token'

仓库中的 token 使用 Ansible Vault 进行了加密,且它已经被撤销,因此你需要用你自己的 token 来更新它。

通过直接跳到roles/slack/tasks/main.yml,你可以看到剧本启动了 Azure 中的资源组、虚拟网络和子网。

启动 Azure 资源的第一个任务没有变化:

- name: "Create the resource group"
  azure.azcollection.azure_rm_resourcegroup:
    name: "{{ resource_group_name }}"
    location: "{{ location }}"
    tags: "{{ common_tags }}"
  register: "resource_group_output"

此外,我们在前几章中使用的调试任务仍然存在;在调试任务后面,我们有一个任务(嗯,有点像)会将通知发送到 Slack:

- name: "Notify the team on Slack about the resource group status"
  include_tasks: slack_notify_generic.yml

如你所见,它触发了slack_notify_generic.yml文件中的另一个任务,我们将注册输出的内容传递为一组变量,其中大部分变量是显而易见的:

  vars:
    resource_changed: "{{ resource_group_output.changed }}"
    resource_type: "Resource Group"
    resource_name: "{{ resource_group_output.state.name }}"
    resource_location: "{{ resource_group_output.state.location }}"

最后两个有点不同;这个需要完整的资源 ID,并将其前缀加上https://portal.azure.com/#resource,因为资源 ID 是 Azure 中资源的 URL;这个和 URL 前缀一起,会生成一个可点击的链接,当用户点击时,直接带领他们到资源页面:

    azure_portal_link: "https://portal.azure.com/#resource{{ resource_group_output.state.id }}"

最终变量使用 Jinja2 模板函数生成一个以逗号分隔的标签和值的列表:

    resource_tags: >
      {% for key, value in resource_group_output.state.tags.items() %}
      *{{ key }}:* {{ value }}{% if not loop.last %}, {% endif %}
      {% endfor %}

你可能也注意到,{{ key }}变量两侧有一个*;这不是模板函数的一部分,而是 Markdown 语法中的粗体,它会将内容样式化为粗体。

在查看roles/slack/tasks/中的slack_notify_generic.yml之前,让我们快速讨论一下为什么我们选择了这种方法。

正如我们在标题中多次提到的,自动化部署的主要目标之一是尽可能地简化一切。在这种情况下,我们调用的任务将在整个剧本中作为标准,唯一需要改变的是内容。

所以我们并不需要在剧本中多次重复community.general.slack任务,我们可以只定义一次,然后多次调用。这意味着如果我们需要更改community.general.slack任务中的内容,我们只需要在一个地方更新它。

任务本身有一点逻辑附加在其中,现在我们来回顾一下:

- name: "Notify the team on Slack about resource changes"
  community.general.slack:
    token: "{{ slack.token }}"
    parse: "none"

如前面的代码所示,我们传递了 webhook 的 token,并将 parse 选项设置为 none。这意味着 community.general.slack 不会处理我们发布到 webhook 的任何内容,不会去除格式等。

我们不是发送一个简单的消息,而是使用 attachments 类型。这将把我们的消息格式化为块状,并且我们可以根据内容是否发生变化来设置状态颜色:

    attachments:
      - fallback: "Notification about Azure resource changes"

设置颜色的逻辑如下:在这里,我们使用 resource_changed 变量传递的布尔值 truefalse。如果变量等于 true,意味着资源发生了变化,因此我们将颜色设置为预定义的 warning 颜色,即橙色;否则,颜色设置为 good,即绿色:

        color: "{% if resource_changed %}warning{% else %}good{% endif %}"
        title: "Ansible: {{ resource_type }}"

接下来,我们有消息内容:在这里,我们使用与设置颜色类似的逻辑,根据资源是否发生变化来决定颜色:

        text: "{{ resource_name }} has been {% if resource_changed %}created/updated{% else %}checked (no changes){% endif %}."

最后,我们有字段;每个字段都以块的形式显示我们传递给任务的信息,除了一个:

        fields:
          - title: "Location"
            value: "{{ resource_location }}"
            short: true
          - title: "Azure Portal"
            value: "<{{ azure_portal_link }}|View in Azure Portal>"
            short: true
          - title: "Tags"
            value: "{{ resource_tags }}"
            short: false

Azure 门户链接的值稍有不同;Slack 使用 mrkdwn,一种类似于 Markdown 的标记语言,但在某些方面有所不同,特别是在格式化链接时。正如你所看到的,我们将其设置为以下内容:

<{{ azure_portal_link }}|View in Azure Portal>

这是创建可点击链接的 mrkdwn 语法。它将链接到 {{ azure_portal_link }} 变量中传递的 URL。管道符号 | 后的文本是会出现在 Slack 消息中的可见文本,并充当可点击链接。

当 Slack 渲染此消息时,它会显示 {{ azure_portal_link }} 变量,指向 Azure 门户。

现在我们已经了解了 playbook 的样子,让我们运行它:

$ ansible-playbook -i hosts site.yml --ask-vault-pass

这将提示你提供一个有效的密码,然后部署资源;在这种情况下,我们不需要了解运行 playbook 的输出,应该将注意力转向 Slack 本身:

图 17.3 – Playbook 的第一次运行

图 17.3 – Playbook 的第一次运行

从前面的输出中可以看出,已经添加了三个资源,因此它们被称为已创建/更新。橙色条形图显示在消息的左侧。

现在,让我们使用以下命令重新运行 playbook:

$ ansible-playbook -i hosts site.yml --ask-vault-pass

你将看到消息现在是这样的:

图 17.4 – 第二次运行 playbook

图 17.4 – 第二次运行 playbook

这次没有发生任何变化,消息也反映了这一点。状态显示为绿色,因此我们可以快速看到没有变化。

我唯一想补充的是,如果你查看仓库中的代码,你会注意到对于子网,我们需要做一些适配:

  • resource_location:子网没有位置,因此我们使用其所在虚拟网络的位置。

  • azure_portal_link:虽然返回的是子网的 ID,但它并没有完全匹配我们用来直接在 Azure 门户中打开资源的逻辑,因此我们链接到配置了子网的虚拟网络。

  • resource_tags:你不能为子网添加标签,因此我们将值设置为N/A

正如你从屏幕上看到的,这对于通知他人你的剧本正在运行非常有用。它还可以让你快速访问正在创建/更新或检查的资源,并提供资源变更的审计记录。

虽然我们讨论的代码仅适用于 Slack 和在 Microsoft Azure 中部署的资源,但这个概念应该适用于 Ansible 支持的任何集成。

其他集成

Ansible Galaxy 上有数十种其他集成,既有社区支持的,也有供应商支持的。如果你找不到适合你用例的集成,而且你的目标服务有 API,你可以非常快速地使用 ansible.builtin.uri 模块构建集成,该模块旨在与 Web API 和服务进行交互。

以下是其他集成模块的一些示例用例。

大多数现代计算机都内置了一定程度的语音合成功能;通过使用此模块,你可以让 Ansible 通过语音告知你剧本运行的状态:

    - name: "Speak an update"
      community.general.say:
        msg: "Hello from Ansible running on {{ inventory_hostname }}"
        voice: "Zarvox"
      delegate_to: localhost

虽然这很有趣,但并不太实用,可能很快就会变得烦人,所以让我们继续。

Syslog

假设你从目标主机发送日志文件。在这种情况下,你可能希望将剧本运行的结果发送到目标主机的 syslog 中,这样它就会被传送到你的中央日志服务中,用于像SIEM(即安全信息和事件管理)这样的外部服务:

- name: "Send a message to the hosts syslog"
  community.general.syslogger:
    msg: "The task has completed and all is well"
    priority: "info"
    facility: "daemon"
    log_pid: "true"

这是一种很好的方式,用来记录目标主机上发生的事件,并将其与目标操作系统上发生的其他所有事件一起记录。

ServiceNow

ServiceNow 是 ServiceNow 公司提供的企业级 IT 服务管理软件。

通过使用servicenow.servicenow.snow_record模块,你的剧本可以在 ServiceNow 安装中打开事件:

- name: "Create an incident in SNOW"
  servicenow.servicenow.snow_record:
    username: "{{ snow.username}}"
    password: "{{ snow.passord}}"
    instance: "{{ snow.instance }}"
    state: "present"
    data:
      short_description: "Ansible playbook run on {{ inventory_hostname }}"
      severity: 3
      priority: 2
  register: snow_incident_result

打开后,你可以像下面这样为它们添加注释:

- name: "Update the SNOW incident with work notes"
  servicenow.servicenow.snow_record:
    username: "{{ snow.username}}"
    password: "{{ snow.passord}}"
    instance: "{{ snow.instance }}"
    state: present
    number: "{{snow_incident_result['record']['number']}}"
    data:
      work_notes : "{{ resource_name }} has been {% if resource_changed %}created/updated{% else %}checked (no changes){% endif %}."

在剧本运行结束时,你可以关闭事件,这将永久记录你从剧本中传输到 ITSM 工具的所有信息。

Microsoft Teams

尽管我们在本章中以 Slack 为主要示例,Ansible 还支持多个 Microsoft 365 产品,包括通过community.general.office_365_connector_card模块的 Microsoft Teams。Microsoft 365 Connector 卡片非常强大,它们的配置,以及因此而来的 Ansible 模块,可能会变得相当复杂;因此,建议你从以下链接开始了解:

从前面的链接中可以看出,连接器卡片可以根据需要简单或复杂。然而,配置它们可能值得专门成章,所以我们继续往下看。

第三方服务总结

我希望你从本书中得到的一个重要收获是,自动化非常棒;它不仅节省了大量时间,而且使用我们在上一章中介绍的工具,第十六章引入 Ansible AWX 和 Red Hat Ansible 自动化平台,使得那些不是系统管理员或开发人员的人也可以通过友好的 web 界面执行他们的 playbook。在本章的最后部分,我们将进一步探讨这一点,我会介绍一些 Ansible 在我曾合作的组织中实施的实际案例。

本节中我们介绍的模块不仅可以让你记录结果,还能在执行 playbook 时自动进行一些清理工作,并通知你的用户,从而将自动化提升到一个新的水平。

例如,你需要向服务器部署一个新配置。你的服务台已为你发出了一个更改请求,要求你在 ServiceNow 安装中处理这项工作。

你的 playbook 可以在执行更改之前,使用 fetch 模块将配置文件复制到你的 Ansible Controller。然后,playbook 可以使用 servicenow.servicenow.snow_record 模块,将现有配置文件的副本附加到更改请求中,进行更改操作,最后自动更新更改请求的结果。

在我们看一些实际案例之前,让我们来看看你如何在 playbook 执行过程中进行调试。

Ansible playbook 调试器

Ansible 内置了调试器。让我们通过创建一个带有错误的简单 playbook,来看看如何将其集成到你的 playbook 中。正如我们刚才提到的,我们将编写一个使用 community.general.say 模块的 playbook。该 playbook 本身如下所示:

- name: "A simple playbook with a mistake"
  hosts: "localhost"
  debugger: "on_failed"
  vars:
    message: "The task has completed and all is well"
    voice: "Daniel"
  tasks:
    - name: "Say a message on your Ansible host"
      community.general.say:
        msg: "{{ massage }}"
        voice: "{{ voice }}"

有两件事需要指出:第一是错误。正如你所看到的,我们定义了一个名为 message 的变量,但当我在任务中使用它时,我打错了一个字,输入了 massage。幸运的是,在开发这个 playbook 时,我指示 Ansible 在任务失败时使用交互式调试器,通过将 debugger 选项设置为 on_failed

调试任务

让我们运行该 playbook,看看会发生什么:

$ ansible-playbook playbook.yml

第一个问题是我们没有传递主机清单文件,因此会有警告,显示只有 localhost 可用;这没问题,因为我们只希望在我们的 Ansible 控制器上运行 Say 模块:

[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

接下来,Ansible 执行 playbook 本身;这应该会导致一个致命错误:

PLAY [A simple playbook with a mistake] *******************
TASK [Gathering Facts] ************************************
ok: [localhost]
TASK [Say a message on your Ansible host] *****************
fatal: [localhost]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'massage' is undefined. 'massage' is undefined\n\nThe error appears to be in '/Users/russ.mckendrick/Code/Learn-Ansible-Second-Edition/Chapter17/debugger/playbook.yml': line 12, column 7, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n  tasks:\n    - name: \"Say a message on your Ansible host\"\n      ^ here\n"}

通常,playbook 执行会停止,你将返回到你的 shell;然而,因为我们指示 Ansible 进入交互式调试器,所以现在我们看到以下提示:

[localhost] TASK: Say a message on your Ansible host (debug)>

从这里,我们可以开始进一步调查问题;例如,我们可以通过输入以下命令来查看错误:

p result._result

在 Ansible 中,使用 debug 模块时,p 命令用于美化变量或表达式的输出。它代表 p result._result,在 Ansible 调试任务中,它将以更易读和格式化的方式显示 result._result 的值。p 命令使用 Python 标准库中的 pprint漂亮打印)函数来格式化输出。

一旦你按下 Enter 键,失败任务的结果将被返回:

{'_ansible_no_log': False,
 'failed': True,
 'msg': 'The task includes an option with an undefined variable. The error was: \'massage\' is undefined. \'massage\' is undefined\n\nThe error appears to be in \'/Users/russ.mckendrick/Code/Learn-Ansible-Second-Edition/Chapter17/debugger/playbook.yml\': line 12, column 7, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n  tasks:\n    - name: "Say a message on your Ansible host"\n      ^ here\n'}

让我们通过输入以下命令来更仔细地查看任务中使用的变量:

p task.args

这将返回我们在任务中使用的两个参数:

{'msg': '{{ massage }}', 'voice': '{{ voice }}'}

现在,让我们通过以下命令查看任务可用的变量:

p task_vars

你可能已经注意到,我们指示 Ansible 在执行 playbook 时执行 setup 模块,因此任务可用的变量列表非常长:

          'inventory_hostname': 'localhost',
          'inventory_hostname_short': 'localhost',
          'message': 'The task has completed and all is well',
          'module_setup': True,
          'omit': '__omit_place_holder__7da4853be448a08d857e98fbabe7afe1b7c97d00',
          'play_hosts': ['localhost'],
          'playbook_dir': '/Users/russ.mckendrick/Code/Learn-Ansible-Second-Edition/Chapter17/debugger',
          'voice': 'Daniel'},

如你所见,关于我们的 playbook 执行环境有很多信息。在变量列表中,你会注意到所有由 setup 模块收集的信息都以 ansible_ 开头,我们的两个变量被列在底部。

我们可以通过运行以下命令来进一步了解这两个变量:

p task_vars['message']
p task_vars['voice']

这将显示变量的内容:

[localhost] TASK: Say a message on your Ansible host (debug)> p task_vars['message']
'The task has completed and all is well'
[localhost] TASK: Say a message on your Ansible host (debug)> p task_vars['voice']
'Daniel'

我们知道我们将一个拼写错误的变量传递给了 msg 参数,因此我们将进行一些修改并继续执行 playbook。为此,我们将运行以下命令:

task.args['msg'] = '{{ message }}'

这将更新参数,使用正确的变量;我们现在可以通过发出以下命令重新运行任务:

redo

这将立即使用正确的参数重新运行任务,如果一切顺利,你应该听到:“任务已完成,一切 顺利:”

changed: [localhost]
PLAY RECAP ************************************************
localhost: ok=1  changed=1  unreachable=0  failed=0\. skipped=0  rescued=0\. ignored=0

如前面的输出所示,因为我们只有一个任务,所以 playbook 执行完毕。如果有更多任务,它会从上次中断的地方继续。现在你可以更新你的 playbook,修正拼写错误,并继续你接下来的工作。此外,如果我们愿意,也可以输入 continuequit 来分别继续或停止。

Ansible 调试器摘要

当你在编写复杂的 playbook 时,Ansible 调试器是一个非常有用的选项;例如,假设你有一个需要大约 20 分钟运行的 playbook,但它在接近结束时抛出错误,比如在运行 playbook 18 分钟后出现问题。

让 Ansible 进入交互式调试器 shell 不仅意味着你可以精确查看已定义和未定义的内容,而且也意味着你不需要盲目地修改 playbook 然后再等 18 分钟来查看这些修改是否解决了致命错误。

一些现实世界的例子

在我们结束本章和本书之前,我将举几个例子,说明过去几年我如何使用和与 Ansible 互动。

自动化一个复杂的部署

在这个例子中,一个应用程序被分布在数十台公共云服务器上。每个应用程序组件都至少安装在三台不同的主机上,并且需要按特定顺序进行更新。

应用程序开发人员与运维团队合作,优化部署流程并创建了一个 Ansible Playbook。这个 playbook 自动化了应用程序每个组件的以下步骤:

  1. 通过连接到目标主机并执行特定命令,将应用程序置于维护模式。

  2. 创建涉及所有部署成本的快照,以确保在需要时可以进行回滚。

  3. 通过从指定的 GitHub 仓库拉取最新代码并执行一系列命令来更新应用程序,从而启动部署过程。

  4. 通过连接到应用程序的 API 并在每个目标主机上运行一组健康检查来验证部署是否成功。

  5. 如果部署和健康检查成功通过,将应用程序从维护模式中取出并继续进行下一个组件的部署。然而,如果任何测试失败,立即停止部署并执行命令将主机恢复到之前的快照,确保安全回滚。

在实施 Ansible 自动化之前,手动执行这些部署步骤需要几个小时,因为应用程序和运维团队必须协调并仔细遵循流程。这个手动方法使得部署充满挑战,且容易出现人为错误。

通过使用 Ansible 自动化部署任务,团队可以专注于处理由于真正的问题而产生的异常,而不是手动执行过程中导致的错误。在自动化投入使用之前,几乎每次发布过程中都会出现错误,涉及许多主机和复杂的手动步骤。

Ansible 自动化的引入显著改善了部署过程,减少了所需时间并最小化了人为错误的风险。该 playbook 确保了多个部署过程中的一致性、可靠性和可重复性,使得团队能够更频繁且更有信心地部署应用程序组件。

这个例子展示了 Ansible 如何处理复杂的部署场景,简化流程,并增强开发和运维团队之间在公共云环境中的协作。

结合 Ansible 和其他工具

在这个实际场景中,我们与一个团队合作,该团队在使用 Terraform 开发其基础设施自动化方面投入了大量精力。他们的 Terraform 代码成功地部署了基础设施,并使用简单的cloud-init脚本执行了基本的主机引导。

然而,随着应用程序需求的日益复杂,显然需要额外的自动化来有效地管理已配置的主机上的应用程序。我们并没有替换现有的 Terraform 代码,而是引入了 Ansible 来补充基础设施自动化。

为了将 Ansible 与现有的 Terraform 工作流集成,我们使用了community.general.terraform模块。该模块使我们能够直接在 Ansible 剧本中执行 Terraform 部署。

通过利用这种集成,我们将 Terraform 部署生成的输出传递给 Ansible。这使得 Ansible 能够收集已配置主机的详细信息,并执行必要的应用程序引导任务。

Terraform 和 Ansible 的结合证明了这是一个强大的解决方案:

  • Terraform 处理了基础设施的配置,确保所需资源在目标环境中正确创建和配置。

  • Ansible 接管了应用程序管理,利用 Terraform 提供的主机信息无缝配置和部署应用程序组件。

这种方法使团队能够在保持现有 Terraform 代码库的基础上,利用 Ansible 扩展自动化功能。两种工具之间的集成提供了无缝的工作流,使团队能够更有效地管理基础设施和应用程序,而无需舍弃已有的代码。

通过为特定任务选择合适的工具并发挥它们的优势,团队实现了一个更全面、高效的自动化解决方案。Terraform 的基础设施即代码能力与 Ansible 的应用程序管理和编排功能相结合,最终形成了一个强大且灵活的自动化流水线。

部署 Ansible AWX

正如在第十六章中讨论的那样,介绍 Ansible AWX 和 Red Hat Ansible 自动化平台,Ansible AWX 是一个强大的工具,提供了超越基础功能的广泛特性。除了核心功能外,Ansible AWX 还提供了调查、与身份服务(如 Microsoft Entra)的集成以及 基于角色的访问控制RBACs)等功能,这些功能能够为项目和模板提供细粒度的访问管理。

Ansible AWX 中的调查问卷允许你创建交互式表单,在运行 playbook 之前收集用户的输入。当你需要从最终用户那里收集特定信息或参数,而又不暴露底层 playbook 复杂性时,这一功能尤其有用。

与身份服务(如 Microsoft Entra)的集成,使 Ansible AWX 用户能够实现无缝的身份验证和授权。此集成让你可以利用现有的用户账户和访问控制,简化用户管理并确保对 Ansible AWX 资源的安全访问。

Ansible AWX 中的 RBAC 提供了一种灵活且细粒度的方式来管理用户权限。通过 RBAC,你可以定义角色并将其与特定的项目、模板和其他资源关联。这样,你可以控制谁可以访问和执行特定的 playbook,确保用户根据其职责和专业知识具有适当的访问权限。

在以下示例中,我们将探讨 Ansible AWX 如何在我曾合作过的各个组织中应用,以简化流程、自动化任务并帮助团队高效地履行职责,同时保持安全和治理。

配置虚拟机

在这种场景下,IT 团队需要为开发人员提供一个自助服务门户,便于他们在不同环境(如开发、暂存和生产环境)中配置虚拟机VMs)。每个环境有不同的要求和配置。

为简化流程,部署了 Ansible AWX,并创建了一个调查问卷来捕获开发人员的必要信息。问卷包括用于指定操作系统、虚拟机大小、环境及其他相关参数的字段。

提交调查问卷后,Ansible AWX 触发了一个 playbook,自动化了资源配置过程。根据调查问卷的回答,playbook 动态生成了适当的虚拟机配置并在指定的环境中配置了虚拟机。

此外,playbook 与组织的工单系统进行了集成,自动创建了一个包含虚拟机详情的工单,并将其与变更管理流程相关联,用于追踪和审计目的。

通过利用 Ansible AWX 和调查问卷,IT 团队赋能开发人员按需配置虚拟机,同时保持对整个过程的控制和治理。

管理应用部署

在另一个用例中,一个软件开发团队需要将其应用程序部署到多个环境,包括开发、QA 和生产环境。每个环境都有自己的配置和依赖关系。

为简化部署过程,使用了 Ansible AWX。创建了一个调查问卷来捕获必要的部署参数,例如应用版本、目标环境和任何特定的配置选项。

随后,调查问卷的响应被作为变量传递给负责执行部署的 Ansible playbook。该 playbook 处理整个部署过程,包括以下内容:

  • 从工件库中检索指定的应用程序版本

  • 根据提供的参数配置目标环境

  • 部署应用程序组件及其依赖项

  • 运行部署后的测试和健康检查

  • 更新组织项目管理工具中的部署状态

通过使用 Ansible AWX 和调查问卷,开发团队可以通过一个用户友好的界面发起部署,确保一致性并减少手动错误的风险。Playbook 自动化了复杂的部署步骤,为需要部署的团队节省了时间和精力,同时释放了本应执行部署的团队的时间。

更新 DNS 记录

在这个例子中,组织管理着多个 DNS(或其全称,域名系统)区域,跨多个提供商,他们需要允许前线支持团队更新 DNS 记录,而无需直接授予他们访问提供商管理控制台的权限。

为了实现这一目标,使用了 Ansible AWX。创建了一个调查问卷,用于捕获更新 DNS 记录所需的信息。调查问卷包括指定域名、记录类型(如 A、CNAME、MX)、记录值和生存时间TTL)的字段。

提交调查问卷后,Ansible AWX 会触发一个 playbook 来自动化 DNS 记录更新过程。该 playbook 执行以下步骤:

  1. 验证提供的调查输入,确保数据完整性并防止无效条目的输入

  2. 根据调查问卷中指定的域名确定了适当的 DNS 提供商

  3. 使用 Ansible Vault 安全存储的必要凭证连接到 DNS 提供商的 API

  4. 检索指定域名和记录类型的现有 DNS 记录

  5. 使用调查问卷中提供的新值和 TTL 更新 DNS 记录

  6. 使用提供商的 API 保存更新后的 DNS 记录

  7. 在组织的变更管理系统中记录该变更,如 ServiceNow,供跟踪和审计使用

通过使用 Ansible AWX,前线支持团队可以轻松更新 DNS 记录,无需直接访问 DNS 提供商的管理控制台。Playbook 自动化了更新多个提供商的 DNS 记录过程中复杂的步骤,确保了一致性并减少了错误的风险。

此外,与变更管理系统的集成提供了所有 DNS 变更的集中记录,便于跟踪、审计,并符合组织的变更控制流程。

这些示例展示了如何利用 Ansible AWX 来运行任务,并简化不同领域(如基础设施配置和应用部署)中最终用户的流程。通过将 Ansible AWX 与调查结合,并与现有工具和流程集成,组织可以启用自助服务功能,同时保持对关键操作的控制和治理。

总结

我们已经到达了本章的结尾,也是我们书本的结尾。我一直在思考如何总结 Ansible;我认为《学习 Ansible》第一版的总结依然适用。

在回应一位技术招聘人员时,该招聘人员联系了他并要求至少三年的 Ansible 使用经验,而 Ansible 这款工具实际上只发布了短短一段时间,Ansible 的创始人 Michael DeHaan 在一条现在已经删除的推文中说了以下话:

“使用 Ansible 几个月的人和使用 Ansible 三年的人一样好。它是故意设计成一个简单的工具。”

这完美地总结了我对 Ansible 的经验,希望也能总结出你的经验。

一旦掌握了基础知识,继续学习并快速构建更复杂的 playbook 是非常简单的。这些 playbook 可以帮助部署基础代码和应用程序,也可以帮助构建复杂的云架构甚至物理架构。

通过重用你的角色并通过 Ansible Galaxy 访问大量社区贡献的角色和模块,你将拥有许多示例或快速启动点来进行下一个项目。因此,你可以比使用其他工具时更早地卷起袖子开始动手。此外,如果 Ansible 无法完成某项任务,通常可以找到一个可以与其集成的工具来提供缺失的功能。

回到我们在第一章中讨论的内容,安装和运行 Ansible,能够以可重复和可共享的方式定义你的基础设施和部署,并鼓励他人参与贡献你的 playbook,应当是将 Ansible 引入日常工作流的目标。

通过本书,我希望你已经开始思考日常任务中 Ansible 能帮助你节省时间的地方,并祝你在开发自己的 playbook 时好运。

posted @ 2025-07-02 17:45  绝不原创的飞龙  阅读(3)  评论(0)    收藏  举报