Ansible-2-实战-全-

Ansible 2 实战(全)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

欢迎阅读Practical Ansible 2,本书将指导您从初学者变成熟练的 Ansible 自动化工程师。本书将为您提供执行第一个安装和自动化任务所需的知识和技能,并带您从执行单个任务的简单一行自动化命令,一直到编写自己的复杂自定义代码来扩展 Ansible 的功能,并自动化云和容器基础设施。本书将提供实际示例,让您不仅可以阅读有关 Ansible 自动化的内容,还可以自己尝试并理解代码的工作原理。然后,您将能够以可扩展、可重复和可靠的方式使用 Ansible 自动化您的基础设施。

这本书适合谁

本书适用于希望自动化 IT 任务的任何人,从日常琐事到基于复杂基础设施即代码的部署。它旨在吸引任何有 Linux 环境先前经验的人,他们希望快速掌握 Ansible 自动化,并吸引各种人群,从系统管理员到 DevOps 工程师,再到考虑整体自动化策略的架构师。它甚至可以为爱好者提供帮助。假设您具有 Linux 系统管理和维护任务的基本熟练程度;但不需要有先前的 Ansible 或自动化经验。

为了充分利用本书

本书的所有章节都假设您至少可以访问一台运行较新 Linux 发行版的 Linux 机器。本书中的所有示例都在 CentOS 7 和 Ubuntu Server 18.04 上进行了测试,但几乎可以在任何其他主流发行版上运行。您还需要在至少一台测试机器上安装 Ansible 2.9——安装步骤将在第一章中介绍。较新版本的 Ansible 也应该可以工作,尽管可能会有一些细微差异,您应该参考较新版本的 Ansible 的发布说明和移植指南。最后一章还将带您完成 AWX 的安装,但这需要一台安装了 Ansible 的 Linux 服务器。大多数示例演示了跨多个主机的自动化,如果您有更多的 Linux 主机可用,您将能够更好地利用这些示例;但是,它们可以根据您的需求进行扩展或缩减。拥有更多主机并非强制要求,但可以让您更好地利用本书。

书中涉及的软件/硬件 操作系统要求
至少一个 Linux 服务器(虚拟机或物理机) CentOS 7 或 Ubuntu Server 18.04,尽管其他主流发行版(包括这些操作系统的更新版本)也应该可以工作。
Ansible 2.9 如上所述
AWX 发布 10.0.0 或更高版本 如上所述

如果您使用的是本书的数字版本,我们建议您自己输入代码或通过 GitHub 存储库访问代码(链接在下一节中提供)。这样做将帮助您避免与复制和粘贴代码相关的潜在错误。

下载示例代码文件

您可以从您的www.packt.com账户下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support注册,将文件直接发送到您的邮箱。

您可以按照以下步骤下载代码文件:

  1. 登录或注册www.packt.com

  2. 选择支持选项卡。

  3. 点击代码下载。

  4. 在搜索框中输入书名,按照屏幕上的指示操作。

下载文件后,请确保使用最新版本的解压缩软件解压缩文件夹:

  • WinRAR/7-Zip 适用于 Windows

  • Zipeg/iZip/UnRarX 适用于 Mac

  • 7-Zip/PeaZip 适用于 Linux

该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Practical-Ansible-2。 如果代码有更新,将在现有的 GitHub 存储库上进行更新。

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

下载彩色图像

我们还提供了一份 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。 您可以在这里下载:www.packtpub.com/sites/default/files/downloads/9781789807462_ColorImages.pdf

使用的约定

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

CodeInText:表示文本中的代码词,数据库表名,文件夹名,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 句柄。 例如:“将下载的WebStorm-10*.dmg磁盘映像文件挂载为系统中的另一个磁盘。”

代码块设置如下:

html, body, #map {
 height: 100%; 
 margin: 0;
 padding: 0
}

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

[default]
exten => s,1,Dial(Zap/1|30)
exten => s,2,Voicemail(u100)
exten => s,102,Voicemail(b100)
exten => i,1,Voicemail(s0)

任何命令行输入或输出都以以下形式编写:

$ mkdir css
$ cd css

粗体:表示新术语,重要单词或屏幕上看到的单词。 例如,菜单或对话框中的单词会在文本中显示为这样。 例如:“从管理面板中选择系统信息。”

警告或重要说明会以这种方式出现。提示和技巧会以这种方式出现。

第一部分:学习 Ansible 的基本原理

在本节中,我们将看一下 Ansible 的基本原理。我们将从安装 Ansible 的过程开始,然后我们将掌握基本原理,包括语言的基础知识和临时命令。然后我们将探索 Ansible 清单,然后看看如何编写我们的第一个 playbooks 和 roles 来完成多阶段自动化任务。

本节包括以下章节:

  • 第一章,使用 Ansible 入门

  • 第二章,理解 Ansible 的基本原理

  • 第三章,定义您的清单

  • 第四章,playbooks 和 roles

第一章:开始使用 Ansible

Ansible 使您能够使用诸如 SSH 和 WinRM 等本机通信协议轻松一致和可重复地部署应用程序和系统。也许最重要的是,Ansible 是无代理的,因此在受管系统上不需要安装任何东西(除了 Python,这些天大多数系统都有)。因此,它使您能够为您的环境构建一个简单而强大的自动化平台。

安装 Ansible 简单直接,并且适用于大多数现代系统。它的架构是无服务器和无代理的,因此占用空间很小。您可以选择从中央服务器或您自己的笔记本电脑上运行它——完全取决于您。您可以从一个 Ansible 控制机器管理单个主机到数十万个远程主机。所有远程机器都可以(通过编写足够的 playbooks)由 Ansible 管理,并且一切创建正确的话,您可能再也不需要单独登录这些机器了。

在本章中,我们将开始教授您实际技能,涵盖 Ansible 的基本原理,从如何在各种操作系统上安装 Ansible 开始。然后,我们将看看如何配置 Windows 主机以使其能够通过 Ansible 自动化进行管理,然后深入探讨 Ansible 如何连接到其目标主机的主题。然后我们将看看节点要求以及如何验证您的 Ansible 安装,最后看看如何获取和运行最新的 Ansible 源代码,如果您希望为其开发做出贡献或获得最新的功能。

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

  • 安装和配置 Ansible

  • 了解您的 Ansible 安装

  • 从源代码运行与预构建的 RPM 包

技术要求

Ansible 有一组相当简单的系统要求——因此,您应该会发现,如果您有一台能够运行 Python 的机器(无论是笔记本电脑、服务器还是虚拟机),那么您就可以在上面运行 Ansible。在本章的后面,我们将演示在各种操作系统上安装 Ansible 的方法,因此您可以决定哪些操作系统适合您。

前述声明的唯一例外是 Microsoft Windows——尽管 Windows 有 Python 环境可用,但目前还没有 Windows 的原生构建。运行更高版本 Windows 的读者可以使用 Windows 子系统来安装 Ansible(以下简称 WSL),并按照后面为所选的 WSL 环境(例如,如果您在 WSL 上安装了 Ubuntu,则应该简单地按照本章中为 Ubuntu 安装 Ansible 的说明进行操作)的程序进行操作。

安装和配置 Ansible

Ansible 是用 Python 编写的,因此可以在各种系统上运行。这包括大多数流行的 Linux、FreeBSD 和 macOS 版本。唯一的例外是 Windows,尽管存在原生的 Python 发行版,但目前还没有原生的 Ansible 构建。因此,在撰写本文时,您最好的选择是在 WSL 下安装 Ansible,就像在本机 Linux 主机上运行一样。

一旦您确定了要运行 Ansible 的系统,安装过程通常是简单直接的。在接下来的章节中,我们将讨论如何在各种不同的系统上安装 Ansible,因此大多数读者应该能够在几分钟内开始使用 Ansible。

在 Linux 和 FreeBSD 上安装 Ansible

Ansible 的发布周期通常约为四个月,在这个短暂的发布周期内,通常会有许多变化,从较小的错误修复到较大的错误修复,到新功能,甚至有时对语言进行根本性的更改。不仅可以使用本地包来快速上手并保持最新状态,而且可以使用本地包来保持最新状态。

例如,如果你希望在诸如 CentOS、Fedora、Red Hat Enterprise Linux(RHEL)、Debian 和 Ubuntu 等 Linux 发行版上运行最新版本的 Ansible,我强烈建议你使用操作系统包管理器,如基于 Red Hat 的发行版上的yum或基于 Debian 的发行版上的apt。这样,每当你更新操作系统时,你也会同时更新 Ansible。

当然,可能是因为你需要保留特定版本的 Ansible 以用于特定目的——也许是因为你的 playbooks 已经经过了测试。在这种情况下,你几乎肯定会选择另一种安装方法,但这超出了本书的范围。此外,建议在可能的情况下,按照记录的最佳实践创建和维护你的 playbooks,这应该意味着它们能够在大多数 Ansible 升级中生存下来。

以下是一些示例,展示了如何在几种 Linux 发行版上安装 Ansible:

  • 在 Ubuntu 上安装 Ansible:要在 Ubuntu 上安装最新版本的 Ansible 控制机,apt包装工具使用以下命令很容易:
$ sudo apt-get update 
$ sudo apt-get install software-properties-common 
$ sudo apt-add-repository --yes --update ppa:ansible/ansible 
$ sudo apt-get install ansible

如果你正在运行较旧版本的 Ubuntu,你可能需要用python-software-properties替换software-properties-common

  • 在 Debian 上安装 Ansible:你应该将以下行添加到你的/etc/apt/sources.list文件中:
deb http://ppa.launchpad.net/ansible/ansible/ubuntu trusty main

你会注意到在上述配置行中出现了ubuntu一词,以及trusty,这是一个 Ubuntu 版本。在撰写本文时,Debian 版本的 Ansible 是从 Ubuntu 的 Ansible 仓库中获取的,并且可以正常工作。你可能需要根据你的 Debian 版本更改上述配置中的版本字符串,但对于大多数常见用例,这里引用的行就足够了。

完成后,你可以按以下方式在 Debian 上安装 Ansible:

$ sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 93C4A3FD7BB9C367 
$ sudo apt-get update 
$ sudo apt-get install ansible
  • 在 Gentoo 上安装 Ansible:要在 Gentoo 上安装最新版本的 Ansible 控制机,portage包管理器使用以下命令很容易:
$ echo 'app-admin/ansible' >> /etc/portage/package.accept_keywords
$ emerge -av app-admin/ansible
  • 在 FreeBSD 上安装 Ansible:要在 FreeBSD 上安装最新版本的 Ansible 控制机,PKG 管理器使用以下命令很容易:
$ sudo pkg install py36-ansible
$ sudo make -C /usr/ports/sysutils/ansible install
  • 在 Fedora 上安装 Ansible:要在 Fedora 上安装最新版本的 Ansible 控制机,dnf包管理器使用以下命令很容易:
$ sudo dnf -y install ansible
  • 在 CentOS 上安装 Ansible:要在 CentOS 或 RHEL 上安装最新版本的 Ansible 控制机,yum包管理器使用以下命令很容易:
$ sudo yum install epel-release
$ sudo yum -y install ansible

如果你在 RHEL 上执行上述命令,你必须确保 Ansible 仓库已启用。如果没有,你需要使用以下命令启用相关仓库:

$ sudo subscription-manager repos --enable rhel-7-server-ansible-2.9-rpms
  • 在 Arch Linux 上安装 Ansible:要在 Arch Linux 上安装最新版本的 Ansible 控制机,pacman包管理器使用以下命令很容易:
$ pacman -S ansible

一旦你在你使用的特定 Linux 发行版上安装了 Ansible,你就可以开始探索。让我们从一个简单的例子开始——当你运行ansible命令时,你会看到类似以下的输出:

$ ansible --version
ansible 2.9.6
 config file = /etc/ansible/ansible.cfg
 configured module search path = [u'/home/jamesf_local/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
 ansible python module location = /usr/lib/python2.7/dist-packages/ansible
 executable location = /usr/bin/ansible
 python version = 2.7.17 (default, Nov 7 2019, 10:07:09) [GCC 9.2.1 20191008]

那些希望测试来自 GitHub 最新版本的 Ansible 的人可能对构建 RPM 软件包以安装到控制机器感兴趣。当然,这种方法只适用于基于 Red Hat 的发行版,如 Fedora、CentOS 和 RHEL。为此,您需要从 GitHub 存储库克隆源代码,并按以下方式构建 RPM 软件包:

$ git clone https://github.com/ansible/ansible.git
$ cd ./ansible
$ make rpm
$ sudo rpm -Uvh ./rpm-build/ansible-*.noarch.rpm

现在您已经了解了如何在 Linux 上安装 Ansible,我们将简要介绍如何在 macOS 上安装 Ansible。

在 macOS 上安装 Ansible

在本节中,您将学习如何在 macOS 上安装 Ansible。最简单的安装方法是使用 Homebrew,但您也可以使用 Python 软件包管理器。让我们从安装 Homebrew 开始,这是 macOS 的快速便捷的软件包管理解决方案。

如果您在 macOS 上尚未安装 Homebrew,可以按照此处的详细说明轻松安装它:

  • 安装 Homebrew:通常,这里显示的两个命令就足以在 macOS 上安装 Homebrew:
$ xcode-select --install
$ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

如果您已经为其他目的安装了 Xcode 命令行工具,您可能会看到以下错误消息:

xcode-select: error: command line tools are already installed, use "Software Update" to update

您可能希望在 macOS 上打开 App Store 并检查是否需要更新 Xcode,但只要安装了命令行工具,您的 Homebrew 安装应该顺利进行。

如果您希望确认您的 Homebrew 安装成功,可以运行以下命令,它会警告您有关安装的任何潜在问题,例如,以下输出警告我们,尽管 Homebrew 已成功安装,但它不在我们的PATH中,因此我们可能无法运行任何可执行文件而不指定它们的绝对路径:

$ brew doctor
Please note that these warnings are just used to help the Homebrew maintainers
with debugging if you file an issue. If everything you use Homebrew for is
working fine: please don't worry or file an issue; just ignore this. Thanks!

Warning: Homebrew's sbin was not found in your PATH but you have installed
formulae that put executables in /usr/local/sbin.
Consider setting the PATH for example like so
 echo 'export PATH="/usr/local/sbin:$PATH"' >> ~/.bash_profile
  • 安装 Python 软件包管理器(pip):如果您不希望使用 Homebrew 安装 Ansible,您可以使用以下简单命令安装pip
$ sudo easy_install pip

还要检查您的 Python 版本是否至少为 2.7,因为旧版本的 Ansible 无法运行(几乎所有现代 macOS 安装都应该是这种情况):

$ python --version
Python 2.7.16

您可以使用 Homebrew 或 Python 软件包管理器在 macOS 上安装最新版本的 Ansible,方法如下:

  • 通过 Homebrew 安装 Ansible:要通过 Homebrew 安装 Ansible,请运行以下命令:
$ brew install ansible
  • 通过 Python 软件包管理器(pip)安装 Ansible:要通过pip安装 Ansible,请使用以下命令:
$ sudo pip install ansible

如果您有兴趣直接从 GitHub 运行最新的 Ansible 开发版本,那么您可以通过运行以下命令来实现:

$ pip install git+https://github.com/ansible/ansible.git@devel 

现在您已经使用您喜欢的方法安装了 Ansible,您可以像以前一样运行ansible命令,如果一切按计划进行,您将看到类似以下的输出:

$ ansible --version
ansible 2.9.6
  config file = None
  configured module search path = ['/Users/james/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/local/Cellar/ansible/2.9.4_1/libexec/lib/python3.8/site-packages/ansible
  executable location = /usr/local/bin/ansible
  python version = 3.8.1 (default, Dec 27 2019, 18:05:45) [Clang 11.0.0 (clang-1100.0.33.16)]

如果您正在运行 macOS 10.9,使用pip安装 Ansible 可能会遇到问题。以下是一个解决此问题的解决方法:

$ sudo CFLAGS=-Qunused-arguments CPPFLAGS=-Qunused-arguments pip install ansible

如果您想更新您的 Ansible 版本,pip可以通过以下命令轻松实现:

$ sudo pip install ansible --upgrade

同样,如果您使用的是brew命令进行安装,也可以使用该命令进行升级:

$ brew upgrade ansible

现在您已经学会了在 macOS 上安装 Ansible 的步骤,让我们看看如何为 Ansible 配置 Windows 主机以进行自动化。

为 Ansible 配置 Windows 主机

正如前面讨论的,Windows 上没有直接的 Ansible 安装方法,建议在可用的情况下安装 WSL,并像在本章前面概述的过程中那样安装 Ansible,就像在 Linux 上本地运行一样。

尽管存在这种限制,但是 Ansible 并不仅限于管理 Linux 和基于 BSD 的系统,它能够使用本机 WinRM 协议对 Windows 主机进行无代理管理,使用 PowerShell 模块和原始命令,这在每个现代 Windows 安装中都可用。在本节中,您将学习如何配置 Windows 以启用 Ansible 的任务自动化。

让我们看看在自动化 Windows 主机时,Ansible 能做些什么:

  • 收集远程主机的信息。

  • 安装和卸载 Windows 功能。

  • 管理和查询 Windows 服务。

  • 管理用户账户和用户列表。

  • 使用 Chocolatey(Windows 的软件存储库和配套管理工具)来管理软件包。

  • 执行 Windows 更新。

  • 从远程机器获取多个文件到 Windows 主机。

  • 在目标主机上执行原始的 PowerShell 命令和脚本。

Ansible 允许你通过连接本地用户或域用户来自动化 Windows 机器上的任务。你可以像在 Linux 发行版上使用sudo命令一样,以管理员身份运行操作,使用 Windows 的runas支持。

另外,由于 Ansible 是开源软件,你可以通过创建自己的 PowerShell 模块或者发送原始的 PowerShell 命令来扩展其功能。例如,信息安全团队可以轻松地管理文件系统 ACL、配置 Windows 防火墙,并使用本地 Ansible 模块和必要时的原始命令来管理主机名和域成员资格。

Windows 主机必须满足以下要求,以便 Ansible 控制机器与之通信:

  • Ansible 尝试支持所有在 Microsoft 的当前或扩展支持下的 Windows 版本,包括桌面平台,如 Windows 7、8.1 和 10,以及服务器操作系统,包括 Windows Server 2008(和 R2)、2012(和 R2)、2016 和 2019。

  • 你还需要在 Windows 主机上安装 PowerShell 3.0 或更高版本,以及至少.NET 4.0。

  • 你需要创建和激活一个 WinRM 监听器,这将在后面详细描述。出于安全原因,这不是默认启用的。

让我们更详细地看一下如何准备 Windows 主机以便被 Ansible 自动化:

  1. 关于先决条件,你必须确保 Windows 机器上安装了 PowerShell 3.0 和.NET Framework 4.0。如果你仍在使用旧版本的 PowerShell 或.NET Framework,你需要升级它们。你可以手动执行这个过程,或者以下的 PowerShell 脚本可以自动处理:
$url = "https://raw.githubusercontent.com/jborean93/ansible-windows/master/scripts/Upgrade-PowerShell.ps1" 
$file = "$env:temp\Upgrade-PowerShell.ps1" (New-Object -TypeName System.Net.WebClient).DownloadFile($url, $file) 

Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Force &$file -Verbose Set-ExecutionPolicy -ExecutionPolicy Restricted -Force

这个脚本通过检查需要安装的程序(如.NET Framework 4.5.2)和所需的 PowerShell 版本,如果需要的话重新启动,并设置用户名和密码参数。脚本将在重新启动时自动重新启动和登录,因此不需要更多的操作,脚本将一直持续,直到 PowerShell 版本与目标版本匹配。

如果用户名和密码参数没有设置,脚本将要求用户在必要时重新启动并手动登录,下次用户登录时,脚本将在中断的地方继续。这个过程会一直持续,直到主机满足 Ansible 自动化的要求。

  1. 当 PowerShell 升级到至少 3.0 版本后,下一步将是配置 WinRM 服务,以便 Ansible 可以连接到它。WinRM 服务配置定义了 Ansible 如何与 Windows 主机进行交互,包括监听端口和协议。

如果以前从未设置过 WinRM 监听器,你有三种选项可以做到这一点:

  • 首先,你可以使用winrm quickconfig来配置 HTTP,使用winrm quickconfig -transport:https来配置 HTTPS。这是在需要在域环境之外运行并创建一个简单监听器时使用的最简单的方法。这个过程的优势在于它会在 Windows 防火墙中打开所需的端口,并自动启动 WinRM 服务。

  • 如果你在域环境中运行,我强烈建议使用组策略对象GPOs),因为如果主机是域成员,那么配置会自动完成,无需用户输入。有许多可用的文档化程序可以做到这一点,由于这是一个非常 Windows 领域中心的任务,它超出了本书的范围。

  • 最后,您可以通过运行以下 PowerShell 命令创建具有特定配置的监听器:

$selector_set = @{
    Address = "*"
    Transport = "HTTPS"
}
$value_set = @{
    CertificateThumbprint = "E6CDAA82EEAF2ECE8546E05DB7F3E01AA47D76CE"
}

New-WSManInstance -ResourceURI "winrm/config/Listener" -SelectorSet $selector_set -ValueSet $value_set

前面的CertificateThumbprint应该与您之前创建或导入到 Windows 证书存储中的有效 SSL 证书的指纹匹配。

如果您正在运行 PowerShell v3.0,您可能会遇到 WinRM 服务的问题,该问题限制了可用内存的数量。这是一个已知的错误,并且有一个热修复程序可用于解决它。这里提供了一个应用此热修复程序的示例过程(用 PowerShell 编写):

$url = "https://raw.githubusercontent.com/jborean93/ansible-windows/master/scripts/Install-WMF3Hotfix.ps1" 
$file = "$env:temp\Install-WMF3Hotfix.ps1" 

(New-Object -TypeName System.Net.WebClient).DownloadFile($url, $file) powershell.exe -ExecutionPolicy ByPass -File $file -Verbose

配置 WinRM 监听器可能是一个复杂的任务,因此重要的是能够检查您的配置过程的结果。以下命令(可以从命令提示符中运行)将显示当前的 WinRM 监听器配置:

winrm enumerate winrm/config/Listener

如果一切顺利,您应该会得到类似于此的输出:

Listener
    Address = *
    Transport = HTTP
    Port = 5985
    Hostname
    Enabled = true
    URLPrefix = wsman
    CertificateThumbprint
    ListeningOn = 10.0.2.15, 127.0.0.1, 192.168.56.155, ::1, fe80::5efe:10.0.2.15%6, fe80::5efe:192.168.56.155%8, fe80::
ffff:ffff:fffe%2, fe80::203d:7d97:c2ed:ec78%3, fe80::e8ea:d765:2c69:7756%7

Listener
    Address = *
    Transport = HTTPS
    Port = 5986
    Hostname = SERVER2016
    Enabled = true
    URLPrefix = wsman
    CertificateThumbprint = E6CDAA82EEAF2ECE8546E05DB7F3E01AA47D76CE
    ListeningOn = 10.0.2.15, 127.0.0.1, 192.168.56.155, ::1, fe80::5efe:10.0.2.15%6, fe80::5efe:192.168.56.155%8, fe80::
ffff:ffff:fffe%2, fe80::203d:7d97:c2ed:ec78%3, fe80::e8ea:d765:2c69:7756%7

根据前面的输出,有两个活动的监听器——一个监听 HTTP 端口5985,另一个监听 HTTPS 端口5986,提供更高的安全性。另外,前面的输出还显示了以下参数的解释:

  • 传输:这应该设置为 HTTPS 或 HTTPS,尽管强烈建议您使用 HTTPS 监听器,以确保您的自动化命令不会受到窥探或操纵。

  • 端口:这是监听器操作的端口,默认情况下为5985(HTTP)或5986(HTTPS)。

  • URL 前缀:这是与之通信的 URL 前缀,默认情况下为wsman。如果更改它,您必须在 Ansible 控制主机上设置ansible_winrm_path主机为相同的值。

  • CertificateThumbprint:如果在 HTTPS 监听器上运行,这是连接使用的 Windows 证书存储的证书指纹。

如果您在设置 WinRM 监听器后需要调试任何连接问题,您可能会发现以下命令很有价值,因为它们在 Windows 主机之间执行基于 WinRM 的连接,而不使用 Ansible,因此您可以使用它们来区分您可能遇到的问题是与您的 Ansible 主机相关还是 WinRM 监听器本身存在问题:

# test out HTTP
winrs -r:http://<server address>:5985/wsman -u:Username -p:Password ipconfig 
# test out HTTPS (will fail if the cert is not verifiable)
winrs -r:https://<server address>:5986/wsman -u:Username -p:Password -ssl ipconfig 

# test out HTTPS, ignoring certificate verification
$username = "Username"
$password = ConvertTo-SecureString -String "Password" -AsPlainText -Force
$cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $username, $password

$session_option = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck
Invoke-Command -ComputerName server -UseSSL -ScriptBlock { ipconfig } -Credential $cred -SessionOption $session_option

如果前面的命令中的任何一个失败,您应该在尝试设置或配置 Ansible 控制主机之前调查您的 WinRM 监听器设置。

在这个阶段,Windows 应该准备好通过 WinRM 接收来自 Ansible 的通信。要完成此过程,您还需要在 Ansible 控制主机上执行一些额外的配置。首先,您需要安装winrm Python 模块,这取决于您的控制主机的配置,可能已经安装或尚未安装。安装方法会因操作系统而异,但通常可以使用pip在大多数平台上安装如下:

$ pip install winrm

完成后,您需要为 Windows 主机定义一些额外的清单变量——现在不要太担心清单,因为我们将在本书的后面部分介绍这些。以下示例仅供参考:

[windows]
192.168.1.52

[windows:vars]
ansible_user=administrator
ansible_password=password
ansible_connection=winrm
ansible_winrm_server_cert_validation=ignore

最后,您应该能够运行 Ansible ping模块,执行类似以下命令的端到端连接性测试(根据您的清单进行调整):

$ ansible -i inventory -m ping windows
192.168.1.52 | SUCCESS => {
 "changed": false,
 "ping": "pong"
}

现在您已经学会了为 Ansible 配置 Windows 主机所需的步骤,让我们在下一节中看看如何通过 Ansible 连接多个主机。

了解您的 Ansible 安装

在本章的这个阶段,无论你选择的操作系统是什么,你应该已经有一个可用的 Ansible 安装,可以开始探索自动化的世界。在本节中,我们将进行对 Ansible 基础知识的实际探索,以帮助你了解如何使用它。一旦掌握了这些基本技能,你就会有足够的知识来充分利用本书的其余部分。让我们从概述 Ansible 如何连接到非 Windows 主机开始。

理解 Ansible 如何连接到主机

除了 Windows 主机(如前一节末讨论的),Ansible 使用 SSH 协议与主机通信。Ansible 设计选择这种方式的原因有很多,其中最重要的是几乎每台 Linux/FreeBSD/macOS 主机都内置了它,许多网络设备(如交换机和路由器)也是如此。这个 SSH 服务通常与操作系统身份验证堆栈集成,使你能够利用诸如 Kerberos 等功能来提高身份验证安全性。此外,OpenSSH 的功能,如 ControlPersist,用于增加自动化任务的性能和用于网络隔离和安全的 SSH 跳转主机。

ControlPersist 在大多数现代 Linux 发行版中默认启用作为 OpenSSH 服务器安装的一部分。然而,在一些较旧的操作系统上,如 Red Hat Enterprise Linux 6(和 CentOS 6),它不受支持,因此你将无法使用它。Ansible 自动化仍然是完全可能的,但更长的 playbooks 可能会运行得更慢。

Ansible 使用与你已经熟悉的相同的身份验证方法,SSH 密钥通常是最简单的方法,因为它们消除了用户在每次运行 playbook 时输入身份验证密码的需要。然而,这绝不是强制性的,Ansible 通过使用 --ask-pass 开关支持密码身份验证。如果你连接到主机上的一个非特权帐户,并且需要执行 Ansible 等效的以 sudo 运行命令,你也可以在运行 playbook 时添加 --ask-become-pass,以允许在运行时指定这个。

自动化的目标是能够安全地运行任务,但最少的用户干预。因此,强烈建议你使用 SSH 密钥进行身份验证,如果你有多个密钥需要管理,那么一定要使用 ssh-agent

每个 Ansible 任务,无论是单独运行还是作为复杂 playbook 的一部分运行,都是针对清单运行的。清单就是你希望运行自动化命令的主机列表。Ansible 支持各种清单格式,包括使用动态清单,它可以自动从编排提供程序中填充自己(例如,你可以动态生成一个 Ansible 清单,从你的 Amazon EC2 实例中,这意味着你不必跟上云基础设施中的所有变化)。

动态清单插件已经为大多数主要的云提供商(例如,Amazon EC2,Google Cloud Platform 和 Microsoft Azure),以及本地系统(如 OpenShift 和 OpenStack)编写。甚至还有 Docker 的插件。开源软件的美妙之处在于,对于大多数你能想到的主要用例,有人已经贡献了代码,所以你不需要自己去弄清楚或编写它。

Ansible 的无代理架构以及它不依赖于 SSL 的事实意味着你不需要担心 DNS 未设置或者由于 NTP 不工作而导致的时间偏移问题——事实上,这些都可以由 Ansible playbook 执行!事实上,Ansible 确实是设计用来从几乎空白的操作系统镜像中运行你的基础设施。

目前,让我们专注于 INI 格式的清单。下面是一个示例,其中有四台服务器,每台服务器分成两个组。可以对整个清单(即所有四台服务器)、一个或多个组(例如webservers)甚至单个服务器运行 Ansible 命令和 playbooks:

[webservers]
web1.example.com
web2.example.com

[apservers]
ap1.example.com
ap2.example.com

让我们使用这个清单文件以及 Ansible 的ping模块,用于测试 Ansible 是否能够成功在所讨论的清单主机上执行自动化任务。以下示例假设您已将清单安装在默认位置,通常为/etc/ansible/hosts。当您运行以下ansible命令时,您将看到类似于这样的输出:

$ ansible webservers -m ping 
web1.example.com | SUCCESS => {
 "changed": false, 
 "ping": "pong"
}
web2.example.com | SUCCESS => {
 "changed": false, 
 "ping": "pong"
}
$

请注意,ping模块仅在webservers组中的两台主机上运行,而不是整个清单——这是因为我们在命令行参数中指定了这一点。

ping模块是 Ansible 的成千上万个模块之一,所有这些模块都执行一组给定的任务(从在主机之间复制文件到文本替换,再到复杂的网络设备配置)。同样,由于 Ansible 是开源软件,有大量的编码人员在编写和贡献模块,这意味着如果您能想到一个任务,可能已经有一个 Ansible 模块。即使没有模块存在的情况下,Ansible 支持发送原始 shell 命令(或者对于 Windows 主机的 PowerShell 命令),因此即使在这种情况下,您也可以完成所需的任务,而无需离开 Ansible。

只要 Ansible 控制主机能够与清单中的主机通信,您就可以自动化您的任务。但是,值得考虑一下您放置控制主机的位置。例如,如果您专门使用一组 Amazon EC2 机器,您的 Ansible 控制机器最好是一个 EC2 实例——这样,您就不需要通过互联网发送所有自动化命令。这也意味着您不需要将 EC2 主机的 SSH 端口暴露给互联网,因此使它们更安全。

到目前为止,我们已经简要解释了 Ansible 如何与其目标主机通信,包括清单是什么以及对所有主机进行 SSH 通信的重要性,除了 Windows 主机。在下一节中,我们将通过更详细地查看如何验证您的 Ansible 安装来进一步了解这一点。

验证 Ansible 安装

在本节中,您将学习如何使用简单的临时命令验证您的 Ansible 安装。

正如之前讨论的,Ansible 可以通过多种方式对目标主机进行身份验证。在本节中,我们将假设您希望使用 SSH 密钥,并且您已经生成了公钥和私钥对,并将公钥应用于您将自动化任务的所有目标主机。

ssh-copy-id实用程序非常有用,可以在继续之前将您的公共 SSH 密钥分发到目标主机。例如命令可能是ssh-copy-id -i ~/.ssh/id_rsa ansibleuser@web1.example.com

为了确保 Ansible 可以使用您的私钥进行身份验证,您可以使用ssh-agent——命令显示了如何启动ssh-agent并将您的私钥添加到其中的简单示例。当然,您应该将路径替换为您自己私钥的路径:

$ ssh-agent bash 
$ ssh-add ~/.ssh/id_rsa

正如我们在前一节中讨论的,我们还必须为 Ansible 定义一个清单。下面是另一个简单的示例:

[frontends]
frt01.example.com
frt02.example.com

我们在上一节中使用的ansible命令有两个重要的开关,您几乎总是会使用:-m <MODULE_NAME>在您指定的清单主机上运行一个模块,还可以使用-a OPT_ARGS开关传递模块参数。使用ansible二进制运行的命令称为临时命令。

以下是三个简单示例,演示了临时命令-它们也对验证您控制机器上的 Ansible 安装和目标主机的配置非常有价值,如果配置的任何部分存在问题,它们将返回错误:

  • Ping 主机:您可以使用以下命令对您的库存主机执行 Ansible“ping”:
$ ansible frontends -i hosts -m ping
  • 显示收集的事实:您可以使用以下命令显示有关您的库存主机的收集事实:
$ ansible frontends -i hosts -m setup | less
  • 过滤收集的事实:您可以使用以下命令过滤收集的事实:
$ ansible frontends -i hosts -m setup -a "filter=ansible_distribution*"

对于您运行的每个临时命令,您将以 JSON 格式获得响应-以下示例输出是成功运行ping模块的结果:

$ ansible frontends -m ping 
frontend01.example.com | SUCCESS => {
 "changed": false, 
 "ping": "pong"
}
frontend02.example.com | SUCCESS => {
 "changed": false, 
 "ping": "pong"
}

Ansible 还可以收集并返回有关目标主机的“事实”-事实是有关主机的各种有用信息,从 CPU 和内存配置到网络参数,再到磁盘几何。这些事实旨在使您能够编写智能的 playbook,执行条件操作-例如,您可能只想在具有 4GB 以上 RAM 的主机上安装特定软件包,或者仅在 macOS 主机上执行特定配置。以下是来自基于 macOS 的主机的过滤事实的示例:

$ ansible frontend01.example.com -m setup -a "filter=ansible_distribution*"
frontend01.example.com | SUCCESS => {
 ansible_facts": {
 "ansible_distribution": "macOS", 
 "ansible_distribution_major_version": "10", 
 "ansible_distribution_release": "18.5.0", 
 "ansible_distribution_version": "10.14.4"
 }, 
 "changed": false

临时命令非常强大,既可以用于验证您的 Ansible 安装,也可以用于学习 Ansible 以及如何使用模块,因为您不需要编写整个 playbook-您只需运行一个临时命令并学习它如何响应。以下是一些供您考虑的其他临时示例:

  • 使用以下命令将文件从 Ansible 控制主机复制到“前端”组中的所有主机:
$ ansible frontends -m copy -a "src=/etc/yum.conf dest=/tmp/yum.conf"
  • 在“前端”库存组中的所有主机上创建一个新目录,并使用特定的所有权和权限创建它:
$ ansible frontends -m file -a "dest=/path/user1/new mode=777 owner=user1 group=user1 state=directory" 
  • 使用以下命令从“前端”组中的所有主机中删除特定目录:
$ ansible frontends -m file -a "dest=/path/user1/new state=absent"
  • 使用yum安装httpd软件包,如果尚未安装-如果已安装,则不更新。同样,这适用于“前端”库存组中的所有主机:
$ ansible frontends -m yum -a "name=httpd state=present"
  • 以下命令与上一个命令类似,只是将state=present更改为state=latest会导致 Ansible 安装(最新版本的)软件包(如果尚未安装),并将其更新到最新版本(如果已安装):
$ ansible frontends -m yum -a "name=demo-tomcat-1 state=latest" 
  • 显示有关库存中所有主机的所有事实(警告-这将产生大量的 JSON!):
$ ansible all -m setup 

现在您已经了解了如何验证您的 Ansible 安装以及如何运行临时命令,让我们继续更详细地查看由 Ansible 管理的节点的要求。

受管节点要求

到目前为止,我们几乎完全专注于 Ansible 控制主机的要求,并假设(除了分发 SSH 密钥之外)目标主机将正常工作。当然,并非总是如此,例如,从 ISO 安装的现代 Linux 安装通常会正常工作,云操作系统映像通常会被剥离以保持其小巧,因此可能缺少重要的软件包,如 Python,没有 Python,Ansible 无法运行。

如果您的目标主机缺少 Python,通常可以通过操作系统的软件包管理系统轻松安装它。Ansible 要求您在 Ansible 控制机器(正如我们在本章前面所介绍的)和每个受管节点上安装 Python 版本 2.7 或 3.5(及以上)。再次强调,这里的例外是 Windows,它依赖于 PowerShell。

如果您使用缺少 Python 的操作系统映像,以下命令提供了快速安装 Python 的指南:

  • 要使用yum安装 Python(在旧版本的 Fedora 和 CentOS/RHEL 7 及以下版本中),请使用以下命令:
$ sudo yum -y install python
  • 在 RHEL 和 CentOS 8 及更新版本的 Fedora 上,您将使用dnf软件包管理器:
$ sudo dnf install python

您也可以选择安装特定版本以满足您的需求,就像这个例子一样:

$ sudo dnf install python37
  • 在 Debian 和 Ubuntu 系统上,您将使用apt软件包管理器安装 Python,如果需要的话再指定一个版本(这里给出的示例是安装 Python 3.6,在 Ubuntu 18.04 上可以工作):
$ sudo apt-get update
$ sudo apt-get install python3.6

我们在本章前面讨论的 Ansible 的ping模块不仅检查与受控主机的连接和身份验证,而且使用受控主机的 Python 环境执行一些基本主机检查。因此,它是一个很棒的端到端测试,可以让您确信您的受控主机已正确配置为主机,具有完美的连接和身份验证设置,但如果缺少 Python,它将返回一个failed结果。

当然,在这个阶段一个完美的问题是:如果您使用一个精简的基础镜像在云服务器上部署了 100 个节点,Ansible 如何帮助您?这是否意味着您必须手动检查所有 100 个节点并手动安装 Python 才能开始自动化?

幸运的是,即使在这种情况下,Ansible 也可以帮助您,这要归功于raw模块。这个模块用于向受控节点发送原始 shell 命令——它既适用于 SSH 管理的主机,也适用于 Windows PowerShell 管理的主机。因此,您可以使用 Ansible 在缺少 Python 的整套系统上安装 Python,甚至运行一个整个的 shell 脚本来引导一个受控节点。最重要的是,raw模块是为数不多的几个不需要在受控节点上安装 Python 的模块之一,因此它非常适合我们的用例,我们必须安装 Python 以启用进一步的自动化。

以下是 Ansible playbook 中的一些任务示例,您可以使用它们来引导受控节点并为其准备好 Ansible 管理:

- name: Bootstrap a host without python2 installed
  raw: dnf install -y python2 python2-dnf libselinux-python

- name: Run a command that uses non-posix shell-isms (in this example /bin/sh doesn't handle redirection and wildcards together but bash does)
  raw: cat < /tmp/*txt
  args:
    executable: /bin/bash

- name: safely use templated variables. Always use quote filter to avoid injection issues.
  raw: "{{package_mgr|quote}}  {{pkg_flags|quote}}  install  {{python|quote}}"

我们现在已经介绍了在控制主机和受控节点上设置 Ansible 的基础知识,并且为您提供了配置第一个连接的简要入门。在结束本章之前,我们将更详细地看一下如何从 GitHub 直接运行最新的 Ansible 开发版本。

从源代码运行与预构建的 RPM 包

Ansible 一直在快速发展,可能会有时候,无论是为了早期访问新功能(或模块),还是作为您自己的开发工作的一部分,您希望从 GitHub 运行最新的、最前沿的 Ansible 版本。在本节中,我们将看一下如何快速启动并运行源代码。本章概述的方法有一个优点,即与基于软件包管理器的安装不同,后者必须以 root 身份执行,最终结果是安装了一个可工作的 Ansible,而无需任何 root 权限。

让我们开始从 GitHub 检出最新版本的源代码:

  1. 您必须首先从git存储库克隆源代码,然后切换到包含已检出代码的目录:
$ git clone https://github.com/ansible/ansible.git --recursive
$ cd ./ansible
  1. 在进行任何开发工作之前,或者确保从源代码运行 Ansible,您必须设置您的 shell 环境。为此提供了几个脚本,每个脚本适用于不同的 shell 环境。例如,如果您使用古老的 Bash shell,您将使用以下命令设置您的环境:
$ source ./hacking/env-setup

相反,如果您使用 Fish shell,您将设置您的环境如下:

**$ source ./hacking/env-setup.fish**
  1. 设置好环境后,您必须安装pip Python 软件包管理器,然后使用它来安装所有所需的 Python 软件包(注意:如果您的系统上已经有pip,则可以跳过第一个命令):
$ sudo easy_install pip
$ sudo pip install -r ./requirements.txt

请注意,当您运行env-setup脚本时,您将从您的源代码检出运行,并且默认的清单文件将是/etc/ansible/hosts。您可以选择指定一个除/etc/ansible/hosts之外的清单文件。

  1. 当您运行env-setup脚本时,Ansible 将从源代码检出运行,默认的清单文件是/etc/ansible/hosts;但是,您可以选择在您的机器上任何地方指定清单文件(有关更多详细信息,请参见使用清单docs.ansible.com/ansible/latest/user_guide/intro_inventory.html#inventory)。以下命令提供了一个示例,说明您可能会这样做,但显然,您的文件名和内容几乎肯定会有所不同:
$ echo "ap1.example.com" > ~/my_ansible_inventory
$ export ANSIBLE_INVENTORY=~/my_ansible_inventory

ANSIBLE_INVENTORY适用于 Ansible 版本 1.9 及以上,并替换了已弃用的ANSIBLE_HOSTS环境变量。

完成这些步骤后,您可以像本章中讨论的那样运行 Ansible,唯一的例外是您必须指定它的绝对路径。例如,如果您像前面的代码中设置清单并将 Ansible 源克隆到您的主目录中,您可以运行我们现在熟悉的临时ping命令,如下所示:

$ ~/ansible/bin/ansible all -m ping
ap1.example.com | SUCCESS => {
 "changed": false, 
 "ping": "pong"
}

当然,Ansible 源树不断变化,您不太可能只想坚持您克隆的副本。当需要更新时,您不需要克隆新副本;您可以使用以下命令更新现有的工作副本(同样,假设您最初将源树克隆到您的主目录中):

$ git pull --rebase
$ git submodule update --init --recursive

这就结束了我们对设置您的 Ansible 控制机和受管节点的介绍。希望您在本章中获得的知识将帮助您启动并为本书的其余部分奠定基础。

摘要

Ansible 是一个强大而多才多艺的简单自动化工具,其主要优势是其无代理架构和简单的安装过程。Ansible 旨在让您迅速实现从零到自动化,并以最小的努力,我们已经在本章中展示了您可以如何轻松地开始使用 Ansible。

在本章中,您学习了设置 Ansible 的基础知识——如何安装它来控制其他主机以及被 Ansible 管理的节点的要求。您了解了为 Ansible 自动化设置 SSH 和 WinRM 所需的基础知识,以及如何引导受管节点以确保它们适合于 Ansible 自动化。您还了解了临时命令及其好处。最后,您学会了如何直接从 GitHub 运行最新版本的代码,这既使您能够直接为 Ansible 的开发做出贡献,又使您能够在您的基础设施上使用最新的功能。

在下一章中,我们将学习 Ansible 语言基础知识,以便您编写您的第一个 playbook,并帮助您创建模板化配置,并开始构建复杂的自动化工作流程。

问题

  1. 您可以在哪些操作系统上安装 Ansible?(多个正确答案)

A)Ubuntu

B)Fedora

C)Windows 2019 服务器

D)HP-UX

E)主机

  1. Ansible 使用哪种协议连接远程机器来运行任务?

A)HTTP

B)HTTPS

C)SSH

D)TCP

E)UDP

  1. 要在 Ansible 临时命令行中执行特定模块,您需要使用-m选项。

A)正确

B)错误

进一步阅读

  • 有关通过 Ansible Mailing Liston Google Groups 安装的任何问题,请参阅以下内容:

groups.google.com/forum/#!forum/ansible-project

  • 如何安装最新版本的pip可以在这里找到:

pip.pypa.io/en/stable/installing/#installation

  • 可以在此处找到使用 PowerShell 的特定 Windows 模块:

github.com/ansible/ansible-modules-core/tree/devel/windows

  • 如果您有 GitHub 账户并想关注 GitHub 项目,您可以继续跟踪 Ansible 的问题、错误和想法:

github.com/ansible/ansible

第二章:理解 Ansible 的基础知识

在其核心,Ansible 是一个简单的框架,它将一个称为Ansible 模块的小程序推送到目标节点。模块是 Ansible 的核心,负责执行所有自动化的繁重工作。然而,Ansible 框架不仅限于此,还包括插件和动态清单管理,以及使用 playbooks 将所有这些内容与一起自动化基础设施的配置管理、应用部署、网络自动化等联系起来,如下所示:

Ansible 只需要安装在管理节点上;从那里,它通过网络传输层(通常是 SSH 或 WinRM)分发所需的模块来执行任务,并在任务完成后删除它们。通过这种方式,Ansible 保持了无代理的架构,并且不会用可能需要进行一次性自动化任务的代码来混淆目标节点。

在本章中,您将更多地了解 Ansible 框架的组成及其各个组件,以及如何在使用 YAML 语法编写的 playbooks 中将它们结合在一起。因此,您将学习如何为 IT 操作任务创建自动化代码,并学习如何使用临时任务和更复杂的 playbooks 应用它们。最后,您将学习 Jinja2 模板如何允许您使用变量和动态表达式重复构建动态配置文件。

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

  • 熟悉 Ansible 框架

  • 探索配置文件

  • 命令行参数

  • 定义变量

  • 理解 Jinja2 过滤器

技术要求

本章假设您已成功将最新版本的 Ansible(在撰写本文时为 2.9)安装到 Linux 节点上,如第一章中所讨论的开始使用 Ansible。它还假设您至少有另一台 Linux 主机用于测试自动化代码;您拥有的主机越多,您就能够开发本章中的示例并了解 Ansible 的内容就越多。假定 Linux 主机之间存在 SSH 通信,并且对它们有一定的了解。

本章的代码包可在github.com/PacktPublishing/Ansible-2-Cookbook/tree/master/Chapter%202获取。

熟悉 Ansible 框架

在本节中,您将了解 Ansible 框架如何适用于 IT 操作自动化。我们将解释如何首次启动 Ansible。一旦您了解了这个框架,您就准备好开始学习更高级的概念,比如创建和运行自己清单的 playbooks。

为了通过 SSH 连接从 Ansible 控制机器运行 Ansible 的临时命令到多个远程主机,您需要确保控制主机上安装了最新的 Ansible 版本。使用以下命令确认最新的 Ansible 版本:

$ ansible --version
ansible 2.9.6
 config file = /etc/ansible/ansible.cfg
 configured module search path = [u'/home/jamesf_local/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
 ansible python module location = /usr/lib/python2.7/dist-packages/ansible
 executable location = /usr/bin/ansible
 python version = 2.7.17 (default, Nov 7 2019, 10:07:09) [GCC 9.2.1 20191008]

您还需要确保与清单中定义的每个远程主机建立 SSH 连接。您可以在每个远程主机上使用简单的手动 SSH 连接来测试连接性,因为在所有远程基于 Linux 的自动化任务中,Ansible 将使用 SSH:

$ ssh <username>@frontend.example.com
The authenticity of host 'frontend.example.com (192.168.1.52)' can't be established.
ED25519 key fingerprint is SHA256:hU+saFERGFDERW453tasdFPAkpVws.
Are you sure you want to continue connecting (yes/no)? yes password:<Input_Your_Password>

在本节中,我们将带您了解 Ansible 的工作原理,从一些简单的连接测试开始。您可以通过以下简单步骤了解 Ansible 框架如何访问多个主机来执行您的任务:

  1. 创建或编辑您的默认清单文件/etc/ansible/hosts(您也可以通过传递选项,如--inventory=/path/inventory_file来指定自己的清单文件的路径)。在清单中添加一些示例主机——这些必须是 Ansible 要测试的真实机器的 IP 地址或主机名。以下是我网络中的示例,但您需要用自己的设备替换这些。每行添加一个主机名(或 IP 地址):
frontend.example.com
backend1.example.com
backend2.example.com 

所有主机都应该使用可解析的地址来指定——即完全合格的域名FQDN)——如果您的主机有 DNS 条目(或者在您的 Ansible 控制节点上的/etc/hosts中)。如果您没有设置 DNS 或主机条目,这可以是 IP 地址。无论您选择哪种格式作为清单地址,您都应该能够成功地对每个主机进行 ping。以下输出是一个例子:

$ ping frontend.example.com
PING frontend.example.com (192.168.1.52): 56 data bytes
64 bytes from 192.168.1.52: icmp_seq=0 ttl=64 time=0.040 ms
64 bytes from 192.168.1.52: icmp_seq=1 ttl=64 time=0.115 ms
64 bytes from 192.168.1.52: icmp_seq=2 ttl=64 time=0.097 ms
64 bytes from 192.168.1.52: icmp_seq=3 ttl=64 time=0.130 ms 
  1. 为了使自动化过程更加无缝,我们将生成一个 SSH 认证密钥对,这样我们就不必每次运行 playbook 时都输入密码。如果您还没有 SSH 密钥对,可以使用以下命令生成一个:
$ ssh-keygen 

当您运行ssh-keygen工具时,您将看到类似以下的输出。请注意,当提示时,您应该将passphrase变量留空;否则,您每次想运行 Ansible 任务时都需要输入一个密码,这将取消使用 SSH 密钥进行认证的便利性:

$ ssh-keygen 
Generating public/private rsa key pair.
Enter file in which to save the key (/Users/doh/.ssh/id_rsa): <Enter>
Enter passphrase (empty for no passphrase): <Press Enter>
Enter same passphrase again: <Press Enter>
Your identification has been saved in /Users/doh/.ssh/id_rsa.
Your public key has been saved in /Users/doh/.ssh/id_rsa.pub.
The key fingerprint is:
SHA256:1IF0KMMTVAMEQF62kTwcG59okGZLiMmi4Ae/BGBT+24 doh@danieloh.com
The key's randomart image is:
+---[RSA 2048]----+
|=*=*BB==+oo |
|B=*+*B=.o+ . |
|=+=o=.o+. . |
|...=. . |
| o .. S |
| .. |
| E |
| . |
| |
+----[SHA256]-----+
  1. 虽然有条件可以自动选择您的 SSH 密钥,但建议您使用ssh-agent,因为这样可以加载多个密钥来对抗各种目标进行认证。即使现在用不上,将来这对您也会非常有用。启动ssh-agent并添加您的新认证密钥,如下(请注意,您需要为每个打开的 shell 执行此操作):
$ ssh-agent bash
$ ssh-add ~/.ssh/id_rsa 
  1. 在您可以对目标主机执行基于密钥的认证之前,您需要将刚刚生成的密钥对的公钥应用到每个主机上。您可以使用以下命令依次将密钥复制到每个主机:
$  ssh-copy-id -i ~/.ssh/id_rsa.pub frontend.example.com
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "~/.ssh/id_rsa.pub"
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
doh@frontend.example.com's password:

Number of key(s) added: 1

Now try logging into the machine, with: "ssh 'frontend.example.com'"
and check to make sure that only the key(s) you wanted were added.
  1. 完成后,您现在应该能够对清单文件中放入的主机执行 Ansible 的ping命令。您会发现在任何时候都不需要输入密码,因为对清单中所有主机的 SSH 连接都使用您的 SSH 密钥对进行了认证。因此,您应该会看到类似以下的输出:
$ ansible all -i hosts -m ping
frontend.example.com | SUCCESS => {
 "changed": false, 
 "ping": "pong"
}
backend1.example.com | SUCCESS => {
 "changed": false, 
 "ping": "pong"
}
backend2.example.com | SUCCESS => {
 "changed": false, 
 "ping": "pong"
}

此示例输出是使用 Ansible 的默认详细级别生成的。如果在此过程中遇到问题,您可以通过在运行时向ansible命令传递一个或多个-v开关来增加 Ansible 的详细级别。对于大多数问题,建议您使用-vvvv,这会为您提供丰富的调试信息,包括原始 SSH 命令和来自它们的输出。例如,假设某个主机(例如backend2.example.com)无法连接,并且您收到类似以下的错误:

backend2.example.com | FAILED => SSH encountered an unknown error during the connection. We recommend you re-run the command using -vvvv, which will enable SSH debugging output to help diagnose the issue 

请注意,即使 Ansible 也建议使用-vvvv开关进行调试。这可能会产生大量输出,但会包括许多有用的细节,例如用于生成与清单中目标主机的连接的原始 SSH 命令,以及可能由此调用产生的任何错误消息。在调试连接或代码问题时,这可能非常有用,尽管一开始输出可能有点压倒性。但是,通过一些实践,您将很快学会如何解释它。

到目前为止,您应该已经对 Ansible 如何通过 SSH 与其客户端进行通信有了一个很好的了解。让我们继续进行下一部分,我们将更详细地了解组成 Ansible 的各个组件,因为这将帮助我们更好地理解如何使用它。

分解 Ansible 组件

Ansible 允许您在 playbooks 中定义策略、配置、任务序列和编排步骤,限制只在于您的想象力。可以同步或异步地在远程机器上执行 playbook 来管理任务,尽管大多数示例都是同步的。在本节中,您将了解 Ansible 的主要组件,并了解 Ansible 如何利用这些组件与远程主机通信。

为了了解各个组件,我们首先需要一个清单来进行工作。让我们创建一个示例清单,最好其中包含多个主机,这可能与您在上一节中创建的相同。如上一节所述,您应该使用主机名或 IP 地址填充清单,这些主机可以从控制主机本身访问到:

remote1.example.com
remote2.example.com
remote3.example.com

要真正了解 Ansible 以及其各个组件的工作原理,我们首先需要创建一个 Ansible playbook。尽管迄今为止我们尝试过的临时命令只是单个任务,但 playbooks 是组织良好的任务组,通常按顺序运行。可以应用条件逻辑,在任何其他编程语言中,它们都将被视为您的代码。在 playbook 的开头,您应该指定 play 的名称,尽管这不是强制性的,但将所有 play 和任务命名是一个良好的做法,没有这一点,其他人很难解释 playbook 的作用,即使您在一段时间后回来也是如此。让我们开始构建我们的第一个示例 playbook:

  1. 在 playbook 的顶部指定 play 名称和清单主机以运行您的任务。还要注意使用---,它表示一个 YAML 文件的开始(用 YAML 编写的 Ansible playbook):
---
- name: My first Ansible playbook
  hosts: all
  1. 之后,我们将告诉 Ansible,我们希望将此 playbook 中的所有任务都作为超级用户(通常为root)执行。我们使用以下语句来实现这一点(为了帮助您记忆,将become视为become superuser的缩写):
  become: yes
  1. 在此标题之后,我们将指定一个任务块,其中将包含一个或多个要按顺序运行的任务。现在,我们将简单地创建一个任务,使用yum模块更新 Apache 的版本(因此,此 playbook 仅适用于针对基于 RHEL、CentOS 或 Fedora 的主机运行)。我们还将指定 play 的一个特殊元素,称为处理程序。处理程序将在第四章《Playbooks and Roles》中详细介绍,所以现在不要太担心它们。简而言之,处理程序是一种特殊类型的任务,仅在某些内容更改时才会调用。因此,在此示例中,它会重新启动 Web 服务器,但仅在更改时才会重新启动,如果多次运行 playbook 并且没有 Apache 的更新,则可以防止不必要的重新启动。以下代码完全执行了这些功能,并应成为您的第一个 playbook 的基础:
  tasks:
  - name: Update the latest of an Apache Web Server
    yum:
      name: httpd
      state: latest
    notify:
      - Restart an Apache Web Server

 handlers:
 - name: Restart an Apache Web Server
   service:
     name: httpd
     state: restarted

恭喜,您现在拥有了您的第一个 Ansible playbook!如果您现在运行此 playbook,您应该会看到它在清单中的所有主机上进行迭代,以及在 Apache 软件包的每次更新时,然后重新启动服务。您的输出应该如下所示:

$ PLAY [My first Ansible playbook] ***********************************************

TASK [Gathering Facts] *********************************************************
ok: [remote2.example.com]
ok: [remote1.example.com]
ok: [remote3.example.com]

TASK [Update the latest of an Apache Web Server] *******************************
changed: [remote2.example.com]
changed: [remote3.example.com]
changed: [remote1.example.com]

RUNNING HANDLER [Restart an Apache Web Server] *********************************
changed: [remote3.example.com]
changed: [remote1.example.com]
changed: [remote2.example.com]

PLAY RECAP *********************************************************************
remote1.example.com : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
remote2.example.com : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
remote3.example.com : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

如果您检查 playbook 的输出,您会发现不仅 play 的名称很重要,每个执行的任务也很重要,因为这使得解释运行的输出变得非常简单。您还会看到运行任务有多种可能的结果;在前面的示例中,我们可以看到两种结果——okchanged。这些结果大多都很容易理解,ok表示任务成功运行,并且由于运行的结果没有发生任何变化。在前面的 playbook 中,Gathering Facts阶段就是一个只读任务,用于收集有关目标主机的信息。因此,它只能返回ok或失败的状态,比如如果主机宕机,则返回unreachable。它不应该返回changed

然而,您可以在前面的输出中看到,所有三个主机都需要升级其 Apache 软件包,因此,“更新 Apache Web 服务器的最新版本”任务的结果对所有主机都是“更改”。这个“更改”结果意味着我们的“处理程序”变量被通知,Web 服务器服务被重新启动。

如果我们第二次运行 playbook,我们知道 Apache 软件包很可能不需要再次升级。请注意这次 playbook 输出的不同之处:

PLAY [My first Ansible playbook] ***********************************************

TASK [Gathering Facts] *********************************************************
ok: [remote1.example.com]
ok: [remote2.example.com]
ok: [remote3.example.com]

TASK [Update the latest of an Apache Web Server] *******************************
ok: [remote2.example.com]
ok: [remote3.example.com]
ok: [remote1.example.com]

PLAY RECAP *********************************************************************
remote1.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
remote2.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
remote3.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

您可以看到,这次“更新 Apache Web 服务器的最新版本”任务的输出对所有三个主机都是ok,这意味着没有应用任何更改(软件包未更新)。因此,我们的处理程序没有收到通知,也没有运行——您可以看到它甚至没有出现在前面的 playbook 输出中。这种区别很重要——Ansible playbook(以及支持 Ansible 的模块)的目标应该是只在需要时才进行更改。如果一切都是最新的,那么目标主机就不应该被更改。应该避免不必要地重新启动服务,也应该避免对文件进行不必要的更改。简而言之,Ansible playbook 被设计为高效实现目标机器状态。

这实际上是一个关于编写您的第一个 playbook 的速成课程,但希望它能让您对 Ansible 从单个临时命令到更复杂的 playbook 时可以做些什么有所了解。在我们进一步探索 Ansible 语言和组件之前,让我们更深入地了解一下 playbook 所写的 YAML 语言。

学习 YAML 语法

在本节中,您将学习如何以正确的语法编写 YAML 文件,并了解在多个远程机器上运行 playbook 的最佳实践和技巧。Ansible 使用 YAML 是因为它比其他常见的数据格式(如 XML 或 JSON)更容易阅读和编写。不需要担心逗号、花括号或标签,代码中强制的缩进确保了代码的整洁和易读。此外,大多数编程语言都有可用于处理 YAML 的库。

这反映了 Ansible 的核心目标之一——产生易于阅读(和编写)的代码,描述给定主机的目标状态。Ansible playbook(理想情况下)应该是自我记录的,因为在繁忙的技术环境中,文档通常是一个事后想法——那么,有什么比通过负责部署代码的自动化系统更好的记录方式呢?

在我们深入了解 YAML 结构之前,先说一下文件本身。以 YAML 编写的文件可以选择性地以---开头(如前一节中示例 playbook 中所见)并以...结尾。这适用于 YAML 中的所有文件,无论是由 Ansible 还是其他系统使用,都表示文件是使用 YAML 语言编写的。您会发现,大多数 Ansible playbook 的示例(以及角色和其他相关的 YAML 文件)都以---开头,但不以...结尾——标题足以清楚地表示文件使用 YAML 格式。

让我们通过前面部分创建的示例 playbook 来探索 YAML 语言:

  1. 列表是 YAML 语言中的一个重要构造——实际上,尽管可能不太明显,playbook 的tasks:块实际上是一个 YAML 列表。YAML 中的列表将所有项目列在相同的缩进级别上,每行以-开头。例如,我们使用以下代码更新了前面 playbook 中的httpd软件包:
  - name: Update the latest of an Apache Web Server
    yum:
      name: httpd
      state: latest

然而,我们可以指定要升级的软件包列表如下:

  - name: Update the latest of an Apache Web Server
    yum:
      name:
        - httpd
        - mod_ssl
      state: latest

现在,我们不再将单个值传递给name:键,而是传递一个包含要更新的两个软件包名称的 YAML 格式列表。

  1. 字典是 YAML 中的另一个重要概念——它们由key: value格式表示,正如我们已经广泛看到的那样,但字典中的所有项目都缩进了一个更高的级别。这最容易通过一个例子来解释,因此考虑我们示例 playbook 中的以下代码:
    service:
      name: httpd
      state: restarted

在这个例子中(来自handler),service定义实际上是一个字典,namestate键的缩进比service键多两个空格。这种更高级别的缩进意味着namestate键与service键相关联,因此,在这种情况下,告诉service模块要操作哪个服务(httpd)以及对其执行什么操作(重新启动)。

已经在这两个例子中观察到,通过混合列表和字典,您可以制作相当复杂的数据结构。

  1. 随着您在 playbook 设计方面变得更加高级(我们将在本书的后面看到这方面的例子),您可能会开始制作相当复杂的变量结构,并将它们放入自己的单独文件中,以保持 playbook 代码的可读性。以下是一个提供公司两名员工详细信息的variables文件示例:
---
employees:
  - name: daniel
    fullname: Daniel Oh
    role: DevOps Evangelist
    level: Expert
    skills:
      - Kubernetes
      - Microservices
      - Ansible
      - Linux Container
  - name: michael
    fullname: Michael Smiths
    role: Enterprise Architect
    level: Advanced
    skills:
      - Cloud
      - Middleware
      - Windows
      - Storage

在这个例子中,您可以看到我们有一个包含每个员工详细信息的字典。员工本身是列表项(您可以通过行首的-来识别),同样,员工技能也被表示为列表项。您会注意到fullnamerolelevelskills键与name处于相同的缩进级别,但它们之前没有-。这告诉您它们与列表项本身在同一个字典中,因此它们代表员工的详细信息。

  1. YAML 在解析语言时非常字面,每个新行始终代表着新的代码行。如果您确实需要添加一块文本(例如,到一个变量)怎么办?在这种情况下,您可以使用一个文字块标量|来写多行,YAML 将忠实地保留新行、回车和每行后面的所有空格(但请注意,每行开头的缩进是 YAML 语法的一部分):
Specialty: |
  Agile methodology
  Cloud-native app development practices
  Advanced enterprise DevOps practices

因此,如果我们让 Ansible 将前面的内容打印到屏幕上,它将显示如下(请注意,前面的两个空格已经消失——它们被正确解释为 YAML 语言的一部分,而没有被打印出来):

Agile methodology
Cloud-native app development practices
Advanced enterprise DevOps practices

与前面类似的是折叠块标量>,它与文字块标量相同,但不保留行结束。这对于您想要在单行上打印的非常长的字符串很有用,但又想要为了可读性的目的将其跨多行包装在代码中。考虑我们示例的以下变化:

Specialty: >
  Agile methodology
  Cloud-native app development practices
  Advanced enterprise DevOps practices

现在,如果我们要打印这个,我们会看到以下内容:

Agile methodologyCloud-native app development practicesAdvanced enterprise DevOps practices

我们可以在前面的示例中添加尾随空格,以防止单词之间相互重叠,但我在这里没有这样做,因为我想为您提供一个易于解释的例子。

当您审查 playbooks、变量文件等时,您会看到这些结构一次又一次地被使用。尽管定义简单,但它们非常重要——缩进级别的遗漏或列表项开头缺少-实例都会导致整个 playbook 无法运行。正如我们发现的,您可以将所有这些不同的结构组合在一起。以下代码块中提供了一个variables文件的额外示例供您考虑,其中显示了我们已经涵盖的各种示例:

---
servers:
  - frontend
  - backend
  - database
  - cache
employees:
  - name: daniel
    fullname: Daniel Oh
    role: DevOps Evangelist
    level: Expert
    skills:
      - Kubernetes
      - Microservices
      - Ansible
      - Linux Container
  - name: michael
    fullname: Michael Smiths
    role: Enterprise Architect
    level: Advanced
    skills:
      - Cloud
      - Middleware
      - Windows
      - Storage
    Speciality: |
      Agile methodology
      Cloud-native app development practices
      Advanced enterprise DevOps practices

您还可以用缩写形式表示字典和列表,称为流集合。以下示例显示了与我们原始的employees变量文件完全相同的数据结构:

--- employees: [{"fullname": "Daniel Oh","level": "Expert","name": "daniel","role": "DevOps Evangelist","skills": ["Kubernetes","Microservices","Ansible","Linux Container"]},{"fullname": "Michael Smiths","level": "Advanced","name": "michael","role": "Enterprise Architect","skills":["Cloud","Middleware","Windows","Storage"]}]

尽管这显示了完全相同的数据结构,但您可以看到肉眼很难阅读。在 YAML 中并不广泛使用流集合,我不建议您自己使用它们,但了解它们是很重要的。您还会注意到,尽管我们已经开始讨论 YAML 中的变量,但我们并没有表达任何变量类型。YAML 尝试根据它们包含的数据对变量类型进行假设,因此如果您想将1.0赋给一个变量,YAML 会假设它是一个浮点数。如果您需要将其表示为字符串(也许是因为它是一个版本号),您需要在其周围加上引号,这会导致 YAML 解析器将其解释为字符串,例如以下示例:

version: "2.0"

这完成了我们对 YAML 语言语法的介绍。现在完成了,在下一节中,让我们看看如何组织您的自动化代码以使其易于管理和整洁。

组织您的自动化代码

可以想象,如果您将所有所需的 Ansible 任务都写在一个庞大的 playbook 中,它将很快变得难以管理——也就是说,它将难以阅读,难以让其他人理解,并且——最重要的是——当出现问题时难以调试。Ansible 提供了许多将代码分割成可管理块的方法;其中最重要的可能是使用角色。角色(简单类比)就像传统高级编程语言中的库。我们将在第四章 Playbooks and Roles中更详细地讨论角色。

然而,Ansible 支持将代码分割成可管理的块的其他方法,我们将在本节简要探讨,作为本书后面更深入探讨角色的先导。

让我们举一个实际的例子。首先,我们知道我们需要为 Ansible 运行创建清单。在这种情况下,我们将创建四个虚构的服务器组,每个组包含两台服务器。我们的假设示例将包含一个前端服务器和位于两个不同地理位置的虚构应用程序的应用程序服务器。我们的清单文件将被称为production-inventory,示例内容如下:

[frontends_na_zone] 
frontend1-na.example.com 
frontend2-na.example.com [frontends_emea_zone]
frontend1-emea.example.com
frontend2-emea.example.com

[appservers_na_zone]
appserver1-na.example.com
appserver2-na.example.com

[appservers_emea_zone]
appserver1-emea.example.com
appserver2-emea.example.com

显然,我们可以编写一个庞大的 playbook 来处理这些不同主机上所需的任务,但正如我们已经讨论过的那样,这将是繁琐和低效的。让我们将自动化这些不同主机的任务分解成更小的 playbook:

  1. 创建一个 playbook 来对特定主机组(例如frontends_na_zone)运行连接测试。将以下内容放入 playbook 中:
---
- hosts: frontends_na_zone
  remote_user: danieloh
  tasks:
    - name: simple connection test
      ping: 
  1. 现在,尝试运行此 playbook 以针对主机(请注意,我们已配置它连接到名为danieloh的清单系统上的远程用户,因此您需要创建此用户并设置适当的 SSH 密钥,或者更改 playbook 中remote_user行中的用户)。在设置身份验证后运行 playbook 时,您应该会看到类似以下的输出:
$ ansible-playbook -i production-inventory frontends-na.yml

PLAY [frontends_na_zone] *******************************************************

TASK [Gathering Facts] *********************************************************
ok: [frontend1-na.example.com]
ok: [frontend2-na.example.com]

TASK [simple connection test] **************************************************
ok: [frontend1-na.example.com]
ok: [frontend2-na.example.com]

PLAY RECAP *********************************************************************
frontend1-na.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frontend2-na.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 
  1. 现在,让我们通过创建一个只在应用服务器上运行的 playbook 来扩展我们的简单示例。同样,我们将使用 Ansible 的ping模块来执行连接测试,但在实际情况下,您可能会执行更复杂的任务,比如安装软件包或修改文件。指定此 playbook 针对appservers_emea_zone清单中的主机组运行。将以下内容添加到 playbook 中:
---
- hosts: appservers_emea_zone
  remote_user: danieloh
  tasks:
    - name: simple connection test
      ping: 

与以前一样,您需要确保可以访问这些服务器,因此要么创建danieloh用户并设置对该帐户的身份验证,要么更改示例 playbook 中的remote_user行。完成这些操作后,您应该能够运行 playbook,并且会看到类似以下的输出:

$ ansible-playbook -i production-inventory appservers-emea.yml

PLAY [appservers_emea_zone] ****************************************************

TASK [Gathering Facts] *********************************************************
ok: [appserver2-emea.example.com]
ok: [appserver1-emea.example.com]

TASK [simple connection test] **************************************************
ok: [appserver2-emea.example.com]
ok: [appserver1-emea.example.com]

PLAY RECAP *********************************************************************
appserver1-emea.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
appserver2-emea.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
  1. 到目前为止,一切都很好。然而,现在我们有两个需要手动运行的 playbook,只涉及到我们清单中的两个主机组。如果我们想要处理所有四个组,我们需要创建总共四个 playbook,所有这些都需要手动运行。这几乎不符合最佳的自动化实践。如果有一种方法可以将这些单独的 playbook 合并在一个顶级 playbook 中一起运行呢?这将使我们能够分割我们的代码以保持可管理性,但在运行 playbook 时也可以防止大量的手动工作。幸运的是,我们可以通过利用import_playbook指令在一个名为site.yml的顶级 playbook 中实现这一点:
---
- import_playbook: frontend-na.yml
- import_playbook: appserver-emea.yml

现在,当您使用(现在已经熟悉的)ansible-playbook命令运行这个单个 playbook 时,您会发现效果与我们实际上连续运行两个 playbook 的效果相同。这样,即使在我们探索角色的概念之前,您也可以看到 Ansible 支持将您的代码分割成可管理的块,而无需手动运行每个块:

$ ansible-playbook -i production-inventory site.yml

PLAY [frontends_na_zone] *******************************************************

TASK [Gathering Facts] *********************************************************
ok: [frontend2-na.example.com]
ok: [frontend1-na.example.com]

TASK [simple connection test] **************************************************
ok: [frontend1-na.example.com]
ok: [frontend2-na.example.com]

PLAY [appservers_emea_zone] ****************************************************

TASK [Gathering Facts] *********************************************************
ok: [appserver2-emea.example.com]
ok: [appserver1-emea.example.com]

TASK [simple connection test] **************************************************
ok: [appserver2-emea.example.com]
ok: [appserver1-emea.example.com]

PLAY RECAP *********************************************************************
appserver1-emea.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
appserver2-emea.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frontend1-na.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frontend2-na.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

在地理多样化的环境中,您可以做的远不止我们这里的简单示例,因为我们甚至还没有涉及将变量放入清单中的事情(例如,将不同的参数与不同的环境关联)。我们将在第三章中更详细地探讨这个问题,定义您的清单

然而,希望这已经为您提供了足够的知识,以便您可以开始对如何组织 playbooks 的代码做出明智的选择。随着您完成本书的进一步章节,您将能够确定您是否希望利用角色或import_playbook指令(或者甚至两者都使用)作为 playbook 组织的一部分。

让我们在下一节继续进行 Ansible 的速成课程,看看配置文件和一些您可能发现有价值的关键指令。

探索配置文件

Ansible 的行为在一定程度上由其配置文件定义。中央配置文件(影响系统上所有用户的 Ansible 行为)可以在/etc/ansible/ansible.cfg找到。然而,这并不是 Ansible 寻找其配置的唯一位置;事实上,它将从顶部到底部查找以下位置。

文件的第一个实例是它将使用的配置;所有其他实例都将被忽略,即使它们存在:

  1. ANSIBLE_CONFIG:由此环境变量的值指定的文件位置,如果设置

  2. ansible.cfg:在当前工作目录

  3. ~/.ansible.cfg:在用户的主目录中

  4. /etc/ansible/ansible.cfg:我们之前提到的中央配置

如果您通过yumapt等软件包管理器安装了 Ansible,您几乎总是会在/etc/ansible中找到名为ansible.cfg的默认配置文件。但是,如果您从源代码构建了 Ansible 或通过pip安装了它,则中央配置文件将不存在,您需要自己创建。一个很好的起点是参考包含在源代码中的示例 Ansible 配置文件,可以在 GitHub 上找到其副本,网址为raw.githubusercontent.com/ansible/ansible/devel/examples/ansible.cfg

在本节中,我们将详细介绍如何定位 Ansible 的运行配置以及如何操作它。大多数通过软件包安装 Ansible 的人发现,在修改默认配置之前,他们可以在很多情况下使用 Ansible,因为它经过精心设计,可以在许多场景中工作。然而,重要的是要了解一些关于配置 Ansible 的知识,以防您在环境中遇到只能通过修改配置来更改的问题。

显然,如果您没有安装 Ansible,探索其配置就没有意义,因此让我们通过发出以下命令来检查您是否已安装并运行 Ansible(所示的输出是在撰写时安装在 macOS 上的最新版本的 Ansible 的输出):

$ ansible 2.9.6
  config file = None
  configured module search path = ['/Users/james/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/local/Cellar/ansible/2.9.6_1/libexec/lib/python3.8/site-packages/ansible
  executable location = /usr/local/bin/ansible
  python version = 3.8.2 (default, Mar 11 2020, 00:28:52) [Clang 11.0.0 (clang-1100.0.33.17)]

让我们开始探索 Ansible 提供的默认配置:

  1. 以下代码块中的命令列出了 Ansible 支持的当前配置参数。这非常有用,因为它告诉您可以用来更改设置的环境变量(请参阅env字段),以及可以使用的配置文件参数和部分(请参阅ini字段)。其他有价值的信息,包括默认配置值和配置的描述,也会给出(请参阅defaultdescription字段)。所有信息均来自lib/constants.py。运行以下命令来探索输出:
$ ansible-config list 

以下是您将看到的输出的示例。当然,它有很多页面,但这里只是一个片段示例:

$ ansible-config list
ACTION_WARNINGS:
  default: true
  description:
  - By default Ansible will issue a warning when received from a task action (module
    or action plugin)
  - These warnings can be silenced by adjusting this setting to False.
  env:
  - name: ANSIBLE_ACTION_WARNINGS
  ini:
  - key: action_warnings
    section: defaults
  name: Toggle action warnings
  type: boolean
  version_added: '2.5'
AGNOSTIC_BECOME_PROMPT:
  default: true
  description: Display an agnostic become prompt instead of displaying a prompt containing
    the command line supplied become method
  env:
  - name: ANSIBLE_AGNOSTIC_BECOME_PROMPT
  ini:
  - key: agnostic_become_prompt
    section: privilege_escalation
  name: Display an agnostic become prompt
  type: boolean
  version_added: '2.5'
  yaml:
    key: privilege_escalation.agnostic_become_prompt
.....
  1. 如果您想看到所有可能的配置参数以及它们的当前值的简单显示(无论它们是从环境变量还是配置文件中的一个配置的),您可以运行以下命令:
$ ansible-config dump 

输出显示了所有配置参数(以环境变量格式),以及当前的设置。如果参数配置为其默认值,则会告诉您(请参阅每个参数名称后的(default)元素):

$ ansible-config dump
ACTION_WARNINGS(default) = True
AGNOSTIC_BECOME_PROMPT(default) = True
ALLOW_WORLD_READABLE_TMPFILES(default) = False
ANSIBLE_CONNECTION_PATH(default) = None
ANSIBLE_COW_PATH(default) = None
ANSIBLE_COW_SELECTION(default) = default
ANSIBLE_COW_WHITELIST(default) = ['bud-frogs', 'bunny', 'cheese', 'daemon', 'default', 'dragon', 'elephant-in-snake', 'elephant', 'eyes', 'hellokitty', 'kitty', 'luke-koala', 'meow', 'milk', 'moofasa', 'moose', 'ren', 'sheep', 'small', 'stegosaurus', 'stimpy', 'supermilker', 'three-eyes', 'turkey', 'turtle', 'tux', 'udder', 'vader-koala', 'vader', 'www']
ANSIBLE_FORCE_COLOR(default) = False
ANSIBLE_NOCOLOR(default) = False
ANSIBLE_NOCOWS(default) = False
ANSIBLE_PIPELINING(default) = False
ANSIBLE_SSH_ARGS(default) = -C -o ControlMaster=auto -o ControlPersist=60s
ANSIBLE_SSH_CONTROL_PATH(default) = None
ANSIBLE_SSH_CONTROL_PATH_DIR(default) = ~/.ansible/cp
....
  1. 通过编辑其中一个配置参数,让我们看看这个输出的影响。通过设置环境变量来实现这一点,如下所示(此命令已在bash shell 中进行了测试,但对于其他 shell 可能有所不同):
$ export ANSIBLE_FORCE_COLOR=True 

现在,让我们重新运行ansible-config命令,但这次让它告诉我们只有从默认值更改的参数:

$ ansible-config dump --only-change
ANSIBLE_FORCE_COLOR(env: ANSIBLE_FORCE_COLOR) = True

在这里,您可以看到ansible-config告诉我们,我们只更改了ANSIBLE_FORCE_COLOR的默认值,它设置为True,并且我们通过env变量设置了它。这非常有价值,特别是如果您必须调试配置问题。

在处理 Ansible 配置文件本身时,您会注意到它是 INI 格式,意味着它有[defaults]等部分,格式为key = value的参数,以及以#;开头的注释。您只需要在配置文件中放置您希望从默认值更改的参数,因此,如果您想要创建一个简单的配置来更改默认清单文件的位置,它可能如下所示:

# Set my configuration variables
[defaults]
inventory = /Users/danieloh/ansible/hosts ; Here is the path of the inventory file

正如前面讨论的那样,ansible.cfg配置文件的可能有效位置之一是您当前的工作目录。很可能这是在您的主目录中,因此在多用户系统上,我们强烈建议您将对 Ansible 配置文件的访问权限限制为仅限于您的用户帐户。在多用户系统上保护重要配置文件时,您应该采取所有通常的预防措施,特别是因为 Ansible 通常用于配置多个远程系统,因此如果配置文件被意外损坏,可能会造成很大的损害!

当然,Ansible 的行为不仅由配置文件和开关控制,您传递给各种 Ansible 可执行文件的命令行参数也非常重要。实际上,我们已经在先前的示例中使用了其中一个——在前面的示例中,我们向您展示了如何使用ansible.cfg中的inventory参数更改 Ansible 查找清单文件的位置。然而,在本书先前介绍的许多示例中,我们使用-i开关覆盖了这一点。因此,让我们继续下一节,看看在运行 Ansible 时使用命令行参数的用法。

命令行参数

在本节中,您将学习有关使用命令行参数执行 playbook 以及如何将一些常用的参数应用到您的优势中。我们已经非常熟悉其中一个参数,即--version开关,我们用它来确认 Ansible 是否已安装(以及安装的版本):

$ ansible 2.9.6
  config file = None
  configured module search path = ['/Users/james/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/local/Cellar/ansible/2.9.6_1/libexec/lib/python3.8/site-packages/ansible
  executable location = /usr/local/bin/ansible
  python version = 3.8.2 (default, Mar 11 2020, 00:28:52) [Clang 11.0.0 (clang-1100.0.33.17)]

就像我们能够直接通过 Ansible 了解各种配置参数一样,我们也可以了解命令行参数。几乎所有的 Ansible 可执行文件都有一个--help选项,您可以运行它来显示有效的命令行参数。现在让我们试一试:

  1. 当您执行ansible命令行时,您可以查看所有选项和参数。使用以下命令:
$ ansible --help 

运行上述命令时,您将看到大量有用的输出;以下代码块显示了一个示例(您可能希望将其导入到分页器中,例如less,以便您可以轻松阅读所有内容):

$ ansible --help
usage: ansible [-h] [--version] [-v] [-b] [--become-method BECOME_METHOD] [--become-user BECOME_USER] [-K] [-i INVENTORY] [--list-hosts] [-l SUBSET] [-P POLL_INTERVAL] [-B SECONDS] [-o] [-t TREE] [-k]
               [--private-key PRIVATE_KEY_FILE] [-u REMOTE_USER] [-c CONNECTION] [-T TIMEOUT] [--ssh-common-args SSH_COMMON_ARGS] [--sftp-extra-args SFTP_EXTRA_ARGS] [--scp-extra-args SCP_EXTRA_ARGS]
               [--ssh-extra-args SSH_EXTRA_ARGS] [-C] [--syntax-check] [-D] [-e EXTRA_VARS] [--vault-id VAULT_IDS] [--ask-vault-pass | --vault-password-file VAULT_PASSWORD_FILES] [-f FORKS]
               [-M MODULE_PATH] [--playbook-dir BASEDIR] [-a MODULE_ARGS] [-m MODULE_NAME]
               pattern

Define and run a single task 'playbook' against a set of hosts

positional arguments:
  pattern host pattern

optional arguments:
  --ask-vault-pass ask for vault password
  --list-hosts outputs a list of matching hosts; does not execute anything else
  --playbook-dir BASEDIR
                        Since this tool does not use playbooks, use this as a substitute playbook directory.This sets the relative path for many features including roles/ group_vars/ etc.
  --syntax-check perform a syntax check on the playbook, but do not execute it
  --vault-id VAULT_IDS the vault identity to use
  --vault-password-file VAULT_PASSWORD_FILES
                        vault password file
  --version show program's version number, config file location, configured module search path, module location, executable location and exit
  -B SECONDS, --background SECONDS
                        run asynchronously, failing after X seconds (default=N/A)
  -C, --check don't make any changes; instead, try to predict some of the changes that may occur
  -D, --diff when changing (small) files and templates, show the differences in those files; works great with --check
  -M MODULE_PATH, --module-path MODULE_PATH
                        prepend colon-separated path(s) to module library (default=~/.ansible/plugins/modules:/usr/share/ansible/plugins/modules)
  -P POLL_INTERVAL, --poll POLL_INTERVAL
                        set the poll interval if using -B (default=15)
  -a MODULE_ARGS, --args MODULE_ARGS
                        module arguments
  -e EXTRA_VARS, --extra-vars EXTRA_VARS
                        set additional variables as key=value or YAML/JSON, if filename prepend with @
  1. 我们可以从前面的代码中选取一个示例来扩展我们之前对ansible的使用;到目前为止,我们几乎完全使用它来使用-m-a参数运行临时任务。但是,ansible还可以执行有用的任务,例如告诉我们清单中组的主机。我们可以使用本章前面使用的production-inventory文件来探索这一点:
$ ansible -i production-inventory --list-host appservers_emea_zone 

运行此命令时,您应该会看到列出appservers_emea_zone清单组的成员。尽管这个例子可能有点牵强,但当您开始使用动态清单文件并且不能再简单地将清单文件传输到终端以查看内容时,这个例子是非常有价值的:

$ ansible -i production-inventory --list-host appservers_emea_zone
  hosts (2):
    appserver1-emea.example.com
    appserver2-emea.example.com

ansible-playbook可执行文件也是如此。我们已经在本书的先前示例中看到了其中一些,并且还有更多可以做的。例如,前面我们讨论了使用ssh-agent来管理多个 SSH 身份验证密钥。虽然这使运行 playbook 变得简单(因为您不必向 Ansible 传递任何身份验证参数),但这并不是唯一的方法。您可以使用ansible-playbook的命令行参数之一来指定私有 SSH 密钥文件,如下所示:

$ ansible-playbook -i production-inventory site.yml --private-key ~/keys/id_rsa

同样,在前一节中,我们在 playbook 中指定了remote_user变量以便 Ansible 连接。然而,命令行参数也可以为 playbook 设置此参数;因此,我们可以完全删除remote_user行,并改用以下命令行字符串运行它:

$ ansible-playbook -i production-inventory site.yml --user danieloh

Ansible 的最终目标是使您的生活更简单,并从您的清单中删除单调的日常任务。因此,没有正确或错误的方法来做到这一点——您可以使用命令行参数指定您的私有 SSH 密钥,也可以使用ssh-agent使其可用。同样,您可以在 playbook 中放置remote_user行,也可以在命令行上使用--user参数。最终,选择权在您手中,但重要的是要考虑,如果您将 playbook 分发给多个用户,并且他们都必须记住在命令行上指定远程用户,他们是否会真的记得这样做?如果他们不这样做会有什么后果?如果remote_user行存在于 playbook 中,是否会使他们的生活更轻松,并且更不容易出错,因为用户帐户已在 playbook 中设置?

与 Ansible 的配置一样,您将经常使用一小部分命令行参数,而您可能永远不会接触到许多命令行参数。重要的是您知道它们的存在以及如何了解它们,并且您可以对何时使用它们做出明智的决定。让我们继续到下一节,在那里我们将更详细地查看使用 Ansible 的临时命令。

理解临时命令

到目前为止,我们已经在本书中看到了一些临时命令,但是为了回顾,它们是您可以使用 Ansible 运行的单个命令,利用 Ansible 模块而无需创建或保存 playbook。它们非常有用,可以在许多远程机器上执行快速的一次性任务,也可以用于测试和了解您打算在 playbook 中使用的 Ansible 模块的行为。它们既是一个很好的学习工具,也是一个快速而肮脏(因为您从不使用 playbook 记录您的工作!)的自动化解决方案。

与每个 Ansible 示例一样,我们需要一个清单来运行。让我们重用之前的production-inventory文件:

[frontends_na_zone]
frontend1-na.example.com
frontend2-na.example.com

[frontends_emea_zone]
frontend1-emea.example.com
frontend2-emea.example.com

[appservers_na_zone]
appserver1-na.example.com
appserver2-na.example.com

[appservers_emea_zone]
appserver1-emea.example.com
appserver2-emea.example.com

现在,让我们从可能是最快最肮脏的临时命令开始——在一组远程机器上运行原始 shell 命令。假设您想要检查 EMEA 地区所有前端服务器的日期和时间是否同步——您可以使用监控工具或手动依次登录到每台服务器并检查日期和时间来执行此操作。但是,您也可以使用 Ansible 的临时命令:

  1. 运行以下临时命令,从所有frontends_emea_zone服务器检索当前日期和时间:
$ ansible -i production-inventory frontends_emea_zone -a /usr/bin/date 

您将看到 Ansible 忠实地依次登录到每台机器并运行date命令,返回当前日期和时间。您的输出将如下所示:

$ ansible -i production-inventory frontends_emea_zone -a /usr/bin/date
frontend1-emea.example.com | CHANGED | rc=0 >>
Sun 5 Apr 18:55:30 BST 2020
frontend2-emea.example.com | CHANGED | rc=0 >>
Sun 5 Apr 18:55:30 BST 2020
  1. 该命令是在您登录时运行的用户帐户中运行的。您可以使用命令行参数(在前一节中讨论)作为不同的用户运行:
$ ansible -i production-inventory frontends_emea_zone -a /usr/sbin/pvs -u danieloh

frontend2-emea.example.com | FAILED | rc=5 >>
  WARNING: Running as a non-root user. Functionality may be unavailable.
  /run/lvm/lvmetad.socket: access failed: Permission denied
  WARNING: Failed to connect to lvmetad. Falling back to device scanning.
  /run/lock/lvm/P_global:aux: open failed: Permission denied
  Unable to obtain global lock.non-zero return code
frontend1-emea.example.com | FAILED | rc=5 >>
  WARNING: Running as a non-root user. Functionality may be unavailable.
  /run/lvm/lvmetad.socket: access failed: Permission denied
  WARNING: Failed to connect to lvmetad. Falling back to device scanning.
  /run/lock/lvm/P_global:aux: open failed: Permission denied
  Unable to obtain global lock.non-zero return code
  1. 在这里,我们可以看到danieloh用户帐户没有成功运行pvs命令所需的权限。但是,我们可以通过添加--become命令行参数来解决这个问题,该参数告诉 Ansible 在远程系统上成为root
$ ansible -i production-inventory frontends_emea_zone -a /usr/sbin/pvs -u danieloh --become

frontend2-emea.example.com | FAILED | rc=-1 >>
Missing sudo password
frontend1-emea.example.com | FAILED | rc=-1 >>
Missing sudo password
  1. 我们可以看到,该命令仍然失败,因为虽然danieloh/etc/sudoers中,但是不允许以root身份运行命令而不输入sudo密码。幸运的是,有一个开关可以让 Ansible 在运行时提示我们,这意味着我们不需要编辑我们的/etc/sudoers文件:
$ ansible -i production-inventory frontends_emea_zone -a /usr/sbin/pvs -u danieloh --become --ask-become-pass
BECOME password:

frontend1-emea.example.com | CHANGED | rc=0 >>
 PV VG Fmt Attr PSize PFree
 /dev/sda2 centos lvm2 a-- <19.00g 0
frontend2-emea.example.com | CHANGED | rc=0 >>
 PV VG Fmt Attr PSize PFree
 /dev/sda2 centos lvm2 a-- <19.00g 0
  1. 默认情况下,如果您不使用-m命令行参数指定模块,Ansible 会假定您想要使用command模块(参见docs.ansible.com/ansible/latest/modules/command_module.html)。如果您希望使用特定模块,可以在命令行参数中添加-m开关,然后在-a开关下指定模块参数,如下例所示:
$ ansible -i production-inventory frontends_emea_zone -m copy -a "src=/etc/yum.conf dest=/tmp/yum.conf"
frontend1-emea.example.com | CHANGED => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python"
    },
    "changed": true,
    "checksum": "e0637e631f4ab0aaebef1a6b8822a36f031f332e",
    "dest": "/tmp/yum.conf",
    "gid": 0,
    "group": "root",
    "md5sum": "a7dc0d7b8902e9c8c096c93eb431d19e",
    "mode": "0644",
    "owner": "root",
    "size": 970,
    "src": "/root/.ansible/tmp/ansible-tmp-1586110004.75-208447517347027/source",
    "state": "file",
    "uid": 0
}
frontend2-emea.example.com | CHANGED => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python"
    },
    "changed": true,
    "checksum": "e0637e631f4ab0aaebef1a6b8822a36f031f332e",
    "dest": "/tmp/yum.conf",
    "gid": 0,
    "group": "root",
    "md5sum": "a7dc0d7b8902e9c8c096c93eb431d19e",
    "mode": "0644",
    "owner": "root",
    "size": 970,
    "src": "/root/.ansible/tmp/ansible-tmp-1586110004.75-208447517347027/source",
    "state": "file",
    "uid": 0
} 

前面的输出不仅显示了成功将复制到两个主机的操作,还显示了copy模块的所有输出值。这在以后开发 playbook 时非常有帮助,因为它使您能够准确了解模块的工作原理以及在需要进一步处理输出的情况下产生的输出。然而,这是一个更高级的话题,超出了本章的范围。

您还会注意到,传递给模块的所有参数都必须用引号括起来(")。所有参数都被指定为key=value对,keyvalue之间不应添加空格(例如,key = value是不可接受的)。如果您需要在一个参数值周围放置引号,可以使用反斜杠字符进行转义(例如,-a "src=/etc/yum.conf dest=\"/tmp/yum file.conf\""

到目前为止,我们执行的所有示例都非常快速,但这并不总是计算任务的情况。当您需要长时间运行操作时,比如超过两个小时,您应该考虑将其作为后台进程运行。在这种情况下,您可以异步运行命令,并稍后确认执行的结果。

例如,要在后台异步执行sleep 2h,并设置超时为 7,200 秒(-B),并且不进行轮询(-P),请使用以下命令:

$ ansible -i production-inventory frontends_emea_zone -B 7200 -P 0 -a "sleep 2h"
frontend1-emea.example.com | CHANGED => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python"
    },
    "ansible_job_id": "537978889103.8857",
    "changed": true,
    "finished": 0,
    "results_file": "/root/.ansible_async/537978889103.8857",
    "started": 1
}
frontend2-emea.example.com | CHANGED => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python"
    },
    "ansible_job_id": "651461662130.8858",
    "changed": true,
    "finished": 0,
    "results_file": "/root/.ansible_async/651461662130.8858",
    "started": 1
}

请注意,此命令的输出为每个主机上的每个任务提供了唯一的作业 ID。现在假设我们想要查看第二个前端服务器上的任务进展。只需从您的 Ansible 控制机发出以下命令:

$ ansible -i production-inventory frontend2-emea.example.com -m async_status -a "jid=651461662130.8858"
frontend2-emea.example.com | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python"
    },
    "ansible_job_id": "651461662130.8858",
    "changed": false,
    "finished": 0,
    "started": 1
} 

在这里,我们可以看到作业已经开始但尚未完成。如果我们现在终止我们发出的sleep命令并再次检查状态,我们可以看到以下内容:

$ ansible -i production-inventory frontend2-emea.example.com -m async_status -a "jid=651461662130.8858"
frontend2-emea.example.com | FAILED! => {
 "ansible_facts": {
 "discovered_interpreter_python": "/usr/bin/python"
 },
 "ansible_job_id": "651461662130.8858",
 "changed": true,
 "cmd": [
 "sleep",
 "2h"
 ],
 "delta": "0:03:16.534212",
 "end": "2020-04-05 19:18:08.431258",
 "finished": 1,
 "msg": "non-zero return code",
 "rc": -15,
 "start": "2020-04-05 19:14:51.897046",
 "stderr": "",
 "stderr_lines": [],
 "stdout": "",
 "stdout_lines": []
}

在这里,我们看到了一个FAILED状态的结果,因为sleep命令被终止;它没有干净地退出,并返回了一个-15的代码(请参阅rc参数)。当它被终止时,没有输出被发送到stdoutstderr,但如果有的话,Ansible 会捕获它并在前面的代码中显示它,这将有助于您调试失败。还包括了许多其他有用的信息,包括任务实际运行的时间、结束时间等。同样,当任务干净地退出时,也会返回有用的输出。

这就结束了我们对 Ansible 中的临时命令的介绍。到目前为止,您应该对 Ansible 的基本原理有了相当扎实的掌握,但还有一件重要的事情我们还没有看到,即使我们简要提到过——变量以及如何定义它们。我们将在下一节继续讨论这个问题。

定义变量

在本节中,我们将介绍变量的主题以及如何在 Ansible 中定义它们。您将逐步学习变量应该如何定义,并了解如何在 Ansible 中使用它们。

尽管自动化消除了以前手动任务中的大部分重复,但并非每个系统都是相同的。如果两个系统在某些细微的方式上不同,您可以编写两个独特的 playbook——一个用于每个系统。然而,这将是低效和浪费的,随着时间的推移也很难管理(例如,如果一个 playbook 中的代码发生了变化,您如何确保它在第二个变体中得到更新?)。

同样,您可能需要在一个系统中使用另一个系统的值——也许您需要获取数据库服务器的主机名并使其可用于另一个系统。所有这些问题都可以通过变量来解决,因为它们允许相同的自动化代码以参数变化的方式运行,以及将值从一个系统传递到另一个系统(尽管这必须小心处理)。

让我们开始实际看一下在 Ansible 中定义变量。

Ansible 中的变量应具有格式良好的名称,符合以下规则:

  • 变量的名称只能包含字母、下划线和数字,不允许包含空格。

  • 变量的名称只能以字母开头,可以包含数字,但不能以数字开头。

例如,以下是良好的变量名称:

  • external_svc_port

  • internal_hostname_ap1

然而,以下示例都是无效的,不能使用:

  • appserver-zone-na

  • cache server ip

  • dbms.server.port

  • 01appserver

如在学习 YAML 语法部分中讨论的,变量可以以字典结构定义,例如以下方式。所有值都以键值对的形式声明:

region:
  east: app
  west: frontend
  central: cache

为了从前面的字典结构中检索特定字段,您可以使用以下任一表示法:

# bracket notation
region['east']

# dot notation
region.east

有一些例外情况;例如,如果变量名以两个下划线开头和结尾(例如__variable__),或包含已知的公共属性,您应该使用括号表示法

  • as_integer_ratio

  • symmetric_difference

您可以在docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html#creating-valid-variable-names找到更多信息。

当定义主机变量时,这种字典结构是有价值的;尽管在本章的早些时候,我们使用了一个定义为 Ansible variables文件的虚构员工记录集,但您可以使用它来指定一些redis服务器参数等内容:

---
redis:
  - server: cacheserver01.example.com
    port: 6379
    slaveof: cacheserver02.example.com

然后,这些可以通过您的 playbook 应用,并且一个通用的 playbook 可以用于所有redis服务器,而不管它们的配置如何,因为可变的参数,如portmaster服务器都包含在变量中。

您还可以直接在 playbook 中传递设置变量,并将它们传递给您调用的角色。例如,以下 playbook 代码调用了四个假设的角色,并且每个角色为username变量分配了不同的值。这些角色可以用于在服务器上设置各种管理角色(或多个服务器),每个角色都传递一个不断变化的用户名列表,因为公司的人员来来去去:

roles:
  - role: dbms_admin
    vars:
      username: James
  - role: system_admin
    vars:
      username: John
  - role: security_amdin
    vars:
      username: Rock
  - role: app_admin
    vars:
      username: Daniel

要从 playbook 中访问变量,只需将变量名放在引号括号中。考虑以下示例 playbook(基于我们之前的redis示例):

---
- name: Display redis variables
  hosts: all

  vars:
    redis:
      server: cacheserver01.example.com
      port: 6379
      slaveof: cacheserver02.example.com

  tasks:
    - name: Display the redis port
      debug:
        msg: "The redis port for {{ redis.server }} is {{ redis.port }}"

在这里,我们在 playbook 中定义了一个名为redis的变量。这个变量是一个字典,包含了一些对我们的服务器可能很重要的参数。为了访问这些变量的内容,我们使用花括号对它们进行配对(如前面所述),并且整个字符串被引号括起来,这意味着我们不必单独引用这些变量。如果您在本地机器上运行 playbook,您应该会看到以下输出:

$ ansible-playbook -i localhost, redis-playbook.yml

PLAY [Display redis variables] *************************************************

TASK [Gathering Facts] *********************************************************
ok: [localhost]

TASK [Display the redis port] **************************************************
ok: [localhost] => {
 "msg": "The redis port for cacheserver01.example.com is 6379"
}

PLAY RECAP *********************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

尽管我们在这里访问这些变量以在调试消息中打印它们,但您可以使用相同的花括号表示法将它们分配给模块参数,或者用于 playbook 需要它们的任何其他目的。

与许多语言一样,Ansible 也有特殊保留的变量,在 playbooks 中具有特定含义。在 Ansible 中,这些被称为魔术变量,您可以在docs.ansible.com/ansible/latest/reference_appendices/special_variables.html找到完整的列表。不用说,您不应该尝试使用任何魔术变量名称作为您自己的变量。您可能会遇到的一些常见的魔术变量如下:

  • inventory_hostname:在播放中迭代的当前主机的主机名

  • groups:清单中主机组的字典,以及每个组的主机成员资格

  • group_names:当前主机(由inventory_hostname指定)所属的组的列表

  • hostvars:清单中所有主机和分配给它们的变量的字典

例如,可以在播放中的任何时候使用hostvars访问所有主机的主机变量,即使您只对一个特定主机进行操作。在 playbook 中,魔术变量非常有用,您将迅速开始发现自己在使用它们,因此了解它们的存在非常重要。

您还应该注意,您可以在多个位置指定 Ansible 变量。 Ansible 具有严格的变量优先级顺序,您可以利用这一点,在优先级较低的位置设置变量的默认值,然后在播放中稍后覆盖它们。这对于各种原因都很有用,特别是当未定义的变量可能在运行 playbook 时造成混乱(甚至当 playbook 由于此原因失败时)。我们尚未讨论变量可以存储的所有位置,因此此处未给出变量优先级顺序的完整列表。

此外,它可能会在 Ansible 版本之间发生变化,因此在处理和理解变量优先级时,重要的是参考文档——请访问docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html#variable-precedence-where-should-i-put-a-variable获取更多信息。

这就结束了我们对 Ansible 中变量的简要概述,尽管我们将在本书的后续示例中再次看到它们的使用。现在让我们通过查看 Jinja2 过滤器来结束本章,它为您的变量定义增添了无限的力量。

理解 Jinja2 过滤器

由于 Ansible 是用 Python 编写的,它继承了一个非常有用和强大的模板引擎,称为 Jinja2。我们将在本书的后面看到模板化的概念,因此现在我们将专注于 Jinja2 的一个特定方面,即过滤器。 Jinja2 过滤器提供了一个非常强大的框架,您可以使用它来操作和转换数据。也许您有一个需要转换为小写的字符串,例如-您可以应用 Jinja2 过滤器来实现这一点。您还可以使用它来执行模式匹配、搜索和替换操作等等。有数百种过滤器供您使用,在本节中,我们希望为您提供对 Jinja2 过滤器的基本理解以及如何应用它们的实际知识,并向您展示如何获取更多关于它们的信息,如果您希望进一步探索这个主题。

值得注意的是,Jinja2 操作是在 Ansible 控制主机上执行的,只有过滤器操作的结果被发送到远程主机。这是出于设计考虑,既为了一致性,也为了尽可能减少各个节点的工作量。

让我们通过一个实际的例子来探讨这个。假设我们有一个包含一些我们想要解析的数据的 YAML 文件。我们可以很容易地从机器文件系统中读取文件,并使用register关键字来捕获结果(register捕获任务的结果并将其存储在一个变量中——在运行shell模块的情况下,它会捕获命令的所有输出)。

我们的 YAML 数据文件可能如下所示:

tags:
  - key: job
    value: developer
  - key: language
    value: java

现在,我们可以创建一个 playbook 来读取这个文件并注册结果,但是我们如何将其实际转换为 Ansible 可以理解和使用的变量结构呢?让我们考虑下面的 playbook:

---
- name: Jinja2 filtering demo 1
  hosts: localhost

  tasks:
    - copy:
        src: multiple-document-strings.yaml
        dest: /tmp/multiple-document-strings.yaml
    - shell: cat /tmp/multiple-document-strings.yaml
      register: result
    - debug:
        msg: '{{ item }}'
      loop: '{{ result.stdout | from_yaml_all | list }}'

shell模块不一定是从 playbook 所在的目录运行的,所以我们不能保证它会找到我们的multiple-document-strings.yaml文件。然而,copy模块会从当前目录中获取文件,所以可以使用它将文件复制到一个已知的位置(比如/tmp),以便shell模块从中读取文件。然后在loop模块中运行debug模块。loop模块用于遍历shell命令的所有stdout行,因为我们使用了两个 Jinja2 过滤器——from_yaml_alllist

from_yaml_all过滤器解析源文档行为 YAML,然后list过滤器将解析后的数据转换为有效的 Ansible 列表。如果我们运行 playbook,我们应该能够看到 Ansible 对原始文件中的数据结构的表示。

$ ansible-playbook -i localhost, jinja-filtering1.yml

PLAY [Jinja2 filtering demo 1] *************************************************

TASK [Gathering Facts] *********************************************************
ok: [localhost]

TASK [copy] ********************************************************************
ok: [localhost]

TASK [shell] *******************************************************************
changed: [localhost]

TASK [debug] *******************************************************************
ok: [localhost] => (item={'tags': [{'value': u'developer', 'key': u'job'}, {'value': u'java', 'key': u'language'}]}) => {
 "msg": {
 "tags": [
 {
 "key": "job",
 "value": "developer"
 },
 {
 "key": "language",
 "value": "java"
 }
 ]
 }
}

PLAY RECAP *********************************************************************
localhost : ok=4 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

正如你所看到的,我们生成了一个包含key-value对的字典列表。

如果这个数据结构已经存储在我们的 playbook 中,我们可以再进一步使用items2dict过滤器将列表转换为真正的key: value对,从数据结构中移除keyvalue项。例如,考虑下面的第二个 playbook:

---
- name: Jinja2 filtering demo 2
  hosts: localhost
  vars:
    tags:
      - key: job
        value: developer
      - key: language
        value: java

  tasks:
    - debug:
        msg: '{{ tags | items2dict }}'

现在,如果我们运行这个,我们可以看到我们的数据被转换成了一组漂亮整洁的key: value对。

$ ansible-playbook -i localhost, jinja2-filtering2.yml
[WARNING]: Found variable using reserved name: tags

PLAY [Jinja2 filtering demo 2] *************************************************

TASK [Gathering Facts] *********************************************************
ok: [localhost]

TASK [debug] *******************************************************************
ok: [localhost] => {
 "msg": {
 "job": "developer",
---
 "language": "java"
 }
}

PLAY RECAP *********************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

观察一下 playbook 顶部的警告。如果你尝试使用保留名称作为变量,Ansible 会显示警告,就像我们在这里做的一样。通常情况下,你不应该使用保留名称创建变量,但是这个例子展示了过滤器的工作原理,以及如果你做一些可能会引起问题的事情,Ansible 会尝试警告你。

在本节的早些时候,我们使用了shell模块来读取文件,并使用register将结果存储在一个变量中。这是完全可以的,虽然有点不够优雅。Jinja2 包含一系列lookup过滤器,其中包括读取给定文件内容的功能。让我们来看看下面的 playbook 的行为:

---
- name: Jinja2 filtering demo 3
  hosts: localhost
  vars:
    ping_value: "{{ lookup('file', '/etc/hosts') }}"
  tasks:
    - debug:
        msg: "ping value is {{ ping_value }}"

当我们运行这个时,我们可以看到 Ansible 已经为我们捕获了/etc/hosts文件的内容,而不需要我们像之前那样使用copyshell模块。

$ ansible-playbook -i localhost, jinja2-filtering3.yml

PLAY [Jinja2 filtering demo 3] *************************************************

TASK [Gathering Facts] *********************************************************
ok: [localhost]

TASK [debug] *******************************************************************
ok: [localhost] => {
 "msg": "ping value is 127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4\n::1 localhost localhost.localdomain localhost6 localhost6.localdomain6\n\n"
}

PLAY RECAP *********************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

有许多其他过滤器可能会让你感兴趣,完整列表可以在官方的 Jinja2 文档中找到(jinja.palletsprojects.com/en/2.11.x/)。以下是一些其他示例,可以让你了解 Jinja2 过滤器可以为你实现的功能,从引用字符串到连接列表,再到获取文件的有用路径信息:

# Add some quotation in the shell - shell: echo {{ string_value | quote }} # Concatenate a list into a specific string
{{ list | join("$") }}

# Have the last name of a specific file path
{{  path  |  basename  }}

# Have the directory from a specific path
{{  path  |  dirname  }} # Have the directory from a specific windows path
{{  path  |  win_dirname  }} 

这就结束了我们对 Jinja2 过滤器的介绍。这是一个庞大的主题,值得有一本专门的书来讲解,但是,我希望这个实用指南能给你一些开始和寻找信息的指引。

总结

Ansible 是一个非常强大和多功能的自动化引擎,可用于各种任务。在解决 playbook 创建和大规模自动化的更复杂挑战之前,了解如何使用它的基础知识至关重要。Ansible 依赖一种称为 YAML 的语言,这是一种简单易读(和写)的语法,支持快速开发易于阅读和易于维护的代码,并从其编写的 Python 语言中继承了许多有价值的特性,包括 Jinja2 过滤器。

在本章中,您学习了使用各种 Ansible 程序的基础知识。然后,您了解了 YAML 语法以及将代码分解为可管理的块的方法,以便更容易阅读和维护。我们探讨了在 Ansible 中使用临时命令、变量定义和结构,以及如何利用 Jinja2 过滤器来操作 playbooks 中的数据。

在下一章中,我们将更深入地了解 Ansible 清单,并探索一些更高级的概念,这些概念在处理它们时可能会对您有用。

问题

  1. Ansible 的哪个组件允许您定义一个块以执行任务组作为 play?

A) handler

B) service

C) hosts

D) tasks

E) name

  1. 您使用 YAML 格式的哪种基本语法来开始一个文件?

A) ###

B) ---

C) %%%

D) ===

E) ***

  1. 真或假 - 为了解释和转换 Ansible 中的输出数据,您需要使用 Jinja2 模板。

A) True

B) False

进一步阅读

第三章:定义您的清单

正如我们在前两章中已经讨论过的,除非告诉它负责哪些主机,否则 Ansible 无法做任何事情。这当然是合乎逻辑的——无论自动化工具有多容易使用和设置,你都不希望它简单地控制网络上的每个设备。因此,至少,您必须告诉 Ansible 它将自动化任务的主机是哪些,这在最基本的术语中就是清单。

然而,清单中有很多东西不仅仅是自动化目标的列表。Ansible 清单可以以几种格式提供;它们可以是静态的或动态的,并且它们可以包含定义 Ansible 与每个主机(或主机组)交互的重要变量。因此,它们值得有一个章节来讨论,而在本章中,我们将对清单进行实际探索,以及如何在使用 Ansible 自动化基础设施时充分利用它们。

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

  • 创建清单文件并添加主机

  • 生成动态清单文件

  • 使用模式进行特殊主机管理

技术要求

本章假设您已经按照第一章 开始使用 Ansible中详细说明的设置了控制主机,并且您正在使用最新版本——本章的示例是使用 Ansible 2.9 进行测试的。本章还假设您至少有一个额外的主机进行测试,并且最好是基于 Linux 的。尽管本章将给出主机名的具体示例,但您可以自由地用自己的主机名和/或 IP 地址替换它们,如何做到这一点的详细信息将在适当的地方提供。

本章的代码包在此处可用:github.com/PacktPublishing/Ansible-2-Cookbook/tree/master/Chapter%203

创建清单文件并添加主机

每当您在 Ansible 中看到“创建清单”的参考时,通常可以安全地假定它是一个静态清单。Ansible 支持两种类型的清单——静态和动态,我们将在本章后面讨论后者。静态清单本质上是静态的;除非有人去手动编辑它们,否则它们是不会改变的。当您开始测试 Ansible 时,这是一个很好的选择,因为它为您提供了一个非常快速和简单的方法来快速启动和运行。即使在小型封闭环境中,静态清单也是管理环境的好方法,特别是在基础设施的更改不频繁时。

大多数 Ansible 安装将在/etc/ansible/hosts中寻找默认的清单文件(尽管这个路径在 Ansible 配置文件中是可配置的,如第二章 理解 Ansible 的基础知识中所讨论的)。您可以填充此文件,或为每个 playbook 运行提供自己的清单,通常可以看到清单与 playbooks 一起提供。毕竟,很少有“一刀切”的 playbook,尽管您可以使用组来细分您的清单(稍后会详细介绍),但通常提供一个较小的静态清单文件与特定的 playbook 一起提供也同样容易。正如您在本书的前几章中所看到的,大多数 Ansible 命令在不使用默认值时使用-i标志来指定清单文件的位置。假设情况下,这可能看起来像以下示例:

$ ansible -i /home/cloud-user/inventory all -m ping

您可能会遇到的大多数静态清单文件都是以 INI 格式创建的,尽管重要的是要注意其他格式也是可能的。在 INI 格式的文件之后,您将发现的最常见格式是 YAML 格式 - 您可以在这里找到更多关于您可以使用的清单文件类型的详细信息:docs.ansible.com/ansible/latest/user_guide/intro_inventory.html

在本章中,我们将为您提供一些 INI 和 YAML 格式的清单文件示例,供您考虑,因为您必须对两者都有所了解。就我个人而言,我已经使用 Ansible 工作了很多年,使用过 INI 格式的文件或动态清单,但他们说知识就是力量,所以了解一下这两种格式也无妨。

让我们从创建一个静态清单文件开始。这个清单文件将与默认清单分开。

/etc/ansible/my_inventory中创建一个清单文件,使用以下格式化的 INI 代码:

target1.example.com ansible_host=192.168.81.142 ansible_port=3333  target2.example.com ansible_port=3333 ansible_user=danieloh  target3.example.com ansible_host=192.168.81.143 ansible_port=5555

清单主机之间的空行不是必需的 - 它们只是为了使本书中的清单更易读而插入的。这个清单文件非常简单,不包括任何分组;但是,在引用清单时,您仍然可以使用特殊的all组来引用所有主机,这个组是隐式定义的,无论您如何格式化和划分您的清单文件。

上述文件中的每一行都包含一个清单主机。第一列包含 Ansible 将使用的清单主机名(并且可以通过我们在第二章中讨论的inventory_hostname魔术变量来访问)。之后同一行上的所有参数都是分配给主机的变量。这些可以是用户定义的变量或特殊的 Ansible 变量,就像我们在这里设置的一样。

有许多这样的变量,但前面的例子特别包括以下内容:

  • ansible_host:如果无法直接访问清单主机名 - 例如,因为它不在 DNS 中,那么这个变量包含 Ansible 将连接的主机名或 IP 地址。

  • ansible_port:默认情况下,Ansible 尝试通过 SSH 的 22 端口进行所有通信 - 如果您在另一个端口上运行 SSH 守护程序,可以使用此变量告诉 Ansible。

  • ansible_user:默认情况下,Ansible 将尝试使用您从中运行 Ansible 命令的当前用户帐户连接到远程主机 - 您可以以多种方式覆盖这一点,其中之一就是这个。

因此,前面的三个主机可以总结如下:

  • target1.example.com主机应该使用192.168.81.142IP 地址连接,端口为3333

  • target2.example.com主机也应该连接到端口3333,但这次使用danieloh用户,而不是运行 Ansible 命令的帐户。

  • target3.example.com主机应该使用192.168.81.143IP 地址连接,端口为5555

通过这种方式,即使没有进一步的构造,您也可以开始看到静态的 INI 格式的清单的强大之处。

现在,如果您想要创建与前面完全相同的清单,但这次以 YAML 格式进行格式化,您可以指定如下:

---
ungrouped:
  hosts:
    target1.example.com:
      ansible_host: 192.168.81.142
      ansible_port: 3333
    target2.example.com:
      ansible_port: 3333
      ansible_user: danieloh
    target3.example.com:
      ansible_host: 192.168.81.143
      ansible_port: 5555

您可能会遇到包含参数如ansible_ssh_portansible_ssh_hostansible_ssh_user的清单文件示例 - 这些变量名称(以及类似的其他变量)在 2.0 版本之前的 Ansible 版本中使用。对许多这些变量已经保持了向后兼容性,但在可能的情况下,您应该更新它们,因为这种兼容性可能在将来的某个时候被移除。

现在,如果您在 Ansible 中运行上述清单,使用一个简单的shell命令,结果将如下所示:

$ ansible -i /etc/ansible/my_inventory.yaml all -m shell -a 'echo hello-yaml' -f 5
target1.example.com | CHANGED | rc=0 >>
hello-yaml
target2.example.com | CHANGED | rc=0 >>
hello-yaml
target3.example.com | CHANGED | rc=0 >>
hello-yaml

这涵盖了创建一个简单静态清单文件的基础知识。现在让我们通过在本章的下一部分将主机组添加到清单中来扩展这一点。

使用主机组

很少有一个 playbook 适用于整个基础架构,尽管很容易告诉 Ansible 为不同的 playbook 使用备用清单,但这可能会变得非常混乱,非常快速,潜在地在你的网络中散布了数百个小清单文件。你可以想象这会变得多么难以管理,而 Ansible 的目的是使事情更容易管理,而不是相反。这个问题的一个可能简单的解决方案是开始在你的清单中添加组。

假设你有一个简单的三层 Web 架构,每层都有多个主机以实现高可用性和/或负载平衡。这种架构中的三个层可能是以下内容:

  • 前端服务器

  • 应用服务器

  • 数据库服务器

有了这个架构,让我们开始创建一个清单,再次混合使用 YAML 和 INI 格式,以便你在两种格式中都有经验。为了使示例清晰简洁,我们假设你可以使用它们的完全限定域名FQDNs)访问所有服务器,因此不会在这些清单文件中添加任何主机变量。当然,没有什么能阻止你这样做,每个示例都是不同的。

首先,让我们使用 INI 格式为三层前端创建清单。我们将称此文件为hostsgroups-ini,此文件的内容应该如下所示:

loadbalancer.example.com

[frontends]
frt01.example.com
frt02.example.com

[apps]
app01.example.com
app02.example.com

[databases]
dbms01.example.com
dbms02.example.com

在前面的清单中,我们创建了三个名为frontendsappsdatabases的组。请注意,在 INI 格式的清单中,组名放在方括号内。在每个组名下面是属于每个组的服务器名,因此前面的示例显示了每个组中的两个服务器。请注意顶部的异常值loadbalancer.example.com - 这个主机不属于任何组。所有未分组的主机必须放在 INI 格式文件的顶部。

在我们进一步进行之前,值得注意的是,清单也可以包含组的组,这对于通过不同的部门处理某些任务非常有用。前面的清单是独立的,但如果我们的前端服务器是建立在 Ubuntu 上,而应用和数据库服务器是建立在 CentOS 上呢?在处理这些主机的方式上会有一些根本的不同 - 例如,我们可能会在 Ubuntu 上使用apt模块来管理软件包,在 CentOS 上使用yum模块。

当然,我们可以使用从每个主机收集的事实来处理这种情况,因为这些事实将包含操作系统的详细信息。我们还可以创建清单的新版本,如下所示:

loadbalancer.example.com

[frontends]
frt01.example.com
frt02.example.com

[apps]
app01.example.com
app02.example.com

[databases]
dbms01.example.com
dbms02.example.com

[centos:children]
apps
databases

[ubuntu:children]
frontends

在组定义中使用children关键字(在方括号内),我们可以创建组的组;因此,我们可以进行巧妙的分组,以帮助我们的 playbook 设计,而无需多次指定每个主机。

INI 格式中的这种结构相当易读,但当转换为 YAML 格式时需要一些时间来适应。下面列出的代码显示了前面清单的 YAML 版本 - 就 Ansible 而言,两者是相同的,但你可以决定你更喜欢使用哪种格式:

all:
  hosts:
    loadbalancer.example.com:
  children:
    centos:
      children:
        apps:
          hosts:
            app01.example.com:
            app02.example.com:
        databases:
          hosts:
            dbms01.example.com:
            dbms02.example.com:
    ubuntu:
      children:
        frontends:
          hosts:
            frt01.example.com:
            frt02.example.com:

你可以看到children关键字仍然在 YAML 格式的清单中使用,但现在结构比 INI 格式更加分层。缩进可能更容易让你理解,但请注意主机最终是在相当高层次的缩进下定义的 - 这种格式可能更难扩展,取决于你希望采用的方法。

当你想要使用前面清单中的任何组时,你可以在你的 playbook 或命令行中简单地引用它。例如,在上一节中我们运行的,我们可以使用以下命令:

$ ansible -i /etc/ansible/my_inventory.yaml all -m shell -a 'echo hello-yaml' -f 5

请注意该行中间的all关键字。这是所有库存中都隐含的特殊all组,并且在你之前的 YAML 示例中明确提到。如果我们想运行相同的命令,但这次只在之前的 YAML 库存中的centos组主机上运行,我们将运行这个命令的变体:

$ ansible -i hostgroups-yml centos -m shell -a 'echo hello-yaml' -f 5
app01.example.com | CHANGED | rc=0 >>
hello-yaml
app02.example.com | CHANGED | rc=0 >>
hello-yaml
dbms01.example.com | CHANGED | rc=0 >>
hello-yaml
dbms02.example.com | CHANGED | rc=0 >>
hello-yaml 

正如你所看到的,这是一种管理库存并轻松运行命令的强大方式。创建多个组的可能性使生活变得简单和容易,特别是当你想在不同的服务器组上运行不同的任务时。

作为开发库存的一部分,值得注意的是,有一种快速的简写表示法,可以用来创建多个主机。假设你有 100 个应用服务器,所有的名称都是顺序的,如下所示:

[apps]
app01.example.com
app02.example.com
...
app99.example.com
app100.example.com

这是完全可能的,但手工创建将是乏味和容易出错的,并且会产生一些非常难以阅读和解释的库存。幸运的是,Ansible 提供了一种快速的简写表示法来实现这一点,以下库存片段实际上产生了一个与我们可以手动创建的相同的 100 个应用服务器的库存:

[apps]
app[01:100].prod.com

也可以使用字母范围以及数字范围——扩展我们的示例以添加一些缓存服务器,你可能会有以下内容:

[caches]
cache-[a:e].prod.com  

这与手动创建以下内容相同:

[caches]
cache-a.prod.com cache-b.prod.com
cache-c.prod.com
cache-d.prod.com
cache-e.prod.com 

现在我们已经完成了对各种静态库存格式的探索以及如何创建组(甚至是子组),让我们在下一节中扩展我们之前简要介绍的主机变量。

向库存添加主机和组变量

我们已经提到了主机变量——在本章的前面部分,当我们用它们来覆盖连接细节时,比如要连接的用户帐户、要连接的地址和要使用的端口。然而,你可以在 Ansible 和库存变量中做的事情远不止这些,重要的是要注意,它们不仅可以在主机级别定义,还可以在组级别定义,这再次为你提供了一些非常强大的方式来高效地管理你的基础设施。

让我们在之前的三层示例基础上继续建设,并假设我们需要为我们的两个前端服务器中的每一个设置两个变量。这些不是特殊的 Ansible 变量,而是完全由我们自己选择的变量,我们将在稍后运行对这台服务器的 playbook 中使用。假设这些变量如下:

  • https_port,定义了前端代理应该监听的端口

  • lb_vip,定义了前端服务器前面的负载均衡器的 FQDN

让我们看看这是如何完成的:

  1. 我们可以简单地将这些添加到我们库存文件中frontends部分的每个主机中,就像我们之前用 Ansible 连接变量做的那样。在这种情况下,我们的 INI 格式的库存的一部分可能是这样的:
[frontends]
frt01.example.com https_port=8443 lb_vip=lb.example.com
frt02.example.com https_port=8443 lb_vip=lb.example.com

如果我们对这个库存运行一个临时命令,我们可以看到这两个变量的内容:

$ ansible -i hostvars1-hostgroups-ini frontends -m debug -a "msg=\"Connecting to {{ lb_vip }}, listening on {{ https_port }}\""
frt01.example.com | SUCCESS => {
 "msg": "Connecting to lb.example.com, listening on 8443"
}
frt02.example.com | SUCCESS => {
 "msg": "Connecting to lb.example.com, listening on 8443"
}

这已经按我们的期望工作了,但这种方法效率低下,因为你必须将相同的变量添加到每个主机。

  1. 幸运的是,你可以将变量分配给主机组以及单独的主机。如果我们编辑前面的库存以实现这一点,frontends部分现在看起来像这样:
[frontends]
frt01.example.com
frt02.example.com

[frontends:vars]
https_port=8443
lb_vip=lb.example.com

请注意这种方式更易读?然而,如果我们对新组织的库存运行与之前相同的命令,我们会发现结果是一样的:

$ ansible -i groupvars1-hostgroups-ini frontends -m debug -a "msg=\"Connecting to {{ lb_vip }}, listening on {{ https_port }}\""
frt01.example.com | SUCCESS => {
 "msg": "Connecting to lb.example.com, listening on 8443"
}
frt02.example.com | SUCCESS => {
 "msg": "Connecting to lb.example.com, listening on 8443"
}
  1. 有时候你会想要为单个主机使用主机变量,有时候组变量更相关。由你来决定哪个对你的情况更好;然而,请记住主机变量可以组合使用。值得注意的是主机变量会覆盖组变量,所以如果我们需要将连接端口更改为8444,我们可以这样做:
[frontends]
frt01.example.com https_port=8444
frt02.example.com

[frontends:vars]
https_port=8443
lb_vip=lb.example.com

现在,如果我们再次使用新的清单运行我们的临时命令,我们可以看到我们已经覆盖了一个主机上的变量:

$ ansible -i hostvars2-hostgroups-ini frontends -m debug -a "msg=\"Connecting to {{ lb_vip }}, listening on {{ https_port }}\""
frt01.example.com | SUCCESS => {
 "msg": "Connecting to lb.example.com, listening on 8444"
}
frt02.example.com | SUCCESS => {
 "msg": "Connecting to lb.example.com, listening on 8443"
}

当然,当只有两个主机时,仅为一个主机执行此操作可能看起来有点无意义,但当你的清单中有数百个主机时,覆盖一个主机的这种方法突然变得非常有价值。

  1. 为了完整起见,如果我们要将之前定义的主机变量添加到我们的清单的 YAML 版本中,frontends部分将如下所示(其余清单已被删除以节省空间):
        frontends:
          hosts:
            frt01.example.com:
              https_port: 8444
            frt02.example.com:
          vars:
            https_port: 8443
            lb_vip: lb.example.com

运行与之前相同的临时命令,你会看到结果与我们的 INI 格式的清单相同:

$ ansible -i hostvars2-hostgroups-yml frontends -m debug -a "msg=\"Connecting to {{ lb_vip }}, listening on {{ https_port }}\""
frt01.example.com | SUCCESS => {
 "msg": "Connecting to lb.example.com, listening on 8444"
}
frt02.example.com | SUCCESS => {
 "msg": "Connecting to lb.example.com, listening on 8443"
}
  1. 到目前为止,我们已经介绍了几种向清单提供主机变量和组变量的方法;然而,还有一种方法值得特别提及,并且在你的清单变得更大更复杂时会变得有价值。

现在,我们的示例很小很简洁,只包含少数组和变量;然而,当你将其扩展到一个完整的服务器基础设施时,再次使用单个平面清单文件可能会变得难以管理。幸运的是,Ansible 也提供了解决方案。两个特别命名的目录host_varsgroup_vars,如果它们存在于剧本目录中,将自动搜索适当的变量内容。我们可以通过使用这种特殊的目录结构重新创建前面的前端变量示例来测试这一点,而不是将变量放入清单文件中。

让我们首先为此目的创建一个新的目录结构:

$ mkdir vartree
$ cd vartree
  1. 现在,在这个目录下,我们将为变量创建两个更多的目录:
$ mkdir host_vars group_vars
  1. 现在,在host_vars目录下,我们将创建一个文件,文件名为需要代理设置的主机名,后面加上.yml(即frt01.example.com.yml)。这个文件应该包含以下内容:
---
https_port: 8444
  1. 同样,在group_vars目录下,创建一个名为要分配变量的组的 YAML 文件(即frontends.yml),内容如下:
---
https_port: 8443
lb_vip: lb.example.com
  1. 最后,我们将像以前一样创建我们的清单文件,只是它不包含变量:
loadbalancer.example.com

[frontends]
frt01.example.com
frt02.example.com

[apps]
app01.example.com
app02.example.com

[databases]
dbms01.example.com
dbms02.example.com

为了清晰起见,你的最终目录结构应该是这样的:

$  tree
.
├── group_vars
│   └── frontends.yml
├── host_vars
│   └── frt01.example.com.yml
└── inventory

2 directories, 3 files
  1. 现在,让我们尝试运行我们熟悉的临时命令,看看会发生什么:
$ ansible -i inventory frontends -m debug -a "msg=\"Connecting to {{ lb_vip }}, listening on {{ https_port }}\""
frt02.example.com | SUCCESS => {
 "msg": "Connecting to lb.example.com, listening on 8443"
}
frt01.example.com | SUCCESS => {
 "msg": "Connecting to lb.example.com, listening on 8444"
}

正如你所看到的,这与以前完全一样,而且在没有进一步的指示的情况下,Ansible 已经遍历了目录结构并摄取了所有的变量文件。

  1. 如果你有数百个变量(或需要更精细的方法),你可以用主机和组的名字命名目录来替换 YAML 文件。现在,我们重新创建目录结构,但现在用目录代替:
$ tree
.
├── group_vars
│   └── frontends
│       ├── https_port.yml
│       └── lb_vip.yml
├── host_vars
│   └── frt01.example.com
│       └── main.yml
└── inventory

注意我们现在有了以frontends组和frt01.example.com主机命名的目录?在frontends目录中,我们将变量分成了两个文件,这对于在组中逻辑地组织变量尤其有用,特别是当你的剧本变得更大更复杂时。

这些文件本身只是我们之前的文件的一种改编:

$ cat host_vars/frt01.example.com/main.yml
---
https_port: 8444

$ cat group_vars/frontends/https_port.yml
---
https_port: 8443

$ cat group_vars/frontends/lb_vip.yml
---
lb_vip: lb.example.com

即使使用这种更细分的目录结构,运行临时命令的结果仍然是相同的:

$ ansible -i inventory frontends -m debug -a "msg=\"Connecting to {{ lb_vip }}, listening on {{ https_port }}\""
frt01.example.com | SUCCESS => {
 "msg": "Connecting to lb.example.com, listening on 8444"
}
frt02.example.com | SUCCESS => {
 "msg": "Connecting to lb.example.com, listening on 8443"
}
  1. 在我们结束本章之前,还有一件事需要注意,即如果您在组级别和子组级别同时定义了相同的变量,则子组级别的变量优先。这并不像听起来那么明显。考虑我们之前的清单,我们在其中使用子组来区分 CentOS 和 Ubuntu 主机——如果我们在ubuntu子组和frontends组(ubuntu组的子组)中都添加了同名的变量,结果会是什么?清单将如下所示:
loadbalancer.example.com

[frontends]
frt01.example.com
frt02.example.com

[frontends:vars]
testvar=childgroup

[apps]
app01.example.com
app02.example.com

[databases]
dbms01.example.com
dbms02.example.com

[centos:children]
apps
databases

[ubuntu:children]
frontends

[ubuntu:vars]
testvar=group

现在,让我们运行一个临时命令,看看testvar的实际设置值是多少:

$ ansible -i hostgroups-children-vars-ini ubuntu -m debug -a "var=testvar"
frt01.example.com | SUCCESS => {
 "testvar": "childgroup"
}
frt02.example.com | SUCCESS => {
 "testvar": "childgroup"
}

需要注意的是,在这个清单中,frontends组是ubuntu组的子组(因此,组定义是[ubuntu:children]),因此在这种情况下,我们在frontends组级别设置的变量值会胜出。

到目前为止,您应该已经对如何使用静态清单文件有了相当好的了解。然而,没有查看动态清单的 Ansible 清单功能是完整的,我们将在下一节中做到这一点。

生成动态清单文件

在云计算和基础设施即代码的今天,您可能希望自动化的主机每天甚至每小时都会发生变化!保持静态的 Ansible 清单最新可能会成为一项全职工作,在许多大规模的场景中,因此,尝试在持续基础上使用静态清单变得不切实际。

这就是 Ansible 的动态清单支持发挥作用的地方。简而言之,Ansible 可以从几乎任何可执行文件中收集其清单数据(尽管您会发现大多数动态清单都是用 Python 编写的)——唯一的要求是可执行文件以指定的 JSON 格式返回清单数据。如果愿意,您可以自己创建清单脚本,但值得庆幸的是,已经有许多可供您使用的脚本,涵盖了许多潜在的清单来源,包括 Amazon EC2、Microsoft Azure、Red Hat Satellite、LDAP 目录等等。

在撰写书籍时,很难确定要使用哪个动态清单脚本作为示例,因为并不是每个人都有一个可以自由使用来进行测试的 Amazon EC2 帐户(例如)。因此,我们将以 Cobbler 配置系统作为示例,因为这是免费提供的,并且在 CentOS 系统上很容易部署。对于感兴趣的人来说,Cobbler 是一个用于动态配置和构建 Linux 系统的系统,它可以处理包括 DNS、DHCP、PXE 引导等在内的所有方面。因此,如果您要使用它来配置基础架构中的虚拟或物理机器,那么使用它作为清单来源也是有道理的,因为 Cobbler 负责首次构建系统,因此了解所有系统名称。

这个示例将为您演示使用动态清单的基本原理,然后您可以将其应用到其他系统的动态清单脚本中。让我们开始这个过程,首先安装 Cobbler——这个过程在 CentOS 7.8 上进行了测试:

  1. 您的第一个任务是使用yum安装相关的 Cobbler 软件包。请注意,在撰写本文时,CentOS 7 提供的 SELinux 策略不支持 Cobbler 的功能,并阻止了一些方面的工作。尽管这不是您在生产环境中应该做的事情,但让这个演示快速运行的最简单方法是简单地禁用 SELinux:
$ yum install -y cobbler cobbler-web
$ setenforce 0
  1. 接下来,请确保cobblerd服务已配置为在环回地址上监听,方法是检查/etc/cobbler/settings中的设置——文件的相关片段如下所示:
# default, localhost server: 127.0.0.1  

这不是一个公共监听地址,请不要使用0.0.0.0。您也可以将其设置为 Cobbler 服务器的 IP 地址。

  1. 完成这一步后,您可以使用systemctl启动cobblerd服务。
$ systemctl start cobblerd.service
$ systemctl enable cobblerd.service
$ systemctl status cobblerd.service
  1. Cobbler 服务已经启动运行,现在我们将逐步介绍向 Cobbler 添加发行版的过程,以创建一些主机。这个过程非常简单,但您需要添加一个内核文件和一个初始 RAM 磁盘文件。获取这些文件的最简单来源是您的/boot目录,假设您已在 CentOS 7 上安装了 Cobbler。在用于此演示的测试系统上使用了以下命令,但是,您必须将vmlinuzinitramfs文件名中的版本号替换为您系统/boot目录中的适当版本号:
$ cobbler distro add --name=CentOS --kernel=/boot/vmlinuz-3.10.0-957.el7.x86_64 --initrd=/boot/initramfs-3.10.0-957.el7.x86_64.img

$ cobbler profile add --name=webservers --distro=CentOS

这个定义非常基础,可能无法生成可用的服务器镜像;但是,对于我们的简单演示来说,它足够了,因为我们可以基于这个假设的基于 CentOS 的镜像添加一些系统。请注意,我们正在创建的配置文件名webservers将在我们的动态清单中成为我们的清单组名。

  1. 现在让我们将这些系统添加到 Cobbler 中。以下两个命令将向我们的 Cobbler 系统添加两个名为frontend01frontend02的主机,使用我们之前创建的webservers配置文件:
$ cobbler system add --name=frontend01 --profile=webservers --dns-name=frontend01.example.com --interface=eth0

$ cobbler system add --name=frontend02 --profile=webservers --dns-name=frontend02.example.com --interface=eth0

请注意,为了使 Ansible 工作,它必须能够到达--dns-name参数中指定的这些 FQDN。为了实现这一点,我还在 Cobbler 系统的/etc/hosts中添加了这两台机器的条目,以确保我们以后可以到达它们。这些条目可以指向您选择的任何两个系统,因为这只是一个测试。

此时,您已成功安装了 Cobbler,创建了一个配置文件,并向该配置文件添加了两个假设系统。我们过程的下一阶段是下载并配置 Ansible 动态清单脚本,以便与这些条目一起使用。为了实现这一点,让我们开始执行以下给出的过程:

  1. 从 GitHub Ansible 存储库下载 Cobbler 动态清单文件以及相关的配置文件模板。请注意,大多数由 Ansible 提供的动态清单脚本也有一个模板化的配置文件,其中包含您可能需要设置的参数,以使动态清单脚本工作。对于我们的简单示例,我们将把这些文件下载到我们当前的工作目录中:
$ wget https://raw.githubusercontent.com/ansible/ansible/devel/contrib/inventory/cobbler.py
$ wget https://raw.githubusercontent.com/ansible/ansible/devel/contrib/inventory/cobbler.ini
$ chmod +x cobbler.py

重要的是要记住,要使您下载的任何动态清单脚本可执行,就像之前展示的那样;如果您不这样做,那么即使其他一切都设置得完美,Ansible 也无法运行该脚本。

  1. 编辑cobbler.ini文件,并确保它指向本地主机,因为在本例中,我们将在同一系统上运行 Ansible 和 Cobbler。在现实生活中,您会将其指向 Cobbler 系统的远程 URL。以下是配置文件的一部分,以便让您了解如何配置:
[cobbler]

# Specify IP address or Hostname of the cobbler server. The default variable is here:
host = http://127.0.0.1/cobbler_api

# (Optional) With caching, you will have responses of API call with the cobbler server quicker
cache_path = /tmp
cache_max_age = 900
  1. 现在,您可以按照您习惯的方式运行 Ansible 的临时命令——这次唯一的区别是,您将指定动态清单脚本的文件名,而不是静态清单文件的名称。假设您已经在 Cobbler 中输入了两个地址的主机,您的输出应该看起来像这样:
$  ansible -i cobbler.py webservers -m ping
frontend01.example.com | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python"
    },
    "changed": false,
    "ping": "pong"
}
frontend02.example.com | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python"
    },
    "changed": false,
    "ping": "pong"
} 

就是这样!您刚刚在 Ansible 中实现了您的第一个动态清单。当然,我们知道许多读者不会使用 Cobbler,一些其他动态清单插件更复杂。例如,Amazon EC2 动态清单脚本需要您的 Amazon Web Services 的身份验证详细信息(或适当的 IAM 帐户)以及 Python botoboto3库的安装。您怎么知道要做所有这些?幸运的是,所有这些都在动态清单脚本或配置文件的头部有记录,所以我能给出的最基本的建议是:每当您下载新的动态清单脚本时,请务必在您喜欢的编辑器中查看文件本身,因为它们的要求很可能已经为您记录了。

在本书的这一节结束之前,让我们看一下使用多个清单来源的其他一些方便提示,从下一节开始。

在清单目录中使用多个清单来源

到目前为止,在本书中,我们一直在使用我们的 Ansible 命令中的-i开关来指定我们的清单文件(静态或动态)。可能不明显的是,您可以多次指定-i开关,因此同时使用多个清单。这使您能够执行跨静态和动态清单的主机的任务,例如运行一个 playbook(或临时命令)。Ansible 将会计算出需要做什么——静态清单不应标记为可执行,因此不会被处理为这样,而动态清单将会被处理。这个小巧但聪明的技巧使您能够轻松地结合多个清单来源。让我们在下一节中继续看一下静态清单组与动态清单组的使用,这是多清单功能的扩展。

在动态组中使用静态组

当然,混合清单的可能性带来了一个有趣的问题——如果您同时定义动态清单和静态清单中的组,会发生什么?答案是 Ansible 会将两者结合起来,这带来了一个有趣的可能性。正如您所看到的,我们的 Cobbler 清单脚本从我们称为webservers的 Cobbler 配置文件中产生了一个名为webservers的 Ansible 组。这对于大多数动态清单提供者来说很常见;大多数清单来源(例如 Cobbler 和 Amazon EC2)都不是 Ansible 感知的,因此不提供 Ansible 可以直接使用的组。因此,大多数动态清单脚本将使用清单来源的某些信息来产生分组,Cobbler 机器配置文件就是一个例子。

让我们通过混合静态清单来扩展前一节中的 Cobbler 示例。假设我们想要将我们的webservers机器作为名为centos的组的子组,以便我们将来可以将所有 CentOS 机器分组在一起。我们知道我们只有一个名为webservers的 Cobbler 配置文件,理想情况下,我们不想开始干扰 Cobbler 设置,只是为了做一些与 Ansible 相关的事情。

解决这个问题的方法是创建一个具有两个组定义的静态清单文件。第一个必须与您从动态清单中期望的组的名称相同,只是您应该将其留空。当 Ansible 组合静态和动态清单内容时,它将重叠这两个组,因此将 Cobbler 的主机添加到这些webservers组中。

第二个组定义应该说明webserverscentos组的子组。生成的文件应该看起来像这样:

[webservers]

[centos:children]
webservers

现在让我们在 Ansible 中运行一个简单的临时ping命令,以查看它如何评估两个清单。请注意,我们将指定centos组来运行ping,而不是webservers组。我们知道 Cobbler 没有centos组,因为我们从未创建过,我们知道当您组合两个清单时,此组中的任何主机必须通过webservers组来,因为我们的静态清单中没有主机。结果将看起来像这样:

$ ansible -i static-groups-mix-ini -i cobbler.py centos -m ping
frontend01.example.com | SUCCESS => {
 "ansible_facts": {
 "discovered_interpreter_python": "/usr/bin/python"
 },
 "changed": false,
 "ping": "pong"
}
frontend02.example.com | SUCCESS => {
 "ansible_facts": {
 "discovered_interpreter_python": "/usr/bin/python"
 },
 "changed": false,
 "ping": "pong"
}

从前面的输出中可以看出,我们引用了两个不同的清单,一个是静态的,另一个是动态的。我们已经组合了组,将仅存在于一个清单源中的主机与仅存在于另一个清单源中的组合在一起。正如您所看到的,这是一个非常简单的例子,很容易将其扩展为组合静态和动态主机的列表,或者向来自动态清单的主机添加自定义变量。

这是 Ansible 的一个鲜为人知的技巧,但在清单扩展和增长时可以非常强大。当我们通过本章工作时,您会注意到我们非常精确地指定了我们的清单主机,要么是单独的,要么是通过组;例如,我们明确告诉ansiblewebservers组中的所有主机运行临时命令。在下一节中,我们将继续探讨 Ansible 如何管理使用模式指定的一组主机。

使用模式进行特殊主机管理

我们已经确定,您经常会想要针对清单的一个子部分运行一个临时命令或一个 playbook。到目前为止,我们一直在做得很精确,但现在让我们通过查看 Ansible 如何使用模式来确定应该针对哪些主机运行命令(或 playbook)来扩展这一点。

作为起点,让我们再次考虑本章早些时候定义的清单,以便探索主机组和子组。为了方便起见,清单内容再次提供如下:

loadbalancer.example.com

[frontends]
frt01.example.com
frt02.example.com

[apps]
app01.example.com
app02.example.com

[databases]
dbms01.example.com
dbms02.example.com

[centos:children]
apps
databases

[ubuntu:children]
frontends

为了演示通过模式进行主机/组选择,我们将使用ansible命令的--list-hosts开关来查看 Ansible 将对哪些主机进行操作。您可以扩展示例以使用ping模块,但出于空间和输出简洁可读的考虑,我们将在这里使用--list-hosts

  1. 我们已经提到了特殊的all组来指定清单中的所有主机:
$ ansible -i hostgroups-children-ini all --list-hosts
 hosts (7):
 loadbalancer.example.com
 frt01.example.com
 frt02.example.com
 app01.example.com
 app02.example.com
 dbms01.example.com
 dbms02.example.com

星号字符具有与all相同的效果,但需要在 shell 中用单引号引起来,以便 shell 正确解释命令:

$ ansible -i hostgroups-children-ini '*' --list-hosts
 hosts (7):
 loadbalancer.example.com
 frt01.example.com
 frt02.example.com
 app01.example.com
 app02.example.com
 dbms01.example.com
 dbms02.example.com
  1. 使用:来指定逻辑OR,意思是“应用于这个组或那个组中的主机”,就像这个例子中一样:
$ ansible -i hostgroups-children-ini frontends:apps --list-hosts
 hosts (4):
 frt01.example.com
 frt02.example.com
 app01.example.com
 app02.example.com
  1. 使用!来排除特定组——您可以将其与其他字符(例如:)结合使用,以显示(例如)除apps组中的所有主机之外的所有主机。同样,!是 shell 中的特殊字符,因此您必须在单引号中引用模式字符串,以使其正常工作,就像这个例子中一样:
$ ansible -i hostgroups-children-ini 'all:!apps' --list-hosts
 hosts (5):
 loadbalancer.example.com
 frt01.example.com
 frt02.example.com
 dbms01.example.com
 dbms02.example.com
  1. 使用:&来指定两个组之间的逻辑AND,例如,如果我们想要在centos组和apps组中的所有主机(再次,您必须在 shell 中使用单引号):
$ ansible -i hostgroups-children-ini 'centos:&apps' --list-hosts
  hosts (2):
    app01.example.com
    app02.example.com
  1. 使用*通配符的方式与在 shell 中使用的方式类似,就像这个例子中一样:
$ ansible -i hostgroups-children-ini 'db*.example.com' --list-hosts
 hosts (2):
 dbms02.example.com
 dbms01.example.com

另一种限制命令运行的主机的方法是使用 Ansible 的--limit开关。这与前面的语法和模式表示完全相同,但它的优势在于您可以在ansible-playbook命令中使用它,而在命令行上指定主机模式仅支持ansible命令本身。因此,例如,您可以运行以下命令:

$ ansible-playbook -i hostgroups-children-ini site.yml --limit frontends:apps

PLAY [A simple playbook for demonstrating inventory patterns] ******************

TASK [Gathering Facts] *********************************************************
ok: [frt02.example.com]
ok: [app01.example.com]
ok: [frt01.example.com]
ok: [app02.example.com]

TASK [Ping each host] **********************************************************
ok: [app01.example.com]
ok: [app02.example.com]
ok: [frt02.example.com]
ok: [frt01.example.com]

PLAY RECAP *********************************************************************
app01.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
app02.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt01.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt02.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

模式是处理清单的非常有用和重要的部分,您无疑会发现它们非常有价值。这结束了我们关于 Ansible 清单的章节;但是,希望这为您提供了一切您需要自信地使用 Ansible 清单。

摘要

创建和管理 Ansible 清单是您使用 Ansible 的工作的重要部分,因此我们在本书的早期阶段就介绍了这个基本概念。它们至关重要,因为没有它们,Ansible 将不知道要针对哪些主机运行自动化任务,但它们提供的远不止这些。它们为配置管理系统提供了一个集成点,它们为存储主机特定(或组特定)变量提供了一个明智的来源,并且它们为您提供了运行此 playbook 的灵活方式。

在本章中,您学习了如何创建简单的静态清单文件并向其中添加主机。然后,我们通过学习如何添加主机组并为主机分配变量来扩展了这一点。我们还研究了在单个平面清单文件变得太难处理时如何组织您的清单和变量。然后,我们学习了如何利用动态清单文件,最后通过查看有用的技巧和诀窍,如组合清单来源和使用模式来指定主机,使您处理清单更加容易,同时更加强大。

在下一章中,我们将学习如何开发 playbooks 和 roles 来使用 Ansible 配置,部署和管理远程机器。

问题

  1. 如何将frontends组变量添加到您的清单中?

A) [frontends::]

B) [frontends::values]

C) [frontends:host:vars]

D) [frontends::variables]

E) [frontends:vars]

  1. 什么使您能够自动执行 Linux 任务,如提供 DNS,管理 DHCP,更新软件包和配置管理?

A) 播放书

B) Yum

C) 修鞋匠

D) Bash

E) 角色

  1. Ansible 允许您使用命令行上的-i选项来指定清单文件位置。

A) 真

B) 错误

进一步阅读

第四章:playbook 和角色

到目前为止,在这本书中,我们主要使用临时的 Ansible 命令来简化操作,并帮助您理解基本原理。然而,Ansible 的生命线无疑是 playbook,它是任务的逻辑组织(类似于临时命令),以创建有用的结果的结构。这可能是在新建的虚拟机上部署 Web 服务器,也可能是应用安全策略。甚至可能处理虚拟机的整个构建过程!可能性是无限的。正如我们已经介绍过的,Ansible playbook 的设计是简单易写、易读——它们旨在自我记录,因此将成为您 IT 流程中宝贵的一部分。

在本章中,我们将更深入地探讨 playbook,从创建的基础知识到更高级的概念,如循环和块中运行任务、执行条件逻辑,以及 playbook 组织和代码重用中可能最重要的概念之一——Ansible 角色。我们将稍后更详细地介绍角色,但请知道,这是您在创建可管理的 playbook 代码时希望尽可能使用的内容。

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

  • 理解 playbook 框架

  • 理解角色——playbook 的组织者

  • 在代码中使用条件

  • 使用循环重复任务

  • 使用块分组任务

  • 通过策略配置 play 执行

  • 使用ansible-pull

技术要求

本章假设您已经按照第一章中详细介绍的方式在控制主机上安装了 Ansible,并且正在使用最新版本——本章中的示例是使用 Ansible 2.9 进行测试的。本章还假设您至少有一个额外的主机进行测试,并且最好是基于 Linux 的。尽管本章中将给出主机名的具体示例,但您可以自由地用自己的主机名和/或 IP 地址替换它们,如何做到这一点的详细信息将在适当的地方提供。

本章的代码包在此处可用:github.com/PacktPublishing/Ansible-2-Cookbook/tree/master/Chapter%204

理解 playbook 框架

playbook 允许您简单轻松地管理多台机器上的多个配置和复杂部署。这是使用 Ansible 交付复杂应用程序的关键优势之一。通过 playbook,您可以将任务组织成逻辑结构,因为任务通常按照编写的顺序执行,这使您能够对自动化过程有很好的控制。话虽如此,也可以异步执行任务,因此我们将强调任务不按顺序执行的情况。我们的目标是,一旦您完成本章,您将了解编写自己的 Ansible playbook 的最佳实践。

尽管 YAML 格式易于阅读和编写,但在间距方面非常严谨。例如,您不能使用制表符来设置缩进,即使在屏幕上,制表符和四个空格看起来可能相同——在 YAML 中,它们并不相同。如果您是第一次编写 playbook,我们建议您采用支持 YAML 的编辑器,例如 Vim、Visual Studio Code 或 Eclipse,这些编辑器将帮助您确保缩进正确。为了测试本章中开发的 playbook,我们将重复使用第三章中创建的清单的变体,定义您的清单(除非另有说明):

[frontends]
frt01.example.com https_port=8443
frt02.example.com http_proxy=proxy.example.com

[frontends:vars]
ntp_server=ntp.frt.example.com
proxy=proxy.frt.example.com

[apps]
app01.example.com
app02.example.com

[webapp:children]
frontends
apps

[webapp:vars]
proxy_server=proxy.webapp.example.com
health_check_retry=3
health_check_interal=60

让我们立即开始编写一个 playbook。在第二章的理解 Ansible 基础中的分解 Ansible 组件一节中,我们涵盖了 playbook 的一些基本方面,因此我们不会在这里详细重复,而是在此基础上展示 playbook 开发的内容:

  1. 创建一个简单的 playbook,在我们的清单文件中定义的frontends主机组中运行。我们可以在 playbook 中使用remote_user指令设置访问主机的用户,如下所示(您也可以在命令行上使用--user开关,但由于本章是关于 playbook 开发的,我们暂时忽略它):
---
- hosts: frontends
  remote_user: danieloh

  tasks:
  - name: simple connection test
    ping:
    remote_user: danieloh
  1. 在第一个任务下面添加另一个任务来运行shell模块(这将依次在远程主机上运行ls命令)。我们还将在这个任务中添加ignore_errors指令,以确保如果ls命令失败(例如,如果我们尝试列出的目录不存在),我们的 playbook 不会失败。小心缩进,并确保它与文件的第一部分匹配:
  - name: run a simple command
    shell: /bin/ls -al /nonexistent
    ignore_errors: True

让我们看看当我们运行时,我们新创建的 playbook 的行为如何:

$ ansible-playbook -i hosts myplaybook.yaml

PLAY [frontends] ***************************************************************

TASK [Gathering Facts] *********************************************************
ok: [frt02.example.com]
ok: [frt01.example.com]

TASK [simple connection test] **************************************************
ok: [frt01.example.com]
ok: [frt02.example.com]

TASK [run a simple command] ****************************************************
fatal: [frt02.example.com]: FAILED! => {"changed": true, "cmd": "/bin/ls -al /nonexistent", "delta": "0:00:00.015687", "end": "2020-04-10 16:37:56.895520", "msg": "non-zero return code", "rc": 2, "start": "2020-04-10 16:37:56.879833", "stderr": "/bin/ls: cannot access /nonexistent: No such file or directory", "stderr_lines": ["/bin/ls: cannot access /nonexistent: No such file or directory"], "stdout": "", "stdout_lines": []}
...ignoring
fatal: [frt01.example.com]: FAILED! => {"changed": true, "cmd": "/bin/ls -al /nonexistent", "delta": "0:00:00.012160", "end": "2020-04-10 16:37:56.930058", "msg": "non-zero return code", "rc": 2, "start": "2020-04-10 16:37:56.917898", "stderr": "/bin/ls: cannot access /nonexistent: No such file or directory", "stderr_lines": ["/bin/ls: cannot access /nonexistent: No such file or directory"], "stdout": "", "stdout_lines": []}
...ignoring

PLAY RECAP *********************************************************************
frt01.example.com : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=1
frt02.example.com : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=1 

从 playbook 运行的输出中,您可以看到我们的两个任务是按照指定的顺序执行的。我们可以看到ls命令失败,因为我们尝试列出一个不存在的目录,但是 playbook 没有注册任何failed任务,因为我们为这个任务设置了ignore_errorstrue(仅针对这个任务)。

大多数 Ansible 模块(除了运行用户定义命令的模块,如shellcommandraw)都被编码为幂等的,也就是说,如果您运行相同的任务两次,结果将是相同的,并且任务不会进行相同的更改两次 - 如果检测到被请求执行的操作已经完成,那么它不会再次执行。当然,对于前述模块来说这是不可能的,因为它们可以用于执行几乎任何可以想象的任务 - 因此,模块如何知道它被执行了两次呢?

每个模块都会返回一组结果,其中包括任务状态。您可以在前面 playbook 运行输出的底部看到这些总结,它们的含义如下:

  • ok:任务成功运行,没有进行任何更改。

  • changed:任务成功运行,并进行了更改。

  • failed:任务运行失败。

  • unreachable:无法访问主机以运行任务。

  • skipped:此任务被跳过。

  • ignored:此任务被忽略(例如,在ignore_errors的情况下)。

  • rescued:稍后我们将在查看块和救援任务时看到一个例子。

这些状态可能非常有用,例如,如果我们有一个任务从模板部署新的 Apache 配置文件,我们知道必须重新启动 Apache 服务才能应用更改。但是,我们只想在文件实际更改时才这样做 - 如果没有进行任何更改,我们不希望不必要地重新启动 Apache,因为这会打断可能正在使用服务的人。因此,我们可以使用notify操作,告诉 Ansible 在任务结果为changed时(仅在此时)调用一个handler。简而言之,处理程序是一种特殊类型的任务,作为notify的结果而运行。但是,与按顺序执行的 Ansible playbook 任务不同,处理程序都被分组在一起,并在 play 的最后运行。此外,它们可以被通知多次,但无论如何只会运行一次,再次防止不必要的服务重启。考虑以下 playbook:

---
- name: Handler demo 1
  hosts: frt01.example.com
  gather_facts: no
  become: yes

  tasks:
    - name: Update Apache configuration
      template:
        src: template.j2
        dest: /etc/httpd/httpd.conf
      notify: Restart Apache

  handlers:
    - name: Restart Apache
      service:
        name: httpd
        state: restarted

为了保持输出简洁,我已经关闭了这个 playbook 的事实收集(我们不会在任何任务中使用它们)。出于简洁起见,我再次只在一个主机上运行,但您可以根据需要扩展演示代码。如果我们第一次运行这个任务,我们将看到以下结果:

$ ansible-playbook -i hosts handlers1.yml

PLAY [Handler demo 1] **********************************************************

TASK [Update Apache configuration] *********************************************
changed: [frt01.example.com]

RUNNING HANDLER [Restart Apache] ***********************************************
changed: [frt01.example.com]

PLAY RECAP *********************************************************************
frt01.example.com : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

请注意,当配置文件更新时,处理程序被运行。然而,如果我们再次运行这个 playbook,而没有对模板或配置文件进行任何更改,我们将看到类似以下的结果:

$ ansible-playbook -i hosts handlers1.yml

PLAY [Handler demo 1] **********************************************************

TASK [Update Apache configuration] *********************************************
ok: [frt01.example.com]

PLAY RECAP *********************************************************************
frt01.example.com : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

这一次,由于配置任务的结果是 OK,处理程序没有被调用。所有处理程序的名称应该是全局唯一的,这样通知操作才能调用正确的处理程序。您还可以通过设置一个公共名称来调用多个处理程序,使用listen指令——这样,您可以调用namelisten字符串中的任何一个处理程序,就像下面的示例中演示的那样:

---
- name: Handler demo 1
  hosts: frt01.example.com
  gather_facts: no
  become: yes

  handlers:
    - name: restart chronyd
      service:
        name: chronyd
        state: restarted
      listen: "restart all services"
    - name: restart apache
      service:
        name: httpd
        state: restarted
      listen: "restart all services"

  tasks:
    - name: restart all services
      command: echo "this task will restart all services"
      notify: "restart all services"

我们的 playbook 中只有一个任务,但当我们运行它时,两个处理程序都会被调用。另外,请记住我们之前说过的,command是一组特殊情况下的模块之一,因为它们无法检测到是否发生了更改——因此,它们总是返回changed值,因此,在这个演示 playbook 中,处理程序将始终被通知:

$ ansible-playbook -i hosts handlers2.yml

PLAY [Handler demo 1] **********************************************************

TASK [restart all services] ****************************************************
changed: [frt01.example.com]

RUNNING HANDLER [restart chronyd] **********************************************
changed: [frt01.example.com]

RUNNING HANDLER [restart apache] ***********************************************
changed: [frt01.example.com]

PLAY RECAP *********************************************************************
frt01.example.com : ok=3 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

这些是您需要了解的一些基础知识,以开始编写自己的 playbooks。有了这些知识,让我们在下一节中比较临时命令和 playbooks。

比较 playbooks 和临时任务

临时命令允许您快速创建和执行一次性命令,而不保留任何已完成的记录(除了可能是您的 shell 历史)。这些命令具有重要的作用,并且在快速进行小改动和学习 Ansible 及其模块方面非常有价值。

相比之下,playbooks 是逻辑上组织的一系列任务(每个任务都可以是一个临时命令),按顺序组合在一起执行一个更大的动作。条件逻辑、错误处理等的添加意味着,很多时候,playbooks 的好处超过了临时命令的用处。此外,只要保持它们有组织,你将拥有你运行的所有以前的 playbooks 的副本,因此你将能够回顾(如果你需要的话)看看你运行了什么以及何时运行的。

让我们来开发一个实际的例子——假设你想在 CentOS 上安装 Apache 2.4。即使默认配置足够(这不太可能,但现在我们将保持例子简单),也涉及到一些步骤。如果你要手动执行基本安装,你需要安装软件包,打开防火墙,并确保服务正在运行(并且在启动时运行)。

要在 shell 中执行这些命令,您可能会这样做:

$ sudo yum install httpd
$ sudo firewall-cmd --add-service=http --permanent 
$ sudo firewall-cmd --add-service=https --permanent
$ sudo firewall-cmd --reload
$ sudo systemctl enable httpd.service
$ sudo systemctl restart httpd.service

现在,对于这些命令中的每一个,都有一个等效的临时 Ansible 命令可以运行。出于篇幅考虑,我们不会在这里逐个讨论它们;然而,假设你想要重新启动 Apache 服务——在这种情况下,你可以运行类似以下的临时命令(同样,为了简洁起见,我们只在一个主机上执行):

$ ansible -i hosts frt01* -m service -a "name=httpd state=restarted"

当成功运行时,您将看到包含从以这种方式运行服务模块返回的所有变量数据的页面式 shell 输出。下面是一个片段供您检查您的结果——关键是命令导致changed状态,这意味着它成功运行,并且服务确实被重新启动了:

frt01.example.com | CHANGED => {
 "ansible_facts": {
 "discovered_interpreter_python": "/usr/bin/python"
 },
 "changed": true,
 "name": "httpd",
 "state": "started",

你可以创建并执行一系列临时命令来复制前面给出的六个 shell 命令,并分别运行它们。通过一些巧妙的方法,你应该可以将这个减少到六个命令(例如,Ansible 的service模块可以在一个临时命令中同时启用服务和重新启动它)。然而,你最终仍然会至少需要三到四个临时命令,如果你想在以后的另一台服务器上再次运行这些命令,你将需要参考你的笔记来弄清楚你是如何做的。

因此,playbook 是一种更有价值的方法来处理这个问题——它不仅会一次性执行所有步骤,而且还会为你记录下来以供以后参考。有多种方法可以做到这一点,但请将以下内容作为一个例子:

---
- name: Install Apache
  hosts: frt01.example.com
  gather_facts: no
  become: yes

  tasks:
    - name: Install Apache package
      yum:
        name: httpd
        state: latest
    - name: Open firewall for Apache
      firewalld:
        service: "{{ item }}"
        permanent: yes
        state: enabled
        immediate: yes
      loop:
        - "http"
        - "https"
    - name: Restart and enable the service
      service:
        name: httpd
        state: restarted
        enabled: yes

现在,当你运行这个时,你应该看到我们所有的安装要求都已经通过一个相当简单和易于阅读的 playbook 完成了。这里有一个新的概念,循环,我们还没有涉及,但不要担心,我们将在本章后面涉及到:

$ ansible-playbook -i hosts installapache.yml

PLAY [Install Apache] **********************************************************

TASK [Install Apache package] **************************************************
changed: [frt01.example.com]

TASK [Open firewall for Apache] ************************************************
changed: [frt01.example.com] => (item=http)
changed: [frt01.example.com] => (item=https)

TASK [Restart and enable the service] ******************************************
changed: [frt01.example.com]

PLAY RECAP *********************************************************************
frt01.example.com : ok=2 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

正如你所看到的,这样做要比实际操作和记录在一个格式中更好,其他人可以很容易地理解。尽管我们将在书的后面讨论循环,但从前面的内容很容易看出它们是如何工作的。有了这个设置,让我们在下一节中更详细地看一下我们已经多次使用的一些术语,以确保你清楚它们的含义:playstasks

定义 plays 和 tasks

到目前为止,当我们使用 playbook 时,我们一直在每个 playbook 中创建一个单一的 play(从逻辑上讲,这是你可以做的最少的)。然而,你可以在一个 playbook 中有多个 play,并且在 Ansible 术语中,“play”简单地是与主机(或主机组)相关联的一组任务(和角色、处理程序和其他 Ansible 方面)。任务是 play 的最小可能元素,负责使用一组参数运行单个模块以实现特定的目标。当然,在理论上,这听起来相当复杂,但在实际示例的支持下,它变得非常容易理解。

如果我们参考我们的示例清单,这描述了一个简单的两层架构(我们暂时忽略了数据库层)。现在,假设我们想编写一个单一的 playbook 来配置前端服务器和应用服务器。我们可以使用两个单独的 playbook 来配置前端和应用服务器,但这会使你的代码变得零散并且难以组织。然而,前端服务器和应用服务器(从它们的本质上)本质上是不同的,因此不太可能使用相同的任务集进行配置。

解决这个问题的方法是创建一个包含两个 play 的单一 playbook。每个 play 的开始可以通过最低缩进的行来识别(即在其前面没有空格)。让我们开始构建我们的 playbook:

  1. 将第一个 play 添加到 playbook 中,并定义一些简单的任务来设置前端的 Apache 服务器,如下所示:
---
- name: Play 1 - configure the frontend servers
  hosts: frontends
  become: yes

  tasks:
  - name: Install the Apache package
    yum:
      name: httpd
      state: latest
  - name: Start the Apache server
    service:
      name: httpd
      state: started
  1. 在同一个文件中,立即在下面添加第二个 play 来配置应用程序层服务器:
- name: Play 2 - configure the application servers
  hosts: apps
  become: true

  tasks:
  - name: Install Tomcat
    yum:
      name: tomcat
      state: latest
  - name: Start the Tomcat server
    service:
      name: tomcat
      state: started

现在,你有两个 plays:一个用于在frontends组中安装 web 服务器,另一个用于在apps组中安装应用服务器,全部合并成一个简单的 playbook。

当我们运行这个 playbook 时,我们将看到两个 play 按顺序执行,按照 playbook 中的顺序。请注意PLAY关键字的存在,它表示每个 play 的开始:

$ ansible-playbook -i hosts playandtask.yml

PLAY [Play 1 - configure the frontend servers] *********************************

TASK [Gathering Facts] *********************************************************
changed: [frt02.example.com]
changed: [frt01.example.com]

TASK [Install the Apache package] *********************************************
changed: [frt01.example.com]
changed: [frt02.example.com]

TASK [Start the Apache server] *************************************************
changed: [frt01.example.com]
changed: [frt02.example.com]

PLAY [Play 2 - configure the application servers] *******************************

TASK [Gathering Facts] *********************************************************
changed: [app01.example.com]
changed: [app02.example.com]

TASK [Install Tomcat] **********************************************************
changed: [app02.example.com]
changed: [app01.example.com]

TASK [Start the Tomcat server] *************************************************
changed: [app02.example.com]
changed: [app01.example.com]

PLAY RECAP *********************************************************************
app01.example.com : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
app02.example.com : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt01.example.com : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt02.example.com : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

我们有一个 playbook,但是有两个不同的 play 在提供的清单中操作不同的主机集。这非常强大,特别是与角色结合使用时(这将在本书的后面部分介绍)。当然,您的 playbook 中可以只有一个 play——您不必有多个 play,但是能够开发多 play playbook 非常重要,因为随着环境变得更加复杂,您几乎肯定会发现它们非常有用。

Playbooks 是 Ansible 自动化的生命线——它们将其扩展到不仅仅是单个任务/命令(它们本身就非常强大),而是一系列以逻辑方式组织的任务。然而,随着您扩展 playbook 库,您如何保持工作的组织?如何有效地重用相同的代码块?在前面的示例中,我们安装了 Apache,这可能是您的许多服务器的要求。但是,您应该尝试从一个 playbook 管理它们所有吗?或者您应该一遍又一遍地复制和粘贴相同的代码块?有一个更好的方法,在 Ansible 术语中,我们需要开始看角色,我们将在下一节中进行介绍。

理解角色——playbook 组织者

角色旨在使您能够高效有效地重用 Ansible 代码。它们始终遵循已知的结构,并且通常会包含变量、错误处理、处理程序等的合理默认值。以前一章中的 Apache 安装示例为例,我们知道这是我们可能想一遍又一遍地做的事情,也许每次都使用不同的配置文件,也许每台服务器(或每个清单组)都需要进行一些其他调整。在 Ansible 中,支持以这种方式重用代码的最有效方法是将其创建为一个角色。

创建角色的过程实际上非常简单——Ansible(默认情况下)将在您运行 playbook 的同一目录中寻找roles/目录,在这里,您将为每个角色创建一个子目录。角色名称源自子目录名称——无需创建复杂的元数据或其他任何东西——就是这么简单。在每个子目录中,都有一个固定的目录结构,告诉 Ansible 每个角色的任务、默认变量、处理程序等是什么。

roles/目录并不是 Ansible 寻找角色的唯一目录——这是它首先查找的目录,但然后它会在/etc/ansible/roles中查找任何额外的角色。这可以通过 Ansible 配置文件进一步定制,如第二章中所讨论的那样,理解 Ansible 的基本原理

让我们更详细地探讨一下。考虑以下目录结构:

site.yml
frontends.yml
dbservers.yml
roles/
   installapache/
     tasks/
     handlers/
     templates/
     vars/
     defaults/
   installtomcat/
     tasks/
     meta/

前面的目录结构显示了在我们假设的 playbook 目录中定义的两个角色,名为installapacheinstalltomcat。在这些目录中,您会注意到一系列子目录。这些子目录不需要存在(稍后会详细说明它们的含义,但例如,如果您的角色没有处理程序,则无需创建handlers/)。但是,如果您确实需要这样的目录,您应该用名为main.yml的 YAML 文件填充它。每个main.yml文件都应该有特定的内容,具体取决于包含它们的目录。

角色中可以存在的子目录如下:

  • tasks:这是在角色中找到的最常见的目录,它包含角色应执行的所有 Ansible 任务。

  • handlers:角色中使用的所有处理程序都应该放在这个目录中。

  • defaults:角色的所有默认变量都放在这里。

  • vars:这些是其他角色变量——它们会覆盖defaults/目录中声明的变量,因为它们在优先顺序中更高。

  • files:角色需要的文件应该放在这里 - 例如,需要部署到目标主机的任何配置文件。

  • templates:与files/目录不同,这个目录应该包含角色使用的所有模板。

  • meta:角色所需的任何元数据都放在这里。例如,角色通常按照从父 playbook 调用它们的顺序执行 - 但是,有时角色会有需要先运行的依赖角色,如果是这种情况,它们可以在这个目录中声明。

对于我们在本章的这一部分中将开发的示例,我们将需要一个清单,所以让我们重用我们在上一节中使用的清单(以下是为了方便包含的):

[frontends]
frt01.example.com https_port=8443
frt02.example.com http_proxy=proxy.example.com

[frontends:vars]
ntp_server=ntp.frt.example.com
proxy=proxy.frt.example.com

[apps]
app01.example.com
app02.example.com

[webapp:children]
frontends
apps

[webapp:vars]
proxy_server=proxy.webapp.example.com
health_check_retry=3
health_check_interal=60

让我们开始一些实际的练习,帮助你学习如何创建和使用角色。我们将首先创建一个名为installapache的角色,该角色将处理我们在上一节中看到的 Apache 安装过程。但是,在这里,我们将扩展它以涵盖在 CentOS 和 Ubuntu 上安装 Apache。这是一个很好的实践,特别是如果您希望将您的角色提交回社区,因为它们越通用(以及能够在更广泛的系统上运行),对人们就越有用。按照以下过程创建您的第一个角色:

  1. 从您选择的 playbook 目录中创建installapache角色的目录结构 - 这就是这么简单:
$ mkdir -p roles/installapache/tasks
  1. 现在,让我们在我们刚刚创建的tasks目录中创建一个必需的main.yml。这实际上不会执行 Apache 安装 - 而是在事实收集阶段检测到目标主机的操作系统后,将调用两个外部任务文件中的一个。我们可以使用这个特殊的变量ansible_distributionwhen条件中确定要导入哪个任务文件:
---
- name: import a tasks based on OS platform 
 import_tasks: centos.yml 
  when: ansible_distribution == 'CentOS' 
- import_tasks: ubuntu.yml 
  when: ansible_distribution == 'Ubuntu'
  1. roles/installapache/tasks中创建一个名为centos.yml的文件,以通过yum软件包管理器安装 Apache Web 服务器的最新版本。这应该包含以下内容:
---
- name: Install Apache using yum
 yum:
    name: "httpd"
    state: latest
- name: Start the Apache server
  service:
    name: httpd
    state: started 
  1. roles/installapache/tasks中创建一个名为ubuntu.yml的文件,以通过apt软件包管理器在 Ubuntu 上安装 Apache Web 服务器的最新版本。注意在 CentOS 和 Ubuntu 主机之间内容的不同:
---
- name: Install Apache using apt
 apt:
    name: "apache2"
    state: latest
- name: Start the Apache server
  service:
    name: apache2
    state: started

目前,我们的角色代码非常简单 - 但是,您可以看到前面的任务文件就像一个 Ansible playbook,只是它们缺少了 play 定义。由于它们不属于一个 play,所以它们的缩进级别也比 playbook 中的低,但是除了这个差异,代码应该对您来说非常熟悉。事实上,这就是角色的美妙之处之一:只要您注意正确的缩进级别,您几乎可以在 playbook 或角色中使用相同的代码。

现在,角色不能自行运行 - 我们必须创建一个 playbook 来调用它们,所以让我们编写一个简单的 playbook 来调用我们新创建的角色。这与我们之前看到的一样有一个 play 定义,但是不是在 play 中有一个tasks:部分,而是有一个roles:部分,在那里声明了角色。惯例规定这个文件被称为site.yml,但您可以自由地称它为任何您喜欢的名字:

---
- name: Install Apache using a role
  hosts: frontends
  become: true

  roles:
    - installapache

为了清晰起见,您的最终目录结构应该如下所示:

.
├── roles
│   └── installapache
│   └── tasks
│   ├── centos.yml
│   ├── main.yml
│   └── ubuntu.yml
└── site.yml

完成后,您现在可以以正常方式使用ansible-playbook运行您的site.yml playbook - 您应该会看到类似于这样的输出:

$ ansible-playbook -i hosts site.yml

PLAY [Install Apache using a role] *********************************************

TASK [Gathering Facts] *********************************************************
ok: [frt01.example.com]
ok: [frt02.example.com]

TASK [installapache : Install Apache using yum] ********************************
changed: [frt02.example.com]
changed: [frt01.example.com]

TASK [installapache : Start the Apache server] *********************************
changed: [frt01.example.com]
changed: [frt02.example.com]

TASK [installapache : Install Apache using apt] ********************************
skipping: [frt01.example.com]
skipping: [frt02.example.com]

TASK [installapache : Start the Apache server] *********************************
skipping: [frt01.example.com]
skipping: [frt02.example.com]

PLAY RECAP *********************************************************************
frt01.example.com : ok=3 changed=2 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0
frt02.example.com : ok=3 changed=2 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0

就是这样 - 您已经在最简单的级别上创建了您的第一个角色。当然(正如我们之前讨论的那样),角色不仅仅是我们在这里添加的简单任务,还有更多内容,当我们在本章中进行工作时,我们将看到扩展的示例。然而,前面的示例旨在向您展示如何快速轻松地开始使用角色。

在我们看一些与角色相关的其他方面之前,让我们看一些调用角色的其他方法。当您编写 playbook 时,Ansible 允许您静态导入或动态包含角色。这两种导入或包含角色的语法略有不同,值得注意的是,两者都在 playbook 的任务部分而不是角色部分。以下是一个假设的示例,展示了一个非常简单的 playbook 中的两种选项。包括commonapprole角色的角色目录结构将以与前面示例类似的方式创建:

--- 
- name: Play to import and include a role
 hosts: frontends

 tasks:
  - import_role:
      name: common
  - include_role:
      name: approle

这些功能在 2.3 之前的 Ansible 版本中是不可用的,并且它们在 2.4 版本中的使用方式略有改变,以保持与其他一些 Ansible 功能的一致性。我们不会在这里担心这些细节,因为现在 Ansible 的版本是 2.9,所以除非您绝对必须运行早期版本的 Ansible,否则可以假定这两个语句的工作方式如我们将在接下来的内容中概述的那样。

基本上,import_role语句在解析所有 playbook 代码时执行您指定的角色的静态导入。因此,使用import_role语句将角色引入您的 playbook 时,Ansible 在开始解析时将其视为 play 或角色中的任何其他代码一样。使用import_role基本上与在site.yml中的roles:语句之后声明您的角色一样,就像我们在前面的示例中所做的那样。

include_role在某种程度上与import_role有根本的不同,因为您指定的角色在解析 playbook 时不会被评估,而是在 playbook 运行期间动态处理,在遇到include_role时进行处理。

在选择前面提到的includeimport语句之间最基本的原因可能是循环——如果您需要在循环内运行一个角色,您不能使用import_role,因此必须使用include_role。然而,两者都有好处和局限性,您需要根据您的情况选择最合适的方法——官方的 Ansible 文档(docs.ansible.com/ansible/latest/user_guide/playbooks_reuse.html#dynamic-vs-static)将帮助您做出正确的决定。

正如我们在本节中所看到的,角色非常简单易用,但却提供了一种非常强大的方式来组织和重用您的 Ansible 代码。在下一节中,我们将通过查看如何将角色特定的变量和依赖项添加到您的代码中来扩展我们简单的基于任务的示例。

设置基于角色的变量和依赖关系

变量是使 Ansible playbook 和角色可重用的核心,因为它们允许相同的代码以略有不同的值或配置数据重新利用。Ansible 角色目录结构允许在两个位置声明特定于角色的变量。虽然乍一看,这两个位置之间的区别可能并不明显,但它具有根本重要性。

基于角色的变量可以放在两个位置之一:

  • defaults/main.yml

  • vars/main.yml

这两个位置之间的区别在于它们在 Ansible 变量优先顺序中的位置。放在defaults/目录中的变量在优先级方面较低,因此很容易被覆盖。这个位置是你想要轻松覆盖的变量的位置,但你不想让变量未定义。例如,如果你要安装 Apache Tomcat,你可能会构建一个安装特定版本的角色。然而,如果有人忘记设置版本,你不希望角色因此退出错误,而是更愿意设置一个合理的默认值,比如7.0.76,然后可以用清单变量或命令行(使用-e--extra-vars开关)轻松覆盖它。这样,即使没有人明确设置这个变量,你也知道角色可以正常工作,但如果需要,它可以很容易地更改为更新的 Tomcat 版本。

然而,放在vars/目录中的变量在 Ansible 的变量优先顺序中更靠前。这不会被清单变量覆盖,因此应该用于更重要的保持静态的变量数据。当然,这并不是说它们不能被覆盖——-e--extra-vars开关是 Ansible 中优先级最高的,因此会覆盖你定义的任何其他内容。

大多数情况下,你可能只会使用基于defaults/的变量,但无疑会有时候,拥有更高优先级变量的选项对你的自动化变得更有价值,因此知道这个选项对你是可用的是至关重要的。

除了之前描述的基于角色的变量之外,还可以使用meta/目录为角色添加元数据。与之前一样,只需在这个目录中添加一个名为main.yml的文件即可。为了解释如何使用meta/目录,让我们构建并运行一个实际的例子,展示它如何被使用。在开始之前,重要的是要注意,默认情况下,Ansible 解析器只允许你运行一个角色一次。这在某种程度上类似于我们之前讨论的处理程序,可以被多次调用,但最终只在 play 结束时运行一次。角色也是一样的,它们可以被多次引用,但实际上只会运行一次。有两个例外情况——第一个是如果角色被多次调用,但使用了不同的变量或参数,另一个是如果被调用的角色在其meta/目录中将allow_duplicates设置为true。在构建示例时,我们将看到这两种情况的例子:

  1. 在我们实际的例子的顶层,我们将有一个与本章节中一直在使用的清单相同的副本。我们还将创建一个名为site.yml的简单 playbook,其中包含以下代码:
---
- name: Role variables and meta playbook
  hosts: frt01.example.com

  roles:
    - platform

请注意,我们只是从这个 playbook 中调用了一个名为platform的角色,playbook 本身没有调用其他内容。

  1. 让我们继续创建platform角色——与我们之前的角色不同,这个角色不包含任何任务,甚至不包含任何变量数据;相反,它只包含一个meta目录。
$ mkdir -p roles/platform/meta

在这个目录中,创建一个名为main.yml的文件,内容如下:

---
dependencies:
- role: linuxtype
  vars:
    type: centos
- role: linuxtype
  vars:
    type: ubuntu

这段代码将告诉 Ansible 平台角色依赖于linuxtype角色。请注意,我们指定了依赖两次,但每次指定时,我们都传递了一个名为type的变量,并赋予不同的值。这样,Ansible 解析器允许我们调用角色两次,因为每次作为依赖项引用时都传递了不同的变量值。

  1. 现在让我们继续创建linuxtype角色,这将不包含任何任务,但会有更多的依赖声明:
$ mkdir -p roles/linuxtype/meta/

再次在meta目录中创建一个main.yml文件,但这次包含以下内容:

---
dependencies:
- role: version
- role: network

再次创建更多的依赖关系——这次,当调用linuxtype角色时,它反过来声明对称为versionnetwork的角色的依赖。

  1. 首先创建version角色——它将包含metatasks目录:
$ mkdir -p roles/version/meta
$ mkdir -p roles/version/tasks

meta目录中,我们将创建一个包含以下内容的main.yml文件:

---
allow_duplicates: true

这个声明在这个例子中很重要——正如前面讨论的,通常情况下,Ansible 只允许一个角色被执行一次,即使它被多次调用。将allow_duplicates设置为true告诉 Ansible 允许角色被执行多次。这是必需的,因为在platform角色中,我们通过依赖两次调用了linuxtype角色,这意味着我们将两次调用version角色。

我们还将在任务目录中创建一个简单的main.yml文件,打印传递给角色的type变量的值:

---
- name: Print type variable
  debug:
    var: type
  1. 现在我们将使用network角色重复这个过程——为了保持我们的示例代码简单,我们将使用与version角色相同的内容定义它:
$ mkdir -p roles/network/meta
$ mkdir -p roles/network/tasks

meta目录中,我们将再次创建一个main.yml文件,其中包含以下内容:

---
allow_duplicates: true

再次在tasks目录中创建一个简单的main.yml文件,打印传递给角色的type变量的值:

---
- name: Print type variable
  debug:
    var: type

在这个过程结束时,你的目录结构应该是这样的:

.
├── hosts
├── roles
│   ├── linuxtype
│   │   └── meta
│   │       └── main.yml
│   ├── network
│   │   ├── meta
│   │   │   └── main.yml
│   │   └── tasks
│   │       └── main.yml
│   ├── platform
│   │   └── meta
│   │       └── main.yml
│   └── version
│       ├── meta
│       │   └── main.yml
│       └── tasks
│           └── main.yml
└── site.yml

11 directories, 8 files

让我们看看运行这个剧本会发生什么。现在,你可能会认为剧本会像这样运行:根据我们在前面的代码中创建的依赖结构,我们的初始剧本静态导入platform角色。platform角色然后声明依赖于linuxtype角色,并且每次使用名为type的变量声明两次不同的值。linuxtype角色然后声明依赖于networkversion角色,这些角色可以运行多次并打印type的值。因此,你可能会认为我们会看到networkversion角色被调用两次,第一次打印centos,第二次打印ubuntu(因为这是我们最初在platform角色中指定依赖关系的方式)。然而,当我们运行它时,实际上看到的是这样的:

$ ansible-playbook -i hosts site.yml

PLAY [Role variables and meta playbook] ****************************************

TASK [Gathering Facts] *********************************************************
ok: [frt01.example.com]

TASK [version : Print type variable] *******************************************
ok: [frt01.example.com] => {
 "type": "ubuntu"
}

TASK [network : Print type variable] *******************************************
ok: [frt01.example.com] => {
 "type": "ubuntu"
}

TASK [version : Print type variable] *******************************************
ok: [frt01.example.com] => {
 "type": "ubuntu"
}

TASK [network : Print type variable] *******************************************
ok: [frt01.example.com] => {
 "type": "ubuntu"
}

PLAY RECAP *********************************************************************
frt01.example.com : ok=5 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

发生了什么?尽管我们看到networkversion角色被调用了两次(如预期的那样),但type变量的值始终是ubuntu。这突出了关于 Ansible 解析器工作方式的重要一点,以及静态导入(我们在这里所做的)和动态包含(我们在前一节中讨论过)之间的区别。

使用静态导入时,角色变量的作用域就好像它们是在播放级别而不是角色级别定义的一样。角色本身在解析时都会被解析并合并到我们在site.yml剧本中创建的播放中,因此,Ansible 解析器会创建(在内存中)一个包含来自我们目录结构的所有合并变量和角色内容的大型剧本。这样做并没有错,但意味着type变量每次声明时都会被覆盖,因此我们声明的最后一个值(在这种情况下是ubuntu)是用于播放运行的值。

那么,我们如何使这个剧本按照我们最初的意图运行——加载我们的依赖角色,但使用我们为type变量定义的两个不同值?

这个问题的答案是,如果我们要继续使用静态导入的角色,那么在声明依赖关系时就不应该使用角色变量。相反,我们应该将type作为角色参数传递。这是一个小但至关重要的区别——即使在运行 Ansible 解析器时,角色参数仍然保持在角色级别上,因此我们可以在不覆盖变量的情况下声明我们的依赖两次。要做到这一点,将roles/platform/meta/main.yml文件的内容更改为以下内容:

---
dependencies:
- role: linuxtype
  type: centos
- role: linuxtype
  type: ubuntu

您注意到微妙的变化了吗?vars:关键字消失了,type的声明现在处于较低的缩进级别,这意味着它是一个角色参数。现在,当我们运行 playbook 时,我们得到了我们所希望的结果:

$ ansible-playbook -i hosts site.yml

PLAY [Role variables and meta playbook] ****************************************

TASK [Gathering Facts] *********************************************************
ok: [frt01.example.com]

TASK [version : Print type variable] *******************************************
ok: [frt01.example.com] => {
 "type": "centos"
}

TASK [network : Print type variable] *******************************************
ok: [frt01.example.com] => {
 "type": "centos"
}

TASK [version : Print type variable] *******************************************
ok: [frt01.example.com] => {
 "type": "ubuntu"
}

TASK [network : Print type variable] *******************************************
ok: [frt01.example.com] => {
 "type": "ubuntu"
}

PLAY RECAP *********************************************************************
frt01.example.com : ok=5 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

这是一个相当高级的 Ansible 角色依赖示例,但是提供给您是为了演示了解变量优先级(即变量的作用域)和解析器工作的重要性。如果您编写简单的、按顺序解析的任务,那么您可能永远不需要了解这一点,但我建议您广泛使用调试语句,并测试您的 playbook 设计,以确保在 playbook 开发过程中不会遇到这种问题。

在对角色的许多方面进行了详细的研究之后,让我们在下一节中看一下一个用于公开可用的 Ansible 角色的集中存储——Ansible Galaxy。

Ansible Galaxy

没有关于 Ansible 角色的部分会完整无缺地提到 Ansible Galaxy。Ansible Galaxy 是 Ansible 托管的一个由社区驱动的 Ansible 角色集合,托管在galaxy.ansible.com/。它包含了许多社区贡献的 Ansible 角色,如果您能构想出一个自动化任务,很有可能已经有人编写了一个角色来完全满足您的需求。它非常值得探索,并且可以让您的自动化项目迅速起步,因为您可以开始使用一组现成的角色。

除了网站之外,ansible-galaxy客户端也包含在 Ansible 中,这为您提供了一种快速便捷的方式,让您下载并部署角色到您的 playbook 结构中。假设您想要在目标主机上更新每日消息MOTD)—这肯定是有人已经想出来的事情。在 Ansible Galaxy 网站上快速搜索返回(在撰写本文时)106 个设置 MOTD 的角色。如果我们想使用其中一个,我们可以使用以下命令将其下载到我们的角色目录中:

$ ansible-galaxy role install -p roles/ arillso.motd

这就是您需要做的一切——一旦下载完成,您可以像在本章讨论的手动创建的角色一样,在 playbook 中导入或包含角色。请注意,如果您不指定-p roles/ansible-galaxy会将角色安装到~/.ansible/roles,这是您的用户帐户的中央角色目录。当然,这可能是您想要的,但如果您希望将角色直接下载到 playbook 目录结构中,您可以添加此参数。

另一个巧妙的技巧是使用ansible-galaxy为您创建一个空的角色目录结构,以便您在其中创建自己的角色——这样可以节省我们在本章中一直在进行的所有手动目录和文件创建,就像在这个例子中一样:

$ ansible-galaxy role init --init-path roles/ testrole
- Role testrole was created successfully
$ tree roles/testrole/
roles/testrole/
├── defaults
│   └── main.yml
├── files
├── handlers
│   └── main.yml
├── meta
│   └── main.yml
├── README.md
├── tasks
│   └── main.yml
├── templates
├── tests
│   ├── inventory
│   └── test.yml
└── vars
 └── main.yml 

这应该为您提供足够的信息,让您开始进入 Ansible 角色的旅程。我无法再次强调开发代码作为角色是多么重要——最初可能看起来不重要,但随着您的自动化用例的扩展,以及重用代码的需求增长,您会为自己的决定感到高兴。在下一节中,让我们扩展一下对 Ansible playbook 的讨论,讨论条件逻辑在您的 Ansible 代码中的使用方式。

在您的代码中使用条件

到目前为止,在我们的大多数示例中,我们创建了一组简单的任务集,这些任务总是运行。然而,当你生成你想要应用于更广泛主机数组的任务(无论是在角色还是 playbooks 中),迟早你会想要执行某种条件动作。这可能是只对先前任务的结果执行任务。或者可能是只对从 Ansible 系统中收集的特定事实执行任务。在本节中,我们将提供一些实际的条件逻辑示例,以演示如何在你的 Ansible 任务中应用这个特性。

和以往一样,我们需要一个清单来开始,并且我们将重用本章中一直使用的清单:

[frontends]
frt01.example.com https_port=8443
frt02.example.com http_proxy=proxy.example.com

[frontends:vars]
ntp_server=ntp.frt.example.com
proxy=proxy.frt.example.com

[apps]
app01.example.com
app02.example.com

[webapp:children]
frontends
apps

[webapp:vars]
proxy_server=proxy.webapp.example.com
health_check_retry=3
health_check_interal=60

假设你只想在某些操作系统上执行 Ansible 任务。我们已经讨论了 Ansible 事实,这为在 playbooks 中探索条件逻辑提供了一个完美的平台。考虑一下:所有你的 CentOS 系统都发布了一个紧急补丁,你想立即应用它。当然,你可以逐个创建一个专门的清单(或主机组)来适用于 CentOS 主机,但这是你不一定需要做的额外工作。

相反,让我们定义一个将执行我们的更新的任务,但在一个简单的示例 playbook 中添加一个包含 Jinja 2 表达式的when子句:

---
- name: Play to patch only CentOS systems
  hosts: all
  become: true

  tasks:
  - name: Patch CentOS systems
    yum:
      name: httpd
      state: latest
    when: ansible_facts['distribution'] == "CentOS"

现在,当我们运行这个任务时,如果你的测试系统是基于 CentOS 的(我的也是),你应该会看到类似以下的输出:

$ ansible-playbook -i hosts condition.yml

PLAY [Play to patch only CentOS systems] ***************************************

TASK [Gathering Facts] *********************************************************
ok: [frt02.example.com]
ok: [app01.example.com]
ok: [frt01.example.com]
ok: [app02.example.com]

TASK [Patch CentOS systems] ****************************************************
ok: [app01.example.com]
changed: [frt01.example.com]
ok: [app02.example.com]
ok: [frt02.example.com]

PLAY RECAP *********************************************************************
app01.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
app02.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt01.example.com : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt02.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

前面的输出显示,我们所有的系统都是基于 CentOS 的,但只有frt01.example.com需要应用补丁。现在我们可以使我们的逻辑更加精确——也许只有我们的旧系统运行在 CentOS 6 上需要应用补丁。在这种情况下,我们可以扩展 playbook 中的逻辑,检查发行版和主要版本,如下所示:

---
- name: Play to patch only CentOS systems
  hosts: all
  become: true

  tasks:
  - name: Patch CentOS systems
    yum:
      name: httpd
      state: latest
    when: (ansible_facts['distribution'] == "CentOS" and ansible_facts['distribution_major_version'] == "6")

现在,如果我们运行我们修改后的 playbook,根据你的清单中有哪些系统,你可能会看到类似以下的输出。在这种情况下,我的app01.example.com服务器基于 CentOS 6,因此已应用了补丁。所有其他系统都被跳过,因为它们不符合我的逻辑表达式:

$ ansible-playbook -i hosts condition2.yml

PLAY [Play to patch only CentOS systems] ***************************************

TASK [Gathering Facts] *********************************************************
ok: [frt01.example.com]
ok: [app02.example.com]
ok: [app01.example.com]
ok: [frt02.example.com]

TASK [Patch CentOS systems] ****************************************************
changed: [app01.example.com]
skipping: [frt01.example.com]
skipping: [frt02.example.com]
skipping: [app02.example.com]

PLAY RECAP *********************************************************************
app01.example.com : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
app02.example.com : ok=1 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
frt01.example.com : ok=1 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
frt02.example.com : ok=1 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0

当你运行任何 Ansible 模块(无论是shellcommandyumcopy还是其他模块),模块都会返回详细的运行结果数据。你可以使用register关键字将其捕获到一个标准的 Ansible 变量中,然后在 playbook 中稍后进一步处理它。

考虑以下 playbook 代码。它包含两个任务,第一个任务是获取当前目录的列表,并将shell模块的输出捕获到一个名为shellresult的变量中。然后打印一个简单的debug消息,但只有在shell命令的输出中包含hosts字符串时才会打印:

---
- name: Play to patch only CentOS systems
  hosts: localhost
  become: true

  tasks:
    - name: Gather directory listing from local system
      shell: "ls -l"
      register: shellresult

    - name: Alert if we find a hosts file
      debug:
        msg: "Found hosts file!"
      when: '"hosts" in shellresult.stdout'

现在,当我们在当前目录中运行这个命令时,如果你是从本书附带的 GitHub 仓库中工作,那么目录中将包含一个名为hosts的文件,那么你应该会看到类似以下的输出:

$ ansible-playbook condition3.yml
[WARNING]: provided hosts list is empty, only localhost is available. Note that
the implicit localhost does not match 'all'

PLAY [Play to patch only CentOS systems] ***************************************

TASK [Gathering Facts] *********************************************************
ok: [localhost]

TASK [Gather directory listing from local system] ******************************
changed: [localhost]

TASK [Alert if we find a hosts file] *******************************************
ok: [localhost] => {
 "msg": "Found hosts file!"
}

PLAY RECAP *********************************************************************
localhost : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

然而,如果文件不存在,那么你会发现debug消息被跳过了。

$ ansible-playbook condition3.yml
[WARNING]: provided hosts list is empty, only localhost is available. Note that
the implicit localhost does not match 'all'

PLAY [Play to patch only CentOS systems] ***************************************

TASK [Gathering Facts] *********************************************************
ok: [localhost]

TASK [Gather directory listing from local system] ******************************
changed: [localhost]

TASK [Alert if we find a hosts file] *******************************************
skipping: [localhost]

PLAY RECAP *********************************************************************
localhost : ok=2 changed=1 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0

你也可以为生产中的 IT 运维任务创建复杂的条件;但是,请记住,在 Ansible 中,默认情况下变量不会被转换为任何特定的类型,因此即使变量(或事实)的内容看起来像一个数字,Ansible 默认也会将其视为字符串。如果你需要执行整数比较,你必须首先将变量转换为整数类型。例如,这是一个 playbook 的片段,它只在 Fedora 25 及更新版本上运行一个任务:

tasks:
  - name: Only perform this task on Fedora 25 and later
 shell: echo "only on Fedora 25 and later"
    when: ansible_facts['distribution'] == "Fedora" and ansible_facts['distribution_major_version']|int >= 25 

你可以应用许多不同类型的条件到你的 Ansible 任务中,这一节只是触及了表面;然而,它应该为你扩展你在 Ansible 任务中应用条件的知识提供了一个坚实的基础。你不仅可以将条件逻辑应用到 Ansible 任务中,还可以在一组数据上运行它们,并且我们将在下一节中探讨这一点。

使用循环重复任务

通常,我们希望执行一个单一的任务,但使用该单一任务来迭代一组数据。例如,你可能不想创建一个用户帐户,而是创建 10 个。或者你可能想要将 15 个软件包安装到系统中。可能性是无穷无尽的,但要点仍然是一样的——你不想编写 10 个单独的 Ansible 任务来创建 10 个用户帐户。幸运的是,Ansible 支持对数据集进行循环,以确保你可以使用紧密定义的代码执行大规模操作。在本节中,我们将探讨如何在你的 Ansible playbook 中实际使用循环。

和以往一样,我们必须从清单开始工作,并且我们将使用我们在本章中一直使用的熟悉清单:

[frontends]
frt01.example.com https_port=8443
frt02.example.com http_proxy=proxy.example.com

[frontends:vars]
ntp_server=ntp.frt.example.com
proxy=proxy.frt.example.com

[apps]
app01.example.com
app02.example.com

[webapp:children]
frontends
apps

[webapp:vars]
proxy_server=proxy.webapp.example.com
health_check_retry=3
health_check_interal=60

让我们从一个非常简单的 playbook 开始,向你展示如何在单个任务中循环一组数据。虽然这是一个相当牵强的例子,但它旨在简单地向你展示循环在 Ansible 中的工作原理。我们将定义一个单个任务,在清单中的单个主机上运行command模块,并使用command模块在远程系统上依次echo数字 1 到 6(可以很容易地扩展到添加用户帐户或创建一系列文件)。

考虑以下代码:

---
- name: Simple loop demo play
  hosts: frt01.example.com

  tasks:
    - name: Echo a value from the loop
      command: echo "{{ item }}"
      loop:
        - 1
        - 2
        - 3
        - 4
        - 5
        - 6

loop:语句定义了循环的开始,循环中的项目被定义为一个 YAML 列表。此外,请注意更高的缩进级别,这告诉解析器它们是循环的一部分。在处理循环数据时,我们使用一个名为item的特殊变量,其中包含要回显的循环迭代的当前值。因此,如果我们运行这个 playbook,我们应该看到类似以下的输出:

$ ansible-playbook -i hosts loop1.yml

PLAY [Simple loop demo play] ***************************************************

TASK [Gathering Facts] *********************************************************
ok: [frt01.example.com]

TASK [Echo a value from the loop] **********************************************
changed: [frt01.example.com] => (item=1)
changed: [frt01.example.com] => (item=2)
changed: [frt01.example.com] => (item=3)
changed: [frt01.example.com] => (item=4)
changed: [frt01.example.com] => (item=5)
changed: [frt01.example.com] => (item=6)

PLAY RECAP *********************************************************************
frt01.example.com : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

你可以将我们在前一节中讨论的条件逻辑与循环结合起来,使循环仅对其数据的子集进行操作。例如,考虑以下 playbook 的迭代:


---
- name: Simple loop demo play
  hosts: frt01.example.com

  tasks:
    - name: Echo a value from the loop
      command: echo "{{ item }}"
      loop:
        - 1
        - 2
        - 3
        - 4
        - 5
        - 6
      when: item|int > 3

现在,当我们运行这个时,我们会看到任务被跳过,直到我们达到循环内容中的整数值 4 及以上:

$ ansible-playbook -i hosts loop2.yml

PLAY [Simple loop demo play] ***************************************************

TASK [Gathering Facts] *********************************************************
ok: [frt01.example.com]

TASK [Echo a value from the loop] **********************************************
skipping: [frt01.example.com] => (item=1)
skipping: [frt01.example.com] => (item=2)
skipping: [frt01.example.com] => (item=3)
changed: [frt01.example.com] => (item=4)
changed: [frt01.example.com] => (item=5)
changed: [frt01.example.com] => (item=6)

PLAY RECAP *********************************************************************
frt01.example.com : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

当然,你可以将这个与基于 Ansible 事实和其他变量的条件逻辑相结合,就像我们之前讨论过的那样。就像我们以前使用register关键字捕获模块执行的结果一样,我们也可以使用循环来做到这一点。唯一的区别是,结果现在将存储在一个字典中,每次循环迭代都会有一个字典条目,而不仅仅是一组结果。

因此,让我们看看如果我们进一步增强 playbook 会发生什么:

---
- name: Simple loop demo play
  hosts: frt01.example.com

  tasks:
    - name: Echo a value from the loop
      command: echo "{{ item }}"
      loop:
        - 1
        - 2
        - 3
        - 4
        - 5
        - 6
      when: item|int > 3
      register: loopresult

    - name: Print the results from the loop
      debug:
        var: loopresult

现在,当我们运行 playbook 时,你将看到包含loopresult内容的字典的输出页面。由于空间限制,以下输出被截断,但演示了运行此 playbook 时你应该期望的结果类型:

$ ansible-playbook -i hosts loop3.yml

PLAY [Simple loop demo play] ***************************************************

TASK [Gathering Facts] *********************************************************
ok: [frt01.example.com]

TASK [Echo a value from the loop] **********************************************
skipping: [frt01.example.com] => (item=1)
skipping: [frt01.example.com] => (item=2)
skipping: [frt01.example.com] => (item=3)
changed: [frt01.example.com] => (item=4)
changed: [frt01.example.com] => (item=5)
changed: [frt01.example.com] => (item=6)

TASK [Print the results from the loop] *****************************************
ok: [frt01.example.com] => {
 "loopresult": {
 "changed": true,
 "msg": "All items completed",
 "results": [
 {
 "ansible_loop_var": "item",
 "changed": false,
 "item": 1,
 "skip_reason": "Conditional result was False",
 "skipped": true
 },
 {
 "ansible_loop_var": "item",
 "changed": false,
 "item": 2,
 "skip_reason": "Conditional result was False",
 "skipped": true
 },

正如你所看到的,输出的结果部分是一个字典,我们可以清楚地看到列表中的前两个项目被skipped,因为我们when子句的结果(Conditional)是false

因此,到目前为止,我们可以看到循环很容易定义和使用,但你可能会问,你能创建嵌套循环吗?这个问题的答案是可以,但有一个问题——特殊变量item会发生冲突,因为内部循环和外部循环都会使用相同的变量名。这意味着你嵌套循环运行的结果将是意想不到的。

幸运的是,有一个名为loop_controlloop参数,允许您更改包含当前loop迭代数据的特殊变量的名称,从item更改为您选择的内容。让我们创建一个嵌套循环来看看它是如何工作的。

首先,我们将以通常的方式创建一个 playbook,其中包含一个要在循环中运行的单个任务。为了生成我们的嵌套循环,我们将使用include_tasks目录来动态包含另一个 YAML 文件中的单个任务,该文件还将包含一个循环。由于我们打算在嵌套循环中使用此 playbook,因此我们将使用loop_var指令将特殊循环内容变量的名称从item更改为second_item

---
- name: Play to demonstrate nested loops
  hosts: localhost

  tasks:
    - name: Outer loop
      include_tasks: loopsubtask.yml
      loop:
        - a
        - b
        - c
      loop_control:
        loop_var: second_item

然后,我们将创建一个名为loopsubtask.yml的第二个文件,其中包含内部循环,并包含在前面的 playbook 中。由于我们已经在外部循环中更改了循环项变量名称,因此在这里不需要再次更改它。请注意,此文件的结构非常类似于角色中的任务文件-它不是一个完整的 playbook,而只是一个任务列表:

---
- name: Inner loop
  debug:
    msg: "second item={{ second_item }} first item={{ item }}"
  loop:
    - 100
    - 200
    - 300

现在,您应该能够运行 playbook,并且您将看到 Ansible 首先迭代外部循环,然后处理由外部循环定义的数据的内部循环。由于循环变量名称不冲突,一切都按我们的预期工作:

$ ansible-playbook loopmain.yml
[WARNING]: provided hosts list is empty, only localhost is available. Note that
the implicit localhost does not match 'all'

PLAY [Play to demonstrate nested loops] ****************************************

TASK [Gathering Facts] *********************************************************
ok: [localhost]

TASK [Outer loop] **************************************************************
included: /root/Practical-Ansible-2/Chapter 4/loopsubtask.yml for localhost
included: /root/Practical-Ansible-2/Chapter 4/loopsubtask.yml for localhost
included: /root/Practical-Ansible-2/Chapter 4/loopsubtask.yml for localhost

TASK [Inner loop] **************************************************************
ok: [localhost] => (item=100) => {
 "msg": "second item=a first item=100"
}
ok: [localhost] => (item=200) => {
 "msg": "second item=a first item=200"
}
ok: [localhost] => (item=300) => {
 "msg": "second item=a first item=300"
}

TASK [Inner loop] **************************************************************
ok: [localhost] => (item=100) => {
 "msg": "second item=b first item=100"
}
ok: [localhost] => (item=200) => {
 "msg": "second item=b first item=200"
}
ok: [localhost] => (item=300) => {
 "msg": "second item=b first item=300"
}

TASK [Inner loop] **************************************************************
ok: [localhost] => (item=100) => {
 "msg": "second item=c first item=100"
}
ok: [localhost] => (item=200) => {
 "msg": "second item=c first item=200"
}
ok: [localhost] => (item=300) => {
 "msg": "second item=c first item=300"
}

PLAY RECAP *********************************************************************
localhost : ok=7 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

循环很容易使用,但非常强大,因为它们允许您轻松地使用一个任务来迭代大量数据。在下一节中,我们将看一下 Ansible 语言的另一个构造,用于控制 playbook 流程-块。

使用块分组任务

在 Ansible 中,块允许您逻辑地将一组任务组合在一起,主要用于两个目的之一。一个可能是对整组任务应用条件逻辑;在这个例子中,您可以将相同的 when 子句应用于每个任务,但这很麻烦和低效-最好将所有任务放在一个块中,并将条件逻辑应用于块本身。这样,逻辑只需要声明一次。块在处理错误和特别是从错误条件中恢复时也非常有价值。在本章中,我们将通过简单的实际示例来探讨这两个问题,以帮助您快速掌握 Ansible 中的块。

一如既往,让我们确保我们有一个清单可以使用:

[frontends]
frt01.example.com https_port=8443
frt02.example.com http_proxy=proxy.example.com

[frontends:vars]
ntp_server=ntp.frt.example.com
proxy=proxy.frt.example.com

[apps]
app01.example.com
app02.example.com

[webapp:children]
frontends
apps

[webapp:vars]
proxy_server=proxy.webapp.example.com
health_check_retry=3
health_check_interal=60

现在,让我们直接看一个如何使用块来对一组任务应用条件逻辑的示例。在高层次上,假设我们想在所有 Fedora Linux 主机上执行以下操作:

  • 为 Apache web 服务器安装软件包。

  • 安装一个模板化的配置。

  • 启动适当的服务。

我们可以通过三个单独的任务来实现这一点,所有这些任务都与一个when子句相关联,但是块为我们提供了更好的方法。以下示例 playbook 显示了包含在块中的三个任务(请注意需要额外的缩进级别来表示它们在块中的存在):

---
- name: Conditional block play
  hosts: all
  become: true

  tasks:
  - name: Install and configure Apache
    block:
      - name: Install the Apache package
        dnf:
          name: httpd
          state: installed
      - name: Install the templated configuration to a dummy location
        template:
          src: templates/src.j2
          dest: /tmp/my.conf
      - name: Start the httpd service
        service:
          name: httpd
          state: started
          enabled: True
    when: ansible_facts['distribution'] == 'Fedora'

当您运行此 playbook 时,您应该发现与您可能在清单中拥有的任何 Fedora 主机上只运行与 Apache 相关的任务;您应该看到三个任务中的所有任务都被运行或跳过-取决于清单的组成和内容,它可能看起来像这样:

$ ansible-playbook -i hosts blocks.yml

PLAY [Conditional block play] **************************************************

TASK [Gathering Facts] *********************************************************
ok: [app02.example.com]
ok: [frt01.example.com]
ok: [app01.example.com]
ok: [frt02.example.com]

TASK [Install the Apache package] **********************************************
changed: [frt01.example.com]
changed: [frt02.example.com]
skipping: [app01.example.com]
skipping: [app02.example.com]

TASK [Install the templated configuration to a dummy location] *****************
changed: [frt01.example.com]
changed: [frt02.example.com]
skipping: [app01.example.com]
skipping: [app02.example.com]

TASK [Start the httpd service] *************************************************
changed: [frt01.example.com]
changed: [frt02.example.com]
skipping: [app01.example.com]
skipping: [app02.example.com]

PLAY RECAP *********************************************************************
app01.example.com : ok=1 changed=0 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
app02.example.com : ok=1 changed=0 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
frt01.example.com : ok=4 changed=3 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
frt02.example.com : ok=4 changed=3 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0

这很容易构建,但在控制大量任务流程方面非常强大。

这一次,让我们构建一个不同的示例,以演示如何利用块来帮助 Ansible 优雅地处理错误条件。到目前为止,您应该已经经历过,如果您的 playbook 遇到任何错误,它们可能会在失败点停止执行。在某些情况下,这远非理想,您可能希望在此事件中执行某种恢复操作,而不仅仅是停止 playbook。

让我们创建一个新的 playbook,这次内容如下:

---
- name: Play to demonstrate block error handling
  hosts: frontends

  tasks:
    - name: block to handle errors
      block:
        - name: Perform a successful task
          debug:
            msg: 'Normally executing....'
        - name: Deliberately create an error
          command: /bin/whatever
        - name: This task should not run if the previous one results in an error
          debug:
            msg: 'Never print this message if the above command fails!!!!'
      rescue:
        - name: Catch the error (and perform recovery actions)
          debug:
            msg: 'Caught the error'
        - name: Deliberately create another error
          command: /bin/whatever
        - name: This task should not run if the previous one results in an error
          debug:
            msg: 'Do not print this message if the above command fails!!!!'
      always:
        - name: This task always runs!
          debug:
            msg: "Tasks in this part of the play will be ALWAYS executed!!!!"

请注意,在前面的 play 中,我们现在有了额外的block部分——除了block本身中的任务外,我们还有两个标记为rescuealways的新部分。执行流程如下:

  1. block部分中的所有任务都按照其列出的顺序正常执行。

  2. 如果block中的任务导致错误,则不会运行block中的其他任务:

  • rescue部分中的任务按其列出的顺序开始运行。

  • 如果block任务没有导致错误,则rescue部分中的任务不会运行。

  1. 如果在rescue部分运行的任务导致错误,则不会执行进一步的rescue任务,执行将移至always部分。

  2. always部分中的任务始终运行,无论blockrescue部分是否出现错误。即使没有遇到错误,它们也会运行。

考虑到这种执行流程,当您执行此 playbook 时,您应该会看到类似以下的输出,注意我们故意创建了两个错误条件来演示流程:

$ ansible-playbook -i hosts blocks-error.yml

PLAY [Play to demonstrate block error handling] ********************************

TASK [Gathering Facts] *********************************************************
ok: [frt02.example.com]
ok: [frt01.example.com]

TASK [Perform a successful task] ***********************************************
ok: [frt01.example.com] => {
    "msg": "Normally executing...."
}
ok: [frt02.example.com] => {
    "msg": "Normally executing...."
}

TASK [Deliberately create an error] ********************************************
fatal: [frt01.example.com]: FAILED! => {"changed": false, "cmd": "/bin/whatever", "msg": "[Errno 2] No such file or directory", "rc": 2}
fatal: [frt02.example.com]: FAILED! => {"changed": false, "cmd": "/bin/whatever", "msg": "[Errno 2] No such file or directory", "rc": 2}

TASK [Catch the error (and perform recovery actions)] **************************
ok: [frt01.example.com] => {
    "msg": "Caught the error"
}
ok: [frt02.example.com] => {
    "msg": "Caught the error"
}

TASK [Deliberately create another error] ***************************************
fatal: [frt01.example.com]: FAILED! => {"changed": false, "cmd": "/bin/whatever", "msg": "[Errno 2] No such file or directory", "rc": 2}
fatal: [frt02.example.com]: FAILED! => {"changed": false, "cmd": "/bin/whatever", "msg": "[Errno 2] No such file or directory", "rc": 2}

TASK [This task always runs!] **************************************************
ok: [frt01.example.com] => {
    "msg": "Tasks in this part of the play will be ALWAYS executed!!!!"
}
ok: [frt02.example.com] => {
    "msg": "Tasks in this part of the play will be ALWAYS executed!!!!"
}

PLAY RECAP *********************************************************************
frt01.example.com : ok=4 changed=0 unreachable=0 failed=1 skipped=0 rescued=1 ignored=0
frt02.example.com : ok=4 changed=0 unreachable=0 failed=1 skipped=0 rescued=1 ignored=0

Ansible 有两个特殊变量,其中包含您可能在rescue块中找到有用的信息以执行恢复操作:

  • ansible_failed_task:这是一个包含来自block失败的任务的详细信息的字典,导致我们进入rescue部分。您可以通过使用debug显示其内容来探索这一点,但例如,失败任务的名称可以从ansible_failed_task.name中获取。

  • ansible_failed_result:这是失败任务的结果,并且与如果您在失败的任务中添加了register关键字的行为相同。这样可以避免在每个任务中添加register以防它失败。

随着您的 playbooks 变得更加复杂,错误处理变得越来越重要(或者条件逻辑变得更加重要),block将成为编写良好、健壮的 playbooks 的重要组成部分。让我们在下一节中继续探讨执行策略,以进一步控制您的 playbook 运行。

通过策略配置 play 执行

随着您的 playbooks 变得越来越复杂,调试任何可能出现的问题变得越来越重要。例如,您是否可以在执行过程中检查给定变量(或变量)的内容,而无需在整个 playbook 中插入debug语句?同样,我们迄今为止已经看到,Ansible 将确保特定任务在应用于所有清单主机之前完成,然后再移动到下一个任务——是否有办法改变这一点?

当您开始使用 Ansible 时,默认情况下看到的执行策略(尽管我们尚未提到它的名称)被称为linear。这正是它所描述的——在开始下一个任务之前,每个任务都会在所有适用的主机上依次执行。然而,还有另一种不太常用的策略称为free,它允许所有任务在每个主机上尽快完成,而不必等待其他主机。

然而,当您开始使用 Ansible 时,最有用的策略将是debug策略,这使得 Ansible 可以在 playbook 中发生错误时直接将您置于集成的调试环境中。让我们通过创建一个有意义的错误的 playbook 来演示这一点。请注意 play 定义中的strategy: debugdebugger: on_failed语句:

---
- name: Play to demonstrate the debug strategy
  hosts: frt01.example.com
  strategy: debug
  debugger: on_failed
  gather_facts: no
  vars:
    username: daniel

  tasks:
    - name: Generate an error by referencing an undefined variable
      ping: data={{ mobile }}

现在,如果您执行此 playbook,您应该会看到它开始运行,但是当遇到它包含的故意错误时,它会将您带入集成调试器。输出的开头应该类似于以下内容:

$ ansible-playbook -i hosts debug.yml

PLAY [Play to demonstrate the debug strategy] **********************************

TASK [Generate an error by referencing an undefined variable] ******************
fatal: [frt01.example.com]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'mobile' is undefined\n\nThe error appears to be in '/root/Practical-Ansible-2/Chapter 4/debug.yml': line 11, column 7, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n tasks:\n - name: Generate an error by referencing an undefined variable\n ^ here\n"}
[frt01.example.com] TASK: Generate an error by referencing an undefined variable (debug)>

[frt02.prod.com] TASK: make an error with refering incorrect variable (debug)> p task_vars
{'ansible_check_mode': False,
 'ansible_current_hosts': [u'frt02.prod.com'],
 'ansible_diff_mode': False,
 'ansible_facts': {},
 'ansible_failed_hosts': [],
 'ansible_forks': 5,
...
[frt02.prod.com] TASK: make an error with refering incorrect variable (debug)> quit
User interrupted execution
$ 

请注意,剧本开始执行,但在第一个任务上失败,并显示错误,因为变量未定义。但是,它不是退出到 shell,而是进入交互式调试器。本书不涵盖调试器的详尽指南,但如果您有兴趣学习,可以在此处找到更多详细信息:docs.ansible.com/ansible/latest/user_guide/playbooks_debugger.html

然而,为了带您进行一个非常简单的实际调试示例,输入p task命令——这将导致 Ansible 调试器打印失败任务的名称;如果您正在进行一个大型剧本,这将非常有用:

[frt01.example.com] TASK: Generate an error by referencing an undefined variable (debug)> p task
TASK: Generate an error by referencing an undefined variable

现在我们知道了剧本失败的原因,所以让我们通过发出p task.args命令来深入了解一下,这将显示传递给任务模块的参数:

[frt01.example.com] TASK: Generate an error by referencing an undefined variable (debug)> p task.args
{u'data': u'{{ mobile }}'}

因此,我们可以看到我们的模块传递了一个名为data的参数,参数值是一个变量(由大括号对表示)称为mobile。因此,可能有必要查看任务可用的变量,看看这个变量是否存在,如果存在的话,值是否合理(使用p task_vars命令来执行此操作):

[frt01.example.com] TASK: Generate an error by referencing an undefined variable (debug)> p task_vars
{'ansible_check_mode': False,
 'ansible_current_hosts': [u'frt01.example.com'],
 'ansible_dependent_role_names': [],
 'ansible_diff_mode': False,
 'ansible_facts': {},
 'ansible_failed_hosts': [],
 'ansible_forks': 5,

上述输出被截断了,您会发现与任务相关的许多变量——这是因为任何收集的事实和内部 Ansible 变量都可用于任务。但是,如果您浏览列表,您将能够确认没有名为mobile的变量。

因此,这应该足够的信息来修复您的剧本。输入q退出调试器:

[frt01.example.com] TASK: Generate an error by referencing an undefined variable (debug)> q
User interrupted execution
$

Ansible 调试器是一个非常强大的工具,您应该学会有效地使用它,特别是当您的剧本复杂性增加时。这结束了我们对剧本设计各个方面的实际考察——在下一节中,我们将看看您可以将 Git 源代码管理集成到您的剧本中的方法。

使用 ansible-pull

ansible-pull命令是 Ansible 的一个特殊功能,允许您一次性从 Git 存储库(例如 GitHub)中拉取一个剧本,然后执行它,因此节省了克隆(或更新工作副本)存储库,然后执行剧本等常规步骤。ansible-pull的好处在于它允许您集中存储和版本控制您的剧本,然后使用单个命令执行它们,从而使它们能够使用cron调度程序执行,而无需甚至在给定的主机上安装 Ansible 剧本。

然而,需要注意的一点是,虽然ansibleansible-playbook命令都可以在整个清单上运行剧本,并针对一个或多个远程主机运行剧本,但ansible-pull命令只打算在本地主机上运行从您的源代码控制系统获取的剧本。因此,如果您想在整个基础架构中使用ansible-pull,您必须将其安装到每个需要它的主机上。

尽管如此,让我们看看这可能是如何工作的。我们将简单地手动运行命令来探索它的应用,但实际上,您几乎肯定会将其安装到您的crontab中,以便定期运行,捕捉您对剧本所做的任何更改版本控制系统中。

由于ansible-pull只打算在本地系统上运行剧本,因此清单文件有些多余——相反,我们将使用一个很少使用的清单规范,您可以在命令行上简单地指定清单主机目录为逗号分隔的列表。如果您只有一个主机,只需指定其名称,然后加上逗号。

让我们使用 GitHub 上的一个简单的剧本,根据变量内容设置每日消息。为此,我们将运行以下命令(我们将在一分钟内分解):

$ ansible-pull -d /var/ansible-set-motd -i ${HOSTNAME}, -U https://github.com/jamesfreeman959/ansible-set-motd.git site.yml -e "ag_motd_content='MOTD generated by ansible-pull'" >> /tmp/ansible-pull.log 2>&1

这个命令分解如下:

  • -d /var/ansible-set-motd:这将设置包含来自 GitHub 的代码检出的工作目录。

  • -i ${HOSTNAME},:这仅在当前主机上运行,由适当的 shell 变量指定其主机名。

  • -U https://github.com/jamesfreeman959/ansible-set-motd.git:我们使用此 URL 来获取 playbooks。

  • site.yml:这是要运行的 playbook 的名称。

  • -e "ag_motd_content='MOTD generated by ansible-pull'":这将设置适当的 Ansible 变量以生成 MOTD 内容。

  • >> /tmp/ansible-pull.log 2>&1:这将重定向命令的输出到日志文件,以便以后分析 - 特别是在cron job中运行命令时,输出将永远不会打印到用户的终端上,这是非常有用的。

当您运行此命令时,您应该会看到类似以下的输出(请注意,为了更容易看到输出,已删除了日志重定向):

$ ansible-pull -d /var/ansible-set-motd -i ${HOSTNAME}, -U https://github.com/jamesfreeman959/ansible-set-motd.git site.yml -e "ag_motd_content='MOTD generated by ansible-pull'"
Starting Ansible Pull at 2020-04-14 17:26:21
/usr/bin/ansible-pull -d /var/ansible-set-motd -i cookbook, -U https://github.com/jamesfreeman959/ansible-set-motd.git site.yml -e ag_motd_content='MOTD generated by ansible-pull'
cookbook |[WARNING]: SUCCESS = Your git > {
    "aversion isfter": "7d too old t3a191ecb2do fully suebe7f84f4fpport the a5817b0f1bdepth argu49c4cd54",ment.
Fall
    "ansing back tible_factso full che": {
     ckouts.
   "discovered_interpreter_python": "/usr/bin/python"
    },
    "before": "7d3a191ecb2debe7f84f4fa5817b0f1b49c4cd54",
    "changed": false,
    "remote_url_changed": false
}

PLAY [Update the MOTD on hosts] ************************************************

TASK [Gathering Facts] *********************************************************
ok: [cookbook]

TASK [ansible.motd : Add 99-footer file] ***************************************
skipping: [cookbook]

TASK [ansible.motd : Delete 99-footer file] ************************************
ok: [cookbook]

TASK [ansible.motd : Delete /etc/motd file] ************************************
skipping: [cookbook]

TASK [ansible.motd : Check motd tail supported] ********************************
fatal: [cookbook]: FAILED! => {"changed": true, "cmd": "test -f /etc/update-motd.d/99-footer", "delta": "0:00:00.004444", "end": "2020-04-14 17:26:25.489793", "msg": "non-zero return code", "rc": 1, "start": "2020-04-14 17:26:25.485349", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
...ignoring

TASK [ansible.motd : Add motd tail] ********************************************
skipping: [cookbook]

TASK [ansible.motd : Add motd] *************************************************
changed: [cookbook]

PLAY RECAP *********************************************************************
cookbook : ok=4 changed=2 unreachable=0 failed=0 skipped=3 rescued=0 ignored=1

这个命令可以成为您整体 Ansible 解决方案的一个非常强大的部分,特别是因为这意味着您不必过于担心集中运行所有 playbooks,或者确保每次运行它们时它们都是最新的。在大型基础架构中,将其安排在cron中的能力尤其强大,理想情况下,自动化应该能够自行处理事务。

这结束了我们对 playbooks 的实际观察,以及如何编写自己的代码 - 通过对 Ansible 模块进行一些研究,现在您应该足够轻松地编写自己的强大 playbooks 了。

总结

Playbooks 是 Ansible 自动化的生命线,提供了一个强大的框架,用于定义任务的逻辑集合并清晰而强大地处理错误条件。将角色添加到这个混合中对于组织代码和支持代码重用都是有价值的,尤其是在您的自动化需求增长时。Ansible playbooks 为您的技术需求提供了一个真正完整的自动化解决方案。

在本章中,您学习了 playbook 框架以及如何开始编写自己的 playbooks。然后,您学习了如何将代码组织成角色,并设计代码以有效地支持重用。然后,我们探讨了一些更高级的 playbook 编写主题,如使用条件逻辑、块和循环。最后,我们看了一下 playbook 执行策略,特别是为了能够有效地调试您的 playbooks,最后,我们看了一下如何直接从 GitHub 在本地机器上运行 Ansible playbooks。

在下一章中,我们将学习如何使用和创建我们自己的模块,为您提供扩展 Ansible 功能的技能,以适应自己定制的环境,并为社区做出贡献。

问题

  1. 如何通过临时命令在frontends主机组中重新启动 Apache Web 服务器?

A) ansible frontends -i hosts -a "name=httpd state=restarted"

B) ansible frontends -i hosts -b service -a "name=httpd state=restarted"

C) ansible frontends -i hosts -b -m service -a "name=httpd state=restarted"

D) ansible frontends -i hosts -b -m server -a "name=httpd state=restarted"

E) ansible frontends -i hosts -m restart -a "name=httpd"

  1. Do 块允许您逻辑地组合一组任务,或执行错误处理吗?

A) 正确

B) 错误

  1. 默认策略是通过 playbook 中的相关模块进行设置。

A) 正确

B) 错误

进一步阅读

ansible-galaxy和文档可以在这里找到:galaxy.ansible.com/docs/

第二部分:扩展 Ansible 的功能

在本节中,我们将介绍 Ansible 插件和模块的重要概念。我们将介绍它们的有效使用以及如何通过编写自己的插件和模块来扩展 Ansible 的功能。我们甚至会看一下提交您的模块和插件到官方 Ansible 项目的要求。我们还将介绍编码最佳实践,以及一些高级的 Ansible 技术,使您能够在使用集群环境时安全地自动化您的基础设施。

本节包括以下章节:

  • 第五章,消费和创建模块

  • 第六章,消费和创建插件

  • 第七章,编码最佳实践

  • 第八章,高级 Ansible 主题

第五章:使用和创建模块

在整本书中,我们几乎不断地提到并使用 Ansible 模块。我们把这些模块视为“黑匣子”——也就是说,我们只是接受它们的存在,并且它们将以某种记录的方式工作。然而,关于 Ansible 的许多伟大之处之一是它是一个开源产品,因此,您不仅可以查看和修改其源代码,还可以编写自己的补充。迄今为止,已经有成千上万的模块可用于 Ansible,处理从简单的命令,如复制文件和安装软件包,到配置高度复杂和定制的网络设备。这一大量的模块已经源于使用 Ansible 解决问题的真正需求,每次发布 Ansible 时,包含的模块数量都在增加。

迟早,您会遇到一个特定的功能,它在当前的 Ansible 模块中不存在。当然,您可以尝试填补这个功能上的空白,要么编写自己的模块,要么为现有模块的增强功能做出贡献,以便其他人也能从 Ansible 项目中受益。在本章中,您将学习创建自己模块的基础知识,以及如果愿意,如何将您的代码贡献回上游的 Ansible 项目。

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

  • 使用命令行执行多个模块

  • 审查模块索引

  • 从命令行访问模块文档

  • 模块返回值

  • 开发自定义模块

让我们开始吧!

技术要求

本章假定您已经按照第一章 开始使用 Ansible中详细介绍的方式设置了您的控制主机,并且正在使用最新版本——本章的示例是使用 Ansible 2.9 进行测试的。本章还假定您至少有一个额外的主机进行测试。理想情况下,这应该是基于 Linux 的。尽管本章中将给出主机名的具体示例,但您可以自由地用您自己的主机名和/或 IP 地址替换它们。如何做到这一点的详细信息将在适当的地方提供。

本章涵盖的模块开发工作假定您的计算机上存在 Python 2 或 Python 3 开发环境,并且您正在运行 Linux、FreeBSD 或 macOS。需要额外的 Python 模块时,它们的安装将被记录。构建模块文档的任务在 Python 3.5 或更高版本周围有一些非常具体的要求,因此如果您希望尝试这个任务,您将需要安装一个合适的 Python 环境。

本章的代码包在这里可用:github.com/PacktPublishing/Ansible-2-Cookbook/tree/master/Chapter%205

使用命令行执行多个模块

由于本章主要讨论模块以及如何创建它们,让我们回顾一下如何使用模块。我们在整本书中都做过这个,但我们并没有特别关注它们工作的一些具体细节。我们没有讨论的关键事情之一是 Ansible 引擎如何与其模块进行通信,反之亦然,所以让我们现在来探讨一下。

与以往一样,当使用 Ansible 命令时,我们需要一个清单来运行我们的命令。在本章中,由于我们的重点是模块本身,我们将使用一个非常简单和小的清单,如下所示:

[frontends]
frt01.example.com

[appservers]
app01.example.com

现在,让我们回顾的第一部分,您可以通过一个临时命令轻松运行一个模块,并使用-m开关告诉 Ansible 您想要运行哪个模块。因此,您可以运行的最简单的命令之一是 Ansible 的ping命令,如下所示:

$ ansible -i hosts appservers -m ping 

现在,我们之前没有看过的一件事是 Ansible 和它的模块之间的通信;然而,让我们来检查一下前面命令的输出:

$ ansible -i hosts appservers -m ping
app01.example.com | SUCCESS => {
 "ansible_facts": {
 "discovered_interpreter_python": "/usr/bin/python"
 },
 "changed": false,
 "ping": "pong"
}

你注意到输出的结构了吗 - 大括号、冒号和逗号?是的,Ansible 使用 JSON 格式的数据与它的模块进行通信,模块也将它们的数据以 JSON 格式返回给 Ansible。前面的输出实际上是ping模块通过 JSON 格式的数据结构向 Ansible 引擎返回的一个子集。

当然,我们在使用模块时不必担心这一点,可以在命令行上使用key=value对或在 playbooks 和 roles 中使用 YAML。因此,JSON 对我们来说是屏蔽的,但这是一个重要的事实,当我们在本章后面进入模块开发的世界时要牢记在心。

Ansible 模块就像高级编程语言中的函数一样,它们接受一组明确定义的参数作为输入,执行它们的功能,然后提供一组输出数据,这些数据也是明确定义和有文档记录的。我们稍后会更详细地看一下这一点。当然,前面的命令没有包括任何参数,所以这是通过 Ansible 最简单的模块调用。

现在,让我们运行另一个带有参数的命令,并将数据传递给模块:

$ ansible -i hosts appservers -m command -a "/bin/echo 'hello modules'"

在这种情况下,我们向命令模块提供了一个字符串作为参数,然后 Ansible 将其转换为 JSON 并传递给命令模块。当你运行这个临时命令时,你会看到类似以下的输出:

$  ansible -i hosts appservers -m command -a "/bin/echo 'hello modules'"
app01.example.com | CHANGED | rc=0 >>
hello modules

在这种情况下,输出数据似乎不是 JSON 格式的;然而,当你运行一个模块时,Ansible 打印到终端的内容只是每个模块返回的数据的一个子集 - 例如,我们命令的CHANGED状态和rc=0退出代码都以 JSON 格式的数据结构传递回 Ansible - 这只是对我们隐藏了。

这一点不需要过多强调,但设置一个上下文是很重要的。正是这个上下文将贯穿本章的始终,所以只需记住这些关键点:

  • Ansible 和它的模块之间的通信是通过 JSON 格式的数据结构完成的。

  • 模块接受控制它们功能的输入数据(参数)。

  • 模块总是返回数据 - 至少是模块执行的状态(例如changedokfailed)。

当然,在开始编写自己的模块之前,检查是否已经存在可以执行所有(或部分)所需功能的模块是有意义的。我们将在下一节中探讨这一点。

审查模块索引

正如前面的部分所讨论的,Ansible 提供了成千上万的模块,使得快速轻松地开发 playbooks 并在多个主机上运行它们。然而,当有这么多模块时,你该如何找到合适的模块呢?幸运的是,Ansible 文档提供了一个组织良好、分类清晰的模块列表,你可以查阅以找到你需要的模块 - 可以在这里找到:docs.ansible.com/ansible/latest/modules/modules_by_category.html

假设你想要查看是否有一个原生的 Ansible 模块可以帮助你配置和管理你的亚马逊网络服务 S3 存储桶。这是一个相当明确、明确定义的需求,所以让我们以一种逻辑的方式来处理:

  1. 首先,像之前讨论的那样,在你的网络浏览器中打开分类的模块索引:
https://docs.ansible.com/ansible/latest/modules/modules_by_category.html
  1. 现在,我们知道亚马逊网络服务几乎肯定会出现在Cloud模块类别中,所以让我们在浏览器中打开它。

  2. 在这个页面上仍然列出了数百,甚至数千个模块!所以,让我们在浏览器中使用查找功能(Ctrl + F)来查看s3关键字是否出现在任何地方:

我们很幸运-确实如此,并且页面下方还有几个更多的列表:

现在我们有了一个要使用的模块的简短列表-当然,有几个,所以我们仍然需要弄清楚我们的 playbook 需要哪一个(或哪些)。正如前面的简短描述所示,这将取决于您的预期任务是什么。

  1. 简短的描述应该足以给您一些关于模块是否适合您的需求的线索。一旦您有了想法,您可以单击适当的文档链接查看有关模块以及如何使用它的更多详细信息:

正如您所看到的,每个模块的文档页面都提供了大量的信息,包括更长的描述。如果您向下滚动页面,您将看到可以向模块提供的可能参数列表,一些如何使用它们的实际示例,以及有关模块输出的一些详细信息。还要注意前面截图中的要求部分-一些模块,特别是与云相关的模块,在运行之前需要在 Python 2.6 或更高版本上安装额外的 Python 模块,如果您尝试在没有在 Python 2.6 或更高版本上安装botoboto3botocore模块的情况下从 playbook 运行aws_s3模块,您将只会收到一个错误。

所有模块必须在被接受为 Ansible 项目的一部分之前创建这样的文档,因此,如果您打算提交自己的模块,您必须牢记这一点。这也是 Ansible 流行的原因之一-具有易于维护和有良好文档的标准,它是自动化的完美社区平台。官方的 Ansible 网站并不是您可以获取文档的唯一地方,因为它甚至可以在命令行上使用。我们将在下一节中看看如何通过这种方式检索文档。

从命令行访问模块文档

正如前一节所讨论的,Ansible 项目以其文档为傲,并且使这些文档易于访问是项目本身的重要部分。现在,假设您正在进行 Ansible 任务(在 playbook、角色或甚至是临时命令中),并且您在只能访问您正在工作的机器的 shell 的数据中心环境中。您将如何访问 Ansible 文档?

幸运的是,我们还没有讨论的 Ansible 安装的一部分是ansible-doc工具,它与熟悉的ansibleansible-playbook可执行文件一起作为标准安装。ansible-doc命令包括一个完整(基于文本的)文档库,其中包含您安装的 Ansible 版本附带的所有模块的文档。这意味着您需要的模块信息就在您的指尖,即使您在数据中心中并且没有工作的互联网连接!

以下是一些示例,向您展示如何与ansible-doc工具进行交互:

  • 您可以通过简单地发出以下命令在您的 Ansible 控制机上列出所有有文档的模块:
**$ ansible-doc -l** 

您应该看到一个类似以下的输出:

fortios_router_community_list          Configure community lists in Fortinet's FortiOS ...
azure_rm_devtestlab_info               Get Azure DevTest Lab facts
ecs_taskdefinition                     register a task definition in ecs
avi_alertscriptconfig                  Module for setup of AlertScriptConfig Avi RESTfu...
tower_receive                          Receive assets from Ansible Tower
netapp_e_iscsi_target                  NetApp E-Series manage iSCSI target configuratio...
azure_rm_acs                           Manage an Azure Container Service(ACS) instance
fortios_log_syslogd2_filter            Filters for remote system server in Fortinet's F...
junos_rpc                              Runs an arbitrary RPC over NetConf on an Juniper...
na_elementsw_vlan                      NetApp Element Software Manage VLAN
pn_ospf                                CLI command to add/remove ospf protocol to a vRo...
pn_snmp_vacm                           CLI command to create/modify/delete snmp-vacm
cp_mgmt_service_sctp                   Manages service-sctp objects on Check Point over...
onyx_ospf                              Manage OSPF protocol on Mellanox ONYX network de.

有许多页面的输出,这只是向您展示有多少模块!实际上,您可以计数它们:

$ ansible-doc -l | wc -l
3387

没错- Ansible 2.9.6 附带了 3,387 个模块!

  • 与以前一样,您可以使用您喜欢的 shell 工具来处理索引来搜索特定的模块;例如,您可以使用grep来查找所有与 S3 相关的模块,就像我们在上一节的 Web 浏览器中交互式地做的那样:
$ ansible-doc -l | grep s3
s3_bucket_notification                    Creates, upda...
purefb_s3user                             Create or del...
purefb_s3acc                              Create or del...
aws_s3_cors                               Manage CORS f...
s3_sync                                   Efficiently u...
s3_logging                                Manage loggin...
s3_website                                Configure an ...
s3_bucket                                 Manage S3 buc...
s3_lifecycle                              Manage s3 buc...
aws_s3_bucket_info                        Lists S3 buck...
aws_s3                                    manage object...

  • 现在,我们可以轻松查找我们感兴趣的模块的具体文档。假设我们想了解更多关于aws_s3模块的信息-就像我们在网站上所做的那样,只需运行以下命令:
$ ansible-doc aws_s3

这应该产生一个类似以下的输出:

$ ansible-doc aws_s3 > AWS_S3 (/usr/lib/python2.7/site-packages/ansible/modules/cloud/amazon/aws_s

 This module allows the user to manage S3 buckets and the
 objects within them. Includes support for creating and
 deleting both objects and buckets, retrieving objects as files
 or strings and generating download links. This module has a
 dependency on boto3 and botocore.

 * This module is maintained by The Ansible Core Team
 * note: This module has a corresponding action plugin.

OPTIONS (= is mandatory):

- aws_access_key
 AWS access key id. If not set then the value of the
 AWS_ACCESS_KEY environment variable is used.
 (Aliases: ec2_access_key, access_key)[Default: (null)]
 type: str
....

虽然格式有些不同,ansible-doc告诉我们关于该模块的信息,提供了我们可以传递的所有参数(OPTIONS)的列表,当我们向下滚动时,甚至给出了一些工作示例和可能的返回值。我们将在下一节中探讨返回值的主题,因为它们对于理解非常重要,特别是当我们接近开发自己的模块的主题时。

模块返回值

正如我们在本章前面讨论的那样,Ansible 模块将它们的结果作为结构化数据返回,以 JSON 格式在后台格式化。在前面的例子中,你遇到了这些返回数据,既以退出代码的形式,也在我们使用register关键字来捕获任务结果的 Ansible 变量中。在本节中,我们将探讨如何发现 Ansible 模块的返回值,以便我们以后在 playbook 中使用它们,例如,进行条件处理(见第四章,Playbooks and Roles)。

由于空间有限,当涉及到返回值时,我们将选择可能是最简单的 Ansible 模块之一——ping模块。

话不多说,让我们使用我们在上一节学到的ansible-doc工具,看看它对于这个模块的返回值有什么说:

$ ansible-doc ping

如果你滚动到前面命令的输出底部,你应该会看到类似这样的内容:

$ ansible-doc ping
...

RETURN VALUES:

ping:
 description: value provided with the data parameter
 returned: success
 type: str
 sample: pong

因此,我们可以看到ping模块只会返回一个值,那就是pingdescription告诉我们我们应该期望这个特定的返回值包含什么,而returned字段告诉我们它只会在success时返回(如果它会在其他条件下返回,这些将在这里列出)。type返回值是一个字符串(用str表示),虽然你可以通过提供给ping模块的参数来改变值,但默认返回值(因此sample)是pong

现在,让我们看看实际情况。例如,这些返回值中没有任何内容告诉我们模块是否成功运行以及是否有任何更改;然而,我们知道这些是关于每个模块运行的基本信息。

让我们把一个非常简单的 playbook 放在一起。我们将使用ping模块而不带任何参数运行,使用register关键字捕获返回值,然后使用debug模块将返回值转储到终端上:

---
- name: Simple play to demonstrate a return value
  hosts: localhost

  tasks:
    - name: Perform a simple module based task
      ping:
      register: pingresult

    - name: Display the result
      debug:
        var: pingresult

现在,让我们看看当我们运行这个 playbook 时会发生什么:

$ ansible-playbook retval.yml
[WARNING]: provided hosts list is empty, only localhost is available. Note that
the implicit localhost does not match 'all'

PLAY [Simple play to demonstrate a return value] *******************************

TASK [Gathering Facts] *********************************************************
ok: [localhost]

TASK [Perform a simple module based task] **************************************
ok: [localhost]

TASK [Display the result] ******************************************************
ok: [localhost] => {
 "pingresult": {
 "changed": false,
 "failed": false,
 "ping": "pong"
 }
}

PLAY RECAP *********************************************************************
localhost : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

注意,ping模块确实返回一个名为ping的值,其中包含pong字符串(因为 ping 成功了)。然而,你可以看到实际上有两个额外的返回值,这些在 Ansible 文档中没有列出。这些伴随着每个任务运行,因此是隐式的 - 也就是说,你可以假设它们将是从每个模块返回的数据中的一部分。如果模块运行导致目标主机上的更改,changed返回值将被设置为true,而如果模块运行因某种原因失败,failed返回值将被设置为true

使用debug模块打印模块运行的输出是一个非常有用的技巧,如果你想收集关于模块、它的工作方式以及返回的数据类型的更多信息。在这一点上,我们已经涵盖了几乎所有关于模块工作的基础知识,所以下一节,我们将开始开发我们自己的(简单)模块。

开发自定义模块

现在我们熟悉了模块,如何调用它们,如何解释它们的结果以及如何找到它们的文档,我们可以开始编写我们自己的简单模块。虽然这不包括许多与 Ansible 一起提供的模块的深入和复杂的功能,但希望这将为您提供足够的信息,以便在构建您自己的更复杂的模块时能够自信地继续。

一个重要的要点是,Ansible 是用 Python 编写的,因此它的模块也是用 Python 编写的。因此,您需要用 Python 编写您的模块,并且要开始开发自己的模块,您需要确保已安装 Python 和一些必要的工具。如果您已经在开发机器上运行 Ansible,您可能已经安装了所需的软件包,但如果您从头开始,您需要安装 Python、Python 软件包管理器(pip)和可能一些其他开发软件包。确切的过程会因操作系统而异,但以下是一些示例,供您开始:

  • 在 Fedora 上,您将运行以下命令来安装所需的软件包:
$ sudo dnf install python python-devel
  • 同样,在 CentOS 上,您将运行以下命令来安装所需的软件包:
$ sudo yum install python python-devel
  • 在 Ubuntu 上,您将运行以下命令来安装所需的软件包:
$ sudo apt-get update
$ sudo apt-get install python-pip python-dev build-essential 
  • 如果您正在 macOS 上使用 Homebrew 包管理系统,以下命令将安装您需要的软件包:
$ sudo brew install python

安装所需的软件包后,您需要将 Ansible Git 存储库克隆到本地机器,因为其中有一些有价值的脚本,我们在模块开发过程中将需要。使用以下命令将 Ansible 存储库克隆到开发机器上的当前目录:

$ git clone https://github.com/ansible/ansible.git

最后(尽管是可选的),在虚拟环境(venv)中开发您的 Ansible 模块是一个很好的做法,因为这意味着您需要安装的任何 Python 软件包都在这里,而不是与全局系统 Python 模块一起。以不受控制的方式为整个系统安装模块有时可能会导致兼容性问题,甚至破坏本地工具,因此虽然这不是必需的步骤,但强烈建议这样做。

为您的 Python 模块开发工作创建虚拟环境的确切命令将取决于您正在运行的操作系统以及您使用的 Python 版本。您应该参考您的 Linux 发行版的文档以获取更多信息;但是,以下命令在默认 Python 2.7.5 的 CentOS 7.7 上进行了测试,以在您刚刚从 GitHub 克隆的 Ansible 源代码目录中创建一个名为moduledev的虚拟环境:

$ cd ansible
$  python -m virtualenv moduledev
New python executable in /home/james/ansible/moduledev/bin/python
Installing setuptools, pip, wheel...done.

有了我们的开发环境设置好了,让我们开始编写我们的第一个模块。这个模块将非常简单,因为本书的范围超出了如何编写大量 Python 代码的深入讨论。但是,我们将编写一些可以使用 Python 库中的函数在目标机器上本地复制文件的代码。

显然,这与现有模块功能有很大的重叠,但它将作为一个很好的简洁示例,演示如何编写一个简单的 Python 程序,以便 Ansible 可以使用它作为模块。现在,让我们开始编写我们的第一个模块:

  1. 在您喜欢的编辑器中,创建一个名为(例如)remote_filecopy.py的新文件:
$ vi remote_filecopy.py
  1. 从一个 shebang 开始,表示这个模块应该用 Python 执行:
#!/usr/bin/python
  1. 虽然不是强制性的,但在新模块的头部添加版权信息以及您的详细信息是一个好习惯。通过这样做,任何使用它的人都会了解他们可以使用、修改或重新分发的条款。这里给出的文本仅仅是一个例子;您应该自行调查各种适当的许可证,并确定哪种对您的模块最合适:
# Copyright: (c) 2018, Jesse Keating <jesse.keating@example.org>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
  1. 在版权部分之后立即添加包含metadata_versionstatussupported_by信息的 Ansible 元数据部分也是一个好习惯。请注意,metadata_version字段代表 Ansible 元数据版本(在撰写本文时应为1.1),与您的模块版本或您使用的 Ansible 版本无关。以下代码中建议的值对于刚开始使用是可以的,但如果您的模块被接受到官方的 Ansible 源代码中,它们可能会改变:
ANSIBLE_METADATA = {'metadata_version': '1.1',
                    'status': ['preview'],
                    'supported_by': 'community'}
  1. 记住ansible-doc和 Ansible 文档网站上提供的优秀文档?所有这些都会自动生成,从你添加到这个文件的特殊部分。让我们开始通过向我们的模块添加以下代码来添加:
DOCUMENTATION = '''
---
module: remote_filecopy
version_added: "2.9"
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 (@omgjlk)
'''

特别注意author字典 - 为了通过官方 Ansible 代码库的语法检查,作者的名字应该在括号中附上他们的 GitHub ID。如果不这样做,你的模块仍然可以工作,但它将无法通过我们稍后进行的测试。

注意文档是用 YAML 格式编写的,用三个单引号括起来?列出的字段应该适用于几乎所有模块,但自然地,如果你的模块接受不同的选项,你应该指定这些选项以使其与你的模块匹配。

  1. 文档中的示例也是从这个文件生成的 - 它们在DOCUMENTATION后面有自己特殊的文档部分,并应该提供如何使用你的模块创建任务的实际示例,如下例所示:
EXAMPLES = '''
   # Example from Ansible Playbooks
   - name: backup a config file
     remote_copy:
       source: /etc/herp/derp.conf
       dest: /root/herp-derp.conf.bak
'''
  1. 你的模块返回给 Ansible 的数据也应该在自己的部分中进行文档化。我们的示例模块将返回以下值:
RETURN = '''
source:
  description: source file used for the copy
  returned: success
  type: str
  sample: "/path/to/file.name"
dest:
  description: destination of the copy
  returned: success
  type: str
  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: str
  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: str
  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: str
  sample: "file"
'''
  1. 我们完成文档部分后,应立即导入我们要使用的任何 Python 模块。在这里,我们将包括shutil模块,该模块将用于执行文件复制:
import shutil
  1. 现在我们已经建立了模块头和文档,我们可以开始编写代码了。现在,你可以看到为每个单独的 Ansible 模块编写文档需要付出多少努力!我们的模块应该从定义一个main函数开始,在这个函数中,我们将创建一个AnsibleModule类型的对象,并使用一个argument_spec字典来获取模块调用时的选项:
 def main():
       module = AnsibleModule(
           argument_spec = dict(
               source=dict(required=True, type='str'),
               dest=dict(required=True, type='str')
           ) 
       )
  1. 在这个阶段,我们已经拥有了编写模块功能代码所需的一切 - 甚至包括它被调用时的选项。因此,我们可以使用 Python 的shutil模块来执行本地文件复制,基于提供的参数:
       shutil.copy(module.params['source'],
                   module.params['dest'])
  1. 在这一点上,我们已经执行了我们的模块旨在完成的任务。然而,可以说我们还没有完成 - 我们需要清理地退出模块,并向 Ansible 提供我们的返回值。通常,在这一点上,你会编写一些条件逻辑来检测模块是否成功以及它是否实际上对目标主机进行了更改。然而,为简单起见,我们将简单地每次以changed状态退出 - 扩展这个逻辑并使返回状态更有意义留给你作为练习:
      module.exit_json(changed=True)

module.exit_json方法来自我们之前创建的AnsibleModule - 记住,我们说过重要的是知道数据是如何使用 JSON 来回传递的!

  1. 在我们接近模块代码的结尾时,我们现在必须告诉 Python 它可以从哪里导入AnsibleModule对象。可以通过以下代码行来完成:
   from ansible.module_utils.basic import *
  1. 现在是模块的最后两行代码 - 这是我们告诉模块在启动时应该运行main函数的地方:
   if __name__ == '__main__':
       main()

就是这样 - 通过一系列良好记录的步骤,你可以用 Python 编写自己的 Ansible 模块。下一步当然是测试它,在我们实际在 Ansible 中测试之前,让我们看看是否可以在 shell 中手动运行它。当然,为了让模块认为它是在 Ansible 中运行,我们必须以 JSON 格式生成一些参数。创建一个文件,包含以下内容以提供参数:

{
 "ANSIBLE_MODULE_ARGS": {
 "source": "/tmp/foo",
        "dest": "/tmp/bar"
    }
} 

有了这个小小的 JSON 片段,你可以直接用 Python 执行你的模块。如果你还没有这样做,你需要按照以下方式设置你的 Ansible 开发环境。请注意,我们还手动创建了源文件/tmp/foo,这样我们的模块就可以真正执行文件复制了:

$ touch /tmp/foo
$ . moduledev/bin/activate
(moduledev) $ . hacking/env-setup
running egg_info
creating lib/ansible_base.egg-info
writing requirements to lib/ansible_base.egg-info/requires.txt
writing lib/ansible_base.egg-info/PKG-INFO
writing top-level names to lib/ansible_base.egg-info/top_level.txt
writing dependency_links to lib/ansible_base.egg-info/dependency_links.txt
writing manifest file 'lib/ansible_base.egg-info/SOURCES.txt'
reading manifest file 'lib/ansible_base.egg-info/SOURCES.txt'
reading manifest template 'MANIFEST.in'
warning: no files found matching 'SYMLINK_CACHE.json'
warning: no previously-included files found matching 'docs/docsite/rst_warnings'
warning: no previously-included files matching '*' found under directory 'docs/docsite/_build'
warning: no previously-included files matching '*.pyc' found under directory 'docs/docsite/_extensions'
warning: no previously-included files matching '*.pyo' found under directory 'docs/docsite/_extensions'
warning: no files found matching '*.ps1' under directory 'lib/ansible/modules/windows'
warning: no files found matching '*.psm1' under directory 'test/support'
writing manifest file 'lib/ansible_base.egg-info/SOURCES.txt'

Setting up Ansible to run out of checkout...

PATH=/home/james/ansible/bin:/home/james/ansible/moduledev/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/home/james/bin
PYTHONPATH=/home/james/ansible/lib
MANPATH=/home/james/ansible/docs/man:/usr/local/share/man:/usr/share/man

Remember, you may wish to specify your host file with -i

Done!

现在,你终于可以第一次运行你的模块了。你可以按照以下步骤进行:

(moduledev) $ python remote_filecopy.py args.json
{"invocation": {"module_args": {"dest": "/tmp/bar", "source": "/tmp/foo"}}, "changed": true}

(moduledev) $ ls -l /tmp/bar
-rw-r--r-- 1 root root 0 Apr 16 16:24 /tmp/bar

成功!你的模块有效 - 它既接收又生成 JSON 数据,正如我们在本章前面讨论的那样。当然,你还有很多东西要添加到你的模块 - 我们还没有处理模块的failedok返回,也不支持检查模式。然而,我们已经有了一个良好的开端,如果你想了解更多关于 Ansible 模块和丰富功能的内容,你可以在这里找到更多详细信息:docs.ansible.com/ansible/latest/dev_guide/developing_modules_general.html

当涉及测试你的模块时,创建一个 JSON 文件中的参数并不直观,尽管,正如我们所见,它确实运行良好。幸运的是,我们可以很容易地在 playbook 中运行我们的 Ansible 模块!默认情况下,Ansible 将检查 playbook 目录是否有一个名为library/的子目录,并将从这里运行引用的模块。因此,我们可以创建以下内容:

$ cd ~
$ mkdir testplaybook
$ cd testplaybook
$ mkdir library
$ cp ~/ansible/moduledev/remote_filecopy.py library/

现在,在这个 playbook 目录中创建一个简单的清单文件,就像我们之前做的那样,并添加一个带有以下内容的 playbook:

---
- name: Playbook to test custom module
  hosts: all

  tasks:
    - name: Test the custom module
      remote_filecopy:
        source: /tmp/foo
        dest: /tmp/bar
      register: testresult

    - name: Print the test result data
      debug:
        var: testresult

为了清晰起见,你的最终目录结构应该如下所示:

testplaybook
├── hosts
├── library
│   └── remote_filecopy.py
└── testplaybook.yml

现在,尝试以通常的方式运行 playbook,看看会发生什么:

$ ansible-playbook -i hosts testplaybook.yml

PLAY [Playbook to test custom module] ******************************************

TASK [Gathering Facts] *********************************************************
ok: [frt01.example.com]
ok: [app01.example.com]

TASK [Test the custom module] **************************************************
changed: [app01.example.com]
changed: [frt01.example.com]

TASK [Print the test result data] **********************************************
ok: [app01.example.com] => {
 "testresult": {
 "changed": true,
 "failed": false
 }
}
ok: [frt01.example.com] => {
 "testresult": {
 "changed": true,
 "failed": false
 }
}

PLAY RECAP *********************************************************************
app01.example.com : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt01.example.com : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

成功!你不仅在本地测试了你的 Python 代码,而且还成功地在 Ansible playbook 中的两台远程服务器上运行了它。这真的很容易,这证明了扩展你的 Ansible 模块以满足你自己的定制需求是多么简单。

尽管成功运行了这段代码,但我们还没有检查文档,也没有从 Ansible 中测试它的操作。在我们更详细地解决这些问题之前,在下一节中,我们将看看模块开发的一些常见陷阱以及如何避免它们。

避免常见陷阱

你的模块必须经过深思熟虑,并且要优雅地处理错误条件 - 有一天人们将依赖于你的模块来自动化可能在成千上万台服务器上执行的任务,所以他们最不想做的就是花费大量时间调试错误,尤其是那些本来可以被捕获或优雅处理的琐碎错误。在本节中,我们将具体看看错误处理和如何做到这一点,以便 playbook 仍然可以正常运行并优雅退出。

在我们开始之前,一个总体的指导是,就像文档在 Ansible 中受到高度关注一样,你的错误消息也应该如此。它们应该是有意义的,易于解释,你应该避免无意义的字符串,比如Error!

所以,现在,如果我们删除我们试图复制的源文件,然后用相同的参数重新运行我们的模块,我认为你会同意输出既不漂亮也不有意义,除非你碰巧是一个经验丰富的 Python 开发者:

(moduledev) $ rm -f /tmp/foo
(moduledev) $ python remote_filecopy.py args.json
Traceback (most recent call last):
 File "remote_filecopy.py", line 99, in <module>
 main()
 File "remote_filecopy.py", line 93, in main
 module.params['dest'])
 File "/usr/lib64/python2.7/shutil.py", line 119, in copy
 copyfile(src, dst)
 File "/usr/lib64/python2.7/shutil.py", line 82, in copyfile
 with open(src, 'rb') as fsrc:
IOError: [Errno 2] No such file or directory: '/tmp/foo'

我们毫无疑问可以做得更好。让我们复制一份我们的模块,并向其中添加一些代码。首先,用以下代码替换shutil.copy行:

    try:
       shutil.copy(module.params['source'], module.params['dest'])
    except:
       module.fail_json(msg="Failed to copy file")

这是 Python 中一些非常基本的异常处理,但它允许代码尝试shutil.copy任务。但是,如果这失败并引发了异常,我们不会使用回溯退出,而是使用module.fail_json调用干净地退出。这将告诉 Ansible 模块失败,并干净地发送 JSON 格式的错误消息回去。当然,我们可以做很多事情来改进错误消息;例如,我们可以从shutil模块获取确切的错误消息并将其传递回 Ansible,但是这又留给您来完成。

现在,当我们尝试使用不存在的源文件运行模块时,我们将看到以下清晰格式的 JSON 输出:

(moduledev) $ rm -f /tmp/foo
(moduledev) $ python better_remote_filecopy.py args.json

{"msg": "Failed to copy file", "failed": true, "invocation": {"module_args": {"dest": "/tmp/bar", "source": "/tmp/foo"}}}

然而,如果复制成功,模块仍然以与以前相同的方式工作:

(moduledev) $ touch /tmp/foo
(moduledev) $ python better_remote_filecopy.py args.json

{"invocation": {"module_args": {"dest": "/tmp/bar", "source": "/tmp/foo"}}, "changed": true}

通过对我们的代码进行这个简单的更改,我们现在可以干净而优雅地处理文件复制操作的失败,并向用户报告一些更有意义的内容,而不是使用回溯。在您的模块中进行异常处理和处理的一些建议如下:

  • 快速失败-在出现错误后不要尝试继续处理。

  • 使用各种模块 JSON 返回函数返回最有意义的可能错误消息。

  • 如果有任何方法可以避免返回回溯,请不要返回回溯。

  • 尝试使错误在模块和其功能的上下文中更有意义(例如,对于我们的模块,文件复制错误文件错误更有意义-我认为您很容易想出更好的错误消息)。

  • 不要用错误轰炸用户;相反,尝试专注于报告最有意义的错误,特别是当您的模块代码很复杂时。

这完成了我们对 Ansible 模块中错误处理的简要而实用的介绍。在下一节中,我们将回到我们在模块中包含的文档,包括如何将其构建为 HTML 文档,以便它可以放在 Ansible 网站上(如果您的模块被接受为 Ansible 源代码的一部分,这正是 Web 文档将如何生成)。

测试和记录您的模块

我们已经在本章前面讨论过,已经为我们的模块做了大量的文档工作。但是,我们如何查看它,以及如何检查它是否正确编译为 HTML,如果它被接受为 Ansible 源代码的一部分,它将放在 Ansible 网站上?

在我们实际查看文档之前,我们应该使用一个名为ansible-test的工具,这个工具是在 2.9 版本中新增的。这个工具可以对我们的模块代码进行健全性检查,以确保我们的文档符合 Ansible 项目团队所需的所有标准,并且代码结构正确(例如,Python 的import语句应该始终放在文档块之后)。让我们开始吧:

  1. 要运行健全性测试,假设您已经克隆了官方存储库,请切换到此目录并设置您的环境。请注意,如果您的标准 Python 二进制文件不是 Python 3,ansible-test工具将无法运行,因此您应确保已安装 Python 3,并在必要时设置虚拟环境以确保您正在使用 Python 3。可以按照以下步骤完成:
$ cd ansible$ python 3 -m venv venv
$ . venv/bin/activate
(venv) $ source hacking/env-setup
running egg_info
creating lib/ansible.egg-info
writing lib/ansible.egg-info/PKG-INFO
writing dependency_links to lib/ansible.egg-info/dependency_links.txt
writing requirements to lib/ansible.egg-info/requires.txt
writing top-level names to lib/ansible.egg-info/top_level.txt
writing manifest file 'lib/ansible.egg-info/SOURCES.txt'
reading manifest file 'lib/ansible.egg-info/SOURCES.txt'
reading manifest template 'MANIFEST.in'
warning: no files found matching 'SYMLINK_CACHE.json'
writing manifest file 'lib/ansible.egg-info/SOURCES.txt'

Setting up Ansible to run out of checkout...

PATH=/home/james/ansible/bin:/home/james/ansible/venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/home/james/bin
PYTHONPATH=/home/james/ansible/lib
MANPATH=/home/james/ansible/docs/man:/usr/local/share/man:/usr/share/man

Remember, you may wish to specify your host file with -i

Done!
  1. 接下来,使用pip安装 Python 要求,以便您可以运行ansible-test工具:
(venv) $ pip install -r test/runner/requirements/sanity.txt
  1. 现在,只要您已将模块代码复制到源树中的适当位置(此处显示了一个示例复制命令),您可以按以下方式运行健全性测试:
(venv) $ cp ~/moduledev/remote_filecopy.py ./lib/ansible/modules/files/
(venv) $ ansible-test sanity --test validate-modules remote_filecopy
Sanity check using validate-modules
WARNING: Cannot perform module comparison against the base branch. Base branch not detected when running locally.
WARNING: Reviewing previous 1 warning(s):
WARNING: Cannot perform module comparison against the base branch. Base branch not detected when running locally.

从前面的输出中,您可以看到除了一个警告与我们没有基本分支进行比较之外,我们在本章前面开发的模块代码已经通过了所有测试。如果您对文档有问题(例如,作者名称格式不正确),这将被视为错误。

现在我们已经通过了ansible-test的检查,让我们看看使用ansible-doc命令文档是否正确。这很容易做到。首先,退出你的虚拟环境,如果你还在其中,然后切换到你之前从 GitHub 克隆的 Ansible 源代码目录。现在,你可以手动告诉ansible-doc在哪里查找模块,而不是默认路径。这意味着你可以运行以下命令:

$ cd ~/ansible
$ ansible-doc -M moduledev/ remote_filecopy

你应该看到我们之前创建的文档的文本呈现 - 这里显示了第一页的示例,以便让你了解它应该是什么样子:

> REMOTE_FILECOPY (/home/james/ansible/moduledev/remote_filecopy.py)

 The remote_copy module copies a file on the remote host from a
 given source to a provided destination.

 * This module is maintained by The Ansible Community
OPTIONS (= is mandatory):

= dest
 Path to the destination on the remote host for the copy

= source
 Path to a file on the source file on the remote host

太好了!所以,我们已经可以使用ansible-doc访问我们的模块文档,并确保它在文本模式下呈现正确。但是,我们如何构建 HTML 版本呢?幸运的是,这方面有一个明确定义的过程,我们将在这里概述:

  1. lib/ansible/modules/下,你会发现一系列分类的目录,模块被放置在其中 - 我们的最适合放在files类别下,所以将其复制到这个位置,为即将到来的构建过程做准备:
$ cp moduledev/remote_filecopy.py lib/ansible/modules/files/
  1. 作为文档创建过程的下一步,切换到docs/docsite/目录:
$ cd docs/docsite/
  1. 构建一个基于文档的 Python 文件。使用以下命令来完成:
$ MODULES=hello_module make webdocs

现在,理论上,制作 Ansible 文档应该是这么简单的;然而,不幸的是,在写作时,Ansible v2.9.6 的源代码拒绝构建webdocs。随着时间的推移,这无疑会得到修复,因为在写作时,文档构建脚本正在迁移到 Python 3。为了让make webdocs命令运行,我不得不将 Ansible v2.8.10 的源代码克隆为起点。

即使在这个环境中,在 CentOS 7 上,make webdocs命令也会失败,除非你有一些非常特定的 Python 3 要求。这些要求没有很好地记录,但从测试中,我可以告诉你,Sphinx v2.4.4 可以工作。CentOS 7 提供的版本太旧并且失败,而 Python 模块仓库提供的最新版本(写作时为 v3.0.1)与构建过程不兼容并且失败。

一旦我从 Ansible v2.8.10 源代码树开始工作,我必须确保我已经从我的 Python 3 环境中删除了任何现有的sphinx模块(你需要 Python 3.5 或更高版本才能在本地构建文档 - 如果你的节点上没有安装这个,请在继续之前安装)然后运行以下命令:

$ pip3 uninstall sphinx
$ pip3 install sphinx==2.4.4
$ pip3 install sphinx-notfound-page

有了这个,你就可以成功地运行make webdocs来构建你的文档。你会看到很多输出。一个成功的运行应该以类似于这里显示的输出结束:

generating indices... genindex py-modindexdone
writing additional pages... search/home/james/ansible/docs/docsite/_themes/sphinx_rtd_theme/search.html:21: RemovedInSphinx30Warning: To modify script_files in the theme is deprecated. Please insert a <script> tag directly in your theme instead.
 {% endblock %}
 opensearchdone
copying images... [100%] dev_guide/style_guide/images/thenvsthan.jpg
copying downloadable files... [ 50%] network/getting_started/sample_files/first_copying downloadable files... [100%] network/getting_started/sample_files/first_playbook_ext.yml
copying static files... ... done
copying extra files... done
dumping search index in English (code: en)... done
dumping object inventory... done
build succeeded, 35 warnings.

The HTML pages are in _build/html.
make[1]: Leaving directory `/home/james/ansible/docs/docsite'

现在,请注意,在这个过程结束时,make命令告诉我们在哪里查找编译好的文档。如果你在这里查找,你会找到以下内容:

$ find /home/james/ansible/docs/docsite -name remote_filecopy*
/home/james/ansible/docs/docsite/rst/modules/remote_filecopy_module.rst
/home/james/ansible/docs/docsite/_build/html/modules/remote_filecopy_module.html
/home/james/ansible/docs/docsite/_build/doctrees/modules/remote_filecopy_module.doctree

尝试在你的网页浏览器中打开 HTML 文件 - 你应该看到页面的呈现就像官方 Ansible 项目文档中的一个页面!这使你能够检查你的文档是否构建正确,并且在将要查看的上下文中看起来和读起来都很好。这也让你有信心,当你提交你的代码到 Ansible 项目时(如果你这样做的话),你提交的是符合 Ansible 文档质量标准的东西。

有关在本地构建文档的更多信息,请参阅这里:docs.ansible.com/ansible/latest/community/documentation_contributions.html#building-the-documentation-locally。虽然这是一个很好的文档,但它目前并没有反映出围绕 Sphinx 的兼容性问题,也没有反映出关于 Ansible 2.9 的构建问题。然而,希望它会给你所有其他你需要开始你的文档的指针。

目前构建文档的过程在支持的环境方面有些麻烦;但是,希望这是一些将来会解决的问题。与此同时,本节中概述的过程已经为您提供了一个经过测试和可行的起点。

模块清单

除了我们迄今为止涵盖的指针和良好的实践之外,在您的模块代码中还有一些事项,您应该遵循,以便产生一个被认为是符合 Ansible 潜在包含标准的东西。以下清单并不详尽,但会给您一个关于您作为模块开发人员应该遵循的实践的良好想法:

  • 尽可能多地测试您的模块,无论是在成功的情况下还是在导致错误的情况下。您可以使用 JSON 数据进行测试,就像我们在本章中所做的那样,或者在测试 playbook 中使用它们。

  • 尽量将您的 Python 要求保持在最低限度。有时,可能无法避免需要额外的 Python 依赖(例如 AWS 特定模块的boto要求),但一般来说,您使用的越少越好。

  • 不要为您的模块缓存数据 - Ansible 在不同主机上的执行策略意味着您不太可能从中获得良好的结果。期望在每次运行时收集您需要的所有数据。

  • 模块应该是一个单独的 Python 文件 - 它们不应该分布在多个文件中。

  • 确保在提交模块代码时调查并运行 Ansible 集成测试。有关这些测试的更多信息,请参阅:docs.ansible.com/ansible/latest/dev_guide/testing_integration.html

  • 确保在模块代码的适当位置包含异常处理,就像我们在本章中所做的那样,以防止出现问题。

  • 在 Windows 模块中不要使用PSCustomObjects,除非您绝对无法避免它。

凭借您从本章中获得的信息,您应该有开始创建自己的模块所需的一切。您可能决定不将它们提交到 Ansible 项目,并且确实没有这样的要求。但是,即使您不这样做,遵循本章中概述的实践将确保您构建一个高质量的模块,无论其预期的受众是谁。最后,基于您确实希望将源代码提交到 Ansible 项目的前提,在接下来的部分中,我们将看看如何通过向 Ansible 项目提交拉取请求来实现这一点。

向上游贡献 - 提交 GitHub 拉取请求

当您努力工作在您的模块上并彻底测试和记录它之后,您可能会觉得是时候将其提交到 Ansible 项目以供包含了。这意味着在官方的 Ansible 存储库上创建一个拉取请求。虽然在 GitHub 上的操作细节超出了本书的范围,但我们将为您提供一个基本程序的实际焦点概述。

遵循此处概述的过程将在 GitHub 上为 Ansible 项目生成一个真实的请求,以便您提交的代码可以与他们的代码合并。除非您真的有一个准备提交到 Ansible 代码库的新模块,否则不要遵循此过程。

要将您的模块作为 Ansible 存储库的拉取请求提交,您需要 fork 官方 Ansible 存储库的devel分支。要做到这一点,请从您的 Web 浏览器登录到您的 GitHub 帐户(或者如果您还没有帐户,则创建一个帐户),然后导航到以下截图中显示的 URL。点击右上角的 Fork。作为提醒,官方 Ansible 源代码存储库的 URL 是github.com/ansible/ansible.git

现在您已经将存储库分叉到您自己的账户,我们将演示您需要运行的命令,以将您的模块代码添加到其中。然后,我们将向您展示如何创建所需的拉取请求(也称为PRs),以便您可以将您的新模块与上游的 Ansible 项目合并:

  1. 克隆您刚刚分叉到本地机器的devel分支。使用类似以下的命令,但确保用与您自己 GitHub 账户匹配的 URL 替换它:
$ git clone https://github.com/danieloh30/ansible.git
  1. 将您的模块代码复制到适当的模块目录中-以下代码中给出的copy命令只是一个示例,让您知道该怎么做,但实际上,您应该选择适当的类别子目录来放置您的模块,因为它不一定适合files类别。添加完 Python 文件后,执行git add使 Git 知道新文件,然后用有意义的提交消息提交它。一些示例命令如下:
$ cd ansible
$ cp ~/ansible-development/moduledev/remote_filecopy.py ./lib/ansible/modules/files/
$ git add lib/ansible/modules/files/remote_filecopy.py
$ git commit -m 'Added tested version of remote_filecopy.py for pull request creation'
  1. 现在,确保使用以下命令将代码推送到您分叉的存储库:
$ git push
  1. 返回到 GitHub 网页浏览器,并导航到拉取请求页面,如下所示。点击“New pull request”按钮:

按照 GitHub 网站的指导,跟随拉取请求创建过程。一旦您成功提交了拉取请求,您应该能够导航到官方 Ansible 源代码存储库的拉取请求列表,并在那里找到您的拉取请求。拉取请求列表的示例如下:

当截图被拍摄时,几乎有 31,000 个关闭的拉取请求和将近 1,700 个待审核的!当您阅读本书时,肯定会有更多,这表明 Ansible 在持续发展和增长中非常依赖开源社区。想一想-你也可以成为其中的一部分!如果您的拉取请求被审查需要很长时间,不要惊慌-这只是因为有很多拉取请求需要审查和处理。您可以像我们之前演示的那样,将模块代码添加到本地的library/目录中,以便您的拉取请求被处理的速度不会妨碍您使用 Ansible 的工作。有关在本地工作时放置插件代码的更多详细信息可以在这里找到:docs.ansible.com/ansible/latest/dev_guide/developing_locally.html

除了为自定义模块创建拉取请求之外,还有许多其他贡献到 Ansible 项目的方式。以下是一些其他贡献项目的示例:

  • 审查 Ansible 文档并报告您发现的任何错误(在第四章的创建中已经提交了一个)

  • 创建一个本地的 Ansible MeetUp 来传播关于 Ansible 的知识。如果你的地区已经有了这样的聚会,考虑定期参加。

  • 通过社交媒体传播关于 Ansible 的知识和意识,使用适当的账户引用和标签;例如,@ansible#ansible等。

这完成了我们学习如何创建模块的旅程,从最初研究模块操作的理论步骤,一直到将您的新模块代码贡献给 GitHub 上官方的 Ansible 项目。我们希望您发现这段旅程有益和有价值,并且增强了您使用 Ansible 并在需要时扩展其功能的能力。

总结

模块是 Ansible 的生命线——没有它们,Ansible 无法在各种系统上执行如此复杂和多样的任务。由于是开源项目,通过一点 Python 知识,您可以轻松扩展 Ansible 的功能。在本章中,我们探讨了如何从头开始编写自定义模块。截至目前,Ansible 非常丰富多功能,但这种易于定制和扩展的特性使得 Ansible 在潜力方面几乎没有限制,尤其是考虑到 Python 作为一种编程语言的强大和流行。

在本章中,我们从回顾如何使用命令行执行多个模块开始。然后,我们探讨了询问当前模块索引的过程,以及如何获取模块文档来评估其是否适合我们的需求,无论我们是否有活动的互联网连接。然后,我们探讨了模块数据及其 JSON 格式,最后通过一个简单的自定义模块的编码过程,带您进行了一次旅程。这为您提供了在未来创建自己的模块的基础,如果您愿意的话。

在下一章中,我们将探讨使用和创建另一个核心的 Ansible 功能,即插件的过程。

发现插件类型

Ansible 的代码一直被设计为模块化的——这是它的核心优势之一。无论是通过使用模块执行任务还是通过插件(我们将很快看到),Ansible 的模块化设计使其能够像本书中展示的那样多才多艺和强大。与模块一样,Ansible 插件都是用 Python 编写的,并且期望以一定的格式摄取和返回数据(稍后会详细介绍)。Ansible 的插件在功能上通常是不可见的,因为您在命令或 playbook 中很少直接调用它们,但它们负责提供 Ansible 提供的一些最重要的功能,包括 SSH 连接、解析清单文件(INI 格式、YAML 或其他格式)以及在数据上运行jinja2过滤器的能力。

像往常一样,在继续之前,让我们验证一下您的测试机上是否安装了合适的 Ansible 版本:

$  ansible-doc --version
ansible-doc 2.9.6
 config file = /etc/ansible/ansible.cfg
 configured module search path = [u'/root/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
 ansible python module location = /usr/lib/python2.7/site-packages/ansible
 executable location = /usr/bin/ansible-doc
 python version = 2.7.5 (default, Aug 7 2019, 00:51:29) [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)]

插件的文档工作与模块的文档工作一样多,您会很高兴地知道,有一个插件索引可在docs.ansible.com/ansible/latest/plugins/plugins.html上找到。

您也可以像之前一样使用ansible-doc命令,只是您需要添加-t开关。插件总是放在适当的类别中,因为它们在类别之间的功能差异很大。如果您没有使用-t开关,您最终会指定ansible-doc -t模块,它会返回可用模块的列表。

截至目前,Ansible 中可以找到以下插件类别:

  • become: 负责使 Ansible 能够获得超级用户访问权限(例如,通过sudo

  • cache: 负责缓存从后端系统检索的事实,以提高自动化性能

  • callback: 允许您在响应事件时添加新的行为,例如更改 Ansible playbook 运行输出中数据的格式

  • cliconf: 提供了对各种网络设备命令行界面的抽象,为 Ansible 提供了一个标准的操作接口

  • connection: 提供了从 Ansible 到远程系统的连接(例如,通过 SSH、WinRM、Docker 等)

  • httpapi: 告诉 Ansible 如何与远程系统的 API 交互(例如,用于 Fortinet 防火墙)

  • inventory: 提供了解析各种静态和动态清单格式的能力

  • lookup:允许 Ansible 从外部来源查找数据(例如,通过读取一个平面文本文件)

  • netconf:为 Ansible 提供抽象,使其能够与启用 NETCONF 的网络设备一起工作

  • shell:提供 Ansible 在不同系统上使用各种 shell 的能力(例如,在 Windows 上使用powershell,在 Linux 上使用sh

  • strategy:为 Ansible 提供不同的执行策略插件(例如,我们在第四章中看到的调试策略,Playbooks and Roles

  • vars:提供 Ansible 从某些来源获取变量的能力,例如我们在第三章中探讨的host_varsgroup_vars目录,定义您的清单

我们将把在 Ansible 网站上探索插件文档作为您完成的练习留给您。但是,如果您想使用ansible-doc工具来探索各种插件,您需要运行以下命令:

  1. 要使用ansible-doc命令列出给定类别中可用的所有插件,可以运行以下命令:
$ ansible-doc -t connection -l

这将返回连接插件的文本索引,类似于我们在查看模块文档时看到的内容。索引输出的前几行如下所示:

kubectl           Execute tasks in pods running on Kubernetes
napalm            Provides persistent connection using NAPALM
qubes             Interact with an existing QubesOS AppVM
libvirt_lxc       Run tasks in lxc containers via libvirt
funcd             Use funcd to connect to target
chroot            Interact with local chroot
psrp              Run tasks over Microsoft PowerShell Remoting Protocol
zone              Run tasks in a zone instance
winrm             Run tasks over Microsoft's WinRM
paramiko_ssh      Run tasks via python ssh (paramiko)
  1. 然后,您可以探索给定插件的文档。例如,如果我们想了解paramiko_ssh插件,我们可以发出以下命令:
**$ ansible-doc -t connection paramiko_ssh**

您会发现插件文档采用非常熟悉的格式,与我们在第五章中看到的模块的格式类似。

> PARAMIKO (/usr/lib/python2.7/site-packages/ansible/plugins/connection/param

 Use the python ssh implementation (Paramiko) to connect to
 targets The paramiko transport is provided because many
 distributions, in particular EL6 and before do not support
 ControlPersist in their SSH implementations. This is needed on
 the Ansible control machine to be reasonably efficient with
 connections. Thus paramiko is faster for most users on these
 platforms. Users with ControlPersist capability can consider
 using -c ssh or configuring the transport in the configuration
 file. This plugin also borrows a lot of settings from the ssh
 plugin as they both cover the same protocol.

 * This module is maintained by The Ansible Community
OPTIONS (= is mandatory):

- host_key_auto_add
 TODO: write it
 [Default: (null)]
 set_via:
 env:
 - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
 ini:
 - key: host_key_auto_add

由于 Ansible 各个领域的所有工作和努力,您可以轻松地了解包含在 Ansible 中的插件以及如何使用它们。到目前为止,我们已经看到,插件的文档与模块的文档一样完整。在本章的下一节中,我们将更深入地了解如何找到与您的 Ansible 发行版配套的插件代码。

问题

  1. 哪个命令行可以作为参数传递给模块?

A) ansible dbservers -m command "/bin/echo 'hello modules'"

B) ansible dbservers -m command -d "/bin/echo 'hello modules'"

C) ansible dbservers -z command -a "/bin/echo 'hello modules'"

D) ansible dbservers -m command -a "/bin/echo 'hello modules'"

E) ansible dbservers -a "/bin/echo 'hello modules'"

  1. 在创建自定义模块并处理异常时,以下哪种做法是不推荐的?

A) 设计一个简单的自定义模块,如果可以避免的话,不要向用户提供回溯。

B) 快速失败您的模块代码,并验证您是否提供了有用和可理解的异常消息。

C) 仅显示与最相关的异常相关的错误消息,而不是所有可能的错误。

D) 确保您的模块文档是相关的并且易于理解。

E) 删除导致错误的 playbook,然后从头开始重新创建它们。

  1. 正确或错误:要为 Ansible 上游项目做出贡献,您需要将代码提交到devel分支。

A) True

B) False

进一步阅读

第六章:使用和创建插件

到目前为止,模块一直是我们在 Ansible 中旅程中非常明显和关键的一部分。它们用于执行明确定义的任务,可以用于一次性命令(使用临时命令)或作为更大的 playbook 的一部分。插件对于 Ansible 同样重要,迄今为止我们一直在使用它们,甚至没有意识到!虽然模块始终用于在 Ansible 中创建某种任务,但插件的使用方式取决于它们的用例。有许多不同类型的插件;我们将在本章中向您介绍它们,并让您了解它们的目的。但是,作为一个引子,您是否意识到当 Ansible 使用 SSH 连接到远程服务器时,连接插件提供了功能?这展示了插件发挥的重要作用。

在本章中,我们将为您提供对插件的深入介绍,并向您展示如何探索 Ansible 附带的各种插件。然后,我们将扩展这一点,演示如何创建自己的插件并在 Ansible 项目中使用它们,这与我们在上一章中使用自定义模块的方式非常相似。这将有助于您理解诸如 Ansible 等开源软件提供的无限可能性。

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

  • 发现插件类型

  • 查找包含的插件

  • 创建自定义插件

技术要求

本章假设您已经按照第一章中详细介绍的方式设置了 Ansible 的控制主机,并且您正在使用最新版本。本章中的示例是使用 Ansible 2.9 进行测试的。本章还假设您至少有一个额外的主机进行测试;最好是基于 Linux 的主机。

尽管本章将给出主机名的具体示例,但您可以自由地用您自己的主机名和/或 IP 地址替换它们,如何做到这一点的详细信息将在适当的位置提供。本章涵盖的插件开发工作假设您的计算机上有 Python 2 或 Python 3 开发环境,并且您正在运行 Linux、FreeBSD 或 macOS。需要额外的 Python 模块时,它们的安装将有文档记录。构建模块文档的任务在 Python 3.5 或更高版本中有一些非常具体的要求,因此假设您可以安装一个合适的 Python 环境,如果您希望尝试这样做。

本章的代码包可以在github.com/PacktPublishing/Ansible-2-Cookbook/tree/master/Chapter%206上找到。

查找包含的插件

正如我们在前一节中讨论的,插件在 Ansible 中并不像它们的模块对应物那样明显,然而迄今为止我们在每个单个 Ansible 命令中都在幕后使用它们!让我们在前一节的工作基础上继续,我们查看了插件文档,看看我们可以在哪里找到插件的源代码。这反过来将作为我们自己构建一个简单插件的前提。

如果您在 Linux 系统上使用软件包管理器(即通过 RPM 或 DEB 软件包)安装了 Ansible,则您的插件位置将取决于您的操作系统。例如,在我安装了来自官方 RPM 软件包的 Ansible 的测试 CentOS 7 系统上,我可以看到安装的插件在这里:

$ ls /usr/lib/python2.7/site-packages/ansible/plugins/
action    cliconf       httpapi        inventory    lookup     terminal
become    connection    __init__.py    loader.py    netconf    test
cache     doc_fragments __init__.pyc   loader.pyc   shell      vars
callback  filter        __init__.pyo   loader.pyo   strategy

注意插件是如何分成子目录的,所有子目录都以它们的类别命名。如果我们想查找我们在前一节中审查过文档的paramiko_ssh插件,我们可以在connection/子目录中查找:

$ ls -l /usr/lib/python2.7/site-packages/ansible/plugins/connection/paramiko_ssh.py
-rw-r--r-- 1 root root 23544 Mar 5 05:39 /usr/lib/python2.7/site-packages/ansible/plugins/connection/paramiko_ssh.py

但是,总的来说,我不建议您编辑或更改从软件包安装的文件,因为在升级软件包时很容易覆盖它们。由于本章的目标之一是编写我们自己的简单自定义插件,让我们看看如何在官方 Ansible 源代码中找到插件:

  1. 从 GitHub 克隆官方 Ansible 存储库,就像我们之前做的那样,并将目录更改为克隆的位置:
$ git clone https://github.com/ansible/ansible.git
$ cd ansible
  1. 在官方源代码目录结构中,您会发现所有插件都包含在lib/ansible/plugins/下(同样,以分类的子目录形式):
$ cd lib/ansible/plugins
  1. 我们可以通过查看connection目录来探索基于连接的插件:
$ ls -al connection/

此目录的确切内容将取决于您克隆的 Ansible 源代码的版本。在撰写本文时,它看起来如下,每个插件都有一个 Python 文件(类似于我们在第五章中看到的每个模块都有一个 Python 文件):

$ ls -al connection/
total 176
drwxr-xr-x 2 root root 109 Apr 15 17:24 .
drwxr-xr-x 19 root root 297 Apr 15 17:24 ..
-rw-r--r-- 1 root root 16411 Apr 15 17:24 __init__.py
-rw-r--r-- 1 root root 6855 Apr 15 17:24 local.py
-rw-r--r-- 1 root root 23525 Apr 15 17:24 paramiko_ssh.py
-rw-r--r-- 1 root root 32839 Apr 15 17:24 psrp.py
-rw-r--r-- 1 root root 55367 Apr 15 17:24 ssh.py
-rw-r--r-- 1 root root 31277 Apr 15 17:24 winrm.py
  1. 您可以查看每个插件的内容,以了解它们的工作原理,这也是开源软件的美妙之处的一部分:
$ less connection/paramiko_ssh.py

以下代码块显示了此文件开头的示例,以便让您了解如果此命令运行正确,您应该看到的输出类型:

# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
# (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

DOCUMENTATION = """
 author: Ansible Core Team
 connection: paramiko
 short_description: Run tasks via python ssh (paramiko)
 description:
 - Use the python ssh implementation (Paramiko) to connect to targets
 - The paramiko transport is provided because many distributions, in particular EL6 and before do not support ControlPersist
 in their SSH implementations.
....

请注意DOCUMENTATION块,它与我们在处理模块源代码时看到的非常相似。如果您探索每个插件的源代码,您会发现其结构与模块代码结构有些相似。但是,下一节,让我们开始构建我们自己的自定义插件,通过一个实际的例子来学习它们是如何组合在一起的,而不是简单地接受这种说法。

创建自定义插件

在本节中,我们将带您完成创建自己插件的实际指南。这个例子必然会很简单。但是,希望它能很好地指导您了解插件开发的原则和最佳实践,并为您构建自己更复杂的插件奠定坚实的基础。我们甚至会向您展示如何将这些与您自己的 playbooks 集成,并在准备就绪时将它们提交给官方 Ansible 项目以供包含。

正如我们在构建自己的模块时所指出的,Ansible 是用 Python 编写的,它的插件也不例外。因此,您需要用 Python 编写您的插件;因此,要开始开发自己的插件,您需要确保已安装 Python 和一些基本工具。如果您的开发机器上已经运行了 Ansible,您可能已经安装了所需的软件包。但是,如果您从头开始,您需要安装 Python、Python 软件包管理器(pip)和可能一些其他开发软件包。具体的过程在不同的操作系统之间会有很大的不同,但是这里有一些示例供您参考:

  • 在 Fedora 上,您可以运行以下命令来安装所需的软件包:
$ sudo dnf install python python-devel
  • 同样,在 CentOS 上,您可以运行以下命令来安装所需的软件包:
$ sudo yum install python python-devel
  • 在 Ubuntu 上,您可以运行以下命令来安装您需要的软件包:
$ sudo apt-get update
$ sudo apt-get install python-pip python-dev build-essential 
  • 如果您正在使用 Homebrew 包装系统的 macOS,以下命令将安装您需要的软件包:
$ sudo brew install python

安装所需的软件包后,您需要将 Ansible Git 存储库克隆到本地计算机,因为其中有一些有价值的脚本,我们在模块开发过程中将需要。使用以下命令将 Ansible 存储库克隆到开发机器上的当前目录:

$ git clone https://github.com/ansible/ansible.git
$ cd ansible

有了所有这些先决条件,让我们开始创建您自己的插件。虽然编写模块和插件之间有许多相似之处,但也有根本的不同之处。实际上,Ansible 可以使用的不同类型的插件实际上是稍微不同编码的,并且有不同的建议。遗憾的是,我们在本书中没有空间来逐一介绍每一种插件,但您可以从官方 Ansible 文档中了解每种插件类型的要求。

对于我们的简单示例,我们将创建一个过滤器插件,用另一个字符串替换给定的字符串。如果您参考前面的文档链接,过滤器插件可能是一些最容易编码的插件,因为与模块一样,对文档没有严格的要求。但是,如果我们要创建一个lookup插件,我们将期望创建与我们在第五章中创建的DOCUMENTATIONEXAMPLESRETURN文档部分相同的文档。我们还需要以相同的方式测试和构建我们的 web 文档。

我们已经涵盖了这一点,因此在本章中不需要重复整个过程。相反,我们将首先专注于创建一个过滤器插件。与其他 Ansible 插件和模块不同,您实际上可以在单个 Python 插件文件中定义多个过滤器。过滤器本质上是相当紧凑的代码。它们也是众多的,因此每个过滤器一个文件的方式不太适用。但是,如果您想编写其他类型的插件(如lookup插件),您将需要为每个插件创建一个 Python 文件。

让我们开始创建我们的简单过滤器插件。由于我们只创建一个,它将存在于自己的单独的 Python 文件中。如果您想将代码提交回 Ansible 项目,可以提出修改 Ansible 核心过滤器 Python 文件的建议;但现在,我们将把这个作为一个项目留给您自己完成。我们的过滤器文件将被称为custom_filter.py,它将存在于一个名为filter_plugins的目录中,该目录必须与您的 playbook 目录位于同一目录中。

执行以下步骤来创建和测试您的插件代码:

  1. 在插件文件中以标题开始,以便人们知道谁编写了插件以及它发布的许可证。当然,您应该更新版权和许可字段,以适合您的插件的值,但以下文本作为一个示例供您开始使用:
# (c) 2020, James Freeman <james.freeman@example.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
  1. 接下来,我们将添加一个非常简单的 Python 函数——您的函数可以像您想要的那样复杂,但对于我们来说,我们将简单地使用 Python 的.replace函数来替换string变量中的一个字符串为另一个字符串。以下示例查找Puppet的实例,并将其替换为Ansible
def improve_automation(a):
 return a.replace("Puppet", "Ansible")
  1. 接下来,我们需要创建FilterModule类的对象,这是 Ansible 将知道这个 Python 文件包含一个过滤器的方法。在这个对象中,我们可以创建一个filters定义,并将我们之前定义的过滤器函数的值返回给 Ansible:
class FilterModule(object):
       '''improve_automation filters'''
       def filters(self):
           return {'improve_automation': improve_automation}
  1. 正如您所看到的,这段代码非常简单,我们可以使用内置的 Python 函数,比如replace,来操作字符串。在 Ansible 中没有特定的插件测试工具,因此我们将通过编写一个简单的 playbook 来测试我们的插件代码。以下 playbook 代码定义了一个包含单词Puppet的简单字符串,并使用debug模块将其打印到控制台,应用我们新定义的过滤器到字符串:
---
- name: Play to demonstrate our custom filter
  hosts: frontends
  gather_facts: false
  vars:
    statement: "Puppet is an excellent automation tool!"

  tasks:
    - name: make a statement
      debug:
        msg: "{{ statement | improve_automation }}"

现在,在我们尝试运行之前,让我们回顾一下目录结构应该是什么样子的。就像我们能够利用我们在第五章中创建的自定义模块一样,通过创建一个library/子目录来存放我们的模块,我们也可以为我们的插件创建一个filter_plugins/子目录。当你完成了前面代码块中各个文件的编码细节后,你的目录树结构应该是这样的:

.
├── filter_plugins
│   ├── custom_filter.py
├── hosts
├── myplugin.yml

现在让我们运行一下我们的小测试 playbook,看看我们得到了什么输出。如果一切顺利,它应该看起来像下面这样:

$ ansible-playbook -i hosts myplugin.yml

PLAY [Play to demonstrate our custom filter] ***********************************

TASK [make a statement] ********************************************************
ok: [frt01.example.com] => {
 "msg": "Ansible is an excellent automation tool!"
}

PLAY RECAP *********************************************************************
frt01.example.com : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

正如你所看到的,我们的新过滤器插件将我们变量的内容中的Puppet字符串替换为Ansible字符串。当然,这只是一个愚蠢的测试,不太可能被贡献回 Ansible 项目。然而,它展示了如何在只有六行代码和一点点 Python 知识的情况下,我们创建了自己的过滤器插件来操作一个字符串。我相信你可以想出更复杂和有用的东西!

其他插件类型需要比这更多的工作;虽然我们不会在这里详细介绍创建过滤器插件的过程,但你会发现编写过滤器插件更类似于编写模块,因为你需要做以下工作:

  • 包括DOCUMENTATIONEXAMPLESRETURN部分的适当文档。

  • 确保你在插件中加入了适当和充分的错误处理。

  • 彻底测试它,包括失败和成功的情况。

举个例子,让我们重复前面的过程,但是创建一个lookup插件。这个插件将基于一个简化版本的lookup插件文件。然而,我们希望调整我们的版本,只返回文件的第一个字符。你可以根据需要调整这个示例,也许从文件中读取头文件,或者你可以添加参数到插件中,允许你使用字符索引提取子字符串。我们将把这个增强活动留给你自己去完成。让我们开始吧!我们的新 lookup 插件将被称为firstchar,而lookup插件与它们的 Python 文件是一对一的映射,插件文件将被称为firstchar.py。(事实上,Ansible 将使用这个文件名作为插件的名称——你在代码中找不到对它的引用!)。如果你打算像之前执行的那样从 playbook 中测试这个插件,你应该在一个名为lookup_plugins/的目录中创建它:

  1. 首先,像之前一样在插件文件中添加一个头部,以便维护者和版权细节清晰可见。我们从原始的file.py lookup插件代码中借用了大部分内容,因此我们需要包含相关的来源信息:
# (c) 2020, James Freeman <james.freeman@example.com>
# (c) 2012, Daniel Hokka Zakrisson <daniel@hozac.com>
# (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
  1. 接下来,添加 Python 3 的头文件——如果你打算通过Pull RequestPR)提交你的插件到 Ansible 项目,这是绝对必需的。
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
  1. 接下来,在你的插件中添加一个DOCUMENTATION块,以便其他用户能够理解如何与它交互:
DOCUMENTATION = """
    lookup: firstchar
    author: James Freeman <james.freeman@example.com>
    version_added: "2.9"
    short_description: read the first character of file contents
    description:
        - This lookup returns the first character of the contents from a file on the Ansible controller's file system.
    options:
      _terms:
        description: path(s) of files to read
        required: True
    notes:
      - if read in variable context, the file can be interpreted as YAML if the content is valid to the parser.
      - this lookup does not understand 'globing', use the fileglob lookup instead.
"""
  1. 添加相关的EXAMPLES块,展示如何使用你的插件,就像我们为模块做的那样:
EXAMPLES = """
- debug: msg="the first character in foo.txt is {{lookup('firstchar', '/etc/foo.txt') }}"

"""
  1. 还要确保你记录了插件的RETURN值:
RETURN = """
  _raw:
    description:
      - first character of content of file(s)
"""
  1. 文档完成后,我们现在可以开始编写我们的 Python 代码了。我们将首先导入所有需要使我们的模块工作的 Python 模块。我们还将设置display对象,它用于详细输出和调试。如果你需要显示debug输出,应该在插件代码中使用这个对象,而不是print语句:
from ansible.errors import AnsibleError, AnsibleParserError
from ansible.plugins.lookup import LookupBase
from ansible.utils.display import Display

display = Display()
  1. 我们现在将创建一个LookupModule类的对象。在其中定义一个名为run的默认函数(这是 Ansible lookup插件框架所期望的),并初始化一个空数组作为我们的返回数据:
class LookupModule(LookupBase):

    def run(self, terms, variables=None, **kwargs):

        ret = []
  1. 有了这个,我们将开始一个循环,遍历每个术语(在我们的简单插件中,这将是传递给插件的文件名)。虽然我们只会在简单的用例上测试这个,但查找插件的使用方式意味着它们需要支持操作的terms列表。在这个循环中,我们显示有价值的调试信息,并且最重要的是,定义一个包含我们将要打开的每个文件的详细信息的对象,称为lookupfile
      for term in terms:
            display.debug("File lookup term: %s" % term)

   lookupfile = self.find_file_in_search_path(variables, 'files', term)

      display.vvvv(u"File lookup using %s as file" % lookupfile)
  1. 现在,我们将读取文件内容。这可能只需要一行 Python 代码,但我们从第五章中对模块的工作中知道,我们不应该认为我们会得到一个实际可以读取的文件。因此,我们将把读取文件内容的语句放入一个try块中,并实现异常处理,以确保插件的行为是合理的,即使在错误情况下,也能传递易于理解的错误消息给用户,而不是 Python 的回溯信息:
            try:
                if lookupfile:
               contents, show_data = self._loader._get_file_contents(lookupfile)
                    ret.append(contents.rstrip()[0])
                else:
                    raise AnsibleParserError()
            except AnsibleParserError:
                raise AnsibleError("could not locate file in lookup: %s" % term)

请注意,在其中,我们将文件内容的第一个字符(用[0]索引表示)附加到我们的空数组中。我们还使用rstrip删除任何尾随空格。

  1. 最后,我们使用return语句将从文件中收集到的字符返回给 Ansible:
        return ret
  1. 再次,我们可以创建一个简单的测试 playbook 来测试我们新创建的插件:
---
- name: Play to demonstrate our custom lookup plugin
  hosts: frontends
  gather_facts: false

  tasks:
    - name: make a statement
      debug:
        msg: "{{ lookup('firstchar', 'testdoc.txt')}}"

同样,我们使用 debug 模块将输出打印到控制台,并引用我们的lookup插件来获取输出。

  1. 创建前面代码块中提到的文本文件,名为testdoc.txt。它可以包含任何你喜欢的内容——我的包含以下简单文本:
Hello

为了清晰起见,你的最终目录结构应该如下所示:

.
├── hosts
├── lookup_plugins
│   └── firstchar.py
├── myplugin2.yml
└── testdoc.txt
  1. 现在,当我们运行我们的新 playbook 时,我们应该看到类似以下的输出:
$ ansible-playbook -i hosts myplugin2.yml

PLAY [Play to demonstrate our custom lookup plugin] ****************************

TASK [make a statement] ********************************************************
ok: [frt01.example.com] => {
 "msg": "H"
}

PLAY RECAP *********************************************************************
frt01.example.com : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

如果一切顺利,你的 playbook 应该返回你创建的文本文件的第一个字符。当然,我们可以做很多事情来增强这段代码,但这是一个很好的简单示例,可以让你开始。

有了这个基础,你现在应该对如何开始编写自己的 Ansible 插件有一个合理的想法。我们下一个逻辑步骤是更深入地了解如何测试我们新编写的插件,我们将在下一节中进行。

学习将自定义插件与 Ansible 源代码集成

到目前为止,我们只是以独立的方式测试了我们的插件。这一切都很好,但如果你真的想要将它添加到你自己的 Ansible 源代码分支,或者更好的是,提交给 Ansible 项目以便包含在 PR 中,那该怎么办呢?幸运的是,这个过程与我们在第五章中介绍的非常相似,只是文件夹结构略有不同。

与以前一样,你的第一个任务将是获取官方 Ansible 项目源代码的副本——例如,通过将 GitHub 存储库克隆到你的本地机器上:

$ git clone https://github.com/ansible/ansible.git
$ cd ansible

接下来,你需要将你的插件代码复制到一个适当的插件目录中。

  1. 例如,我们的示例过滤器将被复制到你刚刚克隆的源代码中的以下目录中:
$ cp ~/custom_filter.py ./lib/ansible/plugins/filter/
  1. 类似地,我们的自定义lookup插件将放在lookup插件的目录中,使用如下命令:
$ cp ~/firstchar.py ./lib/ansible/plugins/lookup/

将代码复制到位后,你需要像以前一样测试文档(即你的插件是否包含它)。你可以像我们在第五章中那样构建webdocs文档,所以我们不会在这里重复。不过,作为一个提醒,我们可以快速检查文档是否正确渲染,使用ansible-doc命令,如下所示:

$ . hacking/env-setup
running egg_info
creating lib/ansible.egg-info
writing requirements to lib/ansible.egg-info/requires.txt
writing lib/ansible.egg-info/PKG-INFO
writing top-level names to lib/ansible.egg-info/top_level.txt
writing dependency_links to lib/ansible.egg-info/dependency_links.txt
writing manifest file 'lib/ansible.egg-info/SOURCES.txt'
reading manifest file 'lib/ansible.egg-info/SOURCES.txt'
reading manifest template 'MANIFEST.in'
warning: no files found matching 'SYMLINK_CACHE.json'
writing manifest file 'lib/ansible.egg-info/SOURCES.txt'

Setting up Ansible to run out of checkout...

PATH=/home/james/ansible/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
PYTHONPATH=/home/james/ansible/lib
MANPATH=/home/james/ansible/docs/man:/usr/local/share/man:/usr/share/man

Remember, you may wish to specify your host file with -i

Done!

$ ansible-doc -t lookup firstchar
> FIRSTCHAR (/home/james/ansible/lib/ansible/plugins/lookup/firstchar.py)

 This lookup returns the first character of the contents from a
 file on the Ansible controller's file system.

 * This module is maintained by The Ansible Community
OPTIONS (= is mandatory):

= _terms
 path(s) of files to read

到目前为止,您已经看到在 Ansible 中插件开发和模块开发之间有很多重叠。特别重要的是要注意异常处理和生成高质量、易于理解的错误消息,并遵守和维护 Ansible 的高标准文档。我们在这里没有涵盖的一个额外的项目是插件输出。所有插件必须返回 Unicode 字符串;这确保它们可以正确通过jinja2过滤器运行。更多指导信息可以在官方 Ansible 文档中找到:docs.ansible.com/ansible/latest/dev_guide/developing_plugins.html

有了这些知识,现在您应该可以开始自己的插件开发工作,甚至可以将您的代码提交回社区,如果您愿意的话。我们将在下一节简要回顾一下这一点。

与社区分享插件

您可能希望将您的新插件提交到 Ansible 项目,就像我们在第五章中考虑我们的自定义模块一样,使用和创建模块。这个过程与模块的过程几乎完全相同,这一部分将对此进行回顾。

使用以下流程将向 GitHub 上的 Ansible 项目提交一个真实的请求,将您提交的代码与他们的代码合并。除非您真的有一个准备提交到 Ansible 代码库的新模块,否则不要按照这个流程进行。

为了将您的插件作为 Ansible 存储库的 PR 提交,您首先需要 fork 官方 Ansible 存储库的devel分支。要做到这一点,在您的网络浏览器上登录 GitHub 账户(或者如果您还没有账户,创建一个),然后导航到github.com/ansible/ansible.git。点击页面右上角的 Fork:

一旦您将存储库 fork 到您自己的账户,我们将指导您运行所需的命令,将您的模块代码添加到其中,然后创建必需的 PRs,以便将您的新模块与上游 Ansible 项目合并:

  1. 克隆您刚刚 fork 到本地计算机的devel分支。使用类似以下命令的命令,但一定要用符合您自己 GitHub 账户的 URL 替换它:
$ git clone https://github.com/<your GitHub account>/ansible.git
  1. 将您的模块代码复制到适当的plugins/目录中。以下代码块中使用的copy命令只是一个示例,让您了解要做什么——实际上,您应该选择适当的类别子目录来放置您的插件,因为它不一定适合lookup类别。一旦您添加了 Python 文件,执行git add命令使 Git 知道新文件,然后用有意义的commit消息提交它。这里显示了一些示例命令:
$ cd ansible
$ cp ~/ansible-development/plugindev/firstchar.py ./lib/ansible/plugins/lookup
$ git add lib/ansible/plugins/lookup/firstchar.py
$ git commit -m 'Added tested version of firstchar.py for pull request creation'
  1. 现在,请确保使用以下命令将代码推送到您 fork 的存储库:
$ git push
  1. 在您的网络浏览器中返回 GitHub,并导航到 Pull Requests 页面,如下面的屏幕截图所示。点击 New pull request 按钮:

按照 GitHub 网站的指导,完成 PR 创建过程。一旦您成功提交了您的 PR,您应该能够导航到官方 Ansible 源代码存储库的 PR 列表,并在那里找到您的 PR。以下是一个 PR 列表的示例截图,供您参考:

如前所述,如果您的 PR 需要很长时间才能得到审查,不要感到惊慌 - 这仅仅是因为有很多 PR 需要审查和处理。您始终可以通过将插件代码添加到本地*_plugins/目录中来在本地使用您的插件代码,就像我们之前演示的那样,这样您的 PR 的处理速度不会妨碍您使用 Ansible。有关在本地工作时放置插件代码的更多详细信息,请参阅docs.ansible.com/ansible/latest/dev_guide/developing_locally.html

我们完成了对插件创建的探讨,包括两个可工作的示例。希望您发现这段旅程是有益和有价值的,并且增强了您使用 Ansible 并在需要时扩展其功能的能力。

总结

Ansible 插件是 Ansible 功能的核心部分,在本章中,我们发现在整本书中一直在使用它们,甚至没有意识到!Ansible 的模块化设计使得无论您是使用模块还是当前支持的各种类型的插件,都可以轻松扩展和添加功能。无论是添加用于字符串处理的新过滤器,还是查找数据的新方法(或者甚至是连接到新技术的新连接机制),Ansible 插件提供了一个完整的框架,可以将 Ansible 的功能远远扩展到其已经广泛的能力之外。

在本章中,我们了解了 Ansible 支持的各种类型的插件,然后更详细地探讨了它们,并了解了如何获取现有插件的文档和信息。然后,我们完成了两个实际示例,为 Ansible 创建了两种不同类型的插件,同时探讨了插件开发的最佳实践以及这如何与模块开发重叠。最后,我们回顾了如何将我们的新插件代码作为 PR 提交回 Ansible 项目。

在下一章中,我们将探讨编写 Ansible playbook 时应遵循的最佳实践,以确保您生成可管理、高质量的自动化代码。

问题

  1. 您可以使用以下哪个ansible-doc命令来列出所有缓存插件的名称?

A) ansible-doc -a cache -l

B) ansible-doc cache -l

C) ansible-doc -a cache

D) ansible-doc -t cache -l

E) ansible-doc cache

  1. 您需要将哪个类添加到您的lookup插件代码中,以包括大部分插件代码,包括run()items循环、tryexcept

A) LookupModule

B) RunModule

C) StartModule

D) InitModule

E) LoadModule

  1. 真或假 - 为了使用 Python 创建自定义插件,您需要在您的操作系统上安装带有相关依赖项的 Python:

A) True

B) False

进一步阅读

您可以通过直接访问 Ansible 存储库来找到所有插件,网址为github.com/ansible/ansible/tree/devel/lib/ansible/plugins

第七章:编码最佳实践

Ansible 可以帮助您自动化几乎所有日常 IT 任务,从单调的任务,如应用补丁或部署配置文件,到部署全新的基础设施作为代码。随着越来越多的人意识到其强大和简单,Ansible 的使用和参与每年都在增长。您会在互联网上找到许多示例 Ansible playbook、角色、博客文章等,再加上本书这样的资源,您将能够熟练地编写自己的 Ansible playbook。

然而,您如何知道在 Ansible 中编写自动化代码的最佳方法是什么?您如何判断在互联网上找到的示例是否实际上是一种好的做事方式?在本章中,我们将带您了解 Ansible 最佳实践的实际指南,向您展示目前被认为是关于目录结构和 playbook 布局的良好实践,如何有效地使用清单(特别是在云上),以及如何最好地区分您的环境。通过本章的学习,您应该能够自信地编写从小型单任务 playbook 到复杂环境的大规模 playbook。

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

  • 首选的目录布局

  • 云清单的最佳方法

  • 区分不同的环境类型

  • 定义组和主机变量的正确方法

  • 使用顶级 playbook

  • 利用版本控制工具

  • 设置操作系统和分发差异

  • Ansible 版本之间的移植

技术要求

本章假设您已经按照第一章 开始使用 Ansible中的方式设置了 Ansible 的控制主机,并且您正在使用最新版本;本章的示例是在 Ansible 2.9 上测试的。本章还假设您至少有一个额外的主机进行测试;理想情况下,这应该是基于 Linux 的。尽管本章将给出主机名的具体示例,但欢迎您用自己的主机名和/或 IP 地址替换它们,如何做到这一点的详细信息将在适当的地方提供。

本章中使用的代码包可以在github.com/PacktPublishing/Ansible-2-Cookbook/tree/master/Chapter%207找到。

首选的目录布局

正如我们在本书中探讨了 Ansible 一样,我们多次表明,随着 playbook 的规模和规模的增长,您越有可能希望将其分成多个文件和目录。这方面的一个很好的例子是角色,在第四章 Playbooks and Roles中,我们定义了角色,不仅使我们能够重用常见的自动化代码,还使我们能够将潜在的庞大的单个 playbook 分成更小、逻辑上组织得更好、更易管理的部分。我们还在第三章 Defining Your Inventory中,探讨了定义清单文件的过程,以及如何将其分成多个文件和目录。然而,我们还没有探讨如何将所有这些放在一起。所有这些都在官方 Ansible 文档中有记录,网址是docs.ansible.com/ansible/latest/user_guide/playbooks_best_practices.html#content-organization

然而,在本章中,让我们从一个实际的例子开始,向您展示一个设置简单基于角色的 playbook 的目录结构的好方法,其中有两个不同的清单——一个用于开发环境,一个用于生产环境(在任何真实的用例中,您都希望将它们分开,尽管理想情况下,您应该能够在两者上执行相同的操作以保持一致性和测试目的)。

让我们开始构建目录结构:

  1. 使用以下命令为您的开发清单创建目录树:
$ mkdir -p inventories/development/group_vars
$ mkdir -p inventories/development/host_vars
  1. 接下来,我们将为我们的开发清单定义一个 INI 格式的清单文件——在我们的示例中,我们将保持非常简单,只有两台服务器。要创建的文件是inventories/development/hosts
[app]
app01.dev.example.com
app02.dev.example.com
  1. 为了进一步说明,我们将为我们的 app 组添加一个组变量。如第三章中所讨论的,创建一个名为app.yml的文件,放在我们在上一步中创建的group_vars目录中:
---
http_port: 8080
  1. 接下来,使用相同的方法创建一个production目录结构:
$ mkdir -p inventories/production/group_vars
$ mkdir -p inventories/production/host_vars
  1. 在新创建的production目录中创建名为hosts的清单文件,并包含以下内容:
[app]
app01.prod.example.com
app02.prod.example.com
  1. 现在,我们将为我们的生产清单的http_port组变量定义一个不同的值。将以下内容添加到inventories/production/group_vars/app.yml中:
---
http_port: 80

这完成了我们的清单定义。接下来,我们将添加任何我们可能发现对我们的 playbook 有用的自定义模块或插件。假设我们想要使用我们在第五章中创建的remote_filecopy.py模块。就像我们在本章中讨论的那样,我们首先为这个模块创建目录:

$ mkdir library

然后,将remote_filecopy.py模块添加到此库中。我们不会在这里重新列出代码以节省空间,但您可以从第五章中名为开发自定义模块的部分复制它,或者利用本书在 GitHub 上附带的示例代码。

插件也可以做同样的事情;如果我们还想使用我们在第六章中创建的filter插件,我们将创建一个适当命名的目录:

$ mkdir filter_plugins

然后,将filter插件代码复制到此目录中。

最后,我们将创建一个角色来在我们的新 playbook 结构中使用。当然,您会有很多角色,但我们将创建一个作为示例,然后您可以为每个角色重复这个过程。我们将称我们的角色为installapp,并使用ansible-galaxy命令(在第四章中介绍)为我们创建目录结构:

$ mkdir roles
$ ansible-galaxy role init --init-path roles/ installapp
- Role installapp was created successfully

然后,在我们的roles/installapp/tasks/main.yml文件中,我们将添加以下内容:

---
- name: Display http_port variable contents
  debug:
    var: http_port

- name: Create /tmp/foo
  file:
    path: /tmp/foo
    state: file

- name: Use custom module to copy /tmp/foo
  remote_filecopy:
    source: /tmp/foo
    dest: /tmp/bar

- name: Define a fact about automation
  set_fact:
    about_automation: "Puppet is an excellent automation tool"

- name: Tell us about automation with a custom filter applied
  debug:
    msg: "{{ about_automation | improve_automation }}"

在上述代码中,我们重用了本书前几章的许多示例。您还可以像之前讨论的那样为角色定义处理程序、变量、默认值等,但对于我们的示例来说,这就足够了。

创建我们最佳实践目录结构的最后阶段是添加一个顶层 playbook 来运行。按照惯例,这将被称为site.yml,并且它将具有以下简单内容(请注意,我们构建的目录结构处理了许多事情,使得顶层 playbook 非常简单):

---
- name: Play using best practise directory structure
  hosts: all

  roles:
    - installapp

为了清晰起见,您的最终目录结构应如下所示:

.
├── filter_plugins
│   ├── custom_filter.py
│   └── custom_filter.pyc
├── inventories
│   ├── development
│   │   ├── group_vars
│   │   │   └── app.yml
│   │   ├── hosts
│   │   └── host_vars
│   └── production
│   ├── group_vars
│   │   └── app.yml
│   ├── hosts
│   └── host_vars
├── library
│   └── remote_filecopy.py
├── roles
│   └── installapp
│   ├── defaults
│   │   └── main.yml
│   ├── files
│   ├── handlers
│   │   └── main.yml
│   ├── meta
│   │   └── main.yml
│   ├── README.md
│   ├── tasks
│   │   └── main.yml
│   ├── templates
│   ├── tests
│   │   ├── inventory
│   │   └── test.yml
│   └── vars
│   └── main.yml
└── site.yml

现在,我们可以以正常方式运行我们的 playbook。例如,要在开发清单上运行它,请执行以下操作:

$ ansible-playbook -i inventories/development/hosts site.yml

PLAY [Play using best practise directory structure] ****************************

TASK [Gathering Facts] *********************************************************
ok: [app02.dev.example.com]
ok: [app01.dev.example.com]

TASK [installapp : Display http_port variable contents] ************************
ok: [app01.dev.example.com] => {
 "http_port": 8080
}
ok: [app02.dev.example.com] => {
 "http_port": 8080
}

TASK [installapp : Create /tmp/foo] ********************************************
changed: [app02.dev.example.com]
changed: [app01.dev.example.com]

TASK [installapp : Use custom module to copy /tmp/foo] *************************
changed: [app02.dev.example.com]
changed: [app01.dev.example.com]

TASK [installapp : Define a fact about automation] *****************************
ok: [app01.dev.example.com]
ok: [app02.dev.example.com]

TASK [installapp : Tell us about automation with a custom filter applied] ******
ok: [app01.dev.example.com] => {
 "msg": "Ansible is an excellent automation tool"
}
ok: [app02.dev.example.com] => {
 "msg": "Ansible is an excellent automation tool"
}

PLAY RECAP *********************************************************************
app01.dev.example.com : ok=6 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
app02.dev.example.com : ok=6 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

同样,对生产清单运行以下命令:

$ ansible-playbook -i inventories/production/hosts site.yml

PLAY [Play using best practise directory structure] ****************************

TASK [Gathering Facts] *********************************************************
ok: [app02.prod.example.com]
ok: [app01.prod.example.com]

TASK [installapp : Display http_port variable contents] ************************
ok: [app01.prod.example.com] => {
 "http_port": 80
}
ok: [app02.prod.example.com] => {
 "http_port": 80
}

TASK [installapp : Create /tmp/foo] ********************************************
changed: [app01.prod.example.com]
changed: [app02.prod.example.com]

TASK [installapp : Use custom module to copy /tmp/foo] *************************
changed: [app02.prod.example.com]
changed: [app01.prod.example.com]

TASK [installapp : Define a fact about automation] *****************************
ok: [app01.prod.example.com]
ok: [app02.prod.example.com]

TASK [installapp : Tell us about automation with a custom filter applied] ******
ok: [app01.prod.example.com] => {
 "msg": "Ansible is an excellent automation tool"
}
ok: [app02.prod.example.com] => {
 "msg": "Ansible is an excellent automation tool"
}

PLAY RECAP *********************************************************************
app01.prod.example.com : ok=6 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
app02.prod.example.com : ok=6 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

注意适当的主机和相关变量是如何被每个清单捕捉到的,以及我们的目录结构是多么整洁和有条理。这是你布置 playbooks 的理想方式,将确保它们可以按需扩展到任何你需要的规模,而不会变得笨重和难以管理或排查故障。在本章的下一节中,我们将探讨处理云清单的最佳方法。

云清单的最佳方法

在第三章《定义清单》中,我们看了一个简单的例子,介绍了如何使用动态清单,并通过使用 Cobbler provisioning 系统的实际示例为你提供了指导。然而,当涉及到使用云清单(它们只是动态清单的一种形式,但专门针对云)时,一开始可能会感到有些困惑,你可能会发现很难让它们运行起来。如果你按照本节概述的高级程序,这将成为一个简单而直接的任务。

由于这是一本实践性的书,我们将选择一个示例进行讨论。遗憾的是,我们没有空间为所有云提供商提供实际示例,但如果你按照我们将为亚马逊 EC2 概述的高级流程,并将其应用到你所需的云提供商(例如,Microsoft Azure 或 Google Cloud Platform),你会发现上手和运行的过程实际上非常简单。

然而,在开始之前需要注意的一点是,在包括 2.8.x 版本在内的 Ansible 版本中,动态清单脚本是 Ansible 源代码的一部分,并且可以从我们在本书中之前检查和克隆的主要 Ansible 存储库中获取。随着 Ansible 不断增长和扩展的性质,已经有必要在 2.9.x 版本(以及以后的版本)中将动态清单脚本分离到一个称为 Ansible 集合的新分发机制中,这将成为 2.10 版本的主流(在撰写本文时尚未发布)。你可以在www.ansible.com/blog/getting-started-with-ansible-collections了解更多关于 Ansible 集合及其内容。

然而,随着 Ansible 2.10 版本的发布,你下载和使用动态清单脚本的方式可能会发生根本性的变化,然而,遗憾的是,在撰写本文时,关于这将是什么样子,目前还没有透露太多。因此,我们将指导你下载当前 2.9 版本所需的动态清单提供商脚本,并建议你在 2.10 版本发布时查阅 Ansible 文档,以获取相关脚本的下载位置。一旦你下载了它们,我相信你将能够按照本章概述的方式继续使用它们。

如果你正在使用 Ansible 2.9 版本,你可以在 GitHub 的 stable-2.9 分支上找到并下载所有最新的动态清单脚本,网址为github.com/ansible/ansible/tree/stable-2.9/contrib/inventory

尽管官方的 Ansible 文档已经更新,但互联网上的大多数指南仍然引用这些脚本的旧 GitHub 位置,你会发现它们已经不再起作用。在使用动态清单时,请记住这一点!现在让我们继续讨论使用云提供商的动态清单脚本的过程;我们将以亚马逊 EC2 动态清单脚本作为工作示例,但我们在这里应用的原则同样适用于任何其他云清单脚本:

  1. 在确定我们要使用 Amazon EC2 之后,我们的第一个任务是获取动态清单脚本及其相关的配置文件。由于云技术发展迅速,最安全的做法可能是直接从 GitHub 上的官方 Ansible 项目下载这些文件的最新版本。以下三个命令将下载动态清单脚本并使其可执行,以及下载模板配置文件:
$ wget https://raw.githubusercontent.com/ansible/ansible/stable-2.9/contrib/inventory/ec2.py
$ chmod +x ec2.py
$ wget https://raw.githubusercontent.com/ansible/ansible/stable-2.9/contrib/inventory/ec2.ini
  1. 成功下载文件后,让我们来看看它们的内容。不幸的是,Ansible 动态清单没有与我们在模块和插件中看到的那样整洁的文档系统。然而,对我们来说幸运的是,这些动态清单脚本的作者在这些文件的顶部放置了许多有用的注释,以帮助我们入门。让我们来看看ec2.py的内容:
#!/usr/bin/env python

'''
EC2 external inventory script
=================================

Generates inventory that Ansible can understand by making API request to
AWS EC2 using the Boto library.

NOTE: This script assumes Ansible is being executed where the environment
variables needed for Boto have already been set:
    export AWS_ACCESS_KEY_ID='AK123'
    export AWS_SECRET_ACCESS_KEY='abc123'

Optional region environment variable if region is 'auto'

This script also assumes that there is an ec2.ini file alongside it. To specify
 a
different path to ec2.ini, define the EC2_INI_PATH environment variable:

    export EC2_INI_PATH=/path/to/my_ec2.ini

有很多文档需要阅读,但其中一些最相关的信息包含在那些开头几行中。首先,我们需要确保Boto库已安装。其次,我们需要为Boto设置 AWS 访问参数。本文档的作者已经给了我们最快的入门方式(确实,他们的工作不是复制Boto文档)。

但是,如果您参考Boto的官方文档,您会发现有很多配置 AWS 凭据的方法——设置环境变量只是其中之一。您可以在boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html上阅读有关配置Boto身份验证的更多信息。

  1. 在继续安装Boto之前,让我们来看看示例ec2.ini文件:
# Ansible EC2 external inventory script settings
#

[ec2]

# to talk to a private eucalyptus instance uncomment these lines
# and edit edit eucalyptus_host to be the host name of your cloud controller
#eucalyptus = True
#eucalyptus_host = clc.cloud.domain.org

# AWS regions to make calls to. Set this to 'all' to make request to all regions
# in AWS and merge the results together. Alternatively, set this to a comma
# separated list of regions. E.g. 'us-east-1,us-west-1,us-west-2' and do not
# provide the 'regions_exclude' option. If this is set to 'auto', AWS_REGION or
# AWS_DEFAULT_REGION environment variable will be read to determine the region.
regions = all
regions_exclude = us-gov-west-1, cn-north-1

同样,您可以在此文件中看到大量经过良好记录的选项,并且如果您滚动到底部,甚至会发现您可以在此文件中指定您的凭据,作为先前讨论的方法的替代。然而,默认设置对于您只是想开始使用的情况已经足够了。

  1. 让我们现在确保Boto库已安装;确切的安装方法将取决于您选择的操作系统和 Python 版本。您可能可以通过软件包安装它;在 CentOS 7 上,您可以按照以下步骤执行此操作:
$ sudo yum -y install python-boto python-boto3

或者,您可以使用pip来实现这一目的。例如,要将其安装为 Python 3 环境的一部分,您可以运行以下命令:

$ sudo pip3 install boto3
  1. 安装了Boto之后,让我们继续使用前面文档中建议给我们的环境变量来设置我们的 AWS 凭据:
$ export AWS_ACCESS_KEY_ID='<YOUR_DATA>'
$ export AWS_SECRET_ACCESS_KEY='<YOUR_DATA>'
  1. 完成这些步骤后,您现在可以像往常一样使用动态清单脚本——只需使用-i参数引用可执行的清单脚本,就像您在静态清单中所做的那样。例如,如果您想对在 Amazon EC2 上运行的所有主机运行 Ansible ping模块作为临时命令,您需要运行以下命令。确保用-u开关指定的用户帐户替换连接到 EC2 实例的用户帐户。还要引用您的私有 SSH 密钥文件:
$ ansible -i ec2.py -u ec2-user --private-key /home/james/my-ec2-id_rsa -m ping all

就是这样——如果您以同样的系统方法处理所有动态清单脚本,那么您将毫无问题地使它们运行起来。只需记住,文档通常嵌入在脚本文件和其附带的配置文件中,请确保在尝试使用脚本之前阅读两者。

需要注意的一点是,许多动态清单脚本,包括ec2.py,会缓存其对云提供商的 API 调用结果,以加快重复运行的速度并避免过多的 API 调用。然而,在快速发展的开发环境中,你可能会发现云基础设施的更改没有被及时捕捉到。对于大多数脚本,有两种解决方法——大多数特性缓存配置参数在其配置文件中,比如ec2.ini中的cache_pathcache_max_age参数。如果你不想为每次运行都设置这些参数,你也可以通过直接调用动态清单脚本并使用特殊开关来手动刷新缓存,例如在ec2.py中:

$ ./ec2.py --refresh-cache

这就结束了我们对云清单脚本的实际介绍。正如我们讨论过的,只要你查阅文档(包括互联网上的文档和每个动态清单脚本中嵌入的文档),并遵循我们描述的简单方法,你应该不会遇到问题,并且应该能够在几分钟内开始使用动态清单。在下一节中,我们将回到静态清单,并探讨区分各种技术环境的最佳方法。

区分不同的环境类型

在几乎每个企业中,你都需要按类型划分你的技术环境。例如,你几乎肯定会有一个开发环境,在这里进行所有的测试和开发工作,并且有一个生产环境,在这里运行所有稳定的测试代码。这些环境(在最理想的情况下)应该使用相同的 Ansible playbooks——毕竟,逻辑是,如果你能够在开发环境成功部署和测试一个应用程序,那么你应该能够以同样的方式在生产环境中部署它,并且它能够正常运行。然而,这两个环境之间总是存在差异,不仅仅是在主机名上,有时还包括参数、负载均衡器名称、端口号等等——这个列表似乎是无穷无尽的。

在本章的首选目录布局部分,我们介绍了使用两个单独的清单目录树来区分开发和生产环境的方法。当涉及到区分这些环境时,你应该按照这种方式进行;因此,显然,我们不会重复这些例子,但重要的是要注意,当处理多个环境时,你的目标应该是:

  • 尽量重用相同的 playbooks 来运行相同代码的所有环境。例如,如果你在开发环境部署了一个 web 应用程序,你应该有信心你的 playbooks 也能在生产环境(以及你的质量保证QA)环境,以及其他可能需要部署的环境)中部署相同的应用程序。

  • 这意味着你不仅在测试应用程序部署和代码,还在测试 Ansible 的 playbooks 和 roles 作为整个测试过程的一部分。

  • 每个环境的清单应该保存在单独的目录树中(就像本章的首选目录布局部分所示),但所有的 roles、playbooks、插件和模块(如果有的话)都应该在相同的目录结构中(这对于两个环境来说应该是一样的)。

  • 不同的环境通常需要不同的身份验证凭据;你应该将这些凭据分开保存,不仅是为了安全,还为了确保 playbooks 不会意外地在错误的环境中运行。

  • 你的 playbooks 应该在你的版本控制系统中,就像你的代码一样。这样可以让你随着时间跟踪变化,并确保每个人都在使用相同的自动化代码副本。

如果您注意这些简单的指针,您会发现您的自动化工作流程成为您业务的真正资产,并确保在所有部署中可靠性和一致性。相反,不遵循这些指针会使您面临在开发中运行正常但在生产中运行失败的可怕情况,这经常困扰着技术行业。现在,让我们在下一节中继续讨论,看看在处理主机和组变量时的最佳实践,正如我们在首选目录布局部分中所看到的,您需要应用这些实践,特别是在处理多个环境时。

定义组和主机变量的正确方法

在处理组和主机变量时,您可以使用我们在首选目录布局部分中使用的基于目录的方法进行拆分。但是,有一些额外的指针可以帮助您管理这一点。首先,您应该始终注意变量的优先级。变量优先级顺序的详细列表可以在docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html#variable-precedence-where-should-i-put-a-variable找到。但是,处理多个环境的关键要点如下:

  • 主机变量始终比组变量的优先级高;因此,您可以使用主机变量覆盖任何组变量。如果您以受控的方式利用此行为,这种行为是有用的,但如果您不了解它,可能会产生意想不到的结果。

  • 有一个名为all的特殊组变量定义,适用于所有清单组。这比特定定义的组变量的优先级低。

  • 如果您在两个组中定义相同的变量会发生什么?如果发生这种情况,两个组具有相同的优先级,那么谁会获胜?为了演示这一点(以及我们之前的例子),我们将为您创建一个简单的实际示例。

要开始,让我们为我们的清单创建一个目录结构。为了尽可能简洁,我们只会创建一个开发环境的例子。但是,您可以通过在本章的首选目录布局部分构建更完整的示例来扩展这些概念:

  1. 使用以下命令创建一个清单目录结构:
$ mkdir -p inventories/development/group_vars
$ mkdir -p inventories/development/host_vars
  1. inventories/development/hosts文件中创建一个包含两个主机的单个组的简单清单文件;内容应如下所示:
[app]
app01.dev.example.com
app02.dev.example.com
  1. 现在,让我们为清单中的所有组创建一个特殊的组变量文件;这个文件将被称为inventories/development/group_vars/all.yml,应包含以下内容:
---
http_port: 8080
  1. 最后,让我们创建一个名为site.yml的简单 playbook,以查询和打印我们刚刚创建的变量的值:
---
- name: Play using best practise directory structure
  hosts: all

  tasks:
    - name: Display the value of our inventory variable
      debug:
        var: http_port
  1. 现在,如果我们运行这个 playbook,我们会看到变量(我们只在一个地方定义)取得了我们期望的值:
$ ansible-playbook -i inventories/development/hosts site.yml

PLAY [Play using best practise directory structure] ****************************

TASK [Gathering Facts] *********************************************************
ok: [app01.dev.example.com]
ok: [app02.dev.example.com]

TASK [Display the value of our inventory variable] *****************************
ok: [app01.dev.example.com] => {
 "http_port": 8080
}
ok: [app02.dev.example.com] => {
 "http_port": 8080
}

PLAY RECAP *********************************************************************
app01.dev.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
app02.dev.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
  1. 到目前为止,一切顺利!现在,让我们向我们的清单目录结构添加一个新文件,all.yml文件保持不变。我们还将创建一个位于inventories/development/group_vars/app.yml的新文件,其中将包含以下内容:
---
http_port: 8081
  1. 我们现在在一个名为all的特殊组和app组中定义了相同的变量(我们的开发清单中的两个服务器都属于这个组)。那么,如果我们现在运行我们的 playbook 会发生什么?输出应如下所示:
$ ansible-playbook -i inventories/development/hosts site.yml

PLAY [Play using best practise directory structure] ****************************

TASK [Gathering Facts] *********************************************************
ok: [app02.dev.example.com]
ok: [app01.dev.example.com]

TASK [Display the value of our inventory variable] *****************************
ok: [app01.dev.example.com] => {
 "http_port": 8081
}
ok: [app02.dev.example.com] => {
 "http_port": 8081
}

PLAY RECAP *********************************************************************
app01.dev.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
app02.dev.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
  1. 如预期的那样,在特定组中的变量定义获胜,这符合 Ansible 文档中记录的优先顺序。现在,让我们看看如果我们在两个特定命名的组中定义相同的变量会发生什么。为了完成这个示例,我们将创建一个子组,称为centos,以及另一个可能包含按照新的构建标准构建的主机的组,称为newcentos,这两个应用服务器都将是其成员。这意味着修改inventories/development/hosts,使其看起来如下:
[app]
app01.dev.example.com
app02.dev.example.com

[centos:children]
app

[newcentos:children]
app
  1. 现在,让我们通过创建一个名为inventories/development/group_vars/centos.yml的文件来重新定义centos组的http_port变量,其中包含以下内容:
---
http_port: 8082
  1. 为了增加混乱,让我们也在inventories/development/group_vars/newcentos.yml中为newcentos组定义这个变量,其中包含以下内容:
---
http_port: 8083
  1. 我们现在在组级别定义了相同的变量四次!让我们重新运行我们的 playbook,看看哪个值会通过:
$ ansible-playbook -i inventories/development/hosts site.yml

PLAY [Play using best practise directory structure] ****************************

TASK [Gathering Facts] *********************************************************
ok: [app01.dev.example.com]
ok: [app02.dev.example.com]

TASK [Display the value of our inventory variable] *****************************
ok: [app01.dev.example.com] => {
 "http_port": 8083
}
ok: [app02.dev.example.com] => {
 "http_port": 8083
}

PLAY RECAP *********************************************************************
app01.dev.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
app02.dev.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

我们在newcentos.yml中输入的值赢了-但为什么?Ansible 文档规定,在清单中(唯一可以这样做的地方)在组级别定义相同的变量时,最后加载的组中的变量获胜。组按字母顺序处理,newcentos是字母表中最后一个字母开头的组-因此,它的http_port值是获胜的值。

  1. 为了完整起见,我们可以通过不触及group_vars目录,但添加一个名为inventories/development/host_vars/app01.dev.example.com.yml的文件来覆盖所有这些,其中包含以下内容:
---
http_port: 9090
  1. 现在,如果我们最后再次运行我们的 playbook,我们会看到我们在主机级别定义的值完全覆盖了我们为app01.dev.example.com设置的任何值。app02.dev.example.com不受影响,因为我们没有为它定义主机变量,所以优先级的下一个最高级别是newcentos组的组变量:
$ ansible-playbook -i inventories/development/hosts site.yml

PLAY [Play using best practise directory structure] ****************************

TASK [Gathering Facts] *********************************************************
ok: [app01.dev.example.com]
ok: [app02.dev.example.com]

TASK [Display the value of our inventory variable] *****************************
ok: [app01.dev.example.com] => {
 "http_port": 9090
}
ok: [app02.dev.example.com] => {
 "http_port": 8083
}

PLAY RECAP *********************************************************************
app01.dev.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
app02.dev.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

有了这些知识,你现在可以做出关于如何在清单中结构化你的变量以确保在主机和组级别都能达到期望结果的高级决策。了解变量优先级顺序是很重要的,因为这些示例已经证明了这一点,但遵循文档中的顺序也将使你能够创建功能强大、灵活的 playbook 清单,可以在多个环境中很好地工作。现在,你可能已经注意到,在本章中,我们在我们的目录结构中使用了一个名为site.yml的顶层 playbook。我们将在下一节更详细地讨论这个 playbook。

使用顶层 playbooks

到目前为止的所有示例中,我们都是使用 Ansible 推荐的最佳实践目录结构构建的,并且不断地引用顶层 playbook,通常称为site.yml。这个 playbook 的理念,实际上,是在我们所有的目录结构中都有一个共同的名字,这样它就可以在整个服务器环境中使用-也就是说,你的site

当然,这并不是说你必须在基础设施的每台服务器或每个功能上使用相同的 playbook 集合;相反,这意味着只有你才能做出最适合你环境的最佳决定。然而,Ansible 自动化的整个目标是所创建的解决方案简单易于运行和操作。想象一下,将一个包含 100 个不同 playbook 的 playbook 目录结构交给一个新的系统管理员-他们怎么知道应该在什么情况下运行哪些 playbook?培训某人使用 playbook 的任务将是巨大的,只会将复杂性从一个领域转移到另一个领域。

在另一端,您可以利用when子句与事实和清单分组,以便您的 playbook 确切地知道在每种可能的情况下在每台服务器上运行什么。当然,这是不太可能发生的,事实是您的自动化解决方案最终会处于中间位置。

最重要的是,当收到新的 playbook 目录结构时,新操作员至少知道运行 playbook 和理解代码的起点在哪里。如果他们遇到的顶级 playbook 总是site.yml,那么至少每个人都知道从哪里开始。通过巧妙地使用角色和import_*include_*语句,您可以将 playbook 分割成可重用代码的逻辑部分,正如我们之前讨论的那样,所有这些都来自一个 playbook 文件。

现在您已经了解了顶级 playbook 的重要性,让我们在下一节中看看如何利用版本控制工具来确保在集中和维护自动化代码时遵循良好的实践。

利用版本控制工具

正如我们在本章前面讨论的那样,对于您的 Ansible 自动化代码,版本控制和测试不仅仅是您的代码,还包括清单(或动态清单脚本)、任何自定义模块、插件、角色和 playbook 代码都至关重要。这是因为 Ansible 自动化的最终目标很可能是使用 playbook(或一组 playbook)部署整个环境。这甚至可能涉及部署基础设施作为代码,特别是如果您要部署到云环境中。

对 Ansible 代码的任何更改可能意味着对您的环境的重大更改,甚至可能意味着重要的生产服务是否正常工作。因此,非常重要的是您保持 Ansible 代码的版本历史,并且每个人都使用相同的版本。您可以自由选择最适合您的版本控制系统;大多数公司环境已经有某种版本控制系统。但是,如果您以前没有使用过版本控制系统,我们建议您在 GitHub 或 GitLab 等地方注册免费帐户,这两者都提供免费的版本控制存储库,以及更高级的付费计划。

关于 Git 的版本控制的完整讨论超出了本书的范围;事实上,整本书都致力于这个主题。但是,我们将带您了解最简单的用例。在以下示例中,假定您正在使用 GitHub 上的免费帐户,但如果您使用不同的提供商,只需更改 URL 以匹配您的版本控制存储库主机给您的 URL。

除此之外,您还需要在 Linux 主机上安装命令行 Git 工具。在 CentOS 上,您可以按照以下方式安装这些工具:

$ sudo yum install git

在 Ubuntu 上,这个过程同样简单:

$ sudo apt-get update
$ sudo apt-get install git

工具安装完成并且您的帐户设置好之后,您的下一个任务是将 Git 存储库克隆到您的计算机。如果您想开始使用自己的存储库进行工作,您需要与提供商一起设置这一点——GitHub 和 GitLab 都提供了出色的文档,您应该按照这些文档设置您的第一个存储库。

一旦设置和初始化,您可以克隆一个副本到您的本地计算机以对代码进行更改。这个本地副本称为工作副本,您可以按照以下步骤进行克隆和更改的过程(请注意,这些纯属假设性的例子,只是为了让您了解需要运行的命令;您应该根据自己的用例进行调整):

  1. 使用以下命令将您的git存储库克隆到本地计算机以创建一个工作副本:
$ git clone https://github.com/<YOUR_GIT_ACCOUNT>/<GIT_REPO>.git
Cloning into '<GIT_REPO>'...
remote: Enumerating objects: 7, done.
remote: Total 7 (delta 0), reused 0 (delta 0), pack-reused 7
Unpacking objects: 100% (7/7), done. 
  1. 切换到您克隆的代码目录(工作副本)并进行任何需要的代码更改:
$ cd <GIT_REPO>
$ vim myplaybook.yml
  1. 确保测试您的代码,并且当您对其满意时,添加已准备提交新版本的更改文件,使用以下命令:
$ git add myplaybook.yml
  1. 接下来要做的是提交您所做的更改。提交基本上是存储库中的新代码版本,因此应该附有有意义的commit消息(在-m开关后面用引号指定),如下所示:
$ git commit -m 'Added new spongle-widget deployment to myplaybook.yml'
[master ed14138] Added new spongle-widget deployment to myplaybook.yml
 Committer: Daniel Oh <doh@danieloh.redhat.com>
Your name and email address were configured automatically based
on your username and hostname. Please check that they are accurate.
You can suppress this message by setting them explicitly. Run the
following command and follow the instructions in your editor to edit
your configuration file:

    git config --global --edit

After doing this, you may fix the identity used for this commit with:

    git commit --amend --reset-author

 1 file changed, 1 insertion(+), 1 deletion(-) 
  1. 现在,所有这些更改都仅存在于您本地计算机上的工作副本中。这本身就很好,但如果代码可以供所有需要在版本控制系统上查看它的人使用,那将更好。要将更新的提交推送回(例如)GitHub,运行以下命令:
$ git push
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 8 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 297 bytes | 297.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
To https://github.com/<YOUR_GIT_ACCOUNT>/<GIT_REPO>.git
   0d00263..ed14138 master -> master 

就是这样!

  1. 现在,其他合作者可以克隆您的代码,就像我们在步骤 1中所做的那样。或者,如果他们已经有您的存储库的工作副本,他们可以使用以下命令更新他们的工作副本(如果您想要更新您的工作副本以查看其他人所做的更改,也可以这样做):
$ git pull

Git 还有一些非常高级的主题和用例超出了本书的范围。但是,您会发现大约 80%的时间,前面的命令就是您需要的所有 Git 命令行知识。还有许多图形界面的 Git 前端,以及与 Git 存储库集成的代码编辑器和集成开发环境IDEs),可以帮助您更好地利用它们。完成这些后,让我们看看如何确保您可以在多个主机上使用相同的 playbook(或 role),即使它们可能具有不同的操作系统和版本。

设置 OS 和分发差异

如前所述,我们的目标是尽可能广泛地使用相同的自动化代码。然而,尽管我们努力标准化我们的技术环境,变体总是会出现。例如,不可能同时对所有服务器进行主要升级,因此当出现主要的新操作系统版本时,例如Red Hat Enterprise LinuxRHEL)8 或 Ubuntu Server 20.04,一些机器将保持在旧版本上,而其他机器将进行升级。同样,一个环境可能标准化为 Ubuntu,但随后引入了一个只在 CentOS 上获得认证的应用程序。简而言之,尽管标准化很重要,但变体总是会出现。

在编写 Ansible playbook 时,特别是 role 时,您的目标应该是使它们尽可能广泛地适用于您的环境。其中一个经典例子是软件包管理——假设您正在编写一个安装 Apache 2 Web 服务器的 role。如果您必须使用此 role 支持 Ubuntu 和 CentOS,不仅要处理不同的软件包管理器(yumapt),还要处理不同的软件包名称(httpdapache2)。

在第四章中,Playbooks and Roles,我们看了如何使用when子句将条件应用于任务,以及 Ansible 收集的事实,如ansible_distribution。然而,还有另一种在特定主机上运行任务的方法,我们还没有看过。在同一章中,我们还看了如何在一个 playbook 中定义多个 play 的概念——有一个特殊的模块可以根据 Ansible 事实为我们创建清单组,我们可以利用这一点以及多个 play 来创建一个 playbook,根据主机的类型在每个主机上运行适当的任务。最好通过一个实际的例子来解释这一点,所以让我们开始吧。

假设我们在此示例中使用以下简单的清单文件,其中有两个主机在一个名为app的单个组中:

[app]
app01.dev.example.com
app02.dev.example.com

现在让我们构建一个简单的 playbook,演示如何使用 Ansible 事实对不同的 play 进行分组,以便操作系统分发确定 playbook 中运行哪个 play。按照以下步骤创建此 playbook 并观察其运行:

  1. 首先创建一个新的 playbook——我们将其称为osvariants.yml——包含以下Play定义。它还将包含一个单独的任务,如下所示:
---
- name: Play to demonstrate group_by module
  hosts: all

  tasks:
    - name: Create inventory groups based on host facts
      group_by:
        key: os_{{ ansible_facts['distribution'] }}

到目前为止,playbook 结构对您来说应该已经非常熟悉了。但是,使用group_by模块是新的。它根据我们指定的键动态创建新的清单组——在本例中,我们根据从Gathering Facts阶段获取的 OS 发行版事实创建组。原始清单组结构保留不变,但所有主机也根据其事实添加到新创建的组中。

因此,我们简单清单中的两台服务器仍然在app组中,但如果它们基于 Ubuntu,它们将被添加到一个名为os_Ubuntu的新创建的清单组中。同样,如果它们基于 CentOS,它们将被添加到一个名为os_CentOS的组中。

  1. 有了这些信息,我们可以继续根据新创建的组创建额外的 play。让我们将以下Play定义添加到同一个 playbook 文件中,以在 CentOS 上安装 Apache:
- name: Play to install Apache on CentOS
  hosts: os_CentOS
  become: true

  tasks:
    - name: Install Apache on CentOS
      yum:
        name: httpd
        state: present

这是一个完全正常的Play定义,它使用yum模块来安装httpd包(在 CentOS 上需要)。唯一与我们之前工作不同的是 play 顶部的hosts定义。这使用了第一个 play 中由group_by模块创建的新创建的清单组。

  1. 同样,我们可以添加第三个Play定义,这次是使用apt模块在 Ubuntu 上安装apache2包:
- name: Play to install Apache on Ubuntu
  hosts: os_Ubuntu
  become: true

  tasks:
    - name: Install Apache on Ubuntu
      apt:
        name: apache2
        state: present
  1. 如果我们的环境是基于 CentOS 服务器并运行此 playbook,则结果如下:
$ ansible-playbook -i hosts osvariants.yml

PLAY [Play to demonstrate group_by module] *************************************

TASK [Gathering Facts] *********************************************************
ok: [app02.dev.example.com]
ok: [app01.dev.example.com]

TASK [Create inventory groups based on host facts] *****************************
ok: [app01.dev.example.com]
ok: [app02.dev.example.com]

PLAY [Play to install Apache on CentOS] ****************************************

TASK [Gathering Facts] *********************************************************
ok: [app01.dev.example.com]
ok: [app02.dev.example.com]

TASK [Install Apache on CentOS] ************************************************
changed: [app02.dev.example.com]
changed: [app01.dev.example.com]
[WARNING]: Could not match supplied host pattern, ignoring: os_Ubuntu

PLAY [Play to install Apache on Ubuntu] ****************************************
skipping: no hosts matched

PLAY RECAP *********************************************************************
app01.dev.example.com : ok=4 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
app02.dev.example.com : ok=4 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

请注意安装 CentOS 上的 Apache 的任务是如何运行的。它是这样运行的,因为group_by模块创建了一个名为os_CentOS的组,而我们的第二个 play 仅在名为os_CentOS的组中运行。由于清单中没有运行 Ubuntu 的服务器,因此os_Ubuntu组从未被创建,因此第三个 play 不会运行。我们会收到有关没有与os_Ubuntu匹配的主机模式的警告,但 playbook 不会失败——它只是跳过了这个 play。

我们提供了这个例子,以展示另一种管理自动化编码中不可避免的 OS 类型差异的方式。归根结底,选择最适合您的编码风格取决于您。您可以使用group_by模块,如此处所述,或者将任务编写成块,并向块添加when子句,以便仅在满足某个基于事实的条件时运行(例如,OS 发行版是 CentOS)——或者甚至两者的组合。选择最终取决于您,这些不同的示例旨在为您提供多种选择,以便您在其中选择最佳解决方案。

最后,让我们通过查看在 Ansible 版本之间移植自动化代码来结束本章。

在 Ansible 版本之间移植

Ansible 是一个快速发展的项目,随着发布和新功能的添加,新模块(和模块增强)被发布,软件中不可避免的错误也得到修复。毫无疑问,您最终会写出针对 Ansible 的一个版本的代码,然后在某个时候需要再次在更新的版本上运行它。举例来说,当我们开始写这本书时,当前的 Ansible 版本是 2.7。当我们编辑这本书准备出版时,版本 2.9.6 是当前的稳定版本。

通常情况下,当你升级时,你会发现你的早期版本的代码“差不多能用”,但这并不总是确定的。有时模块会被弃用(尽管通常不会没有警告地弃用),功能也会发生变化。预计 Ansible 2.10 发布时会有一些重大变更。因此,问题是——当你更新你的 Ansible 安装时,如何确保你的剧本、角色、模块和插件仍然能够正常工作?

回答的第一部分是确定你从哪个版本的 Ansible 开始。例如,假设你正在准备升级到 Ansible 2.10。如果你查询已安装的 Ansible 版本,看到类似以下的内容,那么你就知道你是从 Ansible 2.9 版本开始的:

$ ansible --version
ansible 2.9.6
 config file = /etc/ansible/ansible.cfg
 configured module search path = [u'/home/james/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
 ansible python module location = /usr/lib/python2.7/site-packages/ansible
 executable location = /usr/bin/ansible
 python version = 2.7.5 (default, Aug 7 2019, 00:51:29) [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)]

因此,你首先需要查看 Ansible 2.10 版本的迁移指南;通常每个主要版本(比如 2.8、2.9 等)都会有一个迁移指南。2.10 版本的指南可以在 docs.ansible.com/ansible/devel/porting_guides/porting_guide_2.10.html 找到。

如果我们查看这份文档,我们会发现即将有一些变更——这些变更对你是否重要取决于你正在运行的代码。例如,如果我们查看指南中的 Modules Removed 部分,我们会发现 letsencrypt 模块已被移除,并建议你使用 acme_certificate 模块代替。如果你在 Ansible 中使用 letsencrypt 模块生成免费的 SSL 证书,那么你肯定需要更新你的剧本和角色以适应这一变更。

正如你在前面的链接中看到的,Ansible 2.9 和 2.10 版本之间有大量的变更。因此,重要的是要注意,迁移指南是从升级前一个主要版本的角度编写的。也就是说,如果你查询你的 Ansible 版本并返回以下内容,那么你是从 Ansible 2.8 迁移过来的:

$ ansible --version
ansible 2.8.4
 config file = /etc/ansible/ansible.cfg
 configured module search path = [u'/home/james/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
 ansible python module location = /usr/lib/python2.7/site-packages/ansible
 executable location = /usr/bin/ansible
 python version = 2.7.5 (default, Aug 7 2019, 00:51:29) [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)]

如果你直接升级到 Ansible 2.10,那么你需要查看 2.9(涵盖了从 2.8 到 2.9 的代码变更)和 2.10(涵盖了从 2.9 到 2.10 的升级所需的变更)的迁移指南。所有迁移指南的索引可以在官方 Ansible 网站上找到,网址是 docs.ansible.com/ansible/devel/porting_guides/porting_guides.html

另一个获取信息的好途径,尤其是更精细的信息,是变更日志。这些日志会在每个次要版本发布时发布和更新,目前可以在官方 Ansible GitHub 仓库的stable分支上找到,用于你想查询的版本。例如,如果你想查看 Ansible 2.9 的所有变更日志,你需要前往 github.com/ansible/ansible/blob/stable-2.9/changelogs/CHANGELOG-v2.9.rst

将代码从 Ansible 版本迁移到另一个版本(如果你愿意这么称呼的话)的诀窍就是阅读 Ansible 项目团队发布的优秀文档。大量的工作投入到了创建这些文档中,因此建议你充分利用。这就结束了我们对使用 Ansible 的最佳实践的介绍。希望你觉得这一章很有价值。

总结

Ansible 自动化项目通常从小规模开始,但随着人们意识到 Ansible 的强大和简单,代码和清单往往呈指数增长(至少在我的经验中是这样)。在推动更大规模自动化的过程中,重要的是 Ansible 自动化代码和基础设施本身不会成为另一个头疼事。通过在早期嵌入一些良好的实践并在整个使用 Ansible 进行自动化的过程中始终如一地应用它们,您会发现管理 Ansible 自动化是简单易行的,并且对您的技术基础设施是真正有益的。

在本章中,您了解了应该为 playbook 采用的目录布局的最佳实践,以及在使用云清单时应采用的步骤。然后,您学习了通过 OS 类型区分环境的新方法,以及有关变量优先级以及在处理主机和组变量时如何利用它的更多信息。然后,您探索了顶级 playbook 的重要性,然后看了如何利用版本控制工具来管理您的自动化代码。最后,您探讨了创建单个 playbook 的新技术,该 playbook 将管理不同 OS 版本和发行版的服务器,最后看了将代码移植到新的 Ansible 版本的重要主题。

在下一章中,我们将探讨您可以使用 Ansible 来处理在自动化过程中可能出现的一些特殊情况的一些更高级的方法。

问题

  1. 什么是一种安全且简单的方式来持续管理(即修改、修复和创建)代码更改并与他人共享?

A)Playbook 修订

B)任务历史

C)临时创建

D)使用 Git 存储库

E)日志管理

  1. Ansible Galaxy 支持从中央、社区支持的存储库与其他用户共享角色。

A)真

B)假

  1. 真或假- Ansible 模块保证在将来的所有版本中都可用。

A)真

B)假

进一步阅读

通过创建分支和标签来管理多个存储库、版本或任务,以有效地控制多个版本。有关更多详细信息,请参考以下链接:

第八章:高级 Ansible 主题

到目前为止,我们已经努力为您提供了 Ansible 的坚实基础,这样无论您想要的自动化任务是什么,您都可以轻松自如地实现它。然而,当您真正开始加速自动化时,您如何确保能够以一种优雅的方式处理任何出现的情况呢?例如,当您必须启动长时间运行的操作时,如何确保您可以异步运行它们,并在稍后可靠地检查结果?或者,如果您正在更新一大群服务器,如何确保如果一小部分服务器出现故障,play 会及早失败?您最不希望做的事情就是在一百台服务器上部署一个有问题的更新(让我们面对现实,每个人的代码都会不时出现问题)——最好的办法是检测到一小部分服务器失败并根据这个基础中止整个 play,而不是试图继续并破坏整个负载均衡集群。

在本章中,我们将看看如何解决这些特定问题,以及使用 Ansible 的一些更高级功能来控制 playbook 流程和错误处理。我们将通过实际示例探讨如何使用 Ansible 执行滚动更新,如何使用代理和跳板主机(这对于安全环境和核心网络配置通常至关重要),以及如何使用本地 Ansible Vault 技术在静态环境中保护敏感的 Ansible 数据。通过本章结束时,您将全面了解如何在小型环境中以及在大型、安全、关键任务的环境中运行 Ansible。

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

  • 异步与同步操作

  • 控制滚动更新的 play 执行

  • 配置最大失败百分比

  • 设置任务执行委托

  • 使用run_once选项

  • 在本地运行 playbooks

  • 使用代理和跳板主机

  • 配置 playbook 提示

  • 在 play 和任务中放置标签

  • 使用 Ansible Vault 保护数据

技术要求

本章假设您已经按照第一章中详细介绍的方式在控制主机上设置了 Ansible,并且正在使用最新版本。本章的示例是在 Ansible 2.9 版本下测试的。本章还假设您至少有一个额外的主机进行测试,并且最好是基于 Linux 的。虽然本章将给出特定主机名的示例,但您可以自由地用自己的主机名和/或 IP 地址替换它们;如何做到这一点的详细信息将在适当的位置提供。

本章的代码包可以在github.com/PacktPublishing/Ansible-2-Cookbook/tree/master/Chapter%208上找到。

异步与同步操作

到目前为止,我们已经在本书中看到,Ansible play 是按顺序执行的,每个任务在下一个任务开始之前都会完成。虽然这通常有利于流程控制和逻辑顺序,但有时你可能不希望这样。特别是,可能会出现某个任务运行时间长于配置的 SSH 连接超时时间,而 Ansible 在大多数平台上使用 SSH 来执行自动化任务,这可能会成为一个问题。

幸运的是,Ansible 任务可以异步运行,也就是说,任务可以在目标主机上后台运行,并定期轮询。这与同步任务形成对比,同步任务会保持与目标主机的连接,直到任务完成(这会导致超时的风险)。

和往常一样,让我们通过一个实际的例子来探讨这个问题。假设我们在一个简单的 INI 格式清单中有两台服务器:

[frontends]
frt01.example.com
frt02.example.com

现在,为了模拟一个长时间运行的任务,我们将使用shell模块运行sleep命令。但是,我们不会让它在sleep命令的持续时间内阻塞 SSH 连接,而是会给任务添加两个特殊参数,如下所示:

---
- name: Play to demonstrate asynchronous tasks
  hosts: frontends
  become: true

  tasks:
    - name: A simulated long running task
      shell: "sleep 20"
      async: 30
      poll: 5

两个新参数是asyncpollasync参数告诉 Ansible 这个任务应该异步运行(这样 SSH 连接就不会被阻塞),最多运行30秒。如果任务运行时间超过配置的时间,Ansible 会认为任务失败,整个操作也会失败。当poll设置为正整数时,Ansible 会以指定的间隔(在这个例子中是每5秒)检查异步任务的状态。如果poll设置为0,那么任务会在后台运行,不会被检查,需要你编写一个任务来手动检查它的状态。

如果你不指定poll值,它将被设置为 Ansible 的DEFAULT_POLL_INTERVAL配置参数定义的默认值(即10秒)。

当你运行这个 playbook 时,你会发现它的运行方式和其他 playbook 一样;从终端输出中,你看不出任何区别。但在幕后,Ansible 会每5秒检查任务,直到成功或达到30秒的async超时值:

$ ansible-playbook -i hosts async.yml

PLAY [Play to demonstrate asynchronous tasks] **********************************

TASK [Gathering Facts] *********************************************************
ok: [frt02.example.com]
ok: [frt01.example.com]

TASK [A simulated long running task] *******************************************
changed: [frt02.example.com]
changed: [frt01.example.com]

PLAY RECAP *********************************************************************
frt01.example.com : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt02.example.com : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

如果你想稍后检查任务(也就是说,如果poll设置为0),你可以在 playbook 中添加第二个任务,使其如下所示:

---
- name: Play to demonstrate asynchronous tasks
  hosts: frontends
  become: true

  tasks:
    - name: A simulated long running task
      shell: "sleep 20"
      async: 30
      poll: 0
      register: long_task

    - name: Check on the asynchronous task
      async_status:
        jid: "{{ long_task.ansible_job_id }}"
      register: async_result
      until: async_result.finished
      retries: 30

在这个 playbook 中,初始的异步任务与之前一样定义,只是现在我们将poll设置为0。我们还选择将这个任务的结果注册到一个名为long_task的变量中,这样我们在后面检查时就可以查询任务的作业 ID。play 中的下一个(新的)任务使用async_status模块来检查我们从第一个任务中注册的作业 ID,并循环直到作业完成或达到30次重试为止。在 playbook 中使用这些时,你几乎肯定不会像这样连续添加两个任务——通常情况下,你会在它们之间执行其他任务,但为了保持这个例子简单,我们将连续运行这两个任务。运行这个 playbook 应该会产生类似以下的输出:

$ ansible-playbook -i hosts async2.yml

PLAY [Play to demonstrate asynchronous tasks] **********************************

TASK [Gathering Facts] *********************************************************
ok: [frt01.example.com]
ok: [frt02.example.com]

TASK [A simulated long running task] *******************************************
changed: [frt02.example.com]
changed: [frt01.example.com]

TASK [Check on the asynchronous task] ******************************************
FAILED - RETRYING: Check on the asynchronous task (30 retries left).
FAILED - RETRYING: Check on the asynchronous task (30 retries left).
FAILED - RETRYING: Check on the asynchronous task (29 retries left).
FAILED - RETRYING: Check on the asynchronous task (29 retries left).
FAILED - RETRYING: Check on the asynchronous task (28 retries left).
FAILED - RETRYING: Check on the asynchronous task (28 retries left).
FAILED - RETRYING: Check on the asynchronous task (27 retries left).
FAILED - RETRYING: Check on the asynchronous task (27 retries left).
changed: [frt01.example.com]
changed: [frt02.example.com]

PLAY RECAP *********************************************************************
frt01.example.com : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt02.example.com : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

在上面的代码块中,我们可以看到长时间运行的任务一直在运行,下一个任务会轮询其状态,直到满足我们设置的条件。在这种情况下,我们可以看到任务成功完成,整个操作结果也是成功的。异步操作对于大型下载、软件包更新和其他可能需要长时间运行的任务特别有用。在你的 playbook 开发中,特别是在更复杂的基础设施中,你可能会发现它们很有用。

有了这个基础,让我们来看看另一个在大型基础设施中可能有用的高级技术——使用 Ansible 进行滚动更新。

控制滚动更新的 play 执行

默认情况下,Ansible 会在多个主机上并行执行任务,以加快大型清单中的自动化任务。这个设置由 Ansible 配置文件中的forks参数定义,默认值为5(因此,默认情况下,Ansible 尝试在五个主机上同时运行自动化任务)。

在负载均衡环境中,这并不理想,特别是如果你想避免停机时间。假设我们的清单中有五个前端服务器(或者甚至更少)。如果我们允许 Ansible 同时更新所有这些服务器,终端用户可能会遇到服务中断。因此,重要的是考虑在不同的时间更新所有服务器。让我们重用上一节中的清单,其中只有两个服务器。显然,如果这些服务器在负载均衡环境中,我们只能一次更新一个;如果同时取出服务,那么终端用户肯定会失去对服务的访问,直到 Ansible play 成功完成。

答案是在 play 定义中使用serial关键字来确定一次操作多少个主机。让我们通过一个实际的例子来演示这一点:

  1. 创建以下简单的 playbook,在我们的清单中的两个主机上运行两个命令。在这个阶段,命令的内容并不重要,但如果你使用command模块运行date命令,你将能够看到每个任务运行的时间,以及当你运行 play 时,如果指定了-v来增加详细信息:
---
- name: Simple serial demonstration play
  hosts: frontends
  gather_facts: false

  tasks:
    - name: First task
      command: date
    - name: Second task
      command: date
  1. 现在,如果你运行这个 play,你会发现它在每个主机上同时执行所有操作,因为我们的主机数量少于默认的 fork 数量——5。这种行为对于 Ansible 来说是正常的,但并不是我们想要的,因为我们的用户将会遇到服务中断:
$ ansible-playbook -i hosts serial.yml

PLAY [Simple serial demonstration play] ****************************************

TASK [First task] **************************************************************
changed: [frt02.example.com]
changed: [frt01.example.com]

TASK [Second task] *************************************************************
changed: [frt01.example.com]
changed: [frt02.example.com]

PLAY RECAP *********************************************************************
frt01.example.com : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt02.example.com : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
  1. 现在,让我们修改 play 定义,如下所示。我们将tasks部分保持与步骤 1中完全相同:
---
- name: Simple serial demonstration play
  hosts: frontends
  serial: 1
  gather_facts: false
  1. 注意serial: 1这一行的存在。这告诉 Ansible 在移动到下一个主机之前一次在 1 个主机上完成 play。如果我们再次运行 play,我们可以看到它的运行情况:
$ ansible-playbook -i hosts serial.yml

PLAY [Simple serial demonstration play] ****************************************

TASK [First task] **************************************************************
changed: [frt01.example.com]

TASK [Second task] *************************************************************
changed: [frt01.example.com]

PLAY [Simple serial demonstration play] ****************************************

TASK [First task] **************************************************************
changed: [frt02.example.com]

TASK [Second task] *************************************************************
changed: [frt02.example.com]

PLAY RECAP *********************************************************************
frt01.example.com : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt02.example.com : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

太好了!如果你想象一下,这个 playbook 实际上是在负载均衡器上禁用这些主机,执行升级,然后重新启用负载均衡器上的主机,这正是你希望操作进行的方式。如果没有serial: 1指令,所有主机将同时从负载均衡器中移除,导致服务中断。

值得注意的是,serial指令也可以取百分比而不是整数。当你指定一个百分比时,你告诉 Ansible 一次在百分之多少的主机上运行 play。所以,如果你的清单中有 4 个主机,并指定serial: 25%,Ansible 将一次只在一个主机上运行 play。如果你的清单中有 8 个主机,它将一次在两个主机上运行 play。我相信你明白了!

你甚至可以通过将列表传递给serial指令来构建这个。考虑以下代码:

  serial:
    - 1
    - 3
    - 5

这告诉 Ansible 一开始在 1 个主机上运行 play,然后在接下来的 3 个主机上运行,然后一次在 5 个主机上运行,直到清单完成。你也可以在整数主机数的位置指定一个百分比列表。通过这样做,你将建立一个强大的 playbook,可以执行滚动更新而不会导致终端用户服务中断。完成这一点后,让我们进一步通过控制 Ansible 在中止 play 之前可以容忍的最大失败百分比来增进这一知识,这在高可用或负载平衡环境中将会再次非常有用。

配置最大失败百分比

在其默认操作模式下,只要库存中有主机并且没有记录失败,Ansible 就会继续在一批服务器上执行播放(批处理大小由我们在前一节中讨论的serial指令确定)。显然,在高可用或负载平衡环境中(如我们之前讨论的环境),这并不理想。如果您的播放中有错误,或者可能存在代码问题,您最不希望的是 Ansible 忠实地将其部署到集群中的所有服务器,导致服务中断,因为所有节点都遭受了失败的升级。在这种环境中,最好的做法是尽早失败,并且在某人能够介入并解决问题之前至少保留一些主机不受影响。

对于我们的实际示例,让我们考虑一个扩展的库存,其中有10个主机。我们将定义如下:

[frontends]
frt[01:10].example.com

现在,让我们创建一个简单的 playbook 在这些主机上运行。我们将在 play 定义中将批处理大小设置为5,将max_fail_percentage设置为50%

  1. 创建以下 play 定义来演示max_fail_percentage指令的使用:
---
- name: A simple play to demonstrate use of max_fail_percentage
  hosts: frontends
  gather_facts: no
  serial: 5
  max_fail_percentage: 50

我们在库存中定义了10个主机,因此它将以 5 个一组(由serial: 5指定)进行处理。如果一批中超过 50%的主机失败,我们将失败整个播放并停止处理。

失败主机的数量必须超过max_fail_percentage的值;如果相等,则播放继续。因此,在我们的示例中,如果我们的主机正好有 50%失败,播放仍将继续。

  1. 接下来,我们将定义两个简单的任务。第一个任务下面有一个特殊的子句,我们用它来故意模拟一个失败——这一行以failed_when开头,我们用它告诉任务,如果它在批次中的前三个主机上运行此任务,那么无论结果如何,它都应该故意失败此任务;否则,它应该允许任务正常运行:
  tasks:
    - name: A task that will sometimes fail
      debug:
        msg: This might fail
      failed_when: inventory_hostname in ansible_play_batch[0:3]
  1. 最后,我们将添加一个始终成功的第二个任务。如果播放被允许继续,则运行此任务,但如果播放被中止,则不运行:
    - name: A task that will succeed
      debug:
        msg: Success!

因此,我们故意构建了一个将在 10 个主机库存中以 5 个主机一组的方式运行的 playbook,但是如果任何给定批次中超过 50%的主机经历失败,则播放将中止。我们还故意设置了一个失败条件,导致第一批 5 个主机中的三个(60%)失败。

  1. 运行 playbook,让我们观察发生了什么:
$ ansible-playbook -i morehosts maxfail.yml

PLAY [A simple play to demonstrate use of max_fail_percentage] *****************

TASK [A task that will sometimes fail] *****************************************
fatal: [frt01.example.com]: FAILED! => {
 "msg": "This might fail"
}
fatal: [frt02.example.com]: FAILED! => {
 "msg": "This might fail"
}
fatal: [frt03.example.com]: FAILED! => {
 "msg": "This might fail"
}
ok: [frt04.example.com] => {
 "msg": "This might fail"
}
ok: [frt05.example.com] => {
 "msg": "This might fail"
}

NO MORE HOSTS LEFT *************************************************************

NO MORE HOSTS LEFT *************************************************************

PLAY RECAP *********************************************************************
frt01.example.com : ok=0 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
frt02.example.com : ok=0 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
frt03.example.com : ok=0 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
frt04.example.com : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt05.example.com : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

注意此 playbook 的结果。我们故意使前 5 个批次中的三个失败,超过了我们设置的max_fail_percentage的阈值。这立即导致播放中止,并且第二个任务不会在前 5 个批次上执行。您还会注意到,10 个主机中的第二批次从未被处理,因此我们的播放确实被中止。这正是您希望看到的行为,以防止失败的更新在整个集群中传播。通过谨慎使用批处理和max_fail_percentage,您可以在整个集群上安全运行自动化任务,而不必担心在出现问题时破坏整个集群。在下一节中,我们将介绍 Ansible 的另一个功能,当涉及到与集群一起工作时,这个功能可能非常有用——任务委派。

设置任务执行委托

到目前为止,我们运行的每个 play 都假设所有任务都按顺序在库存中的每个主机上执行。但是,如果您需要在不同的主机上运行一个或两个任务怎么办?例如,我们已经谈到了在集群上自动升级的概念。但是从逻辑上讲,我们希望自动化整个过程,包括从负载平衡器中逐个删除每个主机并在任务完成后将其返回。

尽管我们仍然希望在整个清单上运行我们的 play,但我们肯定不希望从这些主机上运行负载均衡器命令。让我们再次通过一个实际示例详细解释这一点。我们将重用本章前面使用的两个简单主机清单:

[frontends]
frt01.example.com
frt02.example.com

现在,让我们在与我们的 playbook 相同的目录中创建两个简单的 shell 脚本来处理这个问题。这只是示例,因为设置负载均衡器超出了本书的范围。但是,请想象您有一个可以调用的 shell 脚本(或其他可执行文件),可以将主机添加到负载均衡器中并从中删除:

  1. 对于我们的示例,让我们创建一个名为remove_from_loadbalancer.sh的脚本,其中包含以下内容:
#!/bin/sh
echo Removing $1 from load balancer...
  1. 我们还将创建一个名为add_to_loadbalancer.sh的脚本,其中包含以下内容:
#!/bin/sh
echo Adding $1 to load balancer...

显然,在实际示例中,这些脚本中会有更多的代码!

  1. 现在,让我们创建一个 playbook,执行我们在这里概述的逻辑。我们首先创建一个非常简单的 play 定义(您可以自由地尝试serialmax_fail_percentage指令),以及一个初始任务:
---
- name: Play to demonstrate task delegation
  hosts: frontends

  tasks:
    - name: Remove host from the load balancer
      command: ./remove_from_loadbalancer.sh {{ inventory_hostname }}
      args:
        chdir: "{{ playbook_dir }}"
      delegate_to: localhost

请注意任务结构——大部分内容对您来说应该很熟悉。我们使用command模块调用我们之前创建的脚本,将从要从负载均衡器中移除的清单中的主机名传递给脚本。我们使用chdir参数和playbook_dir魔术变量告诉 Ansible 脚本要从与 playbook 相同的目录中运行。

这个任务的特殊之处在于delegate_to指令,它告诉 Ansible,即使我们正在遍历一个不包含localhost的清单,我们也应该在localhost上运行此操作(我们没有将脚本复制到远程主机,因此如果我们尝试从那里运行它,它将不会运行)。

  1. 之后,我们添加一个任务,其中进行升级工作。这个任务没有delegate_to指令,因此实际上是在清单中的远程主机上运行的(这是我们想要的):
    - name: Deploy code to host
      debug:
        msg: Deployment code would go here....
  1. 最后,我们使用我们之前创建的第二个脚本将主机重新添加到负载均衡器。这个任务几乎与第一个任务相同:
    - name: Add host back to the load balancer
      command: ./add_to_loadbalancer.sh {{ inventory_hostname }}
      args:
        chdir: "{{ playbook_dir }}"
      delegate_to: localhost
  1. 让我们看看这个 playbook 的运行情况:
$ ansible-playbook -i hosts delegate.yml

PLAY [Play to demonstrate task delegation] *************************************

TASK [Gathering Facts] *********************************************************
ok: [frt01.example.com]
ok: [frt02.example.com]

TASK [Remove host from the load balancer] **************************************
changed: [frt02.example.com -> localhost]
changed: [frt01.example.com -> localhost]

TASK [Deploy code to host] *****************************************************
ok: [frt01.example.com] => {
 "msg": "Deployment code would go here...."
}
ok: [frt02.example.com] => {
 "msg": "Deployment code would go here...."
}

TASK [Add host back to the load balancer] **************************************
changed: [frt01.example.com -> localhost]
changed: [frt02.example.com -> localhost]

PLAY RECAP *********************************************************************
frt01.example.com : ok=4 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt02.example.com : ok=4 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

请注意,即使 Ansible 正在通过清单(其中不包括localhost)工作,与负载均衡器相关的脚本实际上是从localhost运行的,而升级任务是直接在远程主机上执行的。当然,这并不是您可以使用任务委派的唯一方式,但这是一个常见的例子,说明了它可以帮助您的一种方式。

事实上,您可以将任何任务委派给localhost,甚至是另一个非清单主机。例如,您可以委派一个rsync命令给localhost,使用类似于前面的任务定义来将文件复制到远程主机。这很有用,因为尽管 Ansible 有一个copy模块,但它无法执行rsync能够执行的高级递归copyupdate功能。

另外,请注意,您可以选择在您的 playbooks(和 roles)中使用一种速记符号表示delegate_to,称为local_action。这允许您在单行上指定一个任务,该任务通常会在其下方添加delegate_to: localhost来运行。将所有这些内容整合到第二个示例中,我们的 playbook 将如下所示:

---
- name: Second task delegation example
  hosts: frontends

  tasks:
  - name: Perform an rsync from localhost to inventory hosts
    local_action: command rsync -a /tmp/ {{ inventory_hostname }}:/tmp/target/

上述速记符号等同于以下内容:

tasks:
  - name: Perform an rsync from localhost to inventory hosts
    command: rsync -a /tmp/ {{ inventory_hostname }}:/tmp/target/
    delegate_to: localhost

如果我们运行这个 playbook,我们可以看到local_action确实从localhost运行了rsync,使我们能够高效地将整个目录树复制到清单中的远程服务器上:

$ ansible-playbook -i hosts delegate2.yml

PLAY [Second task delegation example] ******************************************

TASK [Gathering Facts] *********************************************************
ok: [frt02.example.com]
ok: [frt01.example.com]

TASK [Perform an rsync from localhost to inventory hosts] **********************
changed: [frt02.example.com -> localhost]
changed: [frt01.example.com -> localhost]

PLAY RECAP *********************************************************************
frt01.example.com : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt02.example.com : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

这结束了我们对任务委派的讨论,尽管如前所述,这只是两个常见的例子。我相信您可以想出一些更高级的用例来使用这个功能。让我们继续通过在下一节中查看特殊的run_once选项来控制 Ansible 代码的流程。

使用run_once选项

在处理集群时,有时会遇到一项任务,应该只对整个集群执行一次。例如,你可能想要升级集群数据库的模式,或者发出一个重新配置 Pacemaker 集群的命令,这通常会在一个节点上发出,并由 Pacemaker 复制到所有其他节点。当然,你可以通过一个特殊的只有一个主机的清单来解决这个问题,或者甚至通过编写一个特殊的 play 来引用清单中的一个主机,但这是低效的,并且开始使你的代码变得分散。

相反,你可以像平常一样编写你的代码,但是利用特殊的run_once指令来运行你想要在你的清单上只运行一次的任务。例如,让我们重用本章前面定义的包含 10 个主机的清单。现在,让我们继续演示这个选项,如下所示:

  1. 按照下面的代码块创建简单的 playbook。我们使用一个 debug 语句来显示一些输出,但在现实生活中,你可以插入你的脚本或命令来执行你的一次性集群功能(例如,升级数据库模式):
---
- name: Play to demonstrate the run_once directive
  hosts: frontends

  tasks:
    - name: Upgrade database schema
      debug:
        msg: Upgrading database schema...
      run_once: true
  1. 现在,让我们运行这个 playbook,看看会发生什么:
$ ansible-playbook -i morehosts runonce.yml

PLAY [Play to demonstrate the run_once directive] ******************************

TASK [Gathering Facts] *********************************************************
ok: [frt02.example.com]
ok: [frt05.example.com]
ok: [frt03.example.com]
ok: [frt01.example.com]
ok: [frt04.example.com]
ok: [frt06.example.com]
ok: [frt08.example.com]
ok: [frt09.example.com]
ok: [frt07.example.com]
ok: [frt10.example.com]

TASK [Upgrade database schema] *************************************************
ok: [frt01.example.com] => {
 "msg": "Upgrading database schema..."
}
---

PLAY RECAP *********************************************************************
frt01.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt02.example.com : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt03.example.com : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt04.example.com : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt05.example.com : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt06.example.com : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt07.example.com : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt08.example.com : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt09.example.com : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt10.example.com : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

请注意,正如所期望的那样,尽管 playbook 在所有 10 个主机上运行了(并且确实从所有 10 个主机收集了信息),我们只在一个主机上运行了升级任务。

  1. 重要的是要注意,run_once选项适用于每批服务器,因此如果我们在我们的 play 定义中添加serial: 5(在我们的 10 台服务器清单上以 5 台服务器的两批运行我们的 play),模式升级任务实际上会运行两次!它按照要求运行一次,但是每批服务器运行一次,而不是整个清单运行一次。在处理这个指令时,要注意这个细微差别。

serial: 5添加到你的 play 定义中,并重新运行 playbook。输出应该如下所示:

$ ansible-playbook -i morehosts runonce.yml

PLAY [Play to demonstrate the run_once directive] ******************************

TASK [Gathering Facts] *********************************************************
ok: [frt04.example.com]
ok: [frt01.example.com]
ok: [frt02.example.com]
ok: [frt03.example.com]
ok: [frt05.example.com]

TASK [Upgrade database schema] *************************************************
ok: [frt01.example.com] => {
 "msg": "Upgrading database schema..."
}

PLAY [Play to demonstrate the run_once directive] ******************************

TASK [Gathering Facts] *********************************************************
ok: [frt08.example.com]
ok: [frt06.example.com]
ok: [frt07.example.com]
ok: [frt10.example.com]
ok: [frt09.example.com]

TASK [Upgrade database schema] *************************************************
ok: [frt06.example.com] => {
 "msg": "Upgrading database schema..."
}

PLAY RECAP *********************************************************************
frt01.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt02.example.com : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt03.example.com : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt04.example.com : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt05.example.com : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt06.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt07.example.com : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt08.example.com : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt09.example.com : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt10.example.com : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

这就是run_once选项设计的工作原理 - 你可以观察到,在前面的输出中,我们的模式升级运行了两次,这可能不是我们想要的!然而,有了这个意识,你应该能够利用这个选项来控制你的 playbook 流程跨集群,并且仍然实现你想要的结果。现在让我们远离与集群相关的 Ansible 任务,来看一下在本地运行 playbook 和在localhost上运行 playbook 之间微妙但重要的区别。

在本地运行 playbook

重要的是要注意,当我们谈论使用 Ansible 在本地运行 playbook 时,这并不等同于在localhost上运行它。如果我们在localhost上运行 playbook,Ansible 实际上会建立一个到localhost的 SSH 连接(它不区分其行为,也不尝试检测清单中的主机是本地还是远程 - 它只是忠实地尝试连接)。

实际上,我们可以尝试创建一个包含以下内容的local清单文件:

[local]
localhost

现在,如果我们尝试对这个清单运行一个 ad hoc 命令中的ping模块,我们会看到以下内容:

$ ansible -i localhosts -m ping all --ask-pass
The authenticity of host 'localhost (::1)' can't be established.
ECDSA key fingerprint is SHA256:DUwVxH+45432pSr9qsN8Av4l0KJJ+r5jTo123n3XGvZs.
ECDSA key fingerprint is MD5:78:d1:dc:23:cc:28:51:42:eb:fb:58:49:ab:92:b6:96.
Are you sure you want to continue connecting (yes/no)? yes
SSH password:
localhost | SUCCESS => {
 "ansible_facts": {
 "discovered_interpreter_python": "/usr/bin/python"
 },
 "changed": false,
 "ping": "pong"
}

正如你所看到的,Ansible 建立了一个需要主机密钥验证的 SSH 连接,以及我们的 SSH 密码。尽管你可以添加主机密钥(就像我们在前面的代码块中所做的那样),为你的localhost添加基于密钥的 SSH 身份验证等等,但有一种更直接的方法来做到这一点。

现在我们可以修改我们的清单,使其如下所示:

[local]
localhost ansible_connection=local

我们在localhost条目中添加了一个特殊的变量 - ansible_connection变量 - 它定义了用于连接到这个清单主机的协议。因此,我们告诉它使用直接的本地连接,而不是 SSH 连接(这是默认值)。

应该注意的是,ansible_connection变量的这个特殊值实际上覆盖了您在清单中放置的主机名。因此,如果我们将我们的清单更改为如下所示,Ansible 甚至不会尝试连接到名为frt01.example.com的远程主机,它将在本地连接到运行 playbook 的机器(不使用 SSH):

[local]
frt01.example.com ansible_connection=local

我们可以非常简单地演示这一点。让我们首先检查一下我们本地/tmp目录中是否缺少一个测试文件:

ls -l /tmp/foo
ls: cannot access /tmp/foo: No such file or directory

现在,让我们运行一个临时命令,在我们刚刚定义的新清单中的所有主机上触摸这个文件:

$ ansible -i localhosts2 -m file -a "path=/tmp/foo state=touch" all
frt01.example.com | CHANGED => {
 "ansible_facts": {
 "discovered_interpreter_python": "/usr/bin/python"
 },
 "changed": true,
 "dest": "/tmp/foo",
 "gid": 0,
 "group": "root",
 "mode": "0644",
 "owner": "root",
 "size": 0,
 "state": "file",
 "uid": 0
}

命令成功运行,现在让我们看看本地机器上是否存在测试文件:

$ ls -l /tmp/foo
-rw-r--r-- 1 root root 0 Apr 24 16:28 /tmp/foo

是的!因此,临时命令并没有尝试连接到frt01.example.com,即使这个主机名在清单中。ansible_connection=local的存在意味着这个命令在本地机器上运行,而不使用 SSH。

在本地运行命令而无需设置 SSH 连接、SSH 密钥等,这种能力可能非常有价值,特别是如果您需要在本地机器上快速启动和运行。完成后,让我们看看如何使用 Ansible 处理代理和跳转主机。

使用代理和跳转主机

通常,在配置核心网络设备时,这些设备通过代理或跳转主机与主网络隔离。Ansible 非常适合自动化网络设备配置,因为大部分操作都是通过 SSH 执行的:然而,这只在 Ansible 可以安装和从跳转主机操作的情况下才有帮助,或者更好的是可以通过这样的主机操作。

幸运的是,Ansible 确实可以做到这一点。假设您的网络中有两个 Cumulus Networks 交换机(这些基于 Linux 的特殊分发用于交换硬件,非常类似于 Debian)。这两个交换机分别具有cmls01.example.comcmls02.example.com的主机名,但都只能从名为bastion.example.com的主机访问。

支持我们的bastion主机的配置是在清单中进行的,而不是在 playbook 中。我们首先按照正常方式定义一个包含交换机的清单组:

[switches]
cmls01.example.com
cmls02.example.com

然而,现在我们可以开始变得聪明起来,通过在清单变量中添加一些特殊的 SSH 参数来实现。将以下代码添加到您的清单文件中:

[switches:vars]
ansible_ssh_common_args='-o ProxyCommand="ssh -W %h:%p -q bastion.example.com"'

这个特殊的变量内容告诉 Ansible 在设置 SSH 连接时添加额外的选项,包括通过bastion.example.com主机进行代理。-W %h:%p选项告诉 SSH 代理连接,并连接到由%h指定的主机(这通常是cmls01.example.comcmls02.example.com),在由%p指定的端口上(通常是端口22)。

现在,如果我们尝试对这个清单运行 Ansible 的ping模块,我们可以看到它是否有效:

$ ansible -i switches -m ping all
cmls02.example.com | SUCCESS => {
 "ansible_facts": {
 "discovered_interpreter_python": "/usr/bin/python"
 },
 "changed": false,
127.0.0.1 app02.example.com
 "ping": "pong"
}
cmls01.example.com | SUCCESS => {
 "ansible_facts": {
 "discovered_interpreter_python": "/usr/bin/python"
 },
 "changed": false,
 "ping": "pong"
}

您会注意到,我们实际上无法在命令行输出中看到 Ansible 行为上的任何差异。表面上,Ansible 的工作方式与平常一样,并且成功连接到了两个主机。然而,在幕后,它通过bastion.example.com进行代理。

请注意,这个简单的例子假设您使用相同的用户名和 SSH 凭据(或在这种情况下,密钥)连接到bastion主机和switches。有方法可以为这两个变量提供单独的凭据,但这涉及到更高级的 OpenSSH 使用,这超出了本书的范围。然而,本节旨在给您一个起点,并演示这种可能性,您可以自行探索 OpenSSH 代理。

现在让我们改变方向,探讨如何设置 Ansible 在 playbook 运行期间提示您输入数据的可能性。

配置 playbook 提示

到目前为止,我们所有的 playbook 都是在其中为它们指定的数据在运行时在 playbook 中定义的变量中。然而,如果您在 playbook 运行期间实际上想要从某人那里获取信息怎么办?也许您希望用户选择要安装的软件包的版本?或者,也许您希望从用户那里获取密码,以用于身份验证任务,而不将其存储在任何地方。(尽管 Ansible Value 可以加密静态数据,但一些公司可能禁止在他们尚未评估的工具中存储密码和其他凭据。)幸运的是,对于这些情况(以及许多其他情况),Ansible 可以提示您输入用户输入,并将输入存储在变量中以供将来处理。

让我们重用本章开头定义的两个主机前端清单。现在,让我们通过一个实际的例子演示如何在 playbook 运行期间从用户那里获取数据:

  1. 以通常的方式创建一个简单的 play 定义,如下所示:
---
- name: A simple play to demonstrate prompting in a playbook
  hosts: frontends
  1. 现在,我们将在 play 定义中添加一个特殊的部分。我们之前定义了一个vars部分,但这次我们将定义一个叫做vars_prompt的部分(它使您能够通过用户提示定义变量)。在这个部分,我们将提示两个变量——一个是用户 ID,一个是密码。一个将被回显到屏幕上,而另一个不会,通过设置private: yes
  vars_prompt:
    - name: loginid
      prompt: "Enter your username"
      private: no
    - name: password
      prompt: "Enter your password"
      private: yes
  1. 现在,我们将向我们的 playbook 添加一个任务,以演示设置变量的提示过程:
  tasks:
    - name: Proceed with login
      debug:
        msg: "Logging in as {{ loginid }}..."
  1. 现在,让我们运行 playbook 并看看它的行为:
$ ansible-playbook -i hosts prompt.yml
Enter your username: james
Enter your password:

PLAY [A simple play to demonstrate prompting in a playbook] ********************

TASK [Gathering Facts] *********************************************************
ok: [frt01.example.com]
ok: [frt02.example.com]

TASK [Proceed with login] ******************************************************
ok: [frt01.example.com] => {
 "msg": "Logging in as james..."
}
ok: [frt02.example.com] => {
 "msg": "Logging in as james..."
}

PLAY RECAP *********************************************************************
frt01.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt02.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

正如您所看到的,我们提示输入两个变量,但密码没有回显到终端,这对于安全原因很重要。然后我们可以在 playbook 中稍后使用这些变量。在这里,我们只是使用一个简单的debug命令来演示变量已经设置;然而,您可以在此处实现一个实际的身份验证功能,而不是这样做。

完成后,让我们继续下一节,看看如何通过标记有选择地运行 play 中的任务。

在 play 和任务中放置标记

在本书的许多部分,我们已经讨论过,随着您对 Ansible 的信心和经验的增长,您的 playbook 可能会在大小、规模和复杂性上增长。虽然这无疑是一件好事,但有时您可能只想运行 playbook 的一个子集,而不是从头到尾运行它。我们已经讨论了如何根据变量或事实的值有条件地运行任务,但是否有一种方法可以根据在运行 playbook 时做出的选择来运行它们?

Ansible play 中的标记是解决这个问题的方法,在本节中,我们将构建一个简单的 playbook,其中包含两个任务——每个任务都有一个不同的标记,以向您展示标记的工作原理。我们将使用之前使用过的两个简单主机清单:

  1. 创建以下简单的 playbook 来执行两个任务——一个是安装nginx包,另一个是从模板部署配置文件:
---
- name: Simple play to demonstrate use of tags
  hosts: frontends

  tasks:
    - name: Install nginx
      yum:
        name: nginx
        state: present
      tags:
        - install

    - name: Install nginx configuration from template
      template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx.conf
      tags:
        - customize
  1. 现在,让我们以通常的方式运行 playbook,但有一个区别——这一次,我们将在命令行中添加--tags开关。这个开关告诉 Ansible 只运行与指定标记匹配的任务。例如,运行以下命令:
$ ansible-playbook -i hosts tags.yml --tags install

PLAY [Simple play to demonstrate use of tags] **********************************

TASK [Gathering Facts] *********************************************************
ok: [frt02.example.com]
ok: [frt01.example.com]

TASK [Install nginx] ***********************************************************
changed: [frt02.example.com]
changed: [frt01.example.com]

PLAY RECAP *********************************************************************
frt01.example.com : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt02.example.com : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

请注意,部署配置文件的任务没有运行。这是因为它被标记为customize,而我们在运行 playbook 时没有指定这个标记。

  1. 还有一个--skip-tags开关,它与前一个开关相反——它告诉 Ansible 跳过列出的标记。因此,如果我们再次运行 playbook 但跳过customize标记,我们应该会看到类似以下的输出:
$ ansible-playbook -i hosts tags.yml --skip-tags customize

PLAY [Simple play to demonstrate use of tags] **********************************

TASK [Gathering Facts] *********************************************************
ok: [frt02.example.com]
ok: [frt01.example.com]

TASK [Install nginx] ***********************************************************
ok: [frt02.example.com]
ok: [frt01.example.com]

PLAY RECAP *********************************************************************
frt01.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt02.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

这个 play 运行是相同的,因为我们跳过了标记为customize的任务,而不是只包括install标记的任务。

请注意,如果您不指定--tags--skip-tags,则所有任务都会运行,而不管它们的标记如何。

关于标签的一些说明——首先,每个任务可以有多个标签,因此我们看到它们以 YAML 列表格式指定。如果使用--tags开关,如果任何一个标签与命令行上指定的标签匹配,任务将运行。其次,标签可以被重复使用,因此我们可以有五个被全部标记为install的任务,如果您通过--tags--skip-tags要求执行它们,所有五个任务都将被执行或跳过。

您还可以在命令行上指定多个标签,运行与任何指定标签匹配的所有任务。尽管标签背后的逻辑相对简单,但可能需要一点时间来适应它,而您最不希望做的事情就是在真实主机上运行 playbook,以检查您是否理解标签!了解这一点的一个很好的方法是在您的命令中添加--list-tasks,这样—而不是运行 playbook—会列出 playbook 中将要执行的任务。以下代码块为您提供了一些示例,基于我们刚刚创建的示例 playbook:

$ ansible-playbook -i hosts tags.yml --skip-tags customize --list-tasks

playbook: tags.yml

 play #1 (frontends): Simple play to demonstrate use of tags TAGS: []
 tasks:
 Install nginx TAGS: [install]

$ ansible-playbook -i hosts tags.yml --tags install,customize --list-tasks

playbook: tags.yml

 play #1 (frontends): Simple play to demonstrate use of tags TAGS: []
 tasks:
 Install nginx TAGS: [install]
 Install nginx configuration from template TAGS: [customize]

$ ansible-playbook -i hosts tags.yml --list-tasks

playbook: tags.yml

 play #1 (frontends): Simple play to demonstrate use of tags TAGS: []
 tasks:
 Install nginx TAGS: [install]
 Install nginx configuration from template TAGS: [customize]

正如您所看到的,--list-tasks不仅会显示哪些任务将运行,还会显示与它们关联的标签,这有助于您进一步理解标签的工作方式,并确保您实现了所需的 playbook 流程。标签是一种非常简单但强大的控制 playbook 中哪些部分运行的方法,通常在创建和维护大型 playbook 时,最好能够一次只运行所选部分的 playbook。从这里开始,我们将继续进行本章的最后一部分,我们将看看如何通过使用 Ansible Vault 对变量数据进行加密来保护您的静止状态下的变量数据。

使用 Ansible Vault 保护数据

Ansible Vault 是 Ansible 附带的一个工具,允许您在静止状态下加密敏感数据,同时在 playbook 中使用它。通常,需要将登录凭据或其他敏感数据存储在变量中,以允许 playbook 无人值守运行。然而,这会使您的数据面临被恶意使用的风险。幸运的是,Ansible Vault 使用 AES-256 加密在静止状态下保护您的数据,这意味着您的敏感数据不会被窥探。

让我们继续进行一个简单的示例,向您展示如何使用 Ansible Vault:

  1. 首先创建一个新的保险库来存储敏感数据;我们将称这个文件为secret.yml。您可以使用以下命令创建这个文件:
$ ansible-vault create secret.yml
New Vault password:
Confirm New Vault password:

在提示时输入您为保险库选择的密码,并通过第二次输入来确认它(本书在 GitHub 上附带的保险库是用secure密码加密的)。

  1. 当您输入密码后,您将被设置为您的正常编辑器(由EDITOR shell 变量定义)。在我的测试系统中,这是vi。在这个编辑器中,您应该以正常的方式创建一个vars文件,其中包含您的敏感数据:
---
secretdata: "Ansible is cool!"
  1. 保存并退出编辑器(在vi中按Esc,然后输入:wq)。您将退出到 shell。现在,如果您查看文件的内容,您会发现它们已经被加密,对于任何不应该能够读取文件的人来说是安全的:
$ cat secret.yml
$ANSIBLE_VAULT;1.1;AES256
63333734623764633865633237333166333634353334373862346334643631303163653931306138
6334356465396463643936323163323132373836336461370a343236386266313331653964326334
62363737663165336539633262366636383364343663396335643635623463626336643732613830
6139363035373736370a646661396464386364653935636366633663623261633538626230616630
35346465346430636463323838613037386636333334356265623964633763333532366561323266
3664613662643263383464643734633632383138363663323730
  1. 然而,Ansible Vault 的伟大之处在于,您可以在 playbook 中像使用普通的variables文件一样使用这个加密文件(尽管显然,您必须告诉 Ansible 您的保险库密码)。让我们创建一个简单的 playbook,如下所示:
---
- name: A play that makes use of an Ansible Vault
  hosts: frontends

  vars_files:
    - secret.yml

  tasks:
    - name: Tell me a secret
      debug:
        msg: "Your secret data is: {{ secretdata }}"

vars_files指令的使用方式与使用未加密的variables文件时完全相同。Ansible 在运行时读取variables文件的头,并确定它们是否已加密。

  1. 尝试在不告诉 Ansible 保险库密码的情况下运行 playbook——在这种情况下,您应该会收到类似于这样的错误:
$ ansible-playbook -i hosts vaultplaybook.yml
ERROR! Attempting to decrypt but no vault secrets found
  1. Ansible 正确地理解我们正在尝试加载使用ansible-vault加密的variables文件,但我们必须手动告诉它密码才能继续。有多种指定 vault 密码的方法(稍后会详细介绍),但为了简单起见,请尝试运行以下命令,并在提示时输入您的 vault 密码:
$ ansible-playbook -i hosts vaultplaybook.yml --ask-vault-pass
Vault password:

PLAY [A play that makes use of an Ansible Vault] *******************************

TASK [Gathering Facts] *********************************************************
ok: [frt01.example.com]
ok: [frt02.example.com]

TASK [Tell me a secret] ********************************************************
ok: [frt01.example.com] => {
 "msg": "Your secret data is: Ansible is cool!"
}
ok: [frt02.example.com] => {
 "msg": "Your secret data is: Ansible is cool!"
}

PLAY RECAP *********************************************************************
frt01.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt02.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

成功!Ansible 解密了我们的 vault 文件,并将变量加载到了 playbook 中,我们可以从我们创建的debug语句中看到。当然,这违背了使用 vault 的目的,但这是一个很好的例子。

这是使用 vault 的一个非常简单的例子。您可以指定密码的多种方式;您不必在命令行上提示输入密码-可以通过包含 vault 密码的纯文本文件提供,也可以通过脚本在运行时从安全位置获取密码(考虑一个动态清单脚本,只返回密码而不是主机名)。ansible-vault工具本身也可以用于编辑、查看和更改 vault 文件中的密码,甚至解密并将其转换回纯文本。Ansible Vault 的用户指南是获取更多信息的绝佳起点(docs.ansible.com/ansible/latest/user_guide/vault.html)。

需要注意的一点是,您实际上不必为敏感数据单独创建 vault 文件;您实际上可以将其内联包含在 playbook 中。例如,让我们尝试重新加密我们的敏感数据,以便包含在一个否则未加密的 playbook 中(再次使用secure密码作为 vault 的密码,如果您正在测试本书附带的 GitHub 存储库中的示例,请运行以下命令在您的 shell 中(它应该产生类似于所示的输出):

$ ansible-vault encrypt_string 'Ansible is cool!' --name secretdata
New Vault password:
Confirm New Vault password:
secretdata: !vault |
 $ANSIBLE_VAULT;1.1;AES256
 34393431303339353735656236656130336664666337363732376262343837663738393465623930
 3366623061306364643966666565316235313136633264310a623736643362663035373861343435
 62346264313638656363323835323833633264636561366339326332356430383734653030306637
 3736336533656230380a316364313831666463643534633530393337346164356634613065396434
 33316338336266636666353334643865363830346566666331303763643564323065
Encryption successful

您可以将此命令的输出复制并粘贴到 playbook 中。因此,如果我们修改我们之前的例子,它将如下所示:

---
- name: A play that makes use of an Ansible Vault
  hosts: frontends

  vars:
    secretdata: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          34393431303339353735656236656130336664666337363732376262343837663738393465623930
          3366623061306364643966666565316235313136633264310a623736643362663035373861343435
          62346264313638656363323835323833633264636561366339326332356430383734653030306637
          3736336533656230380a316364313831666463643534633530393337346164356634613065396434
          33316338336266636666353334643865363830346566666331303763643564323065

  tasks:
    - name: Tell me a secret
      debug:
        msg: "Your secret data is: {{ secretdata }}"

现在,当您以与之前完全相同的方式运行此 playbook(使用用户提示指定 vault 密码)时,您应该看到它的运行方式与我们使用外部加密的variables文件时一样:

$ ansible-playbook -i hosts inlinevaultplaybook.yml --ask-vault-pass
Vault password:

PLAY [A play that makes use of an Ansible Vault] *******************************

TASK [Gathering Facts] *********************************************************
ok: [frt02.example.com]
ok: [frt01.example.com]

TASK [Tell me a secret] ********************************************************
ok: [frt01.example.com] => {
 "msg": "Your secret data is: Ansible is cool!"
}
ok: [frt02.example.com] => {
 "msg": "Your secret data is: Ansible is cool!"
}

PLAY RECAP *********************************************************************
frt01.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
frt02.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

Ansible Vault 是一个强大而多功能的工具,可以在静止状态下加密您的敏感 playbook 数据,并且(只要小心)应该使您能够在不留下密码或其他敏感数据的情况下无人值守地运行大部分 playbook。这就结束了本节和本章;希望对您有所帮助。

总结

Ansible 具有许多高级功能,可以让您在各种场景中运行 playbook,无论是在受控方式下升级服务器集群,还是在安全的隔离网络上使用设备,或者通过提示和标记来控制 playbook 流程。Ansible 已被大量且不断增长的用户群体采用,因此它的设计和演变都是围绕解决现实世界问题而展开的。我们讨论的大多数 Ansible 的高级功能都是围绕着解决现实世界问题展开的。

在本章中,您学习了如何在 Ansible 中异步运行任务,然后了解了运行 playbook 升级集群的各种功能,例如在小批量清单主机上运行任务,如果一定比例的主机失败则提前失败,将任务委派给特定主机,甚至无论清单(或批量)大小如何都运行任务一次。您还学习了在本地运行 playbook 与在localhost上运行 playbook 之间的区别,以及如何使用 SSH 代理自动化在堡垒主机上的隔离网络上的任务。最后,您学习了如何处理敏感数据,而不是以未加密的形式存储它,可以通过在运行时提示用户或使用 Ansible Vault 来实现。您甚至学习了如何使用标记来运行 playbook 任务的子集。

在下一章中,我们将更详细地探讨我们在本章中简要提到的一个主题 - 使用 Ansible 自动化网络设备管理。

问题

  1. 哪个参数允许您配置在批处理中失败的最大主机数,然后播放被中止?

A)百分比

B)最大失败

C)最大失败百分比

D)最大百分比

E)失败百分比

  1. 真或假 - 您可以使用--connect=local参数在本地运行任何 playbooks 而不使用 SSH:

A)真

B)假

  1. 真或假 - 为了异步运行 playbook,您需要使用async关键字:

A)真

B)假

进一步阅读

如果您安装了 Passlib,这是 Python 2 和 3 的密码哈希库,vars_prompt将使用任何加密方案(如descryptmd5cryptsha56_crypt等)进行加密:

第三部分:在企业中使用 Ansible

在这一部分,我们将从实际角度看如何在企业环境中充分利用 Ansible。我们将首先看如何使用 Ansible 自动化网络设备,然后转向使用 Ansible 来管理云和容器环境。接着,我们将介绍一些更高级的测试和故障排除策略,这些策略将帮助您在企业中使用 Ansible,最后我们将介绍 Ansible Tower/AWX 产品,在企业环境中提供丰富的基于角色的访问控制(RBAC)和审计功能。

本节包括以下章节:

  • 第九章,使用 Ansible 进行网络自动化

  • 第十章,容器和云管理

  • 第十一章,故障排除和测试策略

  • 第十二章,开始使用 Ansible Tower

第九章:使用 Ansible 进行网络自动化

多年前,标准做法是手动配置每个网络设备。这主要是因为路由器和交换机主要是路由物理服务器的流量,因此每个网络设备上不需要太多的配置,而且变化缓慢。此外,只有人类才有足够的信息来设置网络。无论是规划还是执行,一切都非常手动。

虚拟化改变了这种范式,因为它导致了数千台机器连接到同一交换机或路由器,每台机器可能具有不同的网络需求。变化快速且频繁预期,并且随着虚拟基础架构的代码定义,人类管理员只需跟上基础设施的变化就成了全职工作。虚拟化编排平台对机器位置有更好的了解,甚至可以为我们生成清单,正如我们在前几章中看到的。实际上,没有办法让一个人类记住或管理现代大规模虚拟化基础设施。因此,很明显,自动化在配置网络基础设施时是必需的。

我们将在本章中学习更多关于这一点,以及我们可以做些什么来自动化我们的网络,内容包括以下主题:

  • 为什么要自动化网络管理?

  • Ansible 如何管理网络设备

  • 如何启用网络自动化

  • 可用的 Ansible 网络模块

  • 连接到网络设备

  • 网络设备的环境变量

  • 网络设备的自定义条件语句

让我们开始吧!

技术要求

本章假定您已经按照第一章 开始使用 Ansible中详细介绍的方式设置了控制主机,并且正在使用最新版本——本章的示例是使用 Ansible 2.9 进行测试的。本章还假定您至少有一个额外的主机进行测试,并且最好是基于 Linux 的。由于本章以网络设备为中心,我们理解并不是每个人都能够访问特定的网络设备进行测试(例如 Cisco 交换机)。在给出示例并且您可以访问这些设备的情况下,请随时探索这些示例。但是,如果您无法访问任何网络硬件,我们将使用免费提供的 Cumulus VX 进行示例演示,该软件提供了 Cumulus Networks 的交换环境的完整演示。尽管本章将给出特定主机名的示例,但您可以自由地用自己的主机名和/或 IP 地址替换它们。如何进行替换将在适当的位置提供详细信息。

本章的代码包在此处可用:github.com/PacktPublishing/Practical-Ansible-2/tree/master/Chapter%209

为什么要自动化网络管理?

在过去的 30 年里,我们设计数据中心的方式发生了根本性的变化。在 90 年代,典型的数据中心充满了具有非常特定目的的物理机器。在许多公司,服务器是根据机器的用途由不同的供应商购买的。这意味着需要机器、网络设备和存储设备,并且这些设备被购买、配置和交付。

这里的一个重大缺点是在确定需要机器和交付之间存在显著的滞后。在那段时间里,这是可以接受的,因为大多数公司的系统非常少,它们很少改变。此外,这种方法非常昂贵,因为许多设备被低效利用。

随着社会和科技公司的进步,我们知道今天,对于公司来说削减基础设施部署时间和成本变得非常重要。这为一个新的想法打开了道路:虚拟化。通过创建一个虚拟化集群,您不需要拥有正确尺寸的物理主机,因此您可以预先配置一些主机,将它们添加到资源池中,然后在虚拟化平台中创建合适尺寸的机器。这意味着当需要新的机器时,您可以通过几次点击创建它,并且几秒钟内就可以准备好。

这种转变还使企业能够从每个项目都部署具有独特数据中心要求的基础设施转变为一个可以由软件和配置定义行为的大型中央基础设施。这意味着一个单一的网络基础设施可以支持所有项目,而不管它们的规模如何。我们称之为虚拟数据中心基础设施,在这种基础设施中,我们尽可能地利用通用设计模式。这使得企业能够以大规模部署、切换和提供基础设施,以便通过简单地对其进行细分(例如创建虚拟服务器)来成功实施多种项目。

虚拟化带来的另一个重大优势是工作负载和物理主机的解耦。从历史上看,由于工作负载与物理主机绑定,如果主机死机,工作负载本身也会死机,除非在不同硬件上进行适当复制。虚拟化解决了这个问题,因为工作负载现在与一个或多个虚拟主机绑定,但这些虚拟主机可以自由地从一个物理主机移动到另一个物理主机。

快速配置机器的能力以及这些机器能够从一个主机移动到另一个主机的能力,导致了网络配置管理的问题。以前,人为地在安装新机器时调整配置细节是可以接受的,但现在,机器在主机之间移动(因此从一个物理交换机端口移动到另一个端口)而不需要任何人为干预。这意味着系统也需要更新网络配置。

与此同时,VLAN 在网络中占据了一席之地,这使得网络设备的利用得到了显著改善,从而优化了它们的成本。

今天,我们在更大的规模上工作,虚拟对象(机器、容器、函数等)在我们的数据中心中移动,完全由软件系统管理,人类参与的越来越少。

在这种环境中,自动化网络是他们成功的关键部分。

今天,有一些公司(著名的“云服务提供商”)在一个规模上工作,手动网络管理不仅不切实际,而且即使雇佣了大量网络工程师团队,也是不可能的。另一方面,有许多环境在技术上可能(至少部分地)手动管理网络配置,但仍然不切实际。

除了配置网络设备所需的时间之外,网络自动化最大的优势(从我的角度来看)是大大减少人为错误的机会。如果一个人必须在 100 台设备上配置 VLAN,很可能在这个过程中会出现一些错误。这是绝对正常的,但仍然是有问题的,因为这些配置将需要进行全面测试和修改。通常情况下,问题并不会止步于此,因为当设备损坏并因此需要更换时,人们必须以与旧设备相同的方式配置新设备。通常情况下,随着时间的推移,配置会发生变化,而且很多时候没有明确的方法来追踪这一点,因此在更换有故障的网络设备时,可能会出现一些在旧设备中存在但在新设备中不存在的规则问题。

既然我们已经讨论了自动化网络管理的必要性,让我们看看如何使用 Ansible 管理网络设备。

学习 Ansible 如何管理网络设备

Ansible 允许您管理许多不同的网络设备,包括 Arista EOS、Cisco ASA、Cisco IOS、Cisco IOS XR、Cisco NX-OS、Dell OS 6、Dell OS 9、Dell OS 10、Extreme EXOS、Extreme IronWare、Extreme NOS、Extreme SLX-OS、Extreme VOSS、F5 BIG-IP、F5 BIG-IQ、Junos OS、Lenovo CNOS、Lenovo ENOS、MikroTik RouterOS、Nokia SR OS、Pluribus Netvisor、VyOS 和支持 NETCONF 的 OS。正如您可以想象的那样,我们可以通过各种方式让 Ansible 与它们进行通信。

此外,我们必须记住,Ansible 网络模块在控制器主机上运行(即您发出ansible命令的主机),而通常,Ansible 模块在目标主机上运行。这种差异很重要,因为它允许 Ansible 根据目标设备类型使用不同的连接机制。请记住,即使您的主机具有 SSH 管理功能(许多交换机都有),由于 Ansible 在目标主机上运行其模块,因此需要目标主机安装 Python。大多数交换机(和嵌入式硬件)缺乏 Python 环境,因此我们必须使用其他连接协议。Ansible 支持的用于网络设备管理的关键协议在此处给出。

Ansible 使用以下五种主要连接类型连接这些网络设备:

  • network_cli

  • netconf

  • httpapi

  • local

  • ssh

当您与网络设备建立连接时,您需要根据设备支持的连接机制和您的需求选择连接机制:

  • network_cli 得到大多数模块的支持,它与 Ansible 通常使用非网络模块的方式最相似。这种模式通过 SSH 使用 CLI。该协议在配置开始时创建持久连接,并在整个任务的持续时间内保持连接,因此您不必为每个后续任务提供凭据。

  • netconf 受到很少模块的支持(在撰写本文时,这些模块只是支持 NETCONF 和 Junos OS 的操作系统)。这种模式通过 SSH 使用 XML,因此基本上它将基于 XML 的配置应用到设备上。该协议在配置开始时创建持久连接,并在整个任务的持续时间内保持连接,因此您不必为每个后续任务提供凭据。

  • httpapi 受到少量模块的支持(在撰写本文时,这些模块是 Arista EOS、Cisco NX-OS 和 Extreme EXOS)。这种模式使用设备发布的 HTTP API。该协议在配置开始时创建持久连接,并在整个任务的持续时间内保持连接,因此您不必为每个后续任务提供凭据。

  • Local被大多数设备支持,但是它是一个已弃用的模式。这基本上是一个依赖于供应商的连接模式,可能需要使用一些供应商包。这种模式不会创建持久连接,因此在每个任务开始时,你都需要传递凭据。在可能的情况下,避免使用这种模式。

  • 在本节中,ssh 不能被忘记。虽然许多设备依赖于此处列出的连接模式,但正在创建一种新型设备,它们在白盒交换机硬件上原生运行 Linux。Cumulus Networks 就是一个这样的例子,由于软件是基于 Linux 的,所有配置都可以通过 SSH 进行,就好像交换机实际上只是另一台 Linux 服务器一样。

了解 Ansible 如何连接和与你的网络硬件通信是很重要的,因为它能让你理解你构建 Ansible playbooks 和调试问题时所需的知识。在本节中,我们介绍了在处理网络硬件时会遇到的通信协议。在下一节中,我们将在此基础上继续,看看如何使用 Ansible 开始我们的网络自动化之旅的基础知识。

启用网络自动化

在你使用 Ansible 进行网络自动化之前,你需要确保你拥有一切所需的东西。

根据我们将要使用的连接方法的不同,我们需要不同的依赖。举例来说,我们将使用具有network_cli连接性的 Cisco IOS 设备。

Ansible 网络自动化的唯一要求如下:

  • Ansible 2.5+

  • 与网络设备的正确连接

首先,我们需要检查 Ansible 的版本:

  1. 确保你有一个最新的 Ansible 版本,你可以运行以下命令:
$ ansible --version

这将告诉你你的 Ansible 安装的版本。

  1. 如果是 2.5 或更高版本,你可以发出以下命令(带有适当的选项)来检查网络设备的连接:
$ ansible all -i n1.example.com, -c network_cli -u my_user -k -m ios_facts -e ansible_network_os=ios all

这应该返回你设备的事实,证明我们能够连接。对于任何其他目标,Ansible 都能够检索事实,这通常是 Ansible 与目标交互时的第一步。

这是一个关键步骤,因为这使得 Ansible 能够了解设备的当前状态,从而采取适当的行动。

通过在目标设备上运行ios_facts模块,我们只是执行了这个第一个标准步骤(因此不会对设备本身或其配置进行任何更改),但这将确认 Ansible 能够连接到设备并对其执行命令。

显然,只有当你有权限访问运行 Cisco IOS 的网络设备时,你才能运行前面的命令并探索其行为。我们知道并非每个人都有相同的网络设备可用于测试(或者根本没有!)。幸运的是,一种新型交换机正在出现——“白盒”交换机。这些交换机由各种制造商制造,基于标准化硬件,你可以在上面安装自己的网络操作系统。Cumulus Linux 就是这样一种操作系统,它的一个免费测试版本叫做 Cumulus VX,可以供你下载。

在撰写本文时,Cumulus VX 的下载链接是cumulusnetworks.com/products/cumulus-vx/。你需要注册才能下载,但这样做可以让你免费访问开放网络的世界。

只需下载适合你的 hypervisor(例如 VirtualBox)的镜像,然后像运行任何其他 Linux 虚拟机一样运行它。完成后,你可以连接到 Cumulus VX 交换机,就像连接到任何其他 SSH 设备一样。例如,要运行一个关于所有交换机端口接口的事实的临时命令(在 Cumulus VX 上被枚举为swp1swp2swpX),你可以运行以下命令:

$ ansible -i vx01.example.com, -u cumulus -m setup -a 'filter=ansible_swp*' all --ask-pass

如果成功,这应该会导致关于 Cumulus VX 虚拟交换机的交换机端口接口的大量信息。在我的测试系统上,此输出的第一部分如下所示:

vx01.example.com | SUCCESS => {
 "ansible_facts": {
 "ansible_swp1": {
 "active": false,
 "device": "swp1",
 "features": {
 "esp_hw_offload": "off [fixed]",
 "esp_tx_csum_hw_offload": "off [fixed]",
 "fcoe_mtu": "off [fixed]",
 "generic_receive_offload": "on",
 "generic_segmentation_offload": "on",
 "highdma": "off [fixed]",
...

正如您所看到的,使用诸如 Cumulus Linux 之类的操作系统来使用白盒交换机的优势在于,您可以使用标准的 SSH 协议进行连接,甚至可以使用内置的setup模块来收集有关它的信息。使用其他专有硬件并不更加困难,但只是需要指定更多的参数,就像我们在本章前面展示的那样。

现在您已经了解了启用网络自动化的基本知识,让我们学习如何在 Ansible 中发现适合我们所需自动化任务的适当网络模块。

审查可用的 Ansible 网络模块

目前,有超过 20 种不同的网络平台上的数千个模块。让我们学习如何找到对您更相关的模块:

  1. 首先,您需要知道您有哪种设备类型以及 Ansible 如何调用它。在docs.ansible.com/ansible/latest/network/user_guide/platform_index.html页面上,您可以找到 Ansible 支持的不同设备类型以及它们的指定方式。在我们的示例中,我们将以 Cisco IOS 为例。

  2. docs.ansible.com/ansible/latest/modules/list_of_network_modules.html页面上,您可以搜索专门针对您所需交换机家族的类别,并且您将能够看到所有可以使用的模块。

模块列表对于我们来说太大且特定于家族,无法对其进行深入讨论。每个版本都会有数百个新的模块添加,因此此列表每次发布都会变得更大。

如果您熟悉如何以手动方式配置设备,您将很快发现模块的名称相当自然,因此您将很容易理解它们的功能。但是,让我们从 Cisco IOS 模块集合中挑选出一些例子,具体参考docs.ansible.com/ansible/latest/modules/list_of_network_modules.html#ios

  • ios_banner:顾名思义,此模块将允许您微调和修改登录横幅(在许多系统中称为motd)。

  • ios_bgp:此模块允许您配置 BGP 路由。

  • ios_command:这是 Ansible command模块的 IOS 等效模块,它允许您执行许多不同的命令。就像command模块一样,这是一个非常强大的模块,但最好使用特定的模块来执行我们要执行的操作,如果它们可用的话。

  • ios_config:此模块允许我们对设备的配置文件进行几乎任何更改。就像ios_command模块一样,这是一个非常强大的模块,但最好使用特定的模块来执行我们要执行的操作,如果它们可用的话。如果使用了缩写命令,则此模块的幂等性将无法保证。

  • ios_vlan:此模块允许配置 VLAN。

这只是一些例子,但对于 Cisco IOS 还有许多其他模块(在撰写本文时有 27 个),如果您找不到执行所需操作的特定模块,您总是可以退而使用ios_commandios_config,由于它们的灵活性,将允许您执行任何您能想到的操作。

相比之下,如果你正在使用 Cumulus Linux 交换机,你会发现只有一个模块 - nclu(参见docs.ansible.com/ansible/latest/modules/list_of_network_modules.html#cumulus)。这反映了在 Cumulus Linux 中所有配置工作都是用这个命令处理的事实。如果你需要自定义每日消息或 Linux 操作系统的其他方面,你可以以正常方式进行(例如,使用我们在本书中之前演示过的templatecopy模块)。

与往常一样,Ansible 文档是你的朋友,当你学习如何在新类设备上自动化命令时,它应该是你的首要选择。在本节中,我们演示了一个简单的过程,用于查找适用于你的网络设备类别的 Ansible 模块,以 Cisco 作为具体示例(尽管你可以将这些原则应用于任何其他设备)。现在,让我们看看 Ansible 如何连接到网络设备。

连接到网络设备

正如我们所看到的,Ansible 网络中有一些特殊之处,因此需要特定的配置。

为了使用 Ansible 管理网络设备,你至少需要一个设备进行测试。假设我们有一个 Cisco IOS 系统可供使用。可以肯定的是,不是每个人都有这样的设备进行测试,因此以下内容仅作为假设示例提供。

根据docs.ansible.com/ansible/latest/network/user_guide/platform_index.html页面,我们可以看到这个设备的正确ansible_network_osios,我们可以使用network_clilocal连接到它。由于local已经被弃用,我们将使用network_cli。按照以下步骤配置 Ansible,以便你可以管理 IOS 设备:

  1. 首先,让我们创建包含我们设备的清单文件在routers组中:
[routers]
n1.example.com
n2.example.com

[cumulusvx]
vx01.example.com
  1. 要知道要使用哪些连接参数,我们将设置 Ansible 的特殊连接变量,以便它们定义连接参数。我们将在 playbook 的组变量子目录中进行这些操作,因此我们需要创建包含以下内容的group_vars/routers.yml文件:
---
ansible_connection: network_cli
ansible_network_os: ios
ansible_become: True
ansible_become_method: enable

凭借这些特殊的 Ansible 变量,它将知道如何连接到你的设备。我们在本书的前面已经涵盖了一些这些示例,但作为一个回顾,Ansible 使用这些变量的值来确定其行为的方式如下:

  • ansible_connection:这个变量被 Ansible 用来决定如何连接到设备。通过选择network_cli,我们指示 Ansible 以 SSH 模式连接到 CLI,就像我们在前一段讨论的那样。

  • ansible_network_os:这个变量被 Ansible 用来理解我们将要使用的设备的设备系列。通过选择ios,我们指示 Ansible 期望一个 Cisco IOS 设备。

  • ansible_become:这个变量被 Ansible 用来决定是否在设备上执行特权升级。通过指定True,我们告诉 Ansible 执行特权升级。

  • ansible_become_method:在各种设备上执行特权升级有许多不同的方法(通常在 Linux 服务器上是sudo - 这是默认设置),对于 Cisco IOS,我们必须将其设置为enable

通过这些步骤,你已经学会了连接到网络设备的必要步骤。

为了验证连接是否按预期工作(假设你可以访问运行 Cisco IOS 的路由器),你可以运行这个简单的 playbook,名为ios_facts.yaml

---
- name: Play to return facts from a Cisco IOS device
  hosts: routers
  gather_facts: False
  tasks:
    - name: Gather IOS facts
      ios_facts:
        gather_subset: all

你可以使用以下命令来运行这个过程:

$ ansible-playbook -i hosts ios_facts.yml --ask-pass

如果它成功返回,这意味着你的配置是正确的,并且你已经能够给予 Ansible 管理你的 IOS 设备所需的授权。

同样,如果您想连接到 Cumulus VX 设备,您可以添加另一个名为group_vars/cumulusvx.yml的组变量文件,其中包含以下代码:

---
ansible_user: cumulus
become: false

一个类似的 playbook,返回有关我们的 Cumulus VX 交换机的所有信息,可能如下所示:

---
- name: Simply play to gather Cumulus VX switch facts
  hosts: cumulusvx
  gather_facts: no

  tasks:
    - name: Gather facts
      setup:
        gather_subset: all

您可以通过使用以下命令以正常方式运行:

$ ansible-playbook -i hosts cumulusvx_facts.yml --ask-pass

如果成功,您应该会从您的 playbook 运行中看到以下输出:

SSH password:

PLAY [Simply play to gather Cumulus VX switch facts] ************************************************************************************************

TASK [Gather facts] ************************************************************************************************
ok: [vx01.example.com]

PLAY RECAP ************************************************************************************************
vx01.example.com : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

这演示了连接到 Ansible 中两种不同类型的网络设备的技术,其中一种您可以自行测试,而无需访问任何特殊硬件。现在,让我们通过查看如何为 Ansible 中的网络设备设置环境变量来进一步学习。

网络设备的环境变量

网络的复杂性很高,网络系统也非常多样化。因此,Ansible 拥有大量的变量,可以帮助您调整它,使 Ansible 适应您的环境。

假设您有两个不同的网络(即一个用于计算,一个用于网络设备),它们不能直接通信,但必须通过堡垒主机才能从一个网络到达另一个网络。由于我们在计算网络中使用了 Ansible,我们需要通过堡垒主机跳转网络,以配置管理网络中的 IOS 路由器。此外,我们的目标交换机需要代理才能访问互联网。

要连接到数据库网络中的 IOS 路由器,我们需要为我们的网络设备创建一个新的组,这些设备位于一个单独的网络上。例如,对于这个例子,可能会指定如下:

[bastion_routers]
n1.example.com
n2.example.com

[bastion_cumulusvx]
vx01.example.com

创建了更新后的清单后,我们可以创建一个新的组变量文件,例如group_vars/bastion_routers.yaml,其中包含以下内容:

---
ansible_connection: network_cli
ansible_network_os: ios
ansible_become: True
ansible_become_method: enable
ansible_ssh_common_args: '-o ProxyCommand="ssh -W %h:%p -q bastion.example.com"'
proxy_env:
    http_proxy: http://proxy.example.com:8080

如果我们的 Cumulus VX 交换机位于堡垒服务器后面,我们也可以创建一个group_vars/bastion_cumulusvx.yml文件来实现相同的效果:

---
ansible_user: cumulus
ansible_become: false
ansible_ssh_common_args: '-o ProxyCommand="ssh -W %h:%p -q bastion.example.com"'
proxy_env:
    http_proxy: http://proxy.example.com:8080

除了我们在上一节中讨论的选项之外,我们现在还有两个额外的选项:

  • ansible_ssh_common_args:这是一个非常强大的选项,允许我们向 SSH 连接添加额外的选项,以便我们可以调整它们的行为。这些选项应该相当容易识别,因为您已经在您的 SSH 配置中使用它们,只需简单地 SSH 到目标机器。在这种特定情况下,我们正在添加一个ProxyCommand,这是执行跳转到主机(通常是堡垒主机)的 SSH 指令,以便我们可以安全地进入目标主机。

  • http_proxy:这个选项位于proxy_env选项下面,在网络隔离很强的环境中非常关键,因此您的机器除非使用代理,否则无法与互联网进行交互。

假设您已经设置了无密码(例如基于 SSH 密钥的)访问到您的堡垒主机,您应该能够对您的 Cumulus VX 主机运行一个临时的 Ansible ping命令,如下所示:

$ ansible -i hosts -m ping -u cumulus --ask-pass bastion_cumulusvx
SSH password:

vx01.example.com | SUCCESS => {
 "ansible_facts": {
 "discovered_interpreter_python": "/usr/bin/python"
 },
 "changed": false,
 "ping": "pong"
}

请注意,堡垒服务器的使用变得透明 - 您可以继续使用 Ansible 进行自动化,就好像您在同一个平面网络上一样。如果您可以访问基于 Cisco IOS 的设备,您也应该能够对bastion_routers组运行类似的命令,并取得类似的积极结果。现在您已经学会了为网络设备设置环境变量的必要步骤,以及如何使用 Ansible 访问它们,即使它们在隔离的网络中,让我们学习如何为网络设备设置条件语句。

网络设备的条件语句

尽管没有特定于网络的 Ansible 条件语句,但在与网络相关的 Ansible 使用中,条件语句是相当常见的。

在网络中,启用和禁用端口是很常见的。要使数据通过电缆,电缆两端的端口都应该启用,并且结果为“连接”状态(一些供应商可能会使用不同的名称,但概念是相同的)。

假设我们有两个 Arista Networks EOS 设备,并且我们在端口上发出了 ON 状态,并且需要等待连接建立后再继续。

要等待Ethernet4接口启用,我们需要在我们的 playbook 中添加以下任务:

- name: Wait for interface to be enabled
  eos_command:
      commands:
          - show interface Ethernet4 | json
      wait_for:
          - "result[0].interfaces.Ethernet4.interfaceStatus  eq  connected"

eos_command是允许我们向 Arista Networks EOS 设备发出自由形式命令的模块。命令本身需要在commands选项中指定为数组。通过wait_for选项,我们可以指定一个条件,Ansible 将在指定任务上重复,直到条件满足。由于命令的输出被重定向到json实用程序,输出将是一个 JSON,因此我们可以使用 Ansible 操作 JSON 数据的能力来遍历其结构。

我们可以在 Cumulus VX 上实现类似的结果——例如,我们可以查询从交换机收集的事实,看看端口swp2是否已启用。如果没有启用,那么我们将启用它;但是,如果已启用,我们将跳过该命令。我们可以通过一个简单的 playbook 来实现这一点,如下所示:

---
- name: Simple play to demonstrate conditional on Cumulus Linux
  hosts: cumulusvx

  tasks:
    - name: Enable swp2 if it is disabled
      nclu:
        commands:
          - add int swp2
        commit: yes
      when: ansible_swp2.active == false

注意我们任务中when子句的使用,意味着我们只应在swp2不活动时发出配置指令。如果我们在未配置的 Cumulus Linux 交换机上第一次运行此 playbook,我们应该看到类似以下的输出:

PLAY [Simple play to demonstrate conditional on Cumulus Linux] ***************************************************************

TASK [Gathering Facts] 
***************************************************************
ok: [vx01.example.com]

TASK [Enable swp2 if it is disabled] ***************************************************************
changed: [vx01.example.com]

PLAY RECAP 
***************************************************************
vx01.example.com : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

正如我们所看到的,nclu模块将我们的更改提交到了交换机配置中。然而,如果我们再次运行 playbook,输出应该更像这样:

PLAY [Simple play to demonstrate conditional on Cumulus Linux] ***************************************************************

TASK [Gathering Facts] 
***************************************************************
ok: [vx01.example.com]

TASK [Enable swp2 if it is disabled] ***************************************************************
skipping: [vx01.example.com]

PLAY RECAP
***************************************************************
vx01.example.com : ok=1 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0

这一次,任务被跳过了,因为 Ansible 事实显示端口swp2已经启用。这显然是一个非常简单的例子,但它展示了你可以如何在网络设备上使用条件语句,这与你之前在本书中已经看到的在 Linux 服务器上使用条件语句的方式非常相似。

这就结束了我们对使用 Ansible 进行网络设备自动化的简要介绍——更深入的工作需要查看网络配置,并需要更多的硬件,因此这超出了本书的范围。然而,我希望这些信息向你展示了 Ansible 可以有效地用于自动化和配置各种网络设备。

摘要

快速变化的现代大规模基础设施需要自动化网络任务。幸运的是,Ansible 支持各种网络设备,从专有硬件,如基于 Cisco IOS 的设备,到运行操作系统如 Cumulus Linux 的白盒交换机等开放标准。当涉及管理网络配置时,Ansible 是一个强大而有支持性的工具,它允许你快速而安全地实施变更。你甚至可以在网络中替换整个设备,并且有信心能够通过你的 Ansible playbook 在新设备上放置正确的配置。

在本章中,你了解了自动化网络管理的原因。然后,你看了 Ansible 如何管理网络设备,如何在 Ansible 中启用网络自动化,以及如何找到执行你希望完成的自动化任务所需的 Ansible 模块。然后,通过实际示例,你学会了如何连接到网络设备,如何设置环境变量(并通过堡垒主机连接到隔离网络),以及如何对 Ansible 任务应用条件语句以配置网络设备。

在下一章中,我们将学习如何使用 Ansible 管理 Linux 容器和云基础设施。

问题

  1. 以下哪个不是 Ansible 用于连接这些网络设备的四种主要连接类型之一?

A) netconf

B) network_cli

C) local

D) netstat

E) httpapi

  1. 真或假:ansible_network_os变量被 Ansible 用来理解我们将要使用的设备的设备系列。

A) True

B) False

  1. 真或假:为了连接到一个独立网络中的 IOS 路由器,您需要指定主机的特殊连接变量,可能作为清单组变量。

A) 正确

B) 错误

进一步阅读

第十章:容器和云管理

Ansible 是一个非常灵活的自动化工具,可以轻松用于自动化基础架构的任何方面。在过去几年中,基于容器的工作负载和云工作负载变得越来越受欢迎,因此我们将看看如何使用 Ansible 自动化与这些工作负载相关的任务。在本章中,我们将首先使用 Ansible 设计和构建容器。然后,我们将看看如何运行这些容器,最后,我们将看看如何使用 Ansible 管理各种云平台。

具体来说,本章将涵盖以下主题:

  • 使用 playbooks 设计和构建容器

  • 管理多个容器平台

  • 使用 Ansible 自动化 Docker

  • 探索面向容器的模块

  • 针对亚马逊网络服务的自动化

  • 通过自动化来补充谷歌云平台

  • 与 Azure 的无缝自动化集成

  • 通过 Rackspace Cloud 扩展您的环境

  • 使用 Ansible 编排 OpenStack

让我们开始吧!

技术要求

本章假设您已经按照第一章 开始使用 Ansible中详细说明的方式设置了 Ansible 的控制主机,并且正在使用最新版本——本章的示例是在 Ansible 2.9 版本下测试的。尽管本章将给出特定的主机名示例,但您可以自由地用您自己的主机名和/或 IP 地址替换它们。如何做到这一点的细节将在适当的地方提供。本章还假设您可以访问 Docker 主机,尽管在大多数操作系统上都可以安装 Docker,但本章提供的所有命令都是针对 GNU/Linux 的,并且仅在该平台上进行了测试。

本章中的所有示例都可以在本书的 GitHub 存储库中找到:github.com/PacktPublishing/Practical-Ansible-2/tree/master/Chapter%2010

使用 playbooks 设计和构建容器

使用 Dockerfile 构建容器可能是最常见的方法,但这并不意味着这是最好的方法。

首先,即使您在自动化路径上处于非常良好的位置,并且为您的基础架构编写了许多 Ansible 角色,您也无法在 Dockerfile 中利用它们,因此您最终会复制您的工作来创建容器。除了需要花费时间和需要学习一种新语言之外,公司很少能够在一夜之间放弃他们的基础架构并转向容器。这意味着您需要保持两个相同的自动化部分处于活动状态并保持最新,从而使自己处于犯错误和环境之间不一致行为的位置。

如果这还不够成问题,当您开始考虑云环境时,情况很快就会恶化。所有云环境都有自己的控制平面和本地自动化语言,因此在很短的时间内,您会发现自己一遍又一遍地重写相同操作的自动化,从而浪费时间并破坏环境的一致性。

Ansible 提供了ansible-container,以便您可以使用与创建机器相同的组件来创建容器。您应该做的第一件事是确保您已经安装了ansible-container。有几种安装它的方法,但最简单的方法是使用pip。为此,您可以运行以下命令:

$ sudo pip install ansible-container[docker,k8s]

ansible-container工具在撰写本文时带有三个支持的引擎:

  • docker:如果您想要在 Docker 引擎(即在您的本地机器上)中使用它,就需要它。

  • k8s:如果您想要在 Kubernetes 集群中使用它,无论是本地(即 MiniKube)还是远程(即生产集群)都需要它。

  • openshift:如果你想要在 OpenShift 集群中使用它,无论是本地(即 MiniShift)还是远程(即生产集群)。

按照以下步骤使用 playbooks 构建容器:

  1. 发出ansible-container init命令将给我们以下输出:
$ ansible-container init
Ansible Container initialized.

运行这个命令还将创建以下文件:

    • ansible.cfg:一个空文件,用于(最终)用于覆盖 Ansible 系统配置
  • ansible-requirements.txt:一个空文件,用于(最终)列出构建过程中容器的 Python 要求

  • container.yml:一个包含构建的 Ansible 代码的文件

  • meta.yml:一个包含 Ansible Galaxy 元数据的文件

  • requirements.yml:一个空文件,用于(最终)列出构建所需的 Ansible 角色

  1. 让我们尝试使用这个工具构建我们自己的容器-用以下内容替换container.yml的内容:
version: "2"
settings:
  conductor:
    base: centos:7
  project_name: http-server
services:
  web:
    from: "centos:7"
    roles:
      - geerlingguy.apache
    ports:
      - "80:80"
    command:
      - "/usr/bin/dumb-init"
      - "/usr/sbin/apache2ctl"
      - "-D"
      - "FOREGROUND"
    dev_overrides:
      environment:
        - "DEBUG=1"

现在我们可以运行ansible-container build来启动构建。

在构建过程结束时,我们将得到一个应用了geerlingguy.apache角色的容器。ansible-container工具执行多阶段构建能力,启动一个用于构建真实容器的 Ansible 容器。

如果我们指定了多个要应用的角色,输出将是一个具有更多层的图像,因为 Ansible 将为每个指定的角色创建一个层。这样,容器可以很容易地使用现有的 Ansible 角色构建,而不是 Dockerfiles。

现在你已经学会了如何使用 playbooks 设计和构建容器,接下来你将学习如何管理多个容器平台。

管理多个容器平台

在今天的世界中,仅仅能够运行一个镜像并不被认为是一个可以投入生产的设置。

要能够称呼一个部署为“投入生产使用”,你需要能够证明你的应用程序提供的服务将在合理的情况下运行,即使是在单个应用程序崩溃或硬件故障的情况下。通常,你的客户会有更多的可靠性约束。

幸运的是,你的软件并不是唯一具有这些要求的数据,因此为此目的开发了编排解决方案。

今天,最成功的一个是 Kubernetes,因为它有各种不同的发行版/版本,所以我们将主要关注它。

Kubernetes 的理念是,你告诉 Kubernetes 控制平面你想要 X 个实例的 Y 应用程序,Kubernetes 将计算运行在 Kubernetes 节点上的 Y 应用程序实例数量,以确保实例数量为 X。如果实例太少,Kubernetes 将负责启动更多实例,而如果实例太多,多余的实例将被停止。

由于 Kubernetes 不断检查所请求的实例数量是否在运行中,所以在应用程序失败或节点失败的情况下,Kubernetes 将重新启动丢失的实例。

由于安装和管理 Kubernetes 的复杂性,多家公司已经开始销售简化其运营的 Kubernetes 发行版,并且他们愿意提供支持。

目前使用最广泛的发行版是 OpenShift:红帽 Kubernetes 发行版。

为了简化开发人员和运维团队的生活,Ansible 提供了ansible-container,正如我们在前一节中所看到的,这是一个用于创建容器以及支持容器整个生命周期的工具。

使用 ansible-container 部署到 Kubernetes

让我们学习如何运行刚刚用ansible-container构建的镜像。

首先,我们需要镜像本身,你应该已经有了,因为这是上一节的输出!

我们假设您可以访问用于测试的 Kubernetes 或 OpenShift 集群。设置这些超出了本书的范围,因此您可能希望查看 Minikube 或 Minishift 等分发版,这两者都旨在快速且易于设置,以便您可以快速开始学习这些技术。我们还需要正确配置kubectl客户端或oc客户端,根据我们部署的是 Kubernetes 还是 OpenShift。让我们开始吧:

  1. 要将应用程序部署到集群,您需要更改container.yml文件,以便添加一些附加信息。更具体地说,我们需要添加一个名为settings和一个名为k8s_namespace的部分来声明我们的部署设置。此部分将如下所示:
k8s_namespace:
  name: http-server
  description: An HTTP server
  display_name: HTTP server
  1. 现在我们已经添加了关于 Kubernetes 部署的必要信息,我们可以继续部署:
$ ansible-container --engine kubernetes deploy

一旦 Ansible 完成执行,您将能够在 Kubernetes 集群上找到http-server部署。

幕后发生的是,Ansible 有一组模块(其名称通常以k8s开头),用于驱动 Kubernetes 集群,并使用它们自动部署应用程序。

根据我们在上一节中构建的图像和本节开头添加的其他信息,Ansible 能够填充部署模板,然后使用k8s模块部署它。

现在您已经学会了如何在 Kubernetes 集群上部署容器,接下来您将学习如何使用 Ansible 与 Kubernetes 集群进行交互。

使用 Ansible 管理 Kubernetes 对象

现在您已经使用ansible-container部署了第一个应用程序,与该应用程序进行交互将非常有用。获取有关 Kubernetes 对象状态的信息或部署应用程序,更一般地与 Kubernetes API 进行交互,而无需使用ansible-containers

安装 Ansible Kubernetes 依赖项

首先,您需要安装 Python openshift包(您可以通过 pip 或操作系统的打包系统安装它)。

我们现在准备好我们的第一个 Kubernetes playbook 了!

使用 Ansible 列出 Kubernetes 命名空间

Kubernetes 集群在内部有多个命名空间,通常可以使用kubectl get namespaces找到集群中的命名空间。您可以通过创建一个名为k8s-ns-show.yaml的文件来使用 Ansible 执行相同的操作,内容如下:

---
- hosts: localhost
  tasks:
    - name: Get information from K8s
      k8s_info:
        api_version: v1
        kind: Namespace
      register: ns
    - name: Print info
      debug:
        var: ns

我们现在可以执行此操作,如下所示:

$ ansible-playbook k8s-ns-show.yaml

现在您将在输出中看到有关命名空间的信息。

请注意代码的第七行(kind: Namespace),我们在其中指定了我们感兴趣的资源类型。您可以指定其他 Kubernetes 对象类型以查看它们(例如,您可以尝试使用部署、服务和 Pod 进行此操作)。

使用 Ansible 创建 Kubernetes 命名空间

到目前为止,我们已经学会了如何显示现有的命名空间,但通常,Ansible 被用于以声明方式实现期望的状态。因此,让我们创建一个名为k8s-ns.yaml的新的 playbook,内容如下:

---
- hosts: localhost
  tasks:
    - name: Ensure the myns namespace exists
      k8s:
        api_version: v1
        kind: Namespace
        name: myns
        state: present

在运行之前,我们可以执行kubectl get ns,以确保myns不存在。在我的情况下,输出如下:

$ kubectl get ns
NAME STATUS AGE
default Active 69m
kube-node-lease Active 69m
kube-public Active 69m
kube-system Active 69m

我们现在可以使用以下命令运行 playbook:

$ ansible-playbook k8s-ns.yaml

输出应该类似于以下内容:

PLAY [localhost] *******************************************************************

TASK [Gathering Facts] *************************************************************
ok: [localhost]

TASK [Ensure the myns namespace exists] ********************************************
changed: [localhost]

PLAY RECAP *************************************************************************
localhost : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

正如您所看到的,Ansible 报告说它改变了命名空间的状态。如果我再次执行kubectl get ns,很明显 Ansible 创建了我们期望的命名空间:

$ kubectl get ns
NAME STATUS AGE
default Active 74m
kube-node-lease Active 74m
kube-public Active 74m
kube-system Active 74m
myns Active 22s 

现在,让我们创建一个服务。

使用 Ansible 创建 Kubernetes 服务

到目前为止,我们已经看到了如何从 Ansible 创建命名空间,现在让我们在刚刚创建的命名空间中放置一个服务。让我们创建一个名为k8s-svc.yaml的新 playbook,内容如下:

---
- hosts: localhost
  tasks:
    - name: Ensure the Service mysvc is present
      k8s:
        state: present
        definition:
          apiVersion: v1
          kind: Service
          metadata:
            name: mysvc
            namespace: myns
          spec:
            selector:
              app: myapp
              service: mysvc
            ports:
              - protocol: TCP
                targetPort: 800
                name: port-80-tcp
                port: 80

在运行之前,我们可以执行kubectl get svc来确保命名空间中没有服务。在运行之前,请确保您在正确的命名空间中!在我的情况下,输出如下:

$ kubectl get svc
No resources found in myns namespace.

我们现在可以使用以下命令运行它:

$ ansible-playbook k8s-svc.yaml

输出应该类似于以下内容:

PLAY [localhost] *******************************************************************

TASK [Gathering Facts] *************************************************************
ok: [localhost]

TASK [Ensure the myns namespace exists] ********************************************
changed: [localhost]

PLAY RECAP *************************************************************************
localhost : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 

正如您所看到的,Ansible 报告说它改变了服务状态。如果我再次执行kubectl get svc,很明显 Ansible 创建了我们期望的服务:

$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
mysvc ClusterIP 10.0.0.84 <none> 80/TCP 10s

正如您所看到的,我们遵循了在命名空间情况下使用的相同过程,但是我们指定了不同的 Kubernetes 对象类型,并指定了所需的服务类型的各种参数。您可以对所有其他 Kubernetes 对象类型执行相同的操作。

现在您已经学会了如何处理 Kubernetes 集群,您将学习如何使用 Ansible 自动化 Docker。

使用 Ansible 自动化 Docker

Docker 现在是一个非常常见和普遍的工具。在生产中,它通常由编排器管理(或者至少应该在大多数情况下),但在开发中,环境通常直接使用。

使用 Ansible,您可以轻松管理您的 Docker 实例。

由于我们将要管理一个 Docker 实例,我们需要确保我们手头有一个,并且我们机器上的docker命令已经正确配置。我们需要这样做以确保在终端上运行docker images足够。假设您得到类似以下的结果:

REPOSITORY TAG IMAGE ID CREATED SIZE

这意味着一切都正常工作。如果您已经克隆了镜像,可能会提供更多行作为输出。

另一方面,假设它返回了类似于这样的东西:

Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?

这意味着我们没有运行 Docker 守护程序,或者我们的 Docker 控制台配置不正确。

此外,确保您有docker Python 模块是很重要的,因为 Ansible 将尝试使用它与 Docker 守护程序进行通信。让我们来看一下:

  1. 首先,我们需要创建一个名为start-docker-container.yaml的 playbook,其中包含以下代码:
---
- hosts: localhost
  tasks:
    - name: Start a container with a command
      docker_container:
        name: test-container
        image: alpine
        command:
          - echo
          - "Hello, World!"
  1. 现在我们有了 Ansible playbook,我们只需要执行它:
$ ansible-playbook start-docker-container.yaml

正如您可能期望的那样,它将给您一个类似于以下的输出:

PLAY [localhost] *********************************************************************

TASK [Gathering Facts] ***************************************************************
ok: [localhost]

TASK [Start a container with a command] **********************************************
changed: [localhost]

PLAY RECAP ***************************************************************************
localhost : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0  
  1. 现在我们可以检查我们的命令是否正确执行,如下所示:
$ docker container list -a

这将显示运行的容器:

CONTAINER ID IMAGE  COMMAND              CREATED       STATUS                        PORTS NAMES
c706ec55fc0d alpine "echo Hello, World!" 3 minutes ago Exited (0) About a minute ago       test-container

这证明了一个容器被执行。

要检查echo命令是否被执行,我们可以运行以下代码:

$ docker logs c706ec55fc0d

这将返回以下输出:

Hello, World!

在本节中,我们执行了docker_container模块。这不是 Ansible 用来控制 Docker 守护程序的唯一模块,但它可能是最广泛使用的模块之一,因为它用于控制在 Docker 上运行的容器。

其他模块包括以下内容:

  • docker_config:用于更改 Docker 守护程序的配置

  • docker_container_info:用于从容器中收集信息(检查)

  • docker_network:用于管理 Docker 网络配置

还有许多以docker_开头的模块,但实际上用于管理 Docker Swarm 集群,而不是 Docker 实例。一些示例如下:

  • docker_node:用于管理 Docker Swarm 集群中的节点

  • docker_node_info:用于检索 Docker Swarm 集群中特定节点的信息

  • docker_swarm_info:用于检索有关 Docker Swarm 集群的信息

正如我们将在下一节中看到的,还有许多模块可以用来管理以各种方式编排的容器。

现在您已经学会了如何使用 Ansible 自动化 Docker,您将探索面向容器的模块。

探索面向容器的模块

通常,当组织发展壮大时,他们开始在组织的不同部分使用多种技术。通常发生的另一件事是,部门发现某个供应商对他们很有效后,他们更倾向于尝试该供应商提供的新技术。这两个因素的混合以及时间(通常情况下,技术周期较少)最终会在同一组织内为同一个问题创建多种解决方案。

如果您的组织处于这种容器的情况下,Ansible 可以帮助您,因为它能够与大多数,如果不是所有的容器平台进行互操作。

很多时候,使用 Ansible 的最大问题是找到你需要使用的模块的名称,以实现你想要实现的目标。在本节中,我们将尝试帮助您解决这个问题,主要是在容器化领域,但这可能也有助于您寻找不同类型的模块。

所有 Ansible 模块研究的起点应该是模块索引(docs.ansible.com/ansible/latest/modules/modules_by_category.html)。通常情况下,您可以找到一个明显与您寻找的内容匹配的类别,但情况并非总是如此。

容器是这种例外情况之一(至少在撰写本文时是如此),因此没有“容器”类别。解决方案是转到所有模块。从这里,您可以使用浏览器的内置功能进行搜索(通常可以通过使用Ctrl+F来实现),以找到可能与包名称或简短描述匹配的字符串。

Ansible 中的每个模块都属于一个类别,但很多时候,模块适用于多个类别,因此并不总是容易找到它们。

例如,许多与容器服务相关的 Ansible 模块属于云模块类别(ECS、Docker、LXC、LXD 和 Podman),而其他模块属于集群模块类别(Kubernetes、OpenShift 等)。

为了进一步帮助您,让我们来看一下一些主要的容器平台和 Ansible 提供的主要模块。

2014 年,亚马逊网络服务推出了弹性容器服务(ECS),这是一种在其基础设施中部署和编排 Docker 容器的方式。在接下来的一年,亚马逊网络服务还推出了弹性容器注册表(ECR),这是一个托管的 Docker 注册表服务。该服务并没有像 AWS 希望的那样普及,因此在 2018 年,AWS 推出了弹性 Kubernetes 服务(EKS),以允许希望在 AWS 上运行 Kubernetes 的人拥有一个托管服务。如果您正在使用或计划使用 EKS,这只是一个标准的托管 Kubernetes 集群,因此您可以使用我们即将介绍的 Kubernetes 特定模块。如果您决定使用 ECS,有几个模块可以帮助您。最重要的是ecs_cluster,它允许您创建或终止 ECS 集群;ecs_ecr,它允许您管理 ECR;ecs_service,它允许您在 ECS 中创建、终止、启动或停止服务;以及ecs_task,它允许您在 ECS 中运行、启动或停止任务。除此之外,还有ecs_service_facts,它允许 Ansible 列出或描述 ECS 中的服务。

2018 年,微软 Azure 宣布了 Azure 容器服务(ACS),然后宣布了 Azure Kubernetes 服务(AKS)。这些服务由 Kubernetes 解决方案管理,因此可以使用 Kubernetes 模块来管理。除此之外,Ansible 还提供了两个特定的模块:azure_rm_acs模块允许我们创建、更新和删除 Azure 容器服务实例,而azure_rm_aks模块允许我们创建、更新和删除 Azure Kubernetes 服务实例。

Google Cloud 于 2015 年推出了Google Kubernetes EngineGKE)。GKE 是 Google Cloud Platform 的托管 Kubernetes 版本,因此与 Ansible Kubernetes 模块兼容。除此之外,还有各种 GKE 特定的模块,其中一些如下:

  • gcp_container_cluster:允许您创建 GCP Cluster

  • gcp_container_cluster_facts:允许您收集有关 GCP Cluster 的信息

  • gcp_container_node_pool:允许您创建 GCP NodePool

  • gcp_container_node_pool_facts:允许您收集有关 GCP NodePool 的信息

Red Hat 于 2011 年启动了 OpenShift,当时它是基于自己的容器运行时的。在 2015 年发布的第 3 版中,它完全基于 Kubernetes 重新构建,因此所有 Ansible Kubernetes 模块都可以使用。除此之外,还有oc模块,目前仍然存在但处于弃用状态,更倾向于使用 Kubernetes 模块。

2015 年,Google 发布了 Kubernetes,并迅速形成了一个庞大的社区。Ansible 允许您使用一些模块来管理您的 Kubernetes 集群:

  • k8s:允许您管理任何类型的 Kubernetes 对象

  • k8s_auth:允许您对需要显式登录的 Kubernetes 集群进行身份验证

  • k8s_facts:允许您检查 Kubernetes 对象

  • k8s_scale:允许您为部署、副本集、复制控制器或作业设置新的大小

  • k8s_service:允许您在 Kubernetes 上管理服务

LXC 和 LXD 也是可以在 Linux 中运行容器的系统。由于以下模块的支持,这些系统也受到 Ansible 的支持:

  • lxc_container:允许您管理 LXC 容器

  • lxd_container:允许您管理 LXD 容器

  • lxd_profile:允许您管理 LXD 配置文件

现在您已经学会了如何探索面向容器的模块,接下来将学习如何针对亚马逊网络服务进行自动化。

针对亚马逊网络服务进行自动化

在许多组织中,广泛使用云提供商,而在其他组织中,它们只是被引入。但是,无论如何,您可能都必须处理一些云提供商来完成工作。AWS 是最大的,也是最古老的,可能是您必须使用的东西。

安装

要能够使用 Ansible 自动化您的 Amazon Web Service 资源,您需要安装boto库。要执行此操作,请运行以下命令:

$ pip install boto

现在您已经安装了所有必要的软件,可以设置身份验证了。

身份验证

boto库在~/.aws/credentials文件中查找必要的凭据。确保凭据文件正确配置有两种不同的方法。

可以使用 AWS CLI 工具。或者,可以通过创建具有以下结构的文件来使用您选择的文本编辑器:

[default] aws_access_key_id = [YOUR_KEY_HERE] aws_secret_access_key = [YOUR_SECRET_ACCESS_KEY_HERE]

现在您已经创建了具有必要凭据的文件,boto将能够针对您的 AWS 环境进行操作。由于 Ansible 对 AWS 系统的每一次通信都使用boto,这意味着即使您不必更改任何特定于 Ansible 的配置,Ansible 也将被适当配置。

创建您的第一台机器

现在 Ansible 能够连接到您的 AWS 环境,您可以按照以下步骤进行实际的 playbook:

  1. 创建具有以下内容的aws.yaml Playbook:
---
- hosts: localhost
  tasks:
    - name: Ensure key pair is present
      ec2_key:
        name: fale
        key_material: "{{ lookup('file', '~/.ssh/fale.pub') }}"
    - name: Gather information of the EC2 VPC net in eu-west-1
      ec2_vpc_net_facts:
        region: eu-west-1
      register: aws_simple_net
    - name: Gather information of the EC2 VPC subnet in eu-west-1
      ec2_vpc_subnet_facts:
        region: eu-west-1
        filters:
          vpc-id: '{{ aws_simple_net.vpcs.0.id }}'
      register: aws_simple_subnet
    - name: Ensure wssg Security Group is present
      ec2_group:
        name: wssg
        description: Web Security Group
        region: eu-west-1
        vpc_id: '{{ aws_simple_net.vpcs.0.id }}'
        rules:
          - proto: tcp
            from_port: 22
            to_port: 22
            cidr_ip: 0.0.0.0/0
          - proto: tcp
            from_port: 80
            to_port: 80
            cidr_ip: 0.0.0.0/0
          - proto: tcp
            from_port: 443
            to_port: 443
            cidr_ip: 0.0.0.0/0
        rules_egress:
          - proto: all
            cidr_ip: 0.0.0.0/0
      register: aws_simple_wssg
    - name: Setup instance
      ec2:
        assign_public_ip: true
        image: ami-3548444c
        region: eu-west-1
        exact_count: 1
        key_name: fale
        count_tag:
          Name: ws01.ansible2cookbook.com
        instance_tags:
          Name: ws01.ansible2cookbook.coms
        instance_type: t2.micro
        group_id: '{{ aws_simple_wssg.group_id }}'
        vpc_subnet_id: '{{ aws_simple_subnet.subnets.0.id }}'
        volumes:
          - device_name: /dev/sda1
            volume_type: gp2
            volume_size: 10
            delete_on_termination: True
  1. 使用以下命令运行它:
$ ansible-playbook aws.yaml

此命令将返回类似以下内容:

PLAY [localhost] **********************************************************************************

TASK [Gathering Facts] ****************************************************************************
ok: [localhost]

TASK [Ensure key pair is present] *****************************************************************
ok: [localhost]

TASK [Gather information of the EC2 VPC net in eu-west-1] *****************************************
ok: [localhost]

TASK [Gather information of the EC2 VPC subnet in eu-west-1] **************************************
ok: [localhost]

TASK [Ensure wssg Security Group is present] ******************************************************
ok: [localhost]

TASK [Setup instance] *****************************************************************************
changed: [localhost]

PLAY RECAP ****************************************************************************************
localhost : ok=6 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 

如果您检查 AWS 控制台,您将看到现在有一台机器正在运行!

要在 AWS 中启动虚拟机,我们需要准备一些东西,如下所示:

  • 一个 SSH 密钥对

  • 一个网络

  • 一个子网络

  • 一个安全组

默认情况下,您的帐户中已经有一个网络和一个子网络,但您需要检索它们的 ID。

这就是为什么我们首先将 SSH 密钥对的公共部分上传到 AWS,然后查询有关网络和子网络的信息,然后确保我们想要使用的安全组存在,最后触发机器构建。

现在您已经学会了如何针对亚马逊网络服务进行自动化,您将学习如何通过自动化来补充谷歌云平台。

通过自动化来补充谷歌云平台

另一个全球云服务提供商是谷歌,其谷歌云平台。谷歌对云的方法与其他提供商的方法相对不同,因为谷歌不试图在虚拟环境中模拟数据中心。这是因为谷歌希望重新思考云提供的概念,以简化它。

安装

在您开始使用 Ansible 与谷歌云平台之前,您需要确保已安装适当的组件。具体来说,您需要 Python 的requestsgoogle-auth模块。要安装这些模块,请运行以下命令:

$ pip install requests google-auth

现在您已经准备好所有依赖项,可以开始认证过程。

认证

在谷歌云平台中获得工作凭据的两种不同方法:

  • 服务帐户

  • 机器帐户

第一种方法在大多数情况下是建议的,因为第二种方法仅适用于在谷歌云平台环境中直接运行 Ansible 的情况。

创建服务帐户后,您应该设置以下环境变量:

  • GCP_AUTH_KIND

  • GCP_SERVICE_ACCOUNT_EMAIL

  • GCP_SERVICE_ACCOUNT_FILE

  • GCP_SCOPES

现在,Ansible 可以使用适当的服务帐户。

第二种方法是最简单的,因为如果您在谷歌云实例中运行 Ansible,它将能够自动检测到机器帐户。

创建您的第一台机器

现在 Ansible 能够连接到您的 GCP 环境,您可以继续进行实际的 Playbook:

  1. 创建gce.yaml Playbook,并包含以下内容:
---
- hosts: localhost
  tasks:
    - name: create a instance
      gcp_compute_instance:
        name: TestMachine
        machine_type: n1-standard-1
        disks:
        - auto_delete: 'true'
          boot: 'true'
          initialize_params:
            source_image: family/centos-7
            disk_size_gb: 10
        zone: eu-west1-c
        auth_kind: serviceaccount
        service_account_file: "~/sa.json"
        state: present

使用以下命令执行它:

$ ansible-playbook gce.yaml

这将创建以下输出:

PLAY [localhost] **********************************************************************************

TASK [Gathering Facts] ****************************************************************************
ok: [localhost]

TASK [create a instance] **************************************************************************
changed: [localhost]

PLAY RECAP ****************************************************************************************
localhost : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 

与 AWS 示例一样,在云中运行机器对 Ansible 来说非常容易。

在 GCE 的情况下,您不需要预先设置网络,因为 GCE 默认设置将提供一个功能齐全的机器。

与 AWS 一样,您可以使用的模块列表非常庞大。您可以在docs.ansible.com/ansible/latest/modules/list_of_cloud_modules.html#google找到完整的列表。

现在您已经学会了如何通过自动化来补充谷歌云平台,您将学习如何无缝地执行与 Azure 的自动化集成。

与 Azure 的无缝自动化集成

Ansible 可以管理的另一个全球云是 Microsoft Azure。

与 AWS 类似,Azure 集成需要在 Playbooks 中执行相当多的步骤。

您需要首先设置认证,以便 Ansible 被允许控制您的 Azure 帐户。

安装

要让 Ansible 管理 Azure 云,您需要安装 Python 的 Azure SDK。通过执行以下命令来完成:

$ pip install 'ansible[azure]'

现在您已经准备好所有依赖项,可以开始认证过程。

认证

有不同的方法可以确保 Ansible 能够为您管理 Azure,这取决于您的 Azure 帐户设置方式,但它们都可以在~/.azure/credentials文件中配置。

如果您希望 Ansible 使用 Azure 帐户的主要凭据,您需要创建一个类似以下内容的文件:

[default] subscription_id = [YOUR_SUBSCIRPTION_ID_HERE] client_id = [YOUR_CLIENT_ID_HERE] secret = [YOUR_SECRET_HERE] tenant = [YOUR_TENANT_HERE]

如果您希望使用用户名和密码与 Active Directories,可以这样做:

[default] ad_user = [YOUR_AD_USER_HERE] password = [YOUR_AD_PASSWORD_HERE]

最后,您可以选择使用 ADFS 进行 Active Directory 登录。在这种情况下,您需要设置一些额外的参数。您最终会得到类似这样的东西:

[default] ad_user = [YOUR_AD_USER_HERE] password = [YOUR_AD_PASSWORD_HERE] client_id = [YOUR_CLIENT_ID_HERE] tenant = [YOUR_TENANT_HERE] adfs_authority_url = [YOUR_ADFS_AUTHORITY_URL_HERE]

相同的参数可以作为参数传递,也可以作为环境变量传递,如果更合理的话。

创建你的第一台机器

现在,Ansible 已经能够连接到你的 Azure 环境,你可以继续进行实际的 Playbook 了。

  1. 创建azure.yaml Playbook,内容如下:
---
- hosts: localhost
  tasks:
    - name: Ensure the Storage Account is present
      azure_rm_storageaccount:
        resource_group: Testing
        name: mysa
        account_type: Standard_LRS
    - name: Ensure the Virtual Network is present
      azure_rm_virtualnetwork:
        resource_group: Testing
        name: myvn
        address_prefixes: "10.10.0.0/16"
    - name: Ensure the Subnet is present
      azure_rm_subnet:
        resource_group: Testing
        name: mysn
        address_prefix: "10.10.0.0/24"
        virtual_network: myvn
    - name: Ensure that the Public IP is set
      azure_rm_publicipaddress:
        resource_group: Testing
        allocation_method: Static
        name: myip
    - name: Ensure a Security Group allowing SSH is present
      azure_rm_securitygroup:
        resource_group: Testing
        name: mysg
        rules:
          - name: SSH
            protocol: Tcp
            destination_port_range: 22
            access: Allow
            priority: 101
            direction: Inbound
    - name: Ensure the NIC is present
      azure_rm_networkinterface:
        resource_group: Testing
        name: testnic001
        virtual_network: myvn
        subnet: mysn
        public_ip_name: myip
        security_group: mysg
    - name: Ensure the Virtual Machine is present
      azure_rm_virtualmachine:
        resource_group: Testing
        name: myvm01
        vm_size: Standard_D1
        storage_account: mysa
        storage_container: myvm01
        storage_blob: myvm01.vhd
        admin_username: admin
        admin_password: Password!
        network_interfaces: testnic001
        image:
          offer: CentOS
          publisher: OpenLogic
          sku: '8.0'
          version: latest
  1. 我们可以使用以下命令运行它:
$ ansible-playbook azure.yaml

这将返回类似以下的结果:

PLAY [localhost] **********************************************************************************

TASK [Gathering Facts] ****************************************************************************
ok: [localhost]

TASK [Ensure the Storage Account is present] ******************************************************
changed: [localhost] TASK [Ensure the Virtual Network is present] ******************************************************
changed: [localhost]

TASK [Ensure the Subnet is present] ***************************************************************
changed: [localhost]

TASK [Ensure that the Public IP is set] ***********************************************************
changed: [localhost]

TASK [Ensure a Security Group allowing SSH is present] ********************************************
changed: [localhost]

TASK [Ensure the NIC is present] ******************************************************************
changed: [localhost]

TASK [Ensure the Virtual Machine is present] ******************************************************
changed: [localhost]

PLAY RECAP ****************************************************************************************
localhost : ok=8 changed=7 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 

你现在可以在 Azure 云中运行你的机器了!

正如你所看到的,在 Azure 中,你需要在发出创建机器命令之前准备好所有资源。这就是你首先创建存储帐户、虚拟网络、子网、公共 IP、安全组和 NIC,然后只有在那时,才创建机器本身的原因。

除了市场上的三大主要参与者外,还有许多其他云选项。一个非常有趣的选择是 RackSpace,因为它的历史:Rackspace Cloud。

通过 Rackspace Cloud 扩展你的环境

Rackspace 是公共云业务中最早的公司之一。此外,Rackspace 在 2010 年与 NASA 联合创办了 OpenStack。在过去的 10 年中,Rackspace 一直是云基础设施、OpenStack 以及更广泛的托管领域的非常有影响力的提供商。

安装

为了能够从 Ansible 管理 Rackspace,你需要安装pyrax

安装它的最简单方法是运行以下命令:

$ pip install pyrax

如果可用,你也可以通过系统包管理器安装它。

认证

由于pyrax没有凭据文件的默认位置,你需要创建一个文件,然后通过指示pyrax在文件位置设置一个环境变量来做到这一点。

让我们从在~/.rackspace_credentials中创建一个文件开始,文件内容如下:

[rackspace_cloud] username = [YOUR_USERNAME_HERE] api_key = [YOUR_API_KEY_HERE]

现在,我们可以通过将RAX_CREDS_FILE变量设置为正确的位置来继续进行:

**$ export RAX_CREDS_FILE=~/.rackspace_credentials** 

让我们继续使用 Rackspace Cloud 创建一台机器。

创建你的第一台机器

在 Rackspace Cloud 中创建一台机器非常简单,因为它是一个单步操作:

  1. 创建rax.yaml Playbook,内容如下:
---
- hosts: localhost
  tasks:
    - name: Ensure the my_machine exists
      rax:
        name: my_machine
        flavor: 4
        image: centos-8
        count: 1
        group: my_group
        wait: True
  1. 现在,你可以使用以下命令来执行它:
$ ansible-playbook rax.yaml
  1. 这应该会产生类似以下的结果:
PLAY [localhost] **********************************************************************************

TASK [Gathering Facts] ****************************************************************************
ok: [localhost]

TASK [Ensure the my_machine exists] ***************************************************************
changed: [localhost]

PLAY RECAP ****************************************************************************************
localhost : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 

正如你所看到的,在 Rackspace Cloud 中创建机器非常简单直接,而默认的 Ansible 模块已经集成了一些有趣的概念,比如组和计数。这些选项允许你以与单个实例相同的方式创建和管理实例组。

使用 Ansible 来编排 OpenStack

与我们刚讨论的各种公共云服务相反,OpenStack 允许你创建自己的(私有)云。

私有云的缺点是它们向管理员和用户暴露了更多的复杂性,但这也是它们可以被定制以完美适应组织的原因。

安装

能够使用 Ansible 控制 OpenStack 集群的第一步是确保安装了openstacksdk

要安装openstacksdk,你需要执行以下命令:

$ pip install openstacksdk

现在你已经安装了openstacksdk,你可以开始认证过程了。

认证

由于 Ansible 将使用openstacksdk作为其后端,你需要确保openstacksdk能够连接到 OpenStack 集群。

为了做到这一点,你可以更改~/.config/openstack/clouds.yaml文件,确保为你想要使用它的云配置。

一个正确的 OpenStack 凭据集的示例可能如下所示:

clouds:
 test_cloud: region_name: MyRegion auth: auth_url: http://[YOUR_AUTH_URL_HERE]:5000/v2.0/     username: [YOUR_USERNAME_HERE]
 password: [YOUR_PASSWORD_HERE] project_name: myProject

如果愿意,也可以设置不同的配置文件位置,将OS_CLIENT_CONFIG_FILE变量作为环境变量导出。

现在你已经设置了所需的安全性,以便 Ansible 可以管理你的集群,你可以创建你的第一个 Playbook 了。

创建你的第一台机器

由于 OpenStack 非常灵活,它的许多组件可以有许多不同的实现,这意味着它们在行为上可能略有不同。为了能够适应各种情况,管理 OpenStack 的 Ansible 模块往往具有较低的抽象级别,与许多公共云的模块相比。

因此,要创建一台机器,您需要确保公共 SSH 密钥为 OpenStack 所知,并确保 OS 镜像也存在。在这样做之后,您可以设置网络、子网络和路由器,以确保我们要创建的机器可以通过网络进行通信。然后,您可以创建安全组及其规则,以便该机器可以接收连接(在本例中为 ping 和 SSH 流量)。最后,您可以创建一个机器实例。

要完成我们刚刚描述的所有步骤,您需要创建一个名为openstack.yaml的文件,其中包含以下内容:

---
- hosts: localhost
  tasks:
    - name: Ensure the SSH key is present on OpenStack
      os_keypair:
        state: present
        name: ansible_key
        public_key_file: "{{ '~' | expanduser }}/.ssh/id_rsa.pub"
    - name: Ensure we have a CentOS image
      get_url:
        url: http://cloud.centos.org/centos/8/x86_64/images/CentOS-8-GenericCloud-8.1.1911-20200113.3.x86_64.qcow2
        dest: /tmp/CentOS-8-GenericCloud-8.1.1911-20200113.3.x86_64.qcow2
    - name: Ensure the CentOS image is in OpenStack
      os_image:
        name: centos
        container_format: bare
        disk_format: qcow2
        state: present
        filename: /tmp/CentOS-8-GenericCloud-8.1.1911-20200113.3.x86_64.qcow2
    - name: Ensure the Network is present
      os_network:
        state: present
        name: mynet
        external: False
        shared: False
      register: net_out
    - name: Ensure the Subnetwork is present
      os_subnet:
        state: present
        network_name: "{{ net_out.id }}"
        name: mysubnet
        ip_version: 4
        cidr: 192.168.0.0/24
        gateway_ip: 192.168.0.1
        enable_dhcp: yes
        dns_nameservers:
          - 8.8.8.8
    - name: Ensure the Router is present
      os_router:
        state: present
        name: myrouter
        network: nova
        external_fixed_ips:
          - subnet: nova
        interfaces:
          - mysubnet
    - name: Ensure the Security Group is present
      os_security_group:
        state: present
        name: mysg
    - name: Ensure the Security Group allows ICMP traffic
      os_security_group_rule:
        security_group: mysg
        protocol: icmp
        remote_ip_prefix: 0.0.0.0/0
    - name: Ensure the Security Group allows SSH traffic
      os_security_group_rule:
        security_group: mysg
        protocol: tcp
        port_range_min: 22
        port_range_max: 22
        remote_ip_prefix: 0.0.0.0/0
    - name: Ensure the Instance exists
      os_server:
        state: present
        name: myInstance
        image: centos
        flavor: m1.small
        security_groups: mysg
        key_name: ansible_key
        nics:
          - net-id: "{{ net_out.id }}"

现在,您可以运行它,如下:

$ ansible-playbook openstack.yaml

输出应该如下:

PLAY [localhost] **********************************************************************************

TASK [Gathering Facts] ****************************************************************************
ok: [localhost]

TASK [Ensure the SSH key is present on OpenStack] *************************************************
changed: [localhost]

TASK [Ensure we have a CentOS image] **************************************************************
changed: [localhost]

TASK [Ensure the CentOS image is in OpenStack] ****************************************************
changed: [localhost]

TASK [Ensure the Network is present] **************************************************************
changed: [localhost]

TASK [Ensure the Subnetwork is present] ***********************************************************
changed: [localhost]

TASK [Ensure the Router is present] ***************************************************************
changed: [localhost]

TASK [Ensure the Security Group is present] *******************************************************
changed: [localhost]

TASK [Ensure the Security Group allows ICMP traffic] **********************************************
changed: [localhost]

TASK [Ensure the Security Group allows SSH traffic] ***********************************************
changed: [localhost]

TASK [Ensure the Instance exists] *****************************************************************
changed: [localhost]

PLAY RECAP ****************************************************************************************
localhost : ok=11 changed=10 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 

正如您所看到的,这个过程比我们涵盖的公共云要长。但是,您确实可以上传您想要运行的镜像,这是许多云不允许的(或者允许的过程非常复杂)。

总结

在本章中,您学习了如何使用 playbooks 自动化任务,从设计和构建容器到在 Kubernetes 上管理部署,以及管理 Kubernetes 对象和使用 Ansible 自动化 Docker。您还探索了可以帮助您自动化云环境的模块,如 AWS、Google Cloud Platform、Azure、Rackspace 和 OpenShift。您还了解了各种云提供商使用的不同方法,包括它们的默认值以及您总是需要添加的参数。

现在您已经了解了 Ansible 如何与云进行交互,您可以立即开始自动化云工作流程。还记得要查看进一步阅读部分的文档,以查看 Ansible 支持的所有云模块及其选项。

在下一章中,您将学习如何排除故障和创建测试策略。

问题

  1. 以下哪个不是 GKE Ansible 模块?

A) gcp_container_cluster

B) gcp_container_node_pool

C) gcp_container_node_pool_facts

D) gcp_container_node_pool_count

E) gcp_container_cluster_facts

  1. 真或假:为了管理 Kubernetes 中的容器,您需要在设置部分中添加k8s_namespace

A) True

B) False

  1. 真或假:在使用 Azure 时,在创建实例之前,您不需要创建网络接口控制器NIC)。

A) True

B) False

  1. 真或假:Ansible-Container是与 Kubernetes 和 Doc 交互的唯一方式。

A) True

B) False

  1. 真或假:在使用 AWS 时,在创建 EC2 实例之前,需要创建一个安全组。

A) True

B) False

进一步阅读

第十一章:故障排除和测试策略

与任何其他类型的代码类似,Ansible 代码可能包含问题和错误。Ansible 尝试通过在执行任务之前检查任务语法来尽可能地使其安全。然而,这种检查只能防止一小部分可能的错误类型,如不正确的任务参数,但它不会保护您免受其他错误的影响。

重要的是要记住,由于其性质,在 Ansible 代码中,我们描述的是期望的状态,而不是陈述一系列步骤来实现期望的状态。这种差异意味着系统不太容易出现逻辑错误。

然而,Playbook 中的错误可能意味着所有机器上的潜在错误配置。这应该被非常认真对待。当系统的关键部分发生更改时,如 SSH 守护程序或sudo配置时,情况就更加严重,因为风险是您将自己锁在系统外。

有许多不同的方法可以防止或减轻 Ansible playbooks 中的错误。在本章中,我们将涵盖以下主题:

  • 深入研究 playbook 执行问题

  • 使用主机信息来诊断故障

  • 使用 playbook 进行测试

  • 使用检查模式

  • 解决主机连接问题

  • 通过 CLI 传递工作变量

  • 限制主机的执行

  • 刷新代码缓存

  • 检查语法错误

技术要求

本章假定您已经按照《第一章》开始使用 Ansible中详细介绍的方式设置了控制主机,并且正在使用最新版本——本章的示例是使用 Ansible 2.9 进行测试的。尽管本章将给出特定的主机名示例,但您可以自由地用您自己的主机名和/或 IP 地址替换它们。如何做到这一点的详细信息将在适当的地方提供。

本章中的示例可以在本书的 GitHub 存储库中找到:github.com/PacktPublishing/Practical-Ansible-2/tree/master/Chapter%2011

深入研究 playbook 执行问题

有时,Ansible 执行会中断。许多事情都可能导致这种情况。

在执行 Ansible playbooks 时,我发现的问题中最常见的原因是网络。由于发出命令的机器和执行命令的机器通常通过网络连接,网络中的问题会立即显示为 Ansible 执行问题。

有时,特别是对于某些模块,如shellcommand,返回代码是非零的,即使执行是成功的。在这种情况下,您可以通过在模块中使用以下行来忽略错误:

ignore_errors: yes

例如,如果运行/bin/false命令,它将始终返回1。为了在 playbook 中执行这个命令,以避免它在那里阻塞,您可以编写类似以下的内容:

- name: Run a command that will return 1
 command: /bin/false ignore_errors: yes

正如我们所看到的,/bin/false将始终返回1作为返回代码,但我们仍然设法继续执行。请注意,这是一个特殊情况,通常,最好的方法是修复您的应用程序,以便遵循 UNIX 标准,并在应用程序适当运行时返回0,而不是在您的 Playbooks 中放置一个变通方法。

接下来,我们将更多地讨论我们可以使用的方法来诊断 Ansible 执行问题。

使用主机信息来诊断故障

一些执行失败是由目标机器的状态导致的。这种情况最常见的问题是 Ansible 期望文件或变量存在,但实际上并不存在。

有时,打印机器信息就足以找到问题。

为此,我们需要创建一个简单的 playbook,名为print_facts.yaml,其中包含以下内容:

--- - hosts: target_host
 tasks: - name: Display all variables/facts known for a host debug: var: hostvars[inventory_hostname]

这种技术将为您提供有关 Ansible 执行期间目标机器状态的大量信息。

使用 playbook 进行测试

在 IT 领域最复杂的事情之一不是创建软件和系统,而是在它们出现问题时进行调试。Ansible 也不例外。无论您在创建 Ansible playbook 方面有多么出色,迟早都会发现自己在调试一个行为不如您所想象的 playbook。

执行基本测试的最简单方法是在执行期间打印变量的值。让我们学习如何使用 Ansible 来做到这一点,如下所示:

  1. 首先,我们需要一个名为debug.yaml的 playbook,其中包含以下内容:
---
- hosts: localhost
  tasks:
    - shell: /usr/bin/uptime
      register: result
    - debug:
        var: result
  1. 使用以下命令运行它:
$ ansible-playbook debug.yaml

您将收到类似以下内容的输出:

PLAY [localhost] **********************************************************************************

TASK [Gathering Facts] ****************************************************************************
ok: [localhost]

TASK [shell] **************************************************************************************
changed: [localhost]

TASK [debug] **************************************************************************************
ok: [localhost] => {
    "result": {
        "changed": true,
        "cmd": "/usr/bin/uptime",
        "delta": "0:00:00.003461",
        "end": "2019-06-16 11:30:51.087322",
        "failed": false,
        "rc": 0,
        "start": "2019-06-16 11:30:51.083861",
        "stderr": "",
        "stderr_lines": [],
        "stdout": " 11:30:51 up 40 min, 1 user, load average: 1.11, 0.73, 0.53",
        "stdout_lines": [
            " 11:30:51 up 40 min, 1 user, load average: 1.11, 0.73, 0.53"
        ]
    }
}

PLAY RECAP ****************************************************************************************
localhost : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 

在第一个任务中,我们使用command模块执行uptime命令,并将其输出保存在result变量中。然后,在第二个任务中,我们使用debug模块打印result变量的内容。

debug模块是允许您在 Ansible 执行期间打印变量的值(使用var选项)或固定字符串(使用msg选项)的模块。

debug模块还提供了verbosity选项。假设您以以下方式更改了 playbook:

---
- hosts: localhost
  tasks:
    - shell: /usr/bin/uptime
      register: result
    - debug:
       var: result
       verbosity: 2

现在,如果您尝试以与之前相同的方式执行它,您将注意到调试步骤不会被执行,并且输出中将出现以下行:

TASK [debug] **************************************************************************************
skipping: [localhost]

这是因为我们将所需的verbosity最小设置为2,默认情况下,Ansible 以0verbosity运行。

要查看使用新 playbook 的 debug 模块的结果,我们需要运行一个稍微不同的命令:

$ ansible-playbook debug2.yaml -vv

通过在命令行中放置两个-v选项,我们将以2verbosity运行 Ansible。这不仅会影响这个特定的模块,还会影响所有在不同调试级别下设置为以不同方式运行的模块(或 Ansible 本身)。

现在您已经学会了如何使用 playbook 进行测试,让我们学习如何使用检查模式。

使用检查模式

尽管您可能对自己编写的代码很有信心,但在真正的生产环境中运行之前进行测试仍然是值得的。在这种情况下,能够运行您的代码,但同时又有一个安全网是一个好主意。这就是检查模式的作用。请按照以下步骤进行操作:

  1. 首先,我们需要创建一个简单的 playbook 来测试这个功能。让我们创建一个名为check-mode.yaml的 playbook,其中包含以下内容:
---
- hosts: localhost
  tasks:
    - name: Touch a file
      file:
        path: /tmp/myfile
        state: touch
  1. 现在,我们可以通过在调用中指定--check选项来以检查模式运行 playbook:
$ ansible-playbook check-mode.yaml --check

这将输出一切,就好像它真的执行了操作,如下所示:

PLAY [localhost] **********************************************************************************

TASK [Gathering Facts] ****************************************************************************
ok: [localhost]

TASK [Touch a file] *******************************************************************************
ok: [localhost]

PLAY RECAP ****************************************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 

但是,如果您在/tmp中查找,您将找不到myfile

Ansible 检查模式通常被称为干运行。其思想是运行不会改变机器的状态,只会突出当前状态与 playbook 中声明的状态之间的差异。

并非所有模块都支持检查模式,但所有主要模块都支持,而且每个版本都会添加更多的模块。特别要注意的是,commandshell模块不支持它,因为模块无法判断哪些命令会导致更改,哪些不会。因此,这些模块在检查模式之外运行时总是返回更改,因为它们假设已经进行了更改。

与检查模式类似的功能是--diff标志。这个标志允许我们跟踪在 Ansible 执行期间发生了什么变化。因此,假设我们使用以下命令运行相同的 playbook:

$ ansible-playbook check-mode.yaml --diff

这将返回类似以下的内容:

PLAY [localhost] **********************************************************************************

TASK [Gathering Facts] ****************************************************************************
ok: [localhost]

TASK [Touch a file] *******************************************************************************
--- before
+++ after
@@ -1,6 +1,6 @@
 {
- "atime": 1560693571.3594637,
- "mtime": 1560693571.3594637,
+ "atime": 1560693571.3620908,
+ "mtime": 1560693571.3620908,
 "path": "/tmp/myfile",
- "state": "absent"
+ "state": "touch"
 }

changed: [localhost]

PLAY RECAP ****************************************************************************************
localhost : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 

正如您所看到的,输出显示changed,这意味着有些东西发生了改变(更具体地说,文件被创建了),在输出中,我们可以看到类似 diff 的输出,告诉我们状态从absent移动到touch,这意味着文件被创建了。mtimeatime也发生了变化,但这可能是由于文件的创建和检查方式。

现在您已经学会了如何使用检查模式,让我们学习如何解决主机连接问题。

解决主机连接问题

Ansible 通常用于管理远程主机或系统。为此,Ansible 需要能够连接到远程主机,只有在那之后才能发出命令。有时,问题在于 Ansible 无法连接到远程主机。这种问题的典型例子是当您尝试管理尚未启动的机器时。能够快速识别这类问题并及时解决将帮助您节省大量时间。

按照以下步骤开始:

  1. 让我们创建一个名为remote.yaml的 playbook,其中包含以下内容:
---
- hosts: all
  tasks:
    - name: Touch a file
      file:
        path: /tmp/myfile
        state: touch
  1. 我们可以尝试针对不存在的 FQDN 运行remote.yaml playbook,如下所示:
$ ansible-playbook -i host.example.com, remote.yaml

在这种情况下,输出将清楚地告诉我们,SSH 服务没有及时回复:

PLAY [all] ****************************************************************************************

TASK [Gathering Facts] ****************************************************************************
fatal: [host.example.com]: UNREACHABLE! => {"changed": false, "msg": "Failed to connect to the host via ssh: ssh: Could not resolve hostname host.example.com: Name or service not known", "unreachable": true}

PLAY RECAP ****************************************************************************************
host.example.com : ok=0 changed=0 unreachable=1 failed=0 skipped=0 rescued=0 ignored=0 

我们也有可能收到不同的错误:

PLAY [all] ****************************************************************************************

TASK [Gathering Facts] ****************************************************************************
fatal: [host.example.com]: UNREACHABLE! => {"changed": false, "msg": "Failed to connect to the host via ssh: fale@host.example.com: Permission denied (publickey,gssapi-keyex,gssapi-with-mic).", "unreachable": true}

PLAY RECAP ****************************************************************************************
host.example.com : ok=0 changed=0 unreachable=1 failed=0 skipped=0 rescued=0 ignored=0 

在这种情况下,主机确实有回复,但我们没有足够的访问权限来 SSH 进入它。

SSH 连接通常因以下两个原因而失败:

  • SSH 客户端无法与 SSH 服务器建立连接

  • SSH 服务器拒绝 SSH 客户端提供的凭据

由于 OpenSSH 非常高的稳定性和向后兼容性,当第一个问题发生时,很可能是 IP 地址或端口错误,因此 TCP 连接不可行。很少情况下,这种错误发生在 SSH 特定问题中。通常,仔细检查 IP 和主机名(如果是 DNS,请检查是否解析为正确的 IP)可以解决问题。要进一步调查这个问题,您可以尝试从同一台机器执行 SSH 连接,以检查是否存在问题。例如,我会这样做:

$ ssh host.example.com -vvv

我已经从错误本身中获取了主机名,以确保我正在模拟 Ansible 正在做的事情。我这样做是为了确保我能看到 SSH 能够给我排除问题的所有可能日志消息。

第二个问题可能会更复杂一些,因为它可能出现多种原因。其中之一是您试图连接到错误的主机,而您没有该机器的凭据。另一个常见情况是用户名错误。要调试它,您可以使用错误中显示的user@host地址(在我的情况下是fale@host.example.com),并使用您之前使用的相同命令:

$ ssh fale@host.example.com -vvv

这应该引发与 Ansible 报告给您的相同错误,但更详细。

现在您已经学会了如何解决主机连接问题,让我们学习如何通过 CLI 传递工作变量。

通过 CLI 传递工作变量

在调试过程中,有一件事可以帮助,对于代码的可重用性也有帮助,那就是通过命令行向 playbooks 传递变量。每当您的应用程序(无论是 Ansible playbook 还是任何类型的应用程序)从第三方(在这种情况下是人)接收输入时,它都应该确保该值是合理的。一个例子是检查变量是否已设置,因此不是空字符串。这是一个安全的黄金法则,但在用户受信任时也应该应用,因为用户可能会误输入变量名。应用程序应该识别这一点,并通过保护自身来保护整个系统。按照以下步骤:

  1. 我们想要的第一件事是一个简单的 playbook,打印变量的内容。让我们创建一个名为printvar.yaml的 playbook,其中包含以下内容:
---
- hosts: localhost
  tasks:
    - debug:
       var: variable
  1. 现在我们有了一个 Ansible playbook,可以让我们查看变量是否设置为我们期望的值,让我们在执行语句中声明variable并运行它:
$ ansible-playbook printvar.yaml --extra-vars='{"variable": "Hello, World!"}'

通过运行这个,我们将收到类似以下的输出:

PLAY [localhost] **********************************************************************************

TASK [Gathering Facts] ****************************************************************************
ok: [localhost]

TASK [debug] **************************************************************************************
ok: [localhost] => {
 "variable": "Hello, World!"
}

PLAY RECAP ****************************************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 

Ansible 允许以不同的模式和不同的优先级设置变量。更具体地说,您可以使用以下方式设置它们:

  • 命令行值(最低优先级)

  • 角色默认值

  • 清单文件或脚本组vars

  • 清单group_vars/all

  • Playbook group_vars/all

  • 清单group_vars/*

  • Playbook group_vars/*

  • 清单文件或脚本主机变量

  • 清单host_vars/*

  • Playbook host_vars/*

  • 主机 facts/缓存set_facts

  • Play vars

  • Play vars_prompt

  • Play vars_files

  • 角色vars(在role/vars/main.yml中定义)

  • vars(仅适用于块中的任务)

  • 任务vars(仅适用于该任务)

  • include_vars

  • set_facts/registered vars

  • 角色(和include_role)参数

  • include参数

  • 额外变量(最高优先级)

如您所见,最后一个选项(也是所有选项中的最高优先级)是在执行命令中使用--extra-vars

现在您已经学会了如何通过 CLI 传递工作变量,让我们学习如何限制主机的执行。

限制主机的执行

在测试 playbook 时,对一组受限制的机器进行测试可能是有意义的;例如,只有一个。让我们开始吧:

  1. 为了使用 Ansible 的目标主机限制,我们需要一个 playbook。创建一个名为helloworld.yaml的 playbook,其中包含以下内容:
---
- hosts: all
  tasks:
    - debug:
        msg: "Hello, World!"
  1. 我们还需要创建一个至少包含两个主机的清单。在我的情况下,我创建了一个名为inventory的文件,其中包含以下内容:
[hosts]
host1.example.com
host2.example.com
host3.example.com

让我们用以下命令以通常的方式运行 playbook:

$ ansible-playbook -i inventory helloworld.yaml

通过这样做,我们将收到以下输出:

PLAY [all] ****************************************************************************************

TASK [Gathering Facts] ****************************************************************************
ok: [host1.example.com]
ok: [host3.example.com]
ok: [host2.example.com]

TASK [debug] **************************************************************************************
ok: [host1.example.com] => {
 "msg": "Hello, World!"
}
ok: [host2.example.com] => {
 "msg": "Hello, World!"
}
ok: [host3.example.com] => {
 "msg": "Hello, World!"
}

PLAY RECAP ****************************************************************************************
host1.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 
host2.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 
host3.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 

这意味着 playbook 在清单中的所有机器上执行。如果我们只想针对host3.example.com运行它,我们需要在命令行中指定如下:

$ ansible-playbook -i inventory helloworld.yaml --limit=host3.example.com

为了证明这个工作是按预期进行的,我们可以运行它。通过这样做,我们将收到以下输出:

PLAY [all] ****************************************************************************************

TASK [Gathering Facts] ****************************************************************************
ok: [host3.example.com]

TASK [debug] **************************************************************************************
ok: [host3.example.com] => {
 "msg": "Hello, World!"
}

PLAY RECAP ****************************************************************************************
host3.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 

在 Ansible 执行我们在命令行中提到的 playbook 之前,它会分析清单以检测哪些目标在范围内,哪些不在。通过使用--limit关键字,我们可以强制 Ansible 忽略所有超出限制参数指定范围的主机。

可以指定多个主机作为列表或使用模式,因此以下两个命令都将针对host2.example.comhost3.example.com执行 playbook:

$ ansible-playbook -i inventory helloworld.yaml --limit=host2.example.com,host3.example.com

$ ansible-playbook -i inventory helloworld.yaml --limit=host[2-3].example.com

限制不会覆盖清单,而是对其添加限制。所以,假设我们限制到不在清单中的主机,如下所示:

$ ansible-playbook -i inventory helloworld.yaml --limit=host4.example.com

在这里,我们将收到以下错误,并且什么也不会发生:

 [WARNING]: Could not match supplied host pattern, ignoring: host4.example.com

ERROR! Specified hosts and/or --limit does not match any hosts

现在您已经学会了如何限制主机的执行,让我们学习如何刷新代码缓存。

刷新代码缓存

在 IT 中,缓存被用来加快操作的速度,Ansible 也不例外。

通常,缓存是好的,因此它们被广泛地使用。然而,如果它们缓存了不应该缓存的值,或者即使值已经改变但它们没有被刷新,它们可能会造成一些问题。

在 Ansible 中刷新缓存非常简单,只需运行ansible-playbook,我们已经在运行了,加上--flush-cache选项,如下所示:

ansible-playbook -i inventory helloworld.yaml --flush-cache

Ansible 使用 Redis 保存主机变量和执行变量。有时,这些变量可能会被遗留下来,影响后续的执行。当 Ansible 发现一个变量应该在它刚刚启动的步骤中设置时,Ansible 可能会假设该步骤已经完成,因此会将旧变量作为刚刚创建的变量。通过使用--flush-cache选项,我们可以避免这种情况,因为它会确保 Ansible 在执行过程中刷新 Redis 缓存。

现在您已经学会了如何刷新代码缓存,让我们学习如何检查错误的语法。

检查错误的语法

确定一个文件是否具有正确的语法对于机器来说是相当容易的,但对于人类来说可能更复杂。这并不意味着机器能够为您修复代码,但它们可以快速地识别问题是否存在。为了使用 Ansible 内置的语法检查器,我们需要一个具有语法错误的 playbook。让我们开始吧:

  1. 让我们创建一个名为syntaxcheck.yaml的文件,其中包含以下内容:
---
- hosts: all
  tasks:
    - debug:
      msg: "Hello, World!"
  1. 现在,我们可以使用--syntax-check命令:
$ ansible-playbook syntaxcheck.yaml --syntax-check

通过这样做,我们将收到以下输出:

ERROR! 'msg' is not a valid attribute for a Task

The error appears to be in '/home/fale/ansible/Ansible2Cookbook/Ch11/syntaxcheck.yaml': line 4, column 7, but may
be elsewhere in the file depending on the exact syntax problem.

The offending line appears to be:

 tasks:
 - debug:
 ^ here

This error can be suppressed as a warning using the "invalid_task_attribute_failed" configuration
  1. 现在,我们可以继续修复第 4 行的缩进问题:
---
- hosts: all
  tasks:
    - debug:
        msg: "Hello, World!"

当我们重新检查语法时,我们会看到它现在不再返回错误:

$  ansible-playbook syntaxcheck-fixed.yaml --syntax-check

playbook: syntaxcheck.yaml

当语法检查没有发现任何错误时,输出将类似于先前的输出,其中列出了分析的文件而没有列出任何错误。

由于 Ansible 知道所有支持的模块中的所有支持选项,它可以快速读取您的代码并验证您提供的 YAML 是否包含所有必需的字段,并且不包含任何不受支持的字段。

总结

在本章中,您了解了 Ansible 提供的各种选项,以便您可以查找 Ansible 代码中的问题。更具体地说,您学会了如何使用主机事实来诊断故障,如何在 playbook 中包含测试,如何使用检查模式,如何解决主机连接问题,如何从 CLI 传递变量,如何将执行限制为主机的子集,如何刷新代码缓存以及如何检查错误语法。

在下一章中,您将学习如何开始使用 Ansible Tower。

问题

  1. True 或 False:调试模块允许您在 Ansible 执行期间打印变量的值或固定字符串。

A) True

B) False

  1. 哪个关键字允许 Ansible 强制限制主机的执行?

A) --limit

B) --max

C) --restrict

D) --force

E) --except

进一步阅读

关于 Ansible 错误处理的官方文档可以在docs.ansible.com/ansible/latest/user_guide/playbooks_error_handling.html找到。

第十二章:开始使用 Ansible Tower

Ansible 非常强大,但它需要用户使用 CLI。在某些情况下,这不是最佳选择,比如在需要从另一个作业触发 Ansible 作业的情况下(API 可能更好),或者在应该触发作业的人只能触发特定作业的情况下。对于这些情况,AWX 或 Ansible Tower 是更好的选择。

AWX 和 Ansible Tower 之间唯一的区别是 AWX 是上游和开源版本,而 Ansible Tower 是红帽公司的下游产品,它是官方支持的,但需要付费,并且交付方式也不同。AWX 作为一个 Docker 容器可以在任何地方运行,而 Ansible Tower 安装在系统上,需要特定版本的 Linux——具体来说是 RHEL 7.4+,RHEL 8.0+和 CentOS 7.4+。在撰写本文时,我们将使用 AWX 并讨论 AWX,但我们讨论的一切也适用于 Ansible Tower。

本章涵盖以下主题:

  • 安装 AWX

  • 从 AWX 运行你的第一个 playbook

  • 创建一个 AWX 项目

  • 创建一个清单

  • 创建一个作业模板

  • 运行一个作业

  • 控制对 AWX 的访问

  • 创建一个用户

  • 创建一个团队

  • 创建一个组织

  • 在 AWX 中分配权限

技术要求

虽然有几种安装 AWX 的方法,我们将使用建议的基于容器的 AWX 安装。因此,您的计算机上需要安装以下软件:

  • Ansible 2.4+。

  • Docker。

  • docker Python 模块。

  • docker-compose Python 模块。

  • 如果您的系统使用安全增强型 LinuxSELinux),您还需要libselinux Python 模块。

本章假设您已经按照第一章中详细介绍的方式设置了 Ansible 的控制主机,并且您正在使用最新版本——本章的示例是使用 Ansible 2.9 进行测试的。虽然本章将给出特定的主机名示例,但您可以自由地用您自己的主机名和/或 IP 地址替换它们,如何做到这一点的细节将在适当的地方提供。Docker 的安装超出了本书的范围,但您可以安装您的 Linux 操作系统提供的版本或 Docker CE。必需的 Python 模块可以通过使用pip或者通过操作系统包进行安装。

安装 AWX

在我们进一步讨论 AWX 之前,最好是您已经在您的计算机上安装了它,这样您就可以跟随解释并立即开始使用 AWX。安装 AWX 的最方便的方法是按照以下步骤进行:

  1. 首先,我们需要克隆 AWX Git 存储库,可以通过运行以下命令完成:
$ git clone https://github.com/ansible/awx.git
  1. 通过设置installer/inventory文件来修改密码和秘密的合理值(如pg_passwordrabbitmq_passwordadmin_passwordsecret_key)。

  2. 现在我们已经下载了 Ansible AWX 代码和安装程序,我们可以进入安装程序文件夹,并通过运行以下代码执行安装:

$ cd awx/installer
$ ansible-playbook -i inventory install.yml

install.yml playbook 为我们执行整个安装。它首先检查可能的错误配置或缺少依赖项的环境。如果一切正常,它会继续下载几个 Docker 镜像(包括 PostgreSQL,memcached,RabbitMQ,AWX Web 和 AWX workers),然后运行它们。

一旦 playbook 完成,您可以通过发出docker ps命令来检查安装,它应该输出类似以下内容:

CONTAINER ID  IMAGE                      COMMAND                 CREATED        STATUS        PORTS     NAMES
7e388622a9a5  ansible/awx_task:5.0.0     "/tini -- /bin/sh ..."  2 minutes ago  Up 2 minutes  8052/tcp awx_task
03946e9f7a74  ansible/awx_web:5.0.0      "/tini -- /bin/sh ..."  2 minutes ago  Up 2 minutes  0.0.0.0:80->8052/tcp awx_web
d1134f5dc89a  ansible/awx_rabbitmq:3.7.4 "docker-entrypoint..."  2 minutes ago  Up 2 minutes  4369/tcp, 5671-5672/tcp, 15671-15672/tcp, 25672/tcp awx_rabbitmq
2184596d2584  postgres:9.6               "docker-entrypoint..."  2 minutes ago  Up 2 minutes  5432/tcp awx_postgres
dd6ebe2f8c8e  memcached:alpine           "docker-entrypoint..."  2 minutes ago  Up 2 minutes  11211/tcp awx_memcached

如上面的输出所示,我们的系统现在有一个名为awx_web的容器,它已经绑定到端口80

现在,您可以通过浏览http://<您的 AWX 主机的 IP 地址>/并使用您在本节早期的清单文件中指定的凭据来访问 AWX。请注意,默认的管理员用户名是admin,除非您在清单中更改了它。

现在,您已经学会了安装 AWX 的必要步骤。让我们看看如何在 AWX 中创建项目。

从 AWX 运行您的第一个 playbook

与 Ansible 一样,在 AWX 中,目标是运行一个 Ansible playbook,每个运行的 playbook 称为作业。由于 AWX 比 Ansible 提供更多的灵活性和自动化,因此在运行第一个作业之前,需要进行一些配置,因此让我们开始创建 AWX 项目。

创建 AWX 项目

AWX 使用术语项目来标识 Ansible playbooks 的存储库。AWX 项目支持将 playbooks 放置在所有主要的源代码管理SCM)系统中,例如 Git、Mercurial 和 SVN,还支持文件系统上的 playbooks 或由 Red Hat Insights 提供的 playbooks。要创建项目,请按照以下步骤操作:

  1. 首先,您需要转到左侧菜单栏上的项目,然后单击屏幕左上部的绿色背景上带有白色加号的按钮。这将打开一个窗口,如下所示:

  1. 通过填写名称(样本库)并选择Git作为SCM 类型,窗口会增加新的参数:

  1. 现在,您可以添加 SCM URL(github.com/ansible/ansible-tower-samples)并单击现在应该可点击的保存按钮。

正如我们在本节开头提到的,项目是在 AWX 中存储和使用 playbooks 的系统。您可以想象,AWX 项目有许多有趣的附加配置,其中我认为最有趣的是启动时更新版本

如果标记了此选项,这将指示 Ansible 在运行该项目中的任何 playbook 之前始终更新 playbook 的存储库。这确保它始终执行 playbook 的最新版本。这是一个重要的功能,如果您没有勾选它,就有可能(sooner or later,在您的环境中会发生)有人注意到 playbook 中存在问题并修复它,然后他们运行 playbook 时确信自己运行的是最新版本。然后他们忘记在运行 playbook 之前运行同步任务,实际上运行的是旧版本的 playbook。如果以前的版本非常有 bug,这可能会导致重大问题。

使用此选项的缺点是,每次执行 playbook 时,实际上会运行两个 playbook,从而增加了任务执行的时间。我认为这是一个非常小的缺点,不足以抵消使用此选项的好处。

现在,您已经学会了在 Ansible Tower 中创建项目的必要步骤。让我们看看如何在下一节中创建清单。

创建清单

与 Ansible Core 一样,为了使 AWX 了解您的环境中存在的机器,我们使用清单。在 AWX 世界中,清单与 Ansible Core 中的等效物并没有太大的不同。让我们看看如何通过以下步骤在 AWX 中创建您的第一个清单:

  1. 单击左侧菜单栏中的清单选项。您将被重定向到清单窗口,在那里您可以通过单击屏幕左上部带有白色加号的按钮来创建您的第一个清单。这与我们创建新项目时不同,因为此按钮不会立即打开创建表单,而是会首先询问您是否要创建清单或智能清单。

  2. 选择清单选项后,将出现以下框:

  1. 在此窗口中,您需要设置一个名称,然后保存它。保存后,权限、组、主机、来源和已完成作业选项卡将变为可点击状态,因此您可以继续配置。

由于空清单毫无用处,我们将向其中添加localhost

  1. 要做到这一点,选择“主机”选项卡,然后单击屏幕左上角带有白色加号的绿色背景的按钮。这将打开一个窗口,如下所示:

  1. 然后,我们需要添加主机名(localhost)并通过向“变量”框中添加以下代码来指示 Ansible 使用本地连接:
---
ansible_connection: local
ansible_python_interpreter: '{{ ansible_playbook_python }}'
  1. 我们现在可以单击“保存”,保存我们的清单。

我们首先需要在创建清单或智能清单之间进行选择。这些选项之间有什么区别?在 AWX 中,清单与 Ansible Core 清单非常相似,但具有其他功能,例如内置的动态清单支持,这意味着您无需编辑配置文件或安装其他 Python 模块。要启用此功能,只需转到清单中的“来源”选项卡,选择从真实来源(例如公共云提供程序清单(亚马逊网络服务AWS)、Azure 和谷歌云平台GCP)都受支持),私有云清单(例如 VMWare 或 OpenStack)或其他系统,例如 Red Hat Satellite 或自定义脚本)自动填充清单的选项。

关于清单来源的特别说明是,如果选择了“从项目中获取”,将提供以下形式的表单:

在我看来,这是一个非常有趣的功能,因为它允许用户将自己设计的动态清单脚本检入到 Git 存储库中(可以是独立的存储库,也可以是您放入 playbook 的存储库),AWX 从存储库中提取该信息。

至于项目,当您向清单添加源时,您可能选择选择“启动时更新”选项,该选项的行为与项目的“启动时更新”选项的行为完全相同。因此,我强烈建议您也使用此选项。

智能清单是由 AWX 填充的清单,从其他清单中的主机开始,通过使用用户在创建过程中选择的特定智能主机过滤器对它们进行过滤。这对于动态创建具有特定类型主机的清单并根据过滤器保存手动创建许多不同组的需求或更糟糕的是不得不多次添加相同的主机非常有用。

现在,您已经学会了在 AWX 中创建清单所需的步骤。让我们看看如何创建作业模板。

创建作业模板

现在我们已经在项目中有了 playbook,在清单中有了主机,我们可以继续创建作业模板

AWX 中的作业模板是执行作业所需的配置的集合。这与ansible-playbook命令行选项非常相似。我们需要创建作业模板的原因是,可以启动带有很少或没有用户输入的 playbook 运行,这意味着它们可以被委派给可能不知道 playbook 工作原理的团队,甚至可以在没有任何人在场的情况下定期运行:

  1. 首先,您需要单击左侧菜单栏上的“模板”选项。

  2. 现在,您可以通过单击屏幕左上角带有白色加号的绿色背景的按钮来创建新模板。它会询问您是否要创建作业模板或工作流模板-您需要选择作业模板。将出现以下窗口:

如您所见,此视图中有相当多的字段。继续所需的唯一信息是名称(我们将输入Hello World)、清单(我们将选择在本章节的创建清单部分中创建的“测试清单”)、项目(我们将选择在本章节的前一部分中创建的“示例库”项目)和 playbook(我们将选择hello_world.yml,这是唯一可用的 playbook)。然后,我们可以点击保存。请注意,因为我们使用本地连接到localhost运行它,所以不需要创建或指定任何凭据。但是,如果您要针对一个或多个远程主机运行作业模板,则需要创建一个机器凭据,并将其与您的作业模板关联。机器凭据,例如 SSH 用户名和密码或 SSH 用户名和私钥,这些都安全存储在 AWX 的后端数据库中,这意味着您可以再次将与 playbook 相关的任务委派给其他团队,而无需实际提供密码或 SSH 密钥。

我们首先要选择的是创建作业模板还是工作流模板。我们选择了作业模板,因为我们希望能够从这个模板创建简单的作业。也可以创建更复杂的作业,这些作业是由多个作业模板组成,其中一个作业和下一个作业之间具有流程控制功能。这允许更复杂的情况和场景,您可能希望有多个作业(例如创建实例、公司定制、设置 Oracle 数据库、设置 MySQL 数据库等),但您也希望有一个一键部署,例如设置机器、应用所有公司定制并安装 MySQL 数据库。显然,您可能还有另一个部署,使用所有相同的组件,除了最后一个组件,它使用 Oracle 数据库部分来创建 Oracle 数据库机器。这使您能够拥有极大的灵活性,并且可以重复使用许多组件,创建多个几乎相同的 playbook。

有趣的是,作业模板创建窗口中的许多字段都有一个带有“启动时提示”的选项。这是为了能够在创建作业模板时可选地设置此值,但也允许运行作业的用户在运行时输入/覆盖它。当每次运行时有一个字段发生变化时(也许是“限制”字段,它与ansible-playbook命令一起使用时的操作方式相同),或者可以用作健全性检查,因为它在实际运行 playbook 之前提示用户输入值(并让他们有机会修改它)。但是,这可能会阻止计划的作业运行,因此在启用此功能时要小心。

现在,您已经学会了在 AWX 中创建作业模板的必要步骤。让我们看看如何创建作业。

运行作业

作业是作业模板的一个实例,正如名称所示。这意味着要在我们的机器上执行任何操作,我们必须创建一个作业模板实例,或者更简单地说,一个作业,按照以下步骤进行:

  1. 现在我们已经设置了作业模板,我们可以运行作业本身。要这样做,我们需要转到页面左侧的“模板”项目。

  2. 找到你想要运行的作业模板(在我们的例子中,这将是Hello World),然后点击页面右侧对应正确作业模板的小火箭,如下面的截图所示:

当作业运行时,AWX 允许我们在作业的仪表板中跟踪作业执行,如下面的截图所示:

在屏幕的右侧,作业执行期间加载作业输出,而左侧提供有关作业的信息。AWX 和 Ansible Tower 的一个很棒的功能是它们在后端数据库中存档了作业执行输出,这意味着您可以在将来的任何时间回来查询作业运行,查看发生了什么变化。这对于审计和策略执行等场合非常强大和有用。

现在,您已经学会了在 AWX 中创建作业的必要步骤。让我们看看如何创建用户。

控制对 AWX 的访问

在我看来,AWX 相对于 Ansible 的最大优势之一是,AWX 允许多个用户连接和控制/执行操作。这使得公司可以为不同团队、整个组织甚至多个组织安装单个 AWX,这是非常有利的。

基于角色的访问控制RBAC)系统用于管理用户的权限。

AWX 和 Ansible Tower 都可以链接到中央目录,例如轻量级目录访问协议LDAP)和 Azure 活动目录,但我们也可以在 AWX 服务器本身上创建本地用户帐户。让我们从在本地创建我们的第一个用户帐户开始!

创建用户

AWX 的一个重要优势是能够管理多个用户。这使我们可以为每个使用 AWX 系统的人创建一个用户,以确保他们只被授予他们需要的权限。此外,通过使用个人帐户,我们可以确保通过审计日志查看谁执行了什么操作。要创建用户,请按照以下步骤进行:

  1. 转到屏幕左侧的菜单栏,选择“用户”选项。

  2. 现在您可以看到用户列表,并且可以通过单击屏幕左上角带有白色加号的绿色背景按钮来创建新用户。将出现以下表单:

通过添加电子邮件地址、用户名和密码(确认密码),您可以创建新用户。

用户可以分为三种类型:

  • 普通用户:此类型的用户没有任何继承权限,他们需要被授予特定权限才能执行任何操作。

  • 系统审计员:此类型的用户在整个 AWX 安装中具有完全只读权限。

  • 系统管理员:此类型的用户在整个 AWX 安装中拥有完全权限。

现在,您已经学会了在 AWX 中创建用户的必要步骤。让我们简要了解一下团队。

创建团队

尽管拥有个人用户帐户是一种非常强大的工具,特别是对于企业用例,但对每个对象(例如作业模板或清单)逐个设置权限将非常不便和繁琐。每当有人加入团队时,他们的用户帐户必须手动配置为正确的权限,以对每个对象进行操作,并且如果他们离开,也必须手动删除。

AWX 和 Ansible Tower 具有与大多数其他 RBAC 系统中相同的用户分组概念。唯一的细微差别是,在用户界面中,它们被称为团队,而不是群组。但是,您可以简单轻松地创建团队,然后根据需要添加和删除用户。通过用户界面进行此操作非常简单直接,您会发现该过程类似于大多数 RBAC 系统处理用户组的方式,因此我们在这里不会进一步详细说明。

一旦您设置好团队,我建议您将权限分配给团队,而不是通过单个用户,因为随着组织的增长,这将使您更轻松地管理 AWX 对象权限。说到组织,让我们在下一节中看看 AWX 中组织的概念。

创建组织

有时,您可能需要管理多个独立的人员组,这些人员组需要管理独立的机器。对于这种情况,可以使用组织来帮助您。组织基本上是 AWX 的租户,拥有自己独特的用户帐户、团队、项目、清单和作业模板——这几乎就像拥有 AWX 的单独实例!要创建组织,您需要执行以下步骤:

  1. 要创建新组织,您需要转到屏幕左侧的“组织”选项。

  2. 然后,您可以查看现有的组织,并通过单击屏幕左上部分带有绿色背景的白色加号按钮来创建新的组织。

将出现以下窗口:

由于唯一的强制字段是名称,您只需填写名称并单击保存。

创建组织后,您可以将任何类型的资源分配给组织,例如项目、模板、清单、用户等。组织是一个简单的概念,但在 AWX 中在分隔角色和责任方面非常强大。最后,在完成本节之前,让我们看看如何在 AWX 中分配权限。

在 AWX 中分配权限

您可能已经注意到,在我们通过配置 AWX 中的第一个项目、清单和作业模板的过程中,我们导航到的大多数屏幕上都有一个名为“权限”的按钮。当我们使用管理员帐户导航用户界面时,我们可以看到所有选项,但当然,您不希望给每个用户都授予管理员权限。

可以根据对象授予个别用户(或他们所属的团队)权限。例如,您可以有一个数据库管理员团队,他们只能访问和执行特定于其角色的数据库服务器清单上的 playbook,并使用特定于其角色的作业模板。然后,Linux 系统管理员可以访问特定于其角色的清单、项目和作业模板。AWX 会隐藏用户没有权限的对象,这意味着数据库管理员永远看不到 Linux 系统管理员的对象,反之亦然。

您可以授予用户(或团队)多种不同的权限级别,包括以下内容:

  • 管理员:这相当于系统管理员的组织级别等价物。

  • 执行:这种用户只能执行组织中的模板。

  • 项目管理员:这种用户可以更改组织中的任何项目。

  • 清单管理员:这种用户可以更改组织中的任何清单。

  • 凭证管理员:这种用户可以更改组织中的任何凭证。

  • 工作流管理员:这种用户可以更改组织中的任何工作流。

  • 通知管理员:这种用户可以更改组织中的任何通知。

  • 作业模板管理员:这种用户可以更改组织中的任何作业模板。

  • 审计员:这相当于系统审计员的组织级别等价物。

  • 成员:这相当于普通用户的组织级别等价物。

  • 读取:这种用户可以查看组织中的非敏感对象。

这就结束了我们对 AWX 中 RBAC 的简要介绍以及对这个强大工具的介绍。AWX 是 Ansible 在企业环境中强大功能的一个很好的补充,确实有助于确保用户可以以良好管理、安全和可审计的方式运行 Ansible playbooks。在本章中,我们只是触及了表面,但希望本章能让您了解 AWX 如何帮助您的团队或企业的自动化之旅。

摘要

AWX 和 Ansible Tower 是强大的、互补的工具,强力支持在企业或团队环境中使用 Ansible。它们可以帮助安全地存储凭据,否则你将不得不广泛分发,审计 playbook 运行的历史,并强制执行 playbook 的版本控制。这些工具的基于 Web 的用户界面为最终用户创建了一个低门槛,这意味着 playbook 运行可以轻松委托给否则对 Ansible 知识很少的团队(只要适当的升级路径被设置好,以防问题出现)。简而言之,在企业环境中实施 Ansible 时,如果没有添加 Ansible Tower 或 AWX,其使用就不应被视为完整。

在本章中,您学习了如何在 Linux 主机上安装 AWX,以及从 AWX 运行您的第一个 playbook 所需的步骤。您还了解了 AWX 中的 RBAC 以及它如何支持企业环境中的大型多用户环境。

我们现在已经到达了本书的结尾,因为这是最后一章,我想感谢您阅读整本书,希望它教会了您最初希望了解的有关 Ansible 的知识。

问题

  1. C) 作业模板

E) 项目

B) 假

A) 用户

在 Ansible Tower 中,您可以创建哪些对象?

B) 作业

  1. 真或假 - AWX 是 Red Hat Ansible Tower 的上游和开源版本。

A) 真

D) 模块

第十三章:评估

第一章

  1. A,B

  2. C

  3. A

第二章

  1. C

  2. B

  3. A

第三章

  1. E

  2. C

  3. A

第四章

  1. C

  2. A

  3. B

第五章

  1. D

  2. E

  3. A

第六章

  1. D

  2. A

  3. A

第七章

  1. D

  2. A

  3. B

第八章

  1. C

  2. B

  3. A

第九章

  1. D

  2. A

  3. A

第十章

  1. D

  2. A

  3. B

  4. B

  5. A

第十一章

  1. A

  2. A

第十二章

  1. A,B,C,E

  2. A

posted @ 2024-05-20 11:58  绝不原创的飞龙  阅读(23)  评论(0编辑  收藏  举报