精通-Ansible-中文第四版-全-

精通 Ansible 中文第四版(全)

原文:zh.annas-archive.org/md5/F58519F0D978AE01B8EEFA01F4E150D0

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

欢迎阅读《精通 Ansible》,这是您全面更新的指南,介绍了 Ansible 提供的最有价值的高级功能和功能——自动化和编排工具。本书将为您提供所需的知识和技能,真正理解 Ansible 在基本水平上的功能,包括自 3.0 版本发布以来的所有最新功能和变化。这将使您能够掌握处理当今和未来复杂自动化挑战所需的高级功能。您将了解 Ansible 工作流程,探索高级功能的用例,解决意外行为,通过定制扩展 Ansible,并了解 Ansible 的许多新的重要发展,特别是基础设施和网络供应方面。

本书适合对象

本书适用于对 Ansible 核心元素和应用有一定了解,但现在希望通过使用 Ansible 来应用自动化来提高他们的技能的 Ansible 开发人员和运维人员。

本书涵盖的内容

[第一章],《Ansible 的系统架构和设计》,介绍了 Ansible 在工程师代表执行任务时的内部细节,它是如何设计的,以及如何使用清单和变量。

[第二章],《从早期的 Ansible 版本迁移》,解释了从 Ansible 2.x 迁移到 3.x 及更高版本时将经历的架构变化,如何使用 Ansible 集合,以及如何构建自己的集合——对于熟悉早期 Ansible 版本的任何人来说,这是必读的。

[第三章],《使用 Ansible 保护您的秘密》,探讨了加密数据和防止秘密在运行时被揭示的工具。

[第四章],《Ansible 和 Windows-不仅仅适用于 Linux》,探讨了将 Ansible 与 Windows 主机集成,以在跨平台环境中实现自动化的方法。

[第五章],《使用 AWX 进行企业基础设施管理》,概述了强大的、开源的图形化管理框架 AWX,以及在企业环境中如何使用它。

[第六章],《解锁 Jinja2 模板的强大功能》,阐述了 Jinja2 模板引擎在 Ansible 中的各种用途,并讨论了如何充分利用其功能。

[第七章],《控制任务条件》,解释了如何更改 Ansible 的默认行为,定制任务错误和更改条件。

[第八章],《使用角色组合可重用的 Ansible 内容》,解释了如何超越在主机上执行松散组织的任务,而是构建干净、可重用和自包含的代码结构,称为角色,以实现相同的最终结果。

[第九章],《故障排除 Ansible》,带您了解可以用于检查、内省、修改和调试 Ansible 操作的各种方法。

[第十章],《扩展 Ansible》,介绍了通过模块、插件和清单来源添加新功能的各种方法。

[第十一章],《通过滚动部署减少停机时间》,解释了常见的部署和升级策略,以展示相关的 Ansible 功能。

[第十二章],《基础设施供应》,研究了用于创建管理基础设施的云基础设施提供商和容器系统。

第十三章网络自动化,描述了使用 Ansible 自动化网络设备配置的进展。

为了充分利用本书

要跟随本书提供的示例,您需要访问能够运行 Ansible 的计算机平台。目前,Ansible 可以在安装了 Python 2.7 或 Python 3(3.5 及更高版本)的任何机器上运行(Windows 支持控制机,但仅通过在较新版本上运行的 Linux 发行版中的Windows 子系统 Linux(WSL)层支持—有关详细信息,请参见第四章Ansible 和 Windows-不仅适用于 Linux)。支持的操作系统包括(但不限于)Red Hat、Debian、Ubuntu、CentOS、macOS 和 FreeBSD。

本书使用 Ansible 4.x.x 系列版本。Ansible 安装说明可在docs.ansible.com/ansible/latest/installation_guide/intro_installation.html找到。

一些示例使用了 Docker 版本 20.10.8。Docker 安装说明可在docs.docker.com/get-docker/找到。

本书中的一些示例使用了Amazon Web Services(AWS)和 Microsoft Azure 上的帐户。有关这些服务的更多信息,请访问aws.amazon.com/azure.microsoft.com。我们还深入探讨了使用 Ansible 管理 OpenStack,并且本书中的示例是根据此处的说明针对 DevStack 的单个一体化实例进行测试:docs.openstack.org/devstack/latest/

最后,第十三章**,网络自动化,在示例代码中使用了 Arista vEOS 4.26.2F 和 Cumulus VX 版本 4.4.0—请参见此处获取更多信息:www.arista.com/en/support/software-downloadwww.nvidia.com/en-gb/networking/ethernet-switching/cumulus-vx/。如果您使用本书的数字版本,我们建议您自己输入代码或从书的 GitHub 存储库中访问代码(下一节中提供了链接)。这样做将帮助您避免与复制和粘贴代码相关的任何潜在错误。

下载示例代码文件

您可以从 GitHub 上下载本书的示例代码文件,网址为github.com/PacktPublishing/Mastering-Ansible-Fourth-Edition。如果代码有更新,将在 GitHub 存储库中进行更新。

我们还提供了来自我们丰富书籍和视频目录的其他代码包,可在github.com/PacktPublishing/上找到。请查看!

实际代码演示

本书的实际代码演示视频可在bit.ly/3vvkzbP观看。

下载彩色图片

我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图和图表的彩色图片。您可以在这里下载:static.packt-cdn.com/downloads/9781801818780_ColorImages.pdf

使用的约定

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

文本中的代码:表示文本中的代码字词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。例如:“本书将假定ansible.cfg文件中没有设置会影响 Ansible 默认操作的设置”

代码块设置如下:

---
plugin: amazon.aws.aws_ec2
boto_profile: default

任何命令行输入或输出都将按照以下格式编写:

ansible-playbook -i mastery-hosts --vault-id 
test@./password.sh showme.yaml -v

粗体:表示一个新术语,一个重要的词,或者屏幕上看到的词。例如,菜单或对话框中的词以粗体显示。这是一个例子:“您只需导航到您的个人资料首选项页面,然后单击显示 API 密钥按钮。”

提示或重要说明

以这种方式出现。

第一部分:Ansible 概述和基本原理

在本节中,我们将探讨 Ansible 的基本原理,并建立一个健全的基础,以便开发 playbooks 和工作流程。我们还将审查和解释您将发现的变化,如果您熟悉旧版的 Ansible 2.x 发布。

本节包括以下章节:

  • 第一章, Ansible 的系统架构和设计

  • 第二章, 从早期的 Ansible 版本迁移

  • 第三章, 使用 Ansible 保护您的秘密

  • 第四章, Ansible 和 Windows-不仅仅适用于 Linux

  • 第五章, 使用 AWX 进行企业基础设施管理

第一章:Ansible 的系统架构和设计

本章详细探讨了Ansible的架构和设计,以及它如何代表您执行任务。我们将介绍清单解析的基本概念以及数据的发现方式。然后,我们将进行 playbook 解析。我们将详细介绍模块准备、传输和执行。最后,我们将详细介绍变量类型,并找出变量的位置、使用范围以及在多个位置定义变量时确定优先级的方式。所有这些内容将被覆盖,以奠定掌握 Ansible 的基础!

在本章中,我们将涵盖以下主题:

  • Ansible 版本和配置

  • 清单解析和数据源

  • Playbook 解析

  • 执行策略

  • 模块传输和执行

  • Ansible 集合

  • 变量类型和位置

  • 魔术变量

  • 访问外部数据

  • 变量优先级(并将其与变量优先级排序互换)

技术要求

为了跟随本章中提出的示例,您需要一台运行Ansible 4.3或更高版本的 Linux 机器。几乎任何 Linux 版本都可以。对于那些对细节感兴趣的人,本章中提出的所有代码都是在Ubuntu Server 20.04 LTS上测试的,除非另有说明,并且在 Ansible 4.3 上进行了测试。本章附带的示例代码可以从 GitHub 上下载:github.com/PacktPublishing/Mastering-Ansible-Fourth-Edition/tree/main/Chapter01

查看以下视频以查看代码实际操作:bit.ly/3E37xpn

Ansible 版本和配置

假设您已在系统上安装了 Ansible。有许多文档介绍了如何安装 Ansible,适用于您可能使用的操作系统和版本。但是,重要的是要注意,新于 2.9.x 的 Ansible 版本与所有早期版本都有一些重大变化。对于阅读本书的每个人,都曾接触过 2.9.x 及更早版本的 Ansible 的第二章从早期的 Ansible 版本迁移详细解释了这些变化,以及如何解决这些变化。

本书将假定使用 Ansible 版本 4.0.0(或更高版本),配合 ansible-core 2.11.1(或更新版本),这两者都是必需的,并且是撰写时的最新版本。要发现已安装 Ansible 的系统上使用的版本,请使用--version参数,即ansibleansible-playbook,如下所示:

ansible-playbook --version

此命令应该给出与图 1.1类似的输出;请注意,该屏幕截图是在 Ansible 4.3 上进行的,因此您可能会看到与您的ansible-core软件包版本相对应的更新版本号(例如,对于 Ansible 4.3.0,这将是 ansible-core 2.11.1,这是所有命令将返回的版本号):

图 1.1 - 一个示例输出,显示了 Linux 系统上安装的 Ansible 版本

图 1.1 - 一个示例输出,显示了 Linux 系统上安装的 Ansible 版本

重要提示

请注意,ansible是用于执行临时单个任务的可执行文件,而ansible-playbook是用于处理 playbook 以编排多个任务的可执行文件。我们将在本书的后面介绍临时任务和 playbook 的概念。

Ansible 的配置可以存在于几个不同的位置,将使用找到的第一个文件。搜索涉及以下内容:

  • ANSIBLE_CFG:如果设置了此环境变量,则会使用它。

  • ansible.cfg:这位于当前工作目录中。

  • ~/.ansible.cfg:这位于用户的主目录中。

  • /etc/ansible/ansible.cfg:系统的默认中央 Ansible 配置文件。

某些安装方法可能包括将config文件放置在其中一个位置。查看一下是否存在这样的文件,并查看文件中的设置,以了解 Ansible 操作可能会受到影响的情况。本书假设ansible.cfg文件中没有设置会影响 Ansible 的默认操作。

清单解析和数据源

在 Ansible 中,没有清单就不会发生任何事情。即使在本地主机上执行的临时操作也需要清单-尽管该清单可能只包括本地主机。清单是 Ansible 架构的最基本构建块。在执行ansibleansible-playbook时,必须引用清单。清单是存在于运行ansibleansible-playbook的同一系统上的文件或目录。清单的位置可以在运行时使用--inventory-file (-i)参数或通过在 Ansible config文件中定义路径来定义。

清单可以是静态的或动态的,甚至可以是两者的组合,Ansible 不限于单个清单。标准做法是将清单分割成逻辑边界,例如暂存和生产,允许工程师对暂存环境运行一组操作,然后跟随着对生产清单集运行相同的操作。

可以包括变量数据,例如如何连接到清单中特定主机的具体细节,以及以各种方式包含清单,我们将探讨可用的选项。

静态清单

静态清单是所有清单选项中最基本的。通常,静态清单将包含一个ini格式的单个文件。还支持其他格式,包括 YAML,但您会发现当大多数人开始使用 Ansible 时,通常会使用ini。以下是描述单个主机mastery.example.name的静态清单文件的示例:

mastery.example.name 

就是这样。只需列出清单中系统的名称。当然,这并没有充分利用清单所提供的所有功能。如果每个名称都像这样列出,所有操作都必须引用特定的主机名,或者特殊的内置all组(顾名思义,包含清单中的所有主机)。在开发跨您的基础设施中的不同环境的 playbook 时,这可能会非常繁琐。至少,主机应该被分组。

一个很好的设计模式是根据预期功能将系统分组。起初,如果您的环境中单个系统可以扮演许多不同的角色,这可能看起来很困难,但这完全没问题。清单中的系统可以存在于多个组中,甚至组中还可以包含其他组!此外,在列出组和主机时,可以列出没有组的主机。这些主机必须在定义任何其他组之前列出。让我们在之前的示例基础上扩展我们的清单,增加一些更多的主机和分组,如下所示:

[web] 
mastery.example.name 

[dns] 
backend.example.name 

[database] 
backend.example.name 

[frontend:children] 
web 

[backend:children] 
dns 
database 

在这里,我们创建了一个包含一个系统的三个组,然后又创建了两个逻辑上将所有三个组合在一起的组。是的,没错:您可以有组的组。这里使用的语法是[groupname:children],这表明给 Ansible 的清单解析器,名为groupname的这个组只是其他组的分组。

在这种情况下,children是其他组的名称。这个清单现在允许针对特定主机、低级别的角色特定组或高级别的逻辑分组编写操作,或者两者的任意组合。

通过使用通用的组名,比如dnsdatabase,Ansible play 可以引用这些通用组,而不是明确的主机。工程师可以创建一个清单文件,用预生产阶段环境中的主机填充这些组,另一个清单文件用于生产环境中这些组的版本。当在预生产或生产环境中执行时,playbook 的内容不需要更改,因为它引用了存在于两个清单中的通用组名。只需引用正确的清单以在所需的环境中执行它。

清单排序

在 Ansible 2.4 版本中,添加了一个新的 play-level 关键字order。在此之前,Ansible 按照清单文件中指定的顺序处理主机,并且即使在更新的版本中,默认情况下仍然如此。但是,可以为给定的 play 设置order关键字的以下值,从而得到主机的处理顺序,如下所述:

  • inventory:这是默认选项。它只是意味着 Ansible 会像以往一样进行处理,按照inventory文件中指定的顺序处理主机。

  • reverse_inventory:这导致主机按照inventory文件中指定的相反顺序进行处理。

  • sorted:按名称按字母顺序处理主机。

  • reverse_sorted:按照字母顺序的相反顺序处理主机。

  • shuffle:主机以随机顺序处理,每次运行都会随机排序。

在 Ansible 中,使用的字母排序也称为词典排序。简单地说,这意味着值按字符串排序,字符串从左到右处理。因此,假设我们有三个主机:mastery1mastery11mastery2。在这个列表中,mastery1首先出现在字符位置81。然后是mastery11,因为位置8的字符仍然是1,但现在在位置9有一个额外的字符。最后是mastery2,因为字符82,而21之后。这很重要,因为从数字上来看,我们知道11大于2。但是,在这个列表中,mastery11mastery2之前。您可以通过在主机名上添加前导零来轻松解决这个问题;例如,mastery01mastery02mastery11将按照它们在这个句子中列出的顺序进行处理,解决了词典排序的问题。

清单变量数据

清单不仅提供系统名称和分组,还可以传递有关系统的数据。这些数据可能包括以下内容:

  • 用于在模板中使用的特定于主机的数据

  • 用于任务参数或条件的特定于组的数据

  • 调整 Ansible 与系统交互的行为参数

变量是 Ansible 中强大的构造,可以以各种方式使用,不仅仅是这里描述的方式。在 Ansible 中几乎可以包括变量引用的每一件事。虽然 Ansible 可以在设置阶段发现有关系统的数据,但并非所有数据都可以被发现。使用清单定义数据可以扩展这一点。请注意,变量数据可以来自许多不同的来源,一个来源可能会覆盖另一个。我们将在本章后面介绍变量优先级的顺序。

让我们改进现有的示例清单,并向其中添加一些变量数据。我们将添加一些特定于主机和特定于组的数据:

[web] 
mastery.example.name ansible_host=192.168.10.25 

[dns] 
backend.example.name 

[database] 
backend.example.name 

[frontend:children] 
web 

[backend:children] 
dns 
database 

[web:vars] 
http_port=88 
proxy_timeout=5 

[backend:vars] 
ansible_port=314 

[all:vars] 
ansible_ssh_user=otto 

在这个例子中,我们将mastery.example.nameansible_host定义为192.168.10.25的 IP 地址。ansible_host变量是一个行为清单变量,旨在改变 Ansible 在与此主机操作时的行为方式。在这种情况下,该变量指示 Ansible 使用提供的 IP 地址连接到系统,而不是使用mastery.example.name进行名称的 DNS 查找。在本节的末尾列出了许多其他行为清单变量,以及它们的预期用途。

我们的新清单数据还为 web 和 backend 组提供了组级变量。web 组定义了http_port,可以在NGINX配置文件中使用,并且proxy_timeout,可能用于确定HAProxy的行为。backend 组利用了另一个行为清单参数,指示 Ansible 使用端口314连接到此组中的主机,而不是默认的22

最后,引入了一个构造,通过使用内置的all组在清单中的所有主机之间提供变量数据。在这个特定的例子中,我们指示 Ansible 在连接到系统时以otto用户登录。这也是一个行为变化,因为 Ansible 的默认行为是以在控制主机上执行ansibleansible-playbook的用户相同的用户名登录。

以下是行为清单变量及其意图修改的行为的列表:

  • ansible_host:这是 Ansible 将要连接的 DNS 名称或 Docker 容器名称。

  • ansible_port:这指定了 Ansible 将用于连接清单主机的端口号,如果不是默认值22

  • ansible_user:这指定了 Ansible 将用于与清单主机连接的用户名,无论连接类型如何。

  • ansible_password:这用于为认证到清单主机提供密码给 Ansible,与ansible_user一起使用。仅用于测试目的 - 您应该始终使用保险库来存储诸如密码之类的敏感数据(请参阅第三章使用 Ansible 保护您的秘密)。

  • ansible_ssh_private_key_file:这用于指定将用于连接到清单主机的 SSH 私钥文件,如果您没有使用默认值或ssh-agent

  • ansible_ssh_common_args:这定义了要附加到sshsftpscp的默认参数的 SSH 参数。

  • ansible_sftp_extra_args:这用于指定在 Ansible 调用时将传递给sftp二进制文件的附加参数。

  • ansible_scp_extra_args:这用于指定在 Ansible 调用时将传递给scp二进制文件的附加参数。

  • ansible_ssh_extra_args:这用于指定在 Ansible 调用时将传递给ssh二进制文件的附加参数。

  • ansible_ssh_pipelining:此设置使用布尔值来定义是否应该为此主机使用 SSH 流水线。

  • ansible_ssh_executable:此设置覆盖了此主机的 SSH 可执行文件的路径。

  • ansible_become:这定义了是否应该在此主机上使用特权升级(sudo或其他)。

  • ansible_become_method:这是用于特权升级的方法,可以是sudosupbrunpfexecdoasdzdoksu之一。

  • ansible_become_user:这是通过特权升级要切换到的用户,通常在 Linux 和 Unix 系统上是 root。

  • ansible_become_password:这是用于特权升级的密码。仅用于测试目的;您应该始终使用保险库来存储诸如密码之类的敏感数据(请参阅第三章使用 Ansible 保护您的秘密)。

  • ansible_become_exe:这用于设置所选升级方法的可执行文件,如果您没有使用系统定义的默认方法。

  • ansible_become_flags:这用于设置传递给所选升级可执行文件的标志(如果需要)。

  • ansible_connection:这是主机的连接类型。候选项包括localsmartsshparamikodockerwinrm(我们将在本书的后面更详细地讨论这个)。在任何现代 Ansible 发行版中,默认设置为smart(这会检测是否支持ControlPersist SSH 功能,如果支持,则使用ssh作为连接类型;否则,它会回退到paramiko)。

  • ansible_docker_extra_args:这用于指定将传递给给定清单主机上的远程 Docker 守护程序的额外参数。

  • ansible_shell_type:这用于确定问题清单主机的 shell 类型。默认为sh风格的语法,但可以设置为cshfish以适用于使用这些 shell 的系统。

  • ansible_shell_executable:这用于确定问题清单主机的 shell 类型。默认为sh风格的语法,但可以设置为cshfish以适用于使用这些 shell 的系统。

  • ansible_python_interpreter:这用于手动设置清单中给定主机上 Python 的路径。例如,某些 Linux 发行版安装了多个 Python 版本,确保设置正确的版本非常重要。例如,主机可能同时拥有/usr/bin/python27/usr/bin/python3,这用于定义将使用哪个版本。

  • ansible_*_interpreter:这用于 Ansible 可能依赖的任何其他解释语言(例如 Perl 或 Ruby)。这将用指定的解释器二进制替换解释器二进制。

动态清单

静态清单非常好,对许多情况可能足够。然而,有时静态编写的主机集合管理起来太过繁琐。考虑清单数据已经存在于不同系统中的情况,例如 LDAP、云计算提供商或内部配置管理数据库(清单、资产跟踪和数据仓库)系统。复制这些数据将是浪费时间和精力,在按需基础设施的现代世界中,这些数据很快就会变得陈旧或变得灾难性不正确。

当您的站点超出单一剧本集的范围时,可能需要动态清单源的另一个例子。多个剧本存储库可能会陷入持有相同清单数据的多个副本,或者必须创建复杂的流程来引用数据的单个副本。可以轻松利用外部清单来访问存储在剧本存储库之外的常见清单数据,以简化设置。幸运的是,Ansible 不仅限于静态清单文件。

动态清单源(或插件)是 Ansible 在运行时调用的可执行文件,用于发现实时清单数据。这个可执行文件可以访问外部数据源并返回数据,或者它可以只解析已经存在但可能不符合ini/yaml Ansible 清单格式的本地数据。虽然可能并且很容易开发自己的动态清单源,我们将在后面的章节中介绍,但 Ansible 提供了越来越多的示例清单插件。这包括但不限于以下内容:

  • OpenStack Nova

  • Rackspace Public Cloud

  • DigitalOcean

  • Linode

  • Amazon EC2

  • Google Compute Engine

  • Microsoft Azure

  • Docker

  • Vagrant

许多这些插件都需要一定程度的配置,比如 EC2 的用户凭据或者OpenStack Nova的认证端点。由于无法为 Ansible 配置额外的参数以传递给清单脚本,因此脚本的配置必须通过从已知位置读取的ini配置文件或者从用于执行ansibleansible-playbook的 shell 环境中读取的环境变量来管理。另外,请注意,有时这些清单脚本需要外部库才能正常运行。

ansibleansible-playbook指向清单源的可执行文件时,Ansible 将使用单个参数--list执行该脚本。这样,Ansible 可以获取整个清单的列表,以便构建其内部对象来表示数据。一旦数据构建完成,Ansible 将使用不同的参数执行脚本,以发现每个主机的变量数据。在此执行中使用的参数是--host <hostname>,它将返回特定于该主机的任何变量数据。

清单插件的数量太多,我们无法在本书中详细介绍每一个。然而,设置和使用几乎所有这些插件都需要类似的过程。因此,为了演示该过程,我们将介绍如何使用 EC2 动态清单。

许多动态清单插件都作为community.general集合的一部分安装,默认情况下,当您安装 Ansible 4.0.0 时会安装该集合。尽管如此,使用任何动态清单插件的第一步是找出插件属于哪个集合,并在必要时安装该集合。EC2 动态清单插件作为amazon.aws集合的一部分安装。因此,您的第一步将是安装此集合-您可以使用以下命令完成:

ansible-galaxy collection install amazon.aws

如果一切顺利,您应该在终端上看到与图 1.2中类似的输出。

图 1.2 - 使用 ansible-galaxy 安装 amazon.aws 集合的安装

图 1.2 - 使用 ansible-galaxy 安装 amazon.aws 集合的安装

每当您安装新的插件或集合时,都建议阅读附带的文档,因为一些动态清单插件需要额外的库或工具才能正常运行。例如,如果您参考docs.ansible.com/ansible/latest/collections/amazon/aws/aws_ec2_inventory.htmlaws_ec2插件的文档,您将看到该插件需要boto3botocore库才能运行。安装这些库将取决于您的操作系统和 Python 环境。然而,在 Ubuntu Server 20.04(以及其他 Debian 变体)上,可以使用以下命令完成:

sudo apt install python3-boto3 python3-botocore

以下是上述命令的输出:

图 1.3 - 为 EC2 动态清单脚本安装 Python 依赖项

图 1.3 - 为 EC2 动态清单脚本安装 Python 依赖项

现在,查看插件的文档(通常情况下,您还可以通过查看代码和任何附带的配置文件来找到有用的提示),您会注意到我们需要以某种方式向此脚本提供我们的 AWS 凭据。有几种可能的方法可以做到这一点-一个例子是使用awscli工具(如果已安装)来定义配置,然后从您的清单中引用此配置文件。例如,我使用以下命令配置了我的默认 AWS CLI 配置文件:

aws configure

输出将类似于以下屏幕截图(出于明显原因,已删除了安全细节!):

图 1.4 - 使用 AWS CLI 实用程序配置 AWS 凭据

图 1.4 - 使用 AWS CLI 实用程序配置 AWS 凭据

完成这些操作后,我们现在可以创建我们的清单定义,告诉 Ansible 使用哪个插件,并向其传递适当的参数。在我们的示例中,我们只需要告诉插件使用我们之前创建的默认配置文件。创建一个名为mastery_aws_ec2.yml的文件,其中包含以下内容:

---
plugin: amazon.aws.aws_ec2
boto_profile: default

最后,我们将通过使用-graph参数将我们的新清单插件配置传递给ansible-inventory命令来测试它:

ansible-inventory -i mastery_aws_ec2.yml –-graph

假设您在 AWS EC2 中运行了一些实例,您将看到类似以下的输出:

图 1.5 - 动态清单插件的示例输出

图 1.5 - 动态清单插件的示例输出

哇!我们有我们当前 AWS 清单的列表,以及插件执行的自动分组的一瞥。如果您想进一步了解插件的功能,并查看每个主机分配的所有清单变量(其中包含有用的信息,包括实例类型和大小),请尝试将-list参数传递给ansible-inventory,而不是-graph

有了 AWS 清单,您可以立即使用它来针对这个动态清单运行单个任务或整个 playbook。例如,要使用ansible.builtin.ping模块检查 Ansible 对清单中所有主机的身份验证和连接性,您可以运行以下命令:

ansible -i mastery_aws_ec2.yml all -m ansible.builtin.ping

当然,这只是一个例子。然而,如果您对其他动态清单提供程序遵循这个过程,您应该能够轻松地使它们工作。

第十章扩展 Ansible中,我们将开发自己的自定义清单插件,以演示它们的操作方式。

运行时清单添加

就像静态清单文件一样,重要的是要记住,Ansible 将在每次ansibleansible-playbook执行时解析这些数据一次,而且只有一次。这对于云动态源的用户来说是一个相当常见的绊脚石,经常会出现 playbook 创建新的云资源,然后尝试将其用作清单的一部分。这将失败,因为在 playbook 启动时,该资源不是清单的一部分。然而,一切并非都已经丧失!提供了一个特殊的模块,允许 playbook 临时将清单添加到内存中的清单对象,即ansible.builtin.add_host模块。

该模块有两个选项:namegroupsname选项应该很明显;它定义了 Ansible 在连接到这个特定系统时将使用的主机名。groups选项是一个逗号分隔的组列表,您可以将其添加到这个新系统中。传递给该模块的任何其他选项都将成为该主机的主机变量数据。例如,如果我们想要添加一个新系统,命名为newmastery.example.name,将其添加到web组,并指示 Ansible 通过 IP 地址192.168.10.30连接到它。这将创建一个类似以下的任务:

- name: add new node into runtime inventory 
  ansible.builtin.add_host: 
    name: newmastery.example.name 
    groups: web 
    ansible_host: 192.168.10.30 

这个新主机将可供使用 - 无论是通过提供的名称还是通过web组 - 用于ansible-playbook执行的其余部分。然而,一旦执行完成,除非它已被添加到清单源本身,否则该主机将不可用。当然,如果这是一个新创建的云资源,下一个从该云源获取动态清单的ansibleansible-playbook执行将会捕获到新的成员。

清单限制

如前所述,每次执行ansibleansible-playbook都将解析其所提供的整个清单。即使应用了限制,这也是真实的。简单地说,通过使用--limit运行时参数来运行ansibleansible-playbook来在运行时应用限制。该参数接受一个模式,本质上是应用于清单的掩码。整个清单被解析,每次 play 时,所提供的限制掩码都限制了 play 只针对已指定的模式运行。

让我们以前的清单示例,并演示有限制和无限制时 Ansible 的行为。如果您还记得,我们有一个特殊的组all,我们可以用它来引用清单中的所有主机。假设我们的清单写在当前工作目录中,文件名为mastery-hosts,我们将构建一个 playbook 来演示 Ansible 正在操作的主机。让我们将这个 playbook 写成mastery.yaml

--- 
- name: limit example play 
  hosts: all
  gather_facts: false 

  tasks: 
    - name: tell us which host we are on 
      ansible.builtin.debug: 
        var: inventory_hostname 

ansible.builtin.debug模块用于打印文本或变量的值。在本书中,我们将经常使用这个模块来模拟在主机上实际执行的工作。

现在,让我们执行这个简单的 playbook,而不提供限制。为了简单起见,我们将指示 Ansible 使用本地连接方法,这将在本地执行,而不是尝试 SSH 到这些不存在的主机。运行以下命令:

ansible-playbook -i mastery-hosts -c local mastery.yaml

输出应该与图 1.6类似:

图 1.6 - 在未应用限制的清单上运行简单的 playbook

图 1.6 - 在未应用限制的清单上运行简单的 playbook

如您所见,backend.example.namemastery.example.name主机都被操作了。现在,让我们看看如果我们提供一个限制会发生什么,也就是说,通过运行以下命令来限制我们的运行只针对前端系统:

ansible-playbook -i mastery-hosts -c local mastery.yaml --limit frontend

这一次,输出应该与图 1.7类似:

图 1.7 - 在应用了限制的清单上运行简单的 playbook

图 1.7 - 在应用了限制的清单上运行简单的 playbook

在这里,我们可以看到这次只有mastery.example.name被操作了。虽然没有视觉线索表明整个清单已被解析,但如果我们深入研究 Ansible 代码并检查清单对象,我们确实会发现其中的所有主机。此外,我们将看到每次查询对象时限制是如何应用的。

重要的是要记住,无论在 play 中使用的主机模式,还是在运行时提供的限制,Ansible 都会在每次运行时解析整个清单。事实上,我们可以通过尝试访问backend.example.nameansible_port变量数据来证明这一点,这个系统在其他情况下会被我们的限制掩盖。让我们稍微扩展一下我们的 playbook,并尝试访问backend.example.nameansible_port变量:

--- 
- name: limit example play 
  hosts: all 
  gather_facts: false 

  tasks: 
    - name: tell us which host we are on 
      ansible.builtin.debug: 
        var: inventory_hostname 

    - name: grab variable data from backend 
      ansible.builtin.debug: 
        var: hostvars['backend.example.name']['ansible_port'] 

我们仍然会通过与上一次运行相同的命令来应用我们的限制,这将限制我们的操作仅限于mastery.example.name

图 1.8 - 演示即使应用了限制,整个清单仍然被解析

图 1.8 - 演示即使应用了限制,整个清单仍然被解析

我们已成功访问了主机变量数据(通过组变量)的系统,否则会被限制。这是一个重要的技能,因为它允许更高级的场景,比如将任务指向一个被限制的主机。此外,可以使用委托来操纵负载均衡器;这将在升级系统时将系统置于维护模式,而无需将负载均衡器系统包含在限制掩码中。

playbook 解析

清单来源的整个目的是有系统可以操作。操作来自 playbook(或者,在 Ansible 即席执行的情况下,简单的单任务 play)。您应该已经对 playbook 的构建有基本的了解,因此我们不会花太多时间来介绍;但是,我们将深入探讨 playbook 的解析方式的一些具体细节。具体来说,我们将涵盖以下内容:

  • 操作顺序

  • 相对路径假设

  • 播放行为键

  • 为 play 和任务选择主机

  • 播放和任务名称

操作顺序

Ansible 旨在尽可能地让人类理解。开发人员努力在人类理解和机器效率之间取得最佳平衡。为此,几乎可以假定 Ansible 中的所有操作都是按自上而下的顺序执行的;也就是说,文件顶部列出的操作将在文件底部列出的操作之前完成。话虽如此,还有一些注意事项,甚至有一些影响操作顺序的方法。

playbook 只有两个主要操作可以完成。它可以运行一个 play,或者它可以从文件系统的某个地方包含另一个 playbook。这些操作的完成顺序只是它们在 playbook 文件中出现的顺序,从上到下。重要的是要注意,虽然操作是按顺序执行的,但在任何执行之前整个 playbook 和任何包含的 playbook 都会被完全解析。这意味着任何包含的 playbook 文件必须在 playbook 解析时存在-它们不能在较早的操作中生成。这是特定于 playbook 包含的,但不一定适用于可能出现在 play 中的任务包含,这将在后面的章节中介绍。

在 play 中,还有一些更多的操作。虽然 playbook 严格按照自上而下的顺序排列,但 play 具有更细致的操作顺序。以下是可能的操作列表以及它们将发生的顺序:

  • 变量加载

  • 事实收集

  • pre_tasks执行

  • pre_tasks执行通知的处理程序

  • 角色执行

  • 任务执行

  • 从角色或任务执行通知的处理程序

  • post_tasks执行

  • post_tasks执行通知的处理程序

以下是一个示例 play,其中显示了大部分这些操作:

--- 
- hosts: localhost 
  gather_facts: false 

  vars: 
    - a_var: derp 

  pre_tasks: 
    - name: pretask 
      debug: 
        msg: "a pre task" 
      changed_when: true 
      notify: say hi 

  roles: 
    - role: simple 
      derp: newval 

  tasks: 
    - name: task 
      debug: 
        msg: "a task" 
      changed_when: true 
      notify: say hi

  post_tasks: 
    - name: posttask 
      debug: 
        msg: "a post task" 
      changed_when: true 
      notify: say hi 
  handlers:
    - name: say hi
      debug:
        msg: hi

无论这些块在剧本中列出的顺序如何,前面代码块中详细说明的顺序就是它们将被处理的顺序。处理程序(即可以由其他任务触发并导致更改的任务)是一个特殊情况。有一个实用模块ansible.builtin.meta,可以用来在特定点触发处理程序的处理:

- ansible.builtin.meta: flush_handlers 

这将指示 Ansible 在继续下一个任务或播放中的下一个操作块之前,在那一点处理任何待处理的处理程序。了解顺序并能够通过flush_handlers影响顺序是在需要编排复杂操作时必须具备的另一个关键技能;例如,诸如服务重启对顺序非常敏感的情况。考虑服务的初始部署。

play 将有修改config文件并指示应该在这些文件更改时重新启动服务的任务。play 还将指示服务应该在运行。第一次发生这个 play 时,config文件将更改,并且服务将从未运行变为运行。然后,处理程序将触发,这将导致服务立即重新启动。这可能会对服务的任何使用者造成干扰。最好在最后一个任务之前刷新处理程序,以确保服务正在运行。这样,重新启动将在初始启动之前发生,因此服务将启动一次并保持运行。

相对路径假设

当 Ansible 解析一个 playbook 时,可以对 playbook 中的语句引用的项目的相对路径做出一些假设。在大多数情况下,诸如要包含的变量文件、要包含的任务文件、要包含的 playbook 文件、要复制的文件、要渲染的模板和要执行的脚本等的路径都是相对于引用它们的文件所在的目录的。让我们通过一个示例 playbook 和目录列表来探讨这一点,以演示文件的位置:

  • 目录结构如下:
. 
├── a_vars_file.yaml 
├── mastery-hosts 
├── relative.yaml 
└── tasks 
├── a.yaml 
└── b.yaml 
  • a_vars_file.yaml的内容如下:
--- 
something: "better than nothing" 
  • relative.yaml的内容如下:
--- 
- name: relative path play 
hosts: localhost 
gather_facts: false 

vars_files: 
    - a_vars_file.yaml

tasks: 
- name: who am I 
ansible.builtin.debug: 
msg: "I am mastery task" 
- name: var from file 
      ansible.builtin.debug:         
var: something 

- ansible.builtin.include: tasks/a.yaml 
  • tasks/a.yaml的内容如下:
--- 
- name: where am I 
ansible.builtin.debug: 
msg: "I am task a" 

- ansible.builtin.include: b.yaml 
  • tasks/b.yaml的内容如下:
---
- name: who am I
  ansible.builtin.debug:
msg: "I am task b" 

使用以下命令执行 playbook:

ansible-playbook -i mastery-hosts -c local relative.yaml

输出应类似于图 1.9

图 1.9 - 运行利用相对路径的 playbook 的预期输出

图 1.9 - 运行利用相对路径的 playbook 的预期输出

在这里,我们可以清楚地看到对路径的相对引用以及它们相对于引用它们的文件的位置。在使用角色时,还有一些额外的相对路径假设;然而,我们将在后面的章节中详细介绍。

Play 行为指令

当 Ansible 解析一个 play 时,它会寻找一些指令,以定义 play 的各种行为。这些指令与hosts:指令在同一级别编写。以下是一些在 playbook 的这一部分中可以定义的一些更常用键的描述列表:

  • any_errors_fatal:这是一个布尔指令,用于指示 Ansible 将任何失败都视为致命错误,以防止尝试进一步的任务。这会改变默认行为,其中 Ansible 将继续执行,直到所有任务完成或所有主机失败。

  • connection:这个字符串指令定义了在给定 play 中使用哪种连接系统。在这里做出的一个常见选择是local,它指示 Ansible 在本地执行所有操作,但使用清单中系统的上下文。

  • collections:这是在 play 中用于搜索模块、插件和角色的集合命名空间列表,可以用来避免输入完全限定的集合名称FQCNs)的需要 - 我们将在第二章中了解更多,从早期 Ansible 版本迁移。请注意,这个值不会被角色任务继承,因此您必须在meta/main.yml文件中为每个角色单独设置它。

  • gather_facts:这个布尔指令控制 Ansible 是否执行操作的事实收集阶段,其中一个特殊任务将在主机上运行,以揭示关于系统的各种事实。跳过事实收集 - 当您确定不需要任何已发现的数据时 - 可以在大型环境中节省大量时间。

  • Max_fail_percentage:这个数字指令类似于any_errors_fatal,但更加细致。它允许您定义在整个操作被停止之前,您的主机可以失败的百分比。

  • no_log:这是一个布尔指令,用于控制 Ansible 是否记录(到屏幕和/或配置的log文件)给定的命令或从任务接收的结果。如果您的任务或返回涉及机密信息,这一点非常重要。这个键也可以直接应用于一个任务。

  • port:这是一个数字指令,用于定义连接时应使用的 SSH 端口(或任何其他远程连接插件),除非这已经在清单数据中配置。

  • remote_user:这是一个字符串指令,定义了在远程系统上使用哪个用户登录。默认设置是以与启动ansible-playbook的相同用户连接。

  • serial:此指令接受一个数字,并控制在移动到播放中的下一个任务之前,Ansible 将在多少个系统上执行任务。这与正常操作顺序有很大的改变,在正常操作顺序中,任务在移动到下一个任务之前会在播放中的每个系统上执行。这在滚动更新场景中非常有用,我们将在后面的章节中讨论。

  • become:这是一个布尔指令,用于配置是否应在远程主机上使用特权升级(sudo或其他内容)来执行任务。此键也可以在任务级别定义。相关指令包括become_userbecome_methodbecome_flags。这些可以用于配置升级的方式。

  • strategy:此指令设置用于播放的执行策略。

本书中的示例 playbooks 将使用许多这些键。

有关可用播放指令的完整列表,请参阅docs.ansible.com/ansible/latest/reference_appendices/playbooks_keywords.html#play上的在线文档。

执行策略

随着 Ansible 2.0 的发布,引入了一种控制播放执行行为的新方法:strategy。策略定义了 Ansible 如何在一组主机上协调每个任务。每个策略都是一个插件,Ansible 带有三种策略:linear、debug 和 free。线性策略是默认策略,这是 Ansible 一直以来的行为方式。在执行播放时,给定播放的所有主机执行第一个任务。

一旦它们全部完成,Ansible 就会移动到下一个任务。串行指令可以创建批处理主机以这种方式操作,但基本策略保持不变。在执行下一个任务之前,给定批次的所有目标都必须完成一个任务。调试策略使用了前面描述的相同的线性执行模式,只是这里,任务是在交互式调试会话中运行,而不是在没有任何用户干预的情况下运行到完成。这在测试和开发复杂和/或长时间运行的自动化代码时特别有价值,您需要分析 Ansible 代码运行时的行为,而不仅仅是运行它并希望一切顺利!

自由策略打破了这种传统的线性行为。使用自由策略时,一旦主机完成一个任务,Ansible 将立即为该主机执行下一个任务,而不必等待其他主机完成。

这将发生在集合中的每个主机和播放中的每个任务。每个主机将尽可能快地完成任务,从而最大限度地减少每个特定主机的执行时间。虽然大多数 playbooks 将使用默认的线性策略,但也有一些情况下,自由策略会更有优势;例如,在跨大量主机升级服务时。如果播放需要执行大量任务来执行升级,从关闭服务开始,那么每个主机尽可能少地遭受停机时间就更为重要。

允许每个主机独立地尽快地通过播放,将确保每个主机只在必要的时间内停机。如果不使用自由策略,整个集合将会在集合中最慢的主机完成任务所需的时间内停机。

由于自由策略不协调主机之间的任务完成,因此不可能依赖在一个主机上生成的数据在另一个主机上的后续任务中可用。不能保证第一个主机已经完成生成数据的任务。

执行策略被实现为一个插件,因此任何希望为项目做出贡献的人都可以开发自定义策略来扩展 Ansible 的行为。

播放和任务的主机选择

大多数播放定义的第一件事(当然是名称之后)是播放的主机模式。这是用于从清单对象中选择主机以运行任务的模式。一般来说,这很简单;主机模式包含一个或多个块,指示主机、组、通配符模式或正则表达式regex)用于选择。块之间用冒号分隔,通配符只是一个星号,正则表达式模式以波浪号开头:

hostname:groupname:*.example:~(web|db)\.example\.com 

高级用法可以包括组索引选择,甚至是组内的范围:

webservers[0]:webservers[2:4] 

每个块都被视为包含块;也就是说,找到在第一个模式中的所有主机都被添加到在下一个模式中找到的所有主机中,依此类推。但是,可以使用控制字符来改变它们的行为。使用和符号定义了基于包含的选择(存在于两个模式中的所有主机)。

感叹号的使用定义了一个基于排除的选择(存在于先前模式中的所有主机,但不在排除模式中):

  • webservers:&dbservers:主机必须同时存在于webserversdbservers组中。

  • webservers:!dbservers:主机必须存在于webservers组中,但不能存在于dbservers组中。

一旦 Ansible 解析模式,它将根据需要应用限制。限制以限制或失败的主机的形式出现。此结果将存储在播放的持续时间内,并且可以通过play_hosts变量访问。在执行每个任务时,将咨询此数据,并且可能会对其施加额外的限制以处理串行操作。当遇到故障时,无论是连接失败还是执行任务失败,故障主机都将被放置在限制列表中,以便在下一个任务中绕过该主机。

如果在任何时候,主机选择例程被限制为零个主机,播放执行将停止并显示错误。这里的一个警告是,如果播放配置为具有max_fail_precentageany_errors_fatal参数,那么在满足此条件的任务之后,播放簿执行将立即停止。

播放和任务名称

虽然不是严格必要的,但将您的播放和任务标记为名称是一个好习惯。这些名称将显示在ansible-playbook的命令行输出中,并且如果将ansible-playbook的输出定向到日志文件中,这些名称也将显示在日志文件中。任务名称在您想要指示ansible-playbook从特定任务开始并引用处理程序时也会派上用场。

在命名播放和任务时,有两个主要要考虑的点:

  • 播放和任务的名称应该是唯一的。

  • 小心可以在播放和任务名称中使用的变量类型。

通常,为播放和任务命名是一个最佳实践,可以帮助快速确定问题任务可能位于播放簿、角色、任务文件、处理程序等层次结构中的位置。当您首次编写一个小型的单片播放簿时,它们可能看起来并不重要。然而,随着您对 Ansible 的使用和信心的增长,您很快会为自己命名任务而感到高兴!当任务名称重复时,在通知处理程序或从特定任务开始时,唯一性更为重要。当任务名称重复时,Ansible 的行为可能是不确定的,或者至少是不明显的。

以唯一性为目标,许多播放作者将寻求使用变量来满足这一约束。这种策略可能效果很好,但作者需要注意引用的变量数据的来源。变量数据可以来自各种位置(我们将在本章后面介绍),并且分配给变量的值可以多次定义。为了播放和任务名称的缘故,重要的是要记住,只有那些在播放解析时间可以确定值的变量才会正确解析和呈现。如果引用的变量的数据是通过任务或其他操作发现的,那么变量字符串将显示为未解析的输出。让我们看一个利用变量来命名播放和任务的示例播放:

---
- name: play with a {{ var_name }}
  hosts: localhost
  gather_facts: false
  vars:
  - var_name: not-mastery
  tasks:
  - name: set a variable
    ansible.builtin.set_fact:
      task_var_name: "defined variable"
  - name: task with a {{ task_var_name }}
    ansible.builtin.debug:
      msg: "I am mastery task"
- name: second play with a {{ task_var_name }}
  hosts: localhost
  gather_facts: false
  tasks:
  - name: task with a {{ runtime_var_name }}
    ansible.builtin.debug:
      msg: "I am another mastery task" 

乍一看,您可能期望至少var_nametask_var_name能够正确呈现。我们可以清楚地看到task_var_name在使用之前被定义。然而,凭借我们的知识,即播放在执行之前会被完全解析,我们知道得更多。使用以下命令运行示例播放:

ansible-playbook -i mastery-hosts -c local names.yaml

输出应该看起来像图 1.10

图 1.10 - 一个播放运行,显示在执行之前未定义变量时在任务名称中使用变量的效果

图 1.10 - 一个播放运行,显示在执行之前未定义变量时在任务名称中使用变量的效果

正如您在图 1.10中所看到的,唯一正确呈现的变量名称是var_name,因为它被定义为静态播放变量。

模块传输和执行

一旦播放被解析并确定了主机,Ansible 就准备执行一个任务。任务由名称(这是可选的,但仍然很重要,如前面提到的),模块引用,模块参数和任务控制指令组成。在 Ansible 2.9 及更早版本中,模块由单个唯一名称标识。然而,在 Ansible 2.10 及更高版本中,集合的出现(我们将在下一章中更详细地讨论)意味着 Ansible 模块名称现在可以是非唯一的。因此,那些有先前 Ansible 经验的人可能已经注意到,在本书中,我们使用ansible.builtin.debug而不是在 Ansible 2.9 及更早版本中使用的debug。在某些情况下,您仍然可以使用短形式的模块名称(如debug);但是,请记住,具有自己名为debug的集合的存在可能会导致意想不到的结果。因此,Ansible 在其官方文档中的建议是尽快开始与长形式的模块名称交朋友 - 这些被官方称为 FQCNs。我们将在本书中使用它们,并将在下一章中更详细地解释所有这些。除此之外,后面的章节将详细介绍任务控制指令,因此我们只关注模块引用和参数。

模块引用

每个任务都有一个模块引用。这告诉 Ansible 要执行哪个工作。Ansible 被设计为可以轻松地允许自定义模块与播放一起存在。这些自定义模块可以是全新的功能,也可以替换 Ansible 自身提供的模块。当 Ansible 解析一个任务并发现要用于任务的模块的名称时,它会在一系列位置中查找所请求的模块。它查找的位置也取决于任务所在的位置,例如,是否在一个角色内部。

如果任务位于一个角色内,Ansible 首先会在任务所在的角色内部名为library的目录树中查找模块。如果在那里找不到模块,Ansible 会在与主要剧本(由ansible-playbook执行引用的剧本)相同级别的目录中查找名为library的目录。如果在那里找不到模块,Ansible 最终会在配置的库路径中查找,该路径默认为/usr/share/ansible/。可以在 Ansible 的config文件或通过ANSIBLE_LIBRARY环境变量中配置此库路径。

除了之前已经确定为 Ansible 几乎自问世以来的有效模块位置之外,Ansible 2.10 和更新版本的出现带来了Collections。Collections 现在是模块可以组织和与他人共享的关键方式之一。例如,在之前的示例中,我们查看了 Amazon EC2 动态清单插件,我们安装了一个名为amazon.aws的集合。在该示例中,我们只使用了动态清单插件;但是,安装集合实际上安装了一整套模块供我们用于自动化 Amazon EC2 上的任务。如果您运行了本书中提供的命令,该集合将安装在~/.ansible/collections/ansible_collections/amazon/aws中。如果您在那里查看,您将在plugins/modules子目录中找到模块。您安装的其他集合将位于类似的目录中,这些目录的名称是根据安装的集合命名的。

这种设计使模块能够与集合、角色和剧本捆绑在一起,可以快速轻松地添加功能或修复问题。

模块参数

模块的参数并非总是必需的;模块的帮助输出将指示哪些参数是必需的,哪些是可选的。模块文档可以通过ansible-doc命令访问,如下所示(在这里,我们将使用debug模块,这是我们已经用作示例的模块):

ansible-doc ansible.builtin.debug

图 1.11显示了您可以从此命令中期望的输出类型:

图 1.11 - 运行在 debug 模块上的 ansible-doc 命令的输出示例

图 1.11 - 运行在 debug 模块上的 ansible-doc 命令的输出示例

如果您浏览输出,您将找到大量有用的信息,包括示例代码,模块的输出以及参数(即选项),如图 1.11所示。

参数可以使用Jinja2进行模板化,在模块执行时将被解析,允许在以后的任务中使用在先前任务中发现的数据;这是一个非常强大的设计元素。

参数可以以key=value格式或更符合 YAML 本机格式的复杂格式提供。以下是展示这两种格式的参数传递给模块的两个示例:

- name: add a keypair to nova 
  openstack.cloudkeypair: cloud={{ cloud_name }} name=admin-key wait=yes 

- name: add a keypair to nova 
  openstack.cloud.keypair:    
    cloud: "{{ cloud_name }}"     
    name: admin-key     
    wait: yes 

在这个例子中,这两种格式将导致相同的结果;但是,如果您希望将复杂参数传递给模块,则需要使用复杂格式。一些模块期望传递列表对象或数据的哈希;复杂格式允许这样做。虽然这两种格式对于许多任务都是可以接受的,但是复杂格式是本书中大多数示例使用的格式,因为尽管其名称如此,但实际上更容易阅读。

模块黑名单

从 Ansible 2.5 开始,系统管理员现在可以将他们不希望对剧本开发人员可用的 Ansible 模块列入黑名单。这可能是出于安全原因,为了保持一致性,甚至是为了避免使用已弃用的模块。

模块黑名单的位置由 Ansible 配置文件的defaults部分中找到的plugin_filters_cfg参数定义。默认情况下,它是禁用的,建议的默认值设置为/etc/ansible/plugin_filters.yml

目前,该文件的格式非常简单。它包含一个版本头,以便将来更新文件格式,并列出要过滤掉的模块列表。例如,如果您准备过渡到 Ansible 4.0,当前使用的是 Ansible 2.7,您会注意到 sf_account_manager 模块将在 Ansible 4.0 中被完全移除。因此,您可能希望将其列入黑名单,以防止任何人在推出 Ansible 4.0 时创建会出错的代码(请参阅docs.ansible.com/ansible/devel/porting_guides/porting_guide_2.7.html)。因此,为了防止内部任何人使用这个模块,plugin_filters.yml 文件应该如下所示:

---
filter_version:'1.0'
module_blacklist:
  # Deprecated – to be removed in 4.0
  - sf_account_manager

尽管这个功能在帮助确保高质量的 Ansible 代码得到维护方面非常有用,但在撰写本文时,这个功能仅限于模块。它不能扩展到其他任何东西,比如角色。

传输和执行模块

一旦找到一个模块,Ansible 就必须以某种方式执行它。模块的传输和执行方式取决于一些因素;然而,通常的过程是在本地文件系统上定位模块文件并将其读入内存,然后添加传递给模块的参数。然后,在内存中的文件对象中添加来自 Ansible 核心的样板模块代码。这个集合被压缩、Base64 编码,然后包装在一个脚本中。接下来发生的事情取决于连接方法和运行时选项(例如将模块代码留在远程系统上供审查)。

默认的连接方法是 smart,通常解析为 ssh 连接方法。在默认配置下,Ansible 将打开一个 SSH 连接到远程主机,创建一个临时目录,然后关闭连接。然后,Ansible 将再次打开一个 SSH 连接,以便将内存中的包装 ZIP 文件(本地模块文件、任务模块参数和 Ansible 样板代码的结果)写入到我们刚刚创建的临时目录中的文件,并关闭连接。

最后,Ansible 将打开第三个连接,以执行脚本并删除临时目录及其所有内容。模块的结果以 JSON 格式从 stdout 中捕获,Ansible 将适当地解析和处理。如果任务有一个 async 控制,Ansible 将在模块完成之前关闭第三个连接,并在规定的时间内再次 SSH 到主机上,以检查任务的状态,直到模块完成或达到规定的超时时间。

任务性能

关于 Ansible 如何连接到主机的前面讨论导致每个任务对主机有三个连接。在一个任务数量较少的小环境中,这可能不是一个问题;然而,随着任务集的增长和环境规模的增长,创建和拆除 SSH 连接所需的时间也会增加。幸运的是,有几种方法可以缓解这种情况。

第一个是 SSH 功能 ControlPersist,它提供了一个机制,当首次连接到远程主机时创建持久套接字,可以在后续连接中重用,从而绕过创建连接时所需的一些握手。这可以大大减少 Ansible 打开新连接所花费的时间。如果运行 Ansible 的主机平台支持它,Ansible 会自动利用这个功能。要检查您的平台是否支持这个功能,请参考 SSH 的 ControlPersist 手册页。

可以利用的第二个性能增强功能是 Ansible 的一个特性,称为流水线。流水线适用于基于 SSH 的连接方法,并在 Ansible 配置文件的 ssh_connection 部分进行配置:

[ssh_connection] 
pipelining=true 

这个设置改变了模块的传输方式。与其打开一个 SSH 连接来创建一个目录,再打开一个连接来写入组合模块,再打开第三个连接来执行和清理,Ansible 会在远程主机上打开一个 SSH 连接。然后,在这个实时连接上,Ansible 会将压缩的组合模块代码和脚本输入,以便执行。这将连接数从三个减少到一个,这真的可以累积起来。默认情况下,为了与许多启用了sudoers配置文件中的requiretty的 Linux 发行版保持兼容性,流水线作业被禁用。

利用这两种性能调整的组合可以使您的 playbooks 保持快速,即使在扩展环境中也是如此。但是,请记住,Ansible 一次只会处理与配置为运行的 forks 数量相同的主机。Forks 是 Ansible 将分裂为与远程主机通信的工作进程的进程数量。默认值是五个 forks,这将一次处理最多五个主机。随着环境规模的增长,您可以通过调整 Ansible 配置文件中的forks=参数或使用ansibleansible-playbook--forks (-f)参数来提高这个数字,以处理更多的主机。

变量类型和位置

变量是 Ansible 设计的一个关键组成部分。变量允许动态的 play 内容和在不同的清单集中重复使用 play。除了最基本的 Ansible 使用之外,任何其他用途都会使用变量。了解不同的变量类型以及它们的位置以及学习如何访问外部数据或提示用户填充变量数据,是掌握 Ansible 的关键之一。

变量类型

在深入了解变量的优先级之前,首先我们必须了解 Ansible 可用的各种类型和子类型的变量,它们的位置以及可以在哪里使用。

第一个主要的变量类型是清单变量。这些是 Ansible 通过清单获取的变量。这些可以被定义为特定于host_vars、单个主机或适用于整个组的group_vars的变量。这些变量可以直接写入清单文件,通过动态清单插件传递,或者从host_vars/<host>group_vars/<group>目录加载。

这些类型的变量可用于定义 Ansible 处理这些主机或与这些主机运行的应用程序相关的站点特定数据的行为。无论变量来自host_vars还是group_vars,它都将被分配给主机的hostvars,并且可以从 playbooks 和模板文件中访问。可以通过简单地引用名称来访问主机自己的变量,例如{{ foobar }},并且可以通过访问hostvars来访问另一个主机的变量;例如,要访问examplehostfoobar变量,可以使用{{ hostvars['examplehost']['foobar'] }}。这些变量具有全局范围。

第二个主要的变量类型是角色变量。这些变量是特定于角色的,并且被角色任务所利用。然而,值得注意的是,一旦一个角色被添加到一个 playbook 中,它的变量通常可以在 playbook 的其余部分中访问,包括在其他角色中。在大多数简单的 playbooks 中,这并不重要,因为角色通常是一个接一个地运行的。但是当 playbook 结构变得更加复杂时,记住这一点是值得的;否则,由于在不同的角色中设置变量可能会导致意外行为!

这些变量通常作为角色默认值提供,即它们旨在为变量提供默认值,但在应用角色时可以轻松覆盖。当引用角色时,可以同时提供变量数据,无论是覆盖角色默认值还是创建全新的数据。我们将在后面的章节中更深入地介绍角色。这些变量适用于执行角色的所有主机,并且可以直接访问,就像主机自己的hostvars一样。

第三种主要的变量类型是play 变量。这些变量在 play 的控制键中定义,可以直接通过vars键或通过vars_files键从外部文件获取。此外,play 可以通过vars_prompt与用户交互地提示变量数据。这些变量应在 play 的范围内使用,并在 play 的任何任务或包含的任务中使用。这些变量适用于 play 中的所有主机,并且可以像hostvars一样被引用。

第四种变量类型是任务变量。任务变量是由执行任务或在 play 的事实收集阶段发现的数据制成的。这些变量是特定于主机的,并添加到主机的hostvars中,并且可以像这样使用,这也意味着它们在发现或定义它们的点之后具有全局范围。可以通过gather_facts事实模块(即不改变状态而是返回数据的模块)发现这种类型的变量,通过register任务键从任务返回数据中填充,或者由使用set_factadd_host模块的任务直接定义。还可以通过使用pause模块的prompt参数与操作员交互地获取数据并注册结果:

- name: get the operators name 
  ansible.builtin.pause: 
    prompt: "Please enter your name" 
  register: opname 

额外变量,或者extra-vars类型,是在执行ansible-playbook时通过--extra-vars命令行提供的变量。变量数据可以作为key=value对的列表,一个带引号的 JSON 数据,或者一个包含在变量数据中定义的 YAML 格式文件的引用:

--extra-vars "foo=bar owner=fred" 
--extra-vars '{"services":["nova-api","nova-conductor"]}' 
--extra-vars @/path/to/data.yaml 

额外变量被认为是全局变量。它们适用于每个主机,并在整个 playbook 中具有范围。

魔术变量

除了前面列出的变量类型,Ansible 还提供了一组值得特别提及的变量 - 魔术变量。这些变量在运行 playbook 时始终设置,无需显式创建。它们的名称始终保留,不应用于其他变量。

魔术变量用于向 playbooks 本身提供有关当前 playbook 运行的信息,并且在 Ansible 环境变得更大更复杂时非常有用。例如,如果您的 play 需要有关当前主机属于哪些组的信息,group_names魔术变量将返回它们的列表。同样,如果您需要使用 Ansible 配置服务的主机名,inventory_hostname魔术变量将返回在清单中定义的当前主机名。一个简单的例子如下:

---
- name: demonstrate magic variables
  hosts: all
  gather_facts: false
  tasks:
    - name: tell us which host we are on
      ansible.builtin.debug:
        var: inventory_hostname
    - name: tell us which groups we are in
      ansible.builtin.debug:
        var: group_names

与 Ansible 项目中的所有内容一样,魔术变量都有很好的文档记录,您可以在官方 Ansible 文档中找到它们的完整列表以及它们包含的内容docs.ansible.com/ansible/latest/reference_appendices/special_variables.html。魔术变量使用的一个实际例子是:例如,从空白模板设置新一组 Linux 服务器的主机名。inventory_hostname魔术变量直接从清单中提供了我们需要的主机名,无需另一个数据源(或者例如连接到CMDB)。类似地,访问groups_names允许我们定义在单个 playbook 中应在给定主机上运行哪些 play - 例如,如果主机在webservers组中,则安装NGINX。通过这种方式,Ansible 代码可以变得更加灵活和高效;因此,这些变量值得特别一提。

访问外部数据

角色变量、play 变量和任务变量的数据也可以来自外部来源。Ansible 提供了一种机制,可以从控制机器(即运行ansible-playbook的机器)访问和评估数据。这种机制称为查找插件,Ansible 附带了许多这样的插件。这些插件可以用于通过读取文件查找或访问数据,在 Ansible 主机上生成并本地存储密码以供以后重用,评估环境变量,从可执行文件或 CSV 文件中导入数据,访问Redisetcd系统中的数据,从模板文件中呈现数据,查询dnstxt记录等。语法如下:

lookup('<plugin_name>', 'plugin_argument') 

例如,要在ansible.builtin.debug任务中使用etcd中的mastery值,执行以下命令:

- name: show data from etcd 
  ansible.builtin.debug:     
    msg: "{{ lookup('etcd', 'mastery') }}" 

查找在引用它们的任务执行时进行评估,这允许动态数据发现。要在多个任务中重用特定查找并在每次重新评估它时,可以使用查找值定义 playbook 变量。每次引用 playbook 变量时,查找将被执行,随时间可能提供不同的值。

变量优先级

正如您在上一节中学到的,有几种主要类型的变量可以在多种位置定义。这引发了一个非常重要的问题:当相同的变量名称在多个位置使用时会发生什么?Ansible 有一个加载变量数据的优先级,因此,它有一个顺序和定义来决定哪个变量会获胜。变量值覆盖是 Ansible 的高级用法,因此在尝试这样的场景之前,完全理解语义是很重要的。

优先级顺序

Ansible 定义了以下优先顺序,靠近列表顶部的优先级最高。请注意,这可能会因版本而变化。实际上,自 Ansible 2.4 发布以来,它已经发生了相当大的变化,因此如果您正在从旧版本的 Ansible 进行升级,值得进行审查:

  1. 额外的vars(来自命令行)总是优先。

  2. ansible.builtin.include参数。

  3. 角色(和ansible.builtin.include_role)参数。

  4. 使用ansible.builtin.set_facts定义的变量,以及使用register任务指令创建的变量。

  5. 在 play 中包含的变量ansible.builtin.include_vars

  6. 任务vars(仅针对特定任务)。

  7. vars(仅适用于块内的任务)。

  8. Role vars(在角色的vars子目录中的main.yml中定义)。

  9. Play vars_files

  10. Play vars_prompt

  11. Play vars

  12. 主机事实(以及ansible.builtin.set_facts的缓存结果)。

  13. host_vars playbook。

  14. host_vars清单。

  15. 清单文件(或脚本)定义的主机vars

  16. group_vars playbook。

  17. group_vars清单。

  18. group_vars/all playbook。

  19. group_vars/all清单。

  20. 清单文件(或脚本)定义的组vars

  21. 角色默认值。

  22. 命令行值(例如,-u REMOTE_USER)。

Ansible 每次发布都会附带一个移植指南,详细说明您需要对代码进行哪些更改,以便它能够继续按预期运行。在升级 Ansible 环境时,审查这些内容非常重要-这些指南可以在docs.ansible.com/ansible/devel/porting_guides/porting_guides.html找到。

变量组优先级排序

先前的优先级排序列表在编写 Ansible playbook 时显然是有帮助的,并且在大多数情况下,很明显变量不应该冲突。例如,var任务显然胜过var play,所有任务和实际上,plays 都是唯一的。同样,清单中的所有主机都是唯一的;因此,清单中也不应该有变量冲突。

然而,有一个例外,即清单组。主机和组之间存在一对多的关系,因此任何给定的主机都可以是一个或多个组的成员。例如,假设以下代码是我们的清单文件:

[frontend]
host1.example.com
host2.example.com
[web:children]
frontend
[web:vars]
http_port=80
secure=true
[proxy]
host1.example.com
[proxy:vars]
http_port=8080
thread_count=10

在这里,我们有两个假想的前端服务器,host1.example.comhost2.example.com,在frontend组中。这两个主机都是web组的children,这意味着它们被分配了清单中的组变量http_port=80host1.example.com也是proxy组的成员,该组具有相同名称的变量,但是不同的赋值:http_port=8080

这两个变量分配都在group_vars清单级别,因此优先顺序并不定义获胜者。那么,在这种情况下会发生什么?

事实上,答案是可预测的和确定的。group_vars的赋值按照组名称的字母顺序进行(如清单排序部分所述),最后加载的组将覆盖所有之前处理的组的变量值。

这意味着来自mastery2的任何竞争变量将胜过其他两个组。然后,来自mastery11组的变量将优先于mastery1组的变量,因此在创建组名称时请注意这一点!

在我们的示例中,当组按字母顺序处理时,webproxy之后。因此,webgroup_vars赋值将胜过任何先前处理的组的赋值。让我们通过这个示例 playbook 运行之前的清单文件来查看行为:

---
- name: group variable priority ordering example play
  hosts: all
  gather_facts: false
  tasks:
    - name: show assigned group variables
      vars:
        msg: |
             http_port:{{ hostvars[inventory_hostname]['http_port'] }}
             thread_count:{{ hostvars[inventory_hostname]['thread_count'] | default("undefined") }}
             secure:{{ hostvars[inventory_hostname]['secure'] }}
       ansible.builtin.debug:
         msg: "{{ msg.split('\n') }}"

让我们尝试运行以下命令:

ansible-playbook -i priority-hosts -c local priorityordering.yaml

我们应该得到以下输出:

图 1.12 - 一个展示变量如何在清单组级别被覆盖的 playbook 运行

图 1.12 - 一个展示变量如何在清单组级别被覆盖的 playbook 运行

如预期的那样,清单中两个主机的http_port变量的赋值都是80。但是,如果不希望出现这种行为怎么办?假设我们希望proxy组的http_port值优先。不得不重新命名组和所有相关引用以更改组的字母数字排序将是痛苦的(尽管这样也可以!)。好消息是,Ansible 2.4 引入了ansible_group_priority组变量,可以用于处理这种情况。如果没有明确设置,此变量默认为1,不会改变清单文件的其余部分。

让我们将其设置如下:

[proxy:vars]
http_port=8080
thread_count=10
ansible_group_priority=10

现在,当我们使用与之前相同的命令运行相同的 playbook 时,请注意http_ort的赋值如何改变,而所有不巧合的变量名称都会像以前一样表现:

图 1.13 - ansible_group_priority 变量对巧合组变量的影响

图 1.13 - ansible_group_priority 变量对巧合组变量的影响

随着清单随基础设施的增长,一定要利用这个功能,优雅地处理组之间的任何变量分配冲突。

合并哈希

在前一节中,我们关注了变量将如何覆盖彼此的优先级。Ansible 的默认行为是,对于变量名的任何覆盖定义将完全掩盖该变量的先前定义。但是,这种行为可以改变一种类型的变量:哈希变量。哈希变量(或者在 Python 术语中称为字典)是一组键和值的数据集。每个键的值可以是不同类型的,并且甚至可以是复杂数据结构的哈希本身。

在一些高级场景中,最好只替换哈希的一部分或添加到现有哈希中,而不是完全替换哈希。要解锁这种能力,需要在 Ansible 的config文件中进行配置更改。配置条目是hash_behavior,它可以取值replacemerge。设置为merge将指示 Ansible 在出现覆盖场景时合并或混合两个哈希的值,而不是假定默认的replace,它将完全用新数据替换旧的变量数据。

让我们通过一个示例来了解这两种行为。我们将从加载了数据的哈希开始,并模拟提供了作为更高优先级变量的哈希的不同值的情况。

这是起始数据:

hash_var: 
  fred: 
    home: Seattle 
    transport: Bicycle 

这是通过include_vars加载的新数据:

hash_var: 
  fred: 
    transport: Bus 

默认行为下,hash_var的新值将如下所示:

hash_var: 
  fred: 
    transport: Bus 

然而,如果我们启用merge行为,我们将得到以下结果:

hash_var: 
  fred: 
    home: Seattle 
    transport: Bus 

在使用merge时,甚至还有更多微妙和未定义的行为,因此强烈建议只在绝对必要时使用此设置 - 默认情况下禁用是有充分理由的!

总结

虽然 Ansible 的设计侧重于简单和易用性,但架构本身非常强大。在本章中,我们涵盖了 Ansible 的关键设计和架构概念,如版本和配置、playbook 解析、模块传输和执行、变量类型和位置以及变量优先级。

您了解到 playbook 包含变量和任务。任务将称为模块的代码片段与参数链接在一起,这些参数可以由变量数据填充。这些组合从提供的清单来源传输到选定的主机。对这些构建块的基本理解是您可以掌握所有 Ansible 事物的平台!

在下一章中,您将详细了解 Ansible 4.3 中的重大新功能,特别是我们在本章中提到的 Ansible 集合和 FQCNs。

问题

  1. 清单对于 Ansible 的重要性是什么?

a)它是 Ansible 配置管理数据库的一部分。

b)它用于审计您的服务器。

c)它告诉 Ansible 在哪些服务器上执行自动化任务。

d)以上都不是。

  1. 在处理频繁变化的基础设施(如公共云部署)时,Ansible 用户必须定期手动更新他们的清单。这是真的还是假的?

a)真 - 这是唯一的方法。

b)假 - 动态清单是为了这个目的而发明的。

  1. 默认情况下,Ansible 按照清单中的顺序处理主机?

a)按字母顺序

b)按字典顺序

c)随机顺序

d)按照它们在清单中出现的顺序

  1. 默认情况下,简单 playbook 中的 Ansible 任务是按照什么顺序执行的?

a)按照它们被写入的顺序,但必须在所有清单主机上完成每个任务,然后才能执行下一个任务。

b)以最优化的顺序。

c)按照它们被写入的顺序,但一次只能在一个清单主机上进行。

d)其他

  1. 哪种变量类型具有最高优先级,可以覆盖所有其他变量来源?

a)清单变量

b)额外变量(来自命令行)

c)角色默认值

d)通过vars_prompt获取变量源

  1. 特殊的 Ansible 变量名称只在运行时存在是什么?

a)特殊变量

b)运行时变量

c)魔术变量

d)用户变量

  1. 如果您想从 playbook 中访问外部数据,您会使用什么?

a)查找插件

b)查找模块

c)查找可执行文件

d)查找角色

  1. 对于大多数非 Windows 主机,Ansible 首选的默认传输机制是什么?

a)REST API

b)RabbitMQ

c)RSH

d)SSH

  1. 清单变量可以用来做什么?

a)在清单中为每个主机或主机组定义唯一数据。

b)声明您的 playbook 变量。

c)为清单主机定义连接参数。

d)都是(a)和(c)。

  1. 如何覆盖系统上的默认 Ansible 配置?

通过在任何位置创建 Ansible 配置文件,并使用ANSIBLE_CFG环境变量指定此位置。

b)通过在当前工作目录中创建名为ansible.cfg的文件。

c)通过在您的主目录中创建一个名为~/.ansible.cfg的文件。

d)以上任何一种。

第二章:从早期的 Ansible 版本迁移

随着Ansible多年来的发展,某些问题已经出现在开发和管理 Ansible 代码库的团队面前。在许多方面,这些问题是 Ansible 自身增长和成功的代价,并且导致需要以稍微不同的方式构建代码。事实上,任何有一点之前版本 Ansible 经验的人都会注意到,我们在本书中提供的示例代码看起来有些不同,还有一个新术语集合

在本章中,我们将详细解释这些变化以及它们是如何产生的。然后,我们将通过一些实际示例带您了解这些变化在现实世界中是如何工作的,最后教会您如何将您可能拥有的任何现有或旧版 playbook 迁移到 Ansible 4.3 及更高版本。

具体来说,在本章中,我们将涵盖以下主题:

  • Ansible 4.3 的变化

  • 从早期的 Ansible 安装升级

  • 从头开始安装 Ansible

  • 什么是 Ansible 集合?

  • 使用ansible-galaxy安装额外的模块

  • 如何将旧版 playbook 迁移到 Ansible 4.3(入门)

技术要求

要按照本章中提供的示例,您需要一台运行Ansible 4.3或更新版本的 Linux 机器。几乎任何 Linux 发行版都可以。对于那些感兴趣的人,本章中提供的所有代码都是在Ubuntu Server 20.04 LTS上测试的,除非另有说明,并且在Ansible 4.3上测试。本章附带的示例代码可以从 GitHub 的以下网址下载:github.com/PacktPublishing/Mastering-Ansible-Fourth-Edition/tree/main/Chapter02。我们将使用我们在第十章中开发的模块,扩展 Ansible,来向您展示如何构建自己的集合,因此确保您有本书附带代码的副本是值得的。

查看以下视频以查看代码实际操作:bit.ly/3DYi0Co

Ansible 4.3 的变化

虽然我们在第一章中提到了这个话题,Ansible 的系统架构和设计,但重要的是我们更深入地了解这些变化,以帮助您充分理解 Ansible 4.3 与之前版本的不同之处。这将帮助您大大提高编写良好 playbook 的能力,并维护和升级您的 Ansible 基础设施——这是掌握 Ansible 4.3 的必要步骤!

首先,稍微了解一下历史。正如我们在前一章中讨论的那样,Ansible 在设计上具有许多优点,这些优点导致了其迅速增长和接受。其中许多优点,比如无代理设计和易于阅读的 YAML 代码,仍然保持不变。事实上,如果您阅读自 2.9 版本以来的 Ansible 发布的更改日志,您会发现自那个版本以来,核心 Ansible 功能几乎没有什么值得注意的变化,而所有的开发工作都集中在另一个领域。

毫无疑问,Ansible 的模块是其最大的优势之一,任何人,从个人贡献者到硬件供应商和云提供商,都可以提交自己的模块,这意味着到 2.9 版本时,Ansible 包含了成千上万个用于各种用途的模块。

这本身对于管理项目的人来说成了一个头疼的问题。比如说一个模块出现了 bug 需要修复,或者有人给现有的模块添加了一个很棒的新功能,可能会很受欢迎。Ansible 发布本身包含了所有的模块,简而言之,它们与 Ansible 本身的发布紧密耦合。这意味着为了发布一个新模块,必须发布一个全新版本的 Ansible 给社区。

结合数百个模块开发人员的问题和拉取请求,管理核心 Ansible 代码库的人确实头疼不已。很明显,虽然这些模块是 Ansible 成功的重要组成部分,但它们也负责在发布周期和代码库管理中引起问题。需要的是一种将模块(或至少是大部分模块)与 Ansible 引擎的发布解耦的方法——我们在本书的第一章中运行的核心 Ansible 运行时的系统架构和设计。

因此,Ansible 内容集合(或简称集合)诞生了。

Ansible 内容集合

虽然我们很快会更深入地研究这些内容,但需要注意的重要概念是,集合是 Ansible 内容的一种包格式,对于本讨论来说,这意味着所有那些成千上万的模块。通过使用集合分发模块,特别是那些由第三方编写和维护的模块,Ansible 团队有效地消除了核心 Ansible 产品的发布与使其对许多人如此有价值的模块之间的耦合。

当你安装了,比如Ansible 2.9.1,你实际上安装了一个给定版本的 Ansible 二进制文件和其他核心代码,以及那个时候提交和批准包含的所有模块。

现在,当我们谈论安装 Ansible 4.3 时,我们实际上指的是:

Ansible 4.3.0 现在是一个包,其中包含(在撰写本文时)85 个模块、插件和其他重要功能的集合,这将让尽可能多的人在他们需要安装更多集合之前开始他们的 Ansible 之旅。简而言之,这是一个入门集合包。

这里重要的是,Ansible 4.3.0 包含任何实际的自动化运行时。如果你孤立地安装了 Ansible 4.3.0,你实际上无法运行 Ansible!幸运的是,这是不可能的,Ansible 4.3.0 依赖于一个当前称为ansible-core的包。这个包包含了 Ansible 语言运行时,以及一小部分核心插件和模块,比如ansible.builtin.debug,我们在第一章中的示例中经常使用的。Ansible 的系统架构和设计

每个 Ansible 包的发布都会依赖于特定版本的 ansible-core,以便它始终与正确的自动化引擎配对。例如,Ansible 4.3.0 依赖于 ansible-core >= 2.11 and < 2.12。

Ansible 已经开始使用语义化版本控制来管理 Ansible 包本身,从 3.0.0 版本开始。对于还没有接触过语义化版本控制的人来说,可以简单地解释如下:

  • Ansible 4.3.0:这是一个新的 Ansible 包的第一个语义化版本发布。

  • Ansible 4.0.1:这个版本(以及所有右边数字变化的所有版本)将只包含向后兼容的错误修复。

  • Ansible 4.1.0:这个版本(以及所有中间数字变化的所有版本)将包含向后兼容的新功能,可能还包括错误修复。

  • Ansible 5.0.0:这将包含破坏向后兼容性的更改,被称为主要发布。

ansible-core 包不采用语义化版本控制,因此预计 Ansible 5.0.0 将依赖于 ansible-core >= 2.12。请注意,这个 ansible-core 的发布,不受语义化版本控制,可能包含破坏向后兼容性的更改,因此在我们掌握的过程中,了解 Ansible 现在的版本化方式的这些细微差别是很重要的。

重要说明

最后,请注意,ansible-core 包在 2.11 版本中从 ansible-base 更名,因此如果您看到对 ansible-base 的引用,请知道它只是 ansible-core 包的旧名称。

所有这些变化都是经过长时间计划和执行的。虽然它们的实施旨在尽可能顺利地为现有的 Ansible 用户提供服务,但需要解决一些问题,首先是您实际上如何安装和升级 Ansible,我们将在下一节中详细讨论。

从早期的 Ansible 安装升级

将 Ansible 拆分为两个相互依赖的包给包维护者带来了一些麻烦。虽然 CentOS 和 RHEL 的包很容易获得,但目前没有 Ansible 4.3.0 或 ansible-core 2.11.1 的当前包。快速查看 CentOS/RHEL 8 的 EPEL 包目录,最新的 Ansible RPM 版本是 2.9.18。官方的 Ansible 安装指南进一步说明:

自 Ansible 2.10 for RHEL 目前不可用,继续使用 Ansible 2.9。

随着包维护者研究各种升级路径和打包技术的利弊,这种情况将随着时间的推移而发生变化,但在撰写本文时,如果您想立即开始使用 Ansible 4.3.0,最简单的方法是使用 Python 打包技术 pip 进行安装。然而,我们所做的并不是升级,而是卸载后重新安装。

卸载 Ansible 3.0 或更早版本

Ansible 包结构的根本变化意味着,如果您的控制节点上安装了 Ansible 3.0 或更早版本(包括任何 2.x 版本),很遗憾,您不能只是升级您的 Ansible 安装。相反,您需要在安装后删除现有的 Ansible 安装。

提示

与卸载任何软件一样,您应该确保备份重要文件,特别是中央 Ansible 配置文件和清单,以防它们在卸载过程中被删除。

删除软件包的方法取决于您的安装方式。例如,如果您在 CentOS 8 上通过 RPM 安装了 Ansible 2.9.18,可以使用以下命令删除它:

sudo dnf remove ansible

同样,在 Ubuntu 上可以运行以下命令:

sudo apt remove ansible

如果您之前使用 pip 安装了 Ansible,可以使用以下命令删除它:

pip uninstall ansible

简而言之,您如何在控制节点上安装 Ansible 3.0(或更早版本)并不重要。即使您使用 pip 安装了它,并且您将使用 pip 安装新版本,您在做任何其他操作之前必须先卸载旧版本。

当有新的 Ansible 版本可用时,建议查看文档,看看升级是否仍然需要卸载。例如,安装 Ansible 4.3 之前需要卸载 Ansible 3.0,部分原因是 ansible-base 包更名为 ansible-core。

一旦您删除了早期版本的 Ansible,您现在可以继续在控制节点上安装新版本,我们将在下一节中介绍。

从头安装 Ansible

如前一节所讨论的,Ansible 4.3 主要是使用一个名为 pip 的 Python 包管理器进行打包和分发的。这可能会随着时间的推移而发生变化,但在撰写本文时,您需要使用的主要安装方法是通过 pip 进行安装。现在,可以说大多数现代 Linux 发行版已经预装了 Python 和 pip。如果因为任何原因你卡住需要安装它,这个过程在官方网站上有详细说明:pip.pypa.io/en/stable/installing/

一旦您安装了 pip,安装 Ansible 的过程就像运行这个命令一样简单,而且美妙的是,这个命令在所有操作系统上都是相同的(尽管请注意,在某些操作系统上,您的pip命令可能被称为pip3,以区分可能共存的 Python 2.7 和 Python 3 版本):

sudo pip install ansible

当然,这个命令有一些变化。例如,我们给出的命令将为系统上的所有用户安装可用的最新版本的 Ansible。

如果您想测试或坚持使用特定版本(也许是为了测试或资格认证目的),您可以使用以下命令强制 pip 安装特定版本:

sudo pip install ansible==4.3.0

这第二个命令将确保在您的系统上为所有用户安装 Ansible 4.3.0,而不管哪个是最新版本。我们还可以进一步进行;要安装 Ansible 但仅适用于您的用户帐户,您可以运行以下命令:

pip install --user ansible

一个特别方便的技巧是,当您开始使用 pip 时,您可以使用 Python 虚拟环境来隔离特定版本的 Python 模块。例如,您可以创建一个用于 Ansible 2.9 的虚拟环境如下:

  1. 使用以下命令在适当的目录中创建虚拟环境:
virtualenv ansible-2.9

这将在运行命令的目录中创建一个新的虚拟环境,环境(及包含它的目录)将被称为ansible-2.9

  1. 激活虚拟环境如下:
source ansible-2.9/bin/activate
  1. 现在您已经准备安装 Ansible 2.9。要安装 Ansible 2.9 的最新版本,我们需要告诉pip安装大于(或等于)2.9 但小于 2.10 的版本,否则它将只安装 Ansible 4.3:
pip install 'ansible>=2.9,<2.10'
  1. 现在,如果您检查您的 Ansible 版本,您应该会发现您正在运行 2.9 的最新次要版本:
ansible --version

使用虚拟环境的缺点是您需要记住每次登录到 Ansible 控制机时运行步骤 2中的source命令。但好处是您可以在一个单独的虚拟环境中重复上述过程,如下所示,使用 Ansible 4.3:

virtualenv ansible-4.3
source ansible-4.3/bin/activate
pip install 'ansible>=4.3,<4.4'
ansible --version

这样做的好处是,您现在可以随意在两个版本的 Ansible 之间切换,只需发出适当环境的适当源命令,然后以通常的方式运行 Ansible。如果您正在从 Ansible 2.9 迁移到 4.3 的过程中,或者有一些尚未能正常工作但您仍然需要的旧代码,这可能特别有用,直到您有时间进行必要的更改。

最后,如果您想要升级您的新安装的 Ansible,您只需要根据您的安装方法发出适当的pip命令。例如,如果您为所有用户安装了 Ansible,您将发出以下命令:

sudo pip install -U ansible

如果您只为您的用户帐户安装了它,命令将类似:

pip install -U ansible

现在,如果您正在虚拟环境中工作,您必须记住先激活环境。一旦完成,您可以像以前一样升级:

source ansible-2.9/bin/activate
pip install -U ansible

请注意,前面的示例将把安装在 Ansible 2.9 环境中的任何内容升级到最新版本,目前是 4.0。另外,需要注意的一点是,正如在前面的部分从早期的 Ansible 安装升级中讨论的那样,这将破坏安装。要升级到最新的次要版本,记住您可以像在此环境中安装 Ansible 时那样指定版本标准:

pip install -U 'ansible>=2.9,<2.10'

当然,您也可以将版本约束应用于任何其他示例。它们的使用方式不仅限于虚拟环境。

希望到目前为止,您应该已经对如何安装 Ansible 4.3 有了相当好的了解,无论是从头开始,还是从早期安装升级。完成这些工作后,是时候我们来看看Ansible 集合了,因为它们是所有这些变化的驱动力。

什么是 Ansible 集合?

Ansible 集合代表了与 Ansible 发布的传统的单片式方法的重大分歧,在某一时刻,与 Ansible 可执行文件一起发布了超过 3600 个模块。可以想象,这使得 Ansible 发布变得难以管理,并且意味着最终用户必须等待完全新的 Ansible 发布才能获得对单个模块的功能更新或错误修复——显然这是一种非常低效的方法。

因此,Ansible 集合诞生了,它们的前提非常简单:它们是一种用于构建、分发和消费多种不同类型的 Ansible 内容的机制。当您首次从 Ansible 2.9 或更早版本迁移时,您对 Ansible 集合的体验将以模块的形式呈现。正如我们在本章前面讨论的那样,我们所说的 Ansible 4.3 实际上是一个包,包含大约 85 个集合……它根本不包含 Ansible 可执行文件!这些集合中的每一个都包含许多不同的模块,有些由社区维护,有些由特定供应商维护。Ansible 4.3 依赖于 ansible-core 2.11.x,该软件包包含了 Ansible 可执行文件和核心的ansible.builtin模块(如debugfilecopy)。

让我们更详细地看一下集合的结构,以便更充分地理解它们的工作方式。每个集合都有一个由两部分组成的名称:命名空间和集合名称。

例如,ansible.builtin集合的命名空间是ansible,集合名称是builtin。同样,在第一章Ansible 的系统架构和设计中,我们安装了一个名为amazon.aws的集合。在这里,amazon是命名空间,aws是集合名称。所有命名空间必须是唯一的,但集合名称可以在命名空间内相同(因此您理论上可以有ansible.builtinamazon.builtin)。

虽然您可以以多种方式使用集合,包括简单地在本地构建和安装它们,或直接从 Git 存储库中构建和安装它们,但集合的中心位置是 Ansible Galaxy,您将在这里找到所有包含在 Ansible 4.3 软件包中的集合,以及更多其他集合。Ansible Galaxy 网站可在galaxy.ansible.com访问,并且有一个命令行工具(我们在第一章中看到过,Ansible 的系统架构和设计)称为ansible-galaxy,可用于与该网站交互(例如,安装集合)。我们将在本章的其余部分广泛使用此工具,因此您将有机会更加熟悉它。

您可以使用 GitHub 凭据登录 Ansible Galaxy 自由创建自己的帐户,当您这样做时,您的命名空间将自动创建为与您的 GitHub 用户名相同。您可以在这里了解更多关于 Ansible Galaxy 命名空间的信息:galaxy.ansible.com/docs/contributing/namespaces.html

现在您已经了解了 Ansible 集合名称是如何创建的,让我们更深入地了解一下集合是如何组合和工作的。

Ansible 集合的结构

理解集合在幕后如何工作的最简单方法是为自己构建一个简单的集合,所以让我们开始吧。与 Ansible 的所有方面一样,开发人员已经为集合制定了一个强大而易于使用的系统,如果您已经有使用 Ansible 角色的经验,您会发现集合的工作方式类似。然而,如果您没有,不用担心;我们将在这里教会您所需了解的一切。

集合由一系列目录组成,每个目录都有一个特殊的名称,旨在容纳特定类型的内容。这些目录中的任何一个都可以是空的;您不必在集合中包含所有类型的内容。实际上,集合中只有一个强制性文件!Ansible 甚至提供了一个工具来帮助您构建一个空的集合,以便开始使用。让我们现在使用它来创建一个新的空集合,以便学习,通过运行以下命令:

ansible-galaxy collection init masterybook.demo 

当您运行此命令时,您应该看到它创建了以下目录树:

masterybook/
|-- demo
    |-- README.md
    |-- docs
    |-- galaxy.yml
    |-- plugins
        |-- README.md
    |-- roles

您可以从前面的目录树中看到,此命令使用我们的 masterybook 命名空间创建了一个顶级目录,然后创建了一个名为 demo 的集合子目录。然后创建了两个文件和三个目录。

其目的如下:

  • README.md:这是集合的 README 文件,应为第一次查看模块代码的任何人提供有用的信息。

  • docs:此目录用于存储集合的一般文档。所有文档都应采用 Markdown 格式,并且不应放在任何子文件夹中。模块和插件仍应使用 Python 文档字符串嵌入其文档,我们将在第十章中学习更多关于此的内容,扩展 Ansible

  • galaxy.yml:这是集合结构中唯一强制性的文件,包含构建集合所需的所有信息,包括版本信息、作者详细信息、许可信息等。之前运行的命令创建的文件是一个完整的模板,其中包含注释以解释每个参数,因此您应该发现很容易浏览并根据您的要求完成它。

  • plugins:此目录应包含您开发的所有 Ansible 插件。模块也应包含在单独的模块/子目录中,您需要在插件文件夹下创建。我们将在第十章中学习有关为 Ansible 创建插件和模块的内容,扩展 Ansible

  • roles:在 Ansible 3.0 之前,Ansible Galaxy 只用于分发角色:可重复使用的 Ansible 代码集,可以轻松地分发和在其他地方使用以解决常见的自动化挑战。我们将在第八章中学习有关角色的所有内容,使用角色组合可重复使用的 Ansible 内容,所以如果您还没有遇到它们,现在不用担心。角色仍然可以使用 Ansible Galaxy 进行分发,但也可以包含在集合中,这在未来可能会成为常态。

除此之外,集合还可以包含以下内容:

  • tests:此目录用于存储与发布之前测试 Ansible 集合相关的文件,并且要包含在顶层 Ansible 包中,集合必须通过 Ansible 测试流程。您不需要在内部使用自己的集合时执行此操作,但是如果您希望将其包含在主要 Ansible 包中,您将需要完成开发过程的这一部分。更多详细信息请参阅:docs.ansible.com/ansible/latest/dev_guide/developing_collections.html#testing-collections

  • meta/runtime.yml:此文件和目录用于指定有关集合的重要元数据,例如所需 ansible-core 包的版本,以及各种命名空间路由和重定向段,以帮助从 Ansible 2.9 及更早版本(其中没有命名空间)迁移到 Ansible 4.3 及更高版本。

  • playbooks:此目录将在将来的 Ansible 版本中得到支持,以包含与集合一起使用的 playbooks,尽管在撰写本文时,官方文档尚不完整。

现在您已经创建并理解了集合目录结构,让我们向其中添加我们自己的模块。完成后,我们将对其进行打包,然后安装到我们的系统上,并在 playbook 中使用它:这是对集合工作原理的完整端到端测试。我们将从《第十章》《扩展 Ansible》中借用模块代码,所以在这个阶段不用担心深入理解这段代码,因为它在那里有完整的解释。完整的代码清单有好几页长,所以我们不会在这本书中重复它。下载本书附带的代码或参考《第十章》《扩展 Ansible》中的代码清单,获取remote_copy.py模块代码。它包含在本书附带的示例代码的Chapter10/example08/library目录中。

plugins/目录中创建一个modules/子目录,并在其中添加remote_copy.py代码。

当您查看了galaxy.yml中的信息后,可以随意在其中添加您自己的姓名和其他细节,然后就完成了!这就是创建您的第一个集合的全部内容。它真的非常简单,一组文件放在一个井然有序的目录结构中。

提示

如本章前面讨论的那样,预期 Ansible 集合遵循语义化版本控制,因此在创建和构建自己的模块时,请务必采用这一点。

您完成的模块目录结构应该是这样的:

masterybook/
|-- demo
    |-- README.md
    |-- docs
    |-- galaxy.yml
    |-- plugins
        |-- modules
            |-- remote_copy.py
        |-- README.md
    |-- roles

当所有文件就位后,就该构建您的集合了。这非常简单,只需切换到与galaxy.yml所在的同一集合顶级目录,并运行以下命令:

cd masterybook/demo
ansible-galaxy collection build

这将创建一个 tarball,其中包含您的集合文件,您现在可以根据需要使用它!您可以立即将其发布到 Ansible Galaxy,但首先,让我们在本地测试一下看看它是否有效。

默认情况下,Ansible 将集合存储在您的家目录下的~/.ansible/collections中。然而,由于我们正在测试刚刚构建的集合,让我们稍微改变一下 Ansible 的行为,并将其安装在本地目录中。

要尝试这个,为一个简单的测试 playbook 创建一个新的空目录,然后创建一个名为collections的目录,用于安装我们新创建的集合:

mkdir collection-test
cd collection-test
mkdir collections

默认情况下,Ansible 不会知道要在这个目录中查找集合,因此我们必须覆盖其默认配置,告诉它在这里查找。在您的目录中,创建一个新的ansible.cfg文件(如果存在,该文件始终被读取并覆盖任何中央配置文件中的设置,例如/etc/ansible/ansible.cfg)。该文件应包含以下内容:

[defaults]
collections_paths=./collections:~/.ansible/collections:/usr/share/ansible/collections

这个配置指令告诉 Ansible 在检查系统上的默认位置之前,先在当前目录下的 collections 子目录中查找。

现在您已经准备好安装之前构建的集合了。假设您是在家目录中构建的,那么安装它的命令如下:

ansible-galaxy collection install ~/masterybook/demo/masterybook-demo-1.0.0.tar.gz -p ./collections

如果您探索本地的collections目录,您应该会发现它现在包含了您之前创建的集合,以及在构建过程中创建的一些额外文件。

最后,让我们创建一个简单的 playbook 来使用我们的模块。作为《第十章》《扩展 Ansible》的一个预告,这个模块在 Ansible 控制的系统上执行一个简单的文件复制,所以让我们在一个公共可写目录(例如/tmp)中创建一个测试文件,并让我们的模块开始复制。考虑以下 playbook 代码:

---
- name: test remote_copy module
  hosts: localhost
  gather_facts: false
  tasks:
  - name: ensure foo
    ansible.builtin.file:
      path: /tmp/rcfoo
      state: touch
  - name: do a remote copy
    masterybook.demo.remote_copy:
      source: /tmp/rcfoo
      dest: /tmp/rcbar

我们的 playbook 中有两个任务。一个使用ansible.builtin集合中的文件模块来创建一个空文件,供我们的模块复制。第二个任务使用我们的新模块,使用完全限定的集合名称来引用它,来复制文件。

你可以以正常方式运行这个 playbook 代码。例如,要对本地机器运行它,运行以下命令:

ansible-playbook -i localhost, -c local collection_test.yml

注意localhost清单项后的逗号。这告诉 Ansible 我们在命令行上列出清单主机,而不必创建本地清单文件-当你测试代码时,这是一个很方便的小技巧!如果一切顺利,你的 playbook 运行应该如图 2.1所示。

图 2.1-运行示例 playbook 对我们的演示集合的输出

图 2.1-运行示例 playbook 对我们的演示集合的输出

恭喜你,你刚刚创建、构建并运行了你的第一个 Ansible 集合!当然,集合通常比这更复杂,并且可能包含许多模块、插件,甚至角色和其他工件,正如前面所述。但是,要开始,这就是你需要知道的全部。

当你对你的集合满意时,你最后的一步很可能是将其发布到 Ansible Galaxy。假设你已经登录到 Ansible Galaxy 并创建了你的命名空间,你只需要导航到你的个人资料首选项页面,然后点击显示 API 密钥按钮,如图 2.2所示:

图 2.2-从 Ansible Galaxy 获取你的 API 密钥

图 2.2-从 Ansible Galaxy 获取你的 API 密钥

然后,你可以将这个 API 密钥输入到ansible-galaxy命令行工具中,以发布你的集合。例如,要发布本章的集合,你可以运行以下命令:

ansible-galaxy collection publish ~/masterybook/demo/masterybook-demo-1.0.0.tar.gz --token=<API key goes here>

这就结束了我们对集合及其构建和使用的介绍。正如我们提到的,有几种安装集合的方法,而且现在 Ansible 模块已经分布在各种集合中。在下一节中,我们将看看如何找到你需要的模块,以及如何在你的自动化代码中安装和引用集合。

使用 ansible-galaxy 安装额外模块

当你使用集合时,大部分时间你不会自己构建它们。在撰写本书时,Ansible Galaxy 上已经有 780 个可用的集合,而在你阅读本书时可能会有更多。尽管如此,作者个人认为,当我们能亲自动手时,我们学得更好,因此,开发我们自己的,尽管简单,集合是我们研究它们是如何组合和引用的绝佳方式。

然而,现在让我们专注于查找并使用 Ansible 上已有的集合,因为这很可能是你大部分时间的关注点。正如我们已经提到的,Ansible 4.3 包括一组集合,让你开始自动化之旅,以及与 ansible-core 包一起包含的ansible.builtin集合。

如果你想查看在你的系统上安装 Ansible 4.3 时安装了哪些集合,只需运行以下命令:

ansible-galaxy collection list

这将返回一个格式为<namespace>.<collection>的所有已安装集合的列表,以及它们的版本号。请记住,集合现在与你安装的 Ansible 版本无关,因此你可以升级它们而不必升级整个 Ansible 安装。我们很快将会看到这一点。作为 Ansible 的一部分安装的所有集合的完整列表也可以在这里找到:docs.ansible.com/ansible/latest/collections/index.html

当您需要特定目的的模块时,值得注意的是,集合通常以名称命名,以便为您提供有关其包含内容的线索。例如,假设您想要使用 Ansible 在亚马逊网络服务中执行一些云配置; 快速浏览集合索引会发现两个可能的候选项:amazon.aws集合和community.aws集合。同样,如果您想要自动化 Cisco IOS 交换机的功能,cisco.ios集合看起来是一个很好的起点。您可以在 Ansible 文档网站上探索每个集合中的模块,或者通过使用ansible-doc命令来探索集合中的模块。例如,要列出cisco.ios集合中包含的所有模块,您可以运行以下命令:

ansible-doc -l cisco.ios

community.*包旨在提供与 Ansible 2.9 中存在的相同功能,自然而然地具有更新的模块和插件版本,从而帮助您在不太痛苦的情况下将 playbook 从早期的 Ansible 版本移植过来。

当然,如果您在 Ansible 4.3 包中找不到所需的内容,您可以简单地转到 Ansible Galaxy 网站找到更多内容。

一旦确定了您在 playbook 开发中需要的集合,就是安装它们的时候了。我们已经在前一节中看到,我们可以直接从磁盘上的本地文件安装集合。在第一章Ansible 的系统架构和设计中,我们运行了以下命令:

ansible-galaxy collection install amazon.aws

这安装了最新版本的amazon.aws集合直接从 Ansible Galaxy。你们中的鹰眼可能会想,“等等,amazon.aws已经作为 Ansible 4.3 包的一部分包含在内了。”的确是这样。然而,Ansible 及其集合的解耦特性意味着我们可以自由安装和升级集合版本,而无需升级 Ansible。的确,当我们运行前面的命令时,它将最新版本的amazon.aws安装在用户本地集合路径(~/.ansible/collections)内,因为这是默认设置。请注意,这与我们在本章前面测试自己的集合时观察到的行为不同,因为我们专门创建了一个 Ansible 配置文件,指定了不同的集合路径。

通过使用ansible-galaxy命令运行另一个集合列表,我们可以找出发生了什么,只是这一次我们只会过滤amazon.aws集合:

ansible-galaxy collection list amazon.aws

输出将类似于这样:

图 2.3 - 列出已安装集合的多个版本

图 2.3 - 列出已安装集合的多个版本

在这里,我们可以看到这个集合的1.3.0版本是与我们的 Ansible 安装一起安装的,但稍后的1.4.0版本安装在我家目录的.ansible/collections文件夹中,在 playbook 引用它并从我的用户帐户运行时,后者优先。请注意,从此系统上的其他用户帐户运行的 playbook 只会看到1.3.0版本的集合,因为这是系统范围内安装的,它们通常不会引用我家目录中的文件夹。

正如您所期望的,您可以在安装集合时指定您想要的版本。如果我想要安装amazon.aws集合的最新开发版本,我可以使用以下命令在本地安装它:

ansible-galaxy collection install amazon.aws:==1.4.2-dev9 --force

--force选项是必需的,因为ansible-galaxy不会覆盖发布版本的集合与开发版本,除非您强制它这样做-这是一个明智的安全预防措施!

除了从本地文件和 Ansible Galaxy 安装集合外,您还可以直接从 Git 存储库安装它们。例如,要安装假设的 GitHub 存储库的stable分支上的最新提交,您可以运行以下命令:

ansible-galaxy collection install git+https://github.com/jamesfreeman959/repo_name.git,stable

这里有许多可能的排列组合,包括访问私有 Git 存储库甚至本地存储库。

所有这些都是安装集合的完全有效的方式。然而,想象一下,您的 playbook 需要十个不同的集合才能成功运行。您最不想做的事情就是每次在新的地方部署自动化代码时都要运行十个不同的ansible-galaxy命令!而且,这很容易失控,不同的主机上可能有不同的集合版本。

幸运的是,Ansible 在这方面也为您着想,requirements.yml文件(在较早版本的 Ansible 中存在,并在集合成为现实之前用于从 Ansible Galaxy 安装角色)可以用于指定要安装的一组集合。

例如,考虑以下requirements.yml文件:

---
collections:
- name: geerlingguy.k8s
- name: geerlingguy.php_roles
  version: 1.0.0

该文件描述了对两个集合的要求。两者的命名空间都是geerlingguy,集合分别称为k8sphp_rolesk8s集合将安装最新的稳定版本,而php_roles集合只会安装1.0.0版本,而不管最新发布版本是什么。

要安装requirements.yml中指定的所有要求,只需运行以下命令:

ansible-galaxy install -r requirements.yml

该命令的输出应该类似于图 2.4

图 2.4 - 使用 requirements.yml 文件安装集合

图 2.4 - 使用 requirements.yml 文件安装集合

从此输出中可以看出,我们在requirements.yml文件中指定的两个集合都已安装到了适当的版本。这是捕获 playbook 的集合要求的一种非常简单而强大的方式,并且可以一次性安装它们所有,同时保留需要的正确版本。

在这个阶段,您应该对 Ansible 4.3 中的重大变化有一个牢固的理解,特别是集合,如何找到适合您自动化需求的正确集合以及如何安装它们(甚至如何创建自己的集合如果需要)。在本章的最后部分,我们将简要介绍如何将您的 playbook 从 2.9 版本及更早版本迁移到 Ansible 4.3。

如何将传统 playbook 迁移到 Ansible 4.3(入门)

没有两个 Ansible playbook(或角色或模板)是相同的,它们的复杂程度从简单到复杂各不相同。然而,它们对于其作者和用户来说都很重要,随着从 Ansible 2.9 到 4.0 的主要变化,本书没有一个关于如何将您的代码迁移到更新的 Ansible 版本的入门就不完整。

在我们深入研究这个主题之前,让我们来看一个例子。在 2015 年关于 Ansible 1.9 版本的第一版书中,出现了一个示例,使用一个小的 Ansible playbook 渲染了一个Jinja2模板。我们将在本书的第六章中学习关于这段代码的更新版本,解锁 Jinja2 模板的力量,但现在让我们看看原始代码。名为demo.j2的模板如下:

setting = {{ setting }} 
{% if feature.enabled %} 
feature = True 
{% else %} 
feature = False 
{% endif %} 
another_setting = {{ another_setting }}

渲染此模板的 playbook 如下所示:

--- 
- name: demo the template 
  hosts: localhost 
  gather_facts: false  
  vars: 
    setting: a_val 
    feature: 
      enabled: true
    another_setting: b_val  
  tasks: 
    - name: pause with render 
      pause: 
        prompt: "{{ lookup('template', 'demo.j2') }}"

这是第一版书中出现的完全相同的代码,它是为 Ansible 1.9 编写的,所以在过渡到 4.3 时发生了很多变化,你可能会原谅认为这段代码永远不会在 Ansible 4.3 上运行。然而,让我们确切地做到这一点。我们将使用以下命令运行此代码:

ansible-playbook -i localhost, -c local template-demo.yaml

在 Ansible 4.3 上运行此命令的输出,使用 ansible-core 2.11.1,看起来像图 2.5

图 2.5 - 在 Ansible 4.3 上运行本书第一版的示例 playbook

图 2.5 - 在 Ansible 4.3 上运行本书第一版的示例 playbook

如果您问为什么这样做,以及为什么要详细介绍集合,当最初为 Ansible 1.9 编写的代码在 4.3 中不经修改仍然有效时,您将得到原谅。Ansible 4.3 是专门编码的,以为用户提供尽可能少痛苦的路径,甚至在 Ansible 2.10 的迁移指南中都明确指出:

您的 playbook 应该继续工作,无需任何更改

只要模块名称保持唯一,这一点就会成立。然而,现在没有任何阻止模块名称冲突的东西——它们现在只需要在自己的集合中保持唯一。因此,例如,我们在前面的 playbook 中使用了pause模块,在 Ansible 4.3 中它的完全限定集合名称FQCN)是ansible.builtin.pause。前面的代码之所以有效是因为我们的集合中没有其他叫做pause的模块。然而,请考虑我们在本章前面创建的masterybook.demo集合。没有任何阻止我们在这里创建一个叫做pause的自己的模块,它做一些完全不同的事情。Ansible 怎么知道选择哪个模块呢?

答案来自 Ansible 本身,它已经编码以搜索构成 Ansible 4.3 包的所有集合;因此,对pause的引用解析为ansible.builtin.pause。它永远不会解析为masterybook.demo.pause(假设我们创建了该模块),因此如果我们想在任务中使用我们的假设模块,我们需要使用 FQCN。

Ansible 在这个话题上的建议是始终在您的代码中使用 FQCN,以确保您从模块名称冲突中永远不会收到意外的结果。但是,如果您想要避免在一组任务中输入大量内容怎么办?例如,如果您不得不重复输入masterybook.demo.remote_copy,那就太多输入了。

答案以 playbook 中在 play 级别定义的新collections:关键字的形式呈现。当我们在本章前面测试我们新构建的集合时,我们使用了 FCQN 来引用它。然而,同样的 playbook 也可以写成以下形式:

---
- name: test remote_copy module
  hosts: localhost
  gather_facts: false
  collections:
    - masterybook.demo
  tasks:
  - name: ensure foo
    ansible.builtin.file:
      path: /tmp/rcfoo
      state: touch
  - name: do a remote copy
    remote_copy:
      source: /tmp/rcfoo
      dest: /tmp/rcbar

请注意collections:关键字在 play 级别上的存在。这本质上为未通过 FQCN 指定的引用创建了一个有序的搜索路径。因此,我们已经指示我们的 play 在搜索包含的命名空间之前,搜索masterybook.demo命名空间的模块、角色和插件。实际上,您可以将ensure foo任务中的模块引用从ansible.builtin.file更改为file,play 仍将按预期工作。collections指令不会覆盖这些内部命名空间搜索路径,它只是在其前面添加命名空间。

值得注意的是,当您开始使用角色(我们将在本书后面介绍),play 中指定的集合搜索路径不会被角色继承,因此它们都需要手动定义。您可以通过在角色中创建一个meta/main.yml文件来为角色定义集合搜索路径,该文件可以包含例如以下内容:

collections:
  - masterybook.demo

此外,重要的是要提到,这些集合搜索路径不会影响您可能在集合中包含的查找、过滤器或测试等项目。例如,如果我们在我们的集合中包含了一个查找,无论playrole中是否出现collections关键字,都需要使用 FQCN 来引用它。最后,请注意,您必须始终像本章前面演示的那样安装您的集合。在您的代码中包含collections关键字并不会导致 Ansible 自动安装或下载这些集合;它只是它们的搜索路径。

总的来说,您可能会发现在整个代码中使用 FQCN 会更容易,但本节的重要教训是,虽然在您的代码中使用 FQCN 是最佳实践,但目前并不是强制的,如果您正在升级到 Ansible 4.3,您不必逐个更新您曾经编写的所有剧本中对模块、插件等的引用。您可以随时进行这样的操作,但最好是这样做。

当然,如果我们回顾自 2.7 版发布以来发生的所有 Ansible 变化,这本书的第三版就是基于这个版本,那么变化是很多的。然而,它们只会影响特定剧本,因为它们涉及特定剧本方面的特定行为,或者某些模块的工作方式。的确,一些模块会因为较新的 Ansible 版本的发布而被弃用和移除,新的模块会被添加进来。

每当您想要升级您的 Ansible 安装时,建议您查看 Ansible 为每个版本发布的移植指南。它们可以在这里找到:docs.ansible.com/ansible/devel/porting_guides/porting_guides.html

至于我们在本章开始时提到的例子,您可能会发现您的代码根本不需要任何修改。然而,最好是计划升级,而不是简单地希望一切顺利,只是碰到一些意外行为,破坏了您的自动化代码。

希望本章关于剧本移植的部分已经向您展示了如何处理在您的剧本中引入集合,并为您提供了一些指引,指出您在升级 Ansible 时应该寻求指导的地方。

总结

自本书上次发布以来,Ansible 已经发生了许多变化,但最显著的变化(预计会影响到阅读本书的每个人)是引入集合来管理模块、角色、插件等,并将它们与 Ansible 的核心版本分离。对 Ansible 代码最明显的变化可能是引入 FQCNs 以及需要安装集合(如果它们不是 Ansible 4.3 包的一部分)。

在本章中,您了解了在 Ansible 中引入集合的原因,以及它们如何影响从您的剧本代码到您安装、维护和升级 Ansible 本身的一切。您了解到集合很容易从头开始构建,甚至了解了如何构建自己的集合,然后看看如何为您的剧本安装和管理集合。最后,您学会了将您的 Ansible 代码从早期版本移植的基础知识。

在下一章中,您将学习如何在使用 Ansible 时保护秘密数据。

问题

  1. 集合可以包含:

a) 角色

b) 模块

c) 插件

d) 以上所有

  1. 集合意味着 Ansible 模块的版本与 Ansible 引擎的版本无关。

a) 真

b) 假

  1. Ansible 4.3 包括:

a) 包括 Ansible 自动化引擎。

b) 依赖于 Ansible 自动化引擎。

c) 与 Ansible 自动化引擎毫无关系。

  1. 可以直接从 Ansible 2.9 升级到 Ansible 4.3。

a) 真

b) 假

  1. 在 Ansible 4.3 中,模块名称在不同的命名空间之间是唯一的。

a) 真

b) 假

  1. 为了确保您始终访问您打算的正确模块,您现在应该开始在您的任务中使用以下哪个?

a) 完全合格的域名

b) 简短的模块名称

c) 完全合格的集合名称

d) 以上都不是

  1. 哪个文件可以用来列出从 Ansible Galaxy 获取的所有所需集合,以确保在需要时可以轻松安装它们?

a) site.yml

b) ansible.cfg

c) collections.yml

d) requirements.yml

  1. 当您在 Ansible Galaxy 上创建帐户以贡献您自己的集合时,您的命名空间是:

a) 随机生成的。

b) 由您选择。

c) 根据你的 GitHub 用户 ID 自动生成。

  1. 集合存储在哪种常见的文件格式中?

a) .tar.gz

b) .zip

c) .rar

d) .rpm

  1. 你如何列出安装在你的 Ansible 包中的所有集合?

a) ansible --list-collections

b) ansible-doc -l

c) ansible-galaxy --list-collections

d) ansible-galaxy collections list

第三章:使用 Ansible 保护您的机密

机密信息是要保密的。无论是云服务的登录凭据还是数据库资源的密码,它们之所以是机密,是有原因的。如果它们落入错误的手中,它们可以被用来发现商业机密、客户的私人数据、为恶意目的创建基础设施,甚至更糟。所有这些都可能会给您和您的组织带来大量的时间、金钱和头疼!在第二版这本书出版时,只能够将敏感数据加密在外部保险柜文件中,并且所有数据必须完全以加密或未加密的形式存在。每次运行 playbook 时只能使用一个单一的 Vault 密码,这意味着无法将您的机密数据分隔开,并为不同敏感性的项目使用不同的密码。现在一切都已经改变,playbook 运行时允许使用多个 Vault 密码,以及在否则普通的YAML Ain't Markup LanguageYAML)文件中嵌入加密字符串的可能性。

在本章中,我们将描述如何利用这些新功能,并通过以下主题保持您的机密安全使用 Ansible:

  • 加密数据在静止状态下

  • 创建和编辑加密文件

  • 使用加密文件执行ansible-playbook

  • 将加密数据与普通 YAML 混合

  • 在操作时保护机密

技术要求

为了跟随本章节中提供的示例,您需要一台运行Ansible 4.3或更新版本的 Linux 机器。几乎任何 Linux 版本都可以使用——对于那些对细节感兴趣的人,本章中提供的所有代码都是在 Ubuntu Server 20.04 长期支持版LTS)上测试的,除非另有说明,并且在 Ansible 4.3 上测试。本章附带的示例代码可以从 GitHub 上下载,统一资源定位符URL)为:github.com/PacktPublishing/Mastering-Ansible-Fourth-Edition/tree/main/Chapter03

查看以下视频以查看代码的实际操作:bit.ly/2Z4xB42

加密数据在静止状态下

作为配置管理系统或编排引擎,Ansible 具有强大的功能。为了发挥这种力量,有必要将机密数据委托给 Ansible。一个每次连接都提示操作员输入密码的自动化系统并不高效——事实上,如果您不得不坐在那里一遍又一遍地输入密码,它几乎不是完全自动化的!为了最大限度地发挥 Ansible 的功能,机密数据必须被写入一个文件,Ansible 可以读取并从中利用数据。

然而,这样做存在风险!您的机密信息以明文形式存储在文件系统中。这是一种物理风险,也是一种数字风险。从物理上讲,计算机可能被夺走,并且被仔细检查以获取机密数据。从数字上讲,任何能够突破其限制的恶意软件都能够读取您的用户帐户可以访问的任何数据。如果您使用源代码控制系统,那么存储库所在的基础设施同样面临风险。

幸运的是,Ansible 提供了一种保护数据在静止状态下的方法。这种方法就是Vault。这种方法允许对文本文件进行加密,以便它们以加密格式存储在静止状态下。没有密钥或大量的计算能力,数据是无法被破译的,但仍然可以在 Ansible plays 中像未加密数据一样轻松使用。

在处理数据加密时需要学习的关键课程包括以下内容:

  • 有效的加密目标

  • 使用多个密码和保险柜标识符ID)保护不同的数据

  • 创建新的加密文件

  • 加密现有的未加密文件

  • 编辑加密文件

  • 更改文件的加密密码

  • 解密加密文件

  • 在未加密的 YAML 文件中内联加密数据(例如,一个 playbook)

  • 在引用加密文件时运行ansible-playbook

Vault ID 和密码

Ansible 2.4发布之前,一次只能使用一个 Vault 密码。虽然你可以在多个位置存储多个目的的多个密码,但只能使用一个密码。这对于较小的环境显然是可以接受的,但随着 Ansible 的采用增加,对更好和更灵活的安全选项的需求也在增加。例如,我们已经讨论过 Ansible 可以通过清单中的组来管理开发和生产环境。可以预期这些环境将具有不同的安全凭据。同样,你期望核心网络设备具有不同的凭据。事实上,这是一个很好的安全实践。

鉴于此,使用 Vault 仅用一个主密码保护任何秘密似乎是不合理的。Ansible 2.4 引入了 Vault ID 的概念作为解决方案,虽然目前旧的单密码命令仍然有效,但建议在命令行上使用 Vault ID。每个 Vault ID 必须有一个与之关联的单个密码,但多个秘密可以共享相同的 ID。

Ansible Vault 密码可以来自以下三个来源之一:

  • 用户输入的字符串,当需要时 Ansible 会提示输入

  • 一个包含 Vault 密码的纯文本文件(显然,这个文件必须保持安全!)

  • 一个可执行文件,用于获取密码(例如,从凭证管理系统)并将其输出为 Ansible 读取的单行

这三个选项的语法大致相似。如果你只有一个 Vault 凭证,因此不使用 ID(尽管如果你愿意的话,你也可以使用 ID,这是强烈推荐的,因为你可能以后希望添加第二个 Vault ID),那么你将输入以下代码行来运行一个 playbook 并提示输入 Vault 密码:

ansible-playbook --vault-id @prompt playbook.yaml

如果你想从文本文件中获取 Vault 密码,你将运行以下命令:

ansible-playbook --vault-id /path-to/vault-password-text-file playbook.yaml

最后,如果你使用可执行脚本,你将运行以下命令:

ansible-playbook --vault-id /path-to/vault-password-script.py playbook.yaml

如果你正在使用 ID,只需在密码来源前面添加 ID,然后加上@字符——例如,如果你的 vault 的 ID 是prod,那么前面的三个例子变成了以下内容:

ansible-playbook --vault-id prod@prompt playbook.yaml
ansible-playbook --vault-id prod@/path-to/vault-password-text-file playbook.yaml
ansible-playbook --vault-id prod@/path-to/vault-password-script.py playbook.yaml

这些可以组合成一个命令,如下所示:

ansible-playbook --vault-id prod@prompt testing@/path-to/vault-password-text-file playbook.yaml

我们将在本章的其余部分中使用vault-id命令行选项。

Vault 可以加密的内容

Vault 功能可用于加密 Ansible 使用的任何结构化数据。这可以是 Ansible 在操作过程中使用的几乎任何 YAML(或JavaScript 对象表示JSON))文件,甚至是一个未加密的 YAML 文件中的单个变量,例如 playbook 或角色。Ansible 可以处理的加密文件的示例包括以下内容:

  • group_vars/文件

  • host_vars/文件

  • include_vars目标

  • vars_files目标

  • --extra-vars目标

  • 角色变量

  • 角色默认值

  • 任务文件

  • 处理程序文件

  • copy模块的源文件(这些是列表中的一个例外——它们不必是 YAML 格式的)

如果一个文件可以用 YAML 表示并且可以被 Ansible 读取,或者如果一个文件要用copy模块传输,那么它就是 Vault 中加密的有效文件。因为整个文件在休息时都是不可读的,所以在选择要加密的文件时应该小心谨慎。对文件的任何源控制操作都将使用加密内容进行,这将使对文件进行审查变得非常困难。

作为最佳实践,应该尽可能少地加密数据,这甚至可能意味着将一些变量单独移到一个文件中。正是出于这个原因,Ansible 2.3 添加了encrypt_string功能到ansible-vault,允许将单独的秘密内联放置在否则未加密的 YAML 中,从而使用户无需加密整个文件。我们将在本章后面介绍这个功能。

创建和编辑加密文件

要创建新文件,Ansible 提供了一个名为ansible-vault的程序。该程序用于创建和与 Vault 加密文件交互。创建加密文件的子命令是create,您可以通过运行以下命令查看此子命令下可用的选项:

ansible-vault create --help

该命令的输出如下截图所示:

图 3.1 - 创建 Ansible Vault 实例时可用的选项

图 3.1 - 创建 Ansible Vault 实例时可用的选项

要创建新文件,您需要提前知道两件事。第一是ansible-vault将用于加密文件的密码,第二是文件名本身。提供了这些信息后,ansible-vault将启动一个文本编辑器(如在EDITOR环境变量中定义的那样 - 在许多情况下默认为vivim)。保存文件并退出编辑器后,ansible-vault将使用提供的密码作为AES256密码对文件进行加密。

让我们通过几个示例来创建加密文件。首先,我们将创建一个并在提示输入密码时进行操作,然后我们将提供一个password文件,最后,我们将创建一个可执行文件来提供密码。

密码提示

ansible-vault在运行时从用户那里请求密码是开始创建 vault 的最简单方法,因此让我们通过一个简单的示例来创建一个包含我们想要加密的变量的 vault。运行以下命令创建一个新的 vault,并在提示输入密码时:

ansible-vault create --vault-id @prompt secrets.yaml

输出应该类似于这样:

图 3.2 - 在提示输入密码时创建一个新的 Ansible Vault 实例

图 3.2 - 在提示输入密码时创建一个新的 Ansible Vault 实例

输入密码后,我们的编辑器将打开,我们可以将内容放入文件中,如下截图所示:

图 3.3 - 使用 vim 编辑器向新的 Ansible Vault 实例添加内容

图 3.3 - 使用 vim 编辑器向新的 Ansible Vault 实例添加内容

在我的系统上,配置的编辑器是Vim。您的系统可能不同,如果您对默认选择不满意,可以将您喜欢的编辑器设置为EDITOR环境变量的值。

现在,我们保存文件。如果我们尝试使用以下命令读取内容,我们会发现它们实际上是加密的:

cat secrets.yaml

这只是一个小的头部提示,供 Ansible 稍后使用,如下截图所示:

图 3.4 - 显示我们的新 Ansible Vault 实例的内容,这些内容在静止状态下是加密的

图 3.4 - 显示我们的新 Ansible Vault 实例的内容,这些内容在静止状态下是加密的

从标题中可以看出,AES256用于 vault 加密,这意味着只要您在创建 vault 时使用了一个好密码,您的数据就非常安全。

密码文件

要使用带有密码文件的ansible-vault,您首先需要创建这样一个文件。只需将密码回显到文件中即可。完成后,您现在可以在调用ansible-vault创建另一个加密文件时引用此文件。通过运行以下命令来尝试:

echo "my long password" > password_file
ansible-vault create --vault-id ./password_file more_secrets.yaml

这应该看起来像以下截图所示的输出:

图 3.5 - 使用密码文件创建 Ansible Vault 实例

图 3.5 - 使用密码文件创建 Ansible Vault 实例

当你运行上述命令时,你会注意到你没有被要求输入密码 - 这次,保险库的密码是my long password字符串,它已经从password_file的内容中读取。默认编辑器将打开,此时可以像以前一样写入数据。

密码脚本

最后一个例子使用了一个密码脚本。这对于设计一个系统很有用,其中密码可以存储在一个中央系统中,用于存储凭据并与 playbook 树的贡献者共享。每个贡献者可以有自己的密码用于共享凭据存储,从中检索 Vault 密码。我们的例子将会简单得多:只是一个简单的输出到STDOUT,带有一个密码。这个文件将保存为password.sh。现在使用以下内容创建这个文件:

#!/bin/sh
echo "a long password"

为了让 Ansible 使用这个脚本,它必须被标记为可执行 - 对它运行以下命令以使其成为可执行文件:

chmod +x password.sh

最后,您可以通过运行以下命令创建一个使用a long password作为输出的新保险库,这是我们简单脚本的输出:

ansible-vault create --vault-id ./password.sh even_more_secrets.yaml

这个过程的输出应该看起来像这样:

图 3.6 - 使用简单密码脚本创建 Ansible Vault 实例

图 3.6 - 使用简单密码脚本创建 Ansible Vault 实例

自己尝试一下,看看它是如何工作的 - 你应该发现ansible-vault创建了一个使用a long password密码的保险库,正如脚本写入STDOUT的那样。你甚至可以尝试使用以下命令进行编辑:

ansible-vault edit --vault-id @prompt even_more_secrets.yaml

当提示时,现在你应该输入a long password - 然后你就可以成功编辑保险库了!

加密现有文件

之前的例子都涉及使用create子命令创建新的加密文件。但是如果我们想要获取一个已建立的文件并对其进行加密呢?也存在一个子命令来实现这一点。它被命名为encrypt,您可以通过运行以下命令查看此子命令的选项:

ansible-vault encrypt --help

输出将类似于下面截图中显示的内容:

图 3.7 - Ansible Vault encrypt 子命令的可用选项

图 3.7 - Ansible Vault encrypt 子命令的可用选项

create一样,encrypt需要一个password(或密码文件或可执行文件)和要加密的文件的路径。一旦接收到适当的密码,编辑器就会打开,这次我们的原始内容以明文的形式已经对我们可见。

请注意,要加密的文件必须已经存在。

让我们通过加密我们从第一章中得到的现有文件来演示一下,Ansible 的系统架构和设计,名为Chapter01/example09/a_vars_file.yaml。将此文件复制到一个方便的位置,然后使用以下命令对其进行加密:

ansible-vault encrypt --vault-id ./password.sh a_vars_file.yaml

这个过程的输出应该类似于下面截图中显示的内容:

图 3.8 - 使用 Ansible Vault 加密现有变量文件

图 3.8 - 使用 Ansible Vault 加密现有变量文件

在这个例子中,我们可以在调用encrypt之前和之后看到文件内容,在此之后内容确实被加密了。与create子命令不同,encrypt可以操作多个文件,轻松地在一个操作中保护所有重要数据。只需列出要加密的所有文件,用空格分隔。

尝试加密已加密的文件将导致错误。

编辑加密文件

一旦文件被ansible-vault加密,就不能直接编辑。在编辑器中打开文件会显示加密数据。对文件进行任何更改都会损坏文件,Ansible 将无法正确读取内容。我们需要一个子命令,首先解密文件的内容,允许我们编辑这些内容,然后在保存回文件之前加密新内容。这样的子命令存在于edit中,您可以通过运行以下命令查看此子命令的可用选项:

ansible-vault edit --help

输出应该看起来类似于以下截图所示的内容:

图 3.9 – Ansible Vault 编辑子命令的可用选项

图 3.9 – Ansible Vault 编辑子命令的可用选项

正如我们已经看到的,我们的编辑器将以明文打开,我们可以看到我们的内容。所有我们熟悉的vault-id选项都回来了,以及要编辑的文件。因此,我们现在可以使用以下命令编辑刚刚加密的文件:

ansible-vault edit --vault-id ./password.sh a_vars_file.yaml

请注意,ansible-vault使用临时文件作为文件路径打开我们的编辑器。当您保存并退出编辑器时,临时文件将被写入,然后ansible-vault将对其进行加密并将其移动以替换原始文件。以下截图显示了我们以前加密的 vault 的未加密内容可供编辑:

图 3.10 – 编辑我们以前加密的 Ansible Vault

图 3.10 – 编辑我们以前加密的 Ansible Vault

您可以在编辑器窗口中看到的临时文件(…/tmp6ancaxcu.yaml)将在ansible-vault成功加密文件后被删除。

加密文件的密码轮换

随着贡献者的进出,定期更改用于加密您的机密的密码是一个好主意。加密的安全性取决于密码的保护程度。ansible-vault提供了一个rekey子命令,允许我们更改密码,您可以通过运行以下命令探索此子命令的可用选项:

ansible-vault rekey --help

输出应该看起来类似于以下截图所示的内容:

图 3.11 – Ansible Vault 重新生成子命令的可用选项

图 3.11 – Ansible Vault 重新生成子命令的可用选项

rekey子命令的操作方式与edit子命令类似。它接受一个可选的密码、文件或可执行文件,以及一个或多个要重新生成的文件。然后,您需要使用--new-vault-id参数来定义一个新密码(如果需要,还可以定义 ID),同样可以通过提示、文件或可执行文件来定义。让我们通过以下命令重新生成我们的a_vars_file.yaml文件,并将 ID 更改为dev,暂时我们将提示输入新密码,尽管我们知道我们可以使用我们的密码脚本获取原始密码:

ansible-vault rekey --vault-id ./password.sh --new-vault-id dev@prompt a_vars_file.yaml

输出应该看起来类似于以下截图所示的内容:

图 3.12 – 重新生成现有的 Ansible Vault 并同时更改 ID

图 3.12 – 重新生成现有的 Ansible Vault 并同时更改 ID

请记住,所有具有相同 ID的加密文件都需要具有匹配的密码(或密钥)。确保同时重新生成具有相同 ID 的所有文件。

解密加密文件

如果在某个时候,不再需要加密数据文件,ansible-vault提供了一个子命令,可用于删除一个或多个加密文件的加密。这个子命令(令人惊讶地)被命名为decrypt,您可以通过运行以下命令查看此子命令的选项:

ansible-vault decrypt --help

输出应该看起来类似于以下截图所示的内容:

图 3.13 – Ansible Vault 解密子命令的可用选项

图 3.13 – Ansible Vault 解密子命令的可用选项

再次,我们有我们熟悉的--vault-id选项,然后是一个或多个要解密的文件路径。让我们通过运行以下命令解密我们刚刚重新生成的文件:

ansible-vault decrypt --vault-id dev@prompt a_vars_file.yaml

如果成功,你的解密过程应该看起来像以下截图所示:

图 3.14–解密现有保险库

图 3.14–解密现有保险库

在下一节中,我们将看到如何在引用加密文件时执行ansible-playbook

使用加密文件执行 ansible-playbook

为了使用我们的加密内容,我们首先需要告诉ansible-playbook如何访问它可能遇到的任何加密数据。与ansible-vault不同,后者仅用于处理文件加密或解密,ansible-playbook更通用,它不会默认假设它正在处理加密数据。幸运的是,我们在之前示例中熟悉的所有--vault-id参数在ansible-playbook中的工作方式与在ansible-vault中的工作方式完全相同。Ansible 将在 playbook 执行期间将提供的密码和 ID 保存在内存中。

现在让我们创建一个名为show_me.yaml的简单 playbook,它将打印出我们在之前示例中加密的a_vars_file.yaml中变量的值,如下所示:

--- 
- name: show me an encrypted var 
  hosts: localhost 
  gather_facts: false 

  vars_files: 
    - a_vars_file.yaml 

  tasks: 
    - name: print the variable 
      ansible.builtin.debug: 
        var: something 

现在,让我们运行 playbook 并看看会发生什么。注意我们如何以与ansible-vault完全相同的方式使用--vault-id参数;两个工具之间保持连续性,因此你可以应用你在本章早些时候学到的关于使用--vault-id的一切。如果你之前没有完成这一步,请使用以下命令加密你的变量文件:

chmod +x password.sh
ansible-vault encrypt --vault-id dev@./password.sh a_vars_file.yaml

完成后,现在使用以下命令运行 playbook—注意--vault-id参数的存在,与之前类似:

ansible-playbook -i mastery-hosts --vault-id dev@./password.sh showme.yaml

完成后,你的输出应该看起来像以下截图所示:

图 3.15–运行包含加密的 Ansible Vault 实例的简单 playbook

图 3.15–运行包含加密的 Ansible Vault 实例的简单 playbook

正如你所看到的,playbook 成功运行并打印出变量的未加密值,即使我们包含的源变量文件是一个加密的 Ansible Vault 实例。当然,在真正的 playbook 运行中,你不会将秘密值打印到终端上,但这演示了从保险库中访问数据有多么容易。

到目前为止,在我们的所有示例中,我们已经创建了作为外部实体的保险库—这些文件存在于 playbook 之外。然而,将加密的保险库数据添加到一个否则未加密的 playbook 中是可能的,这样可以减少我们需要跟踪和编辑的文件数量。让我们看看在下一节中如何实现这一点。

混合加密数据与普通 YAML

在发布 Ansible 2.3 之前,安全数据必须加密在一个单独的文件中。出于我们之前讨论的原因,希望尽可能少地加密数据。现在通过ansible-vaultencrypt_string子命令可以实现这一点(并且还可以节省作为 playbook 一部分的太多个别文件的需要),它会生成一个加密字符串,可以放入 Ansible YAML 文件中。让我们以以下基本 playbook 作为示例:

---
- name: inline secret variable demonstration
  hosts: localhost
  gather_facts: false
  vars:
    my_secret: secure_password
  tasks:
    - name: print the secure variable
      ansible.builtin.debug:
        var: my_secret

我们可以使用以下命令运行这段代码(尽管不安全!):

ansible-playbook -i mastery-hosts inline.yaml

当这个 playbook 运行时,输出应该类似于以下截图所示:

图 3.16–运行包含敏感数据的未加密 playbook

图 3.16–运行包含敏感数据的未加密 playbook

现在,显然不能像这样留下一个安全密码的明文。因此,我们将使用ansible-vaultencrypt_string子命令对其进行加密。如果您想查看运行此子命令时可用的选项,可以执行以下命令:

ansible-vault encrypt_string --help

该命令的输出应该与下面截图中显示的类似:

图 3.17 – Ansible Vault 的 encrypt_string 子命令的可用选项

图 3.17 – Ansible Vault 的 encrypt_string 子命令的可用选项

因此,如果我们想要为我们的my_secret变量使用test Vault ID 和我们之前为密码创建的password.sh脚本,创建一个加密的文本块,我们将运行以下命令:

chmod +x password.sh
ansible-vault encrypt_string --vault-id test@./password.sh "secure_password" --name my_secret

这些命令的输出将为您提供要包含在现有 playbook 中的加密字符串,下面的截图中显示了一个示例:

图 3.18 – 使用 Ansible Vault 将变量加密为安全字符串

图 3.18 – 使用 Ansible Vault 将变量加密为安全字符串

现在,我们可以将该输出复制粘贴到我们的 playbook 中,确保我们的变量不再是人类可读的,就像下面的截图中演示的那样:

图 3.19 – 在现有的 playbook 中用加密字符串数据替换未加密的变量

图 3.19 – 在现有的 playbook 中用加密字符串数据替换未加密的变量

尽管我们现在直接在我们的 playbook 中嵌入了一个 Ansible Vault 加密的变量,但我们可以像以前一样使用适当的--vault-id运行此 playbook—下面的命令将在这里使用:

ansible-playbook -i mastery-hosts --vault-id test@./password.sh inline.yaml

您将观察到 playbook 正在运行,并且可以访问信息,就像任何其他 vault 数据一样,并且您的输出应该与下面的截图中显示的类似:

图 3.20 – 运行包含加密字符串的 Ansible playbook

图 3.20 – 运行包含加密字符串的 Ansible playbook

您可以看到,当所有数据对世界都是公开的时,playbook 的运行方式与我们第一次测试时完全相同!然而,现在,我们已经成功地将加密数据与一个否则未加密的 YAML playbook 混合在一起,而无需创建单独的 Vault 文件。

在下一节中,我们将更深入地探讨与 Ansible Vault 一起运行 playbook 的一些操作方面。

在操作时保护秘密

在本章的前一节中,我们讨论了如何在文件系统上保护您的秘密。然而,这并不是在操作 Ansible 与秘密时唯一关注的问题。这些秘密数据将用于任务作为模块参数、循环输入或任何其他事情。这可能导致数据传输到远程主机,记录到本地或远程日志文件,甚至显示在屏幕上。本章的这一部分将讨论在操作过程中保护您的秘密的策略。

传输到远程主机的秘密

正如我们在第一章中所学到的,Ansible 的系统架构和设计,Ansible 将模块代码和参数组合起来,并将其写入远程主机上的临时目录。这意味着您的秘密数据通过网络传输,并写入远程文件系统。除非您使用的是安全外壳SSH)或安全套接字层SSL)加密的Windows 远程管理WinRM)之外的连接插件,否则通过网络传输的数据已经加密,防止您的秘密被简单窥视发现。如果您使用的是除 SSH 之外的连接插件,请注意数据在传输时是否加密。强烈建议使用任何未加密的连接方法。

一旦数据传输完成,Ansible 可能会以明文形式将这些数据写入文件系统。如果不使用流水线传输(我们在第一章中了解过,Ansible 的系统架构和设计),或者如果已经指示 Ansible 通过ANSIBLE_KEEP_REMOTE_FILES环境变量保留远程文件,就会发生这种情况。没有流水线传输,Ansible 将模块代码和参数写入一个临时目录,该目录将在执行后立即删除。如果在写出文件和执行之间失去连接,文件将保留在远程文件系统上,直到手动删除。如果明确指示 Ansible 保留远程文件,即使启用了流水线传输,Ansible 也会写入并保留远程文件。在处理高度敏感机密信息时,应谨慎使用这些选项,尽管通常情况下,只有 Ansible 在远程主机上进行身份验证的用户(或通过特权升级成为的用户)应该可以访问剩余的文件。简单地删除远程用户的~/.ansible/tmp/路径中的任何内容就足以清除机密信息。

记录到远程或本地文件的机密信息

当 Ansible 在主机上运行时,它将尝试将操作记录到syslog(如果使用了冗长度级别 3 或更高)。如果这个操作是由具有适当权限的用户执行的,它将导致在主机的syslog文件中出现一条消息。此消息包括模块名称和传递给该命令的参数,其中可能包括您的机密信息。为了防止这种情况发生,存在一个名为no_log的操作和任务键。将no_log设置为true将阻止 Ansible 将操作记录到syslog

Ansible 还可以被指示在本地记录其操作。这可以通过 Ansible 配置文件中的log_path或通过名为ANSIBLE_LOG_PATH的环境变量来控制。默认情况下,日志记录是关闭的,Ansible 只会记录到STDOUT。在config文件中打开日志记录会导致 Ansible 将其活动记录到logpath config设置中定义的文件中。

或者,将ANSIBLE_LOG_PATH变量设置为可以被运行ansible-playbook的用户写入的路径,也会导致 Ansible 将操作记录到该路径。此日志的冗长度与屏幕显示的冗长度相匹配。默认情况下,屏幕上不显示任何变量或返回细节。在冗长度级别为 1(-v)时,返回数据将显示在屏幕上(可能也会显示在本地日志文件中)。将冗长度调到级别 3(-vvv)时,输入参数也可能会显示。由于这可能包括机密信息,因此no_log设置也适用于屏幕显示。让我们以前面显示加密机密信息的示例,并在任务中添加一个no_log键,以防止显示其值,如下所示:

--- 
- name: show me an encrypted var 
  hosts: localhost 
  gather_facts: false 

  vars_files: 
    - a_vars_file.yaml 

  tasks: 
    - name: print the variable 
      ansible.builtin.debug: 
        var: something 
      no_log: true 

我们将以与以前相同的方式执行此操作手册(但增加了冗长度,如使用-v标志指定的那样),通过运行以下命令来执行——如果需要的话,请记得先加密变量文件:

ansible-playbook -i mastery-hosts --vault-id test@./password.sh showme.yaml -v

我们应该看到我们的机密数据受到了保护,即使我们故意尝试使用ansible.builtin.debug打印它,如下面的屏幕截图所示:

图 3.21 – 加密变量文件并运行一个保护敏感数据的操作手册

图 3.21 – 加密变量文件并运行一个保护敏感数据的操作手册

正如您所看到的,Ansible 对自身进行了审查,以防止显示敏感数据。no_log 键可用作指令,用于操作、角色、块或任务。

这就结束了我们对 Ansible Vault 的操作使用的介绍,也结束了对 Ansible Vault 主题的讨论——希望本章对教会您如何在使用 Ansible 进行自动化时保护敏感数据方面是有用的。

总结

在本章中,我们介绍了 Ansible 如何有效且安全地处理敏感数据,利用最新的 Ansible 功能,包括使用不同密码保护不同数据和将加密数据与普通 YAML 混合。我们还展示了这些数据在静止状态下的存储方式以及在使用时如何处理这些数据,只要小心谨慎,Ansible 就可以保护您的秘密。

您学会了如何使用ansible-vault工具来保护敏感数据,包括创建、编辑和修改加密文件以及提供 Vault 密码的各种方法,包括提示用户、从文件获取密码和运行脚本来检索密码。您还学会了如何将加密字符串与普通 YAML 文件混合,以及这如何简化 playbook 布局。最后,您学会了使用 Ansible Vault 的操作方面,从而防止 Ansible 将数据泄漏到远程日志文件或屏幕显示。

在我们的下一章中,我们将探讨如何将 Ansible 的强大功能应用于 Windows 主机,以及如何利用这一功能。

问题

  1. Ansible Vault 使用哪种加密技术在静止状态下加密您的数据?

a) 三重 DES/3DES

b) MD5

c) AES

d) Twofish

  1. Ansible Vault 实例必须始终存在为 playbook 本身的单独文件:

a) 真

b) 假

  1. 在运行 playbook 时,您可以从多个 Ansible Vault 实例中摄取数据:

a) 真

b) 假

  1. 在执行使用 Vault 加密数据的 playbook 时,您可以提供密码:

a) 在 playbook 启动时进行交互

b) 使用仅包含密码的明文文件

c) 使用脚本从另一个来源检索密码

d) 以上所有

  1. 在 playbook 运行期间,Ansible 永远不会将 vault 数据打印到终端:

a) 真

b) 假

  1. 您可以使用以下任务参数防止 Ansible 在 playbook 运行期间无意中将 vault 数据打印到终端:

a) no_print

b) no_vault

c) no_log

  1. 中断的 playbook 运行可能会在远程主机上留下敏感的未加密数据:

a) 真

b) 假

  1. 在运行时用于区分不同 vault(可能具有不同密码)的是什么?

a) Vault 名称

b) Vault ID

c) Vault 标识符

d) 以上都不是

  1. 您可以使用哪个 Ansible 命令编辑现有的加密 vault?

a) ansible-vault vi

b) ansible-vault change

c) ansible-vault update

d) ansible-vault edit

  1. 为什么您可能不希望在 vault 中混合敏感和非敏感数据?

a) 这样做会使得难以运行diff命令并查看版本控制系统VCS)中的更改。

b) 只允许在 Ansible Vault 中放置敏感数据。

c) Ansible Vault 的容量有限。

d) Ansible Vault 使得访问受保护的数据变得困难。

第四章:Ansible 和 Windows-不仅仅适用于 Linux

大量的工作都是在 Linux 操作系统上进行的;事实上,本书的前两版完全围绕在 Linux 中使用 Ansible 展开。然而,大多数环境并不是这样的,至少会有一些微软 Windows 服务器和桌面机器。自本书的第三版出版以来,Ansible 已经进行了大量工作,创建了一个真正强大的跨平台自动化工具,它在 Linux 数据中心和 Windows 数据中心同样得心应手。当然,Windows 和 Linux 主机的操作方式存在根本差异,因此并不奇怪,Ansible 在 Linux 上自动化任务的方式与在 Windows 上自动化任务的方式之间存在一些根本差异。

我们将在本章中介绍这些基础知识,以便为您提供一个坚实的基础,开始使用 Ansible 自动化您的 Windows 任务,具体涵盖以下领域:

  • 在 Windows 上运行 Ansible

  • 为 Ansible 控制设置 Windows 主机

  • 处理 Windows 身份验证和加密

  • 使用 Ansible 自动化 Windows 任务

技术要求

要按照本章介绍的示例,您需要一台运行 Ansible 4.3 或更新版本的 Linux 机器。几乎任何 Linux 的版本都可以;对于那些对细节感兴趣的人,本章中提供的所有代码都是在 Ubuntu Server 20.04 LTS 上测试的,除非另有说明,并且在 Ansible 4.3 上也进行了测试。

在本章中使用 Windows 时,示例代码是在 Windows Server 2019 的 1809 版本、构建 17763.1817 上进行测试和运行的。Windows Store 的屏幕截图是从 Windows 10 Pro 的 20H2 版本、构建 19042.906 中获取的。

本章附带的示例代码可以从 GitHub 的以下网址下载:github.com/PacktPublishing/Mastering-Ansible-Fourth-Edition/tree/main/Chapter04

查看以下视频以查看代码的实际操作:bit.ly/3B2zmvL

在 Windows 上运行 Ansible

如果您浏览 Ansible 的官方安装文档,您会发现针对大多数主流 Linux 变体、Solaris、macOS 和 FreeBSD 的各种说明。然而,您会注意到,没有提到 Windows。这是有充分理由的 - 对于那些对技术细节感兴趣的人来说,Ansible 在其操作中广泛使用 POSIX fork()系统调用,而 Windows 上并不存在这样的调用。POSIX 兼容项目,如备受尊敬的 Cygwin,曾试图在 Windows 上实现fork(),但即使在今天,有时这并不起作用。因此,尽管在 Windows 上有一个可行的 Python 实现,但没有这个重要的系统调用,Ansible 无法在此平台上本地运行。

好消息是,如果您正在运行最新版本的 Windows 10,或 Windows Server 2016 或 2019,由于Windows 子系统WSL),安装和运行 Ansible 现在变得非常容易。现在有两个版本的这项技术,原始的 WSL 发布版(在本书的第三版中有介绍),以及更新的WSL2WSL2目前只在 Windows 10 的 1903 版本(或更高版本)上,构建 18362(或更高版本)上可用。这两项技术允许 Windows 用户在 Windows 之上运行未经修改的 Linux 发行版,而无需虚拟机的复杂性或开销(尽管在幕后,您会发现WSL2是在 Hyper-V 之上运行的,尽管以一种无缝的方式)。因此,这些技术非常适合运行 Ansible,因为它可以轻松安装和运行,并且具有可靠的fork()系统调用的实现。

在我们继续之前,让我们暂停一下看两个重要的点。首先,只有在 Windows 上运行 Ansible 来控制其他机器(运行任何操作系统)时,才需要 WSL 或 WSL2-不需要使用它们来控制 Windows 机器。我们将在本章后面更多地了解这一点。其次,不要让 WSL2 没有 Windows Server 的官方版本阻碍您-如果您有 Windows 堡垒主机,并希望从中运行 Ansible,它在WSL上和WSL2上都可以。在撰写本文时,有关WSL2在 Windows Server 的最新预览版中可用的消息;但是,我预计大多数读者将寻找稳定的、可用于生产的解决方案,因此我们将在本章更多地关注WSL而不是WSL2

官方的 Ansible 安装文档可以在docs.ansible.com/ansible/latest/installation_guide/intro_installation.html找到。

检查您的构建

WSL 仅在特定版本的 Windows 上可用,如下所示:

  • Windows 10-版本 1607(构建 14393)或更高版本:

  • 请注意,如果要通过 Microsoft Store 安装 Linux,则需要构建 16215 或更高版本。

  • 如果您想要使用 WSL2,则需要 Windows 10 的 1903 版本或更高版本(18362 版本或更高版本)。

  • 仅支持 64 位英特尔和 ARM 版本的 Windows 10。

  • Windows Server 2016 版本 1803(构建 16215)或更高版本

  • Windows Server 2019 版本 1709(构建 16237)或更高版本

您可以通过在 PowerShell 中运行以下命令轻松检查您的构建和版本号:

systeminfo | Select-String "^OS Name","^OS Version"

如果您使用的是较早版本的 Windows,仍然可以通过虚拟机或通过 Cygwin 运行 Ansible。但是,这些方法超出了本书的范围。

启用 WSL

验证了您的构建后,启用 WSL 很容易。只需以管理员身份打开 PowerShell 并运行以下命令:

Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux

安装完成后,您将能够选择并安装您喜欢的 Linux 发行版。有很多选择,但是为了运行 Ansible,选择官方 Ansible 安装说明中列出的发行版之一是有意义的,比如 Debian 或 Ubuntu。

在 WSL 下安装 Linux

如果您的 Windows 10 版本足够新,那么安装您喜欢的 Linux 就像打开 Microsoft Store 并搜索它一样简单。例如,搜索Ubuntu,您应该很容易找到。图 4.1显示了在 Windows 10 的 Microsoft Store 中可供下载的 Ubuntu 的最新 LTS 版本:

图 4.1-在 Windows 10 的 Microsoft Store 应用程序中可用的 WSL 和 WSL2 的 Linux 发行版之一

图 4.1-在 Windows 10 的 Microsoft Store 应用程序中可用的 WSL 和 WSL2 的 Linux 发行版之一

要在 WSL 下安装 Ubuntu,只需单击获取按钮,等待安装完成。

如果您使用的是 Windows 10,但是支持的构建早于 16215,或者确实是 Windows Server 2016/2019 的任何支持的构建,那么安装 Linux 就是一个稍微手动的过程。首先,从 Microsoft 下载您喜欢的 Linux 发行版,例如,可以使用以下 PowerShell 命令下载 Ubuntu 20.04:

Invoke-WebRequest -Uri https://aka.ms/wslubuntu2004 -OutFile Ubuntu.appx -UseBasicParsing

下载成功后,解压Ubuntu.appx文件-只要它在系统(引导)驱动器上,通常是C:上的任何位置即可。如果要保持 Linux 发行版的私密性,可以将其解压缩到个人资料目录中的某个位置,否则可以将文件解压缩到系统驱动器的任何位置。例如,以下 PowerShell 命令将解压缩存档到C:\WSL\

Rename-Item Ubuntu.appx Ubuntu.zip 
Expand-Archive Ubuntu.zip C:\WSL\Ubuntu 

完成后,您可以使用以所选发行版命名的可执行文件启动新安装的 Linux 发行版。以我们的 Ubuntu 示例为例,您可以通过资源管理器(或您喜欢的方法)运行以下命令:

C:\WSL\Ubuntu\ubuntu2004.exe

第一次运行新安装的 Linux 发行版时,无论是通过 Microsoft Store 安装还是手动安装,它都会初始化自己。在此过程的一部分中,它将要求您创建一个新用户帐户。请注意,此帐户与您的 Windows 用户名和密码是独立的,因此请务必记住您在此设置的密码!每次通过sudo运行命令时都会需要它,尽管与任何 Linux 发行版一样,如果您愿意,可以通过/etc/sudoers自定义此行为。这在图 4.2中有所示:

图 4.2-WSL Ubuntu 终端在首次运行时的输出

图 4.2-WSL Ubuntu 终端在首次运行时的输出

恭喜!现在您在 WSL 下运行 Linux。从这里开始,您应该按照 Ansible 的标准安装过程进行操作,并且可以像在任何其他 Linux 系统上一样在 Linux 子系统上运行它。

使用 WinRM 为 Ansible 控制设置 Windows 主机

到目前为止,我们已经讨论了从 Windows 本身运行 Ansible。这对于企业环境尤其有帮助,尤其是在那些 Windows 终端用户系统是主流的情况下。但是,实际的自动化任务呢?好消息是,正如已经提到的,使用 Ansible 自动化 Windows 不需要 WSL。Ansible 的一个核心前提是无需代理,这对于 Windows 和 Linux 同样适用。可以合理地假设几乎任何现代 Linux 主机都将启用 SSH 访问,同样,大多数现代 Windows 主机都内置了一个远程管理协议,称为 WinRM。Windows 的狂热追随者将知道,微软在最近的版本中添加了 OpenSSH 客户端和服务器包,并且自本书上一版出版以来,已经为 Ansible 添加了对这些的实验性支持。出于安全原因,这两种技术默认情况下都是禁用的,因此,在本书的这一部分中,我们将介绍启用和保护 WinRM 以进行远程管理的过程。我们还将简要介绍在 Windows 上设置和使用 OpenSSH 服务器-然而,由于 Ansible 对此的支持目前是实验性的,并且在未来的版本中可能会有稳定性和向后不兼容的变化,大多数用户将希望使用 WinRM,尤其是在稳定的生产环境中。

有了这个想法,让我们开始看一下如何在本章的下一部分中使用 WinRM 自动化 Windows 主机上的任务。

使用 WinRM 进行 Ansible 自动化的系统要求

Ansible 使用 WinRM 意味着对新旧 Windows 版本有广泛的支持-在幕后,几乎任何支持以下内容的 Windows 版本都可以使用:

  • PowerShell 3.0

  • .NET 4.0

实际上,这意味着只要满足前面的要求,就可以支持以下 Windows 版本:

  • 桌面:Windows 7 SP1,8.1 和 10

  • 服务器:Windows Server 2008 SP2,2008 R2 SP1,2012,2012 R2,2016 和 2019

请注意,以前列出的旧操作系统(如 Windows 7 或 Server 2008)未附带.NET 4.0 或 PowerShell 3.0,并且在使用 Ansible 之前需要安装它们。正如您所期望的那样,支持更新版本的 PowerShell,并且对.NET 4.0 可能有安全补丁。只要满足这些最低要求,您就可以开始使用 Ansible 自动化 Windows 任务,即使在旧操作系统仍然占主导地位的商业环境中也是如此。

如果您使用的是较旧(但受支持的)PowerShell 版本,例如 3.0,请注意在 PowerShell 3.0 下存在一个 WinRM 错误,该错误限制了服务可用的内存,从而可能导致某些 Ansible 命令失败。这可以通过确保在运行 PowerShell 3.0 的所有主机上应用 KB2842230 来解决,因此,如果您正在通过 PowerShell 3.0 自动化 Windows 任务,请务必检查您的热修复和补丁。

启用 WinRM 监听器

一旦满足了先前详细介绍的所有系统要求,剩下的任务就是启用和保护 WinRM 监听器。完成这一步后,我们实际上可以对 Windows 主机本身运行 Ansible 任务!WinRM 可以在 HTTP 和 HTTPS 协议上运行,虽然通过纯 HTTP 快速且容易上手,但这会使您容易受到数据包嗅探器的攻击,并有可能在网络上泄露敏感数据。如果使用基本身份验证,情况尤其如此。默认情况下,也许并不奇怪,Windows 不允许使用 HTTP 或基本身份验证通过 WinRM 进行远程管理。

有时,基本身份验证就足够了(例如在开发环境中),如果要使用它,那么我们肯定希望启用 HTTPS 作为 WinRM 的传输!但是,在本章后面,我们将介绍 Kerberos 身份验证,这是更可取的,并且还可以使用域帐户。不过,为了演示将 Ansible 连接到具有一定安全性的 Windows 主机的过程,我们将使用自签名证书启用 WinRM 的 HTTPS,并启用基本身份验证,以便我们可以使用本地的Administrator帐户进行工作。

要使 WinRM 在 HTTPS 上运行,必须存在具有以下内容的证书:

  • 与主机名匹配的CN

  • 增强密钥用途字段中的服务器身份验证(1.3.6.1.5.5.7.3.1)

理想情况下,这应该由中央证书颁发机构CA)生成,以防止中间人攻击等 - 更多内容稍后再讨论。但是,为了让所有读者都能够测试,我们将生成一个自签名证书作为示例。在 PowerShell 中运行以下命令以生成合适的证书:

New-SelfSignedCertificate -CertStoreLocation Cert:\LocalMachine\My -DnsName "$env:computername" -FriendlyName "WinRM HTTPS Certificate" -NotAfter (Get-Date).AddYears(5)

New-SelfSignedCertificate命令仅在较新版本的 Windows 上可用 - 如果您的系统上没有该命令,请考虑使用 Ansible 提供的自动化 PowerShell 脚本,网址为raw.githubusercontent.com/ansible/ansible/devel/examples/scripts/ConfigureRemotingForAnsible.ps1

这应该产生类似于图 4.3中显示的内容 - 请记下证书的指纹,稍后会用到:

图 4.3 - 使用 PowerShell 为 WinRM HTTPS 监听器创建自签名证书

图 4.3 - 使用 PowerShell 为 WinRM HTTPS 监听器创建自签名证书

有了证书,我们现在可以使用以下命令设置新的 WinRM 监听器:

New-Item -Path WSMan:\Localhost\Listener -Transport HTTPS -Address * -CertificateThumbprint <thumbprint of certificate>

成功后,该命令将在端口5986上设置一个带有我们之前生成的自签名证书的 WinRM HTTPS 监听器。为了使 Ansible 能够通过 WinRM 自动化此 Windows 主机,我们需要执行另外两个步骤 - 在防火墙上打开此端口,并启用基本身份验证,以便我们可以使用本地的Administrator帐户进行测试。使用以下两个命令可以实现这一点:

New-NetFirewallRule -DisplayName "WinRM HTTPS Management" -Profile Domain,Private -Direction Inbound -Action Allow -Protocol TCP -LocalPort 5986
Set-Item -Path "WSMan:\localhost\Service\Auth\Basic" -Value $true

您应该看到与图 4.4中显示的类似的先前命令的输出:

图 4.4 - 在 PowerShell 中创建和启用对 WinRM HTTPS 监听器的访问

图 4.4 - 在 PowerShell 中创建和启用对 WinRM HTTPS 监听器的访问

这些命令已被单独拆分,以便让您了解为 Ansible 连接设置 Windows 主机所涉及的过程。对于自动化部署和系统,如果New-SelfSignedCertificate不可用,可以考虑使用官方 Ansible GitHub 帐户上提供的ConfigureRemotingForAnsible.ps1脚本,我们在本节前面已经提到过。该脚本执行了我们之前完成的所有步骤(以及更多),可以按照以下方式下载并在 PowerShell 中运行:

$url = "https://raw.githubusercontent.com/ansible/ansible/devel/examples/scripts/ConfigureRemotingForAnsible.ps1"
$file = "$env:temp\ConfigureRemotingForAnsible.ps1"
(New-Object -TypeName System.Net.WebClient).DownloadFile($url, $file)
powershell.exe -ExecutionPolicy ByPass -File $file

还有许多其他方法可以为 Ansible 配置 WinRM 所需的配置,包括通过组策略,这在企业环境中几乎肯定更可取。本章节提供的信息现在应该已经为您提供了在您的环境中设置 WinRM 所需的所有基础知识,准备好启用 Ansible 管理您的 Windows 主机。

使用 WinRM 连接 Ansible 到 Windows

一旦配置了 WinRM,让 Ansible 与 Windows 通信就相当简单,只要记住两个注意事项——它期望使用 SSH 协议,如果您没有指定用户账户,它将尝试使用与 Ansible 运行的用户账户相同的用户账户进行连接。这几乎肯定不会与 Windows 用户名一起使用。

此外,请注意,Ansible 需要安装winrm Python 模块才能成功连接。这并不总是默认安装的,因此在开始使用 Windows 主机之前,值得在 Ansible 系统上测试一下。如果不存在,您将看到类似于图 4.5中显示的错误:

图 4.5 - 在 Ubuntu Server 20.04 上测试 winrm Python 模块的存在

图 4.5 - 在 Ubuntu Server 20.04 上测试 winrm Python 模块的存在

如果您看到此错误,您需要在继续之前安装该模块。您的操作系统可能有预打包版本可用,例如,在 Ubuntu Server 20.04 上,您可以使用以下命令安装它:

sudo apt install python3-winrm

如果没有预打包版本可用,可以使用以下命令直接从pip安装。请注意,在第二章中,我们讨论了使用 Python 虚拟环境安装 Ansible - 如果您已经这样做,您必须确保激活您的虚拟环境,然后在不使用sudo的情况下运行以下命令:

sudo pip3 install "pywinrm>=0.3.0"

完成后,我们可以测试之前的 WinRM 配置是否成功。对于基于 SSH 的连接,有一个名为ansible.builtin.ping的 Ansible 模块,它执行完整的端到端测试,以确保连接、成功的身份验证和远程系统上可用的 Python 环境。类似地,还有一个名为win_ping的模块(来自ansible.windows集合),它在 Windows 上执行类似的测试。

在我的测试环境中,我将准备一个清单,以连接到我新配置的 Windows 主机:

[windows]
10.50.0.101
[windows:vars]
ansible_user=Administrator
ansible_password="Password123"
ansible_port=5986
ansible_connection=winrm
ansible_winrm_server_cert_validation=ignore

请注意在 playbook 的windows:vars部分设置的ansible_开头的连接特定变量。在这个阶段,它们应该是相当容易理解的,因为它们在第一章中已经涵盖了 Ansible 的系统架构和设计,但特别要注意ansible_winrm_server_cert_validation变量,当使用自签名证书时需要设置为ignore。显然,在实际示例中,您不会将ansible_password参数以明文形式留下,它要么放在 Ansible vault 中,要么在启动时使用--ask-pass参数提示输入。

基于证书的身份验证也可以在 WinRM 上实现,它带来的好处和风险与基于 SSH 密钥的身份验证几乎相同。

使用先前的清单(根据您的环境进行适当更改,如主机名/IP 地址和身份验证详细信息),我们可以运行以下命令来测试连接:

ansible -i windows-hosts -m ansible.windows.win_ping all

如果一切顺利,您应该会看到类似于图 4.6中显示的输出:

图 4.6 - 使用 Ansible 的 ansible.windows.win_ping 模块测试 WinRM 上的 Windows 主机连接

图 4.6 - 使用 Ansible 的 ansible.windows.win_ping 模块测试 WinRM 上的 Windows 主机连接

这完成了将 Ansible 主机成功设置到 Windows 主机的端到端设置!通过这样的设置,您可以像在任何其他系统上一样编写和运行 playbooks,只是您必须使用专门支持 Windows 的 Ansible 模块。接下来,我们将致力于改进 Ansible 与 Windows 之间连接的安全性,最后转向一些 Windows playbook 的示例。

处理使用 WinRM 时的 Windows 认证和加密

现在我们已经建立了 Ansible 在 Windows 主机上使用 WinRM 执行任务所需的基本连接级别,让我们更深入地了解认证和加密方面的内容。在本章的前部分,我们使用了基本的认证机制与本地账户。虽然这在测试场景中是可以的,但在域环境中会发生什么呢?基本认证只支持本地账户,所以显然我们在这里需要其他东西。我们还选择不验证 SSL 证书(因为它是自签名的),这在测试目的上是可以的,但在生产环境中并不是最佳实践。在本节中,我们将探讨改进 Ansible 与 Windows 通信安全性的选项。

认证机制

事实上,当使用 WinRM 时,Ansible 支持五种不同的 Windows 认证机制,如下所示:

  • 基本:仅支持本地账户

  • 证书:仅支持本地账户,概念上类似于基于 SSH 密钥的认证

  • Kerberos:支持 AD 账户

  • NTLM:支持本地和 AD 账户

  • CredSSP:支持本地和 AD 账户

值得注意的是,Kerberos、NTLM 和 CredSSP 都提供了在 HTTP 上的消息加密,这提高了安全性。然而,我们已经看到了在 HTTPS 上设置 WinRM 有多么容易,而且 WinRM 管理在普通 HTTP 上默认情况下也是不启用的,所以我们将假设通信通道已经被加密。WinRM 是一个 SOAP 协议,意味着它必须在 HTTP 或 HTTPS 等传输层上运行。为了防止远程管理命令在网络上被拦截,最佳实践是确保 WinRM 在 HTTPS 协议上运行。

在这些认证方法中,最让我们感兴趣的是 Kerberos。Kerberos(在本章中)有效地取代了 NTLM,用于 Ansible 对 Active Directory 账户的认证。CredSSP 提供了另一种机制,但在部署之前最好了解与在目标主机上拦截明文登录相关的安全风险,事实上,它默认是禁用的。

在我们继续配置 Kerberos 之前,简要说明一下证书认证。虽然最初这可能看起来很吸引人,因为它实际上是无密码的,但是 Ansible 中的当前依赖关系意味着证书认证的私钥必须在 Ansible 自动化主机上是未加密的。在这方面,将基本或 Kerberos 认证会话的密码放在 Ansible vault 中实际上更安全(更明智)。我们已经介绍了基本认证,所以我们将把精力集中在 Kerberos 上。

由于 Kerberos 认证只支持 Active Directory 账户,因此假定要由 Ansible 控制的 Windows 主机已经加入了域。还假定 WinRM 在 HTTPS 上已经设置好,就像本章前面讨论的那样。

有了这些要求,我们首先要做的是在 Ansible 主机上安装一些与 Kerberos 相关的软件包。确切的软件包将取决于您选择的操作系统,但在 Red Hat Enterprise Linux/CentOS 8 上,它看起来会像这样:

sudo dnf -y install python3-devel krb5-devel krb5-libs krb5-workstation

在 Ubuntu 20.04 上,您需要安装以下软件包:

sudo apt-get install python3-dev libkrb5-dev krb5-user

信息

有关更广泛的操作系统的 Kerberos 支持的软件包要求,请参阅 Ansible 文档中有关 Windows 远程管理的部分:docs.ansible.com/ansible/latest/user_guide/windows_winrm.html

除了这些软件包,我们还需要安装pywinrm[kerberos] Python 模块。可用性会有所不同——在 Red Hat Enterprise Linux/CentOS 8 上,它不作为 RPM 包提供,因此我们需要通过pip进行安装(同样,如果您使用了 Python 虚拟环境,请确保激活它,并且在没有sudo的情况下运行pip3命令):

sudo dnf -y install gcc
sudo pip3 install pywinrm[kerberos]

请注意,pip3需要gcc来构建模块——如果不再需要,之后可以将其删除。

接下来,确保您的 Ansible 服务器可以解析您的 AD 相关的 DNS 条目。这个过程根据操作系统和网络架构会有所不同,但是至关重要的是,您的 Ansible 控制器必须能够解析您的域控制器的名称和其他相关条目,以便本过程的其余部分能够正常工作。

一旦您为 Ansible 控制主机配置了 DNS 设置,接下来,将您的域添加到/etc/krb5.conf。例如,我的测试域是mastery.example.com,我的域控制器是DEMODEM-O5NVEP9.mastery.example.com,所以我的/etc/krb5.conf文件底部看起来是这样的:

[realms]
MASTERY.EXAMPLE.COM = {
 kdc = DEMODEM-O5NVEP9.mastery.example.com
}
[domain_realm]
.mastery.example.com = MASTERY.EXAMPLE.COM

注意大写——这很重要!使用kinit命令测试您的 Kerberos 集成,使用已知的域用户帐户。例如,我将使用以下命令测试我的测试域的集成:

kinit Administrator@MASTERY.EXAMPLE.COM
klist

成功的测试结果应该像图 4.7中所示的那样:

图 4.7 – 在 Ubuntu Ansible 控制主机和 Windows 域控制器之间测试 Kerberos 集成

图 4.7 – 在 Ubuntu Ansible 控制主机和 Windows 域控制器之间测试 Kerberos 集成

最后,让我们创建一个 Windows 主机清单——请注意,它几乎与我们在基本身份验证示例中使用的清单相同;只是这一次,在用户名之后,我们指定了 Kerberos 域:

[windows]
10.0.50.103
[windows:vars]
ansible_user=administrator@MASTERY.EXAMPLE.COM
ansible_password="Password123"
ansible_port=5986
ansible_connection=winrm
ansible_winrm_server_cert_validation=ignore

现在,我们可以像以前一样测试连接:

图 4.8 – 使用 ansible.windows.win_ping 模块进行 Ansible 连接测试和 Kerberos 身份验证

图 4.8 – 使用 ansible.windows.win_ping 模块和 Kerberos 身份验证进行 Ansible 连接测试

成功!前面的结果显示了与 Windows 的成功端到端连接,包括使用 Kerberos 对域帐户进行成功认证,并访问 WinRM 子系统。

关于账户的说明

默认情况下,WinRM 配置为仅允许由给定 Windows 主机上的本地Administrators组的成员进行管理。这不一定是管理员帐户本身——我们在这里使用它仅用于演示目的。可以启用使用权限较低的帐户进行 WinRM 管理,但是它们的使用可能会受到限制,因为大多数 Ansible 命令需要一定程度的特权访问。如果您希望通过 WinRM 为 Ansible 提供一个权限较低的帐户,可以在主机上运行以下命令:

winrm configSDDL default

运行此命令会打开一个 Windows 对话框。使用它来添加并授予(至少)ReadExecute权限给您希望具有 WinRM 远程管理能力的任何用户或组。

通过 WinRM 进行证书验证

到目前为止,我们一直忽略了 WinRM 通信中使用的自签名 SSL 证书——显然,这不是理想的情况,如果 SSL 证书不是自签名的,让 Ansible 验证 SSL 证书是非常简单的。

如果您的 Windows 机器是域成员,最简单的方法是使用Active Directory Certificate ServicesADCS)- 但是,大多数企业将通过 ADCS 或其他第三方服务拥有自己的认证流程。假设为了继续本节,所涉及的 Windows 主机已生成了用于远程管理的证书,并且 CA 证书以 Base64 格式可用。

就像我们之前在 Windows 主机上所做的那样,您需要设置一个 HTTPS 监听器,但这次要使用您的 CA 签名的证书。您可以使用以下命令(如果尚未完成)来执行此操作:

Import-Certificate -FilePath .\certnew.cer -CertStoreLocation Cert:\LocalMachine\My

自然地,将FilePath证书替换为与您自己证书位置匹配的证书。如果需要,您可以使用以下命令删除以前创建的任何 HTTPS WinRM 监听器:

winrm delete winrm/config/Listener?Address=*+Transport=HTTPS

然后,使用导入证书的指纹创建一个新的监听器:

New-Item -Path WSMan:\Localhost\Listener -Transport HTTPS -Address * -CertificateThumbprint <thumbprint of certificate>

现在到 Ansible 控制器。首先要做的是将 WinRM 监听器的 CA 证书导入到操作系统的 CA 捆绑包中。这种方法和位置在不同的操作系统之间会有所不同,但是在 Ubuntu Server 20.04 上,您可以将 Base64 编码的 CA 证书放在/usr/share/ca-certificates/中。请注意,为了被识别,CA 文件必须具有.crt扩展名。

完成此操作后,运行以下命令:

sudo dpkg-reconfigure ca-certificates

在被问及是否要信任新证书颁发机构的证书时选择“是”,并确保在下一个屏幕上呈现的列表中选择您的新证书文件名。

最后,我们需要告诉 Ansible 在哪里找到证书。默认情况下,Ansible 使用 Python Certifi 模块,并且除非我们告诉它否则,否则将使用默认路径。上述过程更新了 CA 捆绑包,位于/etc/ssl/certs/ca-certificates.crt,幸运的是,我们可以在清单文件中告诉 Ansible 在哪里找到它。请注意清单文件中所示的两个进一步更改,首先,我们现在已经指定了 Windows 主机的完整主机名,而不是 IP 地址,因为清单主机名必须与证书上的CN值匹配,以进行完整验证。此外,我们已经删除了ansible_winrm_server_cert_validation行,这意味着现在所有 SSL 证书都会被隐式验证:

[windows]
DEMODEM-O5NVEP9.mastery.example.com
[windows:vars]
ansible_user=administrator@MASTERY.EXAMPLE.COM
ansible_password="Password123"
ansible_port=5986
ansible_connection=winrm
ansible_winrm_ca_trust_path=/etc/ssl/certs/ca-certificates.crt

如果我们再次运行 ping 测试,我们应该会看到SUCCESS,如图 4.9所示:

图 4.9 - 使用 Kerberos 身份验证和 SSL 验证对 Windows 域控制器进行 Ansible ping 测试

图 4.9 - 使用 Kerberos 身份验证和 SSL 验证对 Windows 域控制器进行 Ansible ping 测试

显然,我们可以改进我们的证书生成以消除subjectAltName警告,但目前,这演示了 Ansible 与 Windows 的连接,使用 Kerberos 身份验证连接到域帐户并进行完整的 SSL 验证。这完成了我们对设置 WinRM 的介绍,并应为您提供了在您的基础架构中为 Ansible 设置 Windows 主机所需的所有基础知识。

在本章的下一部分中,我们将看一下在 Windows 上设置新支持的 OpenSSH 服务器,以启用 Ansible 自动化。

使用 OpenSSH 设置 Windows 主机以进行 Ansible 控制

微软在支持和拥抱开源社区方面取得了巨大进展,并向其操作系统添加了许多流行的开源软件包。就 Ansible 自动化而言,最值得注意的是备受推崇和非常受欢迎的 OpenSSH 软件包,它有客户端和服务器两种版本。

在 Ansible 2.8 中添加了使用 SSH 而不是 WinRM 作为传输的 Windows 自动化任务的支持 - 但是,应该注意官方 Ansible 文档中对此支持有许多警告 - 支持被描述为实验性,并且用户被警告未来可能会以不向后兼容的方式进行更改。此外,开发人员预计在继续测试时会发现更多的错误。

出于这些原因,我们已经付出了很多努力来描述使用 WinRM 自动化 Windows 主机与 Ansible。尽管如此,本章没有涉及使用 OpenSSH 为 Windows 启用 Ansible 自动化的内容将不完整。

Windows 上的 OpenSSH 服务器支持 Windows 10 版本 1809 及更高版本,以及 Windows Server 2019。如果您正在运行较旧版本的 Windows,则有两种选择 - 要么继续使用 WinRM 作为通信协议(毕竟,它是内置的,并且一旦您知道如何配置,就很容易),要么手动安装 Win32-OpenSSH 软件包 - 此过程在此处有详细描述,并且应该支持从 Windows 7 开始的任何版本:github.com/PowerShell/Win32-OpenSSH/wiki/Install-Win32-OpenSSH。鉴于该软件包的积极开发,读者被建议在想要在较旧版本的 Windows 上安装 OpenSSH 服务器时参考此文档,因为说明可能在书籍印刷时已经发生了变化。

但是,如果您正在运行较新版本的 Windows,则安装 OpenSSH 服务器就很简单。首先,使用具有管理员权限的 PowerShell 会话,首先使用以下命令查询可用的OpenSSH选项:

Get-WindowsCapability -Online | ? Name -like 'OpenSSH*'

此命令的输出应该与图 4.10中的内容类似:

![图 4.10 - 在 Windows Server 2019 上的 PowerShell 中显示可用的 OpenSSH 安装选项

]

图 4.10 - 在 Windows Server 2019 上的 PowerShell 中显示可用的 OpenSSH 安装选项

使用此输出,运行以下命令安装 OpenSSH 服务器:

Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0

接下来,运行以下命令以确保 SSH 服务器服务在启动时启动,已启动,并且存在适当的防火墙规则以允许 SSH 流量到服务器:

Start-Service sshd
Set-Service -Name sshd -StartupType 'Automatic'
Get-NetFirewallRule -Name *ssh*

如果不存在适当的防火墙规则,您可以使用以下命令添加一个:

New-NetFirewallRule -Name sshd -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22

最后,Windows 的 OpenSSH 服务器默认为cmd。这对于交互式任务来说很好,但是大多数用于 Windows 的本机 Ansible 模块都是为了支持 PowerShell 而编写的 - 您可以通过在 PowerShell 中运行以下命令来更改 OpenSSH 服务器的默认 shell:

New-ItemProperty -Path 'HKLM:\SOFTWARE\OpenSSH' -Name 'DefaultShell' -Value 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe'

完成所有这些任务后,我们最终可以像以前一样测试我们的ansible.windows.win_ping模块。我们的清单文件将与 WinRM 的不同 - 以下内容应该作为您测试的一个合适的示例:

[windows]
DEMODEM-O5NVEP9.mastery.example.com
[windows:vars]
ansible_user=administrator@MASTERY.EXAMPLE.COM
ansible_password="Password123"
ansible_shell_type=powershell

请注意,我们不再关心证书验证或端口号,因为我们正在使用默认端口22上的 SSH。实际上,除了用户名和密码(您可以像我们在本书早期那样轻松地将其指定为ansible命令的命令行参数),唯一需要设置的清单变量是ansible_shell_type,除非我们另行告知,否则它将默认为 Bourne 兼容的 shell。

win_ping模块在测试连接时使用 PowerShell,使我们能够使用先前的临时命令来测试我们新的 SSH 连接到 Windows。只需运行此命令(现在应该看起来很熟悉!):

ansible -i windows-hosts -m ansible.windows.win_ping all

即使我们现在使用了完全不同的通信协议,但是此命令的输出与之前完全相同,并且应该看起来像下面的图 4.11

图 4.11 - 使用 SSH 作为传输机制测试 Windows 上的 Ansible 集成

图 4.11——使用 SSH 作为传输机制测试 Windows 与 Ansible 集成

因此,将 Ansible 与 Windows 主机集成起来真的非常简单——只需确保关注新版本的发布说明和迁移指南,以防某些不兼容的变化。然而,我认为您会同意,使用 OpenSSH 将 Ansible 与 Windows 集成起来也很简单。当然,您可以以类似的方式设置 SSH 密钥认证,就像在任何其他基于 SSH 的主机上一样,以确保您可以在无需用户交互的情况下运行 playbooks。

现在,在通过 WinRM 和 SSH 演示与 Ansible 的 Windows 集成的方面,我们只使用了 Ansible ansible.windows.win_ping模块来测试连接。让我们通过一些简单的示例 playbooks 结束本章,以帮助您开始创建自己的 Windows 自动化解决方案。

使用 Ansible 自动化 Windows 任务

Ansible 4.3 包含的 Windows 模块列表可在以下链接找到,需要注意的是,虽然您可以在 Windows 主机上使用所有熟悉的 Ansible 构造,如varshandlersblocks,但在定义任务时必须使用特定于 Windows 的模块。引入了集合意味着很容易找到它们,ansible.windows集合是一个很好的起点。其中包含了您在 Ansible 2.9 及更早版本中使用的所有特定于 Windows 的模块:https://docs.ansible.com/ansible/latest/collections/index_module.html#ansible-windows。

在本章的这一部分中,我们将运行一些简单的 Windows playbook 示例,以突出编写 Windows playbook 时需要了解的一些内容。

选择正确的模块

如果您要针对 Linux 服务器运行 Ansible,并且想要创建一个目录,然后将文件复制到其中,您将使用ansible.builtin.fileansible.builtin.copy Ansible 模块,playbook 看起来类似于以下内容:

---
- name: Linux file example playbook
  hosts: all
  gather_facts: false
  tasks:
    - name: Create temporary directory
      ansible.builtin.file:
        path: /tmp/mastery
        state: directory
    - name: Copy across a test file
      ansible.builtin.copy:
        src: mastery.txt
        dest: /tmp/mastery/mastery.txt

然而,在 Windows 上,此 playbook 将无法运行,因为ansible.builtin.fileansible.builtin.copy模块与 PowerShell 或 cmd 不兼容,无论您使用 WinRM 还是 SSH 作为与 Windows 机器通信的协议。因此,执行相同任务的等效 playbook 在 Windows 上将如下所示:

---
- name: Windows file example playbook
  hosts: all
  gather_facts: false
  tasks:
    - name: Create temporary directory
      ansible.windows.win_file:
        path: 'C:\Mastery Test'
        state: directory
    - name: Copy across a test file
      ansible.windows.win_copy:
        src: ~/src/mastery/mastery.txt
        dest: 'C:\Mastery Test\mastery.txt'

请注意以下两个 playbook 之间的区别:

  • ansible.windows.win_fileansible.windows.win_copy用于替代ansible.builtin.fileansible.builtin.copy模块。

  • ansible.windows.win_fileansible.windows.win_copy模块的文档中建议在处理远程(Windows 路径)时使用反斜杠(\)。

  • 继续在 Linux 主机上使用正斜杠(/)。

  • 使用单引号(而不是双引号)引用包含空格的路径。

始终重要的是查阅 playbooks 中使用的各个模块的文档。例如,查看ansible.windows.win_copy模块的文档,它建议在进行大文件传输时使用ansible.windows.win_get_url模块,因为 WinRM 传输机制效率不高。当然,如果您使用 OpenSSH 服务器代替 WinRM,则可能不适用——在撰写本文时,该模块的文档尚未更新以考虑这一点。

还要注意,如果文件名包含某些特殊字符(例如方括号),则需要使用 PowerShell 转义字符`进行转义。例如,以下任务将安装c:\temp\setupdownloader_[aaff].exe文件:

  - name: Install package
    win_package:
      path: 'c:\temp\setupdownloader_`[aaff`].exe'
      product_id: {00000000-0000-0000-0000-000000000000}
      arguments: /silent /unattended
      state: present

许多其他 Windows 模块足以满足您的 Windows 剧本需求,结合这些技巧,您将能够快速轻松地获得所需的结果。

安装软件

大多数 Linux 系统(以及其他 Unix 变体)都有一个原生包管理器,使得安装各种软件变得容易。chocolatey包管理器使得 Windows 也能实现这一点,而 Ansible 的chocolatey.chocolatey.win_chocolatey模块使得以无人值守方式使用 Ansible 安装软件变得简单(注意,这不是我们迄今为止使用的ansible.windows集合的一部分,而是存在于其自己的集合中)。

您可以探索chocolatey仓库,并在chocolatey.org了解更多信息。

例如,如果您想在 Windows 机器群中部署 Adobe 的 Acrobat Reader,您可以使用ansible.windows.win_copyansible.windows.win_get_url模块分发安装程序,然后使用ansible.windows.win_package模块进行安装。然而,以下代码将以更少的代码执行相同的任务:

- name: Install Acrobat Reader
  chocolatey.chocolatey.win_chocolatey:
    name: adobereader
    state: present

使用chocolatey.chocolatey.win_chocolatey模块,您可以运行各种巧妙的安装例程——例如,您可以将软件包锁定到特定版本,安装特定架构,以及更多功能——该模块的文档包含了许多有用的示例。官方 Chocolatey 网站本身列出了所有可用的软件包——大多数您期望需要的常见软件包都可以在那里找到,因此它应该满足您将遇到的大多数安装场景。

超越模块

就像在任何平台上一样,可能会遇到所需的确切功能无法从模块获得的情况。虽然编写自定义模块(或修改现有模块)是解决此问题的可行方案,但有时需要更即时的解决方案。为此,ansible.windows.win_commandansible.windows.win_shell模块派上了用场——这些模块可以在 Windows 上运行实际的 PowerShell 命令。官方 Ansible 文档中有许多示例,但以下代码,例如,将使用 PowerShell 创建C:\Mastery目录:

    - name: Create a directory using PowerShell
      ansible.windows.win_shell: New-Item -Path C:\Mastery -ItemType Directory

我们甚至可以为此任务回退到传统的cmd shell:

    - name: Create a directory using cmd.exe
      ansible.windows.win_shell: mkdir C:\MasteryCMD
      args:
        executable: cmd

有了这些提示,应该可以在几乎任何 Windows 环境中创建所需的功能。

通过以上内容,我们结束了对 Windows 自动化与 Ansible 的探讨——只要您记得使用正确的 Windows 原生模块,您就能像对待任何给定的 Linux 主机一样轻松地将本书其余部分应用于 Windows 主机。

总结

Ansible 处理 Windows 主机与 Linux(及其他 Unix)主机同样有效。本章我们介绍了如何从 Windows 主机运行 Ansible,以及如何将 Windows 主机与 Ansible 集成以实现自动化,包括认证机制、加密,甚至 Windows 特定 playbook 的基础知识。

你已经了解到,Ansible 可以在支持 WSL 的最新版 Windows 上运行,并学会了如何实现这一点。你还学会了如何为 Ansible 控制设置 Windows 主机,以及如何通过 Kerberos 认证和加密来确保安全。你也学会了如何设置和使用 Ansible 与 Windows 主机间的新实验性 SSH 通信支持。最后,你学习了编写 Windows playbook 的基础知识,包括找到适用于 Windows 主机的正确模块、转义特殊字符、为主机创建目录和复制文件、安装软件包,甚至使用 Ansible 在 Windows 主机上运行原始 shell 命令。这是一个坚实的基础,你可以在此基础上构建出管理自己 Windows 主机群所需的 Windows playbook。

下一章我们将介绍如何在企业中通过 AWX 有效管理 Ansible。

问题

  1. Ansible 可以通过以下方式与 Windows 主机通信:

    a) SSH

    b) WinRM

    c) 两者皆是

  2. Ansible 可以可靠地在 Windows 上运行:

    a) 原生地

    b) 使用 Python for Windows

    c) 通过 Cygwin

    d) 通过 WSL 或 WSL2

  3. ansible.builtin.file模块可用于在 Linux 和 Windows 主机上操作文件:

    a) True

    b) False

  4. Windows 机器无需初始设置即可运行 Ansible 自动化:

    a) True

    b) False

  5. Windows 的包管理器称为:

    a) Bournville

    b) Cadbury

    c) Chocolatey

    d) RPM

  6. Windows 的 Ansible 模块默认通过以下方式运行命令:

    a) PowerShell

    b) cmd.exe

    c) Bash for Windows

    d) WSL

    e) Cygwin

  7. 即使没有所需功能的模块,你也可以直接运行 Windows 命令:

    a) True

    b) False

  8. 在使用 Ansible 操作 Windows 上的文件和目录时,你应该:

    a) 使用\表示 Windows 路径引用,使用/表示 Linux 主机上的文件

    b) 对所有路径使用/

  9. Windows 文件名中的特殊字符应使用以下方式转义:

    a) \

    b) `

c)"

d)/

  1. 您的 Ansible 剧本必须根据您是使用 WinRM 还是 SSH 通信而进行更改:

a)真

b)假

第五章:使用 AWX 进行企业基础设施管理

可以明显看出,Ansible 是一个非常强大和多功能的自动化工具,非常适合管理整个服务器和网络设备。单调、重复的任务可以变得可重复和简单,节省大量时间!显然,在企业环境中,这是非常有益的。然而,这种力量是有代价的。如果每个人都在自己的机器上有自己的 Ansible 副本,那么你怎么知道谁运行了什么 playbook,以及何时运行的?如何确保所有 playbooks 都被正确存储和进行版本控制?此外,你如何防止超级用户级别的访问凭据在你的组织中泛滥,同时又能从 Ansible 的强大功能中受益?

这些问题的答案以 AWX 的形式呈现,它是一个用于 Ansible 的开源企业管理系统。AWX 是商业 Ansible Tower 软件的开源上游版本,可从 Red Hat 获得,它提供几乎相同的功能和好处,但没有 Red Hat 提供的支持或产品发布周期。AWX 是一个功能强大、功能丰富的产品,不仅包括 GUI,使非 Ansible 用户可以轻松运行 playbooks,还包括完整的 API,可集成到更大的工作流和 CI/CD 流水线中。

在本章中,我们将为您提供安装和使用 AWX 的坚实基础,具体涵盖以下主题:

  • 启动和运行 AWX

  • 将 AWX 与您的第一个 playbook 集成

  • 超越基础知识

技术要求

要遵循本章中提出的示例,您需要一台运行 Ansible 4.3 或更新版本的 Linux 机器。几乎任何 Linux 版本都可以;对于那些对具体细节感兴趣的人,本章中提供的所有代码都是在 Ubuntu Server 20.04 LTS 上测试的,除非另有说明,并且在 Ansible 4.3 上测试。本章附带的示例代码可以从 GitHub 的以下网址下载:github.com/PacktPublishing/Mastering-Ansible-Fourth-Edition/tree/main/Chapter05

查看以下视频,了解来自 Packt 的实际代码演示视频:bit.ly/3ndx73Q

启动和运行 AWX

在我们深入讨论安装 AWX 之前,值得简要探讨一下 AWX 是什么,以及它不是什么。AWX 是一个与 Ansible 并用的工具。它不以任何方式复制或复制 Ansible 的功能。事实上,当从 AWX 运行 Ansible playbooks 时,幕后实际上是调用了ansible-playbook可执行文件。AWX 应被视为一个补充工具,它增加了许多企业所依赖的以下好处:

  • 丰富的基于角色的访问控制(RBAC)

  • 与集中式登录服务(例如 LDAP 或 AD)集成

  • 安全凭据管理

  • 可审计性

  • 问责制

  • 降低新操作员的准入门槛

  • 改进 playbook 版本控制的管理

  • 完整的 API

大部分 AWX 代码在一组 Linux 容器中运行。然而,自上一版书以来,标准安装方法已经改变,现在更倾向于在 Kubernetes 上部署 AWX。如果您已经精通 Kubernetes,您可能希望尝试在自己的环境中部署,因为 AWX 应该可以在 Red Hat 的 OpenShift、开源 OKD 以及许多其他现有的 Kubernetes 版本上运行。

然而,如果您不精通 Kubernetes,或者正在寻找一些入门指南,那么我们将在本章的这一部分为您详细介绍如何从头开始完整安装 AWX。我们将基于出色的microk8s发行版进行,您可以在 Ubuntu Server 上只用一个命令即可在单个节点上启动和运行!

在开始之前,最后一点。尽管 Kubernetes 现在是首选的安装平台,但在撰写本文时,仍然有一个可用于 Docker 主机的安装方法。但是,AWX 项目的维护者指出,这仅针对开发和测试环境,并没有官方发布的版本。因此,我们在本章中不会涵盖这一点。但是,如果您想了解更多,可以阅读以下链接中的安装说明:github.com/ansible/awx/blob/devel/tools/docker-compose/README.md

有了这个,让我们开始我们基于microk8s的部署。这里概述的安装过程假定您从未修改过的 Ubuntu Server 20.04 安装开始。

首先,让我们安装microk8s本身,使用 Ubuntu 提供的snap

sudo snap install microk8s --classic

唯一需要的其他步骤是将您的用户帐户添加到microk8s组中,以便您可以在本节中运行剩余的命令而无需sudo权限:

sudo gpasswd -a $USER microk8s

您需要注销并重新登录,以使组成员身份的更改应用到您的帐户。一旦您这样做了,让我们开始准备microk8s进行 AWX 部署。我们需要storagednsingress插件来进行我们的部署,因此让我们使用以下命令启用它们:

for i in storage dns ingress; do microk8s enable $i; done

现在我们准备安装 AWX Operator,这又用于管理其余的安装。安装这个就像运行以下命令一样简单:

microk8s kubectl apply -f https://raw.githubusercontent.com/ansible/awx-operator/devel/deploy/awx-operator.yaml

该命令将立即返回,而安装将在后台继续进行。您可以使用以下命令检查安装的状态:

microk8s kubectl get pods

STATUS字段应该在 AWX Operator 部署完成后显示Running

重要提示

上一个命令将克隆 AWX Operator 的最新开发版本。如果您想克隆其中一个发布版,请浏览存储库的Releases部分,可在以下链接找到,并检出您想要的版本:github.com/ansible/awx-operator/releases

图 5.1中的屏幕截图显示了成功部署 AWX Operator 后的输出:

图 5.1 - 成功部署 AWX Operator 后的 microk8s pod 状态

图 5.1 - 成功部署 AWX Operator 后的 microk8s pod 状态

接下来,我们将为我们的 AWX 部署创建一个简单的自签名证书。如果您有自己的证书颁发机构,当然可以生成适合您环境的证书。如果您要使用以下命令生成自签名证书,请确保将awx.example.org替换为您为 AWX 服务器分配的主机名:

openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout awx.key -out awx.crt -subj "/CN=awx.example.org/O=mastery" -addext "subjectAltName = DNS:awx.example.org"

我们将在 Kubernetes 中创建一个包含我们新生成的证书的 secret(包含少量敏感数据的对象):

microk8s kubectl create secret tls awx-secret-ssl --namespace default --key awx.key --cert awx.crt

完成后,现在是考虑存储的时候了。AWX 旨在从源代码存储库(如 Git)中获取其 playbooks,并且因此,默认安装不提供对本地 playbook 文件的简单访问。但是,为了在本书中创建一个每个人都可以遵循的工作示例,我们将创建一个持久卷来存储本地 playbooks。创建一个名为my-awx-storage.yml的 YAML 文件,其中包含以下内容:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: awx-pvc
spec:
  accessModes:
    - ReadWriteMany
  storageClassName: microk8s-hostpath
  resources:
    requests:
      storage: 1Gi

运行以下命令,使用我们刚创建的 YAML 文件来创建这个存储:

microk8s kubectl create -f my-awx-storage.yml

现在是部署 AWX 本身的时候了。为此,我们必须创建另一个描述部署的 YAML 文件。我们将称其为my-awx.yml,对于我们的示例,它应该包含以下内容:

apiVersion: awx.ansible.com/v1beta1
kind: AWX
metadata:
  name: awx
spec:
  tower_ingress_type: Ingress
  tower_ingress_tls_secret: awx-secret-ssl
  tower_hostname: awx.example.org
  tower_projects_existing_claim: awx-pvc
  tower_projects_persistence: true

使用以下命令使用此文件部署 AWX:

microk8s kubectl apply -f my-awx.yml

部署将需要几分钟时间,特别是第一次运行时,因为容器映像必须在后台下载。您可以使用以下命令检查状态:

microk8s kubectl get pods

当部署完成时,所有 pod 的STATUS应显示为Running,如图 5.2所示:

图 5.2-成功部署 AWX 后的 Kubernetes pod 状态

图 5.2-成功部署 AWX 后的 Kubernetes pod 状态

当然,如果我们无法访问 AWX,部署 AWX 就只能有限的用途。我们将使用 Microk8s 的入口附加组件创建一个入口路由器,以便我们可以在我们选择的主机名(在本例中为awx.example.org)上访问我们的 AWX 部署,通过标准的 HTTPS 端口。创建另一个 YAML 文件,这次称为my-awx-ingress.yml。它应包含以下内容:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: awx-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /$1
spec:
  tls:
  - hosts:
    - awx.example.org
    secretName: awx-secret-ssl
  rules:
    - host: awx.example.org
      http:
        paths:
          - backend:
              service:
                name: awx-service
                port:
                  number: 80
            path: /
            pathType: Prefix

部署,然后使用以下命令检查此入口定义:

microk8s kubectl apply -f my-awx-ingress.yml
microk8s kubectl describe ingress

如果您没有看到Reason值设置为CREATE的事件,您可能需要删除然后重新部署入口定义,如下所示:

microk8s kubectl delete -f my-awx-ingress.yml
microk8s kubectl apply -f my-awx-ingress.yml

入口规则的成功部署应该看起来像下图所示:

图 5.3-成功部署 AWX 的入口配置

图 5.3-成功部署 AWX 的入口配置

登录到 AWX 的默认用户名是admin。但是,密码是随机生成的并存储在 Kubernetes 的一个秘密中。要检索这个密码以便您第一次登录,请运行以下命令:

microk8s kubectl get secret awx-admin-password -o jsonpath='{.data.password}' | base64 --decode

恭喜!您现在应该能够通过浏览器登录到您之前选择的主机名的 AWX 部署。在本例中,它将是awx.example.org

在第一次运行 AWX 时,许多操作(如构建数据库模式)都是在后台执行的。因此,最初看起来 GUI 没有响应。如果您的 pod 状态看起来健康,请耐心等待,几分钟后您将看到登录屏幕出现,如下图所示:

图 5.4-部署 AWX 后访问登录屏幕

图 5.4-部署 AWX 后访问登录屏幕

当您第一次登录到 AWX 时,您将看到一个仪表板屏幕和左侧的菜单栏。通过这个菜单栏,我们将探索 AWX 并进行我们的第一个配置工作。同样值得注意的是,当首次安装 AWX 时,会填充一些示例内容,以帮助您更快地上手。请随意探索演示内容,因为示例与本书中给出的示例不同。

在我们完成本节之前,考虑一下我们之前创建的用于存储本地 playbooks 的持久卷。我们如何访问它?当使用microk8s的简单单节点部署时,您可以执行一些命令来查询环境并找出文件应该放在哪里。

首先,检索您的hostpath-provisioner pod 的名称。它应该看起来有点像hostpath-provisioner-5c65fbdb4f-jcq8b,可以使用以下命令检索:

microk8s kubectl get pods -A | awk '/hostpath/ {print $2}'

确定了这个唯一的名称后,运行以下命令来发现文件被存储在您的 pod 的本地目录。确保用您系统中的唯一hostpath-provisioner名称替换它:

microk8s kubectl describe -n kube-system pod/hostpath-provisioner-5c65fbdb4f-jcq8b | awk '/PV_DIR/ {print $2}'

最后,使用以下命令检索您的 AWX playbooks 的持久卷索赔的唯一名称:

microk8s kubectl describe pvc/awx-pvc | awk '/Volume:/ {print $2}'

您的最终路径将是这些结果的综合,包括namespace(在本例中为default),以及您的 PVC 名称(在之前的my-awx-storage.yml文件中定义为awx-pvc)。因此,在我的演示系统上,我的本地 playbooks 应放在以下目录下:

/var/snap/microk8s/common/default-storage/default-awx-pvc-pvc-52ea2e69-f3c7-4dd0-abcb-2a1370ca3ac6/

我们将在本章后面将一些简单的示例操作手册放入此目录,因此现在找到它并做个笔记,以便您可以轻松地在以后的示例中访问它。

在 Microk8s 上运行 AWX 后,我们将在下一节中查看如何将我们的第一个操作手册集成并运行在 AWX 中。

将 AWX 与您的第一个操作手册集成

将操作手册集成到 AWX 中涉及基本的四个阶段过程。一旦您理解了这一点,就为更高级的用法和在企业环境中更完整的集成铺平了道路。在本章的这一部分,我们将掌握这四个阶段,以便达到我们可以运行我们的第一个简单操作手册的地步,这将为我们在 AWX 中自信地前进提供基础。这四个阶段如下:

  1. 定义项目。

  2. 定义清单。

  3. 定义凭据。

  4. 定义模板。

前三个阶段可以以任何顺序执行,但最后一个阶段提到的模板将三个先前创建的方面汇集在一起。因此,它必须最后定义。还要注意,这些项目之间不需要一对一的关系。可以从一个项目创建多个模板。清单和凭据也是如此。

在我们开始之前,我们需要一个简单的操作手册,可以在本章的示例中使用。在 AWX 主机上,找到本地 AWX 持久卷文件夹(如果您在 Microk8s 上运行 AWX,则在上一节中有描述)。我将在以下命令中展示我的演示系统的示例,但您的系统将有其自己的唯一 ID。确保您调整路径以适应您的系统-复制和粘贴我的路径几乎肯定不起作用!

每个本地托管的项目必须在持久卷中有自己的子目录,因此让我们在这里创建一个:

cd /var/snap/microk8s/common/default-storage/default-awx-pvc-pvc-64aee7f5-a65d-493d-bdc1-2c33f7da8a4e
mkdir /var/lib/awx/projects/mastery

现在将以下示例代码放入此文件夹中,作为example.yaml

---
- name: AWX example playbook
  hosts: all
  gather_facts: false
  tasks:
    - name: Create temporary directory
      ansible.builtin.file:
        path: /tmp/mastery
        state: directory
    - name: Create a file with example text
      ansible.builtin.lineinfile:
        path: /tmp/mastery/mastery.txt
        line: 'Created with Ansible Mastery!'
        create: yes

完成后,我们可以继续定义项目。

定义项目。

在 AWX 术语中,项目只是一组组合在一起的 Ansible 操作手册。这些操作手册的集合通常来自源代码管理(SCM)系统。事实上,这是在企业中托管 Ansible 操作手册的推荐方式。使用 SCM 意味着每个人都在使用相同版本的代码,并且所有更改都得到跟踪。这些是企业环境中至关重要的元素。

关于操作手册的分组,没有组织项目的正确或错误方式,因此这很大程度上取决于涉及的团队。简单地说,一个项目链接到一个存储库,因此如果多个操作手册存放在一个存储库中是有意义的,它们将存放在 AWX 中的一个项目中。但这不是必需的-如果适合您的需求,您可以每个项目只有一个操作手册!

如前所述,还可以在本地存储 Ansible 操作手册。在测试或刚开始时,这很有用,我们将在这里的示例中利用这种能力,因为它确保了阅读本书的每个人都可以轻松完成示例。

使用admin帐户登录 AWX 界面,然后单击左侧菜单栏上的项目链接。然后单击窗口右上角附近的添加按钮。这为我们创建了一个新的空白项目。

目前,我们不需要担心所有字段(我们将在后面详细讨论这些)。但是,我们需要配置以下内容:

最终结果应该看起来像下图所示:

图 5.5-使用我们的本地操作手册目录在 AWX 中创建您的第一个项目

图 5.5-使用我们的本地操作手册目录在 AWX 中创建您的第一个项目

单击保存按钮以保存您的编辑。就是这样-您已经在 AWX 中定义了您的第一个项目!从这里开始,我们可以定义清单。

定义库存

AWX 中的库存与我们在第一章中使用命令行引用的库存完全相同,Ansible 的系统架构和设计,它们可以是静态的或动态的,可以由组和/或单个主机组成,并且可以在全局每组或每个主机基础上定义变量-我们现在只是通过用户界面定义它们。

单击左侧菜单栏上的库存项。与项目一样,我们想要定义新的内容,因此单击窗口右上方附近的添加按钮。将出现一个下拉列表。从中选择添加库存

创建新库存屏幕出现时,输入库存的名称(例如Mastery Demo),然后单击保存按钮。

重要说明

在定义主机或组之前,您必须保存空白库存。

完成后,您应该看到一个类似于以下图所示的屏幕:

图 5.6-AWX 中创建新的空库存

图 5.6-AWX 中创建新的空库存

保存新库存后,请注意库存子窗格顶部的选项卡-详情访问主机来源作业。您几乎可以在 AWX 用户界面的每个窗格上找到这样的选项卡-我们在本章早些时候定义了第一个项目后也看到了它们(在那个阶段我们只是不需要使用它们)。

为了简化我们的示例,我们将在一个组中定义一个主机,以便运行我们的示例 playbook。单击选项卡,然后单击添加按钮以添加新的库存组。给组命名并单击保存,如下图所示:

图 5.7-在 AWX 中创建新的库存组

图 5.7-在 AWX 中创建新的库存组

现在单击主机选项卡,然后单击添加按钮,并从下拉菜单中选择添加新主机。将您的 AWX 主机的 IP 地址输入到名称字段中(如果您已设置 DNS 解析,则输入 FQDN)。如果需要,您还可以向主机添加描述,然后单击保存。最终结果应该看起来像以下图所示:

图 5.8-在 Mastery Demo 库存的 Mastery Group 组中创建新主机

图 5.8-在 Mastery Demo 库存的 Mastery Group 组中创建新主机

重要说明

大多数库存屏幕上看到的变量框期望以 YAML 或 JSON 格式定义变量,而不是我们在命令行上使用的 INI 格式。在此之前,我们已经定义了变量,例如ansible_ssh_user=james,如果选择了 YAML 模式,我们现在将输入ansible_ssh_user: james

干得好!您刚刚在 AWX 中创建了您的第一个库存。如果我们要在命令行上创建这个库存,它将如下所示:

[MasteryGroup]
10.0.50.25

这可能很简单,但它为我们运行第一个 playbook 铺平了道路。接下来,让我们看看 AWX 中凭据的概念。

定义凭据

AWX 适用于企业的一种方式是安全存储凭据。鉴于 Ansible 的性质和典型用例,通常以 SSH 密钥或具有 root 或其他管理级别特权的密码的形式提供王国的钥匙。即使在保险库中加密,运行 playbook 的用户也将拥有加密密码,因此可以获取凭据。显然,让许多人不受控制地访问管理员凭据可能是不可取的。幸运的是,AWX 解决了这个问题。

让我们举一个简单的例子。假设我的测试主机(我们之前为其定义了库存)的root密码是Mastery123!。我们如何安全地存储这个密码?

首先,导航到凭据菜单项,然后单击添加按钮(就像我们之前所做的那样)来创建新内容。为凭据命名(例如,Mastery Login),然后单击凭据类型下拉菜单以展开可用凭据类型的列表(如果您在此处找不到所需的凭据类型,甚至可以创建自己的凭据类型!)。

您会看到 AWX 可以存储许多不同的凭据类型。对于我们这样的机器登录,我们希望选择Machine类型。设置凭据类型后,您会看到屏幕发生变化,并出现了创建机器凭据所需的字段。我们可以基于 SSH 密钥和其他各种参数定义登录,但在我们的简单示例中,我们将简单地将用户名和密码设置为适当的值,如下图所示:

图 5.9 - 在 AWX 中添加新的机器凭据

图 5.9 - 在 AWX 中添加新的机器凭据

现在,保存凭据。如果您现在返回编辑凭据,您会注意到密码消失了,并被字符串ENCRYPTED替换。现在无法通过 AWX 用户界面直接检索密码(或 SSH 密钥或其他敏感数据)。您会注意到可以替换现有值(通过单击现在变灰的密码字段左侧的卷曲箭头),但无法看到它。获取凭据的唯一方法将是获得与后端数据库的连接以及安装时使用的数据库的加密密钥。这意味着即使执行对数据库本身的SELECT操作,也无法看到密钥,因为包含敏感数据的数据库行都是使用在安装时自动生成的密钥进行加密的。尽管这显然对组织有巨大的安全益处,但也必须指出,后端数据库的丢失或与之关联的加密密钥将导致 AWX 配置的完全丢失。因此,重要的是(与任何基础设施部署一样)备份您的 AWX 部署和相关机密,以防需要从潜在的灾难情况中恢复。

尽管如此,AWX 以一种与 Ansible Vault 并不完全不同的方式保护了您的敏感访问数据。当然,Ansible Vault 仍然是一个命令行工具,尽管在 AWX 中可以像在命令行上使用 Ansible 时一样使用 vault 数据,但 vault 的创建和修改仍然是一个仅限命令行的活动。有了我们的凭据,让我们继续进行运行我们的第一个来自 AWX 的 playbook 所需的最后一步 - 定义一个模板。

定义模板

作业模板 - 给它完整的名称 - 是一种将之前创建的所有配置项以及任何其他所需参数汇集在一起,以针对清单运行给定 playbook 的方式。可以将其视为定义如果在命令行上运行ansible-playbook时的方式。

让我们立即开始创建我们的模板,按照以下步骤进行:

  1. 在左侧菜单中单击模板

  2. 单击添加按钮创建新模板。

  3. 从下拉列表中选择添加作业模板

  4. 要运行我们的第一个作业,您需要在创建新作业模板屏幕上定义以下字段:

这应该会导致一个屏幕,看起来与下图所示的屏幕有些相似:

图 5.10 - 在 AWX 中创建新模板

图 5.10 - 在 AWX 中创建新模板

在所有字段都填充完毕后,如前面的截图所示,点击Save按钮。恭喜!你现在已经准备好从 AWX 运行你的第一个 playbook。要这样做,返回到templates列表,点击我们新创建的模板右侧的小火箭图标。立即执行后,你将看到作业执行并将看到来自ansible-playbook的输出,这是我们从命令行熟悉的,如下图所示:

图 5.11 - 我们在 AWX 中第一个 playbook 模板运行的输出

图 5.11 - 我们在 AWX 中第一个 playbook 模板运行的输出

在这个屏幕上,你可以看到来自ansible-playbook的原始输出。你可以随时通过点击菜单栏上的Jobs菜单项,浏览所有已运行的作业。这对于审计 AWX 一直在协调的各种活动特别有用,尤其是在大型多用户环境中。

Jobs屏幕的顶部,你可以看到Details选项卡,列出了我们之前定义的所有基本参数,比如ProjectTemplate。还显示了有用的审计信息,比如有关作业启动和完成时间的信息。如下图所示:

图 5.12 - 我们的 playbook 模板运行的 Details 选项卡

图 5.12 - 我们的 playbook 模板运行的 Details 选项卡

虽然 AWX 能够做更多的事情,但这些基本阶段对于你想在 AWX 中执行的大多数任务来说是至关重要的。因此,了解它们的用法和顺序对于学习如何使用 AWX 是至关重要的。现在我们已经掌握了基础知识,在下一节中我们将看一下你可以用 AWX 做的一些更高级的事情。

超越基础知识

我们现在已经涵盖了从 AWX 运行你的第一个 playbook 所需的基础知识 - 这是在这个环境中大多数 Ansible 自动化所需的基础知识。当然,我们不可能在一个章节中涵盖 AWX 提供的所有高级功能。因此,在本节中,我们将重点介绍一些更高级的方面,如果你想了解更多关于 AWX 的内容,可以探索。

基于角色的访问控制(RBAC)

到目前为止,我们只从内置的admin用户的角度来看 AWX 的使用。当然,AWX 的企业级功能之一就是 RBAC。这是通过使用用户团队来实现的。团队基本上是一组用户,用户可以是一个或多个团队的成员。

用户和团队都可以在 AWX 用户界面中手动创建,或通过与外部目录服务(如 LDAP 或 Active Directory)集成来创建。在目录集成的情况下,团队很可能会映射到目录中的组,尽管丰富的配置允许管理员定义这种行为的确切性质。

AWX 内的 RBAC 非常丰富。例如,给定用户可以在一个团队中被授予Admin角色,并在另一个团队中被授予MemberRead角色。

用户帐户本身可以设置为系统管理员、普通用户或系统审计员。

除此之外,当我们在本章的基本设置部分进行设置时,你会注意到 AWX 用户界面的几乎每个页面上都有选项卡。其中,几乎总会有一个名为Permissions的选项卡,它允许实现真正的细粒度访问控制。

例如,给定的普通用户类型的用户可以在其分配的团队中被赋予Admin角色。然而,他们可以在给定项目上被分配READ角色,这种更具体的特权将取代在Team级别设置的不太具体的Admin角色。因此,当他们登录时,他们可以看到相关的项目,但不能更改它或执行任何任务 - 例如,来自 SCM 的更新。

重要提示

一般来说,更具体的权限会覆盖不太具体的权限。因此,在项目级别的权限将优先于团队或用户级别的权限。请注意,对于没有通过用户或其团队指定权限的项目,当用户登录到用户界面时,该人甚至都看不到该项目。唯一的例外是系统管理员,他们可以看到一切并执行任何操作。请谨慎将此类型分配给用户账户!

在涉及 RBAC 时有很多可以探索的内容。一旦掌握了它,就可以轻松创建安全且严格锁定的 AWX 部署,每个人都具有适当的访问权限。

组织

AWX 包含一个名为组织的顶级配置项。这是一组清单、项目、作业模板和团队(这些又是用户的分组)。因此,如果企业的两个不同部分具有完全不同的需求,但仍需要使用 AWX,它们可以共享单个 AWX 实例,而无需在用户界面中重叠配置。

虽然系统管理员类型的用户可以访问所有组织,但普通用户只能看到他们关联的组织和配置。这是一种非常强大的方式,可以将企业部署的 AWX 的不同部分的访问权限进行分隔。

举例来说,当我们在本章的前面创建清单时,您可能已经注意到我们忽略了组织字段(这被设置为默认值 - 在新的 AWX 安装中存在的唯一组织)。如果我们要创建一个名为Mastery的新组织,那么不是该组织成员的任何人都无法看到此清单,无论他们拥有的权限或特权如何(唯一的例外是系统管理员用户类型,可以看到一切)。

调度

一些 AWX 配置项,例如项目(可能需要从 SCM 更新)或作业模板(执行特定任务),可能需要定期运行。拥有像 AWX 这样强大的工具,但又需要操作员定期登录执行常规任务,这是没有意义的。因此,AWX 具有内置的调度功能。

在任何项目或模板的定义页面上,只需查找调度选项卡,然后您就可以使用丰富的调度选项 - 图 5.13显示了一个每天运行一次的日程安排示例,从 2021 年 5 月 7 日到 11 日在伦敦时区的下午 1 点。请注意,此日程安排是针对我们之前创建的Mastery Template作业模板创建的,因此将自动按照定义的日程安排运行此 playbook 模板:

图 5.13 - 创建一个每日日程安排来运行之前创建的 Mastery Template 作业模板

图 5.13 - 创建一个每日日程安排来运行之前创建的 Mastery Template 作业模板

请注意,您可以选择多种调度选项。为了帮助您确保日程安排符合您的要求,在保存新日程安排时会显示日程安排的详细信息。当您有多个用户登录到 AWX 等系统并运行无人值守的日程安排时,您可以维护对正在进行的操作的监督是至关重要的。幸运的是,AWX 具有丰富的功能,允许对发生的事件进行审计,我们将在下一节中介绍这些功能。

审计

在命令行上运行 Ansible 的一个风险是,一旦运行了特定任务,其输出将永远丢失。当然,可以为 Ansible 打开日志记录。但是,在企业中,这需要强制执行,对于许多操作员具有给定 Ansible 机器的 root 访问权限,无论是他们自己的笔记本电脑还是其他地方的服务器,这将是困难的。幸运的是,正如我们在之前的示例中看到的,AWX 不仅存储了谁运行了什么任务以及何时运行的详细信息,还存储了所有ansible-playbook运行的输出。通过这种方式,企业希望使用 Ansible 的合规性和可审计性得到了实现。

只需导航到作业菜单项,将显示所有先前运行的作业(用户有权限查看的)。甚至可以直接从此屏幕重复以前完成的作业,只需单击问题中的火箭图标。请注意,这将立即使用与上次启动时相同的参数启动作业,因此请确保单击是您想要执行的操作!

图 5.14显示了我们用于本书的演示 AWX 实例的作业历史:

图 5.14-用于本书的 AWX 实例的作业历史窗格

图 5.14-用于本书的 AWX 实例的作业历史窗格

单击名称列中的编号条目将带您到我们在图 5.11图 5.12中看到的输出详细信息选项卡窗格,但当然,与您单击的特定作业运行相关。虽然您可以清理作业历史记录,但作业仍然保留在那里供您检查,直到您删除它们。还请注意图 5.14顶部的两个灰色按钮。使用这些按钮,您可以取消运行作业(如果由于任何原因它们被卡住或失败),还可以从作业历史记录中删除多个条目。一旦完成审核,这对于清理非常有用。

当然,对于 playbooks,没有一种大小适合所有的解决方案,有时我们需要操作员能够在运行 playbooks 时输入唯一的数据。AWX 提供了一个名为调查的功能,专门用于此目的,我们将在下一节中看到。

调查

有时,在启动作业模板时,不可能(或不希望)预先定义所有信息。虽然在 AWX 用户界面中使用变量定义参数是完全可能的,但这并不总是理想的,或者用户友好的,因为变量必须以有效的 JSON 或 YAML 语法指定。此外,只被授予模板上的“读取”角色的用户将无法编辑该模板定义-这包括变量!然而,他们可能有正当的理由设置一个变量,即使他们不应该编辑模板本身。

调查提供了答案,对于您创建的任何作业模板,您将在顶部找到一个标记为调查的选项卡。调查本质上是由管理员定义的问卷调查(因此得名!),以用户友好的方式要求输入,并进行简单的用户输入验证。一旦验证,输入的值将被存储在 Ansible 变量中,就像它们如果以 YAML 或 JSON 格式定义一样。

例如,如果我们想要在运行作业模板时捕获http_port变量值,我们可以创建一个调查问题,如图 5.15所示:

图 5.15-创建一个调查问题,以捕获有效的 HTTP 端口号到一个变量

图 5.15-创建一个调查问题,以捕获有效的 HTTP 端口号到一个变量

创建所有问题后,请注意,您需要为作业模板打开调查,如图 5.16所示,否则在运行时问题将不会出现:

图 5.16-为作业模板打开调查

图 5.16 – 为作业模板启用调查

现在,当运行 playbook 时,用户将被提示输入一个值,并且 AWX 将确保它是指定范围内的整数。还定义了一个合理的默认值。现在让我们继续看一下在 AWX 中更高级使用作业模板的方法,称为工作流。

工作流模板

Playbook 运行,特别是来自 AWX,可能会很复杂。例如,可能希望首先从 SCM 系统更新项目和任何动态清单。然后我们可能会运行一个作业模板来部署一些更新的代码。然而,如果失败,几乎肯定希望回滚所做的任何更改(或采取其他补救措施)。当您单击现在熟悉的添加按钮以添加新模板时,您将在下拉菜单中看到两个选项 – 作业模板(我们已经使用过)和工作流模板

一旦为新的工作流模板填写了所有必填字段并保存了,您将自动进入工作流可视化器(要在将来返回到此处,只需通过常规方式在 GUI 中访问工作流模板,然后单击可视化器选项卡)。工作流可视化器从左到右构建了 AWX 执行的任务流程。例如,以下屏幕截图显示了一个工作流,其中我们的演示项目最初与其 SCM 同步。

如果该步骤成功(由指向下一个块的绿色链接表示),则运行演示作业模板。如果这反过来成功,则运行 Mastery 模板。如果前面的任何步骤失败,则工作流在那里停止(尽管可以在任何阶段定义失败时操作)。基于这个简单的构建块前提和在成功、失败或始终发生事件后执行后续操作的能力,将使您能够在 AWX 中构建大规模的运营流程。这将在不必构建庞大的单片剧本的情况下实现。图 5.17显示了我们在可视化器中的简单工作流:

图 5.17 – AWX 中的工作流可视化器

图 5.17 – AWX 中的工作流可视化器

使用这个工具,我们可以强大地构建多步工作流,在每个阶段之后采取智能行动,具体取决于它是否成功。

到目前为止,我们讨论的一切都很棒,如果您直接与 AWX GUI 交互。但是,如果您设置了无人值守的操作来运行,但希望收到其结果的通知(特别是如果它们失败了),会发生什么?同样,如果有人运行了可能影响服务的更改,您如何通知团队?您将在下一节中找到这些问题的答案。

通知

当您检查 AWX 用户界面时,您会注意到大多数屏幕都有一个名为通知的选项卡。AWX 有能力与许多流行的通信平台集成,例如 Slack、IRC、Pagerduty,甚至老式的电子邮件(此列表不是详尽的)。一旦通过用户界面定义了给定平台的配置,就可以在特定事件发生时发送通知。这些事件将根据您希望从中生成通知的项目而变化。例如,对于作业模板,您可以选择在作业开始时、成功时和/或失败时收到通知(以及这些事件的任何组合)。您可以为不同的事件生成不同的通知类型。例如,您可以通知 Slack 频道模板已启动,但如果模板未能自动生成票据以促进进一步调查,则通过电子邮件通知您的票务系统。

例如,图 5.18显示了我们之前配置的Mastery Template设置为在其执行失败时向给定的收件人列表发送电子邮件。在开始和成功时,不会收到通知(当然可以打开):

图 5.18 – 为 Mastery 模板设置失败运行的电子邮件通知

图 5.18 - 设置 Mastery Template 失败运行的电子邮件通知

AWX 中定义的所有通知都显示在通知选项卡中。但是,一旦定义,它们就不必添加。用户只需决定是否为每个通知服务打开或关闭启动成功失败通知。

还有一种与 AWX 交互的方式,而不使用 GUI。当然,这是通过 API,我们将在本章的最后部分进行讨论。

使用 API

在本书的本章中,我们已经使用 GUI 查看了所有 AWX 操作,因为这可能是解释其功能和用法的最简单和最直观的方式。然而,对于任何企业来说,AWX 的一个关键特性是 API,这是一个完整的功能,使我们能够执行所有这里完成的操作(以及更多),而无需触及 UI。

这是一个非常强大的工具,特别是在集成到更大的工作流程中。例如,您可以使用 API 将 AWX 连接到您的 CI/CD 流水线中,在代码成功构建后,您可以触发 AWX 作业来部署一个测试环境来运行它(甚至将代码部署到该环境)。同样,您可以通过 API 自动创建作业模板、清单项和配置的所有其他方面。

API 本身是可浏览的,您可以通过在 AWX 服务器的 URL 中添加/api/api/v2来访问它(分别用于 API 的版本 1 和版本 2)。

尽管通常您会将这些集成到更大的应用程序或工作流程中,但使用curl很容易演示 API 的用法。例如,假设我们想要检索在我们的 AWX 服务器中定义的清单列表,我们可以使用以下命令来执行:

curl -k -s --user admin:adminpassword -X GET https://awx.example.org/api/v2/inventories/ | python -m json.tool

当然,您需要将您的凭据替换到--user参数中,并将您的 AWX 服务器的正确 FQDN 替换到命令中的 URL 中。完成后,此命令将以 JSON 格式检索 AWX 中定义的所有清单的详细信息 - 您不需要通过 Python 的json.tool工具进行管道处理 - 它只是使输出对人类更可读!

同样,我们可以通过 API 启动我们的 Mastery 示例模板。AWX 的所有配置元素都有与之关联的唯一数字 ID,我们必须使用这些 ID 来访问它们。因此,例如,让我们使用 API 从 AWX 检索作业模板的列表:

curl -k -s --user admin:adminpassword -X GET https://awx.example.org/api/v2/job_templates/ | python -m json.tool

通过 JSON 输出,我可以看到在我的系统上,我们的Mastery Template具有12id。另外,因为我在本章的早期示例中为这个模板设置了一个调查,JSON 输出告诉我在启动模板之前需要指定一些变量。在GET查询的输出中可能需要设置一些项目,因此在组合API POST之前仔细审查它们是值得的。图 5.19显示了从API GET调用中获取的输出,显示了在启动模板之前必须设置的变量:

图 5.19 - 从作业模板 12 的 API GET 调用中获取的部分输出

图 5.19 - 从作业模板 12 的 API GET 调用中获取的部分输出

可以使用 API 中的extra_vars数据字段来指定这些变量数据,因此我们可以组合一个类似以下的 API 调用来启动作业:

curl -k -s --user admin:adminpassword -X POST -H 'Content-Type:application/json' https://awx.example.org/api/v2/job_templates/12/launch/ --data '{"extra_vars": "{\"http_port\": 80}"}' | python -m json.tool

此命令的输出将包括作业 ID 等使用详细信息,以便我们可以查询作业运行(如果需要的话)。在我的示例中,作业 ID 返回为10,因此我可以使用以下命令查询此作业的状态(包括是否成功):

curl -k -s --user admin:adminpassword -X GET https://awx.example.org/api/v2/jobs/10/ | python -m json.tool

甚至可以使用类似以下的 API 调用从作业运行中检索ansible-playbook命令的输出:

curl -k -s --user admin:adminpassword -X GET https://awx.example.org/api/v2/jobs/10/stdout/

尽管在生产环境中不太可能使用curl来驱动 API,但希望这些简单、可重复的示例能帮助你开始使用 API 集成 AWX 的旅程。

甚至可以通过 Python 的pip包装系统安装 AWX 的 CLI。这个 CLI 使用了与我们在本节讨论过的基于 HTTP 的 API 一致的命名和命令结构,鉴于相似性,因此这被留作可选练习。然而,为了帮助你入门,AWX CLI 的官方文档可以在这里找到:

docs.ansible.com/ansible-tower/latest/html/towercli/index.html

尽管文档提到了 Ansible Tower,但在使用开源 AWX 软件时同样有效。

总结

这就结束了我们对 AWX 的快速介绍。在本章中,我们展示了一旦你了解了涉及的核心四个步骤过程,AWX 安装和配置起来是很简单的。我们还展示了如何通过调查、通知和工作流等功能来完善这个过程。

你学到了 AWX 安装简单(实际上,它是用 Ansible 安装的!),以及如何为其添加 SSL 加密。然后你了解了平台的工作原理,以及如何从新安装到构建项目、清单、凭据和模板来运行 Ansible 作业。你了解到有许多其他功能可以构建在此基础上。这些在本章的最后部分进行了介绍,以帮助你构建一个强大的企业管理系统来管理 Ansible。

在下一章中,我们将回到 Ansible 语言,看看 Jinja2 模板系统的好处。

问题

  1. AWX 可以在独立的 Docker 容器或 Kubernetes 中运行。

a) True

b) False

  1. AWX 为希望管理其自动化流程的企业提供了以下哪些内容?

a) web UI

b) 一个功能完整的 API

c) 源代码控制集成

d) 以上所有

  1. AWX 直接支持安全管理自动化的凭据。

a) True

b) False

  1. AWX 为创建和测试 Ansible playbook 提供了图形化的开发环境。

a) True

b) False

  1. AWX 可以安排无人值守的作业运行。

a) True

b) False

  1. 在 AWX 中,预配置的ansible-playbook运行的参数集被称为什么?

a) 作业配置

b) Ansible 模板

c) 作业模板

d) Ansible 运行

  1. AWX 可以通过创建以下哪些内容将其配置分为业务的不同部分?

a) 团队

b) 组织

c) 部署第二个 AWX 服务器

d) 组

  1. 在 AWX 中,可以告诉以下哪些内容?

a) playbook 运行的时间

b) 谁运行了 playbook

c) 传递给 playbook 的参数是什么

d) 以上所有

  1. AWX 中的用户友好的变量定义是通过哪个功能提供的?

a) 表单

b) e-Forms

c) 额外的变量

d) 调查

  1. AWX 中的项目由什么组成?

a) 用户的逻辑团队

b) playbook 的逻辑文件夹

c) 任务管理系统

d) 角色的逻辑集合

第二部分:编写和故障排除 Ansible Playbooks

在本节中,您将获得如何编写健壮、多功能 playbook 的扎实理解,适用于各种用例和环境。

本节包括以下章节:

  • 第六章, 释放 Jinja2 模板的力量

  • 第七章, 控制任务条件

  • 第八章, 使用角色组合可重用的 Ansible 内容

  • 第九章, 故障排除 Ansible

  • 第十章, 扩展 Ansible

第六章:解锁 Jinja2 模板的力量

手动操作配置文件是一项繁琐且容易出错的任务。同样,执行模式匹配以对现有文件进行更改是有风险的,并且确保模式可靠和准确可能是耗时的。无论您是使用 Ansible 来定义配置文件内容、在任务中执行变量替换、评估条件语句,还是其他操作,模板化几乎在每个 Ansible playbook 中都发挥作用。事实上,鉴于这项任务的重要性,可以说模板化是 Ansible 的命脉。

Ansible 使用的模板引擎是 Jinja2,这是一种现代且设计友好的 Python 模板语言。Jinja2 值得有一本专门的书;然而,在本章中,我们将介绍 Jinja2 模板在 Ansible 中的一些常见用法模式,以展示它可以为您的 playbook 带来的强大功能。在本章中,我们将涵盖以下主题:

  • 控制结构

  • 数据操作

  • 比较值

技术要求

为了跟随本章中提供的示例,您需要一台运行 Ansible 4.3 或更新版本的 Linux 机器。几乎任何 Linux 版本都可以;对于那些对具体细节感兴趣的人,本章中提供的所有代码都是在 Ubuntu Server 20.04 LTS 上测试的,除非另有说明,并且在 Ansible 4.3 上测试。本章附带的示例代码可以从 GitHub 上下载:github.com/PacktPublishing/Mastering-Ansible-Fourth-Edition/tree/main/Chapter06

查看以下视频以查看代码示例:bit.ly/3lZHTM1

控制结构

在 Jinja2 中,控制结构是指模板中控制引擎解析模板流程的语句。这些结构包括条件、循环和宏。在 Jinja2 中(假设使用默认值),控制结构将出现在{% ... %}块内。这些开放和关闭块会提醒 Jinja2 解析器,提供了一个控制语句,而不是一个普通的字符串或变量名。

条件语句

模板中的条件语句创建了一个决策路径。引擎将考虑条件,并从两个或更多潜在的代码块中进行选择。至少有两个:如果条件满足(评估为true)的路径,以及如果条件不满足(评估为false)的显式定义的else路径,或者另外一个隐含的else路径,其中包含一个空块。

条件语句是if语句。这个语句的工作方式与 Python 中的工作方式相同。if语句可以与一个或多个可选的elif语句和一个可选的最终else结合使用,并且,与 Python 不同,它需要一个显式的endif。下面的示例显示了一个配置文件模板片段,结合了常规变量替换和if else结构:

setting = {{ setting }} 
{% if feature.enabled %} 
feature = True 
{% else %} 
feature = False 
{% endif %} 
another_setting = {{ another_setting }} 

在这个示例中,我们检查feature.enabled变量是否存在,并且它没有被设置为False。如果是True,那么就使用feature = True文本;否则,使用feature = False文本。在这个控制块之外,解析器对大括号内的变量执行正常的变量替换。可以使用elif语句定义多个路径,这会给解析器提供另一个测试,如果前面的测试结果为False

为了演示模板和变量替换的渲染,我们将把示例模板保存为demo.j2。然后,我们将创建一个名为template-demo.yaml的 playbook,定义要使用的变量,然后使用template查找作为ansible.builtin.pause任务的一部分来在屏幕上显示渲染后的模板:

--- 
- name: demo the template 
  hosts: localhost 
  gather_facts: false  
  vars: 
    setting: a_val 
    feature: 
      enabled: true 
    another_setting: b_val  
  tasks: 
    - name: pause with render 
      ansible.builtin.pause: 
        prompt: "{{ lookup('template', 'demo.j2') }}" 

执行此 playbook 将在屏幕上显示渲染的模板,并等待输入。您可以使用以下命令来执行它:

ansible-playbook -i mastery-hosts template-demo.yaml

只需按Enter运行 playbook,如图 6.1所示:

图 6.1-使用 Ansible 渲染带条件的简单模板

图 6.1-使用 Ansible 渲染简单的带条件模板

记住我们在第一章中讨论过的 Ansible 变量优先顺序,我们可以将feature.enabled的值覆盖为False。当运行 playbook 时,我们可以使用--extra-vars(或-e)参数来实现这一点;这是因为额外变量比 playbook 定义的变量具有更高的优先级。您可以通过再次运行 playbook 来实现这一点,但这次使用以下命令:

ansible-playbook -i mastery-hosts template-demo.yaml -e '{feature: {"enabled": false}}'

在这种情况下,输出应该略有不同,如图 6.2所示:

图 6.2-使用 Ansible 渲染带条件的简单模板,同时覆盖变量值

图 6.2-使用 Ansible 渲染带条件的简单模板,同时覆盖变量值

从这些简单的测试中可以看出,Jinja2 提供了一种非常简单但强大的方式来通过模板中的条件来定义数据。

内联条件

请注意,if语句可以在内联表达式中使用。在某些不希望有额外换行的情况下,这可能很有用。让我们构建一个场景,我们需要将 API 定义为cindercinderv2,如下所示:

API = cinder{{ 'v2' if api.v2 else '' }} 

这个例子假设api.v2被定义为布尔值TrueFalse。内联if表达式遵循<条件为真时做某事> if <条件为真> else <否则做某事>的语法。在内联if表达式中,有一个隐含的else;然而,这个隐含的else意味着要被评估为未定义对象,这通常会创建一个错误。我们可以通过定义一个显式的else来保护它,它会渲染一个零长度的字符串。

让我们修改我们的 playbook 来演示内联条件。这次,我们将使用debug模块来渲染简单的模板,如下所示:

--- 
- name: demo the template 
  hosts: localhost 
  gather_facts: false 
  vars: 
    api: 
      v2: true  
  tasks: 
    - name: pause with render 
      ansible.builtin.debug: 
        msg: "API = cinder{{ 'v2' if api.v2 else '' }}" 

请注意,这次我们没有定义外部模板文件;模板实际上是与 Ansible 任务一起的。使用以下命令执行 playbook:

ansible-playbook -i mastery-hosts template-demo-v2.yaml

输出应该与图 6.3中显示的类似:

图 6.3-使用内联模板运行 playbook

图 6.3-使用内联模板运行 playbook

现在,就像我们在之前的例子中所做的那样,我们将使用 Ansible 的额外变量将api.v2的值更改为false,以查看这对内联模板渲染的影响。再次使用以下命令执行 playbook:

ansible-playbook -i mastery-hosts template-demo-v2.yaml -e '{api: {"v2": false}}'

这次,输出应该与图 6.4中显示的类似。注意渲染的字符串如何改变:

图 6.4-使用内联模板运行 playbook,同时使用额外变量改变行为

图 6.4-使用内联模板运行 playbook,同时使用额外变量改变行为

通过这种方式,我们可以创建非常简洁和强大的代码,根据 Ansible 变量定义值,就像我们在这里演示的那样。

循环

循环允许您在模板文件中构建动态创建的部分。当您知道需要以相同方式操作未知数量的项目时,这是很有用的。要启动循环控制结构,我们使用for语句。让我们演示一种简单的方法,循环遍历一个虚构服务可能找到数据的目录列表:

# data dirs 
{% for dir in data_dirs -%} 
data_dir = {{ dir }} 
{% endfor -%} 

提示

默认情况下,当模板被渲染时,{% %}块会打印一个空行。这可能在我们的输出中是不可取的,但幸运的是,我们可以通过在块的结尾使用-%}来修剪它。更多详情请参考官方的 Jinja2 文档jinja.palletsprojects.com/en/3.0.x/templates/#whitespace-control

在这个例子中,我们将得到一个data_dir =行,每个data_dirs变量中的项目,假设data_dirs是一个至少有一个项目的列表。如果变量不是列表(或其他可迭代类型),或者未定义,将生成一个错误。如果变量是一个可迭代类型但是空的,那么将不会生成任何行。Jinja2 可以处理这种情况,并且还允许通过else语句在变量中找不到项目时替换一行。在下面的例子中,让我们假设data_dirs是一个空列表:

# data dirs 
{% for dir in data_dirs -%} 
data_dir = {{ dir }} 
{% else -%} 
# no data dirs found 
{% endfor -%} 

我们可以通过修改我们的 playbook 和模板文件来测试这一点。我们将创建一个名为demo-for.j2的模板文件,其中包含前面列出的模板内容。此外,我们将在我们第一个条件渲染模板并暂停用户输入的示例中创建一个 playbook 文件。应该命名为template-demo-for.yaml,并包含以下代码:

- name: demo the template
  hosts: localhost
  gather_facts: false
  vars:
    data_dirs: []
  tasks:
    - name: pause with render
      ansible.builtin.pause:
        prompt: "{{ lookup('template', 'demo-for.j2') }}"

创建这两个文件后,您可以使用以下命令运行 playbook:

ansible-playbook -i mastery-hosts template-demo-for.yaml

运行我们的 playbook 将渲染模板,并产生一个类似于图 6.5所示的输出:

图 6.5 - 在 Ansible 中使用 for 循环渲染模板

图 6.5 - 在 Ansible 中使用 for 循环渲染模板

正如你所看到的,在for循环中的else语句优雅地处理了空的data_dirs列表,这正是我们在 playbook 运行中想要的。

过滤循环项目

循环也可以与条件结合使用。在循环结构内部,可以使用if语句来检查当前循环项目作为条件的一部分。让我们扩展我们的例子,防止模板的用户意外使用/作为data_dir(对文件系统的根目录执行的任何操作都可能很危险,特别是如果它们是递归执行的):

# data dirs 
{% for dir in data_dirs -%} 
{% if dir != "/" -%} 
data_dir = {{ dir }} 
{% endif -%} 
{% else -%} 
# no data dirs found 
{% endfor -%}

前面的例子成功地过滤掉了任何data_dirs中是/的项目,但这需要的输入比必要的要多得多。Jinja2 提供了一种方便的方法,允许你在for语句中轻松地过滤循环项目。让我们使用这种便利来重复前面的例子:

# data dirs 
{% for dir in data_dirs if dir != "/" -%} 
data_dir = {{ dir }} 
{% else -%} 
# no data dirs found 
{% endfor -%} 

因此,这种结构不仅需要输入更少,而且还正确计算了循环次数,我们将在下一节中学习。

循环索引

循环计数是免费提供的,可以得到当前循环迭代的索引。作为变量,它们可以以几种不同的方式访问。以下表格概述了它们可以被引用的方式:

有关循环内部位置的信息可以帮助确定要渲染的内容。考虑到我们之前的例子,我们可以提供一个单行,其中包含逗号分隔的值,而不是渲染多行data_dir来表示每个数据目录。如果没有访问循环迭代数据,这将是困难的。然而,通过使用这些数据,可以变得简单。为了简单起见,本例假设允许在最后一项后面加上逗号,并且允许在项目之间有任何空格(换行符):

# data dirs
{% for dir in data_dirs if dir != "/" -%}
{% if loop.first -%}
data_dir = {{ dir }},
           {% else -%}
           {{ dir }},
{% endif -%}
{% else -%}
# no data dirs found
{% endfor -%} 

前面的例子使用了loop.first变量来确定是否需要渲染data_dir =部分,或者是否只需要渲染适当间距的目录。通过在for语句中使用过滤器,我们可以得到loop.first的正确值,即使data_dirs中的第一项是不需要的/

重要提示

看一下第一个else语句的缩进 - 为什么我们要这样做?答案与 Jinja2 中的空格控制有关。简单地说,如果您不缩进控制语句(例如ifelse语句),那么您希望渲染的模板内容将会将左侧的所有空格修剪掉;因此,我们随后的目录条目将不会有任何缩进。在某些文件中(包括 YAML 和 Python),缩进非常重要,因此这是一个小但非常重要的细微差别。

为了测试这一点,我们将创建一个名为demo-for.j2的新模板文件,其中包含前面列出的内容。此外,我们将修改template-demo-for.yaml以定义一些data_dirs,包括一个/,应该被过滤掉:

--- 
- name: demo the template 
  hosts: localhost 
  gather_facts: false  
  vars: 
    data_dirs: ['/', '/foo', '/bar']  
  tasks: 
    - name: pause with render 
      ansible.builtin.pause: 
        prompt: "{{ lookup('template', 'demo-for.j2') }}"

现在,我们可以使用以下命令执行 playbook:

ansible-playbook -i mastery-hosts template-demo-for.yaml

当它运行时,我们应该看到我们渲染的内容,如图 6.6所示:

图 6.6 - 在 Ansible 中使用 for 循环渲染模板,同时利用循环索引

图 6.6 - 在 Ansible 中使用 for 循环渲染模板,同时利用循环索引

在前面的例子中,如果不允许有尾随逗号,我们可以利用内联if语句来确定我们是否已经完成循环并正确地渲染逗号。您可以在前面模板代码的以下增强版本中查看这一点:

# data dirs. 
{% for dir in data_dirs if dir != "/" -%} 
{% if loop.first -%} 
data_dir = {{ dir }}{{ ',' if not loop.last else '' }} 
           {% else -%} 
           {{ dir }}{{ ',' if not loop.last else '' }} 
{% endif -%} 
{% else -%} 
# no data dirs found 
{% endfor -%}

使用内联if语句允许我们构建一个模板,只有在循环中有更多项目通过我们的初始过滤时才会渲染逗号。再次,我们将使用前面的内容更新demo-for.j2并使用以下命令执行 playbook:

ansible-playbook -i mastery-hosts template-demo-for.yaml

渲染模板的输出应该与图 6.7中显示的类似:

图 6.7 - 在 Ansible 中使用 for 循环渲染模板,扩展使用循环索引

图 6.7 - 在 Ansible 中使用 for 循环渲染模板,扩展使用循环索引

输出基本上与以前一样。但是,这一次,我们的模板使用内联if语句评估是否在循环中的每个dir值后放置逗号,从而删除最终值末尾的多余逗号。

敏锐的读者会注意到,在前面的例子中,我们有一些重复的代码。重复的代码是任何开发人员的敌人,幸运的是,Jinja2 有一种方法可以帮助!宏就像常规编程语言中的函数:它是定义可重用习语的一种方式。宏在{% macro ... %} ... {% endmacro %}块内定义。它有一个名称,可以接受零个或多个参数。宏内的代码不会继承调用宏的块的命名空间,因此所有参数必须显式传递。宏通过名称在花括号块内调用,并通过括号传递零个或多个参数。让我们创建一个名为comma的简单宏,以取代我们重复的代码:

{% macro comma(loop) -%} 
{{ ',' if not loop.last else '' }} 
{%- endmacro -%} 
# data dirs. 
{% for dir in data_dirs if dir != "/" -%} 
{% if loop.first -%} 
data_dir = {{ dir }}{{ comma(loop) }} 
           {% else -%} 
           {{ dir }}{{ comma(loop) }} 
{% endif -%} 
{% else -%} 
# no data dirs found 
{% endfor -%} 

调用comma并将循环对象传递给宏,允许宏检查循环并决定是否应省略逗号。

宏变量

宏在调用宏时可以访问传递的任何位置或关键字参数。位置参数是根据它们提供的顺序分配给变量的参数,而关键字参数是无序的,并明确地将数据分配给变量名。如果在调用宏时未定义关键字参数,关键字参数也可以具有默认值。还有三个额外的特殊变量可用:

  • varargs:这是一个额外的位置参数的占位符,这些参数将传递给宏。这些位置参数值将组成varargs列表。

  • kwargs:这与varargs相同;但是,它不是保存额外的位置参数值,而是保存额外关键字参数和它们的关联值的哈希。

  • caller:这可以用来回调到可能调用此宏的更高级宏(是的,宏可以调用其他宏)。

除了这三个特殊变量之外,还有许多变量可以公开有关宏本身的内部细节。这些有点复杂,但我们将逐一介绍它们的用法。首先,让我们简要介绍一下每个变量:

  • name:这是宏本身的名称。

  • arguments:这是宏接受的参数的名称元组。

  • defaults:这是默认值的元组。

  • catch_kwargs:这是一个布尔值,如果宏访问(因此接受)kwargs变量,则将其定义为true

  • catch_varargs:这是一个布尔值,如果宏访问(因此接受)varargs变量,则将其定义为true

  • caller:这是一个布尔值,如果宏访问(因此可以从另一个宏调用)caller变量,则将其定义为true

与 Python 中的类类似,这些变量需要通过宏本身的名称引用。尝试在不加上名称的情况下访问这些宏将导致未定义的变量。现在,让我们逐一演示它们的用法。

名称

name变量实际上非常简单。它只是提供了一种访问宏名称作为变量的方式,也许用于进一步操作或使用。以下模板包括一个引用宏名称的宏,以在输出中呈现它:

{% macro test() -%} 
{{ test.name }} 
{%- endmacro -%} 
{{ test() }} 

假设我们要创建demo-macro.j2,其中包含此模板和以下template-demo-macro.yaml playbook:

---
- name: demo the template
  hosts: localhost
  gather_facts: false
  vars:
    data_dirs: ['/', '/foo', '/bar']
  tasks:
    - name: pause with render
      ansible.builtin.pause:
        prompt: "{{ lookup('template', 'demo-macro.j2') }}"

我们将使用以下命令运行此 playbook:

ansible-playbook -i mastery-hosts template-demo-macro.yaml

当您运行 playbook 时,您的输出应该类似于图 6.8中显示的输出:

图 6.8 - 使用名称宏变量呈现模板

图 6.8 - 使用名称宏变量呈现模板

从这次测试运行中可以看出,我们的模板只是以宏名称呈现,没有其他内容,正如预期的那样。

参数

arguments变量是宏接受的参数的元组。请注意,这些是明确定义的参数,而不是特殊的kwargsvarargs。我们之前的例子将呈现一个空元组(),所以让我们修改它以得到其他内容:

{% macro test(var_a='a string') -%} 
{{ test.arguments }} 
{%- endmacro -%} 
{{ test() }} 

像以前一样运行相同的 playbook,以相同的方式呈现此模板,应该产生图 6.9中显示的输出:

图 6.9 - 运行一个 playbook 来呈现打印其宏参数的 Jinja2 模板

图 6.9 - 运行一个 playbook 来呈现打印其宏参数的 Jinja2 模板

在这个例子中,我们可以清楚地看到我们的模板是使用宏接受的参数的名称(而不是它们的值)呈现的。

默认值

defaults变量是宏显式接受的任何关键字参数的默认值的元组。尽管在 Jinja2 的文档中仍然存在(在撰写本文时,有一个问题正在解决文档错误),但此变量已从所有新于版本 2.8.1 的 Jinja2 版本中删除。如果您需要访问此变量,您需要将您的 Jinja2 Python 模块降级到 2.8.1。

对于使用较旧版本的 Jinja2 的人,我们可以如下演示此变量;让我们将我们的宏更改为显示默认值以及参数:

{% macro test(var_a='a string') -%} 
{{ test.arguments }} 
{{ test.defaults }} 
{%- endmacro -%} 
{{ test() }}

我们可以像以前一样运行我们现有的测试 playbook,但现在使用新更新的模板。如果您的 Jinja2 版本支持defaults变量,输出应该类似于图 6.10中显示的输出:

图 6.10 - 使用默认值和名称宏变量呈现 Jinja2 模板

图 6.10 - 使用默认值和名称宏变量呈现 Jinja2 模板

在这里,我们可以看到模板是使用宏接受的参数的名称和默认值进行渲染的。

catch_kwargs

只有当宏本身访问kwargs变量以捕获可能传递的任何额外关键字参数时,此变量才被定义。如果定义了,它将被设置为true。如果没有访问kwargs变量,在调用宏时传递的任何额外关键字参数都将在渲染模板时导致错误。同样,访问catch_kwargs而不访问kwargs将导致未定义错误。让我们再次修改我们的示例模板,以便我们可以传递额外的kwargs变量:

{% macro test() -%} 
{{ kwargs }} 
{{ test.catch_kwargs }} 
{%- endmacro -%} 
{{ test(unexpected='surprise') }}

我们可以再次使用与之前相同的命令将更新后的模板通过现有的渲染模板运行。这次,输出应该类似于图 6.11中显示的结果:

图 6.11 - 渲染使用 catch_kwargs 变量的模板

图 6.11 - 渲染使用 catch_kwargs 变量的模板

从这个输出中可以看出,当向模板传递意外变量时,模板不会产生错误,而是使我们能够访问传递的意外值。

catch_varargs

catch_kwargs类似,只有当宏访问varargs变量时,此变量才存在(并且设置为true)。再次修改我们的示例,我们可以看到它的作用:

{% macro test() -%} 
{{ varargs }} 
{{ test.catch_varargs }} 
{%- endmacro -%} 
{{ test('surprise') }}

模板的渲染结果应该类似于图 6.12中显示的结果:

图 6.12 - 渲染使用 varargs 和 catch_varargs 宏变量的模板

图 6.12 - 渲染使用 varargs 和 catch_varargs 宏变量的模板

同样,我们可以看到我们能够捕获并渲染传递给宏的意外值,而不是在渲染时返回错误,如果我们没有使用catch_varargs,那么将会发生错误。

caller

caller变量需要更多的解释。宏可以调用另一个宏。如果模板的同一部分将被多次使用,但内部数据的一部分更改比作为宏参数轻松传递的更多,这将非常有用。caller变量并不是一个确切的变量;它更像是一个引用,用于获取调用该调用宏的内容。

让我们更新我们的模板来演示它的用法:

{% macro test() -%}
The text from the caller follows: {{ caller() }}
{%- endmacro -%}
{% call test() -%}
This is text inside the call 
{% endcall -%} 

渲染的结果应该类似于图 6.13中显示的结果:

图 6.13 - 渲染使用 caller 变量的模板

图 6.13 - 渲染使用 caller 变量的模板

调用宏仍然可以向该宏传递参数;可以传递任意组合的参数或关键字参数。如果宏使用varargskwargs,那么也可以传递更多的参数。此外,宏还可以将参数传递回给调用者!为了演示这一点,让我们创建一个更大的示例。这次,我们的示例将生成一个适用于 Ansible 清单的文件:

{% macro test(group, hosts) -%} 
[{{ group }}] 
{% for host in hosts -%} 
{{ host }} {{ caller(host) }} 
{%- endfor -%} 
{%- endmacro -%}  
{% call(host) test('web', ['host1', 'host2', 'host3']) -%} 
ssh_host_name={{ host }}.example.name ansible_sudo=true 
{% endcall -%}  
{% call(host) test('db', ['db1', 'db2']) -%} 
ssh_host_name={{ host }}.example.name 
{% endcall -%}

使用我们的测试 playbook 进行渲染后,结果应该如图 6.14中所示:

图 6.14 - 使用 caller 变量渲染的模板的更高级示例

图 6.14 - 使用 caller 变量渲染的模板的更高级示例

我们两次调用了test宏,每次为我们想要定义的每个组调用一次。每个组都有略有不同的host变量集合要应用,并且这些变量是在调用本身中定义的。通过让宏回调到调用者,传递当前循环中的host变量,我们节省了输入。

控制块在模板内提供了编程能力,允许模板作者使其模板更高效。效率不一定体现在模板的初始草稿中;相反,当需要对重复值进行小改动时,效率才真正发挥作用。现在我们已经详细地看了 Jinja2 中构建控制结构,接下来,我们将继续看看这种强大的模板语言如何帮助我们处理另一个常见的自动化需求:数据操作。

数据操作

虽然控制结构影响模板处理的流程,但还有另一种工具可以帮助您修改变量的内容。这个工具叫做过滤器。过滤器与小函数或方法相同,可以在变量上运行。一些过滤器不带参数,一些带可选参数,一些需要参数。过滤器也可以链接在一起,一个过滤器操作的结果被馈送到下一个过滤器,然后是下一个。Jinja2 带有许多内置过滤器,而 Ansible 通过许多自定义过滤器扩展了这些过滤器,当您在模板、任务或任何其他 Ansible 允许模板化的地方使用 Jinja2 时,这些过滤器都可以使用。

语法

通过管道符号|将过滤器应用于变量,然后是过滤器的名称,以及括号内的过滤器参数。变量名称和管道符号之间可以有空格,管道符号和过滤器名称之间也可以有空格。例如,如果我们想将 lower filter(使所有字符变为小写)应用于 my_word 变量,我们将使用以下语法:

{{ my_word | lower }} 

因为 lower filter 不需要任何参数,所以不需要给它附加一个空的括号集。然而,如果我们使用一个需要参数的不同 filter,情况就会改变。让我们使用 replace filter,它允许我们用另一个子字符串替换所有出现的子字符串。在这个例子中,我们想要在 answers 变量中用 yes 替换所有出现的 no 子字符串:

{{ answers | replace('no', 'yes') }} 

通过简单地添加更多的管道符号和更多的过滤器名称来实现应用多个过滤器。让我们结合 replace 和 lower 来演示语法-过滤器按照列出的顺序应用。在下面的例子中,首先,我们将所有的 no 子字符串替换为 yes,然后将整个结果字符串转换为小写:

{{ answers | replace('no', 'yes') | lower }} 

由于我们正在进行区分大小写的字符串替换,您可能选择先执行小写转换,这意味着您不会错过任何情况下的 no 单词-无论大小写如何-假设这是您想要的行为!后一个例子的代码将简单地如下所示:

 {{ answers | lower | replace('no', 'yes') }} 

我们可以通过一个简单的 play 来演示这一点,该 play 使用 debug 命令来渲染这一行:

- name: demo the template
  hosts: localhost
  gather_facts: false
  vars:
    answers: "no so YES no"
  tasks:
    - name: debug the template
      ansible.builtin.debug: 
        msg: "{{ answers | replace('no', 'yes') | lower }}" 

现在,我们可以使用以下命令执行 playbook:

ansible-playbook -i mastery-hosts template-demo-filters.yaml

在我们的 answers 变量中,代码中声明的所有单词 no 的实例都将被替换为单词 yes。此外,所有字符都将转换为小写。输出应该类似于图 6.15中显示的输出:

图 6.15-演示在一个简单的 Ansible playbook 中使用链式过滤器

图 6.15-演示在一个简单的 Ansible playbook 中使用链式过滤器

在这里,我们可以看到 playbook 按预期运行,并结合了两个过滤器来操作我们的测试字符串,就像我们要求的那样。当然,这只是可用的过滤器中的两个。在下一节中,让我们继续看一些 Jinja2 中包含的更有用的过滤器。

有用的内置过滤器

Jinja2 内置的过滤器的完整列表可以在 Jinja2 文档中找到。在撰写本书时,有 50 个内置过滤器。接下来,我们将看一些更常用的过滤器。

提示

如果您想查看所有可用过滤器的列表,可以在当前版本的 Jinja2 文档中找到(在撰写时可用):jinja.palletsprojects.com/en/3.0.x/templates/#builtin-filters

default

default 过滤器是为了为一个未定义的变量提供默认值的一种方式,从而防止 Ansible 生成错误。它是一个复杂的if语句的简写,它在尝试使用else子句提供不同值之前检查变量是否已定义。让我们看两个渲染相同内容的例子。一个使用if/else结构,另一个使用default过滤器:

{% if some_variable is defined -%} 
{{ some_variable }} 
{% else -%} 
default_value 
{% endif -%}
{{ some_variable | default('default_value') }} 

这些例子的渲染结果是相同的;然而,使用default过滤器的例子写起来更快,阅读起来更容易。

虽然default非常有用,但如果您在多个位置使用相同的变量,请谨慎操作。更改默认值可能会变得麻烦,定义默认值可能更有效,可以在 play 或角色级别定义变量的默认值。

length

length 过滤器将返回序列或哈希的长度。在本书的早期版本中,我们引用了一个名为count的变量,它是length的别名,完成了相同的功能。这个过滤器对于执行任何关于主机集大小的数学运算或任何其他需要知道某个集合计数的情况非常有用。让我们创建一个例子,其中我们将max_threads配置条目设置为与 play 中主机数量相匹配的计数:

max_threads: {{ play_hosts | count }} 

这为我们提供了一个简洁的方式来获取play_hosts变量中包含的主机数量,并将答案赋给max_threads变量。

random

random 过滤器用于从序列中进行随机选择。让我们使用这个过滤器将一个任务委派给db_servers组中的随机选择:

name: backup the database 
  shell: mysqldump -u root nova > /data/nova.backup.sql 
  delegate_to: "{{ groups['db_servers'] | random }}" 
  run_once: true 

在这里,我们可以很容易地将这个任务委派给db_servers组中的一个成员,使用我们的过滤器随机选择。

round

round 过滤器用于将数字四舍五入。如果您需要执行浮点数运算,然后将结果转换为四舍五入的整数,这可能很有用。round 过滤器接受可选参数来定义精度(默认为0)和舍入方法。可能的舍入方法有common(四舍五入,是默认值)、ceil(总是向上舍入)和floor(总是向下舍入)。在这个例子中,我们将两个过滤器链接在一起,将一个数学结果舍入到零精度,然后将其转换为整数:

{{ math_result | round | int }} 

因此,如果math_result变量设置为3.4,则前一个过滤器链的输出将为3

有用的 Ansible 提供的自定义过滤器

虽然 Jinja2 提供了许多过滤器,但 Ansible 还包括一些额外的过滤器,playbook 作者可能会发现特别有用。我们将在下面重点介绍这些过滤器。

提示

Ansible 中的这些自定义过滤器在不同版本之间经常发生变化。它们值得审查,特别是如果您经常使用它们。自定义 Ansible 过滤器的完整列表可在docs.ansible.com/ansible/latest/user_guide/playbooks_filters.html上找到。

与任务状态相关的过滤器

Ansible 为每个任务跟踪任务数据。这些数据用于确定任务是否失败、是否导致更改或是否完全跳过。Playbook 作者可以注册任务的结果,在先前版本的 playbook 中,他们将使用过滤器来检查任务的状态。从 Ansible 2.9 开始,这完全被移除了。因此,如果您有来自早期 Ansible 版本的遗留 playbook,您可能需要相应地进行更新。

在 Ansible 2.7 发布之前,您可能会使用一个带有过滤器的条件语句,如下所示:

when: derp | success

现在应该使用新的语法,如下片段所示。请注意,以下代码块中的代码执行相同的功能:

when: derp is success

让我们在以下代码中查看它的运行情况:

--- 
- name: demo the filters 
  hosts: localhost 
  gather_facts: false  
  tasks: 
    - name: fail a task 
      ansible.builtin.debug: 
        msg: "I am not a change" 
      register: derp  
    - name: only do this on change 
      ansible.builtin.debug: 
        msg: "You had a change" 
      when: derp is changed  
    - name: only do this on success 
      ansible.builtin.debug: 
        msg: "You had a success" 
      when: derp is success

您可以使用以下命令运行此 playbook:

ansible-playbook -i mastery-hosts template-demo-filters.yaml

输出显示在图 6.16中:

图 6.16 – 根据任务状态运行 Ansible playbook 的条件

图 6.16 – 根据任务状态运行 Ansible playbook 的条件

如您所见,ansible.builtin.debug语句导致success。因此,我们跳过了要在change上运行的任务,并执行了要在success上运行的任务。

shuffle

random过滤器类似,shuffle过滤器可用于生成随机结果。与从列表中选择一个随机选择的random过滤器不同,shuffle过滤器将对序列中的项目进行洗牌并返回完整的序列:

--- 
- name: demo the filters 
  hosts: localhost 
  gather_facts: false  
  tasks: 
    - name: shuffle the cards 
      ansible.builtin.debug: 
        msg: "{{ ['Ace', 'Queen', 'King', 'Deuce'] | shuffle }}" 

使用以下命令运行此 playbook:

ansible-playbook -i mastery-hosts template-demo-filters.yaml

输出显示在图 6.17中:

图 6.17 – 运行使用 shuffle 过滤器的 playbook

图 6.17 – 运行使用 shuffle 过滤器的 playbook

如预期的那样,整个列表返回但顺序被打乱了。如果重复运行 playbook,您将看到每次运行时返回列表的不同顺序。自己试试吧!

处理路径名的过滤器

配置管理和编排经常涉及路径名,但通常只需要路径的一部分。例如,我们可能需要文件的完整路径,但不需要文件名本身。或者,我们只需要从完整路径中提取文件名,忽略其前面的目录。Ansible 提供了一些过滤器来帮助处理这些任务,我们将在以下部分进行讨论。

basename

假设我们有一个要求,只需使用完整路径中的文件名。当然,我们可以执行一些复杂的模式匹配来做到这一点。但是,通常情况下,这会导致代码难以阅读并且难以维护。幸运的是,Ansible 提供了一个专门用于从完整路径中提取文件名的过滤器,我们将在下面进行演示。在这个例子中,我们将使用basename过滤器从完整路径中提取文件名:

---
- name: demo the filters
  hosts: localhost
  gather_facts: false
  tasks:
    - name: demo basename
      ansible.builtin.debug:
        msg: "{{ '/var/log/nova/nova-api.log' | basename }}"

使用以下命令运行此 playbook:

ansible-playbook -i mastery-hosts template-demo-filters.yaml

输出显示在图 6.18中:

图 6.18 – 运行使用 basename 过滤器的 playbook

图 6.18 – 运行使用 basename 过滤器的 playbook

在这里,您可以看到只返回了所需的完整路径的文件名。

dirname

basename的反义词是dirnamedirname不返回路径的最后部分,而是返回其他所有部分(除了文件名,文件名是完整路径的最后部分)。让我们更改之前的 play 以使用dirname,然后使用相同的命令重新运行它。输出现在应该与图 6.19中显示的类似:

图 6.19 – 使用 dirname 过滤器运行 playbook

图 6.19 – 使用 dirname 过滤器运行 playbook

现在,我们只有变量的路径,这在 playbook 的其他地方可能非常有用。

expanduser

通常,各种东西的路径都使用用户快捷方式提供,例如~/.stackrc。但是,某些任务可能需要文件的完整路径。expanduser过滤器提供了一种将路径扩展到完整定义的方法,而不是复杂的命令和注册调用。在此示例中,用户名是jfreeman

---
- name: demo the filters
  hosts: localhost
  gather_facts: false
  tasks:
    - name: demo filter
      ansible.builtin.debug:
        msg: "{{ '~/.stackrc' | expanduser }}"

您可以使用与之前相同的命令运行此 playbook,输出应该与图 6.20中显示的类似:

图 6.20 – 使用 expanduser 过滤器运行 playbook

图 6.20 – 使用 expanduser 过滤器运行 playbook

在这里,我们成功地扩展了路径,这对于创建配置文件或执行其他可能需要绝对路径名而不是相对路径名的文件操作可能是有用的。

Base64 编码

从远程主机读取内容时,例如使用ansible.builtin.slurp模块(用于将远程主机的文件内容读入变量中),内容将被 Base64 编码。为了解码这样的内容,Ansible 提供了一个b64decode过滤器。同样,如果运行一个需要 Base64 编码输入的任务,常规字符串可以使用b64encode过滤器进行编码。

让我们使用 Ansible 创建一个名为/tmp/derp的测试文件,其中将包含一个测试字符串。然后,我们将使用ansible.builtin.slurp模块获取文件内容,并使用上述过滤器对其进行解码:

--- 
- name: demo the filters 
  hosts: localhost 
  gather_facts: false  
  tasks: 
    - name: create a test file
      ansible.builtin.lineinfile:
        path: /tmp/derp
        line: "Ansible is great!"
        state: present
        create: yes
    - name: read file 
      ansible.builtin.slurp: 
        src: /tmp/derp 
      register: derp  
    - name: display file content (undecoded) 
      ansible.builtin.debug: 
        var: derp.content  
    - name: display file content (decoded) 
      ansible.builtin.debug: 
        var: derp.content | b64decode

如果您正在使用本书附带的示例代码,可以使用以下命令运行 playbook:

ansible-playbook -i mastery-hosts template-demo-filters.yaml

输出显示在图 6.21中:

图 6.21 - 运行包含 b64decode 过滤器的 playbook

图 6.21 - 运行包含 b64decode 过滤器的 playbook

在这里,我们成功地将创建的小文件读入一个变量中。此外,我们可以看到变量内容以 Base64 编码形式(请记住,这个编码是由ansible.builtin.slurp模块执行的)进行编码。然后,我们可以使用过滤器对其进行解码以查看原始文件内容。

搜索内容

在 Ansible 中,搜索字符串以查找子字符串是相对常见的。特别是,管理员常见的任务是运行命令并在输出中使用grep查找特定的关键数据片段,这在许多 playbook 中是一个常见的构造。虽然可以使用 shell 任务执行命令,将输出传递给grep,并使用failed_when的谨慎处理来捕获grep的退出代码,但更好的策略是使用命令任务register输出,然后在后续条件中使用 Ansible 提供的正则表达式regex)过滤器。

让我们看两个例子:一个使用ansible.builtin.shell,管道和grep方法,另一个使用search测试:

- name: check database version 
  ansible.builtin.shell: neutron-manage current | grep juno 
  register: neutron_db_ver 
  failed_when: false  
- name: upgrade db 
  ansible.builtin.command: neutron-manage db_sync 
  when: neutron_db_ver is failed 

前面的例子通过强制 Ansible 始终将任务视为成功来工作,但假设如果 shell 的退出代码为非零,则juno字符串未在neutron-manage命令的输出中找到。这种构造是功能性的,但阅读起来复杂,并且可能掩盖了来自命令的真实错误。让我们再试一次,使用search测试。

正如我们之前提到的,关于任务状态,使用search在 Ansible 中搜索字符串被认为是一个测试,并且已被弃用。尽管可能读起来有点奇怪,但为了符合 Ansible 2.9 及更高版本,我们必须在这种情况下使用is关键字代替管道使用search

- name: check database version 
  ansible.builtin.command: neutron-manage current 
  register: neutron_db_ver  
- name: upgrade db 
  ansible.builtin.command: neutron-manage db_sync 
  when: not neutron_db_ver.stdout is search('juno') 

在这里,我们请求在neutron_db_ver.stdout不包含juno字符串时运行名为upgrade db的任务。一旦你习惯了when: not ... is的概念,你会发现这个版本更容易理解,并且不会掩盖第一个任务的错误。

search过滤器搜索字符串,如果在输入字符串的任何位置找到子字符串,则返回True。但是,如果需要精确完整匹配,可以使用match过滤器。在search/match字符串内可以利用完整的 Python 正则表达式语法。

省略未定义的参数

omit变量需要一点解释。有时,在遍历数据哈希以构建任务参数时,可能只需要为哈希中的某些项目提供一些参数。即使 Jinja2 支持内联if语句来有条件地渲染一行的部分,但这在 Ansible 任务中效果不佳。传统上,playbook 作者会创建多个任务,每个任务针对传入的一组潜在参数,并使用条件语句在每个任务集之间对循环成员进行排序。最近添加的魔术变量omitdefault过滤器一起使用时解决了这个问题。omit变量将完全删除使用该变量的参数。

为了说明这是如何工作的,让我们考虑一个场景,我们需要使用ansible.builtin.pip安装一组 Python 包。一些包有特定版本,而其他包没有。这些包在一个名为pips的哈希列表中。每个哈希都有一个name键,可能还有一个ver键。我们的第一个示例利用了两个不同的任务来完成安装:

- name: install pips with versions 
  ansible.builtin.pip: "name={{ item.name }} version={{ item.ver }}"
  loop: "{{ pips }}"
  when: item.ver is defined  
- name: install pips without versions 
  ansible.builtin.pip: "name={{ item.name }}" 
  loop: "{{ pips }}"
  when: item.ver is undefined 

这种构造方式可以工作,但是循环会被迭代两次,并且每个任务中的一些迭代将被跳过。下面的示例将两个任务合并为一个,并利用omit变量:

- name: install pips 
  ansible.builtin.pip: "name={{ item.name }} version={{ item.ver | default(omit) }}" 
  loop: "{{ pips }}" 

这个示例更短、更清晰,不会生成额外的跳过任务。

Python 对象方法

Jinja2 是一个基于 Python 的模板引擎,因此 Python 对象方法在模板中是可用的。对象方法是直接由变量对象(通常是stringlistintfloat)访问的方法或函数。一个好的思路是:如果你在写 Python 代码时可以写变量,然后是一个句点,然后是一个方法调用,那么你在 Jinja2 中也可以做同样的事情。在 Ansible 中,通常只使用返回修改后的内容或布尔值的方法。让我们探索一些在 Ansible 中可能有用的常见对象方法。

字符串方法

字符串方法可以用来返回新的字符串,返回一组以某种方式被修改的字符串,或者测试字符串的各种条件并返回一个布尔值。一些有用的方法如下:

  • endswith:确定字符串是否以一个子字符串结尾。

  • startswith:与endswith相同,但是从开头开始。

  • split:将字符串按字符(默认为空格)分割成一个子字符串列表。

  • rsplit:与split相同,但是从字符串的末尾开始向后工作。

  • splitlines:将字符串在换行符处分割成一个子字符串列表。

  • upper:返回字符串的大写副本。

  • lower:返回字符串的小写副本。

  • capitalize:返回字符串的副本,只有第一个字符是大写的。

我们可以创建一个简单的 playbook,在一个任务中利用这些方法:

--- 
- name: demo the filters 
  hosts: localhost 
  gather_facts: false 

  tasks: 
    - name: string methods 
      ansible.builtin.debug: 
        msg: "{{ 'foo bar baz'.upper().split() }}" 

如果您正在使用本书附带的示例代码,请使用以下命令运行此 playbook:

ansible-playbook -i mastery-hosts template-demo-objects.yaml

输出将类似于图 6.22所示的内容:

图 6.22 - 运行使用 Python 字符串对象方法的 playbook

图 6.22 - 运行使用 Python 字符串对象方法的 playbook

由于这些是对象方法,我们需要使用点符号访问它们,而不是通过|过滤器。

列表方法

大多数 Ansible 提供的与列表相关的方法都是对列表本身进行修改。然而,在处理列表时,特别是涉及循环时,有两个列表方法非常有用。这两个函数分别是indexcount,它们的功能描述如下:

  • index:返回提供数值的第一个索引位置。

  • count:计算列表中的项目数。

当在循环中迭代列表时,这些函数可以非常有用,因为它允许执行位置逻辑,并在通过列表时采取适当的操作。这在其他编程语言中很常见,幸运的是,Ansible 也提供了这个功能。

int 和 float 方法

大多数intfloat方法对 Ansible 没有用。有时,我们的变量不完全符合我们想要的格式。但是,我们可以利用 Jinja2 过滤器在需要修改的各个地方执行操作,而不是定义更多的变量来轻微修改相同的内容。这使我们能够有效地定义数据,避免大量重复的变量和任务,这些变量和任务可能以后需要更改。

比较值

比较在 Ansible 中的许多地方都有用。任务条件是比较。 Jinja2 控制结构,如if/elif/else块,for循环和宏,通常使用比较;一些过滤器也使用比较。要掌握 Ansible 对 Jinja2 的使用,了解可用的比较是很重要的。

比较

与大多数语言一样,Jinja2 配备了您期望的标准比较表达式集,这些表达式将生成布尔值truefalse

Jinja2 中的表达式如下:

如果您在几乎任何其他编程语言中编写了比较操作(通常以if语句的形式),这些操作应该都很熟悉。Jinja2 在模板中保持了这种功能,允许进行与任何良好的编程语言中条件逻辑相同的强大比较操作。

逻辑

有时,单独执行一个比较操作是不够的 - 也许我们希望在两个比较同时评估为true时执行一个操作。或者,我们可能只想在一个比较不为 true 时执行一个操作。Jinja2 中的逻辑帮助您将两个或多个比较组合在一起,从简单的比较中形成复杂条件。每个比较被称为一个操作数,将这些操作数组合成复杂条件的逻辑在以下列表中给出:

  • and: 如果左操作数和右操作数为 true,则返回true

  • or: 如果左操作数或右操作数为 true,则返回true

  • not: 这否定一个操作数。

  • (): 这将一组操作数包装在一起,形成一个更大的操作数。

为了进一步定义 Jinja2 中的逻辑条件,我们可以对某些变量条件进行测试,比如变量是否已定义或未定义。我们将在下一节中更详细地讨论这个问题。

测试

Jinja2 中的测试用于确定变量是否符合某些明确定义的标准,在本章的特定场景中我们已经遇到过这种情况。is运算符用于启动测试。测试用于需要布尔结果的任何地方,例如if表达式和任务条件。有许多内置测试,但我们将重点介绍一些特别有用的测试,如下所示:

  • defined: 如果变量已定义,则返回true

  • undefined: 这是defined的相反。

  • none: 如果变量已定义但值为 none,则返回true

  • even: 如果数字可以被2整除,则返回true

  • odd: 如果数字不能被2整除,则返回true

要测试一个值是否不是某个值,只需使用is not

我们可以创建一个 playbook 来演示这些值的比较:

---
- name: demo the logic
  hosts: localhost
  gather_facts: false
  vars:
    num1: 10
    num3: 10
  tasks:
    - name: logic and comparison
      ansible.builtin.debug:
        msg: "Can you read me?"
      when: num1 >= num3 and num1 is even and num2 is not defined

如果您正在运行本书附带的代码,可以使用以下命令执行此示例 playbook:

ansible-playbook -i mastery-hosts template-demo-comparisons.yaml

输出显示在图 6.23中:

图 6.23 - 执行包含复杂条件的 playbook

图 6.23 - 执行包含复杂条件的 playbook

在这里,我们可以看到我们的复杂条件评估为true,因此执行了调试任务。

这就结束了我们对 Ansible 广泛的模板能力的探讨。我们希望本章为您提供了有效自动化基础设施的种子想法。

摘要

Jinja2 是 Ansible 广泛使用的强大语言。它不仅用于生成文件内容,还用于使 playbook 的部分动态化。精通 Jinja2 对于创建和维护优雅高效的 playbook 和角色至关重要。

在本章中,我们学习了如何使用 Jinja2 构建简单模板,并从 Ansible playbook 中呈现它们。此外,我们还学习了如何有效地使用控制结构,如何操作数据,甚至如何对变量进行比较和测试,以控制 Ansible playbook 的流程(通过保持代码轻量和高效)并创建和操作数据,而无需重复定义或过多的变量。

在下一章中,我们将更深入地探讨 Ansible 的能力,以定义 play 中任务的变化或失败。

问题

  1. Jinja2 条件可以用于在 playbook 任务中内联渲染内容。

a)真

b)假

  1. 以下哪个 Jinja2 结构将在每次评估时打印一个空行?

a){% if loop.first -%}

b){% if loop.first %}

c){%- if loop.first -%}

d){%- if loop.first %}

  1. Jinja2 宏可用于执行以下哪些操作?

a)定义需要自动化的一系列按键。

b)定义一个用于使用 Ansible 自动化电子表格的函数。

c)定义一个经常从模板中的其他位置调用的函数。

d)宏不在 Jinja2 中使用。

  1. 以下哪个是将两个 Jinja2 过滤器链接在一起并对 Ansible 变量进行操作的有效表达式?

a){{ value.replace('A', 'B').lower }}

b){{ value | replace('A', 'B') | lower }}

c)value.replace('A', 'B').lower

d)lower(replace('A', 'B',value))

  1. Jinja2 过滤器始终具有强制参数。

a)真

b)假

  1. 您将使用哪个 Ansible 自定义过滤器来从列表变量中检索随机条目?

a)洗牌

b)随机

c)选择

d)rand

  1. Ansible 可以使用哪个过滤器从完整路径中提取文件名?

a)文件名

b)dirname

c)expanduser

d)basename

  1. Ansible 提供了一个构造来跳过可选参数以防止未定义的变量错误。它叫什么?

a)skip_var

b)跳过

c)省略

d)prevent_undefined

  1. 可以使用哪些运算符为 Ansible 任务构建复杂的条件?

a)

b)nandnornot

c)&&||

d)|

  1. 以下哪个任务执行条件将允许任务在前一个任务成功完成时运行?

a)previoustask | success

b)previoustask = success

c)previoustask == success

d)previoustask 成功

第七章:控制任务条件

Ansible 是一个在一个或多个主机上运行任务的系统,并确保操作员了解是否发生了变更(以及是否遇到了任何问题)。因此,Ansible 任务会产生四种可能的状态:okchangedfailedskipped。这些状态执行了许多重要的功能。

从运行 Ansible playbook 的操作员的角度来看,它们提供了已完成的 Ansible 运行的概述——无论是否发生了任何变更,以及是否有任何需要解决的失败。此外,它们确定了 playbook 的流程——例如,如果一个任务的状态是changed,我们可能希望执行服务的重启,否则保持运行。Ansible 具有实现这一切所需的所有功能。

同样,如果一个任务的状态是failed,那么 Ansible 的默认行为就是不在该主机上尝试任何进一步的任务。任务还可以使用条件来检查先前任务的状态以控制操作。因此,这些状态或任务条件对于 Ansible 的几乎所有操作都是至关重要的,重要的是要了解如何处理它们,从而控制 playbook 的流程,以满足例如可能发生失败的情况。我们将在本章中详细讨论如何处理这些情况。

在本章中,我们将详细探讨这一点,特别关注以下主题:

  • 控制定义失败的内容

  • 从失败中恢复

  • 控制定义变更的内容

  • 使用循环迭代一组任务

技术要求

要跟随本章中提出的示例,您需要一台运行Ansible 4.3或更新版本的 Linux 机器。几乎任何 Linux 版本都可以使用——对于那些对细节感兴趣的人,本章中提供的所有代码都是在Ubuntu Server 20.04 LTS上测试的,除非另有说明,并且在 Ansible 4.3 上。本章附带的示例代码可以从 GitHub 的以下网址下载:github.com/PacktPublishing/Mastering-Ansible-Fourth-Edition/tree/main/Chapter07

查看以下视频以查看代码的实际操作:bit.ly/3AVXxME

定义失败

大多数与 Ansible 一起提供的模块对于什么构成错误有不同的标准。错误条件高度依赖于模块以及模块试图实现的内容。当一个模块返回错误时,主机将从可用主机集合中移除,阻止在该主机上执行任何进一步的任务或处理程序。此外,ansible-playbookansible可执行文件将以非零退出代码退出以指示失败。然而,我们并不受限于模块对错误的看法。我们可以忽略错误或重新定义错误条件。

忽略错误

名为ignore_errors的任务条件用于忽略错误。这个条件是一个布尔值,意味着值应该是Ansible理解为true的东西,比如yesontrue1(字符串或整数)。

为了演示如何使用ignore_errors,让我们创建一个 playbook,尝试查询一个不存在的 web 服务器。通常,这将是一个错误,如果我们不定义ignore_errors,我们将得到默认行为;也就是说,主机将被标记为失败,并且不会在该主机上尝试任何进一步的任务。创建一个名为error.yaml的新 playbook,如下所示,以进一步查看这种行为:

---
- name: error handling
  hosts: localhost
  gather_facts: false
  tasks:
  - name: broken website 
    ansible.builtin.uri: 
      url: http://notahost.nodomain 

使用以下命令运行此 playbook:

ansible-playbook -i mastery-hosts error.yaml

这本 playbook 中的单个任务应该导致一个看起来像图 7.1中所示的错误:

图 7.1 – 运行一个故意引发任务错误的 playbook

图 7.1 – 运行一个故意引发任务错误的 playbook

现在,假设我们不希望 Ansible 在这里停止,而是希望它继续。我们可以像这样在我们的任务中添加ignore_errors条件:

  - name: broken website 
    ansible.builtin.uri: 
      url: http://notahost.nodomain 
    ignore_errors: true 

这次,当我们使用与之前相同的命令运行 playbook 时,我们的错误将被忽略,如图 7.2所示:

图 7.2 - 运行相同的 playbook,但添加了任务条件

图 7.2 - 运行相同的 playbook,但添加了ignore_errors任务条件

对于该主机的任何进一步任务仍将尝试,并且 playbook 不会注册任何失败的主机。

定义错误条件

ignore_errors条件有点粗糙。来自任务使用的模块的任何错误都将被忽略。此外,乍一看,输出仍然看起来像一个错误,并且可能会让试图发现真正故障的操作员感到困惑。更微妙的工具是failed_when条件。这个条件更像是一把精细的手术刀,允许 playbook 作者非常具体地指出什么对于任务来说构成错误。这个条件执行一个测试来生成一个布尔结果,就像when条件一样。如果条件导致布尔true值,任务将被视为失败。否则,任务将被视为成功。

当与commandshell模块结合使用并注册执行结果时,failed_when条件非常有用。许多执行的程序可能具有详细的非零退出代码,意味着不同的含义。然而,这些 Ansible 模块都认为除0之外的任何退出代码都是失败。让我们看看iscsiadm实用程序。这个实用程序可以用于与 iSCSI 相关的许多事情。为了演示,我们将在error.yaml中替换我们的uri模块,并尝试发现任何活动的iscsi会话:

  - name: query sessions
    ansible.builtin.command: /sbin/iscsiadm -m session
    register: sessions

使用与之前相同的命令运行这个 playbook;除非您在具有活动 iSCSI 会话的系统上,否则您将看到与图 7.3非常相似的输出:

图 7.3 - 运行一个 playbook 来发现没有任何故障处理的活动 iSCSI 会话

图 7.3 - 运行一个 playbook 来发现没有任何故障处理的活动 iSCSI 会话

重要提示

iscsiadm工具可能不是默认安装的,如果是这样,您将得到与前面不同的错误。在我们的 Ubuntu Server 20.04 测试机器上,它是使用以下命令安装的:sudo apt install open-iscsi

我们可以只使用ignore_errors条件,但这将掩盖iscsi的其他问题,所以我们不想这样做,而是想指示 Ansible 退出代码21是可以接受的。为此,我们可以利用注册变量来访问rc变量,该变量保存返回代码。我们将在failed_when语句中使用这个:

  - name: query sessions
    command: /sbin/iscsiadm -m session
    register: sessions
    failed_when: sessions.rc not in (0, 21) 

我们只是声明除021之外的任何退出代码都应被视为失败。再次运行 playbook,但这次增加了详细信息,使用命令的-v标志,就像这样:

ansible-playbook -i mastery-hosts error.yaml -v

再次假设您没有活动的 iSCSI 会话,输出将如图 7.4所示。当然,使用-v标志并不是强制的,但在这种情况下很有帮助,因为它显示了iscsiadm实用程序的退出代码:

图 7.4 - 运行相同的 playbook,但根据命令退出代码处理故障

图 7.4 - 运行相同的 playbook,但根据命令退出代码处理故障

现在输出显示没有错误,实际上,我们在结果中看到了一个新的数据键 - failed_when_result。这显示了我们的failed_when语句是否渲染为truefalse;在这种情况下是false

许多命令行工具没有详细的退出代码。实际上,大多数通常使用0表示成功,另一个非零代码表示所有失败类型。幸运的是,failed_when语句不仅仅限于应用程序的退出代码;它是一个自由形式的布尔语句,可以访问任何所需的数据。让我们看一个不同的问题,涉及Git。我们将想象一个场景,我们想要确保Git检出中不存在特定的分支。此任务假定/srv/app目录中已经检出了Git存储库。删除Git分支的命令是git branch -D。让我们看一下以下代码片段:

  - name: delete branch bad
    ansible.builtin.command: git branch -D badfeature
    args:
      chdir: /srv/app

要使此代码工作,您需要将Git存储库检出到上一个目录中。如果您没有要测试的存储库,可以使用以下命令轻松创建一个(只需确保/srv/app中没有任何重要的内容会被覆盖!):

sudo mkdir -p /srv/app
sudo chown $USER /srv/app
cd /srv/app
git init
git commit --allow-empty -m "initial commit"

完成这些步骤后,您就可以运行我们之前详细介绍的更新后的 playbook 任务。与以前一样,我们将增加输出的详细信息,以便更好地理解我们 playbook 的行为。

重要提示

ansible.builtin.commandansible.builtin.shell模块使用不同的格式来提供模块参数。ansible.buitin.command本身以自由形式提供,而模块参数进入args哈希。

按照描述运行 playbook 应该会产生错误,因为git将产生一个退出代码为1的错误,因为分支不存在,如图 7.5所示:

图 7.5 - 在 Ansible playbook 中运行 git 命令而没有错误处理

图 7.5 - 在 Ansible playbook 中运行 git 命令而没有错误处理

如您所见,错误没有得到优雅处理,localhost的 play 已中止。

重要提示

我们使用ansible.builtin.command模块来轻松演示我们的主题,尽管存在ansible.builtin.git模块。处理 Git 存储库时,应改用ansible.builtin.git模块。

没有failed_whenchanged_when条件,我们将不得不创建一个两步任务组合来保护自己免受错误的影响:

  - name: check if branch badfeature exists
    ansible.builtin.command: git branch
    args:
      chdir: /srv/app
    register: branches
  - name: delete branch bad
    ansible.builtin.command: git branch -D badfeature
    args:
      chdir: /srv/app
    when: branches.stdout is search('badfeature')

在分支不存在的情况下,运行这些任务应该如图 7.6所示:

图 7.6 - 在 Ansible playbook 中使用两个任务处理错误

图 7.6 - 在 Ansible playbook 中使用两个任务处理错误

虽然两个任务集是功能性的,但并不高效。让我们改进这一点,并利用failed_when功能将两个任务减少到一个:

  - name: delete branch bad
    ansible.builtin.command: git branch -D badfeature
    args:
      chdir: /srv/app
    register: gitout
    failed_when:
      - gitout.rc != 0
      - not gitout.stderr is search('branch.*not found')

重要提示

通常会使用and连接的多个条件可以表示为列表元素。这可以使 playbooks 更易于阅读,逻辑问题更易于发现。

我们检查命令返回代码是否为0以外的任何值,然后使用search过滤器来搜索带有branch.*not found正则表达式的stderr值。我们使用 Jinja2 逻辑来组合这两个条件,这将评估为包容的truefalse选项,如图 7.7所示:

图 7.7 - 在 Ansible playbook 中单个任务内有效地处理错误

图 7.7 - 在 Ansible playbook 中单个任务内有效地处理错误

这演示了我们如何重新定义 Ansible playbook 中的失败,并优雅地处理否则会中断 play 的条件。我们还可以重新定义 Ansible 视为更改的内容,接下来我们将看到这一点。

定义更改

与定义任务失败类似,也可以定义什么构成了更改的任务结果。这种能力在ansible.builtin.command系列模块(commandshellrawscript)中特别有用。与大多数其他模块不同,这个系列的模块没有更改可能是什么的固有概念。事实上,除非另有指示,否则这些模块只会产生failedchangedskipped。对于这些模块来说,根本没有办法假设更改与未更改的条件,因为它们不能期望理解或解释您可能使用它们执行的每个可能的 shell 命令。

changed_when条件允许 playbook 的作者指示模块如何解释更改。就像failed_when一样,changed_when执行测试以生成布尔结果。经常与changed_when一起使用的任务是会以非零退出来指示不需要进行任何工作的命令;因此,作者经常会结合changed_whenfailed_when来微调任务结果的评估。

在我们之前的例子中,failed_when条件捕捉到了没有需要做的工作但任务仍然显示了更改的情况。我们希望在退出码0时注册更改,但在任何其他退出码时不注册更改。让我们扩展我们的示例任务以实现这一点:

  - name: delete branch bad
    ansible.builtin.command: git branch -D badfeature
    args:
      chdir: /srv/app
    register: gitout
    failed_when:
      - gitout.rc != 0
      - not gitout.stderr is search('branch.*not found')
    changed_when: gitout.rc == 0

现在,如果我们在分支仍不存在的情况下运行我们的任务(再次增加输出的详细程度,以帮助我们看到底层发生了什么),我们将看到类似于图 7.8所示的输出:

图 7.8 – 通过 changed_when 任务条件扩展我们的 Git playbook

图 7.8 – 通过 changed_when 任务条件扩展我们的 Git playbook

请注意,changed键现在的值为false

为了完整起见,我们将改变场景,使分支存在并再次运行它。要创建分支,只需从/srv/app目录运行git branch badfeature。现在,我们可以再次执行我们的 playbook 以查看输出,输出应该看起来像图 7.9所示:

图 7.9 – 在我们的测试存储库中存在 badfeature 分支时测试相同的 playbook

图 7.9 – 在我们的测试存储库中存在 badfeature 分支时测试相同的 playbook

这次,我们的输出不同了;它注册了一个更改,而stdout数据显示分支被删除了。

命令系列的特殊处理

命令系列模块的一个子集(ansible.builtin.commandansible.builtin.shellansible.builtin.script)有一对特殊参数,它们将影响任务工作是否已经完成,从而决定任务是否会导致更改。这些选项是createsremoves。这两个参数期望一个文件路径作为值。当 Ansible 尝试使用createsremoves参数执行任务时,它将首先检查引用的文件路径是否存在。

如果路径存在并且使用了creates参数,Ansible 将认为工作已经完成,并返回ok。相反,如果路径不存在并且使用了removes参数,那么 Ansible 将再次认为工作已经完成,并返回ok。任何其他组合将导致工作实际发生。预期是任务正在做的任何工作都将导致引用的文件的创建或删除。

createsremoves的便利性使开发人员无需进行两个任务的组合。让我们创建一个场景,我们想要从项目根目录的files/子目录运行frobitz脚本。在我们的场景中,我们知道frobitz脚本将创建一个路径/srv/whiskey/tango。实际上,frobitz的源代码如下:

#!/bin/bash 
rm -rf /srv/whiskey/tango 
mkdir -p /srv/whiskey/tango 

我们不希望这个脚本运行两次,因为它可能对任何现有数据造成破坏。替换我们的error.yaml playbook 中的现有任务,两个任务的组合将如下所示:

  - name: discover tango directory
    ansible.builtin.stat: path=/srv/whiskey/tango
    register: tango
  - name: run frobitz
    ansible.builtin.script: files/frobitz --initialize /srv/whiskey/tango
    when: not tango.stat.exists

像我们在本章中一样,以增加的详细程度运行 playbook。如果/srv/whiskey/tango路径已经存在,输出将如图 7.10所示:

图 7.10 – 一个两个任务的 play,有条件地运行破坏性脚本

图 7.10 – 一个两个任务的 play,有条件地运行破坏性脚本

如果/srv/whiskey/tango路径不存在,ansible.builtin.stat模块将返回更少的数据,exists键的值将为false。因此,我们的frobitz脚本将被运行。

现在,我们将使用creates将其减少为一个单独的任务:

  - name: run frobitz 
    ansible.builtin.script: files/frobitz 
    args:
      creates: /srv/whiskey/tango 

重要提示

ansible.builtin.script模块实际上是一个action_plugin,将在第十章中讨论,扩展 Ansible

这一次,我们的输出将会有些不同,如图 7.11所示:

图 7.11 – 通过将所有任务条件合并为一个任务使我们以前的 playbook 更加高效

图 7.11 – 通过将所有任务条件合并为一个任务使我们以前的 playbook 更加高效

这一次,我们完全跳过了运行脚本,因为在 playbook 甚至运行之前目录已经存在。这样可以节省 playbook 执行时间,也可以防止运行脚本可能导致的任何潜在破坏性行为。

重要提示

充分利用createsremoves将使您的 playbook 简洁高效。

抑制更改

有时,完全抑制更改是可取的。这经常用于执行命令以收集数据。命令执行实际上并没有改变任何东西;相反,它只是收集信息,就像ansible.builtin.setup模块一样。在这种任务上抑制更改可以帮助快速确定 playbook 运行是否导致了舰队中的任何实际更改。

要抑制更改,只需将false作为changed_when任务键的参数。让我们扩展我们以前的一个例子,以发现要抑制更改的活动iscsi会话:

  - name: discover iscsi sessions
    ansible.builtin.command: /sbin/iscsiadm -m session
    register: sessions
    failed_when:
      - sessions.rc != 0
      - not sessions.stderr is
        search('No active sessions')
    changed_when: false

现在,无论返回的数据是什么,Ansible 都会将任务视为ok而不是 changed,如图 7.12所示:

图 7.12 – 抑制 Ansible playbook 中的更改

图 7.12 – 抑制 Ansible playbook 中的更改

因此,这个任务现在只有两种可能的状态——failedok。我们实际上否定了changed任务结果的可能性。当然,运行代码时出现故障是生活的一部分,重要的是我们能够在 playbook 中优雅地处理这些问题。在下一节中,我们将看看在 Ansible 中如何实现这一点。

错误恢复

虽然错误条件可以被严格定义,但有时会发生真正的错误。Ansible 提供了一种方法来对真正的错误做出反应,一种允许在发生错误时运行附加任务的方法,定义特定任务,即使出现错误也始终执行,或者两者都执行。这种方法就是block功能。

block 功能是在 Ansible 2.0 版本中引入的,它为相关的 play 任务集提供了一些额外的结构。块可以将任务组合成一个逻辑单元,该单元(或块)可以对整个单元(或块)应用任务控制。此外,一组任务的块可以有可选的rescuealways部分,它们分别在错误状态下执行和不管错误状态如何执行。我们将在接下来的两个部分中探讨它们的工作原理。

使用 rescue 部分

blockrescue部分定义了一个逻辑单元的任务,当块内遇到实际失败时将执行。当 Ansible 执行块内的任务时,执行通常从上到下进行,当遇到实际失败时,执行将跳转到rescue部分的第一个任务(如果存在;此部分是可选的)。然后,任务将从上到下执行,直到到达rescue部分的末尾或遇到另一个错误为止。

rescue部分完成后,任务执行将继续进行,就像没有错误一样。这提供了一种优雅地处理错误的方式,允许定义cleanup任务,以便系统不会处于完全破碎的状态,并且 play 的其余部分可以继续。这比基于错误状态的一组复杂的任务注册结果和任务条件要干净得多。

为了演示这一点,让我们在一个块内创建一个新的任务集。这个任务集中将有一个未处理的错误,这将导致执行切换到rescue部分,从那里我们将执行一个cleanup任务。

我们还将在块之后提供一个任务,以确保执行继续。我们将重用error.yaml playbook:

---
- name: error handling
  hosts: localhost
  gather_facts: false
  tasks:
  - block:
      - name: delete branch bad
        ansible.builtin.command: git branch -D badfeature
        args:
          chdir: /srv/app
      - name: this task is lost
        ansible.builtin.debug:
          msg: "I do not get seen"

block部分中列出的两个任务按照它们列出的顺序执行。如果其中一个导致failed结果,那么rescue块中显示的以下代码将被执行:

    rescue:
      - name: cleanup task
        ansible.builtin.debug:
          msg: "I am cleaning up"
      - name: cleanup task 2
        ansible.builtin.debug:
          msg: "I am also cleaning up"

最后,无论之前的任务如何,都会执行这个任务。请注意,较低的缩进级别意味着它与块的相同级别运行,而不是作为block结构的一部分运行:

  - name: task after block
    ansible.builtin.debug:
      msg: "Execution goes on" 

尝试执行此 playbook 以观察其行为;像我们在本章中一样,向输出添加详细信息,以帮助您理解发生了什么。当此 play 执行时,第一个任务将导致错误,并且第二个任务将被跳过。执行将继续进行cleanup任务,并且应该如图 7.13所示:

图 7.13 - 执行包含救援部分的块的 playbook

图 7.13 - 执行包含救援部分的块的 playbook

不仅执行了rescue部分,而且整个 play 也完成了,并且整个ansible-playbook执行被认为是成功的,尽管块内的先前任务失败。让我们在下一节中通过查看块的always部分来扩展这个例子。

使用 always 部分

除了rescue,我们还可以使用另一个部分,名为always。块的这部分将始终执行,无论是否出现错误。这个功能对于确保系统状态始终保持功能非常方便,无论一组任务是否成功。由于一些块任务可能由于错误而被跳过,而rescue部分仅在出现错误时执行,always部分提供了在每种情况下执行任务的保证。

让我们扩展我们之前的例子,并向我们的块添加一个always部分:

    always:
      - name: most important task
        ansible.builtin.debug:
          msg: "Never going to let you down"

重新运行我们的 playbook,如前一节所示,我们可以看到额外的任务显示如下,如图 7.14所示:

图 7.14 - 运行包含救援和 always 部分的 Ansible playbook 的块

图 7.14 - 运行包含救援和 always 部分的 Ansible playbook 的块

为了验证always部分确实总是执行,我们可以修改 play,以便使用我们在前一节中开发的任务条件来使 Git 任务被认为是成功的。修改后的 play 的第一部分如下所示,供您参考:

---
- name: error handling
  hosts: localhost
  gather_facts: false
  tasks:
  - block:
      - name: delete branch bad
        ansible.builtin.command: git branch -D badfeature
        args:
          chdir: /srv/app
        register: gitout
        failed_when:
          - gitout.rc != 0
          - not gitout.stderr is search('branch.*not found')

请注意更改的failed_when条件,这将使git命令在不被视为失败的情况下运行。playbook 的其余部分(到目前为止在先前的示例中已经构建起来)保持不变。

这一次,当我们执行 playbook 时,我们的rescue部分被跳过,我们之前由于错误而被屏蔽的任务被执行,我们的always块仍然被执行,正如图 7.15所示:

图 7.15 - 执行一个包含救援和总是部分但没有任务错误的块的 playbook

图 7.15 - 执行一个包含救援和总是部分但没有任务错误的块的 playbook

还要注意,我们之前丢失的任务现在已经被执行,因为delete branch bad任务的失败条件已经更改,因此在此播放中不再失败。类似地,我们的rescue部分不再需要,并且所有其他任务(包括always部分)都如预期地完成。在 ansible 中处理由不可靠环境引起的错误的最后部分中,我们将看到如何处理这些错误。

处理不可靠的环境

到目前为止,在本章中,我们已经专注于优雅地处理错误,并改变了 ansible 对于更改和失败的默认行为。这对于任务来说都很好,但是如果您在一个不可靠的环境中运行 ansible 呢?例如,可能使用较差或瞬时的连接来到达受管主机,或者由于某种原因主机可能经常宕机。后一种情况可能是一个动态扩展的环境,可以在高负载时扩展,并在需求低时缩减以节省资源-因此您无法保证所有主机始终可用。

幸运的是,playbook 关键字ignore_unreachable恰好处理这些情况,并确保在我们的清单上尝试所有任务,即使在执行任务期间标记为不可达的主机。这与默认行为相反,即当 ansible 发生第一个错误时,将停止处理给定主机的任务。就像在许多情况下一样,最好通过一个例子来解释,所以让我们重用error.yaml playbook 来创建这样一个情况:

---
- name: error handling
  hosts: all
  gather_facts: false
  tasks:
  - name: delete branch bad
    ansible.builtin.command: git branch -D badfeature
    args:
      chdir: /srv/app
  - name: important task
    ansible.builtin.debug:
      msg: It is important we attempt this task!

我们将尝试从我们的清单中定义的两个远程主机的 Git 仓库中删除badfeature分支。这个清单将与本书中使用的其他清单有所不同,因为我们将故意创建两个不可达的虚构主机。这些主机的实际名称或定义的 IP 地址并不重要,但是为了使本节中描述的示例能够正常工作,这些主机必须是不可达的。我的清单文件如下所示:

[demo]
mastery.example.com ansible_host=192.168.10.25
backend.example.com ansible_host=192.168.10.26

由于我们故意创建了一个不存在的主机清单,我们知道它们将在尝试第一个任务时被标记为不可达。尽管如此,在第二个任务中仍然有一个绝对必须尝试的任务。让我们按原样运行 playbook,看看会发生什么;输出应该如图 7.16所示:

图 7.16 - 尝试在不可达主机清单上进行两个任务的播放

图 7.16 - 尝试在不可达主机清单上进行两个任务的播放

从输出中可以看出,名为important task的任务从未被尝试过-在第一个任务后播放被中止,因为主机不可达。然而,让我们使用我们新发现的标志来改变这种行为。将代码更改为如下所示:

---
- name: error handling
  hosts: all
  gather_facts: false
  tasks:
  - name: delete branch bad
    ansible.builtin.command: git branch -D badfeature
    args:
      chdir: /srv/app
    ignore_unreachable: true
  - name: important task
    ansible.builtin.debug:
      msg: It is important we attempt this task!

这一次,请注意,即使在第一次尝试时主机不可达,我们的第二个任务仍然被执行,正如图 7.17所示:

图 7.17 - 尝试在不可达主机上进行相同的两个任务播放,但这次忽略可达性

图 7.17 - 尝试在不可达主机上进行相同的两个任务播放,但这次忽略可达性

如果像debug命令一样,它可能在本地运行,或者它是至关重要的,并且即使在第一次尝试时连接失败也应该尝试。到目前为止,在本章中,你已经了解了 Ansible 提供的处理各种错误条件的工具。接下来,我们将继续探讨使用循环来控制任务流程——这是使代码简洁并防止重复的特别重要的工具。

使用循环的迭代任务

循环在本章中值得特别提及。到目前为止,我们已经专注于以自上而下的方式控制 playbook 的流程——我们已经改变了在 playbook 运行时可能被评估的各种条件,并且我们也专注于创建简洁、高效的代码。然而,如果你有一个单独的任务,但需要针对一组数据运行它会发生什么呢?例如,创建多个用户帐户、目录,或者更复杂的东西?

循环在 Ansible 2.5 中发生了变化——在此之前,循环通常是使用with_items等关键字创建的,你可能仍然在旧代码中看到这种情况。尽管一些向后兼容性仍然存在,但建议使用更新的loop关键字。

让我们举一个简单的例子——我们需要创建两个目录。创建loop.yaml如下:

---
- name: looping demo
  hosts: localhost
  gather_facts: false
  become: true
  tasks:
  - name: create a directory
    ansible.builtin.file:
      path: /srv/whiskey/alpha
      state: directory
  - name: create another directory
    ansible.builtin.file:
      path: /srv/whiskey/beta
      state: directory

当我们运行这个时,如预期的那样,我们的两个目录被创建了,就像图 7.18所示:

图 7.18 – 运行一个简单的 playbook 来创建两个目录

图 7.18 – 运行一个简单的 playbook 来创建两个目录

然而,你可以看到这段代码是重复的和低效的。相反,我们可以将其改为以下内容:

---
- name: looping demo
  hosts: localhost
  gather_facts: false
  become: true
  tasks:
  - name: create a directory
    ansible.builtin.file:
      path: "{{ item }}"
      state: directory
    loop:
      - /srv/whiskey/alpha
      - /srv/whiskey/beta

注意特殊的item变量的使用,它现在用于定义任务底部的loop项的path。现在,当我们运行这段代码时,输出看起来有些不同,就像图 7.19所示:

图 7.19 – 一个用循环创建相同两个目录的 playbook 一个用于更高效的代码的循环

图 7.19 – 一个用循环创建相同两个目录的 playbook,这次使用循环以获得更高效的代码

这两个目录仍然像以前一样被创建,但这次是在一个任务中。这使得我们的 playbook 更加简洁和高效。Ansible 提供了许多更强大的循环选项,包括嵌套循环和创建循环,直到满足给定条件(在其他语言中通常称为do until循环),而不是特定的有限数据集。

do until循环在等待满足某个条件时非常有用。例如,如果我们想要等待直到文件系统写入了一个标志文件,我们可以使用ansible.builtin.stat模块来查询文件,将模块运行的结果注册到一个变量中,然后在循环中运行,直到满足文件存在的条件。以下代码片段正是这样做的——它将循环(retries)五次,每次重试之间间隔 10 秒:

    - name: Wait until /tmp/flag exists
      ansible.builtin.stat:
        path: /tmp/flag
      register: statresult
      until: statresult.stat.exists
      retries: 5
      delay: 10

嵌套循环可以通过两种方式创建——要么通过对嵌套列表进行迭代,要么通过对包含的任务文件进行迭代。例如,假设我们想要在两个路径中分别创建两个新文件(由 Ansible 中的两个列表定义)。我们的代码可能是这样的:

---
- name: Nested loop example
  hosts: all
  gather_facts: no
  vars:
    paths:
      - /tmp
      - /var/tmp
    files:
      - test1
      - test2
  tasks:
    - name: Create files with nested loop
      ansible.builtin.file:
        path: "{{ item[0] }}/{{ item[1] }}"
        state: touch
      loop: "{{ paths | product(files) | list }}"

在这里,我们使用了product Jinja2 过滤器,将两个变量列表创建为嵌套列表,然后loop忠实地为我们迭代。运行这个 playbook 应该会产生类似图 7.20中的输出:

图 7.20 – 使用 product Jinja2 过滤器构建嵌套循环运行 playbook

图 7.20 – 使用 product Jinja2 过滤器构建嵌套循环运行 playbook

您还可以通过在外部循环中包含一个外部任务文件,然后在任务文件中放置一个内部循环来创建嵌套循环。现在,如果您这样做而不做任何进一步的操作,两个循环都将使用item循环变量,这当然会发生冲突。为了防止这成为一个问题,有必要使用特殊的loop_control参数之一来更改外部循环的循环变量名称。因此,使用与之前相同的标题代码和变量,我们可以将我们的原始任务更改为以下内容:

    - name: Create files with nested loop
      ansible.builtin.include_tasks: createfile.yml
      loop: "{{ paths }}"
      loop_control:
        loop_var: pathname

然后包含的任务文件将如下所示:

---
- name: Create a file
  ansible.builtin.file:
    path: "{{ pathname }}/{{ item }}"
    state: touch
  loop: "{{ files }}"

这段代码执行的功能与第一个嵌套循环示例完全相同,但稍微麻烦一些,因为它需要一个外部任务文件。此外,您将从图 7.21的屏幕截图中看到它的操作方式有些不同。在构建嵌套循环时,这一点很重要,因为这可能(或可能不)是您想要的:

图 7.21 - 通过包含的任务文件在 Ansible 中构建嵌套循环,使用 loop_control 变量

图 7.21 - 通过包含的任务文件在 Ansible 中构建嵌套循环,使用 loop_control 变量

可以说这种格式更容易阅读,但最终由您决定哪种更适合您的需求,以及是否有一种比另一种更适合您。有关循环创建技术和参数的完整详细信息,请参阅 Ansible 文档:docs.ansible.com/ansible/latest/user_guide/playbooks_loops.html

总结

在本章中,您了解到可以具体定义 Ansible 在运行特定任务时如何感知失败或更改,如何使用块来优雅地处理错误和执行清理,并且如何使用循环编写紧凑高效的代码。

因此,您现在应该能够修改任何给定任务,以提供特定条件,使得 Ansible 在失败或者考虑更改成功时失败。当运行 shell 命令时,这是非常有价值的,正如我们在本章中所演示的,也适用于定义现有模块的专门用例。您现在还应该能够将您的 Ansible 任务组织成块,确保如果发生故障,可以采取恢复操作,否则不需要运行。最后,您现在应该能够使用循环编写紧凑高效的 Ansible Playbook,消除重复代码和冗长低效的 Playbook 的需要。

在下一章中,我们将探讨使用角色来组织任务、文件、变量和其他内容。

问题

  1. 默认情况下,Ansible 在给定主机的第一个失败发生后将停止处理进一步的任务:

a) 真

b) 假

  1. ansible.builtin.commandansible.builtin.shell模块的默认行为是只给出任务状态为changedfailed

a) 真

b) 假

  1. 您可以使用哪个 Ansible 关键字存储任务的结果?

a) store:

b) variable:

c) register:

d) save:

  1. 以下哪个指令可以用来改变任务的失败条件?

a) error_if:

b) failed_if:

c) error_when:

d) failed_when:

  1. 您可以使用以下哪个来组合多个条件语句?

a) and

b)

c) YAML 列表格式(与逻辑AND相同)

d) 以上所有

  1. 以下哪个可以抑制更改?

a) suppress_changed: true

b) changed_when: false

c) changed: false

d) failed_when: false

  1. block部分中,所有任务都按顺序在所有主机上执行:

a) 直到发生第一个错误

b) 无论任何错误条件

  1. 块任务中的哪个可选部分只有在块任务中发生错误时才运行?

a) recover

b) rescue

c) always

d) on_error

  1. 块中的always部分中的任务将被运行:

a) 无论发生了什么,无论是在块任务还是在rescue部分

b) 只有在rescue部分没有运行时

c) 只有在没有遇到错误时

d) 当用户手动调用时

  1. 循环中引用当前元素的变量的默认名称是:

a) loopvar

b) loopitem

c) item

d) val

第八章:使用角色组合可重用的 Ansible 内容

对于许多项目,一个简单的、单一的Ansible剧本可能就足够了。随着时间的推移和项目的增长,会添加额外的剧本和变量文件,并且任务文件可能会被拆分。组织内的其他项目可能希望重用一些内容,要么将项目添加到目录树中,要么将所需内容复制到多个项目中。随着场景的复杂性和规模的增长,远不止一个松散组织的一小部分剧本、任务文件和变量文件是非常需要的。创建这样的层次结构可能是令人生畏的,这也可以解释为什么许多 Ansible 实现一开始都很简单,只有在分散的文件变得难以控制和难以维护时才变得更加有组织。迁移可能很困难,并且可能需要重写剧本的重要部分,这可能会进一步延迟重新组织的努力。

在本章中,我们将介绍在 Ansible 中组合、可重用和组织良好的最佳实践。本章中学到的经验将帮助开发人员设计能够与项目良好增长的 Ansible 内容,避免以后需要进行困难的重新设计工作。以下是我们将要涵盖的内容大纲:

  • 任务、处理程序、变量和剧本包含概念

  • 角色(结构、默认值和依赖项)

  • 设计顶层剧本以利用角色

  • 在项目之间共享角色(通过 Galaxy 进行依赖项;类似 Git 的存储库)

技术要求

要按照本章中提供的示例,您需要一台运行Ansible 4.3或更新版本的 Linux 机器。几乎任何 Linux 版本都可以——对于那些对具体情况感兴趣的人,本章中提供的所有代码都是在Ubuntu Server 20.04 长期支持版LTS)上测试的,除非另有说明,并且在 Ansible 4.3 上也进行了测试。

本章附带的示例代码可以从 GitHub 的以下链接下载:github.com/PacktPublishing/Mastering-Ansible-Fourth-Edition/tree/main/Chapter08

查看以下视频,了解代码的实际操作:bit.ly/3E0mmIX

任务、处理程序、变量和剧本包含概念

了解如何高效组织 Ansible 项目结构的第一步是掌握包含文件的概念。包含文件的行为允许在一个专题文件中定义内容,并在项目中的一个或多个文件中包含这些内容。这种包含功能支持不要重复自己DRY)的概念。

包括任务

任务文件是YAML Ain't Markup LanguageYAML)文件,用于定义一个或多个任务。这些任务与任何特定的游戏或剧本没有直接联系;它们纯粹存在作为任务列表。这些文件可以通过include运算符被剧本或其他任务文件引用。现在,您可能期望include运算符是 Ansible 自己的关键字——然而,事实并非如此;它实际上是一个模块,就像ansible.builtin.debug一样。为了简洁起见,我们在本章中将其称为include运算符,但当我们说这个时候,您的代码实际上将包含Fully Qualified Collection NameFQCN—参见第二章从早期 Ansible 版本迁移),即ansible.builtin.include。您很快就会看到它的作用,所以不用担心——这一切很快就会讲得通!这个运算符接受一个任务文件的路径,正如我们在第一章中学到的那样,Ansible 的系统架构和设计,路径可以是相对于引用它的文件的。

为了演示如何使用include运算符来包含任务,让我们创建一个简单的 play,其中包含一个带有一些调试任务的任务文件。首先,让我们编写我们的 playbook 文件,我们将其命名为includer.yaml,如下所示:

--- 
- name: task inclusion 
  hosts: localhost 
  gather_facts: false 

  tasks: 
  - name: non-included task
    ansible.builtin.debug:
      msg: "I am not included"
  - ansible.builtin.include: more-tasks.yaml

接下来,我们将创建一个more-tasks.yaml文件,你可以在include语句中看到它的引用。这应该在保存includer.yaml的同一目录中创建。代码如下所示:

--- 
- name: included task 1 
  ansible.builtin.debug: 
    msg: "I am the first included task" 

- name: included task 2 
  ansible.builtin.debug: 
    msg: "I am the second included task" 

现在,我们可以使用以下命令执行我们的 playbook 以观察输出:

ansible-playbook -i mastery-hosts includer.yaml

如果一切顺利,你应该看到类似于这样的输出:

图 8.1 - 执行包含单独任务文件的 Ansible playbook

图 8.1 - 执行包含单独任务文件的 Ansible playbook

我们可以清楚地看到我们的include文件执行的任务。因为include运算符是在 play 的tasks部分中使用的,所以包含的任务在该 play 中执行。实际上,如果我们在include运算符之后向 play 添加一个任务,如下面的代码片段所示,我们会看到执行顺序遵循包含文件的所有任务存在的位置:

  tasks:
  - name: non-included task
    ansible.builtin.debug:
      msg: "I am not included"
  - ansible.builtin.include: more-tasks.yaml
  - name: after-included tasks
    ansible.builtin.debug:
      msg: "I run last"

如果我们使用与之前相同的命令运行我们修改后的 playbook,我们将看到我们期望的任务顺序,如下面的截图所示:

图 8.2 - 演示使用 include 运算符的 playbook 中任务执行顺序

图 8.2 - 演示使用 include 运算符的 playbook 中任务执行顺序

通过将这些任务拆分成它们自己的文件,我们可以多次包含它们或在多个 playbook 中包含它们。如果我们需要修改其中一个任务,我们只需要修改一个文件,无论这个文件被引用了多少次。

将变量值传递给包含的任务

有时,我们想要拆分一组任务,但这些任务的行为可能会根据变量数据略有不同。include运算符允许我们在包含时定义和覆盖变量数据。定义的范围仅限于包含的任务文件(以及该文件可能包含的任何其他文件)。

为了说明这种能力,让我们创建一个新的场景,我们需要触摸两个文件,每个文件都在自己的目录路径中。我们将创建一个任务文件,其中包含每个任务的变量名称。然后,我们将两次包含任务文件,每次传递不同的数据。首先,我们将使用files.yaml任务文件,如下所示:

---
- name: create leading path
  ansible.builtin.file:
    path: "{{ path }}"
    state: directory
- name: touch the file
  ansible.builtin.file:
    path: "{{ path + '/' + file }}"
    state: touch

接下来,我们将修改我们的includer.yaml playbook,包含我们刚刚创建的任务文件,并传递pathfile变量的变量数据,如下所示:

---
- name: touch files
  hosts: localhost
  gather_facts: false
  tasks:
  - ansible.builtin.include: files.yaml
    vars:
      path: /tmp/foo
      file: herp
  - ansible.builtin.include: files.yaml
    vars:
      path: /tmp/foo
      file: derp

重要提示

在包含文件时提供的变量定义可以是key=value的内联格式,也可以是key: value的 YAML 格式,位于vars哈希内。

当我们运行这个 playbook 时,我们将看到四个任务被执行:两个任务来自包含的files.yaml文件,每个任务执行两次。第二组应该只有一个更改,因为两组的路径相同,并且应该在执行任务时创建。通过使用以下命令添加详细信息来运行 playbook,以便我们可以更多地了解底层发生了什么:

ansible-playbook -i mastery-hosts includer.yaml -v

运行此 playbook 的输出应该类似于这样:

图 8.3 - 运行一个包含两个不同变量数据的任务文件的 playbook

图 8.3 - 运行一个包含两个不同变量数据的任务文件的 playbook

正如我们在这里所看到的,用于创建前导路径和文件的代码被重复使用,每次只是使用不同的值,使我们的代码非常高效易于维护。

将复杂数据传递给包含的任务

当想要向包含的任务传递复杂数据,比如列表或哈希时,可以在包含文件时使用另一种语法。让我们重复上一个场景,只是这次不是两次包含任务文件,而是一次包含并传递路径和文件的哈希。首先,我们将重新创建files.yaml文件,如下所示:

--- 
- name: create leading path 
  ansible.builtin.file: 
    path: "{{ item.value.path }}" 
    state: directory 
  loop: "{{ files | dict2items }}" 

- name: touch the file 
  ansible.builtin.file: 
    path: "{{ item.value.path + '/' + item.key }}" 
    state: touch 
  loop: "{{ files | dict2items }}" 

现在,我们将修改我们的includer.yaml playbook,以提供文件的哈希值在单个ansible.builtin.include语句中,如下所示:

---
- name: touch files
  hosts: localhost
  gather_facts: false
  tasks:
  - ansible.builtin.include: files.yaml
    vars:
      files:
        herp:
          path: /tmp/foo
        derp:
          path: /tmp/foo

如果我们像以前一样运行这个新的 playbook 和任务文件,我们应该会看到一个类似但略有不同的输出,最终结果是/tmp/foo目录已经存在,并且两个herpderp文件被创建为空文件(被触摸)在其中,如下面的截图所示:

图 8.4 - 将复杂数据传递给 Ansible play 中包含的任务文件

图 8.4 - 将复杂数据传递给 Ansible play 中包含的任务文件

使用这种方式传递数据的哈希允许创建一组事物,而无需在主 playbook 中增加include语句的数量。

条件任务包括

类似于将数据传递给包含的文件,条件也可以传递给包含的文件。这是通过将when语句附加到include运算符来实现的。这个条件并不会导致 Ansible 评估测试以确定是否应该包含文件;相反,它指示 Ansible 将条件添加到包含文件中的每个任务以及该文件可能包含的任何其他文件中。

重要提示

不可能有条件地包含一个文件。文件将始终被包含;但是,可以对include层次结构中的每个任务应用任务条件。

让我们通过修改包含简单调试语句的第一个示例来演示这一点。我们将添加一个条件并传递一些数据供条件使用。首先,让我们修改includer.yaml playbook,如下所示:

---
- name: task inclusion
  hosts: localhost
  gather_facts: false
  tasks:
  - ansible.builtin.include: more-tasks.yaml
    when: item | bool
    vars:
      a_list:
        - true
        - false

接下来,让我们修改more-tasks.yaml,在每个任务中循环a_list变量,如下所示:

---
- name: included task 1
  ansible.builtin.debug:
    msg: "I am the first included task"
  loop: "{{ a_list }}"
- name: include task 2
  ansible.builtin.debug:
    msg: "I am the second included task"
  loop: "{{ a_list }}"

现在,让我们用与之前相同的命令运行 playbook,并查看我们的新输出,应该是这样的:

图 8.5 - 将条件应用于包含文件中的所有任务

图 8.5 - 将条件应用于包含文件中的所有任务

我们可以看到每个任务的跳过迭代,其中item被评估为false布尔值。重要的是要记住,所有主机都将评估所有包含的任务。没有办法影响 Ansible 不为一部分主机包含文件。最多,可以对include层次结构中的每个任务应用条件,以便可以跳过包含的任务。根据主机事实包含任务的一种方法是利用ansible.builtin.group_by动作插件根据主机事实创建动态组。然后,您可以为这些组提供自己的 play 以包含特定的任务。这是留给您的一个练习。

对包含的任务进行标记

在包含任务文件时,可以对文件中的所有任务进行标记。tags关键字用于定义要应用于include层次结构中所有任务的一个或多个标记。在include时进行标记的能力可以使任务文件本身不对任务应该如何标记持有意见,并且可以允许一组任务被多次包含,但传递不同的数据和标记。

重要提示

可以在include语句或 play 本身中定义标记,以覆盖给定 play 中所有包含(和其他任务)。

让我们创建一个简单的演示来说明标记如何使用。我们将首先编辑我们的includer.yaml文件,创建一个包含任务文件的 playbook,每个任务文件都有不同的标记名称和不同的变量数据。代码如下所示:

---
- name: task inclusion
  hosts: localhost
  gather_facts: false
  tasks:
  - ansible.builtin.include: more-tasks.yaml
    vars:
      data: first
    tags: first
  - ansible.builtin.include: more-tasks.yaml
    vars:
      data: second
    tags: second

现在,我们将更新more-tasks.yaml以处理提供的数据,如下所示:

---
- name: included task
  ansible.builtin.debug:
    msg: "My data is {{ data }}"

如果我们在不选择标记的情况下运行这个 playbook,我们将看到这个任务运行两次,如下的屏幕截图所示:

图 8.6 - 运行带有标记的包含任务的 playbook,但没有启用任何基于标记的过滤

图 8.6 - 运行带有标记的包含任务的 playbook,但没有启用任何基于标记的过滤

现在,我们可以通过修改我们的ansible-playbook参数来选择要运行的标记,比如第二个标记,如下所示:

ansible-playbook -i mastery-hosts includer.yaml -v --tags second

在这种情况下,我们应该只看到被包含任务的发生,如下的屏幕截图所示:

图 8.7 - 运行带有标记的包含任务的 playbook,只运行标记为"second"的任务

图 8.7 - 运行带有标记的包含任务的 playbook,只运行标记为"second"的任务

我们的示例使用--tags命令行参数来指示要运行的标记任务。另一个参数--skip-tags允许表示相反的意思,或者换句话说,不要运行哪些标记的任务。

循环中的任务包含

任务包含也可以与循环结合使用。当向任务包含添加一个loop实例(或者如果使用早于 2.5 版本的 Ansible,则使用with_循环),文件内的任务将使用item变量执行,该变量保存当前循环值的位置。整个include文件将重复执行,直到循环用完项目。让我们更新我们的示例 play 来演示这一点,如下所示:

---
- name: task inclusion
  hosts: localhost
  gather_facts: false
  tasks:
  - ansible.builtin.include: more-tasks.yaml
    loop:
      - one
      - two

我们还需要更新我们的more-tasks.yaml文件,以使用循环item变量,如下所示:

--- 
- name: included task 1 
  ansible.builtin.debug: 
    msg: "I am the first included task with {{ item }}"
- name: included task 2 
  ansible.builtin.debug: 
    msg: "I am the second included task with {{ item }}" 

当以增加的详细程度执行时,我们可以看到任务12针对循环中的每个item变量执行一次,如下的屏幕截图所示:

图 8.8 - 在循环中运行包含的任务文件

图 8.8 - 在循环中运行包含的任务文件

包含循环是一个强大的概念,但它确实引入了一个问题。如果包含的文件中有其自己循环的任务,会产生item变量的冲突,导致意外的结果。因此,在 Ansible 的 2.1 版本中添加了loop_control功能。除其他功能外,此功能提供了一种方法来命名用于循环的变量,而不是默认的item。使用这个功能,我们可以区分include语句外部的item实例和include语句内部使用的任何item变量。为了演示这一点,我们将在我们的外部include语句中添加一个loop_var循环控制,如下所示:

---
- name: task inclusion
  hosts: localhost
  gather_facts: false
  tasks:
    - ansible.builtin.include: more-tasks.yaml
      loop:
        - one
        - two
      loop_control:
        loop_var: include_item

more-tasks.yaml中,我们将有一个带有自己循环的任务,使用include_item和本地的item变量,如下所示:

--- 
- name: included task 1 
  ansible.builtin.debug: 
    msg: "I combine {{ item }} and {{ include_item }}" 
  loop: 
    - a 
    - b 

当执行时,我们看到每次包含循环都会执行任务 1两次,并且使用了两个loop变量,如下的屏幕截图所示:

图 8.9 - 在包含的任务文件中运行嵌套循环,避免循环变量名称冲突

图 8.9 - 在包含的任务文件中运行嵌套循环,避免循环变量名称冲突

还有其他循环控制,比如label,它将定义在任务输出中显示在屏幕上的item值(用于防止大型数据结构在屏幕上混乱),以及pause,提供在每个循环之间暂停一定秒数的能力。

包括处理程序

处理程序本质上是任务。它们是由其他任务的通知触发的一组潜在任务。因此,处理程序任务可以像常规任务一样被包含。include运算符在handlers块内是合法的。

与任务包含不同,当包含handler任务时,无法传递变量数据。但是,可以将条件附加到handler包含中,以将条件应用于文件中的每个handler任务。

让我们创建一个示例来演示这一点。首先,我们将创建一个总是会改变的任务的 playbook,并包含一个handler任务文件,并将条件附加到该包含中。代码如下所示:

--- 
- name: touch files 
  hosts: localhost 
  gather_facts: false 

  tasks:
  - name: a task
    ansible.builtin.debug:
      msg: "I am a changing task"
    changed_when: true
    notify: a handler
  handlers:
  - ansible.builtin.include: handlers.yaml
    when: foo | default('true') | bool

重要提示

在评估可能在 playbook 外定义的变量时,最好使用bool过滤器来确保字符串被正确转换为它们的布尔含义。

接下来,我们将创建一个handlers.yaml文件来定义我们的handler任务,如下所示:

---
- name: a handler
  ansible.builtin.debug:
    msg: "handling a thing"

如果我们在不提供任何进一步数据的情况下执行这个 playbook,我们应该看到我们的handler任务被触发,如下面的截图所示:

图 8.10 - 使用包含运算符从任务文件运行处理程序

图 8.10 - 使用包含运算符从任务文件运行处理程序

现在,让我们再次运行 playbook;这次,我们将在ansible-playbook执行参数中将foo定义为extra-var(覆盖每个其他实例),并将其设置为false,如下所示:

ansible-playbook -i mastery-hosts includer.yaml -v -e foo=false

这次,输出将看起来有些不同,如下面的截图所示:

图 8.11 - 运行相同的 play,但这次强制 foo 条件变量为 false

图 8.11 - 运行相同的 play,但这次强制 foo 条件变量为 false

由于foo评估为false,所以在这次运行 playbook 时我们的包含处理程序被跳过了。

包含变量

变量数据也可以分开成可加载的文件。这允许在多个 play 或 playbook 之间共享变量,或者包含项目目录之外的变量数据(如秘密数据)。变量文件是简单的YAML 格式文件,提供键和值。与任务包含文件不同,变量包含文件不能包含更多文件。

变量可以通过三种不同的方式包含:通过vars_files,通过include_vars,或通过--extra-vars(-e)。

vars_files

vars_files键是一个 play 指令。它定义了要从中读取变量数据的文件列表。这些文件在解析 playbook 本身时被读取和解析。与包含任务和处理程序一样,路径是相对于引用文件的文件的。

这是一个从文件加载变量的示例 play:

--- 
- name: vars 
  hosts: localhost 
  gather_facts: false 

  vars_files:
  - variables.yaml
  tasks:
  - name: a task
    ansible.builtin.debug:
      msg: "I am a {{ varname }}" 

现在,我们需要在与我们的 playbook 相同的目录中创建一个variables.yaml文件,如下所示:

---
varname: derp 

使用我们通常的命令运行 playbook 将显示varname变量值正确地从variables.yaml文件中获取,如下面的截图所示:

图 8.12 - 使用 vars_files 指令在 play 中包含变量

图 8.12 - 使用 vars_files 指令在 play 中包含变量

当然,这只是一个非常简单的例子,但它清楚地演示了从单独文件导入变量的简易性。

动态 vars_files 包含

在某些情况下,希望参数化要加载的变量文件。可以通过使用变量作为文件名的一部分来实现这一点;然而,变量必须在解析 playbook 时有一个定义的值,就像在任务名称中使用变量时一样。让我们根据执行时提供的数据更新我们的示例 play,以加载基于数据提供的变量文件,如下所示:

--- 
- name: vars 
  hosts: localhost 
  gather_facts: false 

  vars_files:
  - "{{ varfile }}"
  tasks:
  - name: a task
    ansible.builtin.debug:
      msg: "I am a {{ varname }}"

现在,当我们执行 playbook 时,我们将使用类似以下命令的-e参数为varfile提供值:

ansible-playbook -i mastery-hosts includer.yaml -v -e varfile=variables.yaml

输出应该如下所示:

图 8.13 - 在 playbook 运行时动态加载 variables.yaml 文件

图 8.13 - 在 playbook 运行时动态加载 variables.yaml 文件

除了需要在执行时定义变量值之外,要加载的文件也必须在执行时存在。即使文件是由 Ansible playbook 自己生成的,这条规则也适用。假设一个 Ansible playbook 由四个 play 组成。第一个 play 生成一个 YAML 变量文件。然后,在更下面,第四个 play 在vars_file指令中引用这个文件。尽管最初看起来这似乎会起作用,但是文件在执行时(即首次运行ansible-playbook时)并不存在,因此会报告错误。

include_vars

包含从文件中加载变量数据的第二种方法是通过include_vars模块。该模块将变量作为task操作加载,并将为每个主机执行。与大多数模块不同,此模块在 Ansible 主机上本地执行;因此,所有路径仍然相对于 play 文件本身。由于变量加载是作为任务执行的,因此在执行任务时会评估文件名中的变量。文件名中的变量数据可以是特定于主机的,并在前面的任务中定义。此外,文件本身在执行时不必存在;它也可以由前面的任务生成。如果使用正确,这是一个非常强大和灵活的概念,可以导致非常动态的 playbook。

在我们继续之前,让我们通过修改现有的 play 来演示include_vars的简单用法,将变量文件加载为一个任务,如下所示:

--- 
- name: vars 
  hosts: localhost 
  gather_facts: false 

  tasks: 
    - name: load variables 
      ansible.builtin.include_vars: "{{ varfile }}" 

    - name: a task 
      ansible.builtin.debug: 
        msg: "I am a {{ varname }}" 

与前面的示例一样,playbook 的执行与之前的示例中保持一致,我们将varfile变量的值指定为额外变量。我们的输出与以前的迭代略有不同,如下面的截图所示:

图 8.14 - 运行使用 include_vars 语句的 playbook

图 8.14 - 运行使用 include_vars 语句的 playbook

与其他任务一样,可以循环执行以在单个任务中加载多个文件。当使用特殊的with_first_found循环通过一系列越来越通用的文件名迭代直到找到要加载的文件时,这是特别有效的。

让我们通过更改我们的 play 来演示这一点,使用收集的主机事实来尝试加载特定于分发的变量文件,特定于分发系列,或者最后是默认文件,如下所示:

---
- name: vars
  hosts: localhost
  gather_facts: true
  tasks:
  - name: load variables
    ansible.builtin.include_vars: "{{ item }}"
    with_first_found:
      - "{{ ansible_distribution }}.yaml"
      - "{{ ansible_os_family }}.yaml"
      - variables.yaml
  - name: a task
    ansible.builtin.debug:
      msg: "I am a {{ varname }}"

执行应该看起来与以前的运行非常相似,只是这次我们将看到一个收集事实的任务,并且在执行中不会传递额外的变量数据。输出应该如下所示:

图 8.15 - 动态包含在 Ansible play 中找到的第一个有效变量文件

图 8.15 - 动态包含在 Ansible play 中找到的第一个有效变量文件

我们还可以从输出中看到找到要加载的文件。在这种情况下,variables.yaml被加载,因为其他两个文件不存在。这种做法通常用于加载特定于主机的操作系统的变量。可以将各种操作系统的变量写入适当命名的文件中。通过使用由收集事实填充的ansible_distribution变量,可以通过with_first_found参数加载使用ansible_distribution值作为其名称一部分的变量文件。可以在一个不使用任何变量数据的文件中提供一组默认变量作为备用,就像我们在variables.yaml文件中所做的那样。

extra-vars

从文件中加载变量数据的最终方法是使用--extra-vars(或-e)参数引用文件路径到ansible-playbook。通常,此参数期望一组key=value数据;但是,如果提供了文件路径并以@符号为前缀,Ansible 将读取整个文件以加载变量数据。让我们修改我们之前的一个示例,其中我们使用了-e,而不是直接在命令行上定义变量,我们将包含我们已经编写的变量文件,如下所示:

--- 
- name: vars 
  hosts: localhost 
  gather_facts: false 

  tasks:
  - name: a task
    ansible.builtin.debug:
      msg: "I am a {{ varname }}" 

当我们在@符号后提供路径时,该路径是相对于当前工作目录的,而不管 playbook 本身位于何处。让我们执行我们的 playbook 并提供variables.yaml的路径,如下所示:

ansible-playbook -i mastery-hosts includer.yaml -v -e @variables.yaml

输出应该如下所示:

图 8.16 - 通过额外的变量命令行参数包含 variables.yaml 文件

图 8.16 - 通过额外的变量命令行参数包含 variables.yaml 文件

在这里,我们可以看到我们的variables.yaml文件再次成功包含,但是,正如您从前面的代码中看到的那样,它甚至没有在 playbook 中提到 - 我们能够通过-e标志完全加载它。

重要提示

使用--extra-vars参数包含变量文件时,文件必须在ansible-playbook执行时存在。

在 Ansible 中,变量包含非常强大 - 但是 playbooks 本身呢?在这里,情况有所不同,随着本章的进行,我们将看到如何有效地重用任务和 playbook 代码,从而鼓励使用 Ansible 进行良好的编程实践。

包含 playbooks

Playbook 文件可以包含其他整个 playbook 文件。这种结构对于将几个独立的 playbook 绑定成一个更大、更全面的 playbook 非常有用。Playbook 包含比任务包含更为原始。在包含 playbook 时,您不能执行变量替换,也不能应用条件,也不能应用标签。要包含的 playbook 文件必须在执行时存在。

在 Ansible 2.4 之前,可以使用include关键字来实现 playbook 包含 - 但是在 Ansible 2.8 中已将其删除,因此不应使用。相反,现在应该使用ansible.builtin.import_playbook。这是一个 play 级别的指令 - 不能用作任务。但是,它非常容易使用。让我们定义一个简单的示例来演示这一点。首先,让我们创建一个将被包含的 playbook,名为includeme.yaml。以下是要执行此操作的代码:

---
- name: include playbook
  hosts: localhost
  gather_facts: false
  tasks:
  - name: an included playbook task
    ansible.builtin.debug:
      msg: "I am in the included playbook"

正如您现在无疑已经认识到的那样,这是一个完整的独立 playbook,我们可以使用以下命令单独运行它:

ansible-playbook -i mastery-hosts includeme.yaml

成功运行将产生如下所示的输出:

图 8.17 - 首先作为独立 playbook 运行我们的 playbook

图 8.17 - 首先作为独立 playbook 运行我们的 playbook

但是,我们也可以将其导入到另一个 playbook 中。修改原始的includer.yaml playbook,使其如下所示:

---
- name: include playbook
  hosts: localhost
  gather_facts: false
  tasks:
  - name: a task
    ansible.builtin.debug:
      msg: "I am in the main playbook"
- name: include a playbook
  ansible.builtin.import_playbook: includeme.yaml

然后使用以下命令运行它:

ansible-playbook -i mastery-hosts includer.yaml

我们可以看到两条调试消息都显示出来,并且导入的 playbook 在初始任务之后运行,这是我们在原始 playbook 中定义的顺序。以下截图显示了这一点:

图 8.18 - 运行包含第二个 playbook 的 playbook

图 8.18 - 运行包含第二个 playbook 的 playbook

通过这种方式,非常容易地重用整个 playbooks,而无需将它们重构为角色、任务文件或其他格式。但是,请注意,此功能正在积极开发中,因此建议您始终参考文档,以确保您可以实现所需的结果。

角色(结构、默认值和依赖关系)

通过对变量、任务、处理程序和剧本的包含的功能理解,我们可以进一步学习角色的更高级主题。角色将 Ansible 代码创建的不同方面结合在一起,提供了一套完全独立的变量、任务、文件、模板和模块的集合,可以在不同的剧本中重复使用。尽管设计上并不受限制,但通常每个角色通常被限制在特定的目的或期望的最终结果上,所有必要的步骤要么在角色本身内,要么通过依赖项(换句话说,进一步的角色本身被指定为角色的依赖项)中。重要的是要注意,角色不是剧本,也没有直接执行角色的方法。角色没有设置适用于哪些主机的设置。顶层剧本是将清单中的主机与应该应用于这些主机的角色绑定在一起的粘合剂。正如我们在第二章中所看到的,从早期 Ansible 版本迁移,角色也可以是 Ansible 集合的一部分。由于我们已经在早期章节中看过集合的结构,因此在本节中,我们将更深入地关注如何构建角色本身。

角色结构

角色文件系统上有一个结构化的布局。这个结构存在是为了自动包含任务、处理程序、变量、模块和角色依赖关系。该结构还允许轻松地从角色内的任何位置引用文件和模板。

第二章中,从早期 Ansible 版本迁移,我们将看看如何从集合中引用角色。但是,它们不一定要作为集合的一部分使用,假设您在这种情况之外使用角色,它们都位于roles/目录下的剧本目录结构的子目录中。当然,这可以通过roles_path通用配置键进行配置,但让我们坚持使用默认值。每个角色本身都是一个目录树。角色名称是roles/目录中的目录名称。每个角色可以有许多具有特殊含义的子目录,在将角色应用于一组主机时会进行处理。

一个角色可以包含所有这些元素,也可以只包含其中的一个。缺少的元素将被简单地忽略。有些角色只是为项目提供通用处理程序。其他角色存在作为单个依赖点,反过来又依赖于许多其他角色。

任务

任务文件是角色的核心部分,如果roles/<role_name>/tasks/main.yaml存在,那么该文件中的所有任务(以及它包含的任何其他文件)将被加载到播放中并执行。

处理程序

与任务类似,如果存在roles/<role_name>/handlers/main.yaml文件,则处理程序将自动从中加载。这些处理程序可以被角色内的任何任务引用,或者被列出该角色为依赖项的任何其他角色内的任务引用。

变量

角色中可以定义两种类型的变量。有角色变量,从roles/<role_name>/vars/main.yaml加载,还有角色默认值,从roles/<role_name>/defaults/main.yaml加载。varsdefaults之间的区别在于优先顺序。有关顺序的详细描述,请参阅第一章Ansible 的系统架构和设计角色默认值是最低优先级的变量。实际上,任何其他变量的定义都将优先于角色默认值。角色默认值可以被视为实际数据的占位符,开发人员可能有兴趣使用站点特定值来定义哪些变量。另一方面,角色变量具有更高的优先级。角色变量可以被覆盖,但通常在角色内多次引用相同数据集时使用。如果要使用站点本地值重新定义数据集,则应该将变量列在角色默认值而不是角色变量中。

模块和插件

一个角色可以包括自定义模块和插件。虽然我们正在过渡到 Ansible 4.0 及更高版本的阶段,但这仍然受支持,但您无疑已经注意到集合也可以包括自定义模块插件。在当前时期,您放置模块和插件的位置将取决于您为其编写角色的目标 Ansible 版本。如果您希望与 2.x 版本保持向后兼容性,那么您应该将模块和插件放入角色目录结构中,如此处所述。如果您只希望与 Ansible 3.0 及更高版本兼容,您可以考虑将它们放入集合中。然而,请注意,随着转向集合,您的插件和模块不太可能被接受到ansible-core包中,除非它们提供被认为是核心功能。

(如果在角色中存在)模块从roles/<role_name>/library/加载,并且可以被角色中的任何任务或者后续的角色使用。重要的是要注意,此路径中提供的模块将覆盖同名模块的任何其他副本,因此尽可能使用 FQCNs 引用模块以避免任何意外结果。

如果在角色的一个以下子目录中找到插件,插件将自动加载:

  • action_plugins

  • lookup_plugins

  • callback_plugins

  • connection_plugins

  • filter_plugins

  • strategy_plugins

  • cache_plugins

  • test_plugins

  • shell_plugins

依赖

角色可以表达对另一个角色的依赖。一组角色通常都依赖于一个常见的角色,用于任务、处理程序、模块等。这些角色可能只依赖于一次定义。当 Ansible 处理一组主机的角色时,它首先查找roles/<role_name>/meta/main.yaml中列出的依赖关系。如果有任何定义,那么这些角色将立即被处理,并且这些角色中包含的任务将被执行(在检查其中列出的任何依赖关系之后)。这个过程会一直持续,直到所有依赖关系都被建立和加载(并在存在的情况下执行任务),然后 Ansible 开始执行初始角色任务。请记住——依赖关系总是在角色本身之前执行。我们将在本章后面更深入地描述角色依赖关系。

文件和模板

任务和处理程序模块只能在roles/<role_name>/files/中使用相对路径引用文件。文件名可以提供没有任何前缀(尽管如果您愿意,这是允许的),并且将从roles/<role_name>/files/<relative_directory>/<file_name>获取。诸如ansible.builtin.templateansible.builtin.copyansible.builtin.script之类的模块是您将看到许多利用这一有用功能的示例的典型模块。

同样,ansible.builtin.template模块使用的模板可以在roles/<role_name>/templates/中相对引用。以下代码示例使用相对路径从完整路径roles/<role_name>/templates/herp/derp.j2加载derp.j2模板:

- name: configure herp 
  ansible.builtin.template: 
    src: herp/derp.j2 
    dest: /etc/herp/derp.j2 

通过这种方式,可以轻松地在标准角色目录结构中组织文件,并且仍然可以轻松地从角色内部访问它们,而无需输入长而复杂的路径。在本章后面,我们将向您介绍ansible-galaxy role init命令,该命令将帮助您更轻松地为新角色构建骨架目录结构-有关更多详细信息,请参见角色共享部分。

将所有内容放在一起

为了说明完整的角色结构可能是什么样子,这里有一个名为demo的示例角色:

roles/demo 
├── defaults 
|   |--- main.yaml 
|---- files 
|   |--- foo 
|---- handlers 
|   |--- main.yaml 
|---- library 
|   |--- samplemod.py 
|---- meta 
|   |--- main.yaml 
|---- tasks 
|   |--- main.yaml 
|---- templates 
|   |--- bar.j2 
|--- vars 
    |--- main.yaml 

创建角色时,并不是每个目录或文件都是必需的。只有存在的文件才会被处理。因此,我们的角色示例不需要或使用处理程序;整个树的handlers部分可以简单地被省略。

角色依赖

如前所述,角色可以依赖于其他角色。这些关系称为依赖关系,并且它们在角色的meta/main.yaml文件中描述。该文件期望具有dependencies键的顶级数据哈希;其中的数据是角色列表。您可以在以下代码片段中看到这一点的说明:

--- 
dependencies: 
  - role: common 
  - role: apache 

在这个例子中,Ansible 将在继续apache角色并最终开始角色任务之前,首先完全处理common角色(及其可能表达的任何依赖关系)。

如果依赖项存在于相同的目录结构中或位于配置的roles_path配置键中,则可以通过名称引用依赖项而无需任何前缀。否则,可以使用完整路径来定位角色,如下所示:

role: /opt/ansible/site-roles/apache 

在表达依赖关系时,可以将数据传递给依赖项。数据可以是变量、标签,甚至是条件。

角色依赖变量

在列出依赖项时传递的变量将覆盖defaults/main.yamlvars/main.yaml中定义的匹配变量的值。这对于使用常见角色(例如apache角色)作为依赖项并提供特定于站点的数据(例如在防火墙中打开哪些端口或启用哪些apache模块)非常有用。变量表示为角色列表的附加键。因此,继续我们的假设示例,考虑到我们需要将一些变量传递给我们讨论的commonapache角色依赖项,如下所示:

--- 
dependencies: 
  - role: common 
    simple_var_a: True 
    simple_var_b: False 
  - role: apache 
    complex_var: 
      key1: value1 
      key2: value2 
    short_list: 
      - 8080 
      - 8081 

在提供依赖变量数据时,有两个名称被保留,不应该用作角色变量:tagswhen。前者用于将标签数据传递到角色中,后者用于将条件传递到角色中。

标签

标签可以应用于依赖角色中找到的所有任务。这与标签应用于包含的任务文件的方式相同,如本章前面所述。语法很简单:tags键可以是单个项目或列表。为了演示,让我们通过添加一些标签来进一步扩展我们的理论示例,如下所示:

--- 
dependencies: 
  - role: common 
    simple_var_a: True 
    simple_var_b: False 
    tags: common_demo 
  - role: apache 
    complex_var: 
      key1: value1 
      key2: value2 
    short_list: 
      - 8080 
      - 8081 
    tags: 
      - apache_demo 
      - 8080 
      - 8181 

与向包含的任务文件添加标签一样,所有在依赖中找到的任务(以及该层次结构中的任何依赖)都将获得提供的标签。

角色依赖条件

虽然不可能通过条件来阻止依赖角色的处理,但可以通过将条件应用到依赖项来跳过依赖角色层次结构中的所有任务。这也反映了使用条件的任务包含的功能。when关键字用于表达条件。我们将再次通过添加一个依赖项来扩展我们的示例,以演示语法,如下所示:

--- 
dependencies: 
  - role: common 
    simple_var_a: True 
    simple_var_b: False 
    tags: common_demo 
  - role: apache 
    complex_var: 
      key1: value1 
      key2: value2 
    short_list: 
      - 8080 
      - 8081 
    tags: 
      - apache_demo 
      - 8080 
      - 8181 
    when: backend_server == 'apache' 

在这个例子中,apache角色将始终被处理,但角色内的任务只有在backend_server变量包含apache字符串时才会运行。

角色应用

角色不是剧本。它们不会对角色任务应该在哪些主机上运行、使用哪种连接方法、是否按顺序操作或者在第一章中描述的任何其他剧本行为方面持有任何意见。角色必须在剧本中的一个剧本中应用,所有这些意见都可以在其中表达。

在播放中应用角色时,使用roles操作符。该操作符期望应用到播放中的主机的角色列表。与描述角色依赖关系类似,当描述要应用的角色时,可以传递数据,例如变量、标签和条件。语法完全相同。

为了演示在播放中应用角色,让我们创建一个简单的角色并将其应用到一个简单的剧本中。首先,让我们构建一个名为simple的角色,它将在roles/simple/tasks/main.yaml中具有一个单独的debug任务,打印在roles/simple/defaults/main.yaml中定义的角色默认变量的值。首先,让我们创建一个任务文件(在tasks/子目录中),如下所示:

--- 
- name: print a variable 
  ansible.builtin.debug: 
    var: derp 

接下来,我们将编写我们的默认文件,其中包含一个变量derp,如下所示:

--- 
derp: herp 

要执行此角色,我们将编写一个播放以应用该角色。我们将称我们的剧本为roleplay.yaml,它将与roles/目录处于相同的目录级别。代码如下所示:

--- 
- hosts: localhost 
  gather_facts: false 

  roles: 
  - role: simple 

重要提示

如果没有为角色提供数据,可以使用另一种语法,只列出要应用的角色,而不是哈希。但为了保持一致,我觉得最好在项目中始终使用相同的语法。

我们将重用之前章节中的mastery-hosts清单,并以正常方式执行这本手册(这里我们不需要任何额外的冗长),通过运行以下命令:

ansible-playbook -i mastery-hosts roleplay.yaml

输出应该看起来像这样:

图 8.19 - 从剧本中运行我们的简单角色,使用默认角色变量数据

图 8.19 - 从剧本中运行我们的简单角色,使用默认角色变量数据

由于角色的魔力,derp变量值会自动从角色默认值中加载。当应用角色时,当然可以覆盖默认值。让我们修改我们的剧本,并为derp提供一个新值,如下所示:

--- 
- hosts: localhost 
  gather_facts: false 

  roles: 
  - role: simple 
    derp: newval 

这次,当我们执行(使用与之前相同的命令),我们将看到newval作为derp的值,如下截图所示:

图 8.20 - 从剧本中运行相同的角色,但这次在播放级别覆盖默认变量数据

图 8.20 - 运行相同的角色,但这次在播放级别覆盖默认变量数据

可以在一个播放中应用多个角色。roles:关键字期望一个列表值。只需添加更多角色以应用更多角色,如下所示(下一个示例是理论的,留给你作为练习):

--- 
- hosts: localhost 
  gather_facts: false 

  roles: 
  - role: simple 
    derp: newval 
  - role: second_role 
    othervar: value 
  - role: third_role 
  - role: another_role 

这本手册将加载四个角色——simplesecond_rolethird_roleanother_role——并且每个角色将按照它们列出的顺序执行。

混合角色和任务

使用角色的 play 不仅限于角色。这些 play 可以有自己的任务,以及两个其他任务块:pre_taskspost_tasks块。与本书中一直关注的任务执行顺序不同,这些任务的执行顺序不取决于这些部分在 play 中列出的顺序,而是在 play 内部块执行中有严格的顺序。有关 playbook 操作顺序的详细信息,请参见第一章Ansible 的系统架构和设计

play 的处理程序在多个点被刷新。如果有pre_tasks块,则在执行所有pre_tasks块后刷新处理程序。然后执行角色和任务块(首先是角色,然后是任务,不管它们在 playbook 中的书写顺序如何),之后处理程序将再次被刷新。最后,如果存在post_tasks块,则在执行所有post_tasks块后再次刷新处理程序。当然,可以随时使用meta: flush_handlers调用刷新处理程序。让我们扩展我们的roleplay.yaml文件,以演示处理程序可以被触发的所有不同时间,如下所示:

---
- hosts: localhost
  gather_facts: false
  pre_tasks:
  - name: pretask
    ansible.builtin.debug:
      msg: "a pre task"
    changed_when: true
    notify: say hi
  roles:
  - role: simple
    derp: newval
  tasks:
  - name: task
    ansible.builtin.debug:
      msg: "a task"
    changed_when: true
    notify: say hi

  post_tasks:
  - name: posttask
    ansible.builtin.debug:
      msg: "a post task"
    changed_when: true
    notify: say hi
  handlers:
  - name: say hi
    ansible.builtin.debug:
      msg: "hi"

我们还将修改我们简单角色的任务,以通知say hi处理程序,如下所示:

--- 
- name: print a variable 
  ansible.builtin.debug:     
    var: derp 
  changed_when: true 
  notify: say hi 

重要提示

这仅在调用simple角色的 play 中定义了say hi处理程序才有效。如果处理程序未定义,将会出现错误。最佳实践是只通知存在于相同角色或任何标记为依赖项的角色中的处理程序。

再次运行我们的 playbook,使用与之前示例中相同的命令,应该会导致say hi处理程序被调用三次:一次用于pre_tasks块,一次用于角色和任务,一次用于post_tasks块,如下面的屏幕截图所示:

图 8.21 - 运行 playbook 以演示混合角色和任务以及处理程序执行

图 8.21 - 运行 playbook 以演示混合角色和任务以及处理程序执行

pre_tasksrolestaskspost_tasks块的书写顺序不会影响这些部分执行的顺序,但最佳实践是按照它们将被执行的顺序进行书写。这是一个视觉提示,有助于记住顺序,并在以后阅读 playbook 时避免混淆。

角色包含和导入

在 Ansible 2.2 版本中,新的ansible.builtin.include_role动作插件作为技术预览可用。然后,在Ansible 2.4版本中,通过添加ansible.builtin.import_role插件进一步开发了这个概念。为了简洁起见,我们将不使用它们的 FQCNs 来引用这些插件。

这些插件用于在任务中包含和执行整个角色。两者之间的区别微妙但重要——include_role插件被认为是动态的,这意味着在遇到引用它的任务时,代码会在运行时进行处理。

另一方面,import_role插件被认为是静态的,这意味着所有导入都在解析 playbook 时进行预处理。这对于在 playbooks 中的使用有各种影响,例如,import_role不能在循环中使用,而include_role可以。

重要提示

有关导入和包含之间权衡的详细信息可以在官方 Ansible 文档中找到:docs.ansible.com/ansible/latest/user_guide/playbooks_reuse.html

在本书的上一版中,这些插件被视为技术预览,但现在它们已经成为ansible.builtin集合的一部分,因此现在可以认为它们是稳定的,并且可以根据需要用于您的代码。

角色共享

使用角色的一个优势是能够在不同的 play、playbook、整个项目空间甚至不同的组织之间共享角色。角色被设计为自包含的(或者清楚地引用依赖角色),以便它们可以存在于应用角色的 playbook 所在的项目空间之外。角色可以安装在 Ansible 主机上的共享路径上,也可以通过源代码控制进行分发。

Ansible Galaxy

Ansible Galaxygalaxy.ansible.com/),正如我们在第二章中讨论的那样,从早期的 Ansible 版本迁移,是一个用于查找和共享 Ansible 角色和集合的社区中心。任何人都可以访问该网站浏览这些角色和评论;此外,创建登录的用户可以对他们测试过的角色进行评论。可以使用ansible-galaxy工具提供的实用程序下载 Galaxy 中的角色。

ansible-galaxy实用程序可以连接到 Ansible Galaxy 网站并安装角色。该实用程序默认将角色安装到/etc/ansible/roles中。如果配置了roles_path,或者使用--roles-path(或-p)选项提供了运行时路径,角色将安装到那里。如果已经将角色安装到roles_path选项或提供的路径中,ansible-galaxy也可以列出这些角色并显示有关这些角色的信息。为了演示ansible-galaxy的用法,让我们使用它将一个用于在 Ubuntu 上安装和管理 Docker 的角色从 Ansible Galaxy 安装到我们一直在使用的roles目录中。从 Ansible Galaxy 安装角色需要username.rolename,因为多个用户可能上传了具有相同名称的角色。为了演示,我们将使用angstwad用户的docker_ubuntu角色,如下面的截图所示:

图 8.22 - 在 Ansible Galaxy 上找到一个示例社区贡献的角色

图 8.22 - 在 Ansible Galaxy 上找到一个示例社区贡献的角色

现在我们可以通过在 play 或其他角色的依赖块中引用angstwad.docker_ubuntu来使用这个角色。然而,让我们首先演示如何在当前工作目录中安装这个角色。我们首先创建一个roles/目录,然后使用以下命令将上述角色安装到这个目录中:

mkdir roles/
ansible-galaxy role install -p roles/ angstwad.docker_ubuntu

一旦我们安装了示例角色,我们可以使用以下命令查询它(以及可能存在于roles/目录中的任何其他角色):

ansible-galaxy role list -p roles/

你还可以使用以下命令在本地查询有关角色的描述、创建者、版本等信息:

ansible-galaxy role info -p roles/ angstwad.docker_ubuntu

以下截图给出了你可以从前面两个命令中期望的输出类型:

图 8.23 - 使用 ansible-galaxy 命令查询已安装的角色

图 8.23 - 使用 ansible-galaxy 命令查询已安装的角色

输出已经被截断以节省书中的空间,如果你浏览输出,会发现更多有用的信息。info命令显示的一些数据存在于角色本身,在meta/main.yml文件中。以前,我们只在这个文件中看到了依赖信息,也许给目录命名为meta并没有太多意义,但现在我们看到这个文件中还有其他元数据,如下面的截图所示:

图 8.24 - 可以放置在角色的 meta/main.yml 文件中的元数据的示例

图 8.24 - 可以放置在角色的 meta/main.yml 文件中的元数据的示例

ansible-galaxy工具还可以帮助创建新的角色。role init方法将为角色创建一个骨架目录树,并在meta/main.yml文件中填充与 Galaxy 相关数据的占位符。

让我们通过使用这个命令在我们的工作目录中创建一个名为autogen的新角色来演示这种能力:

ansible-galaxy role init --init-path roles/ autogen

如果你检查这个命令创建的目录结构,你会看到创建全新角色所需的所有目录和占位符文件,如下面的截图所示:

图 8.25 - 使用 ansible-galaxy 工具创建一个空的骨架角色

图 8.25 - 使用 ansible-galaxy 工具创建一个空的骨架角色

请注意,在过去用于指定本地roles/目录的-p开关,现在必须改用init命令的--init-path开关。对于不适合 Ansible Galaxy 的角色,例如处理内部系统的角色,ansible-galaxy可以直接从 Git Uniform Resource Locator (URL)安装。不仅可以提供一个角色名称给install方法,还可以提供一个带有可选版本的完整 Git URL。例如,如果我们想要从内部 Git 服务器安装foowhiz角色,我们可以简单地运行以下命令:

ansible-galaxy role install -p /opt/ansible/roles git+git@git.internal.site:ansible-roles/foowhiz

没有版本信息时,将使用master分支。没有名称数据时,名称将根据 URL 本身确定。要提供版本,请附加一个逗号和 Git 可以理解的版本字符串,例如标签或分支名称,例如v1,如下所示:

ansible-galaxy role install -p /opt/ansible/roles git+git@git.internal.site:ansible-roles/foowhiz,v1

可以在另一个逗号后面添加一个角色名称,如下面的代码片段所示。如果需要提供名称但不希望提供版本,则仍然需要为版本留出一个空位:

ansible-galaxy role install -p /opt/ansible/roles git+git@git.internal.site:ansible-roles/foowhiz,,foo-whiz-common

角色也可以直接从 tarballs 安装,只需提供 tarball 的 URL,而不是完整的 Git URL 或要从 Ansible Galaxy 获取的角色名称。

当你需要为一个项目安装许多角色时,可以在以.yaml(或.yml)结尾的 YAML 格式文件中定义要下载和安装的多个角色。该文件的格式允许你从多个来源指定多个角色,并保留指定版本和角色名称的能力。此外,还可以列出源代码控制方法(目前仅支持githg)。你可以在以下代码片段中看到一个例子:

--- 
- src: <name or url> 
  version: <optional version> 
  name: <optional name override> 
  scm: <optional defined source control mechanism, defaults to git>

要安装文件中的所有角色,请使用role install方法的--roles-file-r)选项,如下所示:

ansible-galaxy role install -r foowhiz-reqs.yaml

通过这种方式,非常容易在运行 playbooks 之前收集所有角色的依赖关系,无论你需要的角色是在 Ansible Galaxy 上公开可用,还是保存在你自己的内部源代码管理系统中,这一简单步骤都可以大大加快 playbook 的部署速度,同时支持代码重用。

总结

Ansible 提供了将内容逻辑地分成单独文件的能力。这种能力帮助项目开发人员不再重复相同的代码。Ansible 中的角色进一步利用了这种能力,并在内容的路径周围包装了一些魔法。角色是可调整的、可重用的、可移植的和可共享的功能块。Ansible Galaxy 作为开发人员的社区中心存在,可以在其中找到、评价和共享角色和集合。ansible-galaxy命令行工具提供了一种与 Ansible Galaxy 站点或其他角色共享机制进行交互的方法。这些能力和工具有助于组织和利用常见代码。

在本章中,您学习了与任务、处理程序、变量甚至整个 playbooks 相关的包含概念。然后,您通过学习角色的结构、设置默认变量值和处理角色依赖关系来扩展了这些知识。然后,您继续学习了设计 playbooks 以有效利用角色,并应用了角色缺乏的标签等选项。最后,您学习了如何使用 Git 和 Ansible Galaxy 等存储库在项目之间共享角色。

在下一章中,我们将介绍有用且有效的故障排除技术,以帮助您在 Ansible 部署遇到问题时解决问题。

问题

  1. 在运行 playbook 时,可以使用哪个 Ansible 模块来运行来自单独外部任务文件的任务?

a) ansible.builtin.import

b) ansible.builtin.include

c) ansible.builtin.tasks_file

d) ansible.builtin.with_tasks

  1. 变量数据可以在调用外部任务文件时传递:

a) True

b) False

  1. 包含当前循环值的变量的默认名称是:

a) i

b) loop_var

c) loop_value

d) item

  1. 在循环外部任务文件时,重要的是考虑设置哪个特殊变量以防止循环变量名称冲突?

a) loop_name

b) loop_item

c) loop_var

d) item

  1. 处理程序通常运行:

a) 一次,在剧终

b) 每次,在pre_tasksroles/taskspost_tasks部分的最后

c) 每次,在pre_tasksroles/taskspost_tasks部分的最后,只有在通知时

d) 每次,在pre_tasksroles/taskspost_tasks部分的最后,只有在导入时

  1. Ansible 可以从以下外部来源加载变量:

a) 静态vars_files包含

b) 动态vars_files包含

c) 通过include_vars语句

d) 通过extra-vars命令行参数

e) 以上所有

  1. 角色从角色目录名称中获取其名称(例如,roles/testrole1的名称为testrole1):

a) True

b) False

  1. 如果一个角色缺少tasks/main.yml文件,Ansible 将会:

a) 用错误中止播放

b) 完全跳过角色

c) 仍然引用角色的任何其他有效部分,包括元数据,默认变量和处理程序

d) 显示警告

  1. 角色可以依赖于其他角色:

a) True

b) False

  1. 当您为角色指定标签时,Ansible 的行为是:

a) 将标签应用于整个角色

b) 将标签应用于角色内的每个任务

c) 完全跳过角色

d) 仅执行具有相同标签的角色的任务

第九章:故障排除 Ansible

Ansible 简单而强大。Ansible 的简单意味着它的操作易于理解和遵循。然而,即使是最简单和最用户友好的系统,有时也会出现问题——也许是因为我们正在学习编写自己的代码(playbooks、roles、modules 或其他)并需要调试它,或者更少见的是,当我们可能在已发布版本的集合或 ansible-core 中发现了错误时。

在调试意外行为时,能够理解和遵循 Ansible 的操作至关重要。Ansible 提供了许多选项和工具,帮助您调试其核心组件的操作,以及您自己的 playbook 代码。我们将在本章中详细探讨这些内容,目标是让您有信心调试自己的 Ansible 工作。

具体来说,在本章中,我们将讨论以下主题:

  • Playbook 日志记录和详细程度

  • 变量内省

  • 调试代码执行

技术要求

要跟随本章中提出的示例,您需要运行 Ansible 4.3 或更新版本的 Linux 机器。几乎任何 Linux 版本都可以——对于那些感兴趣的人,本章中提出的所有代码都是在 Ubuntu Server 20.04 长期支持LTS)上测试的,除非另有说明,并且在 Ansible 4.3 上测试。本章附带的示例代码可以从 GitHub 下载:github.com/PacktPublishing/Mastering-Ansible-Fourth-Edition/tree/main/Chapter09

查看以下视频以查看代码的实际操作:bit.ly/2Xx46Ym

Playbook 日志记录和详细程度

增加 Ansible 输出的详细程度可以解决许多问题。从无效的模块参数到不正确的连接命令,增加详细程度在准确定位错误源头方面至关重要。在 第三章 中简要讨论了 playbook 日志记录和详细程度,关于在执行 playbook 时保护秘密值。本节将更详细地介绍详细程度和日志记录。

详细程度

在使用 ansible-playbook 执行 playbook 时,输出显示在 标准输出stdout)上。在默认详细程度下,几乎没有显示任何信息。当执行 play 时,ansible-playbook 将打印一个带有 play 名称的 play 标头。然后,对于每个任务,将打印一个带有任务名称的 task 标头。当每个主机执行任务时,将显示主机的名称以及任务状态,可以是 okfatalchanged。不会显示有关任务的进一步信息——例如正在执行的模块、提供给模块的参数或执行的返回数据。虽然这对于已经建立的 playbook 来说是可以的,但我倾向于想要更多关于我的 play 的信息。在本书的早期示例中,我们使用了更高级别的详细程度,最高达到二级 (-vv),以便我们可以看到任务的位置和返回数据。总共有五个详细程度级别,如下所述:

  • :默认级别

  • (-v):显示返回数据和条件信息的位置

  • (-vv):用于任务位置和处理程序通知信息

  • (-vvv):提供连接尝试和任务调用信息的详细信息

  • (-vvvv):将额外的详细选项传递给连接插件(例如将 -vvv 传递给 ssh 命令)

增加详细程度可以帮助准确定位错误发生的位置,以及提供额外的洞察力,了解 Ansible 如何执行其操作。

正如我们在第三章中提到的,使用 Ansible 保护您的机密信息,超过一级的冗余度可能会将敏感数据泄露到标准输出和日志文件中,因此在可能共享的环境中增加冗余度时应谨慎使用。

日志记录

虽然ansible-playbook的默认日志记录到标准输出,但输出量可能大于所使用的终端仿真器的缓冲区;因此,可能需要将所有输出保存到文件中。虽然各种 shell 提供了一些重定向输出的机制,但更优雅的解决方案是将ansible-playbook指向日志记录到文件。这可以通过在ansible.cfg文件中定义log_path或者将ANSIBLE_LOG_PATH设置为环境变量来实现。任何一个的值都应该是文件的路径。如果路径不存在,Ansible 将尝试创建一个文件。如果文件已经存在,Ansible 将追加到文件,允许合并多个ansible-playbook执行日志。

使用日志文件并不意味着与记录到标准输出互斥。两者可以同时发生,并且所提供的冗余级别对两者都有影响。日志记录当然是有帮助的,但它并不一定告诉我们代码中发生了什么,以及我们的变量可能包含什么。我们将在下一节中看看如何执行变量内省,以帮助您完成这个任务。

变量内省

在开发 Ansible playbook 时遇到的常见问题是变量的值的不正确使用或无效假设。当在变量中注册一个任务的结果,然后在另一个任务或模板中使用该变量时,这种情况特别常见。如果没有正确访问结果的所需元素,最终结果将是意外的,甚至可能是有害的。

要排除变量使用不当的问题,检查变量值是关键。检查变量值的最简单方法是使用ansible.builtin.debug模块。ansible.builtin.debug模块允许在屏幕上显示自由格式的文本,并且与其他任务一样,模块的参数也可以利用 Jinja2 模板语法。让我们通过创建一个执行任务的示例播放来演示这种用法,注册结果,然后使用 Jinja2 语法在ansible.builtin.debug语句中显示结果,如下所示:

--- 
- name: variable introspection demo
  hosts: localhost
  gather_facts: false   
  tasks:     
- name: do a thing       
  ansible.builtin.uri:         
    url: https://derpops.bike       
    register: derpops     
- name: show derpops       
  ansible.builtin.debug:         
    msg: "derpops value is {{ derpops }}" 

我们将使用以下命令以一级冗余度运行此播放:

ansible-playbook -i mastery-hosts vintro.yaml -v

假设我们正在测试的网站是可访问的,我们将看到derpops的显示值,如下面的屏幕截图所示:

图 9.1 - 使用一级冗余度检查注册变量的值

图 9.1 - 使用一级冗余度检查注册变量的值

ansible.builtin.debug模块还有一个不同的选项,可能也很有用。该模块不是将自由格式的字符串打印到调试模板中,而是可以简单地打印任何变量的值。这是通过使用var参数而不是msg参数来完成的。让我们重复我们的例子,但这次我们将使用var参数,并且我们将仅访问derpops变量的server子元素,如下所示:

--- 
- name: variable introspection demo 
  hosts: localhost 
  gather_facts: false 

  tasks: 
    - name: do a thing 
      ansible.builtin.uri: 
        url: https://derpops.bike 
      register: derpops 

    - name: show derpops 
      ansible.builtin.debug: 
        var: derpops.server 

使用相同冗余度运行此修改后的播放将只显示derpops变量的server部分,如下面的屏幕截图所示:

图 9.2 - 使用调试模块的 var 参数检查变量子元素

图 9.2 - 使用调试模块的 var 参数来检查变量子元素

在我们使用ansible.builtin.debug的例子中,需要使用msg参数将变量表达为花括号内,但是在使用var时不需要。这是因为msg期望一个字符串,因此 Ansible 需要通过模板引擎将变量呈现为字符串。然而,var期望一个单个未呈现的变量。

变量子元素

playbook 中经常出现的一个错误是错误地引用复杂变量的子元素。复杂变量不仅仅是一个字符串,它可以是一个列表或一个哈希表。经常会引用错误的子元素,或者错误地引用元素,期望得到不同的类型。

虽然列表相当容易处理,但哈希表提出了一些独特的挑战。哈希表是一个无序的键-值集合,可能包含不同类型的元素,也可能是嵌套的。一个哈希表可以有一个元素是单个字符串,而另一个元素可以是一个字符串列表,第三个元素可以是另一个哈希表,其中包含更多的元素。知道如何正确访问正确的子元素对于成功至关重要。

例如,让我们稍微修改我们之前的 play。这一次,我们将允许 Ansible 收集事实,然后显示ansible_python的值。这是我们需要的代码:

--- 
- name: variable introspection demo 
  hosts: localhost 

  tasks: 
    - name: show a complex hash 
      ansible.builtin.debug: 
        var: ansible_python 

以一级详细程度运行此代码,您应该看到以下输出:

图 9.3 – 使用 ansible.builtin.debug 检查 ansible_python 事实子元素

图 9.3 – 使用 ansible.builtin.debug 检查 ansible_python 事实子元素

使用ansible.builtin.debug来显示整个复杂变量是学习所有子元素名称的好方法。

这个变量有一些元素是字符串,还有一些元素是字符串列表。让我们访问标志列表中的最后一个项目,如下所示:

--- 
- name: variable introspection demo 
  hosts: localhost 

  tasks: 
    - name: show a complex hash 
      ansible.builtin.debug: 
        var: ansible_python.version_info[-1] 

输出如下所示:

图 9.4 – 进一步检查 ansible_python 事实子元素

图 9.4 – 进一步检查 ansible_python 事实子元素

因为ansible_python.version_info是一个列表,我们可以使用列表索引方法来从列表中选择特定的项目。在这种情况下,-1将给我们列表中的最后一个项目。

子元素与 Python 对象方法

一个不太常见但令人困惑的坑来自 Jinja2 语法的一个怪癖。在 Ansible playbook 和模板中,复杂变量可以以两种方式引用。第一种样式是通过名称引用基本元素,后跟括号,括号内用引号括起来的子元素。这是标准下标语法。例如,要访问derp变量的herp子元素,我们将使用以下代码:

{{ derp['herp'] }} 

第二种样式是 Jinja2 提供的一种便利方法,即使用句点来分隔元素。这被称为点表示法,看起来像这样:

{{ derp.herp }} 

这些样式的工作方式有微妙的差异,这与 Python 对象和对象方法有关。由于 Jinja2 在本质上是一个 Python 实用程序,Jinja2 中的变量可以访问其本机 Python 方法。字符串变量可以访问 Python 字符串方法,列表可以访问列表方法,字典可以访问字典方法。使用第一种样式时,Jinja2 首先会搜索提供的名称的元素以查找子元素。如果找不到子元素,则 Jinja2 将尝试访问提供的名称的 Python 方法。然而,当使用第二种样式时,顺序是相反的;首先搜索 Python 对象方法,如果找不到,然后搜索子元素。当子元素和方法之间存在名称冲突时,这种差异很重要。想象一个名为derp的变量,它是一个复杂的变量。这个变量有一个名为keys的子元素。使用每种样式来访问keys元素将得到不同的值。让我们构建一个 playbook 来演示这一点,如下所示:

--- 
- name: sub-element access styles 
  hosts: localhost 
  gather_facts: false 
  vars: 
    - derp: 
        keys: 
          - c 
          - d 
  tasks: 
    - name: subscript style 
      ansible.builtin.debug: 
        var: derp['keys']  
    - name: dot notation style 
      ansible.builtin.debug: 
        var: derp.keys 

在运行这个剧本时,我们可以清楚地看到两种风格之间的区别。第一种风格成功地引用了keys子元素,而第二种风格引用了 Python 字典的keys方法,如下面的屏幕截图所示:

图 9.5 - 演示标准下标语法和点符号在名称冲突发生时的区别

图 9.5 - 演示标准下标语法和点符号在名称冲突发生时的区别

通常最好避免使用与 Python 对象方法冲突的子元素名称。但是,如果不可能的话,下一件最好的事情就是意识到子元素引用风格的差异,并选择适当的风格。

当然,变量只是剧本行为的一方面 - 有时,我们需要实际进入调试代码本身,我们将在下一节中仔细研究这一点。

调试代码执行

有时,记录和检查变量数据并不足以解决问题。当这种情况发生时,有必要交互式地调试剧本,或者深入研究 Ansible 代码的内部。Ansible 代码有两个主要集:在 Ansible 主机上本地运行的代码,以及在目标主机上远程运行的模块代码。

剧本调试

可以通过使用在 Ansible 2.1 中引入的执行策略调试策略来交互式地调试剧本。如果一个剧本在遇到错误状态时使用了这个策略,将开始一个交互式调试会话。这个交互式会话可以用于显示变量数据,显示任务参数,更新任务参数,更新变量,重新执行任务,继续执行或退出调试器。

让我们用一个成功的任务,然后是一个出错的任务,最后是一个成功的任务来演示这一点。我们将重用我们一直在使用的剧本,但稍微更新一下,如下面的代码所示:

--- 
- name: sub-element access styles 
  hosts: localhost 
  gather_facts: false 
  strategy: debug 

  vars: 
    - derp: 
        keys: 
          - c 
          - d 

  tasks: 
    - name: subscript style 
      ansible.builtin.debug: 
        var: derp['keys'] 

    - name: failing task 
      ansible.builtin.debug: 
        msg: "this is {{ derp['missing'] }}" 

    - name: final task 
      ansible.builtin.debug: 
        msg: "my only friend the end" 

执行时,Ansible 将在我们失败的任务中遇到错误,并显示(debug)提示,如下面的屏幕截图所示:

图 9.6 - Ansible 调试器在执行失败任务时启动(执行策略为 debug)时

图 9.6 - Ansible 调试器在执行失败任务时启动(执行策略为 debug)时

从这个提示中,我们可以使用p命令显示任务和任务参数,如下面的屏幕截图所示:

图 9.7 - 使用 p 命令检查失败剧本任务的详细信息

图 9.7 - 使用 p 命令检查失败剧本任务的详细信息

我们还可以即时更改剧本以尝试不同的参数或变量值。让我们定义derp变量的缺失键,然后重试执行。所有变量都在顶层vars字典中。我们可以使用 Python 语法和task_vars命令直接设置变量数据,然后使用r命令重试,如下面的屏幕截图所示:

图 9.8 - 添加先前未定义的变量值并从调试器中重试剧本

图 9.8 - 添加先前未定义的变量值并从调试器中重试剧本

调试执行策略是一个方便的工具,可以快速迭代不同的任务参数和变量组合,以找出正确的前进路径。然而,由于错误导致交互式控制台,调试策略不适用于剧本的自动执行,因为控制台上没有人来操作调试器。

重要观点

更改调试器中的数据不会保存更改到后备文件中。始终记得更新剧本文件以反映在调试过程中发现的内容。

调试本地代码

本地 Ansible 代码是随 Ansible 一起提供的大部分代码。所有的 playbook、play、role 和 task 解析代码都存储在本地。所有的任务结果处理代码和传输代码都存储在本地。除了传输到远程主机的组装模块代码之外,所有代码都存储在本地。

本地 Ansible 代码仍然可以分为三个主要部分:清单playbook执行器。清单代码处理来自主机文件,动态清单脚本或两者组合在目录树中的清单数据的解析。Playbook 代码用于将 playbook YAML Ain't Markup Language (YAML)代码解析为 Ansible 内的 Python 对象。执行器代码是核心应用程序编程接口 (API),处理分叉进程,连接到主机,执行模块,处理结果以及大多数其他事情。学习开始调试的一般区域需要实践,但这里描述的一般区域是一个起点。

由于 Ansible 是用 Python 编写的,因此用于调试本地代码执行的工具是pdb Python 调试器。这个工具允许我们在 Ansible 代码内部插入断点,并逐行交互式地执行代码。这对于检查 Ansible 在本地代码执行时的内部状态非常有用。许多书籍和网站涵盖了pdb的使用方法,可以通过简单的网络搜索找到 Python pdb的介绍,因此我们在这里不再重复。如果您正在寻找使用pdb的实践介绍,那么在书籍Django 1.1 Testing and DebuggingKaren M. TraceyPackt Publishing中有许多很好的例子,这将使您能够在 Django(用 Python 编写)中使用pdb进行实际调试技术的练习。官方的 Python 文档也提供了大量关于使用调试器的信息。您可以在这里查看:https://docs.python.org/3/library/pdb.html。基本的方法是编辑要调试的源文件,插入新的代码行以创建断点,然后执行代码。代码执行将在创建断点的地方停止,并提供一个提示来探索代码状态。

当然,Ansible 有许多不同的组件,这些组件共同构建了其功能,从清单处理代码到实际的 playbook 执行引擎本身。可以在所有这些地方添加断点和调试,以帮助解决可能遇到的问题,尽管您需要编辑的文件在每种情况下略有不同。我们将在本章的以下小节中详细讨论您可能需要调试的 Ansible 代码的最常见方面。

调试清单代码

Inventory 代码处理查找清单来源、读取或执行已发现的文件、将清单数据解析为清单对象,并加载清单的变量数据。要调试 Ansible 如何处理清单,必须在 inventory/__init__.pyinventory/ 子目录中的其他文件中添加断点。此目录将位于安装了 Ansible 的本地文件系统上。由于大多数 Ansible 4.0 的安装都是通过 pip 进行的,因此您的安装路径将根据诸如是否使用了虚拟环境、是否在用户目录中安装了 Ansible,或者是否使用 sudo 来系统范围安装 Ansible 等因素而有很大不同。例如,在我的 Ubuntu 20.04 测试系统上,此文件可能位于 /usr/local/lib/python3.8/dist-packages/ansible/inventory 路径下。要帮助您发现 Ansible 的安装位置,只需在命令行中输入 which ansible。此命令将显示 Ansible 可执行文件的安装位置,并可能指示 Python 虚拟环境。对于本书来说,Ansible 已经作为 root 用户使用操作系统 Python 发行版进行了安装,Ansible 二进制文件位于 /usr/local/bin/ 中。

要发现 Ansible Python 代码的路径,只需输入 python3 -c "import ansible; print(ansible)"。请注意,就像我一样,您可能已经安装了 Python 2 和 Python 3 —— 如果您不确定 Ansible 运行在哪个版本的 Python 下,您需要执行版本 2 和 3 的二进制文件,以便发现您的模块位置。

在我的系统上,这显示 <module 'ansible' from '/usr/local/lib/python3.8/dist-packages/ansible/__init__.py'>,从中我们可以推断出清单子目录位于 /usr/local/lib/python3.8/dist-packages/ansible/inventory/

清单目录在后续版本的 Ansible 中进行了重组,在 4.0 版本中,我们需要查看 inventory/manager.py。请注意,此文件来自 ansible-core 软件包,而不是依赖于它的 ansible 软件包。

在这个文件中,有一个 Inventory 类的定义。这是在整个 playbook 运行期间将使用的清单对象,当 ansible-playbook 解析为清单来源提供的选项时,它就会被创建。Inventory 类的 __init__ 方法执行所有的清单发现、解析和变量加载。要排除这三个领域的问题,应该在 __init__() 方法中添加断点。一个好的起点是在所有类变量都被赋予初始值之后,以及在处理任何数据之前。

ansible-core 的 2.11.1 版本中,这将是 inventory/manager.py 的第 167 行,其中调用了 parse_sources 函数。

我们可以跳到第 215 行的 parse_sources 函数定义处插入我们的断点。要插入断点,我们必须首先导入 pdb 模块,然后调用 set_trace() 函数,如下面的截图所示:

图 9.9 – 在 ansible-core inventory manager 代码中添加 pdb 断点

图 9.9 – 在 ansible-core inventory manager 代码中添加 pdb 断点

要开始调试,保存源文件,然后像平常一样执行 ansible-playbook。当达到断点时,执行将停止,并显示 pdb 提示,如下面的截图所示:

图 9.10 – Ansible 在开始为我们的 play 设置清单时达到 pdb 断点

图 9.10 – Ansible 在开始为我们的 play 设置清单时达到 pdb 断点

从这里,我们可以发出任意数量的调试器命令,比如 help 命令,如下面的截图所示:

图 9.11 – 演示 pdb 调试器的 help 命令

图 9.11 – 演示 pdb 调试器的帮助命令

wherelist命令可以帮助我们确定我们在堆栈中的位置和代码中的位置,如下面的屏幕截图所示:

图 9.12 – 演示 where 和 list pdb 命令

图 9.12 – 演示 where 和 list pdb 命令

where命令显示我们在inventory/manager.py中的parse_sources()方法中。下一个框架是相同的文件——__init__()函数。在此之前是另一个文件,playbook.py文件,该文件中的函数是run()。这一行调用ansible.inventory.InventoryManager来创建一个清单对象。在此之前是原始文件ansible-playbook,调用cli.run()

list命令显示我们当前执行点周围的源代码,前后各五行。

从这里,我们可以使用next命令逐行引导pdb通过函数,如果选择,我们可以使用step命令跟踪其他函数调用。我们还可以使用print命令打印变量数据以检查值,如下面的屏幕截图所示:

图 9.13 – 演示打印命令在执行过程中分析变量值

图 9.13 – 演示打印命令在执行过程中分析变量值

我们可以看到self._sources变量具有我们的mastery-hosts清单文件的完整路径,这是我们为清单数据提供给ansible-playbook的字符串。我们可以继续逐步进行或跳转,或者只需使用continue命令运行直到下一个断点或代码完成。

调试 playbook 代码

Playbook 代码负责加载、解析和执行 playbooks。调试 playbook 处理的主要入口点是通过定位 Ansible 路径找到的,就像我们在调试清单代码部分中所做的那样,然后找到playbook/__init__.py文件。在这个文件中有PlayBook类。调试 playbook 处理的一个很好的起点是大约第68行(对于ansible-core 2.11.1),尽管这将根据您安装的版本而有所不同。以下屏幕截图显示了相邻的代码,以帮助您找到您版本的正确行:

图 9.14 – 添加 pdb 调试器以调试 playbook 加载和执行

图 9.14 – 添加 pdb 调试器以调试 playbook 加载和执行

在这里设置断点将允许我们跟踪查找 playbook 文件并解析它。具体来说,通过步入self._loader.load_from_file()函数调用,我们将能够跟踪解析过程。

PlayBook类的_load_playbook_data()函数只是进行初始解析。其他目录中的其他类用于执行 plays 和 tasks。一个特别有趣的目录是executor/目录,其中包含用于执行 playbooks、plays 和 tasks 的类文件。executor/playbook_executor.py文件中PlaybookExecutor类中的run()函数将循环遍历 playbook 中的所有 plays 并执行这些 plays,这将依次执行各个 tasks。如果遇到与 play 解析、play 或 task 回调、标签、play 主机选择、串行操作、处理程序运行或其他任何问题相关的问题,这就是要遍历的函数。

调试执行器代码

在 Ansible 中,执行器代码是连接清单数据、playbooks、plays、tasks 和连接方法的连接器代码。虽然这些其他代码片段可以分别进行调试,但它们的交互方式可以在执行器代码中进行检查。

执行器类在executor/中的各个文件中定义,PlaybookExecutor类。这个类处理给定 playbook 中所有 plays 和 tasks 的执行。__init__()类创建函数创建一系列占位符属性,并设置一些默认值,而run()函数是大部分有趣的地方。

调试通常会将您从一个文件带到另一个文件,跳转到代码库中的其他位置。例如,在PlaybookExecutor类的__init__()函数中,有一段代码来缓存默认的Secure ShellSSH)可执行文件是否支持ControlPersist。您可以通过定位ansible安装路径中的executor/playbook_executor.py文件(就像我们在前面的部分中所做的那样),并查找声明set_default_transport()的行来找到它。这在ansible-core 2.11.1 中是第 76 行,以便您知道要查找的位置。一旦找到代码中的适当位置,请在此处设置断点,以便您可以跟踪代码,如下面的屏幕截图所示:

图 9.15-将 Python 调试器插入到 Ansible playbook 执行器代码中

图 9.15-将 Python 调试器插入到 Ansible playbook 执行器代码中

现在我们可以再次运行我们的objmethod.yml playbook 以进入调试状态,如下面的屏幕截图所示:

图 9.16-执行示例 playbook 以触发调试器

图 9.16-执行示例 playbook 以触发调试器

我们需要步入函数以跟踪执行。步入函数将带我们到另一个文件,如下所示:

图 9.17-步入代码以跟踪执行

图 9.17-步入代码以跟踪执行

从这里,我们可以使用list来查看我们新文件中的代码,如下面的屏幕截图所示:

图 9.18-列出调试器中我们当前位置附近的代码

图 9.18-列出调试器中我们当前位置附近的代码

再走几行,我们来到一段代码块,将执行一个ssh命令并检查输出以确定ControlPersist是否受支持,如下面的屏幕截图所示:

图 9.19-定位代码以确定是否支持 ControlPersist

图 9.19-定位代码以确定是否支持 ControlPersist

让我们走过接下来的几行,然后打印出err的值。这将向我们展示ssh执行的结果以及Ansible将在其中搜索的整个字符串,如下面的屏幕截图所示:

图 9.20-使用 pdb 调试器分析 SSH 连接结果

图 9.20-使用 pdb 调试器分析 SSH 连接结果

正如我们所看到的,搜索字符串不在err变量中,因此has_cp的值仍然保持为True的默认值。

有关分叉和调试的快速说明

Ansible使用多进程进行多个分叉时,调试变得困难。调试器可能连接到一个分叉而不是另一个分叉,这将使调试代码变得非常困难。除非专门调试多进程代码,最好还是坚持使用单个分叉。

调试远程代码

远程代码是Ansible传输到远程主机以执行的代码。这通常是模块代码,或者在动作插件的情况下,是其他代码片段。使用我们在前一节讨论的调试方法来调试模块执行将不起作用,因为Ansible只是复制代码然后执行它。远程代码执行没有连接到终端,因此没有办法将其连接到调试提示符,即在不编辑模块代码的情况下是不可能的。

要调试模块代码,我们需要编辑模块代码本身以插入调试器断点。不要直接编辑已安装的模块文件,而是在与 playbooks 相关的library/目录中创建文件的副本。这个模块代码的副本将被用来代替已安装的文件,这样就可以在不影响系统上模块的其他用户的情况下临时编辑模块。

与其他 Ansible 代码不同,模块代码不能直接使用pdb进行调试,因为模块代码是组装然后传输到远程主机的。幸运的是,有一个解决方案,即一个稍微不同的调试器,名为rpdb - 远程 Python 调试器。这个调试器有能力在提供的端口上启动一个监听服务,以允许远程连接到 Python 进程。远程连接到进程将允许逐行调试代码,就像我们对其他 Ansible 代码所做的那样。

为了演示这个调试器是如何工作的,我们首先需要一个远程主机。在这个例子中,我们使用一个名为debug.example.com的远程主机(当然,你可以根据需要使用你自己的示例进行相应的调整)。接下来,我们需要一个 playbook 来执行我们想要调试的模块。代码如下所示:

---
- name: remote code debug
  hosts: debug.example.com
  gather_facts: false
  become: true
  tasks:
    - name: a remote module execution
      systemd:
        name: nginx
        state: stopped
        enabled: no

重要提示

你们中敏锐的人可能已经注意到,在本书中,我们第一次没有使用完全限定类名FQCN)来引用模块。这是因为 FQCN 告诉 Ansible 使用它自己期望的位置的内置模块,而我们实际上想要加载我们将放置在本地library/目录中的本地副本。因此,在这个特定情况下,我们必须只使用模块的简称。

我们还需要一个新的清单文件来引用我们的新测试主机。由于我没有为这个主机设置域名系统DNS)条目,我在清单中使用特殊的ansible_host变量,告诉 Ansible 连接到debug.example.com上的互联网协议IP)地址,如下面的代码片段所示:

debug.example.com ansible_host=192.168.81.154

重要提示

不要忘记在两个主机之间设置 SSH 身份验证 - 我使用 SSH 密钥,这样我就不需要每次运行ansible-playbook时都输入密码。

这个 play 只是调用ansible.builtin.systemd模块来确保nginx服务被停止,并且不会在启动时启动。正如我们之前所述,我们需要复制服务模块并将其放置在library/中。要复制的服务模块的位置将根据 Ansible 的安装方式而变化。在我为本书演示的演示系统上,它位于/usr/local/lib/python3.8/dist-packages/ansible/modules/systemd.py。然后,我们可以编辑它以插入我们的断点。我在我的系统上将其插入到第358行 - 这对于ansible-core 2.11.1 是正确的,但随着新版本的发布可能会发生变化。然而,下面的屏幕截图应该给你一个插入代码的想法:

图 9.21 - 将远程 Python 调试器插入到 Ansible 模块代码中

图 9.21 - 将远程 Python 调试器插入到 Ansible 模块代码中

我们将在创建systemctl变量值之前设置断点。首先,必须导入rpdb模块(这意味着远程主机上必须存在rpdb Python 库),然后需要使用set_trace()创建断点。

重要提示

在 Ubuntu Server 20.04 上(就像演示中使用的主机一样),可以使用以下命令使用pip安装rpdbsudo pip3 install rpdb

与常规调试器不同,此函数将打开一个端口并监听外部连接。默认情况下,该函数将在地址127.0.0.1上监听端口4444的连接。但是,该地址不会在网络上公开,因此在我的示例中,我已经指示rpdb在地址0.0.0.0上监听,这实际上是主机上的每个地址(尽管我相信您会理解,这会带来您需要小心的安全隐患!)。

重要提示

如果运行rpdb的主机有防火墙(例如firewalldufw),则需要为本例中的端口4444打开端口。

现在我们可以运行这个 playbook 来设置等待客户端连接的服务器,如下所示:

图 9.22 - 运行远程模块调试的测试 playbook

图 9.22 - 运行远程模块调试的测试 playbook

现在服务器正在运行,我们可以从另一个终端连接到它。可以使用telnet程序连接到正在运行的进程,如下面的截图所示:

图 9.23 - 使用 telnet 连接到远程 Python 调试器会话进行模块调试

图 9.23 - 使用 telnet 连接到远程 Python 调试器会话进行模块调试

从这一点开始,我们可以像平常一样进行调试。我们之前使用的命令仍然存在,比如list用来显示当前帧在代码中的位置,如下面的截图所示:

图 9.24 - 在远程调试会话中使用现熟悉的 Python 调试器命令

图 9.24 - 在远程调试会话中使用现熟悉的 Python 调试器命令

使用调试器,我们可以逐步跟踪systemd模块,以跟踪它如何确定底层工具的路径,跟踪在主机上执行了哪些命令,确定如何计算更改等。整个文件都可以逐步执行,包括模块可能使用的任何其他外部库,从而允许调试远程主机上的其他非模块代码。

如果调试会话允许模块干净地退出,playbook 的执行将恢复正常。但是,如果在模块完成之前断开调试会话,playbook 将产生错误,如下面的截图所示:

图 9.25 - 在提前终止远程调试会话时产生错误的示例

图 9.25 - 在提前终止远程调试会话时产生错误的示例

由于这种副作用,最好不要提前退出调试器,而是在调试完成后发出continue命令。

调试动作插件

有些模块实际上是动作插件。这些是在将代码传输到远程主机之前在本地执行一些代码的任务。一些示例动作插件包括copyfetchscripttemplate。这些插件的源代码可以在plugins/action/中找到。该目录中的每个插件都有自己的文件,可以在其中插入断点,以便调试执行的代码,而不是将代码发送到远程主机。调试这些通常使用pdb来完成,因为大多数代码是在本地执行的。

摘要

Ansible 是一款软件,软件会出现故障;这不是一个“如果”,而是一个“何时”的问题。无效的输入、不正确的假设和意外的环境都可能导致任务和操作表现不如预期时产生令人沮丧的情况。内省和调试是可以快速将沮丧转化为喜悦的故障排除技术,当发现根本原因时。

在本章中,我们学习了如何让 Ansible 将其操作记录到文件中,以及如何更改 Ansible 输出的详细程度。然后,我们学习了如何检查变量,以确保它们的值符合您的期望,然后再详细调试 Ansible 代码。此外,我们演示了如何在核心 Ansible 代码中插入断点,并使用标准 Python 工具执行本地和远程 Python 调试会话。

在下一章中,我们将学习如何通过编写自己的模块、插件和清单来源来扩展 Ansible 的功能。

问题

  1. 要查看连接尝试等详细信息,您需要以哪个详细程度启动 Ansible?

a)级别为 3 或以上

b)级别为 2 或以上

c)级别为 1 或以上

d)级别为 4

  1. 如果您在 playbook 中使用敏感数据,为什么应该小心使用高于一级的详细程度?

a)更高的详细程度不支持使用 vaults。

b)更高的详细程度可能会将敏感数据记录到控制台和/或日志文件中。

c)更高的详细程度将打印 SSH 密码。

  1. 可以通过集中配置 Ansible 将其输出记录到文件:

a)使用ANSIBLE_LOG_PATH环境变量

b)在ansible.cfg中使用log_path指令

c)将每个 playbook 运行的输出重定向到文件

d)所有这些

  1. 用于变量内省的模块的名称是:

a)ansible.builtin.analyze

b)ansible.builtin.introspect

c)ansible.builtin.debug

d)ansible.builtin.print

  1. 在引用 Ansible 变量中的子元素时,哪种语法最安全,以防止与保留的 Python 名称冲突?

a)点表示法

b)标准下标语法

c)Ansible 子元素表示法

d)标准点表示法

  1. 除非您需要执行低级别的代码调试,否则可以使用以下方法调试 playbook 的流程:

a)调试策略

b)调试执行

c)调试任务计划程序

d)这些都不是

  1. 在本书中演示的 Python 本地调试器的名称是:

a)PyDebug

b)python-debug

c)pdb

d)pdebug

  1. 您还可以调试远程主机上模块的执行:

a)使用 Python 的rpdb模块。

b)通过将 playbook 复制到主机并使用pdb

c)通过数据包跟踪器,如tcpdump

d)这是不可能的。

  1. 除非另有配置,远程 Python 调试器会在哪里接收连接?

a)127.0.0.1:4433

b)0.0.0.0:4444

c)127.0.0.1:4444

d)0.0.0.0:4433

  1. 为什么不应该在不让代码运行完成的情况下结束远程 Python 调试会话?

a)这会导致在您的 playbook 运行中出现错误。

b)这将导致文件丢失。

c)这可能会损坏您的 Ansible 安装。

d)这将导致挂起的调试会话。

第十章:扩展 Ansible

必须说Ansible采用了厨房水槽的功能方法,并试图在开箱即用时提供您可能需要的所有功能。随着ansible-core包及其相关集合,截至撰写本文时,几乎有 6000 个模块可供在 Ansible 中使用-与本书第二版出版时包含的(大约)800 个相比!除此之外,还有丰富的插件和过滤器架构,包括多个回调插件、查找插件、过滤器插件和包括动态清单插件在内的插件。现在,集合提供了一个全新的向量,通过它可以提供新的功能。

尽管如此,总会有一些情况,Ansible 并不能完全执行所需的任务,特别是在大型和复杂的环境中,或者在自定义的内部系统已经开发的情况下。幸运的是,Ansible 的设计,加上其开源性质,使任何人都可以通过开发功能来扩展它变得很容易。随着 Ansible 3.0 的集合的出现,扩展功能比以往任何时候都更容易。然而,在本章中,我们将专注于为ansible-core包做出贡献的具体内容。如果您希望通过创建集合来做出贡献,您可以按照本章提供的步骤轻松开发所需的代码(例如,创建一个新模块),然后将其打包为集合,就像我们在第二章中描述的那样,从早期的 Ansible 版本迁移。您如何做出贡献取决于您和您的目标受众-如果您觉得您的代码将帮助所有使用 Ansible 的人,那么您可能希望将其提交给ansible-core;否则,最好将其构建到一个集合中。

本章将探讨以下几种方式,可以向 Ansible 添加新功能:

  • 开发模块

  • 开发插件

  • 开发动态清单插件

  • 向 Ansible 项目贡献代码

技术要求

要按照本章中提供的示例,您需要一台运行Ansible 4.3或更新版本的 Linux 机器。几乎任何 Linux 发行版都可以-对于那些感兴趣的人,所有本章中提供的代码都是在Ubuntu Server 20.04 LTS上测试的,除非另有说明,并且在 Ansible 4.3 上测试。本章附带的示例代码可以从 GitHub 上下载:github.com/PacktPublishing/Mastering-Ansible-Fourth-Edition/tree/main/Chapter10

查看以下视频,了解代码的实际操作:bit.ly/3DTKL35

开发模块

模块是 Ansible 的工作马。它们提供了足够的抽象,使得 playbook 可以简单明了地陈述。由核心 Ansible 开发团队维护的模块和插件有 100 多个,并作为ansible-core包的一部分进行分发,涵盖命令、文件、软件包管理、源代码控制、系统、实用程序等。此外,社区贡献者维护了近 6000 个其他模块,扩展了许多这些类别和其他许多功能,例如公共云提供商、数据库、网络等,通过集合。真正的魔力发生在模块的代码内部,它接收传递给它的参数,并努力建立所需的结果。

在 Ansible 中,模块是被传输到远程主机以执行的代码片段。它们可以用远程主机可以执行的任何语言编写;然而,Ansible 提供了一些非常有用的快捷方式,用于用 Python 编写模块,您会发现大多数模块确实是用 Python 编写的。

基本模块构造

模块存在以满足需求-在主机上执行一项工作的需求。模块通常需要输入,但并不总是期望输入,并且将返回某种输出。模块还努力成为幂等,允许模块一遍又一遍地运行而不会产生负面影响。在 Ansible 中,输入以命令行参数的形式提供给模块,并且输出以 JSON 格式传递到STDOUT

输入通常以空格分隔的key=value语法提供,模块负责将其解构为可用数据。如果您使用 Python,有方便的函数来管理这一点,如果您使用不同的语言,那么完全处理输入就取决于您的模块代码。

输出采用 JSON 格式。惯例规定,在成功的情况下,JSON 输出应至少有一个键changed,这是一个布尔值,表示模块执行是否导致更改。还可以返回其他数据,这些数据可能有助于定义发生了什么变化,或者为以后使用向 playbook 提供重要信息。此外,主机信息可以在 JSON 数据中返回,以根据模块执行结果自动创建主机变量。我们将在以后更详细地看一下这一点,在提供事实数据部分。

自定义模块

Ansible 提供了一种简单的机制来利用除 Ansible 自带模块之外的自定义模块。正如我们在[第一章](B17462_01_Final_JC_ePub.xhtml#_idTextAnchor015)中学到的,Ansible 的系统架构和设计,Ansible 会搜索许多位置来找到所请求的模块。其中一个位置,实际上是第一个位置,是顶层 playbook 所在路径的library/子目录。这就是我们将放置自定义模块的地方,以便我们可以在示例 playbook 中使用它,因为我们的重点是为ansible-core软件包开发。但是,正如我们已经提到的,您也可以通过集合分发模块,并且[第二章](B17462_02_Final_JC_ePub.xhtml#_idTextAnchor047)描述了(以本章节为例的实际示例)如何打包模块以通过集合进行分发。

除此之外,模块也可以嵌入在角色中,以提供角色可能依赖的附加功能。这些模块仅对包含模块的角色或在包含模块的角色之后执行的任何其他角色或任务可用。要使用角色提供模块,将模块放在角色根目录的library/子目录中。虽然这仍然是一种可行的途径,但预计随着 Ansible 3.0 及以后版本的普及,您将通过集合分发您的模块。提供了一个重叠期来支持许多现有的 Ansible 2.9 及更早版本的发行版。

示例-简单模块

为了演示编写基于 Python 的模块的简易性,让我们创建一个简单的模块。这个模块的目的是远程复制源文件到目标文件,这是一个简单的任务,我们可以逐步构建起来。为了启动我们的模块,我们需要创建模块文件。为了方便访问我们的新模块,我们将在已经使用的工作目录的library/子目录中创建文件。我们将这个模块称为remote_copy.py,为了开始它,我们需要放入一个 shebang 行,以指示这个模块将使用 Python 执行:

#!/usr/bin/python 
# 

对于基于 Python 的模块,约定使用/usr/bin/python作为列出的可执行文件。在远程系统上执行时,将使用远程主机的配置 Python 解释器来执行模块,因此如果您的 Python 代码不存在于此路径,也不必担心。接下来,我们将导入一个稍后在模块中使用的 Python 库,称为shutil

import shutil 

现在,我们准备创建我们的main函数。main函数本质上是模块的入口点,模块的参数将在这里定义,执行也将从这里开始。在 Python 中创建模块时,我们可以在这个main函数中采取一些捷径,绕过大量样板代码,直接进行参数定义。

我们可以通过创建一个AnsibleModule对象并为参数提供一个argument_spec字典来实现这一点:

def main(): 
    module = AnsibleModule( 
        argument_spec = dict( 
            source=dict(required=True, type='str'), 
            dest=dict(required=True, type='str') 
        ) 
    ) 

在我们的模块中,我们提供了两个参数。第一个参数是source,我们将用它来定义复制的源文件。第二个参数是dest,它是复制的目的地。这两个参数都标记为必需,如果其中一个未提供,将引发错误。这两个参数都是string类型。AnsibleModule类的位置尚未定义,因为这将在文件的后面发生。

有了模块对象,我们现在可以创建在远程主机上执行实际工作的代码。我们将利用shutil.copy和我们提供的参数来实现这一点:

    shutil.copy(module.params['source'], 
                module.params['dest']) 

shutil.copy函数期望一个源和一个目的地,我们通过访问module.params来提供这些。module.params字典包含模块的所有参数。完成复制后,我们现在准备将结果返回给 Ansible。这是通过另一个AnsibleModule方法exit_json完成的。这个方法期望一组key=value参数,并将适当地格式化为 JSON 返回。由于我们总是执行复制,出于简单起见,我们将始终返回一个更改:

    module.exit_json(changed=True) 

这一行将退出函数,因此也将退出模块。这个函数假设操作成功,并将以成功的适当返回代码0退出模块。不过,我们还没有完成模块的代码;我们仍然需要考虑AnsibleModule的位置。这是一个小魔术发生的地方,我们告诉 Ansible 要与我们的模块结合的其他代码,以创建一个完整的可传输的作品:

from ansible.module_utils.basic import * 

就是这样!这一行就可以让我们访问所有基本的module_utils,一组不错的辅助函数和类。我们应该在我们的模块中加入最后一件事:几行代码,告诉解释器在执行模块文件时执行main()函数。

if __name__ == '__main__': 
    main() 

现在,我们的模块文件已经完成,这意味着我们可以用一个 playbook 来测试它。我们将称我们的 playbook 为simple_module.yaml,并将其存储在与library/目录相同的目录中,我们刚刚编写了我们的模块文件。出于简单起见,我们将在localhost上运行 play,并在/tmp中使用一些文件名作为源和目的地。我们还将使用一个任务来确保我们首先有一个源文件:

--- 
- name: test remote_copy module 
  hosts: localhost 
  gather_facts: false 

  tasks: 
  - name: ensure foo
    ansible.builtin.file:
      path: /tmp/rcfoo
      state: touch
  - name: do a remote copy
    remote_copy:
      source: /tmp/rcfoo
      dest: /tmp/rcbar

由于我们的新模块是从与 playbook 本地的library/目录运行的,它没有一个完全合格的集合名称FQCN),因此在 playbook 中我们只会用它的简称来引用它。要运行这个 playbook,我们将运行以下命令:

ansible-playbook -i mastery-hosts simple_module.yaml -v

如果remote_copy模块文件写入了正确的位置,一切都将正常工作,屏幕输出将如下所示:

图 10.1-运行一个简单的 playbook 来测试我们的第一个自定义 Ansible 模块

图 10.1-运行一个简单的 playbook 来测试我们的第一个自定义 Ansible 模块

我们的第一个任务涉及/tmp/rcfoo路径,以确保它存在,然后我们的第二个任务使用remote_copy/tmp/rcfoo复制到/tmp/rcbar。两个任务都成功,每次都会产生一个changed状态。

记录模块

除非包含了有关如何操作它的文档,否则不应该认为模块是完整的。模块的文档存在于模块本身中,称为DOCUMENTATIONEXAMPLESRETURN的特殊变量中。

DOCUMENTATION变量包含一个特殊格式的字符串,描述了模块的名称,ansible-core的版本或其添加到的父集合的版本,模块的简短描述,更长的描述,模块参数的描述,作者和许可信息,额外要求以及对模块用户有用的任何额外说明。让我们在现有的import shutil语句下为我们的模块添加一个DOCUMENTATION字符串:

import shutil 

DOCUMENTATION = ''' 
--- 
module: remote_copy 
version_added: future 
short_description: Copy a file on the remote host 
description: 
  - The remote_copy module copies a file on the remote host from a given source to a provided destination. 
options: 
  source: 
    description: 
      - Path to a file on the source file on the remote host 
    required: True 
  dest: 
    description: 
      - Path to the destination on the remote host for the copy 
    required: True 
author: 
  - Jesse Keating 
''' 

字符串的格式本质上是 YAML,其中一些顶级键包含其中的哈希结构(与options键相同)。每个选项都有子元素来描述选项,指示选项是否是必需的,列出选项的任何别名,列出选项的静态选择,或指示选项的默认值。将此字符串保存到模块后,我们可以测试我们的格式,以确保文档将正确呈现。这是通过ansible-doc工具完成的,使用参数指示在哪里搜索模块。如果我们从与我们的 playbook 相同的位置运行它,命令将如下所示:

ansible-doc -M library/ remote_copy

输出应如下所示:

图 10.2 - 使用 ansible-doc 工具查看我们的新模块的文档

](Images/B17462_10_02.jpg)

图 10.2 - 使用 ansible-doc 工具查看我们的新模块的文档

在这个例子中,我将输出导入cat以防止分页程序隐藏执行行。我们的文档字符串似乎格式正确,并为用户提供了有关模块使用的重要信息。

EXAMPLES字符串用于提供模块的一个或多个示例用法,以及在 playbook 中使用的任务代码片段。让我们添加一个示例任务来演示其用法。这个变量定义传统上是在DOCUMENTATION定义之后:

EXAMPLES = ''' 
# Example from Ansible Playbooks 
- name: backup a config file 
  remote_copy: 
    source: /etc/herp/derp.conf 
    dest: /root/herp-derp.conf.bak 
''' 

有了这个变量定义,我们的ansible-doc输出现在将包括示例,如下所示:

图 10.3 - 通过 EXAMPLES 部分扩展我们的模块文档

](Images/B17462_10_03.jpg)

图 10.3 - 通过 EXAMPLES 部分扩展我们的模块文档

最后一个文档变量RETURN用于描述模块执行的返回数据。返回数据通常作为注册变量对后续使用很有用,并且有关预期返回数据的文档可以帮助 playbook 的开发。我们的模块还没有任何返回数据;因此,在我们可以记录任何返回数据之前,我们必须添加返回数据。这可以通过修改module.exit_json行来添加更多信息来完成。让我们将sourcedest数据添加到返回输出中:

    module.exit_json(changed=True, source=module.params['source'], 
                     dest=module.params['dest']) 

重新运行 playbook 将显示返回额外数据,如下面的截图所示:

图 10.4 - 运行我们扩展的模块并添加返回数据

](Images/B17462_10_04.jpg)

图 10.4 - 运行我们扩展的模块并添加返回数据

仔细观察返回数据,我们可以看到比我们在模块中放入的更多数据。这是 Ansible 中的一些辅助功能;当返回数据集包括dest变量时,Ansible 将收集有关目标文件的更多信息。收集的额外数据是gid(组 ID),group(组名),mode(权限),uid(所有者 ID),owner(所有者名),sizestate(文件,链接或目录)。我们可以在我们的RETURN变量中记录所有这些返回项,它是在EXAMPLES变量之后添加的。两组三个单引号(''')之间的所有内容都会被返回 - 因此,这第一部分返回文件路径和所有权:

RETURN = ''' 
source: 
  description: source file used for the copy 
  returned: success 
  type: string 
  sample: "/path/to/file.name" 
dest: 
  description: destination of the copy 
  returned: success 
  type: string 
  sample: "/path/to/destination.file" 
gid: 
  description: group ID of destination target 
  returned: success 
  type: int 
  sample: 502 
group: 
  description: group name of destination target 
  returned: success 
  type: string 
  sample: "users" 
uid: 
  description: owner ID of destination target 
  returned: success 
  type: int 
  sample: 502 
owner: 
  description: owner name of destination target 
  returned: success 
  type: string 
  sample: "fred"

继续模块定义文件的这一部分,这一部分返回有关文件大小,状态和权限的详细信息:

mode: 
  description: permissions of the destination target 
  returned: success 
  type: int 
  sample: 0644 
size: 
  description: size of destination target 
  returned: success 
  type: int 
  sample: 20 
state: 
  description: state of destination target 
  returned: success 
  type: string 
  sample: "file" 
''' 

每个返回的项目都列有描述、项目在返回数据中的情况、项目的类型和值的示例。RETURN字符串由ansible-doc解析,但返回值按字母顺序排序,在本书的上一个版本中,我们看到值是按模块本身中列出的顺序打印的。以下屏幕截图显示了这一点:

图 10.5 - 向我们的模块添加返回数据文档

图 10.5 - 向我们的模块添加返回数据文档

通过这种方式,我们建立了一个包含文档的模块,如果我们将其贡献给社区,对其他人来说非常有用,甚至对我们自己来说,当我们一段时间后回来时也很有用。

提供事实数据

与作为模块的一部分返回的数据类似,例如exit,模块可以通过在名为ansible_facts的键中返回数据来直接为主机创建事实。直接从模块提供事实可以消除需要使用后续的set_fact任务注册任务的返回的需要。为了演示这种用法,让我们修改我们的模块以返回sourcedest数据作为事实。因为这些事实将成为顶级主机变量,我们希望使用比sourcedest更具描述性的事实名称。用以下代码替换我们模块中的当前module.exit_json行:

    facts = {'rc_source': module.params['source'], 
             'rc_dest': module.params['dest']} 

    module.exit_json(changed=True, ansible_facts=facts) 

我们还将向我们的 playbook 添加一个任务,使用debug语句中的一个事实:

  - name: show a fact 
    ansible.builtin.debug: 
      var: rc_dest 

现在,运行 playbook 将显示新的返回数据,以及变量的使用,如下屏幕截图所示:

图 10.6 - 向我们的自定义模块添加事实,并在 playbook 执行期间查看它们的值

图 10.6 - 向我们的自定义模块添加事实,并在 playbook 执行期间查看它们的值

如果我们的模块不返回事实(我们之前的remote_copy.py版本没有),我们将不得不注册输出并使用set_fact为我们创建事实,如下面的代码所示:

  - name: do a remote copy 
    remote_copy: 
      source: /tmp/rcfoo 
      dest: /tmp/rcbar 
    register: mycopy 

  - name: set facts from mycopy 
    ansible.builtin.set_fact: 
      rc_dest: "{{ mycopy.dest }}" 

虽然能够这样做很有用,但在设计我们的模块时,最好让模块定义所需的事实。如果不这样做,那么以前的注册和set_fact代码将需要在 playbook 中每次使用我们的模块时重复!

检查模式

自其存在以来,Ansible 就支持检查模式,这是一种操作模式,会假装对系统进行更改,而实际上并未更改系统。检查模式对于测试是否会发生更改或系统状态是否已漂移自上次 Ansible 运行以来非常有用。检查模式取决于模块是否支持它并返回数据,就好像已经完成了更改一样。在我们的模块中支持检查模式需要两个更改;第一个是指示模块支持检查模式,而第二个是在执行之前检测检查模式是否激活并返回数据。

支持检查模式

要指示模块支持检查模式,必须在创建模块对象时设置一个参数。这可以在定义模块对象中的argument_spec变量之前或之后完成;在这里,我们将在定义之后完成:

    module = AnsibleModule( 
        argument_spec = dict( 
            source=dict(required=True, type='str'), 
            dest=dict(required=True, type='str') 
        ), 
        supports_check_mode=True 
    ) 

如果您正在修改现有代码,请不要忘记在argument_spec字典定义之后添加逗号,如前面的代码所示。

处理检查模式

检测检查模式是否激活非常容易。模块对象将具有一个check_mode属性,当检查模式激活时,它将设置为布尔值true。在我们的模块中,我们希望在执行复制之前检测检查模式是否激活。我们可以简单地将复制操作移到一个if语句中,以避免在检查模式激活时进行复制。除此之外,对模块不需要进行进一步的更改:

    if not module.check_mode: 
        shutil.copy(module.params['source'], 
                    module.params['dest']) 

现在,我们可以运行我们的 playbook,并在执行中添加-C参数。这个参数启用检查模式。我们还将测试以确保 playbook 没有创建和复制文件。以下截图显示了这一点:

图 10.7-为我们的 Ansible 模块添加检查模式支持

图 10.7-为我们的 Ansible 模块添加检查模式支持

尽管模块的输出看起来好像创建并复制了文件,但我们可以看到在执行之前这些文件并不存在,并且在执行之后仍然不存在,这清楚地表明我们的简单模块是在检查模式下运行的。

现在我们已经看了我们的简单示例模块,我们将探讨如何通过另一个重要的项目-插件来扩展 Ansible 的功能。

开发插件

插件是扩展或修改 Ansible 功能的另一种方式。虽然模块是作为任务执行的,但插件在各种其他地方使用。插件根据它们插入到 Ansible 执行的位置被分为几种类型。Ansible 为每个领域提供了一些插件,最终用户可以创建自己的插件来扩展这些特定领域的功能。

连接类型插件

每当 Ansible 连接到主机执行任务时,都会使用连接插件。Ansible 附带了一些连接插件,包括sshcommunity.docker.dockerlocalwinrm。Ansible 可以通过创建连接插件来利用其他连接机制,这可能会有用,如果您必须连接到一些新类型的系统,比如网络交换机,或者也许有一天连接到您的冰箱。要创建一个新的连接插件,我们必须了解并使用底层通信协议,这本身可能需要一本专门的书籍;因此,我们不会在这里尝试创建一个。然而,开始的最简单方法是阅读与 Ansible 一起提供的现有插件,并选择一个进行必要的修改。现有的插件可以在您的系统上安装 Ansible Python 库的位置中找到,例如在我的系统上是/usr/local/lib/python3.8/dist-packages/ansible/plugins/connection/。您也可以在 GitHub 上查看它们-例如,如果您想查找与ansible-core2.11.1版本相关的文件,您可以在这里查看:github.com/ansible/ansible/tree/v2.11.1/lib/ansible/plugins/connection

Shell 插件

与连接插件类似,Ansible 使用shell 插件在 shell 环境中执行操作。每个 shell 都有 Ansible 关心的微妙差异,以正确执行命令,重定向输出,发现错误等交互。Ansible 支持多种 shell,包括shansible.posix.cshansible.posix.fishpowershell。我们可以通过实现新的 shell 插件来添加更多的 shell。您可以在这里查看它们的代码(对于ansible-core2.11.1版本):github.com/ansible/ansible/tree/v2.11.1/lib/ansible/plugins/shell

查找插件

查找插件是 Ansible 从主机系统访问外部数据源并实现语言特性,比如循环结构(loopwith_*)的方式。可以创建查找插件来访问现有数据存储中的数据或创建新的循环机制。现有的查找插件可以在plugins/lookup/中找到,或者在 GitHub 上找到:github.com/ansible/ansible/tree/v2.11.1/lib/ansible/plugins/lookup。查找插件可以添加以引入新的循环内容的方式,或者用于在外部系统中查找资源。

Vars 插件

存在用于注入变量数据的构造,形式为vars 插件。诸如host_varsgroup_vars之类的数据是通过插件实现的。虽然可以创建新的变量插件,但通常最好创建自定义清单源或事实模块。

事实缓存插件

Ansible 可以在 playbook 运行之间缓存事实。事实的缓存位置取决于所使用的配置缓存插件。Ansible 包括在memory(它们在运行之间不会被缓存,因为这不是持久的)、community.general.memcachedcommunity.general.redisjsonfile中缓存事实的插件。创建一个事实缓存插件可以启用额外的缓存机制。

过滤插件

虽然 Jinja2 包含了几个过滤器,但 Ansible 已经使过滤器可插拔以扩展 Jinja2 的功能。Ansible 包括了一些对 Ansible 操作有用的过滤器,并且 Ansible 的用户可以添加更多过滤器。现有的插件可以在plugins/filter/中找到。

为了演示过滤器插件的开发,我们将创建一个简单的过滤器插件来对文本字符串进行一些愚蠢的操作。我们将创建一个过滤器,它将用字符串somebody else's computer替换任何出现的the cloud。我们将在现有工作目录中的新目录filter_plugins/中的文件中定义我们的过滤器。文件的名称无关紧要,因为我们将在文件中定义过滤器的名称;所以,让我们将文件命名为filter_plugins/sample_filter.py

首先,我们需要定义执行翻译的函数,并提供翻译字符串的代码:

def cloud_truth(a): 
    return a.replace("the cloud", "somebody else's computer") 

接下来,我们需要构建一个FilterModule对象,并在其中定义我们的过滤器。这个对象是 Ansible 将要加载的对象,Ansible 期望在对象内有一个filters函数,该函数返回文件中的一组过滤器名称到函数的映射:

class FilterModule(object): 
    '''Cloud truth filters''' 
    def filters(self): 
        return {'cloud_truth': cloud_truth} 

现在,我们可以在一个 playbook 中使用这个过滤器,我们将其命名为simple_filter.yaml

--- 
- name: test cloud_truth filter 
  hosts: localhost 
  gather_facts: false 
  vars: 
    statement: "I store my files in the cloud" 
  tasks: 
  - name: make a statement 
    ansible.builtin.debug: 
      msg: "{{ statement | cloud_truth }}" 

现在,让我们运行我们的 playbook,看看我们的过滤器如何运行:

图 10.8 - 执行 playbook 以测试我们的新过滤器插件

图 10.8 - 执行 playbook 以测试我们的新过滤器插件

我们的过滤器起作用了,它将the cloud这个词替换为somebody else's computer。这是一个愚蠢的例子,没有任何错误处理,但它展示了我们扩展 Ansible 和 Jinja2 的过滤器功能的能力。

重要提示

虽然包含过滤器定义的文件的名称可以是开发人员想要的任何名称,但最佳做法是将其命名为过滤器本身,以便将来可以轻松找到它,可能是由其他合作者找到。这个例子没有遵循这个规则,以演示文件名不附加到过滤器名称。

回调插件

回调是可以插入以增加功能的 Ansible 执行中的位置。有预期的回调点可以注册以触发这些点的自定义操作。以下是可能用于在编写时触发功能的点的列表:

  • v2_on_any

  • v2_runner_on_failed

  • v2_runner_on_ok

  • v2_runner_on_skipped

  • v2_runner_on_unreachable

  • v2_runner_on_async_poll

  • v2_runner_on_async_ok

  • v2_runner_on_async_failed

  • v2_runner_on_start

  • v2_playbook_on_start

  • v2_playbook_on_notify

  • v2_playbook_on_no_hosts_matched

  • v2_playbook_on_no_hosts_remaining

  • v2_playbook_on_task_start

  • v2_playbook_on_cleanup_task_start

  • v2_playbook_on_handler_task_start

  • v2_playbook_on_vars_prompt

  • v2_playbook_on_import_for_host

  • v2_playbook_on_not_import_for_host

  • v2_playbook_on_play_start

  • v2_playbook_on_stats

  • v2_on_file_diff

  • v2_playbook_on_include

  • v2_runner_item_on_ok

  • v2_runner_item_on_failed

  • v2_runner_item_on_skipped

  • v2_runner_retry

当 Ansible 运行达到这些状态时,任何具有在这些点运行代码的插件都将被执行。这提供了在不修改基本代码的情况下扩展 Ansible 的巨大能力。

回调可以以各种方式使用:更改屏幕上的显示方式,更新进度的中央状态系统,实现全局锁定系统,或者几乎可以想象的任何事情。这是扩展 Ansible 功能的最强大方式。但是,您会注意到先前列出的项目在官方 Ansible 文档网站(docs.ansible.com)上没有出现,也不会被ansible-doc命令列出。查找这些回调并了解更多关于它们的信息的好地方是plugins/callback/__init__.py文件,在您的ansible-core安装目录下。例如,在我的系统上,Ansible 是使用 pip 安装的,完整路径是/usr/local/lib/python3.8/dist-packages/ansible/plugins/callback/__init__.py(如果您想在互联网上查找此文件,ansible-core2.11.1版本的文件可以在此处找到:github.com/ansible/ansible/blob/v2.11.1/lib/ansible/plugins/callback/__init__.py)。

为了演示我们开发回调插件的能力,我们将创建一个简单的插件,当 playbook 在最后打印 play 摘要时,它将在屏幕上打印一些愚蠢的东西:

  1. 首先,我们需要创建一个新目录来保存我们的回调。Ansible 将查找的位置是callback_plugins/。与之前的filter插件不同,我们确实需要仔细命名我们的回调插件文件,因为它也必须在ansible.cfg文件中反映出来。

  2. 我们将命名为callback_plugins/shrug.py。由于 Ansible 版本大于 3.0 正在向 Python 3 支持移动(尽管在撰写本文时仍支持 Python 2.7),因此您的插件代码应该是为 Python 3 编写的。首先在插件中添加以下 Python 3 头:

from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
  1. 接下来,您需要添加一个文档块,就像我们在本章的开发模块部分所做的那样。在本书的上一版中,不需要这样做,但现在,如果您不这样做,将会收到弃用警告,并且您的回调插件在ansible-core 2.14 发布时可能无法工作。我们的文档块将如下所示:
DOCUMENTATION = '''
    callback: shrug
    type: stdout
    short_description: modify Ansible screen output
    version_added: 4.0
    description:
        - This modifies the default output callback for ansible-playbook.
    extends_documentation_fragment:
      - default_callback
    requirements:
      - set as stdout in configuration
'''

文档中的大多数项目都是不言自明的,但值得注意的是extends_documentation_fragment项目。文档块的这一部分是与ansible-core 2.14 兼容所必需的部分,因为我们在这里扩展了default_callback插件,我们需要告诉 Ansible 我们正在扩展这一部分文档。

  1. 完成后,我们需要创建一个CallbackModule类,它是从ansible.plugins.callback.default中找到的default回调插件中定义的CallbackModule的子类,因为我们只需要更改正常输出的一个方面。

  2. 在这个类中,我们将定义变量值来指示它是2.0版本的回调,它是stdout类型的回调,并且它的名称是shrug

  3. 此外,在这个类中,我们必须初始化它,以便我们可以定义我们想要插入以使某些事情发生的回调点中的一个或多个。在我们的示例中,我们想要修改运行结束时生成的 playbook 摘要的显示,因此我们将修改v2_playbook_on_stats回调。

  4. 为了完成我们的插件,我们必须调用原始的回调模块本身。Ansible 现在一次只支持一个stdout插件,因此如果我们不调用原始插件,我们将发现我们的插件的输出是唯一产生的输出-有关 playbook 运行的所有其他信息都将丢失!文档块下面的最终代码应该如下所示:

from ansible.plugins.callback.default import CallbackModule as CallbackModule_default
class CallbackModule(CallbackModule_default):
  CALLBACK_VERSION = 2.0
  CALLBACK_TYPE = 'stdout'
  CALLBACK_NAME = 'shrug'
  def __init__(self):
    super(CallbackModule, self).__init__()
  def v2_playbook_on_stats(self, stats):
    msg = b'\xc2\xaf\\_(\xe3\x83\x84)_/\xc2\xaf'
    self._display.display(msg.decode('utf-8') * 8)
    super(CallbackModule, self).v2_playbook_on_stats(stats)
  1. 由于此回调是stdout_callback,我们需要创建一个ansible.cfg文件,并在其中指示应使用shrug stdout回调。ansible.cfg文件可以在/etc/ansible/中找到,也可以在与 playbook 相同的目录中找到:
[defaults] 
stdout_callback = shrug 
  1. 这就是我们在回调中要写的全部内容。一旦保存,我们就可以重新运行之前的 playbook,这个 playbook 练习了我们的sample_filter,但这次,在屏幕上会看到不同的东西:

图 10.9-将我们的 shrug 插件添加到修改 playbook 运行输出

图 10.9-将我们的 shrug 插件添加到修改 playbook 运行输出

这很愚蠢,但它展示了插入到 playbook 执行的各个点的能力。我们选择在屏幕上显示一系列耸肩,但我们也可以与一些内部审计和控制系统进行交互,记录操作,或者向 IRC 或 Slack 频道报告进度。

动作插件

动作插件用于在不实际执行模块的情况下钩入任务结构,或者在在远程主机上执行模块之前在 Ansible 主机上执行代码。Ansible 包含多个动作插件,它们可以在plugins/action/中找到。其中一个动作插件是template插件,它可以用来代替template模块。当 playbook 作者编写template任务时,该任务将调用template插件来执行工作。该插件将在将内容复制到远程主机之前在本地渲染模板。因为动作必须在本地发生,所以由动作插件完成工作。我们应该熟悉的另一个动作插件是debug插件,在本书中我们大量使用它来打印内容。当我们尝试在同一个任务中完成本地工作和远程工作时,创建自定义动作插件是有用的。

分发插件

与分发自定义模块类似,存储自定义插件的标准位置与期望使用插件的 playbooks 一起。插件的默认位置是与 Ansible 代码安装一起提供的位置,~/.ansible/plugins/的子目录,以及项目根目录的子目录(顶级 playbook 存储的位置)。插件也可以在角色的相同子目录中分发,以及集合中,正如我们在第二章中所介绍的那样,从早期 Ansible 版本迁移。要使用来自任何其他位置的插件,我们需要在ansible.cfg文件中定义查找插件类型的插件的位置,或者引用集合,就像我们在第二章中加载示例过滤器模块时所演示的那样,从早期 Ansible 版本迁移

如果您在项目根目录内分发插件,每种插件类型都有自己的顶级目录:

  • action_plugins/

  • cache_plugins/

  • callback_plugins/

  • connection_plugins/

  • shell_plugins/

  • lookup_plugins/

  • vars_plugins/

  • filter_plugins/

与其他 Ansible 结构一样,找到的具有相同名称的第一个插件将被使用,并且与模块一样,首先检查相对于项目根目录的路径,允许本地覆盖现有插件。只需将过滤器文件放在适当的子目录中,当引用时将自动使用它。

开发动态清单插件

清单插件是一些代码,将为 Ansible 执行创建清单数据。在许多环境中,简单的ini文件样式的清单源和变量结构不足以表示实际管理的基础设施。在这种情况下,需要动态清单源,它将在每次执行 Ansible 时动态发现清单和数据。许多这些动态源与 Ansible 一起提供,主要是为了在一个云计算平台或另一个云计算平台内部构建的基础设施上操作 Ansible。与 Ansible 4.3 一起提供的动态清单插件的简短而不完整的列表(现在有超过 40 个)包括以下内容-请注意来自 FQCNs 的这些插件,这些插件曾经作为 Ansible 2.x 版本的一部分提供,现在作为构成 Ansible 4.3 的更广泛集合的一部分被包含进来:

  • azure.azcollection.azure_rm

  • community.general.cobbler

  • community.digitalocean.digitalocean

  • community.docker.docker_containers

  • amazon.aws.aws_ec2

  • google.cloud.gcp_compute

  • community.libvirt.libvirt

  • community.general.linode

  • kubernetes.core.openshift

  • openstack.cloud.openstack

  • community.vmware.vmware_vm_inventory

  • servicenow.servicenow.now

清单插件本质上是可执行脚本。Ansible 使用设置的参数(--list--host <hostname>)调用脚本,并期望在STDOUT上以 JSON 格式输出。当提供--list参数时,Ansible 期望列出要管理的所有组的列表。每个组可以列出主机成员资格、子组成员资格和组变量数据。当使用--host <hostname>参数调用脚本时,Ansible 期望返回特定于主机的数据(或空的 JSON 字典)。

使用动态清单源很容易。可以通过在ansibleansible-playbook中使用-i--inventory-file)选项直接引用源,也可以通过将插件文件放在ansible.cfg中清单路径引用的目录中。

在创建清单插件之前,我们必须了解在使用我们的脚本时--list--host的预期格式。

列出主机

--list参数传递给清单脚本时,Ansible 期望 JSON 输出数据具有一组顶级键。这些键以清单中的组命名。每个组都有一个键。组键内的结构因需要在组中表示的数据而异。如果一个组只有主机而没有组级变量,则键内的数据可以简单地是主机名的列表。如果组有变量或子组(一组组),则数据需要是一个哈希,可以有一个或多个名为hostsvarschildren的键。hostschildren子键具有列表值,即组中存在的主机列表或子组列表。vars子键具有哈希值,其中每个变量的名称和值由键和值表示。

列出主机变量

--host <hostname>参数传递给清单脚本时,Ansible 期望 JSON 输出数据只是变量的哈希,其中每个变量的名称和值由键和值表示。如果对于给定主机没有变量,则期望一个空的哈希。

简单的库存插件

为了演示开发清单插件,我们将创建一个简单打印一些静态清单主机数据的插件 - 它不会是动态的,但这是理解基础知识和所需输出格式的一个很好的第一步。这是基于我们在整本书中使用过的一些清单,所以它们在某些部分可能看起来很熟悉。我们将把我们的清单插件写入项目根目录中名为mastery-inventory.py的文件,并使其可执行。我们将使用 Python 编写此文件,以便轻松处理执行参数和 JSON 格式化,但请记住,您可以使用任何您喜欢的语言编写清单脚本,只要它们产生所需的 JSON 输出:

  1. 首先,我们需要添加一个 shebang 行来指示此脚本将使用 Python 执行:
#!/usr/bin/env python 
# 
  1. 接下来,我们需要导入一些稍后在插件中需要的 Python 模块:
import json 
import argparse 
  1. 现在,我们将创建一个 Python 字典来保存我们所有的组。我们的一些组只有主机,而其他组有变量或子组。我们将相应地格式化每个组:
inventory = {} 
inventory['web'] = {'hosts': ['mastery.example.name'], 
'vars': {'http_port': 80, 
'proxy_timeout': 5}} 
inventory['dns'] = {'hosts': ['backend.example.name']} 
inventory['database'] = {'hosts': ['backend.example.name'], 
'vars': {'ansible_ssh_user': 'database'}} 
inventory['frontend'] = {'children': ['web']} 
inventory['backend'] = {'children': ['dns', 'database'], 
'vars': {'ansible_ssh_user': 'blotto'}} 
inventory['errors'] = {'hosts': ['scsihost']} 
inventory['failtest'] = {'hosts': ["failer%02d" % n for n in 
                                   range(1,11)]} 
  1. 创建我们的failtest组(您将在下一章中看到此操作),在我们的清单文件中将表示为failer[01:10],我们可以使用 Python 列表推导来为我们生成列表,格式化列表中的项目与我们的ini格式的清单文件完全相同。其他组条目应该是不言自明的。

  2. 我们的原始清单还有一个all组变量,它为所有组提供了一个默认变量ansible_ssh_user(组可以覆盖),我们将在这里定义并在文件后面使用:

allgroupvars = {'ansible_ssh_user': 'otto'} 
  1. 接下来,我们需要在它们自己的字典中输入特定于主机的变量。我们原始清单中只有一个节点具有特定于主机的变量 - 我们还将添加一个新主机scsihost,以进一步开发我们的示例:
hostvars = {} 
hostvars['mastery.example.name'] = {'ansible_ssh_host': '192.168.10.25'} 
hostvars['scsihost'] = {'ansible_ssh_user': 'jfreeman'} 
  1. 定义了所有数据后,我们现在可以继续处理参数解析的代码。这可以通过我们在文件中导入的argparse模块来完成:
parser = argparse.ArgumentParser(description='Simple Inventory')
parser.add_argument('--list', action='store_true', help='List all hosts')
parser.add_argument('--host', help='List details of a host')
args = parser.parse_args()
  1. 解析参数后,我们可以处理--list--host操作。如果请求列表,我们只需打印我们清单的 JSON 表示。这是我们将考虑allgroupvars数据的地方;每个组的默认ansible_ssh_user。我们将循环遍历每个组,创建allgroupvars数据的副本,更新该数据以及可能已经存在于组中的任何数据,然后用新更新的副本替换组的变量数据。最后,我们将打印结果:
if args.list: 
for group in inventory: 
ag = allgroupvars.copy() 
ag.update(inventory[group].get('vars', {})) 
inventory[group]['vars'] = ag 
    print(json.dumps(inventory)) 
  1. 最后,我们将通过构建一个字典来处理--host操作,该字典包含可以应用于传递给此脚本的主机的所有变量。我们将使用 Ansible 在解析ini格式清单时使用的优先顺序的近似值来执行此操作。这段代码是迭代的,嵌套循环在生产环境中效率不高,但在这个例子中,它对我们很有用。输出是提供的主机的 JSON 格式的变量数据,如果提供的主机没有特定的变量数据,则为空哈希:
elif args.host:
    hostfound = False
    agghostvars = allgroupvars.copy()
    for group in inventory:
        if args.host in inventory[group].get('hosts', {}):
            hostfound = True
            for childgroup in inventory:
                if group in inventory[childgroup].get('children', {}):
                    agghostvars.update(inventory[childgroup].get('vars', {}))
    for group in inventory:
        if args.host in inventory[group].get('hosts', {}):
            hostfound = True
            agghostvars.update(inventory[group].get('vars', {}))
    if hostvars.get(args.host, {}):
        hostfound = True
    agghostvars.update(hostvars.get(args.host, {}))
    if not hostfound:
        agghostvars = {}
    print(json.dumps(agghostvars))

现在,我们的清单已经准备好测试了!我们可以直接执行它,并传递--help参数,我们可以免费使用argparse获得。这将根据我们之前在文件中提供的argparse数据显示我们脚本的用法:

图 10.10 - 测试我们的动态清单脚本的内置帮助函数

图 10.10 - 测试我们的动态清单脚本的内置帮助函数

重要提示

不要忘记使动态清单脚本可执行;例如,chmod +x mastery-inventory.py

如果我们传递--list,我们将得到所有组的输出,以及每个组中的所有主机和所有相关的清单变量:

图 10.11 - 显示我们的动态清单脚本的--list 参数产生的 JSON 输出

图 10.11-显示我们的动态清单脚本的--list 参数产生的 JSON 输出

同样,如果我们使用--host参数和我们知道在清单中的主机名运行这个 Python 脚本,我们将看到传递的主机名的主机变量。如果我们传递一个组名,什么都不应该返回,因为脚本只返回有效的单个主机名的数据:

图 10.12-显示我们的动态清单脚本的--list 参数产生的 JSON 输出

图 10.12-显示我们的动态清单脚本的--list 参数产生的 JSON 输出

现在,我们准备使用我们的清单文件与 Ansible。让我们制作一个新的 playbook(inventory_test.yaml)来显示主机名和ssh用户名数据:

--- 
- name: test the inventory 
  hosts: all 
  gather_facts: false 

  tasks: 
  - name: hello world 
    ansible.builtin.debug: 
      msg: "Hello world, I am {{ inventory_hostname }}. 
            My username is {{ ansible_ssh_user }}"

在我们可以使用新的清单插件之前,我们还有一件事要做。默认情况下(作为安全功能),大多数 Ansible 的清单插件都是禁用的。为了确保我们的动态清单脚本能够运行,打开适用的ansible.cfg文件编辑器,并在[inventory]部分查找enable_plugins行。至少,它应该看起来像这样(尽管如果您愿意,您可以选择启用更多插件):

[inventory]
enable_plugins = ini, script

要使用我们的新清单插件与这个 playbook,我们可以简单地使用-i参数引用插件文件。因为我们在 playbook 中使用了all主机组,我们还将限制运行到一些组以节省屏幕空间。我们还将计时执行,这在下一节中将变得重要,所以运行以下命令来执行 playbook:

time ansible-playbook -i mastery-inventory.py inventory_test.yaml --limit backend,frontend,errors

这次运行的输出应该如下所示:

图 10.13-运行测试 playbook 针对我们的动态清单脚本

图 10.13-运行测试 playbook 针对我们的动态清单脚本

正如你所看到的,我们得到了我们期望的主机,我们得到了master.example.name的默认ssh用户。backend.example.namescsihost分别显示了它们特定于主机的ssh用户名。

优化脚本性能

使用这个清单脚本,当 Ansible 启动时,它将使用--list一次执行脚本来收集组数据。然后,Ansible 将再次使用--host <hostname>执行脚本,对于第一次调用中发现的每个主机。使用我们的脚本,这需要很少的时间,因为主机很少,我们的执行非常快。然而,在具有大量主机或需要较长时间运行的插件的环境中,收集清单数据可能是一个耗时的过程。幸运的是,有一个优化可以在--list调用的返回数据中进行,这将防止 Ansible 为每个主机重新运行脚本。主机特定的数据可以一次性返回到组数据返回中,放在名为_meta的顶级键内,它有一个名为hostvars的子键,其中包含具有主机变量和变量数据本身的所有主机的哈希。当 Ansible 在--list返回中遇到_meta键时,它将跳过--host调用,并假定所有主机特定的数据已经返回,可能节省大量时间!让我们修改我们的清单脚本,将主机变量返回到_meta中,然后在--host选项中创建一个错误条件,以显示--host没有被调用:

  1. 首先,一旦所有的hostvars都使用与之前相同的算法构建起来,我们将在清单字典中添加_meta键,并在参数解析之前:
hostvars['scsihost'] = {'ansible_ssh_user': 'jfreeman'}
agghostvars = dict()
for outergroup in inventory:
    for grouphost in inventory[outergroup].get('hosts', {}):
        agghostvars[grouphost] = allgroupvars.copy()
        for group in inventory:
            if grouphost in inventory[group].get('hosts', {}):
                for childgroup in inventory:
                    if group in inventory[childgroup].get('children', {}):
                        agghostvars[grouphost].update(inventory[childgroup].get('vars', {}))
        for group in inventory:
            if grouphost in inventory[group].get('hosts', {}):
                agghostvars[grouphost].update(inventory[group].get('vars', {}))
        agghostvars[grouphost].update(hostvars.get(grouphost, {}))
inventory['_meta'] = {'hostvars': agghostvars}
parser = argparse.ArgumentParser(description='Simple Inventory')

接下来,我们将改变--host处理以引发异常:

elif args.host:
raise StandardError("You've been a bad person") 
  1. 现在,我们将使用与之前相同的命令重新运行inventory_test.yaml playbook,以确保我们仍然得到正确的数据:图 10.14-运行我们优化的动态清单脚本

图 10.14-运行我们优化的动态清单脚本

  1. 只是为了确保,我们将手动使用--host参数运行清单插件,以显示异常:

图 10.15 - 演示--host 参数在我们新优化的脚本上不起作用

图 10.15 - 演示--host 参数在我们新优化的脚本上不起作用

通过这种优化,我们的简单 playbook,使用我们的清单模块,现在运行速度快了几个百分点,因为清单解析效率提高了。这在这里可能看起来不算什么,但是当扩展到更复杂的清单时,这将是显著的。

为 Ansible 项目做贡献

并非所有修改都需要符合本地站点的要求。Ansible 用户通常会发现可以对项目进行增强的地方,从而使其他人受益。这些增强可以通过集合进行贡献,在 Ansible 3.0 版本之后的新结构中,这很可能是大多数人最合适的途径。在这种情况下,您可以按照第二章中给出的指导,从早期的 Ansible 版本迁移,构建和发布一个集合。但是,如果您开发了下一个杀手级插件或过滤器,应该将其添加到ansible-core项目本身呢?在本节中,我们将看看您可以如何做到这一点。贡献可以是对现有内置模块或核心 Ansible 代码的更新,对文档的更新,新的过滤器或插件,或者仅仅是测试其他社区成员提出的贡献。

贡献提交

Ansible 项目使用 GitHub (github.com)来管理代码存储库、问题和项目的其他方面。Ansible 组织(github.com/ansible)是代码存储库的所在地。主要存储库是ansible存储库(现在包含ansible-core代码),出于传统原因,它位于这里:github.com/ansible/ansible。这是ansible-core代码、内置模块和文档的所在地。这是应该分叉以开发贡献的存储库。

重要提示

Ansible 项目使用名为devel的开发分支,而不是传统的master名称。大多数贡献都针对devel分支或稳定发布分支。

Ansible 存储库

Ansible 存储库的根目录下有几个文件和文件夹。这些文件主要是高级文档文件、代码许可证或持续集成测试平台配置。

其中一些目录值得注意:

  • bin:各种 ansible 核心可执行文件的源代码

  • docs:API 文档、docs.ansible.com网站和主要页面的源代码

  • hacking:用于在 Ansible 源上进行黑客攻击的指南和实用程序

  • lib/ansible:核心 Ansible 源代码

  • test:单元测试和集成测试代码

对 Ansible 的贡献可能会出现在这些关键文件夹中的一个。

执行测试

在 Ansible 接受任何提交之前,更改必须通过测试。这些测试分为三类:单元测试、集成测试和代码风格测试。单元测试涵盖源代码功能的非常狭窄的方面,而集成测试则采用更全面的方法,确保所需的功能发生。代码风格测试检查使用的语法,以及空格和其他风格方面。

在执行任何测试之前,必须准备好与 Ansible 代码检出一起工作的 shell 环境。存在一个 shell 环境文件来设置所需的变量,可以使用以下命令激活:

    $ source ./hacking/env-setup

确保在进行修改之前通过测试可以节省大量的调试时间,因为devel分支是最前沿的,有可能已提交到该分支的代码未能通过所有测试。

单元测试

所有单元测试都位于从test/units开始的目录树中。这些测试应该都是自包含的,不需要访问外部资源。运行测试就像从 Ansible 源代码检出的根目录执行make tests一样简单。这将测试大部分代码库,包括模块代码。

重要提示

执行测试可能需要安装其他软件。在使用 Python 虚拟环境管理 Python 软件安装时,最好创建一个新的venv用于测试 Ansible-一个没有安装 Ansible 的venv

要运行特定的一组测试,可以直接调用pytest(有时作为py.test访问),并提供要测试的目录或特定文件的路径。在 Ubuntu Server 20.04 上,您可以使用以下命令安装此工具:

sudo apt install python3-pytest

假设您已经检出了ansible-core存储库代码,您可以使用以下命令仅运行parsing单元测试。请注意,其中一些测试需要您安装额外的 Python 模块,并且 Ansible 现在默认在 Python 3 下运行,因此您应始终确保安装和使用基于 Python 3 的模块和工具。以下命令可能不足以运行所有测试,但足以运行解析测试,并让您了解为准备运行包含的测试套件需要做的事情的类型:

sudo apt install python3-pytest python3-tz python3-pytest-mock
cd ansible
source ./hacking/env-setup
pytest-3 test/units/parsing

如果一切顺利,输出应如下所示,并显示任何警告和/或错误,以及最后的摘要:

图 10.16 - 使用 Python 3 的 pytest 工具运行 ansible-core 源代码中包含的解析单元测试

图 10.16 - 使用 Python 3 的 pytest 工具运行 ansible-core 源代码中包含的解析单元测试

正如您所看到的,pytest-3实用程序正在运行定义的单元测试,并将报告它发现的任何错误,这将极大地帮助您检查您可能计划提交的任何代码。在前面的截图中一切似乎都很顺利!

集成测试

Ansible 集成测试是旨在验证 playbook 功能的测试。测试也是由 playbooks 执行的,这使得事情有点递归。测试被分为几个主要类别:

  • 非破坏性

  • 破坏性

  • 遗留云

  • Windows

  • 网络

这些测试类别的更详细解释可以在这里找到:docs.ansible.com/ansible/latest/dev_guide/testing_integration.html

重要提示

许多集成测试需要ssh到 localhost 是可用的。请确保ssh正常工作,最好不需要密码提示。远程主机可以通过更改特定集成测试所需的清单文件来使用。例如,如果要运行connection_ssh集成测试,请确保查看test/integration/targets/connection_ssh/test_connection.inventory并根据需要进行更新。您可以自行探索此目录树,并找到可能需要更新的适当清单文件。

与单元测试一样,可以使用位于bin/ansible-testansible-test实用程序来执行单个集成测试。许多集成测试需要外部资源,例如计算机云帐户,再次,您需要探索文档和目录树,以确定您需要配置什么来在您的环境中运行这些测试。test/integration/targets中的每个目录都是可以单独测试的目标。让我们选择一个简单的示例来测试ping目标的 ping 功能。可以使用以下命令完成:

source ./hacking/env-setup
ansible-test integration --python 3.8 ping

请注意,我们已经专门设置了要针对的 Python 环境。这很重要,因为我的 Ubuntu Server 20.04 测试机安装了一些 Python 2.7,并且已经安装和配置了使用 Python 3.8 的 Ansible(也已经存在)。如果ansible-test工具使用 Python 2.7 环境,它可能会发现缺少模块,测试将失败,但这并不是因为我们的代码有错 - 而是因为我们没有正确设置环境。

当您运行ansible-test时,请确保知道您正在使用的 Python 环境,并相应地在命令中设置它。如果要针对另一个 Python 版本进行测试,您需要确保 Ansible 依赖的所有先决 Python 模块(如 Jinja2)都安装在该 Python 环境下。

成功的测试运行应该如下所示:

图 10.17 - 对 Python 3.8 环境运行 Ansible ping 集成测试

图 10.17 - 对 Python 3.8 环境运行 Ansible ping 集成测试

请注意,甚至在这个测试套件中设计了一个旨在失败的测试 - 最终,我们将看到ok=7failed=0,意味着所有测试都通过了。可以通过以下命令执行一组大型的与 POSIX 兼容的非破坏性集成测试,这些测试由持续集成系统在对 Ansible 的建议更改上运行:

ansible-test integration shippable/ --docker fedora32

重要提示

为了确保一致和稳定的测试环境,这些测试在本地 Fedora 32 容器中运行。您需要确保 Docker 在您的测试主机上设置并可访问,以使此命令生效。

代码风格测试

Ansible 测试的第三类是代码风格类别。这些测试检查 Python 文件中使用的语法,确保代码库中的外观统一。强制执行的代码风格由 PEP8 定义,这是 Python 的风格指南。更多信息请参见:docs.ansible.com/ansible/latest/dev_guide/testing/sanity/pep8.html。这种风格是通过pep8健全性测试目标来强制执行的。要运行此测试,您必须为 Python 3 安装了pycodestyle模块。因此,您的命令可能如下所示:从您的 Ansible 源目录的根目录开始。

sudo apt install python3-pycodestyle
source ./hacking/env-setup
ansible-test sanity --test pep8
echo $?

如果没有错误,此目标不会输出任何文本;但是可以验证返回代码。退出代码为0表示没有错误,如下截图所示:

图 10.18 - 成功运行的 pep8 Python 代码风格测试

图 10.18 - 成功运行的 pep8 Python 代码风格测试

重要提示

正如您已经看到的,运行任何 Ansible 测试可能需要额外的 Python 模块 - 安装它们的方法会因系统而异,所需的模块也会因测试而异。这些通常可以通过使用pip3工具或本地操作系统包来安装,就像我们在这里所做的那样。

如果 Python 文件确实存在pep8违规,输出将反映违规 - 例如,我们将故意编辑ansible.builtin.file模块的代码,该模块可以在源代码根目录下的lib/ansible/modules/file.py中找到。我们将故意引入一些错误,比如带有空格的空行,并将一些至关重要的缩进空格替换为制表符,然后像之前一样重新运行测试。我们不需要重新安装 Python 模块或重新设置环境;此测试的输出将准确显示错误的位置,如下截图所示:

图 10.19 - 重新运行带有故意引入文件模块的编码风格错误的 pep8 健全性测试

图 10.19 - 重新运行带有故意引入文件模块的 pep8 健全性测试

pep8错误将指示一个错误代码,可以查找详细的解释和指导,以及位置和文件名,甚至行号和列号,以帮助您快速定位和纠正问题。

发起拉取请求

一旦所有测试都通过了,就可以提交。Ansible 项目使用 GitHub 拉取请求来管理提交。要创建拉取请求,您的更改必须提交并推送到 GitHub。开发者使用他们账户下的 Ansible 存储库的分支来推送建议的更改。

一旦推送,可以使用 GitHub 网站打开拉取请求。这将创建拉取请求,开始持续集成测试,并通知审阅者有一个新的提交。有关 GitHub 拉取请求的更多信息,请访问docs.github.com/en/github/collaborating-with-pull-requests

一旦拉取请求打开,审阅者将对拉取请求进行评论,要么要求更多信息,要么建议更改,要么批准更改。对于新的模块提交,建议您使用集合路线,但如果您希望进一步探索,这里有大量有价值的信息可供开发者使用:docs.ansible.com/ansible/latest/dev_guide/index.html

经过接受的提交将在下一个 Ansible 版本中普遍可用。这结束了我们对向 Ansible 项目贡献代码和对 Ansible 进行扩展的章节的讨论。希望本章能给您一些想法和灵感,让您能够在 Ansible 提供的优秀基础上解决自动化挑战。

摘要

Ansible 是一个很好的工具;然而,有时它并不能提供您所需的所有功能。并非所有功能都适合提交到ansible-core项目,也不可能为自定义专有数据源提供定制集成,因为每种情况都不同。因此,Ansible 内部有许多设施来扩展其功能。通过共享的模块基础代码,创建和使用自定义模块变得非常容易。可以创建许多不同类型的插件,并与 Ansible 一起使用,以各种方式影响操作。除了 Ansible 发布集合提供的清单源之外,仍然可以相对轻松和高效地使用其他清单源。

在本章中,您学习了开发模块并将其包含在 playbooks 中。然后,您了解了通过插件扩展 Ansible,并详细介绍了创建动态清单插件的具体细节。最后,您学会了如何向 Ansible 项目贡献代码,以增强整个社区的代码。总之,您学会了,在所有情况下,都有机制可以在 playbooks 和依赖于增强功能的角色旁边提供模块、插件和清单源,使其无缝分发。这使得几乎可以无限地扩展或定制 Ansible 以满足您的需求,并且如果需要,可以轻松地为更广泛的社区做出贡献。

第十二章基础设施配置中,我们将探讨使用 Ansible 创建要管理的基础设施。

问题

  1. 对于 3.0 之后的 Ansible 版本,您几乎总是会开发一个新模块,并通过以下哪种方式分发?

a) ansible-core项目。

b) 您的集合。

c) 与项目维护者批准的现有集合功能重叠。

d) 一个角色。

e) 只有 b、c,也许 d

  1. 开发自定义模块的最简单方法是用哪种语言编写?

a) Bash

b) Perl

c) Python

d) C++

  1. 从自定义模块提供事实会做什么?

a) 节省您不需要注册输出到变量,然后使用set_fact

b) 使您的代码具有更大的能力。

c) 帮助您调试您的代码。

d) 显示模块的运行方式。

  1. 回调插件允许您做什么?

a) 帮助您调用其他 playbook。

b) 在关键操作点轻松改变 Ansible 的行为,而无需修改ansible-core代码。

c) 提供一种有效的方式来改变代码的状态。

d) 帮助您在运行时回调到您的 playbook。

  1. 要分发插件,您应该把它们放在哪里?

a) 在与它们的功能相关的专门命名的目录中(例如,回调插件将放在callback_plugins/目录中)。

b) 在 Ansible 安装目录中。

c) 在~/.ansible/plugins下。

d) 无论在哪里,只要您在ansible.cfg中指定它们。

  1. 动态清单插件应该用什么语言编写?

a) Python。

b) Bash。

c) C++。

d) 任何语言,只要输出以正确的 JSON 数据结构返回。

  1. 动态清单插件应该解析哪两个命令行参数?

a) --list--hostname

b) --list--host

c) --list-all--hosts

d) --list--server

  1. 动态清单性能可以通过做什么来提高?

a) 当传递--list参数时,在_meta键下返回所有特定于主机的数据。

b) 返回所有特定于主机的数据,无论传递了哪些参数。

c) 缓存脚本运行的输出。

d) 压缩输出数据以减少传输时间。

  1. 如果您希望向ansible-core项目贡献代码,您应该通过以下哪种方法提交它?

a) 对项目提出的一张票,详细说明您的更改

b) 向 Red Hat 提交支持票

c) 一旦您的代码通过了所有包含的测试,就可以通过 GitHub 拉取请求提交。

d) 在 Twitter 上大声抱怨

  1. 哪个实用程序用于启动和控制大部分 Ansible 代码测试?

a) test-runner

b) integration-test

c) Jenkins

d) ansible-test

第三部分:使用 Ansible 进行编排

在本节中,我们将了解 Ansible 在现实世界中协调和管理系统和服务的用途,无论是在本地还是在云中。

本节包括以下章节:

  • 第十一章 通过滚动部署最小化停机时间

  • 第十二章 基础设施供应

  • 第十三章 网络自动化

第十一章:通过滚动部署最小化停机时间

Ansible 非常适合在实时服务环境中升级或部署应用程序的任务。当然,可以采用多种不同的策略来处理应用程序的部署和升级。最佳方法取决于应用程序本身、应用程序运行的基础设施的能力以及与应用程序用户承诺的服务级别协议(SLA)。无论采用何种方法,都必须控制、可预测和可重复地进行应用程序部署或升级,以确保用户在自动部署后体验到稳定的服务。任何人都不希望由其自动化工具的意外行为导致中断;自动化工具应该是可信赖的,而不是额外的风险因素。

尽管有很多选择,但有些部署策略比其他策略更常见,在本章中,我们将介绍一些更常见的部署策略。在这样做的过程中,我们将展示在这些策略中有用的 Ansible 功能。我们还将讨论一些在两种部署策略中都常见的其他部署考虑因素。为了实现这一点,我们将在滚动 Ansible 部署的背景下深入讨论以下主题的细节:

  • 原地升级

  • 扩展和收缩

  • 快速失败

  • 最小化中断

  • 串行单个任务

技术要求

要按照本章中提供的示例,您需要一台运行Ansible 4.3或更新版本的 Linux 机器。几乎任何 Linux 版本都可以使用——对于感兴趣的人,本章中提供的所有代码都是在Ubuntu Server 20.04 长期支持版(LTS)上测试的,除非另有说明,并且在 Ansible 4.3 上进行了测试。本章附带的示例代码可以从 GitHub 上下载:github.com/PacktPublishing/Mastering-Ansible-Fourth-Edition/tree/main/Chapter11

查看以下视频,以查看代码的运行情况:bit.ly/3lZ6Y9W

原地升级

我们将要介绍的第一种部署类型是原地升级。这种部署方式在已经存在的基础设施上进行,以升级现有的应用程序。这种模式是一种传统模式,当创建新基础设施是一项耗时和昂贵的工作时使用。

在这种类型的升级过程中,最小化停机时间的一般设计模式是将应用程序部署在多个主机上,负载平衡器后面。负载平衡器将充当应用程序用户和运行应用程序的服务器之间的网关。应用程序的请求将发送到负载平衡器,根据配置,负载平衡器将决定将请求发送到哪个后端服务器。

要执行使用此模式部署的应用程序的滚动原地升级,将禁用每个服务器(或一小部分服务器)在负载平衡器上,进行升级,然后重新启用以接受新请求。这个过程将重复进行,直到池中的其余服务器都升级完毕。由于只有部分可用的应用程序服务器被下线进行升级,整个应用程序仍然可用于请求。当然,这假设应用程序可以在同时运行的不同版本下表现良好。

让我们创建一个用于升级虚构应用程序的 playbook。我们的虚构应用程序将在foo-app01foo-app08服务器上运行,这些服务器存在于foo-app组中。这些服务器将有一个简单的网站,通过nginx Web 服务器提供,内容来自foo-app Git 存储库,由foo-app.repo变量定义。一个运行haproxy软件的负载均衡器服务器foo-lb将为这些应用服务器提供前端服务。

为了在我们的foo-app服务器子集上操作,我们需要使用serial模式。这种模式改变了 Ansible 执行 play 的方式。默认情况下,Ansible 将按照任务列出的顺序在每个主机上执行 play 的任务。Ansible 在继续执行 play 中的下一个任务之前,会在每个主机上执行 play 的每个任务。如果我们使用默认方法,我们的第一个任务将从负载均衡器中移除每个服务器,这将导致我们的应用程序完全中断。相反,serial模式让我们可以在子集上操作,以便整个应用程序保持可用,即使一些成员处于离线状态。在我们的示例中,我们将使用2的串行计数,以保持大多数应用程序成员在线:

--- 
- name: Upgrade foo-app in place 
  hosts: foo-app 
  serial: 2 

重要提示

Ansible 2.2 引入了serial批处理的概念:一个可以增加每次通过 play 串行处理的主机数量的数字列表。这允许在信心增加时增加处理的主机数量。如果serial关键字提供了一组数字,那么提供的最后一个数字将是任何剩余批次的大小,直到清单中的所有主机都已完成。

现在,我们可以开始创建我们的任务。第一个任务将是从负载均衡器中禁用主机。负载均衡器运行在foo-lb主机上;但是,我们正在操作foo-app主机。因此,我们需要使用delegate_to任务运算符委派任务。该运算符重定向 Ansible 将连接到以执行任务的位置,但它保留了原始主机的所有变量上下文。我们将使用community.general.haproxy模块来禁用当前主机的foo-app后端池。代码如下所示:

  tasks: 
  - name: disable member in balancer 
    community.general.haproxy: 
      backend: foo-app 
      host: "{{ inventory_hostname }}" 
      state: disabled 
    delegate_to: foo-lb 

在主机禁用的情况下,我们现在可以更新foo-app内容。我们将使用ansible.builtin.git模块将所需版本定义为foo-version的内容路径进行更新。我们将为此任务添加一个notify处理程序,以便在内容更新导致更改时重新加载nginx服务器。这种重启可以每次都进行,但我们也将其用作notify的示例用法。您可以在下面的代码片段中查看代码:

  - name: pull stable foo-app 
    ansible.builtin.git: 
      repo: "{{ foo-app.repo }}" 
      dest: /srv/foo-app/ 
      version: "{{ foo-version }}" 
    notify: 
      - reload nginx 

我们的下一步将是重新启用负载均衡器中的主机;但是,如果我们下一步执行该任务,我们会将旧版本放回原位,因为我们的通知处理程序尚未运行。因此,我们需要通过meta: flush_handlers调用提前触发我们的处理程序,你在第十章中学到了这一点,扩展 Ansible。你可以在这里再次看到这一点:

  - meta: flush_handlers 

现在,我们可以重新启用负载均衡器中的主机。我们可以立即启用它,并依赖负载均衡器等待主机健康后再发送请求。但是,由于我们正在使用较少数量的可用主机,我们需要确保所有剩余的主机都是健康的。我们可以利用ansible.builtin.wait_for任务来等待nginx服务再次提供连接。ansible.builtin.wait_for模块将等待端口或文件路径上的条件。在下面的示例中,我们将等待端口80,并且端口应该在其中的条件。如果它已启动(默认情况下),这意味着它正在接受连接:

  - name: ensure healthy service 
    ansible.builtin.wait_for: 
      port: 80 

最后,我们可以重新启用haproxy中的成员。再次,我们将将任务委派给foo-lb,如下面的代码片段所示:

  - name: enable member in balancer 
    community.general.haproxy: 
      backend: foo-app 
      host: "{{ inventory_hostname }}" 
      state: enabled 
    delegate_to: foo-lb 

当然,我们仍然需要定义我们的reload nginx处理程序。我们可以通过运行以下代码来实现这一点:

  handlers: 
  - name: reload nginx 
    ansible.builtin.service: 
      name: nginx 
      state: restarted 

当运行此剧本时,现在将执行我们应用程序的滚动就地升级。当然,并不总是希望进行就地升级 - 总是有可能会影响服务,特别是如果服务遇到意外负载。在下一节中,将探讨一种可以防止这种情况发生的替代策略,即扩张和收缩。

扩张和收缩

扩张和收缩策略是对就地升级策略的一种替代方案。由于自助服务性质的按需基础设施(如云计算或虚拟化池)的流行,这种策略近来变得流行起来。可以从大量可用资源池中按需创建新服务器的能力意味着每次应用程序的部署都可以在全新的系统上进行。这种策略避免了一系列问题,例如长时间运行系统上的积累问题,例如以下问题:

  • 不再由 Ansible 管理的配置文件被遗留下来

  • 后台运行的进程消耗资源

  • 对服务器进行人工手动更改而不更新 Ansible 剧本

每次重新开始也消除了初始部署和升级之间的差异。可以使用相同的代码路径,减少升级应用程序时出现意外的风险。这种类型的安装也可以使回滚变得非常容易,如果新版本的表现不如预期。除此之外,随着新系统被创建来替换旧系统,在升级过程中应用程序不需要降级。

让我们重新使用扩张和收缩策略来重新处理我们之前的升级剧本。我们的模式将是创建新服务器,部署我们的应用程序,验证我们的应用程序,将新服务器添加到负载均衡器,并从负载均衡器中删除旧服务器。让我们从创建新服务器开始。在这个例子中,我们将利用 OpenStack 计算云来启动新实例:

--- 
- name: Create new foo servers 
  hosts: localhost 

  tasks: 
  - name: launch instances
    openstack.cloud.os_server:
      name: foo-appv{{ version }}-{{ item }}
      image: foo-appv{{ version }}
      flavor: 4
      key_name: ansible-prod
      security_groups: foo-app
      auto_floating_ip: false
      state: present
      auth:
        auth_url: https://me.openstack.blueboxgrid.com:5001/v2.0
        username: jlk
        password: FAKEPASSW0RD
        project_name: mastery
    register: launch
    loop: "{{ range(1, 8 + 1, 1)|list }}"

在这个任务中,我们正在循环遍历8的计数,使用在 Ansible 2.5 中引入的新的looprange语法。对于循环的每次迭代,item变量将被一个数字替换。这使我们能够创建基于应用程序版本和循环次数的八个新服务器实例。我们还假设有一个预构建的镜像,这样我们就不需要对实例进行任何进一步的配置。为了在将来的剧本中使用这些服务器,我们需要将它们的详细信息添加到清单中。为了实现这一点,我们将运行结果注册到launch变量中,然后使用它来创建运行时清单条目。代码如下所示:

  - name: add hosts 
    ansible.builtin.add_host: 
      name: "{{ item.openstack.name }}" 
      ansible_ssh_host: "{{ item.openstack.private_v4 }}" 
      groups: new-foo-app 
    loop: launch.results 

此任务将创建具有与我们服务器实例相同名称的新清单项目。为了帮助 Ansible 知道如何连接,我们将ansible_ssh_host设置为云提供商分配给实例的IP地址(假设该地址可以被运行 Ansible 的主机访问)。最后,我们将主机添加到new-foo-app组中。由于我们的launch变量来自一个带有循环的任务,我们需要通过访问results键来迭代该循环的结果。这使我们能够循环遍历每个launch操作以访问特定于该任务的数据。

接下来,我们将在服务器上操作,以确保新服务已经准备好供使用。我们将再次使用ansible.builtin.wait_for,就像之前一样,作为在new-foo-app组上操作的新任务的一部分。代码如下所示:

- name: Ensure new app 
  hosts: new-foo-app 
  tasks: 
    - name: ensure healthy service 
      ansible.builtin.wait_for: 
        port: 80 

一旦它们都准备就绪,我们可以重新配置负载均衡器以利用我们的新服务器。为了简单起见,我们将假设haproxy配置的模板期望new-foo-app组中的主机,并且最终的结果将是一个了解我们的新主机并忘记我们的旧主机的配置。这意味着我们可以在负载均衡器系统本身上简单地调用ansible.builtin.template任务,而不是尝试操纵负载均衡器的运行状态。代码如下所示:

- name: Configure load balancer 
  hosts: foo-lb 
  tasks:
  - name: haproxy config
    ansible.builtin.template:
      dest: /etc/haproxy/haproxy.cfg
      src: templates/etc/haproxy/haproxy.cfg
  - name: reload haproxy
    ansible.builtin.service:
      name: haproxy
      state: reloaded

一旦新的配置文件就位,我们可以重新加载haproxy服务。这将解析新的配置文件并为新的传入连接启动一个新的监听进程。现有的连接最终会关闭,旧进程将终止。所有新的连接将被路由到运行我们新应用程序版本的新服务器。

这个 playbook 可以扩展到退役旧版本的服务器,或者当决定不再需要回滚到旧版本时,该操作可能会在不同的时间发生。

扩展和收缩策略可能涉及更多的任务,甚至为创建一个黄金镜像集而单独创建 playbooks,但是每次发布都为新基础架构带来的好处远远超过了额外的任务或创建后删除的复杂性。

快速失败

在升级应用程序时,可能希望在出现错误的迹象时完全停止部署。具有混合版本的部分升级系统可能根本无法工作,因此在留下失败的系统的同时继续部分基础架构可能会导致重大问题。幸运的是,Ansible 提供了一种机制来决定何时达到致命错误的情况。

默认情况下,当 Ansible 通过 playbook 运行并遇到错误时,它将从 play 主机列表中删除失败的主机,并继续执行任务或 play。当所有 play 的请求主机都失败或所有 play 都已完成时,Ansible 将停止执行。要更改此行为,可以使用一些 play 控件。这些控件是any_errors_fatalmax_fail_percentageforce_handlers,下面将讨论这些控件。

any_errors_fatal 选项

此设置指示 Ansible 将整个操作视为致命错误,并在任何主机遇到错误时立即停止执行。为了演示这一点,我们将编辑我们的mastery-hosts清单,定义一个可以扩展到 10 个新主机的模式,如下面的代码片段所示:

[failtest] 
failer[01:10] 

然后,我们将在这个组上创建一个 play,将any_errors_fatal设置为true。我们还将关闭事实收集,因为这些主机不存在。代码如下所示:

--- 
- name: any errors fatal 
  hosts: failtest 
  gather_facts: false 
  any_errors_fatal: true 

我们希望有一个任务会对其中一个主机失败,但不会对其他主机失败。然后,我们还希望有第二个任务,仅仅是为了演示它不会运行。这是我们需要执行的代码:

  tasks: 
  - name: fail last host
    ansible.builtin.fail:
      msg: "I am last"
    when: inventory_hostname == play_hosts[-1]
  - name: never run
    ansible.builtin.debug:
      msg: "I should never be run"
    when: inventory_hostname == play_hosts[-1]

然后,我们将使用以下命令执行 playbook:

ansible-playbook -i mastery-hosts failtest.yaml

当我们这样做时,我们会看到一个主机失败,但整个 play 将在第一个任务后停止,并且ansible.builtin.debug任务从未尝试,如下面的屏幕截图所示:

图 11.1 - 当清单中的一个主机失败时提前失败整个 playbook

图 11.1 - 当清单中的一个主机失败时提前失败整个 playbook

我们可以看到只有一个主机失败;但是,Ansible 报告了NO MORE HOSTS LEFT(暗示所有主机都失败了),并在进入下一个 play 之前中止了 playbook。

max_fail_percentage 选项

这个设置允许 play 开发人员定义可以失败的主机的百分比,然后整个操作就会中止。在每个任务结束时,Ansible 将进行计算,以确定 play 所针对的主机中达到失败状态的数量,如果该数量大于允许的数量,Ansible 将中止 playbook。这类似于any_errors_fatal;实际上,any_errors_fatal内部只是表示max_fail_percentage参数为0,其中任何失败都被视为致命。让我们编辑上一节的 play,并删除any_errors_fatal,将其替换为设置为20max_fail_percentage参数,如下所示:

--- 
- name: any errors fatal 
  hosts: failtest 
  gather_facts: false 
  max_fail_percentage: 20 

通过进行这种更改并使用与之前相同的命令运行我们的 playbook,我们的 play 应该能够完成两个任务而不会中止,如下面的截图所示:

图 11.2 - 演示我们之前的失败测试 playbook 在失败主机少于 20%的情况下继续进行

图 11.2 - 演示我们之前的失败测试 playbook 在失败主机少于 20%的情况下继续进行

现在,如果我们更改我们第一个任务的条件,以便故意在超过20%的主机上失败,我们将看到 playbook 提前中止:

  - name: fail last host
    ansible.builtin.fail:
      msg: "I am last"
    when: inventory_hostname in play_hosts[0:3]

我们故意设置三个主机失败,这将使我们的失败率超过20%。 max_fail_percentage设置是允许的最大值,因此我们的设置为20将允许十个主机中的两个失败。由于有三个主机失败,我们将在第二个任务被允许执行之前看到致命错误,如下面的截图所示:

图 11.3 - 当百分比超过限制时,演示 max_fail_percentage 操作导致 play 失败

图 11.3 - 当百分比超过限制时,演示 max_fail_percentage 操作导致 play 失败

通过这些参数的组合,我们可以轻松设置和控制一组主机上的快速失败条件,这在 Ansible 部署期间维护环境的完整性方面非常有价值。

强制处理程序

通常,当 Ansible 失败时,它会停止在该主机上执行任何操作。这意味着任何未决的处理程序都不会运行。这可能是不希望的,有一个 play 控制可以强制 Ansible 处理失败的主机的未决处理程序。这个 play 控制是force_handlers,必须设置为true布尔值。

让我们稍微修改上一个示例,以演示这个功能。我们将删除我们的max_fail_percentage参数,并添加一个新的第一个任务。我们需要创建一个任务,它将返回成功的更改。这可以通过ansible.builtin.debug模块实现,使用changed_when任务控制,因为这个模块否则永远不会注册更改。我们将将我们的ansible.builtin.fail任务条件恢复到原始状态。代码如下所示:

--- 
- name: any errors fatal 
  hosts: failtest 
  gather_facts: false 
  tasks:
  - name: run first
    ansible.builtin.debug:
      msg: "I am a change"
    changed_when: true
    when: inventory_hostname == play_hosts[-1]
    notify: critical handler
  - name: change a host
    ansible.builtin.fail:
      msg: "I am last"
    when: inventory_hostname == play_hosts[-1] 

我们的第三个任务保持不变,但我们将定义我们的关键处理程序,如下所示:

  - name: never run
    ansible.builtin.debug:
      msg: "I should never be run"
    when: inventory_hostname == play_hosts[-1]
  handlers:
    - name: critical handler
      ansible.builtin.debug:
        msg: "I really need to run"

让我们运行这个新的 play 来展示处理程序不被执行的默认行为。为了减少输出,我们将限制执行到其中一个主机,使用以下命令:

ansible-playbook -i mastery-hosts failtest.yaml --limit failer01:failer01

请注意,尽管处理程序在 play 输出中被引用,但实际上并没有运行,这可以从缺少任何调试消息来证明,如下面的截图清楚地显示:

图 11.4 - 即使在 play 失败时也不运行处理程序的演示

图 11.4 - 演示即使在 play 失败时也不运行处理程序的情况

现在,我们添加force_handlers play 控制并将其设置为true,如下所示:

---
- name: any errors fatal
  hosts: failtest
  gather_facts: false
  force_handlers: true

这次,当我们运行 playbook(使用与之前相同的命令)时,我们应该看到即使对于失败的主机,处理程序也会运行,如下面的截图所示:

图 11.5-演示处理程序可以被强制运行,即使在失败的 play 中也是如此

图 11.5-演示处理程序可以被强制运行,即使在失败的 play 中也是如此

重要提示

强制处理程序也可以是一个运行时决定,可以在ansible-playbook上使用--force-handlers命令行参数。它也可以作为ansible.cfg中的参数进行全局设置。

强制处理程序运行对于重复的 playbook 运行非常有用。第一次运行可能会导致一些更改,但如果在刷新处理程序之前遇到致命错误,那些处理程序调用将丢失。重复运行不会导致相同的更改,因此处理程序将永远不会在没有手动交互的情况下运行。强制处理程序可以确保这些处理程序调用不会丢失,因此无论任务结果如何,处理程序始终会运行。当然,任何升级策略的整体目标是尽可能降低对任何给定服务的影响-您能想象您最喜欢的零售网站因为有人升级软件而宕机吗?在当今这个时代是不可想象的!在下一节中,我们将探讨使用 Ansible 来最小化潜在的破坏性行为的方法。

最小化中断

在部署过程中,通常有一些可以被视为具有破坏性或破坏性的任务。这些任务可能包括重新启动服务,执行数据库迁移等。破坏性任务应该被集中在一起,以最小化对应用程序的整体影响,而破坏性任务应该只执行一次。接下来的两个小节将探讨如何使用 Ansible 来实现这两个目标。

延迟中断

重新启动服务以适应新的配置或代码版本是一个非常常见的需求。当单独查看时,只要应用程序的代码和配置发生了变化,就可以重新启动单个服务,而不必担心整个分布式系统的健康状况。通常,分布式系统将为系统的每个部分分配角色,每个角色将在目标主机上独立运行。首次部署应用程序时,无需担心整个系统的运行时间,因此可以随意重新启动服务。然而,在升级过程中,可能希望延迟所有服务的重新启动,直到每个服务都准备就绪,以最小化中断。

强烈鼓励重用角色代码,而不是设计完全独立的升级代码路径。为了适应协调的重启,特定服务的角色代码需要在服务重新启动周围进行保护。一个常见的模式是在破坏性任务上放置一个条件语句,检查变量的值。在执行升级时,可以在运行时定义变量以触发这种替代行为。这个变量也可以在主 playbook 完成所有角色后触发协调的服务重启,以便对中断进行集群化处理并最小化总的中断时间。

让我们创建一个虚构的应用程序升级,其中涉及两个角色,模拟服务的重新启动。我们将这些角色称为microAmicroB。代码如下所示:

roles/microA 
├── handlers 
│   └── main.yaml 
└── tasks 
    └── main.yaml 
roles/microB 
├── handlers 
│   └── main.yaml 
└── tasks 
    └── main.yaml 

对于这两个角色,我们将有一个简单的调试任务,模拟安装软件包。我们将通知一个处理程序来模拟服务的重新启动,并确保处理程序将触发,我们将强制任务始终注册为更改。以下代码片段显示了roles/microA/tasks/main.yaml的内容:

--- 
- name: install microA package 
  ansible.builtin.debug: 
    msg: "This is installing A" 
  changed_when: true 
  notify: restart microA 

roles/microB/tasks/main.yaml的内容如下所示:

---
- name: install microB package
  ansible.builtin.debug:
    msg: "This is installing B"
  changed_when: true
  notify: restart microB

这些角色的处理程序也将是调试操作,并且我们将附加一个条件语句到处理程序任务,只有当升级变量评估为false布尔值时才重新启动。我们还将使用默认过滤器为这个变量赋予默认值falseroles/microA/handlers/main.yaml的内容如下所示:

--- 
- name: restart microA 
  ansible.builtin.debug: 
    msg: "microA is restarting" 
  when: not upgrade | default(false) | bool 

roles/microB/handlers/main.yaml的内容如下所示:

---
- name: restart microB
  ansible.builtin.debug:
    msg: "microB is restarting"
  when: not upgrade | default(false) | bool

对于我们的顶层 playbook,我们将创建四个 play(记住 playbook 可以由一个或多个 play 组成)。前两个 play 将应用每个微服务角色,最后两个 play 将进行重新启动。只有在执行升级时,最后两个 play 才会被执行;因此,它们将使用upgrade变量作为条件。让我们看一下以下代码片段(名为micro.yaml):

---
- name: apply microA
  hosts: localhost
  gather_facts: false
  roles:
  - role: microA
- name: apply microB
  hosts: localhost
  gather_facts: false
  roles:
  - role: microB
- name: restart microA
  hosts: localhost
  gather_facts: false
  tasks:
  - name: restart microA for upgrade
    ansible.builtin.debug:
      msg: "microA is restarting"
    when: upgrade | default(false) | bool
- name: restart microB
  hosts: localhost
  gather_facts: false
  tasks:
  - name: restart microB for upgrade
    ansible.builtin.debug:
      msg: "microB is restarting"
    when: upgrade | default(false) | bool

我们在不定义upgrade变量的情况下执行这个 playbook,使用以下命令:

ansible-playbook -i mastery-hosts micro.yaml

当我们这样做时,我们将看到每个角色的执行,以及其中的处理程序。最后两个 play 将有跳过的任务,如下截图所示:

图 11.6 - 演示了安装微服务架构的基于角色的 playbook

图 11.6 - 演示了安装微服务架构的基于角色的 playbook

现在,让我们再次执行 playbook;这次,我们将在运行时将upgrade变量定义为true,使用-e标志如下:

ansible-playbook -i mastery-hosts micro.yaml -e upgrade=true

这次,结果应该是这样的:

图 11.7 - 演示相同的 playbook,但在升级场景中所有重新启动都在最后批处理

图 11.7 - 演示相同的 playbook,但在升级场景中所有重新启动都集中在最后

这次,我们可以看到我们的处理程序被跳过,但最后两个 play 有执行的任务。在一个真实的场景中,在microAmicroB角色中发生了更多的事情(可能还有其他主机上的其他微服务角色),这种差异可能会达到几分钟甚至更长。将重新启动集中在最后可以显著减少中断时间。

仅运行破坏性任务一次

破坏性任务有很多种。它们可以是极其难以回滚的单向任务,无法轻易重新运行的一次性任务,或者如果并行执行会导致灾难性失败的竞争条件任务。因此,非常重要的是这些任务只能从单个主机执行一次。Ansible 通过run_once任务控制提供了一种实现这一点的机制。

run_once任务控制将确保任务只从单个主机执行一次,而不管 play 中有多少个主机。虽然还有其他方法可以实现这个目标,比如使用条件语句使任务只在 play 的第一个主机上执行,但run_once控制是表达这个愿望最简单直接的方式。此外,从run_once控制的任务注册的任何变量数据将对 play 的所有主机可用,而不仅仅是由 Ansible 选择执行操作的主机。这可以简化后续变量数据的检索。

让我们创建一个示例 playbook 来演示这个功能。我们将重用之前创建的failtest主机,以便有一个主机池,然后我们将通过主机模式选择其中的两个。我们将创建一个设置为run_onceansible.builtin.debug任务并注册结果,然后我们将在不同的任务中使用不同的主机访问结果。代码如下:

--- 
- name: run once test 
  hosts: failtest[0:1] 
  gather_facts: false 

  tasks: 
  - name: do a thing
    ansible.builtin.debug:
      msg: "I am groot"
    register: groot
    run_once: true
  - name: what is groot
    ansible.builtin.debug:
      var: groot
    when: inventory_hostname == play_hosts[-1]

我们使用以下命令运行这个 play:

ansible-playbook -i mastery-hosts runonce.yaml

当我们这样做时,我们将特别关注每个任务操作中列出的主机名,如下截图所示:

图 11.8 - 演示了 run_once 任务参数的使用,以及该任务在播放中其他主机上的变量数据的可用性

图 11.8 - 演示了使用 run_once 任务参数以及在剧本中的其他主机上可用的变量数据的使用

我们可以看到do a thing任务在failer01主机上执行,而检查来自do a thing任务的数据的what is groot任务在failer02主机上操作。当然,通过使用我们在这里讨论的技术,您可以减少对生产服务的干扰风险,还有更多的事情可以做,比如限制任务运行的次数或运行的主机数量。我们将在本章的下一节中探讨这个话题。

序列化单个任务

运行多个服务副本的某些应用程序可能对所有这些服务同时重新启动作出不良反应。通常,在升级此类应用程序时,会使用serial剧本。但是,如果应用程序规模足够大,序列化整个剧本可能会非常低效。可以使用不同的方法,即仅对敏感任务(通常是重新启动服务的处理程序)进行序列化。

要对特定的处理程序任务进行序列化,我们可以使用内置变量play_hosts。该变量保存应作为剧本的一部分用于给定任务的主机列表。它会随着失败或不可达的主机而更新。使用此变量,我们可以构建一个循环,以便遍历每个可能运行处理程序任务的主机。我们将使用when条件和delegate_to指令中的item值,而不是在模块参数中使用item值。通过这种方式,剧本中通知的处理程序任务可以被委派到上述循环中的主机,而不是原始主机。但是,如果我们将其作为loop指令的列表使用,我们将会为触发处理程序的每个主机执行任务。这显然是不希望的,因此我们可以使用任务指令run_once来改变行为。run_once指令指示 Ansible 仅为一个主机执行任务,而不是通常会目标的每个主机。结合run_once和我们的play_hosts循环,就会创建一种情况,即 Ansible 只会通过循环运行一次。最后,我们希望在每个循环之间等待一小段时间,以便重新启动的服务在我们重新启动下一个服务之前可以正常运行。我们可以使用一个名为pauseloop_control参数(在 Ansible 版本 2.2 中引入)在循环的每次迭代之间插入暂停。

为了演示这种序列化的工作原理,我们将编写一个使用我们failtest组中的一些主机的剧本,其中包含一个创建更改并注册输出的任务,以便我们可以在我们通知的处理程序任务中检查此输出,称为restart groot。然后我们在剧本底部创建一个序列化的处理程序任务本身。代码如下所示:

--- 
- name: parallel and serial 
  hosts: failtest[0:3] 
  gather_facts: false 

  tasks: 
  - name: do a thing
    ansible.builtin.debug:
      msg: "I am groot"
    changed_when: inventory_hostname in play_hosts[0:2]
    register: groot
    notify: restart groot
  handlers:
  - name: restart groot
    debug:
      msg: "I am groot?"
    loop: "{{ play_hosts }}"
    delegate_to: "{{ item }}"
    run_once: true
    when: hostvars[item]['groot']['changed'] | bool
    loop_control:
      pause: 2

在执行此剧本时,我们可以看到处理程序通知(通过使用以下命令进行双重详细度):

ansible-playbook -i mastery-hosts forserial.yaml -vv

在处理程序任务中,我们可以看到循环、条件和委托,如下面的屏幕截图所示:

图 11.9 - 一个带有序列化处理程序路由的剧本,用于重新启动服务

图 11.9 - 一个带有序列化处理程序路由的剧本,用于重新启动服务

如果您自己尝试了这段代码,您会注意到每个处理程序运行之间的延迟,就像我们在任务的loop_control部分中指定的那样。使用这些技术,您可以自信地推出更新和升级到您的环境,同时将干扰降到最低。希望本章为您提供了在您的环境中自信地执行此类操作的工具和技术。

总结

部署和升级策略是一种品味。每种策略都有明显的优势和劣势。Ansible 不会对哪种更好发表意见,因此它非常适合执行部署和升级,无论采用哪种策略。Ansible 提供了功能和设计模式,可以轻松地促进各种风格。了解每种策略的性质以及如何调整 Ansible 以适应该策略将使你能够决定并设计每个应用的部署。任务控制和内置变量提供了有效升级大规模应用程序的方法,同时小心处理特定任务。

在本章中,你学会了如何使用 Ansible 进行就地升级以及一些不同的方法论,包括扩展和收缩环境等技术。你了解了快速失败以确保 playbook 在 play 的早期出现问题时不会造成严重损害,以及如何最小化破坏性和破坏性行为。最后,你学会了对单个任务进行串行化,以最小化对正在运行的服务的干扰,通过以最小受控的方式将节点脱离服务来确保服务在维护工作(如升级)进行时仍然保持运行。这确保了服务在维护工作(如升级)进行时仍然保持运行。

在下一章中,我们将详细介绍如何使用 Ansible 与云基础设施提供商和容器系统合作,以创建一个用于管理的基础设施。

问题

  1. 在进行就地升级时,最小化干扰的有效策略是什么?

a) 使用serial模式来改变 Ansible 一次执行升级的主机数量。

b) 使用limit参数来改变 Ansible 一次执行升级的主机数量。

c) 拥有许多小清单,每个清单中只有少量主机。

d) 撤销 Ansible 对主机的访问权限。

  1. 扩展和收缩作为升级策略的一个关键好处是什么?

a) 减少云操作成本。

b) 它与开发运维(DevOps)文化相契合。

c) 每次应用部署或升级都会为所有主机新建,减少了过期库和配置的可能性。

d) 它为升级的方法提供了灵活性。

  1. 为什么你想要快速失败?

a) 这样你就可以尽快了解你的 playbook 错误。

b) 这样你就可以最小化失败 play 造成的损害或中断。

c) 这样你就可以调试你的代码。

d) 这样你就可以在部署中灵活应对。

  1. 你会使用哪个 Ansible play 选项来确保你的 play 在任何单个主机出现错误时提前停止执行?

a) ansible.builtin.fail

b) any_errors_fatal

c) when: failed

d) max_fail_percentage: 50

  1. 你会使用哪个 Ansible play 选项来确保在清单中超过 30%的主机出现错误时,你的 play 会提前停止执行?

a) any_errors_fatal

b) max_fail_percentage: 30%

c) max_fail_percentage: 30

d) max_fail: 30%

  1. 你可以指定哪个 play 级选项来确保即使 play 失败,也会运行 handlers?

a) handlers_on_fail

b) handlers_on_failure

c) always_handlers

d) force_handlers

  1. 为什么你可能希望延迟运行 handlers 到 play 的最后?

a) 这可能会节省 play 执行的时间。

b) 它使操作更可预测。

c) 它减少了停机的风险。

d) 这可能有助于增加升级成功的机会。

  1. 你可以使用哪个任务级参数来确保任务不会在清单中有多个主机时执行多次?

a) task_once

b) run_once

c) limit: 1

d) run: once

  1. 哪个loop_control参数可以在 Ansible 的循环迭代之间插入延迟?

a) pause

b) sleep

c) delay

d) wait_for

  1. 你可以使用哪个任务条件来确保只在清单中的前四个主机上运行任务?

a) when: inventory_hostname in play_hosts[0:3]

b) when: inventory_hostname in play_hosts[1:4]

c) when: inventory_hostname[0:3]

d) when: play_hosts[0:3]

第十二章:基础设施供应

数据中心中的几乎所有内容都变成了软件定义,从网络到我们的软件运行的服务器基础设施。基础设施即服务IaaS)提供商提供 API,用于以编程方式管理镜像、服务器、网络和存储组件。通常期望这些资源是即时创建的,以降低成本并提高效率。

因此,多年来,Ansible 在云供应方面投入了大量的工作,官方发布的 Ansible 版本中支持了 30 多个基础设施提供商。这些范围从 OpenStack 和 oVirt 等开源解决方案到专有提供商如 VMware 和云提供商如 AWS、GCP 和 Azure。

本章涵盖的用例比我们能够覆盖的要多,但尽管如此,我们将探讨 Ansible 与各种这些服务进行交互的方式:

  • 管理本地云基础设施

  • 管理公共云基础设施

  • 与 Docker 容器交互

  • 使用 Ansible 构建容器

技术要求

要跟随本章中提供的示例,您需要一台运行Ansible 4.3或更新版本的 Linux 机器。几乎任何 Linux 版本都可以 - 对于那些对具体细节感兴趣的人,本章中提供的所有代码都是在 Ubuntu Server 20.04 LTS 上测试的,除非另有说明,并且在 Ansible 4.3 上测试。本章附带的示例代码可以从 GitHub 的以下网址下载:github.com/PacktPublishing/Mastering-Ansible-Fourth-Edition/tree/main/Chapter12

观看以下视频以查看代码的实际操作:bit.ly/3BU6My2

管理本地云基础设施

云是一个常见但模糊的术语,用于描述 IaaS。云可以提供许多类型的资源,尽管最常讨论的是计算和存储。Ansible 能够与许多云提供商进行交互,以便在其中发现、创建或管理资源。请注意,尽管本章将专注于计算和存储资源,但 Ansible 还有一个模块用于与许多其他云资源类型进行交互,例如负载均衡器,甚至云角色访问控制。

Ansible 可以与之交互的一个这样的云提供商是 OpenStack(一个开源的云操作系统),对于那些需要本地 IaaS 功能的人来说,这是一个可能的解决方案。一套服务提供了管理计算、存储和网络服务以及许多其他支持服务的接口。OpenStack 并不是一个单一的提供商;相反,许多公共和私有云提供商使用 OpenStack 构建其产品,因此尽管提供商本身可能是分散的,它们提供相同的 API 和软件接口,以便 Ansible 可以轻松地在这些环境中自动化任务。

Ansible 自项目早期就支持 OpenStack 服务,现在这种支持可以在OpenStack.Cloud集合中找到。最初的支持已经发展到包括 70 多个模块,支持管理以下内容:

  • 计算

  • 裸金属计算

  • 计算镜像

  • 认证账户

  • 网络

  • 对象存储

  • 块存储

除了在前面的资源类型上执行创建、读取、更新和删除(CRUD)操作之外,Ansible 还包括使用 OpenStack(和其他云)作为清单来源的能力,我们之前在第一章中已经提到过这一点,Ansible 的系统架构和设计。再次强调,动态清单提供程序可能在OpenStack.Cloud集合中找到。每次使用 OpenStack 云作为清单来源的ansibleansible-playbook执行都将获取关于现有计算资源的即时信息,以及有关这些计算资源的各种事实。由于云服务已经跟踪了这些细节,这可以通过消除资源的手动跟踪来减少开销。

为了展示 Ansible 管理和与云资源交互的能力,我们将演示两种情景:一个是创建并与新的计算资源交互的情景,另一个是演示使用 OpenStack 作为清单来源的情景。

创建服务器

OpenStack 计算服务提供了一个 API,用于创建、读取、更新和删除虚拟机服务器。通过这个 API,我们将能够为我们的演示创建服务器。在通过 SSH 访问和修改服务器之后,我们还将使用 API 来删除服务器。这种自助服务能力是云计算的一个关键特性。

Ansible 可以使用各种openstack.cloud模块来管理这些服务器:

  • openstack.cloud.server:此模块用于创建和删除虚拟服务器。

  • openstack.cloud.server_info:此模块用于收集有关服务器的信息-在 Ansible 2.9 及更早版本中,它将这些信息返回为事实,但现在不再是这样。

  • openstack.cloud.server_action:此模块用于对服务器执行各种操作。

  • openstack.cloud.server_group:此模块用于创建和删除服务器组。

  • openstack.cloud.server_volume:此模块用于将块存储卷附加到服务器或从服务器分离。

  • openstack.cloud.server_metadata:此模块用于创建、更新和删除虚拟服务器的元数据。

启动虚拟服务器

对于我们的演示,我们将使用openstack.cloud.server。我们需要提供关于我们的云的身份验证详细信息,如认证 URL 和登录凭据。除此之外,我们还需要为我们的 Ansible 主机设置正确的先决条件软件,以使此模块正常运行。正如我们在本书早期讨论动态清单时所讨论的,Ansible 有时需要主机上的额外软件或库才能正常运行。事实上,Ansible 开发人员的政策是不将云库与 Ansible 本身一起发布,因为它们会迅速过时,并且不同的操作系统需要不同的版本-即使是集合的出现也没有改变这一点。

您可以在每个模块的 Ansible 文档中找到软件依赖关系,因此在第一次使用模块时(特别是云提供商模块)值得检查这一点。本书中用于演示的 Ansible 主机基于 Ubuntu Server 20.04,为了使openstack.cloud.server模块正常运行,我首先必须运行以下命令:

sudo apt install python3-openstacksdk

确切的软件和版本将取决于我们的主机操作系统,并可能随着较新的 Ansible 版本而改变。您的操作系统可能有本机软件包可用,或者您可以使用pip安装这个 Python 模块。在继续之前,值得花几分钟时间检查您的操作系统的最佳方法。

一旦先决条件模块就位,我们就可以继续创建服务器。为此,我们将需要一个 flavor,一个 image,一个 network 和一个名称。您还需要一个密钥,在继续之前需要在 OpenStack GUI(或 CLI)中定义。当然,这些细节可能对每个 OpenStack 云都不同。在这个演示中,我正在使用基于DevStack的单个一体化虚拟机,并尽可能使用默认设置,以便更容易跟进。您可以在这里下载 DevStack 并了解快速入门:docs.openstack.org/devstack/latest/

我将命名我们的剧本为boot-server.yaml。我们的剧本以一个名称开始,并使用localhost作为主机模式,因为我们调用的模块直接从本地 Ansible 机器与 OpenStack API 交互。由于我们不依赖于任何本地事实,我也会关闭事实收集:

--- 
- name: boot server 
  hosts: localhost 
  gather_facts: false 

为了创建服务器,我将使用openstack.cloud.server模块,并提供与我可以访问的 OpenStack 云相关的auth详细信息,以及一个 flavor,image,network 和 name。请注意key_name,它指示了在编写此剧本之前您在 OpenStack 中为自己创建的密钥对的 SSH 公钥(如本章前面讨论的)。这个 SSH 公钥被集成到我们在 OpenStack 上首次引导时使用的Fedora34镜像中,以便我们随后可以通过 SSH 访问它。我还上传了一个Fedora34镜像,以便在本章中进行演示,因为它比 OpenStack 发行版中包含的默认 Cirros 镜像具有更大的操纵空间。这些镜像可以从alt.fedoraproject.org/cloud/免费下载。最后,正如您所期望的,我已经对我的密码进行了混淆:

  tasks:
    - name: boot the server
      openstack.cloud.server:
        auth:
          auth_url: "http://10.0.50.32/identity/v3"
          username: "demo"
          password: "password"
          project_name: "demo"
          project_domain_name: "default"
          user_domain_name: "default"
        flavor: "ds1G"
        image: "Fedora34"
        key_name: "mastery-key"
        network: "private"
        name: "mastery1"

重要提示

认证详细信息可以写入一个外部文件,该文件将被底层模块代码读取。这个模块代码使用openstacksdk,这是一个用于管理 OpenStack 凭据的标准库。或者,它们可以存储在 Ansible 保险库中,正如我们在第三章中描述的那样,用 Ansible 保护您的秘密,然后作为变量传递给模块。

按原样运行这个剧本将只是创建服务器,没有别的。要测试这一点(假设您可以访问合适的 OpenStack 环境),请使用以下命令运行剧本:

export ANSIBLE_PYTHON_INTERPRETER=$(which python3)
ansible-playbook -i mastery-hosts boot-server.yaml -vv

确保使用正确的 Python 环境

请注意,在 Ubuntu Server 20.04 上,默认情况下,Ansible 在 Python 2.7 下运行 - 这不是问题,我们在本书中到目前为止已经忽略了这一点 - 但是,在这种特殊情况下,我们只在 Python 3 上安装了openstacksdk模块,因此我们必须告诉 Ansible 使用 Python 3 环境。我们通过设置一个环境变量来做到这一点,但您也可以通过ansible.cfg文件轻松地完成这一点 - 这留给您去探索。

成功运行剧本应该产生类似于图 12.1所示的输出:

图 12.1 - 使用 Ansible 在 OpenStack 中创建虚拟实例

图 12.1 - 使用 Ansible 在 OpenStack 中创建虚拟实例

我已经截断了输出,因为模块返回了大量数据。最重要的是,我们获得了有关主机 IP 地址的数据。这个特定的云使用浮动 IP 来提供对服务器实例的公共访问,我们可以通过注册输出然后调试打印openstack.accessIPv4的值来看到这个值:

  tasks:
    - name: boot the server
      openstack.cloud.server:
        auth:
          auth_url: "http://10.0.50.32/identity/v3"
          username: "demo"
          password: "password"
          project_name: "demo"
          project_domain_name: "default"
          user_domain_name: "default"
        flavor: "ds1G"
        image: "Fedora34"
        key_name: "mastery-key"
        network: "private"
        name: "mastery1"
      register: newserver
    - name: show floating ip
      ansible.buitin.debug:
        var: newserver.openstack.accessIPv4

使用类似于前面的命令执行此剧本(但不要添加冗长):

export ANSIBLE_PYTHON_INTERPRETER=$(which python3)
ansible-playbook -i mastery-hosts boot-server.yaml

这一次,第一个任务不会导致更改,因为我们想要的服务器已经存在 - 但是,它仍然会检索有关服务器的信息,使我们能够发现其 IP 地址:

图 12.2 - 使用 Ansible 检索我们在上一个示例中启动的 OpenStack 虚拟机的 IP 地址

图 12.2 - 使用 Ansible 检索我们在上一个示例中启动的 OpenStack 虚拟机的 IP 地址

输出显示 IP 地址为172.24.4.81。我可以使用这些信息连接到我新创建的云服务器。

添加到运行时清单

启动服务器本身并不是很有用。服务器存在是为了使用,并且可能需要一些配置才能变得有用。虽然可以有一个 playbook 来创建资源,另一个完全不同的 playbook 来管理配置,但我们也可以在同一个 playbook 中完成所有这些。Ansible 提供了一个功能,可以在 play 的一部分中将主机添加到清单中,这将允许在后续 play 中使用这些主机。

根据上一个示例,我们有足够的信息通过ansible.builtin.add_host模块将新主机添加到运行时清单:

    - name: add new server
      ansible.builtin.add_host:
        name: "mastery1"
        ansible_ssh_host: "{{ newserver.openstack.accessIPv4 }}"
        ansible_ssh_user: "fedora" 

我知道这个镜像有一个默认的用户fedora,所以我相应地设置了一个主机变量,并设置 IP 地址作为连接地址。

重要提示

这个例子也忽略了在 OpenStack 中所需的安全组配置,以及接受 SSH 主机密钥。可以添加其他任务来管理这些事情,或者您可以像我在我的环境中所做的那样预先配置它们。

将服务器添加到清单后,我们可以对其进行操作。假设我们想要使用这个云资源来转换图像文件,使用ImageMagick软件。为了实现这一点,我们需要一个新的 play 来利用新的主机。我知道这个特定的 Fedora 镜像不包含 Python,所以我们需要添加 Python 和dnf的 Python 绑定(这样我们就可以使用ansible.builtin.dnf模块)作为我们的第一个任务,使用ansible.builtin.raw模块:

- name: configure server 
  hosts: mastery1 
  gather_facts: false 

  tasks: 
    - name: install python 
      ansible.builtin.raw: "sudo dnf install -y python python-dnf" 

接下来,我们需要ImageMagick软件,我们可以使用dnf模块安装它:

    - name: install imagemagick 
      ansible.builtin.dnf: 
        name: "ImageMagick" 
      become: "yes" 

此时运行 playbook 将显示我们新主机的更改任务;请注意,这一次,我们必须给ansible-playbook提供来自 OpenStack 的私钥文件的位置,以便它可以使用以下命令对Fedora镜像进行身份验证:

export ANSIBLE_PYTHON_INTERPRETER=$(which python3)
ansible-playbook -i mastery-hosts boot-server.yaml --private-key=mastery-key

成功运行 playbook 应该产生像图 12.3中显示的输出:

图 12.3 - 在我们的 OpenStack 虚拟机上执行实例化后配置使用 Ansible 的机器

图 12.3 - 在我们的 OpenStack 虚拟机上执行实例化后配置,使用 Ansible

我们可以看到 Ansible 在主机mastery1上报告了两个更改的任务,这是我们在第一个 play 中刚刚创建的。这个主机在mastery-hosts清单文件中不存在。

这里我们也关闭了冗长的报告,因为输出会很繁琐;但是,鉴于我们有 OpenStack 实例的私钥文件,我们可以手动登录并检查我们 playbook 的结果,例如,使用以下命令:

rpm -qa --last | head

这个命令查询 RPM 软件包数据库,并显示最近安装的软件包的简短列表。输出可能看起来像图 12.4中显示的那样,尽管日期肯定会有所不同:

图 12.4 - 检查我们在 OpenStack VM 上的 playbook 成功

图 12.4 - 检查我们在 OpenStack VM 上的 playbook 成功

从这里开始,我们可以扩展我们的第二个 play,通过使用ansible.builtin.copy上传源图像文件,然后通过在主机上使用ImageMagick执行命令来转换图像。可以添加另一个任务,通过使用ansible.builtin.slurp模块将转换后的文件下载回来,或者将修改后的文件上传到基于云的对象存储中。最后,可以添加最后一个 play 来删除服务器本身。

服务器的整个生命周期,从创建到配置再到使用,最后到移除,都可以通过一个单一的 playbook 来管理。通过读取运行时变量数据,playbook 可以变得动态,以定义应上传/修改哪个文件以及应存储在何处,从而将 playbook 转变为可重复使用的程序。虽然有些简单,但希望这能让您清楚地了解 Ansible 在与基础设施服务提供商合作时有多强大。

使用 OpenStack 清单源

我们之前的示例展示了一个一次性的短暂的云服务器。如果我们想要创建和使用长期的云服务器呢?每次想要操作它们时,都要手动记录创建它们并将它们添加到临时清单的任务似乎效率低下。在静态清单中手动记录服务器详细信息似乎也效率低下,而且容易出错。幸运的是,有一个更好的方法:使用云本身作为动态清单源。

Ansible 附带了许多云提供商的动态清单脚本,正如我们在[第一章](B17462_01_Final_JC_ePub.xhtml#_idTextAnchor015)中讨论的那样,Ansible 的系统架构和设计。我们将在这里继续使用 OpenStack 的示例。回顾一下,openstack.cloud集合提供了我们需要的动态清单脚本。要使用此脚本,我们需要创建一个 YAML 文件,告诉 Ansible 使用此清单脚本 - 此文件必须命名为openstack.yamlopenstack.yml。它应该包含类似以下的代码:

# file must be named openstack.yaml or openstack.yml
plugin: openstack.cloud.openstack
expand_hostvars: yes
fail_on_errors: yes
all_projects: yes

配置文件需要更多考虑。该文件保存了连接到 OpenStack 云的身份验证详细信息。这使得该文件非常敏感,只应对需要访问这些信息的用户可见。此外,清单脚本将尝试从os-client-confighttps://docs.openstack.org/os-client-config/latest/user/configuration.html#config-files)使用的标准路径加载配置,这是底层身份验证代码。这意味着此清单源的配置可以存在于以下位置:

  • clouds.yaml(在执行清单脚本时的当前工作目录)

  • ~/.config/openstack/clouds.yaml

  • /etc/openstack/clouds.yaml

找到的第一个文件将被使用。您可以通过在我们之前在本节中创建的openstack.yaml中添加clouds_yaml_path来覆盖此设置。在我们的示例中,我将在 playbook 目录中与脚本本身一起使用clouds.yaml文件,以便将配置与任何其他路径隔离开来。

您的clouds.yaml文件将与我们在之前示例中使用的openstack.cloud.server模块的参数的auth:部分非常相似。但有一个关键的区别 - 在我们之前的示例中,我们使用了demo账户,并且限制了自己只能在 OpenStack 的demo项目中。为了查询所有项目中的所有实例(我们想要演示一些功能),我们需要一个具有管理员权限而不是demo账户的账户。在本章的这部分中,我的clouds.yaml文件包含以下内容:

clouds:
  mastery_cloud:
    auth:
      auth_url: "http://10.0.50.32/identity/v3"
      username: "admin"
      password: "password"
      project_name: "demo"
      project_domain_name: "default"
      user_domain_name: "default"

实际的动态清单脚本有一个内置的帮助功能,您也可以使用它来了解更多信息。如果您可以在系统上找到它,您可以运行以下命令 - 在我的系统上,我使用了这个命令:

python3 /usr/local/lib/python3.8/dist-packages/ansible_collections/openstack/cloud/scripts/inventory/openstack_inventory.py --help

在我们开始之前,还有一件事需要知道:如果您使用的是 Ansible 4.0 版本,它附带了openstack.cloud集合的1.4.0版本。其中存在一个错误,使得动态清单脚本无法运行。您可以使用以下命令查询您安装的集合版本:

ansible-galaxy collection list | grep openstack.cloud

如果您需要安装更新版本,可以使用以下命令进行安装:

ansible-galaxy collection install openstack.cloud

这将在您的主目录中的一个隐藏目录中安装集合,因此如果您使用本地副本,请不要使用此命令:

/usr/local/lib/python3.8/dist-packages/ansible_collections/openstack/cloud/scripts/inventory/openstack_inventory.py

请使用这个代替:

~/.ansible/collections/ansible_collections/openstack/cloud/scripts/inventory/openstack_inventory.py

脚本的help输出显示了一些可能的参数;然而,Ansible 将使用的是--list--host,就像图 12.5所示:

图 12.5 – 展示 openstack_inventory.py 脚本的帮助功能

图 12.5 – 展示 openstack_inventory.py 脚本的帮助功能

第一个用于获取账户可见的所有服务器列表,第二个用于从每个服务器获取主机变量数据,不过这个清单脚本使用--list调用返回所有主机变量。使用主机列表返回数据是一种性能增强,正如我们在本书前面讨论的那样,消除了需要为每个主机调用 OpenStack API 的需求。

使用--list的输出相当长;以下是前几行:

图 12.6 – 展示 openstack_inventory.py 动态清单返回的数据

图 12.6 – 展示 openstack_inventory.py 动态清单返回的数据

配置的账户只有一个可见的服务器,其 UUID 为875f88bc-ae18-42da-b988-0e4481e35f7e,这是我们在之前的示例中启动的实例。我们在flavor-ds1Gimage-Fedora34组中看到了这个实例的列表,例如。第一个组是所有使用ds1G口味运行的服务器,第二个组是所有使用我们的Fedora34镜像运行的服务器。这些分组在清单插件中自动发生,可能根据您使用的 OpenStack 设置而有所不同。输出的末尾将显示插件提供的其他组:

图 12.7 – 展示 openstack_inventory.py 动态清单返回的更多数据

图 12.7 – 展示 openstack_inventory.py 动态清单返回的更多数据

重要提示

请注意,要出现前述分组,openstack.yaml文件中必须设置expand_hostvars: True

一些额外的组如下:

  • mastery_cloud:在我们的clouds.yaml文件中指定的mastery_cloud实例上运行的所有服务器

  • flavor-ds1G:使用ds1G口味的所有服务器

  • image-Fedora 29:使用Fedora 29镜像的所有服务器

  • instance-875f88bc-ae18-42da-b988-0e4481e35f7e:以实例本身命名的一个组

  • nova:在nova服务下运行的所有服务器

提供了许多组,每个组可能都有清单脚本找到的服务器的不同部分。这些组使得通过 play 轻松地定位到正确的实例。主机被定义为服务器的 UUID。由于这些本质上是唯一的,而且也相当长,它们在 play 中作为目标是笨拙的。这使得组变得更加重要。

为了演示使用此脚本作为清单来源,我们将重新创建前面的示例,跳过创建服务器的步骤,只需使用适当的组目标编写第二个 play。我们将命名这个 playbook 为configure-server.yaml

--- 
- name: configure server 
  hosts: all 
  gather_facts: false 
  remote_user: fedora 

  tasks: 
    - name: install python 
      ansible.builtin.raw: "sudo dnf install -y python python-dnf" 

    - name: install imagemagick 
      ansible.builtin.dnf: 
        name: "ImageMagick" 
      become: "yes" 

此镜像的默认用户是fedora;然而,这些信息在 OpenStack API 中并不容易获得,因此在我们的清单脚本提供的数据中并没有反映出来。我们可以在 play 级别简单地定义要使用的用户。

这次,主机模式设置为all,因为我们的演示 OpenStack 服务器上目前只有一个主机;然而,在现实生活中,你不太可能在 Ansible 中如此公开地定位主机。

play 的其余部分保持不变,输出应该与以前的执行类似:

图 12.8 – 通过动态清单插件重新配置我们的虚拟实例

图 12.8 – 通过动态清单插件重新配置我们的虚拟实例

这个输出与上次执行boot-server.yaml playbook 时有一些不同。首先,mastery1实例没有被创建或启动。我们假设我们想要交互的服务器已经被创建并正在运行。其次,我们直接从 OpenStack 服务器本身中提取了这个 playbook 运行的清单,使用了一个动态清单插件,而不是在 playbook 中使用add_host创建一个清单。除此之外,输出是一样的,除了两个弃用警告。关于组名的警告出现是因为动态清单脚本提供了自动创建的组名,需要进行清理 - 我想这将在插件的未来版本中得到修复。此外,Python 弃用警告在 Ansible 完全转向 Python 3 的过渡阶段是常见的,只要你的 Python 2 环境没有缺少任何模块,它是无害的。

随着时间的推移,每次运行清单插件时都会发现当前 playbook 执行时有哪些服务器被添加或移除。这可以节省大量时间,因为不需要试图维护静态清单文件中服务器的准确列表。

管理公共云基础设施

使用 Ansible 管理公共云基础设施并不比使用它管理 OpenStack 更困难,就像我们之前讨论的那样。一般来说,对于任何被 Ansible 支持的 IaaS 提供商,让它工作的过程是一个三步骤的过程。

  1. 建立支持云提供商的 Ansible 集合、模块和动态清单插件。

  2. 在 Ansible 主机上安装任何先决条件软件或库。

  3. 定义 playbook 并对基础设施提供商运行它。

大多数提供商也有现成的动态清单插件可用,我们在本书中已经演示了其中两个:

  • amazon.aws.aws_ec2第一章中讨论过,Ansible 的系统架构和设计

  • openstack.cloud.openstack在本章前面已经演示过。

让我们来看看亚马逊网络服务AWS),特别是 EC2 服务。我们可以使用我们选择的镜像启动一个新的服务器,使用与之前在 OpenStack 中完全相同的高级流程。然而,正如你现在肯定已经猜到的那样,我们必须使用一个提供特定 EC2 支持的 Ansible 模块。让我们构建 playbook。首先,我们的初始 play 将再次从本地主机运行,因为这将调用 EC2 来启动我们的新服务器:

---
- name: boot server
  hosts: localhost
  gather_facts: false

接下来,我们将使用community.aws.ec2_instance模块来代替openstack.cloud.server模块来启动我们想要的服务器。这段代码只是一个示例,用来展示如何使用模块;通常情况下,就像我们的openstack.cloud.server示例一样,你不会在 playbook 中包含密钥,而是会将它们存储在某个保险库中:

    - name: boot the server
      community.aws.ec2_instance:
        access_key: XXXXXXXXXXXXXXXXX
        secret_key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
        key_name: mastery-demo
        security_group: default
        instance_type: t2.micro
        image_id: "ami-04d4a52790edc7894"
        region: eu-west-2
        tags: "{'ansible_group':'mastery_server', 'Name':'mastery1'}"
        wait: true
        user_data: |
          #!/bin/bash
          sudo dnf install -y python python-dnf
      register: newserver

重要提示

community.aws.ec2_instance模块需要在 Ansible 主机上安装 Python 的boto3库;这个方法在不同的操作系统上会有所不同,但在我们的 Ubuntu Server 20.04 演示主机上,它是使用sudo apt install python3-boto3命令安装的。另外,如果你在 Python 3 下安装这个模块,请确保你的 Ansible 安装使用 Python 3,设置ANSIBLE_PYTHON_INTERPRETER变量。

上述代码旨在执行与我们的openstack.cloud.server示例相同的工作,尽管在高层次上看起来相似,但有许多不同之处。因此,在使用新模块时,有必要阅读模块文档,以确切了解如何使用它。特别值得注意的是,user_data字段可用于将创建后的脚本发送到新的 VM;当需要立即进行初始配置时,这非常有用,适用于ansible.builtin.raw命令。在这种情况下,我们使用它来安装后续使用 Ansible 安装ImageMagick所需的 Python 3 先决条件。

接下来,我们可以通过使用在前面的任务中注册的newserver变量来获取我们新创建的服务器的公共 IP 地址。但是,请注意与使用openstack.cloud.server模块时访问此信息的方式相比,变量结构不同(再次,始终参考文档):

    - name: show floating ip 
      ansible.builtin.debug: 
        var: newserver.instances[0].public_ip_address 

community.aws.ec2_instance模块和openstack.cloud.server模块之间的另一个关键区别是,community.aws.ec2_instance不一定会在 SSH 连接可用之前完成 - 这可以使用wait参数进行设置;因此,定义一个专门用于此目的的任务是一个良好的做法,以确保我们的 playbook 不会因为缺乏连接而在后来失败:

    - name: Wait for SSH to come up
      ansible.builtin.wait_for_connection:
        delay: 5
        timeout: 320 

完成此任务后,我们将知道我们的主机是活动的并且响应 SSH,因此我们可以继续使用ansible.builtin.add_host将这个新主机添加到清单中,然后像之前一样安装ImageMagick(这里使用的图像是在 OpenStack 示例中使用的相同的 Fedora 34 云图像):

    - name: add new server 
      ansible.builtin.add_host: 
        name: "mastery1" 
        ansible_ssh_host: "{{ newserver.instances[0].public_ip_address }}" 
        ansible_ssh_user: "fedora"
- name: configure server
  hosts: mastery1
  gather_facts: false
  tasks:
    - name: install imagemagick
      ansible.builtin.dnf:
        name: "ImageMagick"
      become: "yes" 

将所有这些放在一起并运行 playbook 应该会产生类似以下截图的结果。请注意,我已经关闭了 SSH 主机密钥检查,以防止 SSH 传输代理在第一次运行时询问添加主机密钥,这将导致 playbook 挂起并等待用户干预,使用以下命令:

export ANSIBLE_PYTHON_INTERPRETER=$(which python3)
ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook -i mastery-hosts boot-ec2-server.yaml --private-key mastery-key.pem

您还会注意到,我已经将我在 AWS 帐户上生成的密钥对中保存的私人 SSH 密钥保存为mastery-key.pem,保存在与 playbook 相同的目录中 - 您需要将您自己的密钥保存在此位置,并相应地在命令行中引用它。成功运行应该看起来像图 12.9中显示的输出:

图 12.9 - 使用 Ansible 引导和设置 Amazon EC2 实例

图 12.9 - 使用 Ansible 引导和设置 Amazon EC2 实例

正如我们在这里看到的,我们可以在不同的云提供商上使用略有不同的 playbook 来实现相同的结果。关键在于阅读每个模块附带的文档,并确保正确引用参数和返回值。

我们可以将这种方法应用到 Azure、Google Cloud 或 Ansible 支持的任何其他云提供商。如果我们想在 Azure 上重复这个例子,那么我们需要使用azure.azcollection.azure_rm_virtualmachine模块。该模块的文档说明我们需要 Python 2.7 或更新版本(这已经是我们 Ubuntu Server 20.04 演示机的一部分),以及一整套 Python 模块,这些模块的名称以及所需版本可以在一个名为requirements-azure.txt的文件中找到,该文件包含在集合中。期望您将使用pip安装这些要求,并且您可以通过在文件系统上找到上述文件,然后安装所需的模块来实现这一点。在我的演示系统上,我使用了以下命令来实现这一点:

locate requirements-azure.txt
sudo pip3 install -r /usr/local/lib/python3.8/dist-packages/ansible_collections/azure/azcollection/requirements-azure.txt

满足了这些先决条件,我们可以再次构建我们的 playbook。请注意,使用 Azure,可以使用多种身份验证方法。为了简单起见,我使用了为此演示创建的 Azure Active Directory 凭据;但是,为了启用此功能,我还必须安装官方的 Azure CLI 实用程序(按照此处提供的说明进行:docs.microsoft.com/en-gb/cli/azure/install-azure-cli-linux?pivots=apt),并使用以下命令登录:

az login

这确保您的 Ansible 主机受到 Azure 的信任。在实践中,您可以设置一个服务主体,从而无需进行此操作,鼓励您自行探索这个选项。继续进行当前的简单示例,我们像以前一样设置 playbook 的头部:

---
- name: boot server
  hosts: localhost
  gather_facts: false
  vars:
    vm_password: Password123!

请注意,这一次,我们将为新 VM 存储一个密码变量;通常情况下,我们会将其存储在保险库中,但这又留给读者作为练习。从这里开始,我们使用azure.azcollection.azure_rm_virtualmachine模块来启动我们的新 VM。为了保持与之前示例的连贯性,我必须在 Azure 的图像市场上找到Fedora 34图像,这需要定义一些额外的参数,例如plan。为了使 Ansible 能够使用此图像,我首先必须找到它,然后接受作者的条款以启用其使用,使用以下命令使用az命令行实用程序:

az vm image list --offer fedora --all --output table
az vm image show --urn tunnelbiz:fedora:fedoraupdate:34.0.1
az vm image terms accept --urn tunnelbiz:fedora:fedoraupdate:34.0.1

我还必须创建 VM 将使用的资源组和网络;这些都是非常 Azure 特定的步骤,并且有很好的文档记录(如果您熟悉 Azure,则被认为是基本操作)。完成所有先决条件后,我就能够编写以下 playbook 代码来启动我们基于 Azure 的Fedora 34图像:

  tasks:
    - name: boot the server
      azure.azcollection.azure_rm_virtualmachine:
        ad_user: masteryadmin@example.com
        password: < insert your ad password here >
        subscription_id: xxxxxxxx-xxxxxx-xxxxxx-xxxxxxxx
        resource_group: mastery
        name: mastery1
        admin_username: fedora
        admin_password: "{{ vm_password }}"
        vm_size: Standard_B1s
        managed_disk_type: "Standard_LRS"
        image:
          offer: fedora
          publisher: tunnelbiz
          sku: fedoraupdate
          version: 34.0.1
        plan:
          name: fedoraupdate
          product: fedora
          publisher : tunnelbiz
      register: newserver

与之前的示例一样,我们获取图像的公共 IP 地址(注意访问此地址所需的复杂变量),确保 SSH 访问正常工作,然后使用ansible.builtin.add_host将新的 VM 添加到我们的运行时清单中:

    - name: show floating ip
      ansible.builtin.debug:
        var: newserver.ansible_facts.azure_vm.properties.networkProfile.networkInterfaces[0].properties.ipConfigurations[0].properties.publicIPAddress.properties.ipAddress
    - name: Wait for SSH to come up
      ansible.builtin.wait_for_connection:
        delay: 1
        timeout: 320
    - name: add new server
      ansible.builtin.add_host:
        name: "mastery1"
        ansible_ssh_host: "{{ newserver.ansible_facts.azure_vm.properties.networkProfile.networkInterfaces[0].properties.ipConfigurations[0].properties.publicIPAddress.properties.ipAddress }}"
        ansible_ssh_user: "fedora"
        ansible_ssh_pass: "{{ vm_password }}"
        ansible_become_pass: "{{ vm_password }}"

Azure 允许在 Linux VM 上使用基于密码或基于密钥的 SSH 身份验证;我们在这里使用基于密码的方式是为了简单起见。还要注意新使用的ansible_become_pass连接变量,因为我们使用的Fedora 34图像在使用sudo时会提示输入密码,可能会阻止执行。最后,完成这项工作后,我们像以前一样安装ImageMagick

- name: configure server
  hosts: mastery1
  gather_facts: false
  tasks:
    - name: install python
      ansible.builtin.raw: "dnf install -y python python-dnf"
      become: "yes"
    - name: install imagemagick
      ansible.builtin.dnf:
        name: "ImageMagick"
      become: "yes"

代码完成后,使用以下命令运行它(根据需要设置您系统的 Python 环境):

export ANSIBLE_PYTHON_INTERPRETER=$(which python3)
ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook -i mastery-hosts boot-azure-server.yaml

让我们看看它是如何运作的:

图 12.10–使用 Ansible 创建和配置 Azure 虚拟机

图 12.10–使用 Ansible 创建和配置 Azure 虚拟机

输出与我们的 AWS 示例非常相似,表明我们可以非常轻松地跨不同的云平台执行相同的操作,只需稍微学习每个云提供商所需模块的工作原理。本章的这一部分绝不是最终的,因为 Ansible 支持的平台和操作数量很多,但我们希望所提供的信息能够让您了解将 Ansible 与新的云平台集成所需的流程和步骤。接下来,我们将看看如何使用 Ansible 与 Docker 容器交互。

与 Docker 容器交互

Linux 容器技术,特别是 Docker,在近年来变得越来越受欢迎,自本书上一版出版以来这种趋势一直在持续。容器提供了一种快速的资源隔离路径,同时保持运行时环境的一致性。它们可以快速启动,并且运行效率高,因为几乎没有额外的开销。诸如 Docker 之类的实用工具为容器管理提供了许多有用的工具,例如用作文件系统的镜像注册表、构建镜像本身的工具、集群编排等。通过其易用性,Docker 已成为管理容器的最流行方式之一,尽管其他工具,如 Podman 和 LXC,也变得越来越普遍。不过,目前我们将专注于 Docker,因为它具有广泛的吸引力和广泛的安装基础。

Ansible 也可以以多种方式与 Docker 进行交互。特别是,Ansible 可以用于构建镜像,启动或停止容器,组合多个容器服务,连接到并与活动容器进行交互,甚至从容器中发现清单。Ansible 提供了一整套用于与 Docker 一起工作的工具,包括相关模块、连接插件和清单脚本。

为了演示如何使用 Docker,我们将探讨一些用例。第一个用例是构建一个新的镜像以供 Docker 使用。第二个用例是从新镜像启动一个容器并与其交互。最后一个用例是使用清单插件与活动容器进行交互。

重要提示

创建一个功能齐全的 Docker 安装非常依赖于您的基础操作系统。一个很好的资源是 Docker 网站,提供了详细的安装和使用说明,网址是docs.docker.com。Ansible 在 Linux 主机上与 Docker 配合效果最佳,因此我们将继续使用本书中一直使用的 Ubuntu Server 20.04 LTS 演示机。

构建镜像

Docker 镜像基本上是与运行时使用的参数捆绑在一起的文件系统。文件系统通常是 Linux Userland 的一小部分,包含足够的文件来启动所需的进程。Docker 提供了构建这些镜像的工具,通常基于非常小的、预先存在的基础镜像。该工具使用 Dockerfile 作为输入,Dockerfile 是一个带有指令的纯文本文件。该文件由 docker build 命令解析,我们可以通过 docker_image 模块解析它。其余的示例将来自使用 Docker CE 版本 20.10.8 的 Ubuntu Server 20.04 虚拟机,其中添加了 cowsay 和 nginx 包,以便运行容器将提供一个显示 cowsay 内容的 Web 服务器。

首先,我们需要一个 Dockerfile。如果您以前没有遇到过这个文件,它是用于构建 Docker 容器的一组指令-如果您愿意,您可以在这里了解更多信息:docs.docker.com/engine/reference/builder/。这个文件需要存在于 Ansible 可以读取的路径中,我们将把它放在与我的 playbooks 相同的目录中。Dockerfile 的内容将非常简单。我们需要定义一个基本镜像,一个运行安装必要软件的命令,一些最小的软件配置,一个要暴露的端口,以及一个使用此镜像运行容器的默认操作:

FROM docker.io/fedora:34 

RUN dnf install -y cowsay nginx 
RUN echo "daemon off;" >> /etc/nginx/nginx.conf 
RUN cowsay boop > /usr/share/nginx/html/index.html 

EXPOSE 80 

CMD /usr/sbin/nginx 

构建过程执行以下步骤:

  1. 我们正在使用 Docker Hub 镜像注册表上的 fedora 存储库中的 Fedora 34 镜像。

  2. 为了安装必要的 cowsay 和 nginx 包,我们使用 dnf。

  3. 要直接在容器中运行 nginx,我们需要在 nginx.conf 中将 daemon 模式关闭。

  4. 我们使用 cowsay 生成默认网页的内容。

  5. 然后,我们指示 Docker 在容器中暴露端口 80,其中 nginx 将监听连接。

  6. 最后,这个容器的默认操作将是运行 nginx。

构建和使用镜像的 playbook 可以放在同一个目录中。我们将其命名为docker-interact.yaml。该 playbook 将在localhost上运行,并有两个任务;一个是使用community.docker.docker_image构建镜像,另一个是使用community.docker.docker_container启动容器:

--- 
- name: build an image 
  hosts: localhost 
  gather_facts: false 

  tasks: 
    - name: build that image 
      community.docker.docker_image: 
        path: . 
        state: present 
        name: fedora-moo 

    - name: start the container 
      community.docker.docker_container: 
        name: playbook-container 
        image: fedora-moo 
        ports: 8080:80 
        state: started
        container_default_behavior: no_defaults

在运行我们的 playbook 之前,我们将检查可能与我们之前的 playbook 定义匹配的任何可能的容器镜像或正在运行的容器 - 这将帮助我们确信我们的代码正在产生期望的结果。如果您有从以前的测试中运行的任何其他容器,可以运行以下命令来检查与我们的规范匹配的fedora-based 容器:

docker ps -a --filter ancestor=fedora-moo
docker images --filter reference='fedora*'

除非您之前已运行过此代码,否则应该看到没有正在运行的容器,如图 12.11所示:

图 12.11 - 在运行我们的 playbook 之前检查容器的缺席

图 12.11 - 在运行我们的 playbook 之前检查容器的缺席

现在,让我们运行 playbook 来构建镜像并使用该镜像启动容器 - 请注意,与许多其他 Ansible 模块一样,您可能需要安装额外的 Python 模块才能使您的代码正常工作。在我的 Ubuntu Server 20.04 演示机器上,我不得不运行以下命令:

sudo apt install python3-docker
export ANSIBLE_PYTHON_INTERPRETER=$(which python3)

安装了 Python 支持后,您可以使用以下命令运行 playbook:

ansible-playbook -i mastery-hosts docker-interact.yaml

成功运行 playbook 应该类似于图 12.12

图 12.12 - 使用 Ansible 构建和运行我们的第一个 Docker 容器

图 12.12 - 使用 Ansible 构建和运行我们的第一个 Docker 容器

为了节省屏幕空间,此 playbook 执行的冗长度已经减少。我们的输出只是显示构建镜像的任务和启动容器的任务都产生了变化。快速检查运行的容器和可用的镜像应该反映我们的工作 - 您可以使用与 playbook 运行之前相同的docker命令来验证这一点:

图 12.13 - 验证我们在 Docker 中运行的 Ansible playbook 的结果

图 12.13 - 验证我们在 Docker 中运行的 Ansible playbook 的结果

我们可以使用curl来访问 Web 服务器来测试容器的功能,这应该会显示一头牛说boop,就像图 12.14中演示的那样:

图 12.14 - 检索使用 Ansible 创建和运行的容器的结果

图 12.14 - 检索使用 Ansible 创建和运行的容器的结果

通过这种方式,我们已经展示了使用 Ansible 与 Docker 进行交互有多么容易。但是,这个例子仍然是基于使用本地的Dockerfile,随着我们在本章中的进展,我们将看到一些更高级的 Ansible 用法,这些用法不需要Dockerfile

构建不需要 Dockerfile 的容器

Dockerfile 很有用,但 Dockerfile 内部执行的许多操作都可以用 Ansible 来完成。Ansible 可以用于使用基础镜像启动容器,然后使用docker连接方法(而不是 SSH)与该容器进行交互以完成配置。让我们通过重复之前的示例来演示这一点,但不需要Dockerfile。相反,所有的工作都将由一个名为docker-all.yaml的全新 playbook 处理。该 playbook 的第一部分从 Docker Hub 的Fedora 34的预先存在的镜像中启动一个容器,并使用ansible.builtin.add_host将生成的容器详细信息添加到 Ansible 的内存库存中:

--- 
- name: build an image 
  hosts: localhost 
  gather_facts: false 
  tasks: 
    - name: start the container 
      community.docker.docker_container: 
        name: playbook-container 
        image: docker.io/fedora:34
        ports: 8080:80 
        state: started 
        command: sleep 500 
        container_default_behavior: no_defaults

    - name: make a host 
      ansible.builtin.add_host: 
        name: playbook-container 
        ansible_connection: docker 
        ansible_ssh_user: root

然后,使用这个新添加的库存主机,我们定义了第二个播放,该播放在刚刚启动的容器中运行 Ansible 任务,配置我们的cowsay服务,就像以前一样,但不需要Dockerfile

- name: do things 
  hosts: playbook-container 
  gather_facts: false 

  tasks: 
    - name: install things 
      ansible.builtion.raw: dnf install -y python-dnf 

    - name: install things 
      ansible.builtin.dnf: 
        name: ['nginx', 'cowsay']

    - name: configure nginx 
      ansible.builtin.lineinfile: 
        line: "daemon off;" 
        dest: /etc/nginx/nginx.conf 
    - name: boop 
      ansible.builtin.shell: cowsay boop > /usr/share/nginx/html/index.html 

    - name: run nginx 
      ansible.builtin.shell: nginx & 

回顾一下,播放书包括两个播放。第一个播放从基本Fedora 34镜像创建容器。community.docker.docker_container任务被赋予一个sleep命令,以保持容器运行一段时间,因为docker连接插件只能与活动容器一起工作(从 Docker Hub 获取的未配置的操作系统镜像通常在运行时立即退出,因为它们没有默认操作要执行)。第一个播放的第二个任务创建了容器的运行时清单条目。清单主机名必须与容器名称匹配。连接方法也设置为docker

第二个播放目标是新创建的主机,第一个任务使用ansible.builtin.raw模块来放置python-dnf包(这将带来其余的Python),以便我们可以在下一个任务中使用ansible.builtin.dnf模块。然后,使用ansible.builtin.dnf模块安装所需的软件包,即nginxcowsay。然后,使用ansible.builtin.lineinfile模块向nginx配置添加新行。一个ansible.builtin.shell任务使用cowsay来为nginx创建内容。最后,nginx本身作为后台进程启动。

在运行播放书之前,让我们通过运行以下命令删除上一个示例中的任何运行的容器:

docker ps -a --filter ancestor=fedora-moo
docker rm -f playbook-container
docker ps -a --filter ancestor=fedora-moo

您可以将其与图 12.15中的屏幕截图进行验证:

图 12.15 - 清理上一次播放书运行中的运行容器

图 12.15 - 清理上一次播放书运行中的运行容器

删除运行的容器后,我们现在可以运行我们的新播放书来重新创建容器,绕过构建镜像的步骤,使用以下命令:

ansible-playbook -i mastery-hosts docker-all.yaml

成功运行的输出应该看起来像图 12.16中显示的那样:

图 12.16 - 使用 Ansible 构建没有 Dockerfile 的容器

图 12.16 - 使用 Ansible 构建没有 Dockerfile 的容器

我们看到第一个播放执行任务在localhost上,然后第二个播放在playbook-container上执行。完成后,我们可以使用以下命令测试 Web 服务并列出运行的容器以验证我们的工作:

curl http://localhost:8080
docker ps -a --filter ancestor=fedora:34

注意这次不同的过滤器;我们的容器是直接从fedora镜像构建和运行的,而没有创建fedora-moo镜像的中间步骤 - 输出应该看起来像图 12.17中显示的那样:

图 12.17 - 验证我们的播放书运行结果

图 12.17 - 验证我们的播放书运行结果

使用 Ansible 配置运行容器的这种方法有一些优势。首先,您可以重用现有角色来设置应用程序,轻松地从云虚拟机目标切换到容器,甚至切换到裸金属资源(如果需要的话)。其次,您可以通过审查播放书内容轻松地审查所有配置进入应用程序的内容。

使用这种交互方法的另一个用例是使用 Docker 容器模拟多个主机,以验证跨多个主机执行播放书的执行。可以启动一个带有init系统作为运行进程的容器,允许启动其他服务,就像它们在完整的操作系统上一样。在持续集成环境中,这种用例对于快速有效地验证播放书内容的更改非常有价值。

Docker 清单

与本书前面详细介绍的 OpenStack 和 EC2 清单插件类似,还提供了 Docker 清单插件。如果您希望检查 Docker 清单脚本或以类似于我们在本章前面使用其他动态清单插件的方式使用它,可以找到 Docker 清单脚本,通过创建一个 YAML 清单文件来引用该插件。

让我们首先找到清单脚本本身 - 在我的演示系统上,它位于这里:

/usr/local/lib/python3.8/dist-packages/ansible_collections/community/general/scripts/inventory/docker.py

一旦你习惯了 Ansible 的安装基本路径,你会发现通过集合轻松浏览目录结构,找到你要找的东西。让我们尝试直接运行这个脚本,看看在配置它用于 playbook 清单目的时我们有哪些选项可用:

python3 /usr/local/lib/python3.8/dist-packages/ansible_collections/community/general/scripts/inventory/docker.py --help

脚本的help输出显示了许多可能的参数;然而,Ansible 将使用的是--list--host - 您的输出将类似于图 12.18所示:

图 12.18 - 检查 Docker 动态清单脚本上可用的选项

图 12.18 - 检查 Docker 动态清单脚本上可用的选项

如果之前构建的容器在执行此脚本时仍在运行,您可以使用以下命令列出主机:

python3 /usr/local/lib/python3.8/dist-packages/ansible_collections/community/general/scripts/inventory/docker.py --list --pretty | grep -C2 playbook-container

它应该出现在输出中(grep已经被用来在截图中更明显地显示这一点):

图 12.19 - 手动运行 Docker 动态清单插件以探索其行为

图 12.19 - 手动运行 Docker 动态清单插件以探索其行为

与之前一样,提供了许多组,这些组的成员是正在运行的容器。之前显示的两个组是短容器 ID 和长容器 ID。许多变量也作为输出的一部分进行了定义,在前面的截图中已经被大幅缩减。输出的最后一部分显示了另外一些组:

图 12.20 - 进一步探索动态清单脚本输出

图 12.20 - 进一步探索动态清单脚本输出

附加的组如下:

  • docker_hosts:所有与动态清单脚本通信并查询容器的 Docker 守护程序运行的主机。

  • image_name:每个被发现容器使用的图像的组。

  • container name:与容器名称匹配的组

  • running:所有正在运行的容器的组。

  • stopped:所有已停止的容器的组 - 您可以在前面的输出中看到,我们之前启动的容器现在已经停止,因为 500 秒的休眠时间已经过期。

此清单插件及其提供的组和数据可以被 playbook 用来针对可用的各种容器进行交互,而无需手动清单管理或使用add_host。在 playbook 中使用插件只需简单地定义一个 YAML 清单文件,其中包含插件名称和连接详细信息 - 要查询本地 Docker 主机,我们可以定义我们的清单如下:

---
plugin: community.docker.docker_containers
docker_host: unix://var/run/docker.sock

您可以以正常方式对此清单定义使用 Ansible 运行临时命令或 playbook,并获取本地主机上运行的所有容器的详细信息。连接到远程主机并不会更加困难,插件文档(可在此处找到:docs.ansible.com/ansible/latest/collections/community/docker/docker_containers_inventory.html)向您展示了可用于此的选项。我们现在已经看过了几种构建和与 Docker 容器交互的方法,但如果我们想要一个更加协调的方法呢?我们将在下一节中详细讨论这个问题。

使用 Ansible 构建容器

正如我们在上一节开头提到的,自本书上一版出版以来,容器的世界已经取得了很大的进步。尽管 Docker 仍然是一种非常流行的容器技术,但新的和改进的技术已经成为首选,并且被纳入到 Linux 操作系统中。Canonical(Ubuntu 的发布者)正在支持LXC容器环境,而 Red Hat(Ansible 的所有者)正在支持BuildahPodman

如果你读过本书的第三版,你会知道我们介绍了一个名为Ansible Container的技术,它用于直接集成 Ansible 和 Docker,消除了glue步骤,比如将主机添加到内存中的清单,有两个单独的 play 来实例化容器,以及构建容器镜像内容。Ansible Container 现在已经被弃用,所有的开发工作都已经停止(根据他们的 GitHub 页面 - 如果你感兴趣,可以查看github.com/ansible/ansible-container)。

Ansible Container 已被一个名为ansible-bender的新工具取代,它具有不同容器构建环境的可插拔架构。在开发的早期阶段,它只支持Buildah,但希望将来会支持更多的容器技术。

Podman/Buildah 工具集可在较新版本的 Red Hat Enterprise Linux、CentOS、Fedora 和 Ubuntu Server 上使用(但不包括 20.04,除非你选择更先进的版本)。由于我们在本书中一直使用 Ubuntu Server 作为演示机器,我们将继续使用这个操作系统,但在本章的这一部分,我们将切换到 20.10 版本,虽然不是 LTS 版本,但有 Buildah 和 Podman 的本地版本可用。

要在 Ubuntu Server 20.10(以及更新版本)上安装 Buildah 和 Podman,只需运行以下命令:

sudo apt update
sudo apt install podman runc

一旦你安装了容器环境(如果你还没有安装 Ansible,请不要忘记安装 - ansible-bender需要它来运行!),你可以使用以下命令安装ansible-bender

sudo pip3 install ansible-bender

就是这样 - 现在你已经准备好了!在我们深入示例代码之前,值得注意的是ansible-bender在功能上比 Ansible Container 简单得多。虽然 Ansible Container 可以管理容器的整个生命周期,但ansible-bender只关注容器的构建阶段 - 尽管如此,它提供了一个有用的抽象层,可以使用 Ansible 轻松构建容器镜像,一旦它支持其他容器化构建平台(如 LXC 和/或 Docker),它将成为你自动化工具中非常有价值的工具,因为你将能够使用几乎相同的 playbook 代码在各种平台上构建容器镜像。

让我们为ansible-bender构建我们的第一个 playbook。play 的头部现在看起来应该很熟悉 - 有一个重要的例外。注意 play 定义中的vars:部分 - 这部分包含了ansible-bender使用的重要保留变量,并定义了诸如源容器镜像(我们将再次使用Fedora 34)和目标容器镜像详细信息,包括容器启动时要运行的命令:

--- 
- name: build an image with ansible-bender
  hosts: localhost 
  gather_facts: false 
  vars:
    ansible_bender:
      base_image: fedora:34
      target_image:
        name: fedora-moo
        cmd: nginx &

有了这个定义,我们编写我们的 play 任务的方式与之前完全相同。请注意,我们不需要担心清单定义(无论是通过动态清单提供程序还是通过ansible.builtin.add_host) - ansible-bender会在实例化容器镜像时使用ansible_bender变量结构中的详细信息运行所有任务。因此,我们的代码应该是这样的 - 它与我们之前使用的第二个 play 完全相同,只是我们不运行最后的ansible.builtin.shell任务来启动nginx web 服务器,因为这是由ansible_bender变量中的详细信息处理的。

  tasks: 
    - name: install things 
      ansible.builtin.raw: dnf install -y python-dnf 

    - name: install things 
      ansible.builtin.dnf: 
        name: ['nginx', 'cowsay']

    - name: configure nginx 
      ansible.builtin.lineinfile: 
        line: "daemon off;" 
        dest: /etc/nginx/nginx.conf 

    - name: boop 
      ansible.builtin.shell: cowsay boop > /usr/share/nginx/html/index.html

就是这样 - 代码没有比这更复杂的了!现在,使用ansible-bender构建你的第一个容器就像运行以下命令一样简单:

sudo ansible-bender build moo-bender.yaml

请注意,命令必须以 root 身份运行(即通过sudo) - 这是与 Buildah 和 Podman 相关的特定行为。

ansible-bender的一个奇怪之处是,当它开始运行时,您会看到一些声明ERROR的行(见图 12.21)。这是ansible-bender中的一个错误,因为这些行实际上并不是错误 - 它们只是从 Buildah 工具返回的信息:

图 12.21 - 使用 ansible-bender 开始容器构建过程,以及虚假的 ERROR 消息

图 12.21 - 使用 ansible-bender 开始容器构建过程,以及虚假的 ERROR 消息

随着构建的继续,您应该看到 Ansible playbook 消息以您熟悉的方式返回。在过程结束时,您应该看到类似于图 12.22所示的成功构建的输出:

图 12.22 - 使用 ansible-bender 成功构建容器

图 12.22 - 使用 ansible-bender 成功构建容器

从这里,您可以使用以下命令运行您新构建的容器:

sudo podman run -d fedora-moo

fedora-moo容器名称是在之前的 playbook 文件中的ansible_bender变量结构中设置的,而-d标志用于从容器中分离并在后台运行。与 Docker 类似,您可以使用以下命令查询系统上正在运行的容器:

sudo podman ps

这个过程的输出看起来有点像图 12.23所示:

图 12.23 - 运行和查询我们新构建的 Podman 容器

图 12.23 - 运行和查询我们新构建的 Podman 容器

最后,让我们看看我们是否可以从容器中实际检索到我们的cowsay网页。与我们的 Docker 示例不同,我们没有指示 Podman 将 Web 服务器端口重定向到构建机器上的端口,因此我们需要查询容器本身的 IP 地址。在获得sudo podman ps输出中的CONTAINER IDNAMES后,我们可以使用以下命令查询这个(确保用您系统中的 ID 替换容器 ID):

sudo podman inspect -f '{{ .NetworkSettings.IPAddress }}' f711

与 Docker 一样,只要您输入的字符在正在运行的容器列表中是唯一的,您就可以缩写您的容器 ID。一旦获得了 IP 地址,您就可以使用curl下载网页,就像我们之前做的那样 - 例如:

curl http://172.16.16.9

整个过程应该看起来像图 12.24所示:

图 12.24 - 从使用 ansible-bender 构建的 Podman 容器中下载我们的 cowsay 网页

图 12.24 - 从使用 ansible-bender 构建的 Podman 容器中下载我们的 cowsay 网页

就是这样了!ansible-bender工具在使用一种通用语言 - 我们自己喜欢的 Ansible 来构建容器映像方面显示出了巨大的潜力!随着工具的发展,希望一些粗糙的地方(比如虚假的ERROR语句)将得到解决,并且对更多容器平台的支持的添加将真正使其成为一个有价值的容器映像自动化工具。这就结束了我们对使用 Ansible 进行基础架构提供的介绍 - 希望您觉得有价值。

总结

DevOps 已经推动了许多新方向的自动化,包括应用程序的容器化,甚至基础架构本身的创建。云计算服务使得可以自助管理用于运行服务的服务器群。Ansible 可以轻松地与这些服务进行交互,提供自动化和编排引擎。

在本章中,您学习了如何使用 Ansible 管理本地云基础架构,例如 OpenStack。然后,我们通过 AWS 和 Microsoft Azure 的公共云基础架构提供示例进行了扩展。最后,您学习了如何使用 Ansible 与 Docker 进行交互,以及如何使用 Ansible Container 整洁地打包 Docker 服务定义。

Ansible 可以启动几乎任何主机,除了正在运行的主机之外,并且在具有适当凭据的情况下,它可以创建它想要管理的基础架构,无论是一次性操作还是将应用程序的新版本部署到生产容器管理系统中。 最终结果是,一旦硬件就位并且服务提供商已配置,如果您愿意,您可以通过 Ansible 管理整个基础架构!

在本书的最后一章中,我们将研究自动化的一个新且迅速增长的领域:使用 Ansible 进行网络配置。

问题

  1. 在 OpenStack 上创建或删除 VM 实例时,在您的播放中应该引用哪个清单主机?

a) OpenStack 主机

b) 本地主机

c) VM 浮动 IP 地址

d) 以上都不是

  1. 如何在第二个播放中引用新创建的虚拟机,而无需使用动态清单脚本?

a) 使用ansible.builtin.raw命令。

b) 使用ansible.builtin.shell命令。

c) 使用ansible.builtin.add_host将新的 VM 添加到内存清单中。

d) 您需要使用动态清单插件。

  1. 您仍然可以直接在 Ansible 4.x 及更高版本中运行动态清单脚本,就像在 Ansible 2.x 版本中一样。

a) 正确

b) 错误

  1. 要使用动态清单脚本,并设置其参数,您现在可以(假设集合已安装):

a) 使用插件名称和参数定义 YAML 清单文件。

b) 在ansible/ansible-playbook-i参数中引用动态清单脚本。

c) 将插件名称放在播放定义中。

  1. 第一次使用集合中的新模块(例如,与云提供商一起),您应该:

a) 始终阅读文档,检查已知问题。

b) 始终阅读文档,查看是否需要安装其他 Python 模块。

c) 始终阅读文档,查看应如何定义您的身份验证参数。

d) 以上所有

  1. 如果目标主机上没有 Python 环境,Ansible 无法运行(这在最小的云操作系统映像上有时是这样)。 如果是这种情况,您仍然可以使用哪个模块从 playbook 任务中安装 Python?

a) ansible.builtin.python

b) ansible.builtin.raw

c) ansible.builtin.command

d) ansible.builtin.shell

  1. 所有云提供商模块都将等待 VM 实例启动,然后才允许播放继续执行下一个任务。

a) 正确

b) 错误

  1. 如果要等待确保主机在执行其他任务之前可以通过 SSH 访问,可以使用哪个模块?

a) ansible.builtin.wait_for

b) ansible.builtin.ssh

c) ansible.builtin.test_connection

d) ansible.builtin.connect

  1. Ansible 可以使用 Dockerfile 构建 Docker 容器,也可以不使用 Dockerfile。

a) 正确

b) 错误

  1. ansible-bender工具目前支持哪种构建环境?

a) Docker

b) LXC

c) Podman/Buildah

d) 以上所有

第十三章:网络自动化

从历史上看,网络主要由硬件组成,只有少量软件参与。更改其拓扑结构涉及安装和配置新的交换机或机箱中的刀片,或者至少重新连接一些电缆。现在,情况已经改变,为了满足云托管或基于微服务的部署等多租户环境的复杂基础设施,需要一个更具敏捷性和灵活性的网络。这导致了软件定义网络SDN)的出现,这种方法将网络配置集中化(在历史上是在每个设备上配置),并导致网络拓扑被定义为一个整体,而不是一系列组件部分。这可以说是网络本身的抽象层,因此意味着就像基础设施即服务一样,现在可以用代码来定义网络。

自本书上一版出版以来,Ansible 已经进行了大量工作,以增强和标准化项目内的网络自动化。除此之外,集合的出现使得许多网络设备的模块能够从ansible-core软件包中解耦出来,从而使网络供应商能够更好地拥有他们的代码,并根据需要发布它们,而不是受到 Ansible 发布节奏的驱动。在撰写本文时,只有少数 Ansible 集合(因此模块)仍由 Ansible 网络团队负责,大多数现在直接由供应商自己维护。这对所有相关方都是件好事,并确保了 Ansible 网络产品的更可靠和更快速的发展。

最终,这意味着一件事 - 您现在可以在 Ansible playbook 中定义您的网络基础设施,就像您可以描述您的计算基础设施一样,就像我们在上一章中描述的那样。

在本章中,我们将通过以下主题探讨这一迅速增长的重要领域:

  • 用于网络管理的 Ansible

  • 处理多种设备类型

  • 使用cli_command模块

  • 使用 Ansible 配置 Arista EOS 交换机

  • 使用 Ansible 配置 Cumulus Networks 交换机

  • 最佳实践

技术要求

要跟随本章中提出的示例,您需要一台运行Ansible 4.3或更新版本的 Linux 机器。几乎任何 Linux 版本都可以 - 对于那些对具体情况感兴趣的人,本章中提供的所有代码都是在Ubuntu Server 20.04 LTS上测试的,除非另有说明,并且在 Ansible 4.3 上进行了测试。本章附带的示例代码可以从 GitHub 的以下网址下载:github.com/PacktPublishing/Mastering-Ansible-Fourth-Edition/tree/main/Chapter13

查看以下视频以查看代码的实际操作:bit.ly/3G5pNjJ

用于网络管理的 Ansible

核心网络设备,如交换机、路由器和防火墙,在企业环境中一直具有管理接口。命令行界面CLI)一直在这些设备上很受欢迎,因为它们支持脚本编写,因此,正如您已经猜到的那样,它们非常适合 Ansible 自动化。

从历史上看,团队在管理这些设备时面临着一系列挑战,包括维护配置,应对设备的故障/丢失,并在出现问题时获得支持。通常,公司发现自己被锁定在单一网络供应商(或者说,最多是一小部分供应商)中,以便使用专有工具来管理网络。与任何被技术所束缚的情况一样,这既有好处也有坏处。再加上软件定义网络的复杂性正在迅速变化和发展,挑战变得更加严峻。在本节中,我们将探讨 Ansible 如何解决这些挑战。

跨平台支持

正如我们在本书中所看到的,Ansible 旨在使自动化代码在尽可能多的场景中可移植和可重用。在[第十二章](B17462_12_Final_JC_ePub.xhtml#_idTextAnchor224),基础设施配置中,我们使用了几乎相同的 playbook 来配置四个不同提供商的基础设施,并且为了支持这一点,所给出的示例相当简单。当然,如果我们愿意,我们可以通过使用角色来进一步开发这一点,以消除如此多的重复代码,但这种简单性是故意的,以演示代码的相似性,无论使用的是哪个提供商。

简而言之,Ansible 使得编写可以在多个环境中运行的 playbook 以最小的工作量实现相同的目标成为可能,一旦我们定义了第一个目标。网络也是如此。集合的出现意味着不再有中央网络模块索引,因为集合本身定义了支持哪些平台。然而,用于网络自动化的 Ansible页面,可在docs.ansible.com/ansible/latest/network/index.html找到,这是一个很好的起点,因为它提供了许多受支持平台的列表。然而,此页面上的平台列表并不完整-例如,在本章的后面部分,我们将看到如何配置基于 Cumulus Linux 平台的交换机,而该平台的支持并未明确列在前述页面上。

部分原因是 Cumulus Linux 和其他广泛的网络技术的支持由Community.Network集合支持。支持的平台和模块列表可以在这里找到:docs.ansible.com/ansible/latest/collections/community/network/

由于 Ansible 文档是自动构建的,将模块分散到集合中在诸如网络等领域有些许颠覆性,并且毫无疑问会随着时间的推移而改进。与此同时,通过一点搜索,您肯定会发现对您的网络平台的支持,因为随着 Ansible 的发展,这种支持一直在扩大。

结果是,由于有如此广泛(并且不断增长)的设备支持范围,网络管理员可以轻松地从一个中心位置管理所有设备,而无需专有工具。然而,好处不仅仅是这些。

配置可移植性

正如我们已经讨论过的,Ansible 代码具有很高的可移植性。在网络自动化领域,这是非常有价值的。首先,这意味着您可以在开发网络(或模拟器)上推出配置更改并进行测试,然后一旦配置被认为已经成功测试,就可以对不同的清单(例如生产清单)使用相同的代码进行配置更改。

然而,好处并不止于此。在软件升级或配置更改出现问题时,网络工程师的挑战是成功地与供应商联系寻求支持和帮助。这需要向供应商发送足够的细节,以使他们至少能够理解问题,并且很可能想要重现它(特别是在固件问题的情况下)。当网络的配置在 Ansible 中定义时,playbooks 本身可以发送给供应商,使他们能够快速准确地理解网络拓扑并诊断问题。我遇到过一些情况,网络供应商现在开始坚持要求在提出支持票时包含网络配置的 Ansible playbooks。这是因为这使他们能够比以往更快地解决问题。

有效使用Ansible Vault确保敏感数据不会出现在主要 playbooks 中,这意味着可以在发送给第三方之前轻松删除它(即使它被意外发送,也不会被读取,因为它在静止状态下是加密的)。

备份,恢复和版本控制

尽管大多数企业都有健全的变更控制程序,但并不能保证这些程序 100%遵守,人类已经知道在不准确记录所做更改的情况下调整配置。将网络配置移至 Ansible 可以消除这个问题,因为配置是由 playbooks 定义的已知状态,可以轻松地使用check运行与运行配置进行比较。

不仅如此,配置也可以轻松地进行备份和恢复。例如,如果交换机故障并且需要更换,如果更换的是相同类型的交换机,可以通过运行与配置其前身相同的 Ansible playbooks 快速配置并投入使用,如果适当的话,playbook 运行可能仅限于替换交换机的清单主机 - 尽管 Ansible 的幂等性意味着在整个网络上运行它应该是无害的。

这也适用于版本控制 - 网络配置 playbooks 可以推送到源代码控制存储库,从而可以跟踪配置版本,并轻松检查随时间的差异。

自动变更请求

通常,可能需要对网络进行微小更改以推出新项目 - 也许是新的 VLAN 或 VXLAN,或者一些以前未使用的端口已被引入服务。配置参数将由变更请求和/或网络设计明确定,可能并不是高素质网络工程师最好的选择来进行简单的配置更改。这些任务通常是例行公事,因为配置更改可以在 Ansible playbook 中进行模板化,传递给它的变量已经由变更请求定义(例如,端口号和 VLAN 成员资格详情)。

这样就可以为工程师节省更多重要工作的时间,例如设计新架构,新产品研究和测试。

结合使用 AWX 或 Ansible Tower 等软件包(正如我们在本书中早期讨论的那样),简单而经过充分测试的更改可以完全自动化,或者通过简单传递所需的参数传递给一线团队来安全执行。通过这种方式,无论执行更改的人的技能水平如何,人为错误的风险都会大大降低。

有了这些好处得到了很好的建立和理解,让我们看看如何开始编写 playbooks 来处理多设备网络。

处理多种设备类型

在一个我们不被锁定在单一供应商的世界中,了解如何处理基础设施中的不同网络设备是很重要的。我们在前一章中已经确定,对于不同的基础设施提供商,已经建立了与 Ansible 交互的类似流程。这在交换机上可能会有所不同,因为并非所有命令行交换机接口都是相同的。例如,在 Cumulus Networks 交换机上,可以利用直接的 SSH 连接,这意味着到目前为止我们在本书中学到的关于连接到支持 SSH 的设备的一切仍然适用。

然而,其他设备,比如 F5 BIG-IP,不使用这样的接口,因此需要从 Ansible 主机上运行模块。配置参数必须直接传递给模块,而不是使用简单的与连接相关的主机变量,比如ansible_user

当然,在这个讨论中还存在一个灰色地带。一些设备,比如 Arista EOS 或基于 Cisco IOS 的设备,将由 SSH 管理,因此你可能会误以为可以像连接其他 Linux 主机一样直接使用 SSH 连接到它们。然而,事实并非如此——如果我们回顾[第一章]《Ansible 的系统架构和设计》,我们了解到,为了让 Ansible 能够通过 SSH 自动化命令,它会发送一小段 Python 代码到远程主机进行执行(或者在 Windows 主机的情况下是 PowerShell)。大多数交换机虽然有基于 SSH 的用户界面,但不能指望在其上有 Python 环境,因此这种操作模式是不可能的(Cumulus Linux 是个例外,因为它具有可用的 Python 环境)。因此,像 Arista EOS 和 Cisco IOS 这样的设备在历史上使用本地执行,即 Ansible 代码在控制节点上运行,然后自动化请求被转换为适当的 CLI(或 API)调用并直接传递给设备。因此,不需要远程 Python 环境。

你会发现许多历史示例使用了这种操作模式,并且它们很容易被识别,因为在 play 定义中会有以下行:

connection: local

可能还可以将其定义为清单变量:

ansible_connection=local 

无论如何,本地连接模式的操作已经被弃用,虽然大多数使用这种连接模式的传统网络 playbook 今天仍然可以运行,但预计明年将停止支持这种模式。

在可能的情况下,鼓励用户使用以下通信协议之一:

  • ansible.netcommon.network_cli:这种协议将 play 任务转换为通过 SSH 的 CLI 命令。

  • ansible.netcommon.netconf:这种协议将 play 任务转换为通过 SSH 发送到设备的 XML 数据,以便由netconf进行配置。

  • ansible.netcommon.httpapi:这种协议使用基于 HTTP 或 HTTPS 的 API 与网络设备通信。

前面三种通信协议都是持久的——也就是说,它们不需要为每个任务建立和拆除网络连接——而本地连接方法不支持这一点,因此比这些模式要低效得多。在前面的列表中,ansible.netcommon.network_cli是你可能会遇到的最常见的,我们将在下一节中讨论这个问题。

我们不希望你们中的许多人能够访问各种各样的网络硬件来在本章的示例中使用。稍后,我们将看两个示例,这些示例可以免费下载(在撰写本文时,取决于您是否愿意分享一些个人信息),如果您愿意,您可以尝试。不过,现在,我们将更详细地介绍首次自动化新网络设备时要采用的流程,以便您知道如何将其应用到特定情况和首选网络供应商。

研究您的模块

与任何网络设备一起工作时的第一个任务是了解您需要使用哪个模块与 Ansible 一起使用。这将涉及两件事:

  • 您希望自动管理哪个设备?

  • 您希望在设备上执行什么任务?

有了这些信息,您可以搜索 Ansible 文档站点和 Ansible Galaxy,以了解您的设备和所需任务是否受支持。比如,例如,您有一个 F5 BIG-IP 设备,并且您想在该设备上保存和加载配置。

快速扫描 Ansible Galaxy 上可用集合的建议是我们应该查看f5networks.f5_modules集合(galaxy.ansible.com/f5networks/f5_modules),并且从中,我们应该查看f5networks.f5_modules.bigip_config模块,这将满足我们的需求。因此,我们可以继续进行模块配置(请参阅下一节),然后围绕此模块编写所需的 playbook。

但是,如果您的设备没有模块会发生什么呢?在这种情况下,您有两种选择。首先,您可以编写一个新的模块,以便 Ansible 执行您需要的任务。这是您可以为社区做出贡献的事情,第十章扩展 Ansible,包含了您开始这项任务所需的所有细节。

或者,如果您想快速启动某些东西,请记住 Ansible 可以在大多数支持的传输方法中发送原始命令。例如,在作者的实验室设置中,他们有一个 TP-Link 托管交换机。没有原生的 Ansible 模块支持这个特定的交换机 - 但是,除了基于 Web 的 GUI 之外,这个交换机还有一个 SSH 管理接口。如果我想快速启动某些东西,我可以使用 Ansible 的ansible.builtin.raw模块通过 SSH 发送原始命令到交换机。当然,这种解决方案缺乏优雅性,并且很难编写幂等的 playbook,但它确实使我能够快速使用 Ansible 和这个设备。

这捕捉了 Ansible 的美丽之处 - 新设备可以轻松管理,而且只需稍加巧思,就可以为社区的利益进行扩展。

配置您的模块

正如我们在本书的前面已经演示了ansible.builtin.raw模块的使用以及扩展 Ansible,我们将继续处理我们找到了一个想要使用的模块的情况。正如您可能已经在本书的一些早期章节中注意到的那样,尽管 Ansible 包含了许多模块,但并非所有模块都可以立即使用。

Ansible 是用 Python 编写的,在大多数情况下,如果有依赖关系,就会有 Python 模块。重要的是要查看文档。例如,我们在前一节中选择的f5networks.f5_modules.bigip_config模块。快速查看文档的Requirements部分显示,如果您使用的是旧于 3.5 版本的 Python,则需要(在撰写本文时)ipaddress Python 模块。

如果您没有运行 Python 3.5 或更高版本,则需要安装此版本以使集合的模块正常运行。有多种安装方法——一些操作系统可能内置了一个本地包,如果可用并且满足版本要求,那么使用它是完全可以的。这可能在供应商支持方面具有优势。但是,如果没有这样的软件包可用,Python 模块可以很容易地使用pip(或pip3)工具进行安装。假设这已经在您的系统上,安装就像使用以下代码一样简单:

sudo pip install ipaddress

还要确保查看文档的Notes部分(对于我们当前讨论的模块,请转到clouddocs.f5.com/products/orchestration/ansible/devel/modules/bigip_config_module.html#notes)。继续这个例子,我们可以看到它只支持 BIG-IP 软件版本 12 及更高版本,因此,如果您使用的是早期版本,您将不得不找到另一种自动化设备的方法(或者如果这是可接受的路径,则升级软件)。

编写您的 playbooks

一旦您的模块已经配置好,并且满足了所有要求(无论是 Python 模块依赖项还是设备软件要求),就该开始编写您的 playbook 了。这应该是一个简单的任务,只需按照模块的文档进行。假设我们想要重置F5 BIG-IP设备上的配置。从文档中,我们可以看到认证参数是传递给模块本身的。此外,示例代码显示了delegate_to任务关键字的使用;这两个线索告诉我们,该模块并没有使用 Ansible 的本地 SSH 传输,而是使用了模块本身定义的传输。因此,重置单个设备配置的 playbook 可能如下所示:

---
- name: reset an F5
  hosts: localhost
  gather_facts: false
  tasks:
    - name: reset my F5
      f5networks.f5_modules.bigip_config:
        reset: yes
        save: yes
        provider:
          server: lb.mastery.example.com
          user: admin
          password: mastery
          validate_certs: no 

在这种情况下,我们正在使用文档中的一个经典示例来重置我们的配置。请注意,由于我们的hosts参数只定义了localhost,所以我们不需要delegate_to关键字,因为在这个 playbook 中,f5networks.f5_modules.bigip_config模块只会从localhost运行。

通过这种方式,我们已经自动化了一个简单的、但否则需要手动和重复执行的任务。运行 playbook 就像执行以下命令一样简单:

ansible-playbook -i mastery-hosts reset-f5.yaml

当然,要测试这个 playbook,您需要有一个 F5 BIG-IP 设备进行测试。并非每个人都有这个设备,因此,在本章的后面,我们将继续演示每个读者都可以使用的真实示例。然而,本章的这一部分旨在为您提供一个关于如何将您的网络设备与 Ansible 集成的坚实概述。因此,希望即使您有一个我们在这里没有提到的设备,您也能理解如何使其工作的基本原理。

使用 cli_command 模块

在我们开始实际的实例之前,我们必须看一下自从上一版书籍出版以来已经成为网络设备配置中心的模块。

正如我们在前一节中讨论的,大多数网络设备不能指望在其上有一个可用的 Python 环境,因此,Ansible 将使用本地执行——也就是说,与网络设备相关的所有任务都在 Ansible 控制节点上执行,转换为设备接收的正确格式(无论是 CLI、基于 HTTP 的 API 还是其他格式),然后通过网络发送到设备。Ansible 2.7 主要依赖于一个名为local的通信协议进行网络设备自动化。这样做效果很好,但也存在一些缺点,包括以下几点:

  • local协议不支持持久的网络连接 - 需要为执行的每个任务建立一个新连接,然后将其拆除。这是极其低效和缓慢的,与 Ansible 最初的愿景完全不符。

  • 每个模块负责自己的通信协议,因此每个模块的库要求通常是不同的,代码也没有被共享。

  • 在提供网络设备通信的方式上几乎没有共同点,并且必须在每个任务中提供凭据,这再次导致了低效的代码。

由于这些问题,预计local协议将在未来一年内从 Ansible 中删除,并鼓励您开始使用本章前面列出的三种新协议中的一种处理多种设备类型。其中,最常见的是ansible.netcommon.network_cli协议,它可以用于连接到您可能希望使用 Ansible 自动化的许多网络设备 - 您可以通过查看提供的表格来看到此模块用于网络设备配置的普遍程度docs.ansible.com/ansible/latest/network/user_guide/platform_index.html#settings-by-platform

这种协议的美妙之处在于现在可以在清单中设置认证参数,就像对任何其他操作系统一样,简化了操作手册并消除了设置重复凭据的需要。还支持持久连接,意味着自动化运行速度更快。那么,它是如何工作的呢?

好吧,假设我们有一个要配置的基于 Cisco IOS 的网络设备。我们可以定义一个看起来像这样的简单清单文件:

[ios_devices]
ios-switch1.example.org
[ios_devices:vars]
ansible_connection: ansible.netcommon.network_cli
ansible_network_os: cisco.ios.ios
ansible_user: admin
ansible_password: password123
ansible_become: yes
ansible_become_method: enable
ansible_become_password: password123

注意这是多么容易?我们设置了与本书中的示例中已经看到的相同的ansible_useransible_passwordansible_become清单变量。但是,我们在这里添加了ansible_connection变量,它告诉 Ansible 使用ansible.netcommon.network_cli协议。当然,这只是故事的一半 - 这个协议告诉 Ansible 通过 SSH 发送 CLI 命令,但并没有告诉 Ansible 连接的另一端是什么类型的设备。由于所有的 CLI 在某种程度上都是不同的,这很重要,因此我们使用ansible_network_os告诉 Ansible 它正在与什么类型的设备交流,以便它可以向设备说出正确的 CLI 语言。

最后,我们需要更改ansible_become方法 - 在 Linux 上,这几乎肯定是sudo,但在 IOS 交换机上,它是enable。我们还需要提供权限提升的密码,就像如果您配置了需要密码的sudo一样。

这就是它的复杂性 - 使用这个清单及其分配的变量的简单操作手册可能如下所示:

---
- name: Simple IOS example playbook
  hosts: all
  gather_facts: no
  tasks:
    - name: Save the running config 
      cisco.ios.ios_config:
        save_when: always

注意这是多么容易 - 现在我们可以以与处理 Linux 或 Windows 主机相同的方式编写操作手册。当然,每个网络平台都有微妙的差异,并且可以在此找到支持的各种设备的特定于平台的选项:docs.ansible.com/ansible/latest/network/user_guide/platform_index.html

使用ansible.netcommon.network_cli协议的另一个好处是,它支持ansible_ssh_common_args清单变量,就像任何其他 SSH 管理的主机(使用 OpenSSH 的 Linux 或 Windows)一样。这很重要,因为许多网络设备是在安全的隔离网络上管理的 - 如果这些访问权限落入错误的手中,可能会造成严重的损害。这意味着这些主机通常使用跳板主机(也称为跳板主机)进行访问。要通过这个跳板运行您的自动化 playbook,您可以将以下内容添加到您的清单变量中:

ansible_ssh_common_args: '-o ProxyCommand="ssh -W %h:%p -q jumphost.example.org"'

前面的示例假设您的堡垒机或跳板主机的主机名为jumphost.example.org,并且您已经从 Ansible 控制节点为其设置了基于密钥的无密码 SSH 访问。当然,还有其他方法可以与这个堡垒主机进行身份验证,这留给您去探索。

当然,这只是一个例子,而基于 Cisco IOS 的设备并不是每个阅读本书的人都能接触到的东西。然而,在撰写本文时,您可以轻松且免费地通过在其网站上注册免费帐户并转到www.arista.com/en/support/software-download来下载 Arista vEOS 镜像。从这里,您可以将这些镜像加载到网络模拟工具(如 GNS3)中,并自行尝试使用 Ansible 进行网络自动化,而无需访问任何昂贵的硬件。我们将在下一节中讨论这个问题。

使用 Ansible 配置 Arista EOS 交换机

使用 Arista 交换机(或虚拟交换机)进行启动和运行是您的任务,但是如果您有兴趣在 GNS3 中进行此操作,GNS3 是一个流行且免费的开源工具,用于学习网络知识,这里有一些很好的指导:gns3.com/marketplace/appliances/arista-veos

您可能很幸运,手边有一台基于 Arista EOS 的设备,这也没关系 - 本节中的自动化代码在任何情况下都能很好地工作。

以下示例是针对 GNS3 中的 Arista vEOS 设备创建的,使用了上述链接中找到的说明。首次启动设备时,您需要取消 ZeroTouch 配置。要做到这一点,请使用admin用户名登录(默认情况下密码为空),并输入以下命令:

zerotouch cancel

虚拟设备将重新启动,当它再次启动时,使用相同的凭据登录。输入以下命令进入特权用户模式:

enable

请注意,这是我们在之前的 Cisco IOS 示例中使用的ansible_become方法,我们将很快再次使用相同的方法。现在,使用以下命令进入配置模式:

configure terminal

如果 vEOS 设备默认密码为空,则无法通过 SSH 管理该设备,因此我们将使用以下命令为我们的虚拟设备设置一个简单的密码:

username admin secret admin

这将把admin用户的密码设置为admin。接下来(假设您已经将 vEOS 设备的管理接口连接到虚拟网络),您需要启用此接口并为其分配一个有效的 IP 地址。确切的 IP 地址将取决于您的测试网络,但实现此目的的命令如下所示:

interface management 1
no shutdown
ip address 10.0.50.99/8

最后,退出配置模式并将配置写入交换机,以便在下次重新启动时再次启动:

end
write

就是这样 - 您的 vEOS 设备现在已准备好使用 Ansible 进行管理!

有了这个配置,现在您可以为测试交换机定义清单。我创建了我的清单如下(基于前面的配置):

[eos]
mastery-eos ansible_host=10.0.50.99
[eos:vars]
ansible_connection=ansible.netcommon.network_cli
ansible_network_os=arista.eos.eos
ansible_user=admin
ansible_password=admin
ansible_become=yes
ansible_become_method=enable

注意这与我们在上一节中创建的基于 Cisco IOS 的示例清单有多相似?这是ansible.netcommon.network_cli协议的伟大之处之一 - 当使用此协议时,您的所有代码编写起来都更容易。当然,与本书中大多数示例一样,您不会将您的管理密码明文留在外面,但这有助于保持示例简单和简洁,鼓励您探索使用 Ansible Vault 来安全存储它们。

从这里,我们可以开发一个简单的 playbook 来演示针对我们的虚拟交换机的命令自动化。让我们选择一个简单的任务 - 我们将确保交换机上的Ethernet1接口是启用的,给它一个有意义的名称,然后将配置写入交换机,以便在重新启动后保持不变。实现这一点的 playbook 可能看起来像这样:

---
- name: A simple play to enable Ethernet1 on our virtual switch and write the config
  hosts: all
  gather_facts: no
  tasks:
    - name: Enable Ethernet1 on the switch
      arista.eos.eos_interfaces:
        config:
        - name: Ethernet1
          enabled: yes
          description: Managed by Ansible
        state: replaced
    - name: Write the config to flash if it has been modified
      arista.eos.eos_config:
        save_when: modified

您可以以您习惯的方式运行此 playbook。如果您正在运行本书附带的示例代码,那么命令将如下所示:

ansible-playbook -i mastery-nethosts eosconfig.yml

当您对交换机运行此命令时,您应该看到类似以下的 Ansible 输出:

图 13.1 - 使用 Ansible 配置 Arista vEOS 设备

图 13.1 - 使用 Ansible 配置 Arista vEOS 设备

现在,当然,由于我们正在使用 Ansible 进行此配置更改,我们期望更改是幂等的 - 我们应该能够再次运行相同的 playbook,而不会发生任何破坏性的事情。如果您再次运行 playbook,输出应该如下所示:

图 13.2 - 再次运行相同的 playbook 以演示幂等性

图 13.2 - 再次运行相同的 playbook 以演示幂等性

通过绿色的ok任务状态,可以看到这个 playbook 第二次成功运行,并且这一次没有对交换机配置进行任何更改。

如果您愿意,您可以通过直接 SSH 到交换机并执行以下命令来验证我们 playbook 运行的结果:

enable
show running-config

从这里,您应该看到类似以下的内容:

图 13.3 - 手动查询我们的 vEOS 设备的配置

图 13.3 - 手动查询我们的 vEOS 设备的配置

在这里,我们可以看到Ethernet1接口具有我们在 playbook 中设置的描述,并且没有禁用它的指令,因此确保它是启用的。

就是这样 - 通过完成这个示例,您刚刚在 Ansible 中执行了您的第一个真实的网络设备自动化!希望这能向您展示,特别是现在,鉴于ansible.netcommon.network_cli协议的出现,实现您想要的配置非常容易和快速。大多数支持此协议的设备将类似工作,如果您对此感兴趣,鼓励您进一步探索。但是,如果我们想要使用另一台设备怎么办?好吧,Cumulus Linux(现在由 NVIDIA 拥有)是一个网络设备的开源操作系统,可以在白盒硬件上运行 - 也就是说,它不是专有于任何特定的硬件。幸运的是,您可以免费下载 Cumulus VX 的副本,这是他们交换机操作系统的虚拟版本,以进行实验。我们将在下一节中看看 Ansible 如何自动化这个网络平台。

使用 Ansible 配置 Cumulus Networks 交换机

Cumulus Linux(由 NVIDIA 收购的 Cumulus Networks 创建)是一种开源网络操作系统,可以在各种裸机交换机上运行,为数据中心网络提供了开源方法。这是网络设计的重大进步,也是从过去专有模型的重大转变。他们提供了一个免费版本的软件,可以在您选择的 hypervisor 上运行,用于测试和评估,称为 Cumulus VX。本节中的示例基于 Cumulus VX 版本 4.4.0。

定义我们的清单

快速的研究显示,Cumulus VX 将使用 Ansible 的标准 SSH 传输方法。由于它是专门设计用于在交换机硬件上运行的 Linux 发行版,因此它能够以远程执行模式运行,因此不需要ansible.netcommon.network_cli协议。此外,仅为使用该系统而定义了一个模块,network.cumulus.nclu,它是community.network集合的一部分(galaxy.ansible.com/community/network)。使用此模块不需要先决条件模块,因此我们可以直接定义我们的清单。

默认情况下,Cumulus VX 启动时使用已配置为使用 DHCP 获取 IP 地址的管理接口。根据您的运行方式,您可能会发现它还有其他三个虚拟交换机端口供我们测试和配置,尽管如果您将其集成到诸如 GNS3 之类的工具中,您将发现可以轻松重新配置可用于您的虚拟交换机端口的数量。

如果您运行的 Cumulus Linux 版本旧于 3.7,您会发现镜像已经设置了默认登录凭据。因此,只要您确定了虚拟交换机的 IP 地址,您可以创建一个简单的清单,例如以下清单,它使用默认用户名和密码:

[cumulus]
mastery-switch1 ansible_host=10.0.50.110
[cumulus:vars]
ansible_user=cumulus
ansible_ssh_pass=CumulusLinux!

Cumulus Linux 的新版本,例如 4.4.0 - 在撰写本文时可用的最新版本,并用于本节示例 - 需要您在首次启动交换机时设置密码。如果您使用此版本,您需要首次启动虚拟交换机,然后使用默认用户名cumulus和默认密码cumulus登录。然后,您将被提示更改密码。

完成这些步骤后,您就可以开始自动化交换机配置。您可以使用我们之前定义的清单,并简单地将ansible_ssh_pass的值替换为您设置的密码。

请注意以下内容:

  • ansible_host中指定的 IP 地址几乎肯定与我的不同 - 请确保将其更改为 Cumulus VX 虚拟机的正确值。您可能需要登录 VM 控制台以获取 IP 地址,例如使用ip addr show命令。

  • 再次强调,您绝对不会在清晰文本中放置密码在清单文件中 - 但是,为了简单起见并节省时间,我们将在这里指定默认密码。在实际用例中,始终使用保险库,或者设置基于密钥的 SSH 身份验证。

定义了清单后,让我们使用临时命令使用ping模块测试连接,如下所示:

ansible -i switch-inventory -m ansible.builtin.ping all

如果一切设置正确,您应该收到以下输出:

图 13.4 - 检查 Ansible 与我们的虚拟 Cumulus Linux 交换机的连接

图 13.4 - 检查 Ansible 与我们的虚拟 Cumulus Linux 交换机的连接

正如我们在第一章中讨论的那样,Ansible 的系统架构和设计ansible.builtin.ping模块执行完整的端到端连接测试,包括在传输层进行身份验证。因此,如果您收到了像之前显示的成功测试结果,我们可以有信心继续编写我们的第一个 playbook。

实际例子

Cumulus VX 镜像完全未配置(除了eth0管理端口上的 DHCP 客户端配置)。根据您下载的版本,它可能有三个标记为swp1swp2swp3的交换机端口。让我们查询其中一个接口,看看是否有任何现有配置。我们可以使用一个名为switch-query.yaml的简单 playbook 来查询swp1

---
- name: query switch
  hosts: mastery-switch1
  tasks:
  - name: query swp1 interface
    community.network.nclu:
      commands:
        - show interface swp1
    register: interface
  - name: print interface status
    ansible.builtin.debug:
      var: interface

现在,假设我们使用以下命令运行:

ansible-playbook -i switch-inventory switch-query.yaml

我们应该看到类似以下的内容:

图 13.5 - 使用 Ansible 在 Cumulus Linux 上查询交换机端口的默认设置

图 13.5 - 使用 Ansible 查询 Cumulus Linux 交换机端口的默认值

这证实了我们对 VM 镜像的初始说法 - 我们可以看到交换机端口没有配置。使用 Ansible 和community.network.nclu模块将这个 VM 变成一个简单的二层交换机非常容易。下面的 playbook,名为switch-l2-configure.yaml,就是这样做的:

---
- name: configure switch
  hosts: mastery-switch1
  tasks:
  - name: bring up ports swp[1-3]
    community.network.nclu:
      template: |
        {% for interface in range(1,3) %}
        add interface swp{{interface}}
        add bridge ports swp{{interface}}
        {% endfor %}
      commit: true
  - name: query swp1 interface
    community.network.nclu:
      commands:
        - show interface swp1
    register: interface
  - name: print interface status
    ansible.builtin.debug:
      var: interface

请注意,我们正在使用一些巧妙的内联 Jinja2 模板来在三个接口上运行for循环,省去了创建重复和繁琐代码的需要。这些命令启动了三个交换机接口,并将它们添加到默认的二层桥接中。

这也展示了 Ansible 中可用的各种网络模块之间的差异。在前一节中,我们配置基于 EOS 的交换机时,有许多不同的模块可供使用,每个模块都有不同的目的 - 例如配置接口、配置路由和配置 VLAN。相比之下,基于 Cumulus Linux 的交换机只有一个模块:community.network.nclu。这并不是问题,但由于我们通过一个模块发送所有配置命令,利用 Jinja2 模板(可以支持诸如for循环之类的结构)对我们很有帮助。

最后,commit: true行立即将这些配置应用到交换机上。现在,让我们用以下命令运行它:

ansible-playbook -i switch-inventory switch-l2-configure.yaml

此时,我们将看到swp1的不同状态,如下所示:

图 13.6 - 使用 Ansible 成功配置我们的 Cumulus Linux 虚拟交换机

图 13.6 - 使用 Ansible 成功配置我们的 Cumulus Linux 虚拟交换机

正如我们所看到的,swp1接口现在已经启动并加入了桥接,准备好进行交换流量。然而,如果您仔细观察前面的截图,您会发现bring up ports swp[1-3]任务的状态是ok,而不是changed。然而,我们可以从switch查询结果中看到配置已经改变。这似乎是community.network集合版本3.0.0中的一个 bug,并已经被作者提出。希望这个集合的新版本能够正确显示当 Cumulus 交换机的配置发生变化时的改变状态。

由于我们正在查询端口的状态,我们仍然可以再次运行 playbook 来测试幂等性。让我们看看如果我们在交换机上不执行任何其他步骤的情况下再次运行 playbook 会发生什么:

图 13.7 - 测试我们的 playbook 对 Cumulus Linux 虚拟交换机的幂等性

图 13.7 - 测试我们的 playbook 对 Cumulus Linux 虚拟交换机的幂等性

这一次,这个任务的状态仍然是ok,但我们可以看到接口状态查询结果是相同的,显示我们的配置已经持久化,并且没有以任何不利的方式被修改(如果不支持幂等性,可能会发生这种情况)。通过这种方式,自动化配置我们的 Cumulus Linux 交换机的 playbook 是幂等的,并且在多次运行时会产生一致的状态。这也意味着,如果交换机的配置漂移(例如由于用户干预),很容易看出发生了变化。不幸的是,community.network.nclu模块目前不支持ansible-playbookcheck模式,但它仍然提供了一种强大的方式来配置和管理您的交换机。

使用 Ansible 自动化运行 Arista EOS 和 Cumulus Linux 等网络硬件就是这么简单-只需想象一下使用这样的组合来自动化您的网络配置!我鼓励您探索这些免费工具,以了解更多关于网络自动化的知识;我相信您很快就会看到它的价值。希望即使在这些简单的示例中,您也能看到使用 Ansible 自动化网络基础设施现在不再比自动化基础设施中的任何其他内容更困难。

最佳实践

使用 Ansible 自动化网络设备时,所有通常的最佳实践都适用。例如,永远不要以明文存储密码,并在适当的情况下使用ansible-vault。尽管如此,当涉及到 Ansible 时,网络设备是其自己特殊的设备类别,并且对它们的支持从 Ansible 2.5 版本开始蓬勃发展。因此,在使用 Ansible 进行网络自动化时,有一些特殊的最佳实践值得一提。

清单

在组织网络基础设施和特别关注分组时,充分利用 Ansible 支持的库存结构。这样做将使您的 playbook 开发变得更加容易。例如,假设您的网络上有两个交换机-一个是 Cumulus Linux 交换机,正如我们之前所检查的,另一个是基于 Arista EOS 的设备。您的清单可能如下所示:

[switches:children]
eos
cumulus
[eos]
mastery-eos ansible_host=10.0.50.99
[cumulus]
mastery-switch1 ansible_host=10.0.50.110

我们知道我们不能在除 Cumulus 交换机以外的任何设备上运行community.network.nclu模块,因此,通过谨慎使用when语句,我们可以在 playbooks 中构建任务,以确保我们在正确的设备上运行正确的命令。以下是一个只在我们在前面的清单中定义的cumulus组中的设备上运行的任务:

  - name: query swp1 interface
    community.network.nclu:
      commands:
        - show interface swp1
    register: interface
    when: inventory_hostname in groups['cumulus']

同样,良好的分组使用使我们能够根据设备设置变量。尽管您不会将密码明文放入库存中,但也许您的给定类型的交换机都使用相同的用户名(例如,在 Cumulus Linux 设备的情况下为cumulus)。或者,也许您的 EOS 设备需要设置特定的 Ansible 主机变量以使连接工作并实现所需的特权升级来执行配置。因此,我们可以通过添加以下代码来扩展我们之前的清单示例:

[cumulus:vars]
ansible_user=cumulus
ansible_password=password
[eos:vars]
ansible_connection=ansible.netcommon.network_cli
ansible_network_os=arista.eos.eos
ansible_user=admin
ansible_password=admin
ansible_become=yes
ansible_become_method=enable

良好的库存结构和变量定义将使您的 playbooks 开发变得更加容易,结果代码将更易于管理和操作。

收集事实

Ansible 包括几个专门的收集事实模块用于网络设备,这些模块可能对运行条件任务或简单地报告有关设备的数据非常有用。如果您正在使用较旧的基于local连接的协议,则这些特定于设备的事实模块不能在 playbook 运行开始时运行,因为在这个阶段,Ansible 不知道它正在与什么类型的设备通信。因此,我们必须告诉它根据需要收集每个设备的事实。使用本章前面描述的ansible.netcommon.network_cli协议解决了这个问题,因为 Ansible 从清单中知道它正在与什么类型的设备交流。

即便如此,有时手动收集设备的事实是有用的-也许是为了验证作为较大 playbook 的一部分执行的一些配置工作。无论您是这样做,还是出于传统原因,仍然依赖于local连接的协议,您都需要意识到对于不同的连接类型,您将使用不同的事实收集模块。让我们扩展我们的 Arista EOS 和 Cumulus Linux 示例来看看这一点。

Cumulus Linux 交换机没有专门的事实模块(尽管它们基于 Linux,仍然可以收集标准主机事实)。以我们的 Arista EOS 设备为例,我们将在基于清单中的唯一密钥运行arista.eos.eos_facts模块的 playbook 中。在我们在上一节中定义的示例清单中,我们的 Arista EOS 交换机在eos组中,并且ansible_network_os也设置为arista.eos.eos。我们可以在when语句中使用其中任何一个作为条件来在我们的交换机上运行arista.eos.eos_facts模块。因此,我们的 playbook 开头可能是这样的:

---
- name: "gather all device facts"
  hosts: all
  gather_facts: false
  tasks:
  - name: gather eos facts
    arista.eos.eos_facts:
    when: ansible_network_os is defined and ansible_network_os == 'arista.eos.eos'
  - name: gather cumulus facts
    ansible.builtin.setup:
    when: inventory_hostname in groups['cumulus'] 

请注意,在此 playbook 的开头将gather_facts设置为false。如果您使用基于local连接的协议,您需要像我们之前讨论的那样这样做-否则,使用ansible.netcommon.network_cli,您可以在 play 开始时收集事实(显然,在这个例子中是多余的!)。

您还会注意到我们在 Arista EOS 设备上使用了更复杂的条件。由于 Cumulus Linux 交换机使用与 Linux 主机相同的基于 SSH 的传输,它们不需要(或者确实没有)设置ansible_network_os,如果我们尝试在条件中测试这个变量,将会产生变量未定义的错误,因此对于没有定义变量的主机(在这种情况下是我们的 Cumulus Linux 交换机),将不会尝试任何后续任务。这显然不是我们想要的结果!因此,在将这些主机与同一 play 中的其他网络设备组合时,我们必须始终检查ansible_network_os变量是否定义,然后再尝试执行任何查询,就像我们在前面的示例中所做的那样。

如果您按照本章中的示例并设置了虚拟的 Arista EOS 和基于 Cumulus Linux 的设备,您可以使用以下命令运行此 playbook:

ansible-playbook -i switch-inventory switch-facts.yaml

成功运行 playbook 的输出应该是这样的:

图 13.8-使用单个 playbook 收集多种设备类型的事实

图 13.8-使用单个 playbook 收集多种设备类型的事实

在这里,您可以看到我们的不同事实模块是如何在适当的设备类型上运行的,这要归功于我们在 playbook 中使用的条件。您可以使用本章的这一部分中概述的技术来通过推断我们一起完成的工作来构建更复杂的多设备设置。然而,没有关于网络自动化的章节会完整无缺,没有更多关于跳板主机的细节。我们将在下一节中讨论这些。

跳板主机

最后,关于跳板主机的一点说明。出于重要的安全原因,网络设备通常位于某种跳板或跳转主机后面。根据底层网络传输,Ansible 提供了几种机制来实现这一点。例如,SSH 连接(例如 Cumulus Linux 交换机)可以利用 SSH 的代理命令功能。有几种方法可以实现这些,但最简单的方法是将另一个组变量添加到清单中。例如,如果我们只能通过名为bastion01.example.com的主机访问我们的 Cumulus Linux 交换机,并且使用名为jfreeman的账户进行身份验证,我们的清单变量部分将如下所示:

[cumulus:vars]
ansible_user=cumulus
ansible_ssh_pass=CumulusLinux!
ansible_ssh_common_args='-o ProxyCommand="ssh -W %h:%p -q jfreeman@bastion01.example.com"'

上述代理命令假定无密码身份验证已经配置并且在bastion01.example.com上的jfreeman账户上正常工作,并且 SSH 主机密钥已经被接受。未能完成这些任务将导致错误。

像这样的 SSH 代理命令也适用于用于网络设备管理的其他ansible_connection模式,包括ansible.netcommon.netconfansible.netcommon.network_cli,支持使用跳板主机处理各种网络设备。与往常一样,确保处理特定类型的连接的最佳方法是查阅特定网络设备的文档,并遵循其中的指导。

如果我们重复之前的示例来查询 Cumulus Linux 交换机的swp1接口,我们会发现(在正确设置了堡垒主机的情况下),playbook 的工作方式与本章早期完全相同,并且不需要进一步的步骤或代码更改:

图 13.9 - 运行之前的示例 playbook,但这次通过预先配置的堡垒主机

图 13.9 - 运行之前的示例 playbook,但这次通过预先配置的堡垒主机

这也是为什么 Ansible 变得如此受欢迎的另一个原因 - 无需设置特殊的代理应用程序或服务器来访问隔离网络。使用标准的 SSH 协议,它可以通过网络上配置了 SSH 的任何安全主机进行连接。

这标志着我们对使用 Ansible 进行网络自动化的探索的结束。您可以自动化配置的设备数量和范围仅受您的想象力限制,我希望本章能够帮助您在这一重要领域建立坚实的基础,并给您探索更多的信心。

总结

随着越来越多的基础设施被代码定义和管理,通过 Ansible 有效地自动化网络层变得更加重要。自上一版书籍发布以来,Ansible 在这一领域做了大量工作,特别是在发布 Ansible 2.5 之后。通过这些进步,现在可以轻松地构建 playbook 来自动化网络任务,从简单的设备更改到通过 Ansible 部署整个网络架构。所有与代码重用、可移植性等相关的 Ansible 好处都适用于管理网络设备的人员。

在本章中,您了解了 Ansible 如何实现网络管理。您学习了处理基础设施中不同设备类型的有效策略,以及如何为它们编写 playbook,然后通过一些特定的 Arista EOS 和 Cumulus Linux 示例进行了扩展。最后,您了解了在使用 Ansible 管理网络基础设施时必须应用的一些最佳实践。

这就是本书的结尾。感谢您加入我一起探索 Ansible 的旅程,希望您会觉得受益。我相信您现在应该了解了使用 Ansible 管理一切的策略和工具,从小的配置更改到整个基础设施部署,祝您在这个重要且不断发展的技术领域好运。

问题

回答以下问题,测试您对本章的了解:

  1. Ansible 将自动化的所有好处从基础设施管理带入到网络设备管理的世界中。

a) 正确

b) 错误

  1. 第一次使用新的网络设备类型时,您应该始终做什么?

a) 对设备执行出厂重置。

b) 查阅 Ansible 文档,了解支持它的集合和模块,以及这些模块的要求。

c) 使用ansible.netcommon.network_cli连接协议。

d) 使用本地连接协议。

  1. Ansible 描述的执行类型是指在远程主机上直接运行其自动化代码?

a) 远程执行

b) 本地执行

  1. Ansible 描述的执行类型是指在控制节点上运行其自动化代码,然后通过预选通道(例如 SSH 或基于 HTTP 的 API)发送所需的数据?

a) 远程执行

b) 本地执行

  1. 哪种连接协议(在大多数情况下)已经取代了旧的基于本地连接的网络设备协议?

a) ansible.netcommon.netconf

b) ansible.netcommon.httpapi

c) ansible.netcommon.network_cli

d) 本地

  1. 你能在一场比赛开始时收集关于基于 Arista EOS 设备的事实吗?

a) 是的。

b) 不。

c) 是的,但只有在使用ansible.netcommon.network_cli协议时。

  1. Arista EOS 上的所有网络配置都是使用单个模块执行的。

a) 正确

b) 错误

  1. Cumulus Linux 不需要ansible.netcommon.network_cli协议,因为哪个原因?

a) 它不是一个网络操作系统。

b) 它包含完整的 Linux 实现,包括 Python。

c) 它使用 SSH 协议进行管理。

d) 它没有 CLI。

  1. 良好的库存管理在处理多种设备类型的网络时尤为重要。

a) 正确

b) 错误

  1. Ansible 可以支持跳板主机或跳转主机的使用,而无需特殊配置或软件安装。

a) 正确

b) 错误

posted @ 2024-05-20 12:04  绝不原创的飞龙  阅读(15)  评论(0编辑  收藏  举报