Ansible-2-7-学习手册-全-

Ansible 2.7 学习手册(全)

原文:zh.annas-archive.org/md5/89BF78DDE1DEE382F084F8254DF8B8DD

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

信息技术领域是一个快速发展的领域,始终试图加速。为了跟上这一步伐,公司需要能够快速行动并频繁迭代。直到几年前,这主要适用于软件,但现在我们开始看到以类似速度开发基础设施的必要性。未来,我们将需要以软件本身的速度改变我们运行软件的基础设施。

在这种场景下,许多技术(如软件定义的一切,例如存储、网络和计算)将至关重要,但这些技术需要以同样可扩展的方式进行管理,而这种方式将涉及使用 Ansible 和类似产品。

今天,Ansible 非常相关,因为与竞争产品不同,它是无代理的,可以实现更快的部署、更高的安全性和更好的可审计性。

本书适用对象

本书适用于希望使用 Ansible 2 自动化其组织基础架构的开发人员和系统管理员。不需要有关 Ansible 的先前知识。

本书涵盖内容

第一章,开始使用 Ansible,讲解了如何安装 Ansible。

第二章,自动化简单任务,讲解了如何创建简单的 Playbook,让您能够自动化一些您每天已经执行的简单任务。

第三章,扩展至多个主机,讲解了如何以易于扩展的方式处理 Ansible 中的多个主机。

第四章,处理复杂部署,讲解了如何创建具有多个阶段和多台机器的部署。

第五章,走向云端,讲解了 Ansible 如何与各种云服务集成,以及如何简化您的生活,为您管理云端。

第六章,从 Ansible 获取通知,讲解了如何设置 Ansible 以向您和其他利益相关者返回有价值的信息。

第七章,创建自定义模块,讲解了如何创建自定义模块以利用 Ansible 给予你的自由。

第八章,调试和错误处理,讲解了如何调试和测试 Ansible 以确保您的 Playbook 总是有效的。

第九章,复杂环境,讲解了如何使用 Ansible 管理多个层次、环境和部署。

第十章,介绍企业级 Ansible,讲解了如何从 Ansible 管理 Windows 节点,以及如何利用 Ansible Galaxy 来最大化您的生产力。

第十一章,开始使用 AWX,解释了 AWX 是什么以及如何开始使用它。

第十二章,与 AWX 用户、权限和组织一起工作,解释了 AWX 用户和权限管理的工作原理。

要充分利用本书

本书假定您具有 UNIX shell 的基本知识,以及基本的网络知识。

下载示例代码文件

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

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

  1. 登录或注册,网址为 www.packt.com

  2. 选择“支持”选项卡。

  3. 点击“代码下载和勘误”。

  4. 在搜索框中输入书名,然后按照屏幕上的说明操作。

文件下载完成后,请确保使用最新版本的软件解压或提取文件夹:

  • Windows 上的 WinRAR/7-Zip

  • Mac 上的 Zipeg/iZip/UnRarX

  • Linux 上的 7-Zip/PeaZip

本书的代码包也托管在 GitHub 上,网址为 github.com/PacktPublishing/Learning-Ansible-2.X-Third-Edition。如果代码有更新,将在现有的 GitHub 存储库中更新。

我们还有来自丰富书籍和视频目录的其他代码包可供使用。请查看:github.com/PacktPublishing/

使用的约定

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

CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。以下是一个示例:"sudo 命令是一个众所周知的命令,但通常以更危险的形式使用。"

代码块设置如下:

- hosts: all 
  remote_user: vagrant
  tasks: 
    - name: Ensure the HTTPd package is installed 
      yum: 
        name: httpd 
        state: present 
      become: True 

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

- hosts: all 
  remote_user: vagrant
  tasks: 
    - name: Ensure the HTTPd package is installed 
      yum: 
        name: httpd 
        state: present 
      become: True

任何命令行输入或输出都按如下方式编写:

$ sudo dnf install ansible

警告或重要提示如下。

技巧和提示如下。

联系我们

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

一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并发送邮件至customercare@packtpub.com

勘误:尽管我们已尽一切努力确保内容准确无误,但错误难免会发生。如果您在本书中发现错误,我们将不胜感激。请访问 www.packt.com/submit-errata,选择您的书籍,点击勘误提交表链接,并输入详细信息。

盗版:如果您在互联网上发现我们作品的任何形式的非法副本,我们将不胜感激您提供位置地址或网站名称。请通过copyright@packt.com联系我们,并提供材料链接。

如果您有兴趣成为作者:如果您有专业知识并对撰写或为书籍做贡献感兴趣,请访问 authors.packtpub.com

评论

请留下您的评论。当您阅读并使用了本书后,为何不在您购买的网站上留下评论呢?潜在的读者可以看到并使用您的客观意见来做出购买决定,我们在 Packt 公司可以了解您对我们产品的看法,而我们的作者也可以看到您对他们的书的反馈。谢谢!

欲了解有关 Packt 的更多信息,请访问 packt.com

第一节:使用 Ansible 创建 Web 服务器

本节将帮助您创建简单的 Playbooks,使您能够自动化一些您每天已经执行的简单任务。

本节包括以下章节:

  • 第一章,开始使用 Ansible

  • 第二章,自动化简单任务

使用 Ansible 入门

信息和通信技术ICT)通常被描述为一个快速增长的行业。我认为 ICT 行业最好的特质与其能够以超高速度增长无关,而是与其能够以惊人的速度革新自身和世界其他部分的能力有关。

每隔 10 到 15 年,该行业都会发生重大转变,每次转变都会解决之前非常难以管理的问题,从而带来新的挑战。此外,在每次重大转变中,许多先前迭代的最佳实践被归类为反模式,并创建了新的最佳实践。虽然这些变化可能看起来无法预测,但这并不总是正确的。显然,不可能准确地知道将发生什么变化以及何时会发生,但通常观察拥有大量服务器和许多代码行的公司可以揭示下一步的走向。

当前的转变已经在亚马逊网络服务(Amazon Web Services, AWS)、Facebook 和谷歌等大公司中发生。这是实施 IT 自动化系统来创建和管理服务器。

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

  • IT 自动化

  • 什么是 Ansible?

  • 安全外壳

  • 安装 Ansible

  • 使用 Vagrant 创建测试环境

  • 版本控制系统

  • 使用 Ansible 与 Git

技术要求

为了支持学习 Ansible,建议拥有一台可以安装 Vagrant 的机器。使用 Vagrant 将允许您尝试许多操作,甚至是破坏性操作,而不必担心。

此外,建议拥有 AWS 和 Azure 帐户,因为其中一些示例将在这些平台上展示。

本书中的所有示例都可以在 GitHub 仓库中找到:github.com/PacktPublishing/-Learning-Ansible-2.X-Third-Edition/

IT 自动化

IT 自动化在更广泛的意义上是指帮助管理 IT 基础设施(服务器、网络和存储)的过程和软件。在当前的转变中,我们正在支持大规模实施这些过程和软件。

在 IT 历史的早期阶段,服务器数量很少,需要很多人来确保它们正常工作,通常每台机器需要多于一个人。随着时间的推移,服务器变得更可靠、更容易管理,因此可以有一个系统管理员管理多个服务器。在那个时期,管理员手动安装软件,手动升级软件,并手动更改配置文件。这显然是一个非常耗时且容易出错的过程,因此许多管理员开始实施脚本和其他手段来简化他们的生活。这些脚本通常非常复杂,而且不太容易扩展。

在本世纪初,由于公司的需求,数据中心开始快速增长。虚拟化有助于降低成本,而且许多这些服务都是 Web 服务,这意味着许多服务器彼此非常相似。此时,需要新的工具来替代以前使用的脚本:配置管理工具。

CFEngine 是在上世纪九十年代展示配置管理功能的第一个工具;最近,除了 Ansible 还有 Puppet、Chef 和 Salt。

IT 自动化的优点

人们常常疑惑 IT 自动化是否真的带来足够的优势,考虑到实施它存在一些直接和间接的成本。IT 自动化的主要好处包括:

  • 快速提供机器的能力

  • 能够在几分钟内从头开始重建一台机器的能力

  • 能够追踪对基础设施进行的任何更改

凭借这些优点,通过减少系统管理员经常执行的重复操作,可以降低管理 IT 基础设施的成本。

IT 自动化的缺点

与任何其他技术一样,IT 自动化也存在一些缺点。从我的角度来看,这些是最大的缺点:

  • 自动化曾经用于培训新的系统管理员的所有小任务。

  • 如果发生错误,它将在所有地方传播。

第一个的结果是需要采取新的方法来培训初级系统管理员。

第二个问题更棘手。有很多方法来限制这种损害,但没有一种方法能完全防止。以下是可用的缓解选项:

  • 始终备份:备份无法防止你毁掉你的机器 - 它们只能使恢复过程成为可能。

  • 始终在非生产环境中测试你的基础设施代码(playbooks/roles):公司已经开发了不同的流程来部署代码,通常包括开发、测试、暂存和生产等环境。使用相同的流程来测试您的基础设施代码。如果有一个有错误的应用程序到达生产环境,可能会有问题。如果有一个有错误的 playbook 到达生产环境,情况就可能变得灾难性。

  • 始终对基础设施代码进行同行评审:一些公司已经引入了对应用代码的同行评审,但很少有公司对基础设施代码进行同行评审。如我之前所说,在我看来,基础设施代码比应用代码更加关键,所以你应该始终对基础设施代码进行同行评审,无论你是否对应用代码进行同行评审。

  • 启用 SELinux:SELinux 是一个安全内核模块,在所有 Linux 发行版上都可用(默认情况下安装在 Fedora、Red Hat Enterprise Linux、CentOS、Scientific Linux 和 Unbreakable Linux 上)。它允许您以非常细粒度的方式限制用户和进程权限。我建议使用 SELinux 而不是其他类似模块(如 AppArmor),因为它能够处理更多情况和权限。SELinux 将防止大量损坏,因为如果配置正确,它将阻止执行许多危险命令。

  • 以有限权限账户运行 playbook:尽管用户和特权升级方案在 Unix 代码中已经存在了 40 多年,但似乎并不多有公司使用它们。对于所有你的 playbook 使用有限用户,并仅在需要更高权限的命令时提升权限,这样做将有助于防止在尝试清理应用程序临时文件时意外清除机器。

  • 使用水平特权升级sudo 命令是众所周知的,但通常以更危险的形式使用。sudo 命令支持 -u 参数,允许您指定要模拟的用户。如果您必须更改属于另一个用户的文件,请不要升级到 root,而是升级到该用户。在 Ansible 中,您可以使用 become_user 参数来实现这一点。

  • 尽可能不要同时在所有机器上运行 playbook:分阶段的部署可以帮助您在为时已晚之前检测到问题。许多问题在开发、测试、暂存和 QA 环境中无法检测到。其中大多数与在这些非生产环境中无法正确模拟的负载相关。您刚刚添加到 Apache HTTPd 或 MySQL 服务器的新配置可能从语法上来说是完全正确的,但对您的生产负载下的特定应用程序来说可能是灾难性的。分阶段的部署将允许您在实际负载下测试您的新配置,而不会在出现问题时造成停机。

  • 避免猜测命令和修改器:许多系统管理员会尝试记住正确的参数,并在记不清楚时尝试猜测。我也经常这样做,但这是非常危险的。查看手册页或在线文档通常不会花费您两分钟,而且通常通过阅读手册,您会发现您不知道的有趣笔记。猜测修改器是危险的,因为您可能会被非标准的修改器所愚弄(即 -v 不是 grep 的详细模式,而 -h 不是 MySQL CLI 的 help 命令)。

  • 避免容易出错的命令:并非所有命令都是平等创建的。一些命令(远远)比其他命令更危险。如果你可以假设一个基于 cat 的命令是安全的,那么你必须假设一个基于 dd 的命令是危险的,因为它执行文件和卷的复制和转换。我见过有人在脚本中使用 dd 来将 DOS 文件转换为 Unix(而不是 dos2unix)等许多其他非常危险的例子。请避免这样的命令,因为如果出了问题,可能会导致巨大的灾难。

  • 避免不必要的修改器:如果你需要删除一个简单的文件,使用 rm ${file},而不是 rm -rf ${file}。后者经常被那些已经学会确保安全,总是使用 rm -rf 的用户执行,因为在他们过去的某个时候,他们曾经需要删除一个文件夹。如果 ${file} 变量设置错误,这将防止你删除整个文件夹。

  • 始终检查变量未设置时可能发生的情况:如果你想删除文件夹的内容,并且你使用 rm -rf ${folder}/* 命令,那你就是在找麻烦。如果某种原因导致 ${folder} 变量未设置,shell 将读取一个 rm -rf /* 命令,这是致命的(考虑到 rm -rf / 命令在大多数当前操作系统上不起作用,因为它需要一个 --no-preserve-root 选项,而 rm -rf /* 将按预期工作)。我使用这个特定的命令作为示例,因为我见过这样的情况:变量是从一个由于一些维护工作而关闭的数据库中提取出来的,然后给该变量赋了一个空字符串。接下来会发生什么,可能很容易猜到。如果你不能阻止在危险的地方使用变量,至少检查它们是否为空,然后再使用。这不会挽救你免受每一个问题的困扰,但可能会避免一些最常见的问题。

  • 仔细检查你的重定向:重定向(连同管道)是 Unix shell 中最强大的元素。它们也可能非常危险:一个 cat /dev/rand > /dev/sda 可以摧毁一个磁盘,即使一个基于 cat 的命令通常被忽视,因为它通常不危险。始终仔细检查包含重定向的所有命令。

  • 尽可能使用特定的模块:在这个列表中,我使用了 shell 命令,因为很多人会试图将 Ansible 当作一种分发它们的方式:这不是。Ansible 提供了很多模块,我们将在本书中看到它们。它们将帮助你创建更可读、可移植和安全的 playbooks。

IT 自动化的类型

有很多方法可以对 IT 自动化系统进行分类,但迄今为止最重要的是与配置如何传播有关。基于此,我们可以区分基于代理的系统和无代理的系统。

基于代理的系统

基于代理的系统有两个不同的组件:一个服务器,和一个称为代理的客户端。

只有一个服务器,它包含了整个环境的所有配置,而代理与环境中的机器数量一样多。

在某些情况下,可能会出现多个服务器以确保高可用性,但要将其视为单个服务器,因为它们将以相同的方式配置。

客户端定期会联系服务器,以查看是否存在其机器的新配置。如果有新配置存在,客户端将下载并应用它。

无代理系统

在无代理系统中,不存在特定的代理。无代理系统并不总是遵循服务器/客户端范式,因为可能存在多个服务器,甚至可能有与服务器数量相同的服务器和客户端。通信是由服务器初始化的,它将使用标准协议(通常是通过 SSH 和 PowerShell)联系客户端。

基于代理与无代理系统

除了先前概述的差异之外,还有其他由于这些差异而产生的对比因素。

从安全性的角度来看,基于代理的系统可能不太安全。由于所有机器都必须能够启动与服务器机器的连接,因此这台机器可能比无代理情况下更容易受到攻击,而后者通常位于不会接受任何传入连接的防火墙后面

从性能的角度来看,基于代理的系统存在使服务器饱和的风险,因此部署可能较慢。还需要考虑到,在纯代理系统中,不可能立即向一组机器推送更新。它将不得不等待这些机器进行检查。因此,多个基于代理的系统已经实现了超出带外方式来实现这些功能。诸如 Chef 和 Puppet 之类的工具是基于代理的,但也可以在没有集中式服务器的情况下运行,以扩展大量的机器,分别称为无服务器 Chef无主 Puppet

无代理系统更容易集成到已有的基础设施(褐地)中,因为客户端会将其视为普通的 SSH 连接,因此不需要额外的配置。

什么是 Ansible?

Ansible是一种无代理 IT 自动化工具,由前红帽公司的员工 Michael DeHaan 于 2012 年开发。Ansible 的设计目标是尽可能精简、一致、安全、高度可靠和易于学习。Ansible 公司于 2015 年 10 月被红帽公司收购,现在作为红帽公司的一部分运营。

Ansible 主要使用 SSH 以推送模式运行,但您也可以使用 ansible-pull 运行 Ansible,在每个代理上安装 Ansible,本地下载 playbooks,并在各个机器上运行它们。如果有大量的机器(大量是一个相对的术语;但在这种情况下,将其视为大于 500 台),并且您计划并行部署更新到机器上,则可能是正确的方法。正如我们之前讨论过的,无论是代理模式还是无代理系统都有其优缺点。

在下一节中,我们将讨论安全外壳(SSH),这是 Ansible 和 Ansible 理念的核心部分。

安全外壳

安全外壳(也称为 SSH)是一种网络服务,允许您通过完全加密的连接远程登录并访问外壳。今天,SSH 守护程序已成为 UNIX 系统管理的标准,取代了未加密的 telnet。SSH 协议的最常用实现是 OpenSSH。

在过去几年中,微软已为 Windows 实现了 OpenSSH。我认为这证明了 SSH 所处的事实标准情况。

由于 Ansible 以与任何其他 SSH 客户端相同的方式执行 SSH 连接和命令,因此未对 OpenSSH 服务器应用特定配置。

要加快默认的 SSH 连接,您可以始终启用 ControlPersist 和管道模式,这使得 Ansible 更快速、更安全。

为什么选择 Ansible?

我们将在本书的过程中尝试比较 Ansible 与 Puppet 和 Chef,因为许多人对这些工具有很好的经验。我们还将具体指出 Ansible 如何解决与 Chef 或 Puppet 相比的问题。

Ansible、Puppet 和 Chef 都是声明性的,期望将一台机器移动到配置中指定的期望状态。例如,在这些工具中的每一个中,为了在某个时间点启动服务并在重新启动时自动启动,您需要编写一个声明性的块或模块;每次工具在机器上运行时,它都会努力获得您的 playbook(Ansible)、cookbook(Chef)或 manifest(Puppet)中定义的状态。

在简单水平上,工具集之间的差异很小,但随着情况的增多和复杂性的增加,您会开始发现不同工具集之间的差异。在 Puppet 中,您不设置任务执行的顺序,Puppet 服务器会在运行时决定序列和并行执行,这使得最终可能出现难以调试的错误变得更加容易。要利用 Chef 的功能,您需要一个优秀的 Ruby 团队。您的团队需要擅长 Ruby 语言,以定制 Puppet 和 Chef,而且使用这两种工具会有更大的学习曲线。

与 Ansible 不同的是。它在执行顺序上使用了 Chef 的简单性 - 自上而下的方法 - 并允许你以 YAML 格式定义最终状态,这使得代码极易阅读和易于理解,无论是从开发团队到运维团队,都能轻松掌握并进行更改。在许多情况下,即使没有 Ansible,运维团队也会被给予 playbook 手册,以便在遇到问题时执行指令。Ansible 模仿了那种行为。如果由于其简单性而最终使你的项目经理更改 Ansible 代码并将其提交到 Git,也不要感到惊讶!

安装 Ansible

安装 Ansible 相当快速简单。你可以直接使用源代码,从 GitHub 项目 (github.com/ansible/ansible) 克隆;使用系统的软件包管理器进行安装;或者使用 Python 的软件包管理工具 (pip)。你可以在任何 Windows 或类 Unix 系统上使用 Ansible,比如 macOS 和 Linux。Ansible 不需要任何数据库,也不需要运行任何守护程序。这使得维护 Ansible 版本和升级变得更容易,而且没有任何中断。

我们想要称呼我们将要安装 Ansible 的机器为 Ansible 工作站。有些人也将其称为指挥中心。

使用系统的软件包管理器安装 Ansible

可以使用系统的软件包管理器安装 Ansible,就我个人而言,如果你的系统软件包管理器至少提供了 Ansible 2.0,这是首选选项。我们将研究通过 YumAptHomebrewpip 安装 Ansible。

通过 Yum 安装

如果你正在运行 Fedora 系统,你可以直接安装 Ansible,因为从 Fedora 22 开始,Ansible 2.0+ 可以在官方仓库中找到。你可以按照以下步骤安装它:

    $ sudo dnf install ansible

对于 RHEL 和基于 RHEL 的系统(CentOS、Scientific Linux 和 Unbreakable Linux),版本 6 和 7 在 EPEL 仓库中有 Ansible 2.0+,因此在安装 Ansible 之前,你应该确保已启用 EPEL 仓库,步骤如下:

    $ sudo yum install ansible

在 RHEL 6 上,你必须运行 $ sudo rpm -Uvh http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm 命令来安装 EPEL,而在 RHEL 7 上,$ sudo yum install epel-release 就足够了。

通过 Apt 安装

Ansible 可用于 Ubuntu 和 Debian。要在这些操作系统上安装 Ansible,请使用以下命令:

    $ sudo apt-get install ansible

通过 Homebrew 安装

你可以使用 Homebrew 在 Mac OS X 上安装 Ansible,步骤如下:

    $ brew update
    $ brew install ansible

通过 pip 安装

你可以通过 pip 安装 Ansible。如果你的系统上没有安装 pip,先安装它。你也可以在 Windows 上使用以下命令行使用 pip 安装 Ansible:

    $ sudo easy_install pip

现在你可以使用 pip 安装 Ansible,步骤如下:

    $ sudo pip install ansible

安装完 Ansible 后,运行 ansible --version 来验证是否已经安装:

    $ ansible --version

从上述命令行输出中,你将得到许多行,如下所示:

ansible 2.7.1
 config file = /etc/ansible/ansible.cfg
 configured module search path = [u'/home/fale/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
 ansible python module location = /usr/lib/python2.7/site-packages/ansible
 executable location = /bin/ansible
 python version = 2.7.15 (default, Oct 15 2018, 15:24:06) [GCC 8.1.1 20180712 (Red Hat 8.1.1-5)]

从源代码安装 Ansible

如果前面的方法不适合您的用例,您可以直接从源代码安装 Ansible。从源代码安装不需要任何 root 权限。让我们克隆一个存储库并激活 virtualenv,它是 Python 中的一个隔离环境,您可以在其中安装包而不会干扰系统的 Python 包。存储库的命令和结果输出如下:

    $ git clone git://github.com/ansible/ansible.git
    Cloning into 'ansible'...
    remote: Counting objects: 116403, done.
    remote: Compressing objects: 100% (18/18), done.
    remote: Total 116403 (delta 3), reused 0 (delta 0), pack-reused 116384
    Receiving objects: 100% (116403/116403), 40.80 MiB | 844.00 KiB/s, done.
    Resolving deltas: 100% (69450/69450), done.
    Checking connectivity... done.
    $ cd ansible/
    $ source ./hacking/env-setup
    Setting up Ansible to run out of checkout...
    PATH=/home/vagrant/ansible/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/sbin:/home/vagrant/bin
    PYTHONPATH=/home/vagrant/ansible/lib:
    MANPATH=/home/vagrant/ansible/docs/man:
    Remember, you may wish to specify your host file with -i
    Done!

Ansible 需要安装一些 Python 包,您可以使用 pip 安装。如果您的系统没有安装 pip,请使用以下命令进行安装。如果您没有安装 easy_install,您可以在 Red Hat 系统上使用 Python 的 setuptools 包安装它,或者在 macOS 上使用 Brew 安装:

    $ sudo easy_install pip
    <A long output follows>

安装了 pip 后,使用以下命令行安装 paramikoPyYAMLjinja2httplib2 包:

    $ sudo pip install paramiko PyYAML jinja2 httplib2
    Requirement already satisfied (use --upgrade to upgrade): paramiko in /usr/lib/python2.6/site-packages
    Requirement already satisfied (use --upgrade to upgrade): PyYAML in /usr/lib64/python2.6/site-packages
    Requirement already satisfied (use --upgrade to upgrade): jinja2 in /usr/lib/python2.6/site-packages
    Requirement already satisfied (use --upgrade to upgrade): httplib2 in /usr/lib/python2.6/site-packages
    Downloading/unpacking markupsafe (from jinja2)
      Downloading MarkupSafe-0.23.tar.gz
      Running setup.py (path:/tmp/pip_build_root/markupsafe/setup.py) egg_info for package markupsafe
    Installing collected packages: markupsafe
      Running setup.py install for markupsafe
        building 'markupsafe._speedups' extension
        gcc -pthread -fno-strict-aliasing -O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector --param=ssp-buffer-size=4 -m64 -mtune=generic -D_GNU_SOURCE -fPIC -fwrapv -DNDEBUG -O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector --param=ssp-buffer-size=4 -m64 -mtune=generic -D_GNU_SOURCE -fPIC -fwrapv -fPIC -I/usr/include/python2.6 -c markupsafe/_speedups.c -o build/temp.linux-x86_64-2.6/markupsafe/_speedups.o
        gcc -pthread -shared build/temp.linux-x86_64-2.6/markupsafe/_speedups.o -L/usr/lib64 -lpython2.6 -o build/lib.linux-x86_64-2.6/markupsafe/_speedups.so
    Successfully installed markupsafe
    Cleaning up...

默认情况下,Ansible 将运行在开发分支上。您可能想要切换到最新的稳定分支。使用以下 $ git branch -a 命令来检查最新的稳定版本。

复制您想要使用的最新版本。

在撰写时,版本 2.0.2 是最新版本。使用以下命令行检查最新版本:

    [node ansible]$ git checkout v2.7.1
    Note: checking out 'v2.0.2'.
    [node ansible]$ ansible --version
    ansible 2.7.1 (v2.7.1 c963ef1dfb) last updated 2018/10/25 20:12:52 (GMT +000)

您现在已经准备好了 Ansible 的工作设置。从源代码运行 Ansible 的一个好处是您可以立即享受到新功能,而不必等待软件包管理器为您提供它们。

使用 Vagrant 创建测试环境

为了学习 Ansible,我们需要制作相当多的 playbooks 并运行它们。

直接在您的计算机上执行此操作将非常危险。因此,我建议使用虚拟机。

可以在几秒钟内使用云提供商创建测试环境,但通常更有用的是在本地拥有这些机器。为此,我们将使用 Vagrant,这是 Hashicorp 公司提供的一款软件,允许用户独立快速地设置虚拟环境,与本地系统上使用的虚拟化后端无关。它支持许多虚拟化后端(在 Vagrant 生态系统中被称为 Providers),例如 Hyper-V、VirtualBox、Docker、VMWare 和 libvirt。这使您可以在任何操作系统或环境中使用相同的语法。

首先,我们将安装 vagrant。在 Fedora 上,运行以下代码就足够了:

    $ sudo dnf install -y vagrant  

在 Red Hat/CentOS/Scientific Linux/Unbreakable Linux 上,我们需要先安装 libvirt,然后启用它,然后从 Hashicorp 网站安装 vagrant

$ sudo yum install -y qemu-kvm libvirt virt-install bridge-utils libvirt-devel libxslt-devel libxml2-devel libvirt-devel libguestfs-tools-c
$ sudo systemctl enable libvirtd
$ sudo systemctl start libvirtd
$ sudo rpm -Uvh https://releases.hashicorp.com/vagrant/2.2.1/vagrant_2.2.1_x86_64.rpm
$ vagrant plugin install vagrant-libvirt

如果您使用的是 Ubuntu 或 Debian,您可以使用以下代码进行安装:

    $ sudo apt install virtualbox vagrant

对于以下示例,我将虚拟化 CentOS 7 机器。这是出于多种原因;主要原因如下:

  • CentOS 是免费的,与 Red Hat、Scientific Linux 和 Unbreakable Linux 完全兼容。

  • 许多公司将 Red Hat/CentOS/Scientific Linux/Unbreakable Linux 用于其服务器。

  • 这些发行版是唯一内置 SELinux 支持的发行版,正如我们之前所见,SELinux 可以帮助你使环境更加安全。

为了测试一切是否顺利,我们可以运行以下命令:

$ sudo vagrant init centos/7 && sudo vagrant up

如果一切顺利,你应该期待一个以这样结尾的输出:

==> default: Configuring and enabling network interfaces...
default: SSH address: 192.168.121.60:22
default: SSH username: vagrant
default: SSH auth method: private key
==> default: Rsyncing folder: /tmp/ch01/ => /vagrant

所以,你现在可以执行vagrant ssh,你会发现自己在刚刚创建的机器中。

当前文件夹中将有一个Vagrant文件。在这个文件中,你可以使用vagrant init创建指令来创建虚拟环境。

版本控制系统

在本章中,我们已经遇到了表达式基础设施代码来描述将创建和维护您的基础设施的 Ansible 代码。我们使用基础设施代码这个表达式来区分它与应用代码,后者是组成您的应用程序、网站等的代码。这种区别是为了清晰起见,但最终,这两种类型都是软件能够读取和解释的一堆文本文件。

因此,版本控制系统将会给你带来很多帮助。它的主要优点如下:

  • 多人同时在同一项目上工作的能力。

  • 进行简单方式的代码审查的能力。

  • 拥有多个分支用于多个环境(即开发、测试、QA、暂存和生产)的能力。

  • 能够追踪更改,以便我们知道更改是何时引入的,以及是谁引入的。这样一来,几个月或几年后,就更容易理解为什么那段代码存在。

这些优点是由现有的大多数版本控制系统提供给你的。

版本控制系统可以根据它们实现的三种不同模型分为三大类:

  • 本地数据模型

  • 客户端-服务器模型

  • 分布式模型

第一类别,即本地数据模型,是最古老的(大约在 1972 年左右)方法,用于非常特定的用例。这种模型要求所有用户共享相同的文件系统。它的著名示例有Revision Control SystemRCS)和Source Code Control SystemSCCS)。

第二类别,客户端-服务器模型,后来(大约在 1990 年左右)出现,并试图解决本地数据模型的限制,创建了一个遵循本地数据模型的服务器和一组客户端,这些客户端与服务器而不是与存储库本身打交道。这个额外的层允许多个开发人员使用本地文件并将它们与一个集中式服务器同步。这种方法的著名示例是 Apache SubversionSVN)和Concurrent Versions SystemCVS)。

第三类别,即分布式模型,于二十一世纪初出现,并试图解决客户端-服务器模型的限制。事实上,在客户端-服务器模型中,您可以脱机工作,但需要在 在线 提交更改。分布式模型允许您在本地存储库上处理所有事务(如本地数据模型),并以轻松的方式合并不同机器上的不同存储库。在这种新模型中,可以执行与客户端-服务器模型中的所有操作相同的操作,而且还能够完全脱机工作,以及在同行之间合并更改而不必通过集中式服务器。这种模型的示例包括 BitKeeper(专有软件)、Git、GNU Bazaar 和 Mercurial。

只有分布式模型才能提供的一些额外优势,例如以下内容:

  • 即使服务器不可用也可以进行提交、浏览历史记录以及执行任何其他操作的可能性。

  • 更容易管理不同环境的多个分支。

当涉及基础设施代码时,我们必须考虑到管理您的基础设施代码的基础设施本身经常保存在基础设施代码中。这是一个递归的情况,可能会引发问题。分布式版本控制系统将防止此问题发生。

关于管理多个分支的简易性,虽然这不是一个硬性规则,但通常分布式版本控制系统比其他类型的版本控制系统具有更好的合并处理能力。

使用 Ansible 与 Git

出于我们刚刚看到的原因,以及由于其巨大的流行度,我建议始终使用 Git 作为您的 Ansible 存储库。

我总是向我交谈的人提供一些建议,以便 Ansible 充分利用 Git:

  • 创建环境分支:创建环境分支,例如开发、生产、测试和预发布,将使您能够轻松跟踪不同环境及其各自的更新状态。我经常建议将主分支保留给开发环境,因为我发现很多人习惯直接向主分支推送新更改。如果您将主分支用于生产环境,人们可能会无意中将更改推送到生产环境,而他们本想将其推送到开发环境。

  • 始终保持环境分支稳定:拥有环境分支的一个重要优势是可以在任何时刻从头开始销毁和重建任何环境。只有在环境分支处于稳定(非破损)状态时才能实现这一点。

  • 使用功能分支:为特定的长期开发功能(如重构或其他大的更改)使用不同的分支,这样您就可以在 Git 存储库中保持日常运营,而您的新功能正在进行中(这样您就不会失去对谁做了什么以及何时做了什么的追踪)。

  • 经常推送:我总是建议人们尽可能经常推送提交。这将使 Git 成为版本控制系统和备份系统。我经常看到笔记本电脑损坏、丢失或被盗,其中有数天或数周的未推送工作。不要浪费你的时间——经常推送。而且,经常推送还会更早地检测到合并冲突,合并冲突总是在早期检测到时更容易处理,而不是等待多个更改。

  • 在进行更改后始终部署:我见过开发人员在基础架构代码中进行更改后,在开发和测试环境中进行了测试,推送到生产分支,然后去吃午饭的情况。他的午餐并不愉快。他的一位同事无意中将代码部署到生产环境(他当时试图部署他所做的小改动),而且没有准备好处理其他开发人员的部署。生产基础架构崩溃了,他们花了很多时间弄清楚一个小小的改动(部署者知道的那个)怎么可能造成如此大的混乱。

  • 选择多个小的更改而不是几个大的更改:尽可能进行小的更改将使调试更容易。调试基础架构并不容易。没有编译器可以让您看到“明显的问题”(即使 Ansible 执行您的代码的语法检查,也不会执行其他测试),而且查找故障的工具并不总是像您想象的那样好。基础架构即代码范例是新的,工具还不像应用程序代码的工具那样好。

  • 尽量避免二进制文件:我总是建议将二进制文件保存在 Git 存储库之外,无论是应用程序代码存储库还是基础架构代码存储库。在应用程序代码示例中,我认为保持存储库轻量化很重要(Git 以及大多数版本控制系统对二进制大对象的性能表现不佳),而在基础架构代码示例中,这是至关重要的,因为你会受到诱惑,想要在其中放入大量二进制对象,因为往往将二进制对象放入存储库比找到更干净(和更好)的解决方案更容易。

总结

在这一章中,我们已经了解了什么是 IT 自动化,它的优缺点,你可以找到什么样的工具,以及 Ansible 如何融入这个大局。我们还看到了如何安装 Ansible 以及如何创建一个 Vagrant 虚拟机。最后,我们分析了版本控制系统,并谈到了 Git 如何在正确使用时为 Ansible 带来的优势。

在下一章中,我们将开始看到我们在本章中提到的基础架构代码,而不是详细解释它是什么以及如何编写它。我们还将看到如何自动化那些你可能每天都要进行的简单操作,比如管理用户,管理文件和文件内容。

自动化简单任务

如前一章所述,Ansible 可用于创建和管理整个基础架构,也可集成到已经运行的基础架构中。

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

  • YAML

  • 使用 Playbook

  • Ansible 速度

  • Playbook 中的变量

  • 创建 Ansible 用户

  • 配置基本服务器

  • 安装和配置 Web 服务器

  • 发布网站

  • Jinja2 模板

首先,我们将讨论YAML Ain't Markup LanguageYAML),这是一种人类可读的数据序列化语言,广泛用于 Ansible。

技术要求

您可以从本书的 GitHub 存储库下载所有文件:github.com/PacktPublishing/Learning-Ansible-2.X-Third-Edition/tree/master/Chapter02

YAML

YAML,像许多其他数据序列化语言(如 JSON)一样,有着极少的基本概念:

  • 声明

  • 列表

  • 关联数组

声明与任何其他语言中的变量非常相似,如下所示:

name: 'This is the name'

要创建列表,我们将使用-

- 'item1' 
- 'item2' 
- 'item3' 

YAML 使用缩进来在逻辑上将父项与子项分隔开来。因此,如果我们想创建关联数组(也称为对象),我们只需要添加一个缩进:

item: 
  name: TheName 
  location: TheLocation 

显然,我们可以将它们混合在一起,如下所示:

people:
  - name: Albert 
    number: +1000000000 
    country: USA 
  - name: David 
    number: +44000000000 
    country: UK 

这些是 YAML 的基础知识。YAML 可以做得更多,但目前这些就足够了。

你好 Ansible

正如我们在前一章中看到的,可以使用 Ansible 自动化您可能每天已经执行的简单任务。

让我们从检查远程机器是否可达开始;换句话说,让我们从对机器进行 ping 开始。这样做的最简单方法是运行以下命令:

$ ansible all -i HOST, -m ping 

在这里,HOST是您拥有 SSH 访问权限的机器的 IP 地址、Fully Qualified Domain NameFQDN)或别名(您可以使用像我们在上一章中看到的 Vagrant 主机)。

HOST之后,逗号是必需的,因为否则,它不会被视为列表,而是视为字符串。

在这种情况下,我们执行了针对我们系统上的虚拟机:

$ ansible all -i test01.fale.io, -m ping 

你应该收到类似这样的结果:

test01.fale.io | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

现在,让我们看看我们做了什么以及为什么。让我们从 Ansible 帮助开始。要查询它,我们可以使用以下命令:

$ ansible --help 

为了更容易阅读,我们已删除了与我们未使用的选项相关的所有输出:

Usage: ansible <host-pattern> [options]

Options:
  -i INVENTORY, --inventory=INVENTORY, --inventory-file=INVENTORY
                        specify inventory host path or comma separated host
                        list. --inventory-file is deprecated
  -m MODULE_NAME, --module-name=MODULE_NAME
                        module name to execute (default=command)

所以,我们所做的是:

  1. 我们调用了 Ansible。

  2. 我们指示 Ansible 在所有主机上运行。

  3. 我们指定了我们的清单(也称为主机列表)。

  4. 我们指定了要运行的模块(ping)。

现在我们可以 ping 服务器,让我们尝试echo hello ansible!,如下命令所示:

$ ansible all -i test01.fale.io, -m shell -a '/bin/echo hello ansible!' 

你应该收到类似这样的结果:

test01.fale.io | CHANGED | rc=0 >>
hello ansible!

在此示例中,我们使用了一个额外的选项。让我们检查 Ansible 帮助以查看它的作用:

Usage: ansible <host-pattern> [options]
Options:
  -a MODULE_ARGS, --args=MODULE_ARGS
                        module arguments

从上下文和名称可以猜到,args 选项允许你向模块传递额外的参数。某些模块(如 ping)不支持任何参数,而其他模块(如 shell)将需要参数。

使用 playbooks

Playbooks 是 Ansible 的核心特性之一,告诉 Ansible 要执行什么。它们就像 Ansible 的待办事项列表,包含一系列任务;每个任务内部链接到一个称为 模块 的代码片段。Playbooks 是简单易读的 YAML 文件,而模块是可以用任何语言编写的代码片段,条件是其输出格式为 JSON。你可以在一个 playbook 中列出多个任务,这些任务将由 Ansible 串行执行。你可以将 playbooks 视为 Puppet 中的清单、Salt 中的状态或 Chef 中的菜谱的等价物;它们允许你输入你想在远程系统上执行的任务或命令列表。

研究 playbook 的结构

Playbooks 可以具有远程主机列表、用户变量、任务、处理程序等。你还可以通过 playbook 覆盖大部分配置设置。让我们开始研究 playbook 的结构。

我们现在要考虑的 playbook 的目的是确保 httpd 包已安装并且服务已 启用启动。这是 setup_apache.yaml 文件的内容:


- hosts: all 
  remote_user: vagrant
  tasks: 
    - name: Ensure the HTTPd package is installed 
      yum: 
        name: httpd 
        state: present 
      become: True 
    - name: Ensure the HTTPd service is enabled and running 
      service: 
        name: httpd 
        state: started 
        enabled: True 
      become: True

setup_apache.yaml 文件是一个 playbook 的示例。文件由三个主要部分组成,如下所示:

  • hosts: 这列出了我们要针对哪个主机或主机组运行任务。hosts 字段是必需的。Ansible 使用它来确定哪些主机将成为列出任务的目标。如果提供的是主机组而不是主机,则 Ansible 将尝试根据清单文件查找属于它的主机。如果没有匹配项,Ansible 将跳过该主机组的所有任务。--list-hosts 选项以及 playbook (ansible-playbook <playbook> --list-hosts) 将告诉你准确地 playbook 将运行在哪些主机上。

  • remote_user: 这是 Ansible 的配置参数之一(例如,tom' - remote_user),告诉 Ansible 在登录系统时使用特定用户(在本例中为 tom)。

  • tasks: 最后,我们来到了任务。所有 playbook 应该包含任务。任务是你想执行的一系列操作。一个 tasks 字段包含了任务的名称(即,用户关于任务的帮助文本)、应该执行的模块以及模块所需的参数。让我们看一下在 playbook 中列出的单个任务,如前面代码片段所示。

本书中的所有示例将在 CentOS 上执行,但是对同一组示例进行少量更改后也可以在其他发行版上运行。

在上述情况下,有两个任务。name参数表示任务正在做什么,并且主要是为了提高可读性,正如我们在 playbook 运行期间将看到的那样。name参数是可选的。yumservice模块有自己的一组参数。几乎所有模块都有name参数(有例外,比如debug模块),它表示对哪个组件执行操作。让我们看看其他参数:

  • state参数在yum模块中保存了最新的值,它表示应该安装httpd软件包。执行的命令大致相当于yum install httpd

  • service模块的场景中,带有started值的state参数表示httpd服务应该启动,它大致相当于/etc/init.d/httpd启动。在此模块中,我们还有enabled参数,它定义了服务是否应该在启动时启动。

  • become: True参数表示任务应该以sudo访问权限执行。如果sudo用户的文件不允许用户运行特定命令,那么当运行 playbook 时,playbook 将失败。

你可能会问,为什么没有一个包模块能够在内部确定架构并根据系统的架构运行yumapt或任何其他包选项。Ansible 将包管理器的值填充到一个名为ansible_pkg_manager的变量中。

一般来说,我们需要记住,在不同操作系统中具有通用名称的软件包的数量是实际存在的软件包数量的一个很小的子集。例如,httpd软件包在 Red Hat 系统中称为httpd,但在基于 Debian 的系统中称为apache2。我们还需要记住,每个包管理器都有自己的一组选项,使其功能强大;因此,使用明确的包管理器名称更合理,这样终端用户编写 playbook 时就可以使用完整的选项集。

运行 playbook

现在,是时候(是的,终于!)运行 playbook 了。为了指示 Ansible 执行 playbook 而不是模块,我们将不得不使用一个语法非常类似于我们已经看到的ansible命令的不同命令(ansible-playbooks):

$ ansible-playbook -i HOST, setup_apache.yaml

如您所见,除了在 playbook 中指定的主机模式(已消失)和模块选项(已被 playbook 名称替换)之外,没有任何变化。因此,要在我的机器上执行此命令,确切的命令如下:

$ ansible-playbook -i test01.fale.io, setup_apache.yaml 

结果如下:

PLAY [all] ***********************************************************

TASK [Gathering Facts] ***********************************************
ok: [test01.fale.io]

TASK [Ensure the HTTPd package is installed] *************************
changed: [test01.fale.io]

TASK [Ensure the HTTPd service is enabled and running] ***************
changed: [test01.fale.io]

PLAY RECAP ***********************************************************
test01.fale.io                : ok=3 changed=2 unreachable=0 failed=0

哇!示例运行成功。现在让我们检查一下httpd软件包是否已安装并且现在正在机器上运行。要检查 HTTPd 是否已安装,最简单的方法是询问rpm

$ rpm -qa | grep httpd 

如果一切正常工作,你应该有如下输出:

httpd-tools-2.4.6-80.el7.centos.1.x86_64
httpd-2.4.6-80.el7.centos.1.x86_64

要查看服务的状态,我们可以询问systemd

$ systemctl status httpd

预期结果如下所示:

httpd.service - The Apache HTTP Server
 Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled; vendor preset: disabled)
 Active: active (running) since Tue 2018-12-04 15:11:03 UTC; 29min ago
 Docs: man:httpd(8)
 man:apachectl(8)
 Main PID: 604 (httpd)
 Status: "Total requests: 0; Current requests/sec: 0; Current traffic: 0 B/sec"
 CGroup: /system.slice/httpd.service
 ├─604 /usr/sbin/httpd -DFOREGROUND
 ├─624 /usr/sbin/httpd -DFOREGROUND
 ├─626 /usr/sbin/httpd -DFOREGROUND
 ├─627 /usr/sbin/httpd -DFOREGROUND
 ├─628 /usr/sbin/httpd -DFOREGROUND
 └─629 /usr/sbin/httpd -DFOREGROUND 

根据 playbook,已达到最终状态。让我们简要地看一下 playbook 运行过程中确切发生了什么:

PLAY [all] ***********************************************************

此行建议我们将从这里开始执行 playbook,并且将在所有主机上执行:

TASK [Gathering Facts] ***********************************************
ok: [test01.fale.io]

TASK 行显示任务的名称(在本例中为setup)以及其对每个主机的影响。有时,人们会对setup任务感到困惑。实际上,如果您查看 playbook,您会发现没有setup任务。这是因为在执行我们要求的任务之前,Ansible 会尝试连接到机器并收集有关以后可能有用的信息。正如您所看到的,该任务结果显示为绿色的ok状态,因此成功了,并且服务器上没有发生任何更改:

TASK [Ensure the HTTPd package is installed] *************************
changed: [test01.fale.io]

TASK [Ensure the HTTPd service is enabled and running] ***************
changed: [test01.fale.io]

这两个任务的状态是黄色的,并拼写为changed。这意味着这些任务已执行并成功,但实际上已更改了机器上的某些内容:

PLAY RECAP ***********************************************************
test01.fale.io : ok=3 changed=2 unreachable=0 failed=0

最后几行是 playbook 执行情况的总结。现在让我们重新运行任务,然后查看两个任务实际运行后的输出:

PLAY [all] ***********************************************************

TASK [Gathering Facts] ***********************************************
ok: [test01.fale.io]

TASK [Ensure the HTTPd package is installed] *************************
ok: [test01.fale.io]

TASK [Ensure the HTTPd service is enabled and running] ***************
ok: [test01.fale.io]

PLAY RECAP ***********************************************************
test01.fale.io                : ok=3 changed=0 unreachable=0 failed=0

正如您所预期的那样,所涉及的两个任务的输出为ok,这意味着在运行任务之前已满足了所需状态。重要的是要记住,许多任务(例如收集事实任务)会获取有关系统特定组件的信息,并不一定会更改系统中的任何内容;因此,这些任务之前没有显示更改的输出。

在第一次和第二次运行时,PLAY RECAP部分显示如下。在第一次运行时,您将看到以下输出:

PLAY RECAP ***********************************************************
test01.fale.io                : ok=3 changed=2 unreachable=0 failed=0

第二次运行时,您将看到以下输出:

PLAY RECAP ***********************************************************
test01.fale.io                : ok=3 changed=0 unreachable=0 failed=0

如您所见,区别在于第一个任务的输出显示changed=2,这意味着由于两个任务而更改了系统状态两次。查看此输出非常有用,因为如果系统已达到其所需状态,然后您在其上运行 playbook,则预期输出应为changed=0

如果您在这个阶段考虑了幂等性这个词,那么您是完全正确的,并且值得表扬!幂等性是配置管理的关键原则之一。维基百科将幂等性定义为一个操作,如果对任何值应用两次,则其结果与仅应用一次时相同。您在童年时期遇到的最早的例子是对数字1进行乘法运算,其中1*1=1每次都成立。

大多数配置管理工具都采用了这个原则,并将其应用于基础架构。在大型基础架构中,强烈建议监视或跟踪基础架构中更改任务的数量,并在发现异常时警告相关任务;这通常适用于任何配置管理工具。在理想状态下,你只应该在引入新的更改时看到更改,比如对各种系统组件进行创建删除更新删除CRUD)操作。如果你想知道如何在 Ansible 中实现它,继续阅读本书,你最终会找到答案的!

让我们继续。你也可以将前面的任务写成如下形式,但是从最终用户的角度来看,任务非常易读(我们将此文件称为setup_apache_no_com.yaml):

--- 
- hosts: all 
  remote_user: vagrant
  tasks: 
    - yum: 
        name: httpd 
        state: present 
      become: True 
    - service: 
        name: httpd 
        state: started 
        enabled: True 
      become: True

让我们再次运行 playbook,以发现输出中的任何差异:

$ ansible-playbook -i test01.fale.io, setup_apache_no_com.yaml

输出将如下所示:

PLAY [all] ***********************************************************

TASK [Gathering Facts] ***********************************************
ok: [test01.fale.io]

TASK [yum] ***********************************************************
ok: [test01.fale.io]

TASK [service] *******************************************************
ok: [test01.fale.io]

PLAY RECAP ***********************************************************
test01.fale.io                : ok=3 changed=0 unreachable=0 failed=0

如你所见,区别在于可读性。在可能的情况下,建议尽可能简化任务(KISS原则:保持简单,笨拙),以确保长期保持脚本的可维护性。

现在我们已经看到了如何编写一个基本的 playbook 并对其运行到主机,让我们看看在运行 playbooks 时会帮助你的其他选项。

Ansible 详细程度

任何人首先选择的选项之一是调试选项。为了了解在运行 playbook 时发生了什么,你可以使用详细(-v)选项运行它。每个额外的v将为最终用户提供更多的调试输出。

让我们看一个使用这些选项调试简单ping命令(ansible all -i test01.fale.io, -m ping)的示例:

  • -v选项提供了默认输出:
Using /etc/ansible/ansible.cfg as config file
test01.fale.io | SUCCESS => {
    "changed": false, 
    "ping": "pong"
}
  • -vv选项会提供有关 Ansible 环境和处理程序的更多信息:
ansible 2.7.2
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/home/fale/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python2.7/site-packages/ansible
  executable location = /bin/ansible
  python version = 2.7.15 (default, Oct 15 2018, 15:24:06) [GCC 8.1.1 20180712 (Red Hat 8.1.1-5)]
Using /etc/ansible/ansible.cfg as config file
META: ran handlers
test01.fale.io | SUCCESS => {
    "changed": false, 
    "ping": "pong"
}
META: ran handlers
META: ran handlers
  • -vvv选项提供了更多信息。例如,它显示 Ansible 用于在远程主机上创建临时文件并在远程运行脚本的ssh命令。完整脚本可在 GitHub 上找到。
ansible 2.7.2
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/home/fale/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python2.7/site-packages/ansible
  executable location = /bin/ansible
  python version = 2.7.15 (default, Oct 15 2018, 15:24:06) [GCC 8.1.1 20180712 (Red Hat 8.1.1-5)]
Using /etc/ansible/ansible.cfg as config file
Parsed test01.fale.io, inventory source with host_list plugin
META: ran handlers
<test01.fale.io> ESTABLISH SSH CONNECTION FOR USER: None
<test01.fale.io> SSH: EXEC ssh -C -o ControlMaster=auto -o 
...

现在我们了解了在运行 playbook 时发生了什么,使用详细-vvv选项。

playbook 中的变量

有时,在 playbook 中设置和获取变量是很重要的。

很多时候,你会需要自动化多个类似的操作。在这些情况下,你将想要创建一个可以使用不同变量调用的单个 playbook,以确保代码的可重用性。

另一个情况下变量非常重要的案例是当你有多个数据中心时,一些值将是特定于数据中心的。一个常见的例子是 DNS 服务器。让我们分析下面的简单代码,这将向我们介绍设置和获取变量的 Ansible 方法:

- hosts: all 
  remote_user: vagrant
  tasks: 
    - name: Set variable 'name' 
      set_fact: 
        name: Test machine 
    - name: Print variable 'name' 
      debug: 
        msg: '{{ name }}' 

让我们以通常的方式运行它:

$ ansible-playbook -i test01.fale.io, variables.yaml

你应该看到以下结果:

PLAY [all] *********************************************************

TASK [Gathering Facts] *********************************************
ok: [test01.fale.io]

TASK [Set variable 'name'] *****************************************
ok: [test01.fale.io]

TASK [Print variable 'name'] ***************************************
ok: [test01.fale.io] => {
 "msg": "Test machine"
}

PLAY RECAP *********************************************************
test01.fale.io              : ok=3 changed=0 unreachable=0 failed=0 

如果我们分析刚刚执行的代码,应该很清楚发生了什么。我们设置了一个变量(在 Ansible 中称为 facts),然后用 debug 函数打印它。

当你使用这个扩展版本的 YAML 时,变量应该总是用引号括起来。

Ansible 允许你以许多不同的方式设置变量 - 也就是说,通过传递一个变量文件,在 playbook 中声明它,使用 -e / --extra-vars 参数将它传递给 ansible-playbook 命令,或者在清单文件中声明它(我们将在下一章中深入讨论这一点)。

现在是时候开始使用 Ansible 在设置阶段获取的一些元数据了。让我们开始查看 Ansible 收集的数据。为此,我们将执行以下代码:

$ ansible all -i HOST, -m setup 

在我们特定的情况下,这意味着执行以下代码:

$ ansible all -i test01.fale.io, -m setup 

显然我们也可以用 playbook 来做同样的事情,但这种方式更快。另外,对于 setup 情况,你只需要在开发过程中看到输出以确保使用正确的变量名称来达到你的目标。

输出将会是类似这样的。完整的代码输出可在 GitHub 上找到。

test01.fale.io | SUCCESS => {
    "ansible_facts": {
        "ansible_all_ipv4_addresses": [
            "192.168.121.190"
        ], 
        "ansible_all_ipv6_addresses": [
            "fe80::5054:ff:fe93:f113"
        ], 
        "ansible_apparmor": {
            "status": "disabled"
        }, 
        "ansible_architecture": "x86_64", 
        "ansible_bios_date": "04/01/2014", 
        "ansible_bios_version": "?-20180531_142017-buildhw-08.phx2.fedoraproject.org-1.fc28", 
        ...

正如你从这个大量选项的列表中所看到的,你可以获得大量的信息,并且你可以像使用任何其他变量一样使用它们。让我们打印操作系统的名称和版本。为此,我们可以创建一个名为 setup_variables.yaml 的新 playbook,内容如下:


- hosts: all
  remote_user: vagrant
  tasks: 
    - name: Print OS and version
      debug:
        msg: '{{ ansible_distribution }} {{ ansible_distribution_version }}'

使用以下代码运行它:

$ ansible-playbook -i test01.fale.io, setup_variables.yaml

这将给我们以下输出:

PLAY [all] *********************************************************

TASK [Gathering Facts] *********************************************
ok: [test01.fale.io]

TASK [Print OS and version] ****************************************
ok: [test01.fale.io] => {
 "msg": "CentOS 7.5.1804"
}

PLAY RECAP *********************************************************
test01.fale.io              : ok=2 changed=0 unreachable=0 failed=0 

如你所见,它按预期打印了操作系统的名称和版本。除了之前看到的方法之外,还可以使用命令行参数传递变量。实际上,如果我们查看 Ansible 帮助,我们会注意到以下内容:

Usage: ansible <host-pattern> [options]

Options:
  -e EXTRA_VARS, --extra-vars=EXTRA_VARS
                        set additional variables as key=value or YAML/JSON, if
                        filename prepend with @

ansible-playbook 命令中也存在相同的行。让我们创建一个名为 cli_variables.yaml 的小 playbook,内容如下:

---
- hosts: all
  remote_user: vagrant
  tasks:
    - name: Print variable 'name'
      debug:
        msg: '{{ name }}'

使用以下代码执行它:

$ ansible-playbook -i test01.fale.io, cli_variables.yaml -e 'name=test01'

我们将会收到以下内容:

 [WARNING]: Found variable using reserved name: name

PLAY [all] *********************************************************

TASK [Gathering Facts] *********************************************
ok: [test01.fale.io]

TASK [Print variable 'name'] ***************************************
ok: [test01.fale.io] => {
 "msg": "test01"
}

PLAY RECAP *********************************************************
test01.fale.io              : ok=2 changed=0 unreachable=0 failed=0 

如果我们忘记添加额外参数来指定变量,我们将会这样执行它:

$ ansible-playbook -i test01.fale.io, cli_variables.yaml

我们将会收到以下输出:

PLAY [all] *********************************************************

TASK [Gathering Facts] *********************************************
ok: [test01.fale.io]

TASK [Print variable 'name'] ***************************************
fatal: [test01.fale.io]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'name' is undefined\n\nThe error appears to have been in '/home/fale/Learning-Ansible-2.X-Third-Edition/Ch2/cli_variables.yaml': line 5, 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: Print variable 'name'\n ^ here\n"}
 to retry, use: --limit @/home/fale/Learning-Ansible-2.X-Third-Edition/Ch2/cli_variables.retry

PLAY RECAP *********************************************************
test01.fale.io : ok=1 changed=0 unreachable=0 failed=1

现在我们已经学会了 playbook 的基础知识,让我们使用它们从头开始创建一个 Web 服务器。为此,让我们从创建一个 Ansible 用户开始,然后从那里继续。

在前面的示例中,我们注意到一个警告弹出,通知我们正在重新声明一个保留变量(name)。截至 Ansible 2.7,完整的保留变量列表如下:addappendas_integer_ratiobit_lengthcapitalizecenterclearconjugatecopycountdecodedenominatordifferencedifference_updatediscardencodeendswithexpandtabsextendfindformatfromhexfromkeysgethas_keyheximagindexinsertintersectionintersection_updateisalnumisalphaisdecimalisdigitisdisjointis_integerislowerisnumericisspaceissubsetissupersetistitleisupperitemsiteritemsiterkeysitervaluesjoinkeysljustlowerlstripnumeratorpartitionpoppopitemrealremovereplacereverserfindrindexrjustrpartitionrsplitrstripsetdefaultsortsplitsplitlinesstartswithstripswapcasesymmetric_differencesymmetric_difference_updatetitletranslateunionupdateuppervaluesviewitemsviewkeysviewvalueszfill

创建 Ansible 用户

当您创建一台机器(或从任何托管公司租用一台机器)时,它只带有root用户,或其他用户,如vagrant。让我们开始创建一个 Playbook,确保创建一个 Ansible 用户,可以使用 SSH 密钥访问它,并且能够代表其他用户(sudo)执行操作而无需密码。我们经常将此 Playbook 称为 firstrun.yaml,因为我们在创建新机器后立即执行它,但之后我们不再使用它,因为出于安全原因,我们会禁用默认用户。我们的脚本将类似于以下内容:

--- 
- hosts: all 
  user: vagrant 
  tasks: 
    - name: Ensure ansible user exists 
      user: 
        name: ansible 
        state: present 
        comment: Ansible 
      become: True
    - name: Ensure ansible user accepts the SSH key 
      authorized_key: 
        user: ansible 
        key: https://github.com/fale.keys 
        state: present 
      become: True
    - name: Ensure the ansible user is sudoer with no password required 
      lineinfile: 
        dest: /etc/sudoers 
        state: present 
        regexp: '^ansible ALL\=' 
        line: 'ansible ALL=(ALL) NOPASSWD:ALL' 
        validate: 'visudo -cf %s'
      become: True

在运行之前,让我们稍微看一下。我们使用了三个不同的模块(userauthorized_keylineinfile),这些我们从未见过。

user 模块,正如其名称所示,允许我们确保用户存在(或不存在)。

authorized_key 模块允许我们确保某个 SSH 密钥可以用于登录到该机器上的特定用户。此模块不会替换已为该用户启用的所有 SSH 密钥,而只会添加(或删除)指定的密钥。如果您想改变此行为,可以使用 exclusive 选项,它允许您删除在此步骤中未指定的所有 SSH 密钥。

lineinfile 模块允许我们修改文件的内容。它的工作方式与sed(流编辑器)非常相似,您指定用于匹配行的正则表达式,然后指定要用于替换匹配行的新行。如果没有匹配的行,则该行将添加到文件的末尾。

现在让我们使用以下代码运行它:

$ ansible-playbook -i test01.fale.io, firstrun.yaml

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

PLAY [all] *********************************************************

TASK [Gathering Facts] *********************************************
ok: [test01.fale.io]

TASK [Ensure ansible user exists] **********************************
changed: [test01.fale.io]

TASK [Ensure ansible user accepts the SSH key] *********************
changed: [test01.fale.io]

TASK [Ensure the ansible user is sudoer with no password required] *
changed: [test01.fale.io]

PLAY RECAP *********************************************************
test01.fale.io              : ok=4 changed=3 unreachable=0 failed=0

配置基本服务器

在为 Ansible 创建了具有必要权限的用户之后,我们可以继续对操作系统进行一些其他小的更改。为了更清晰,我们将看到每个动作是如何执行的,然后我们将查看整个 playbook。

启用 EPEL

EPEL 是企业 Linux 中最重要的仓库,它包含许多附加软件包。它也是一个安全的仓库,因为 EPEL 中的任何软件包都不会与基本仓库中的软件包发生冲突。

要在 RHEL/CentOS 7 中启用 EPEL,只需安装 epel-release 软件包即可。要在 Ansible 中执行此操作,我们将使用以下内容:

- name: Ensure EPEL is enabled 
  yum: 
    name: epel-release 
    state: present 
  become: True 

如您所见,我们使用了 yum 模块,就像我们在本章的第一个示例中所做的那样,指定了软件包的名称以及我们希望它存在。

安装 SELinux 的 Python 绑定

由于 Ansible 是用 Python 编写的,并且主要使用 Python 绑定来操作操作系统,因此我们需要安装 SELinux 的 Python 绑定:

- name: Ensure libselinux-python is present 
  yum: 
    name: libselinux-python 
    state: present 
  become: True 
- name: Ensure libsemanage-python is present 
  yum: 
    name: libsemanage-python 
    state: present 
  become: True 

这可以用更短的方式编写,使用循环,但我们将在下一章中看到如何做到这一点。

升级所有已安装的软件包

要升级所有已安装的软件包,我们将需要再次使用 yum 模块,但是使用不同的参数;实际上,我们将使用以下内容:

- name: Ensure we have last version of every package 
  yum: 
    name: "*" 
    state: latest 
  become: True 

正如您所看到的,我们已将 * 指定为软件包名称(这代表了一个通配符,用于匹配所有已安装的软件包),并且 state 参数为 latest。这将会将所有已安装的软件包升级到可用的最新版本。

您可能还记得,当我们谈到 present 状态时,我们说它将安装最新可用版本。那么 presentlatest 之间有什么区别?present 将在未安装软件包时安装最新版本,而如果软件包已安装(无论版本如何),它将继续前进而不进行任何更改。latest 将在未安装软件包时安装最新版本,并且如果软件包已安装,它将检查是否有更新版本可用,如果有,则 Ansible 将更新软件包。

确保 NTP 已安装、配置并运行

要确保 NTP 存在,我们使用 yum 模块:

- name: Ensure NTP is installed 
  yum: 
    name: ntp 
    state: present 
  become: True

现在我们知道 NTP 已安装,我们应该确保服务器使用我们想要的 timezone。为此,我们将在 /etc/localtime 中创建一个符号链接,该链接将指向所需的 zoneinfo 文件:

- name: Ensure the timezone is set to UTC 
  file: 
    src: /usr/share/zoneinfo/GMT 
    dest: /etc/localtime 
    state: link 
  become: True 

如您所见,我们使用了 file 模块,指定它需要是一个链接(state: link)。

要完成 NTP 配置,我们需要启动 ntpd 服务,并确保它将在每次后续引导时运行:

- name: Ensure the NTP service is running and enabled 
  service: 
    name: ntpd 
    state: started 
    enabled: True 
  become: True 

确保 FirewallD 存在并已启用

您可以想象,第一步是确保 FirewallD 已安装:

- name: Ensure FirewallD is installed 
  yum: 
    name: firewalld 
    state: present 
  become: True

由于我们希望在启用 FirewallD 时不会丢失 SSH 连接,因此我们将确保 SSH 流量始终可以通过它传递:

- name: Ensure SSH can pass the firewall 
  firewalld: 
    service: ssh 
    state: enabled 
    permanent: True 
    immediate: True 
  become: True

为此,我们使用了firewalld模块。此模块将采用与firewall-cmd控制台非常相似的参数。您将需要指定要通过防火墙的服务,是否要立即应用此规则以及是否要将规则设置为永久性规则,以便在重新启动后规则仍然存在。

您可以使用service参数指定服务名称(如ssh),也可以使用port参数指定端口(如22/tcp)。

现在我们已经安装了 FirewallD,并且确保我们的 SSH 连接将存活,我们可以像对待其他任何服务一样启用它:

- name: Ensure FirewallD is running 
  service: 
    name: firewalld 
    state: started 
    enabled: True 
  become: True 

添加自定义 MOTD。

要添加 MOTD,我们将需要一个模板,该模板将对所有服务器都相同,并且一个任务来使用该模板。

我发现为每个服务器添加 MOTD 非常有用。如果你使用 Ansible,那就更有用了,因为你可以用它来警告用户系统的更改可能会被 Ansible 覆盖。我通常的模板叫做motd,内容如下:

                This system is managed by Ansible 
  Any change done on this system could be overwritten by Ansible 

OS: {{ ansible_distribution }} {{ ansible_distribution_version }} 
Hostname: {{ inventory_hostname }} 
eth0 address: {{ ansible_eth0.ipv4.address }} 

            All connections are monitored and recorded 
     Disconnect IMMEDIATELY if you are not an authorized user

这是一个jinja2模板,它允许我们使用在 playbooks 中设置的每个变量。这也允许我们使用后面将在本章中看到的复杂的条件和循环语法。为了从 Ansible 中的模板填充文件,我们将需要使用以下命令:

- name: Ensure the MOTD file is present and updated 
  template: 
    src: motd 
    dest: /etc/motd 
    owner: root 
    group: root 
    mode: 0644 
  become: True 

template模块允许我们指定一个本地文件(src),该文件将被jinja2解释,并且此操作的输出将保存在远程机器上的特定路径(dest)中,将由特定用户(owner)和组(group)拥有,并且将具有特定的访问模式(mode)。

更改主机名。

为了保持简单,我发现将机器的主机名设置为有意义的内容很有用。为此,我们可以使用一个非常简单的 Ansible 模块叫做hostname

- name: Ensure the hostname is the same of the inventory 
  hostname: 
    name: "{{ inventory_hostname }}" 
  become: True

复审并运行 playbook。

把所有事情都放在一起,我们现在有了以下 playbook(为简单起见称为common_tasks.yaml):

--- 
- hosts: all 
  remote_user: ansible 
  tasks: 
    - name: Ensure EPEL is enabled 
      yum: 
        name: epel-release 
        state: present 
      become: True 
    - name: Ensure libselinux-python is present 
      yum: 
        name: libselinux-python 
        state: present 
      become: True 
  ...

由于这个playbook相当复杂,我们可以运行以下命令:

$ ansible-playbook common_tasks.yaml --list-tasks 

这要求 Ansible 以更简洁的形式打印所有任务,以便我们可以快速查看playbook执行的任务。输出应该类似于以下内容:

playbook: common_tasks.yaml
 play #1 (all): all TAGS: []
 tasks:
 Ensure EPEL is enabled TAGS: []
 Ensure libselinux-python is present TAGS: []
 Ensure libsemanage-python is present TAGS: []
 Ensure we have last version of every package TAGS: []
 Ensure NTP is installed TAGS: []
 Ensure the timezone is set to UTC TAGS: []
 Ensure the NTP service is running and enabled TAGS: []
 Ensure FirewallD is installed TAGS: []
 Ensure FirewallD is running TAGS: []
 Ensure SSH can pass the firewall TAGS: []
 Ensure the MOTD file is present and updated TAGS: []
 Ensure the hostname is the same of the inventory TAGS: []

现在我们可以使用以下命令运行playbook

$ ansible-playbook -i test01.fale.io, common_tasks.yaml

我们将收到以下输出。完整的代码输出可在 GitHub 上找到。

PLAY [all] ***************************************************

TASK [Gathering Facts] ***************************************
ok: [test01.fale.io]

TASK [Ensure EPEL is enabled] ********************************
changed: [test01.fale.io]

TASK [Ensure libselinux-python is present] *******************
ok: [test01.fale.io]

TASK [Ensure libsemanage-python is present] ******************
changed: [test01.fale.io]

TASK [Ensure we have last version of every package] **********
changed: [test01.fale.io]
...

安装和配置 web 服务器。

现在我们已经对操作系统进行了一些通用更改,让我们继续实际创建 web 服务器。我们将这两个阶段拆分开来,以便我们可以在每台机器之间共享第一个阶段,并仅将第二个应用于 Web 服务器。

对于这个第二阶段,我们将创建一个名为webserver.yaml的新 playbook,内容如下:

--- 
- hosts: all 
  remote_user: ansible
  tasks: 
    - name: Ensure the HTTPd package is installed 
      yum: 
        name: httpd 
        state: present 
      become: True 
    - name: Ensure the HTTPd service is enabled and running 
      service: 
        name: httpd 
        state: started 
        enabled: True 
      become: True 
    - name: Ensure HTTP can pass the firewall 
      firewalld: 
        service: http 
        state: enabled 
        permanent: True 
        immediate: True 
      become: True 
    - name: Ensure HTTPS can pass the firewall 
      firewalld: 
        service: https 
        state: enabled 
        permanent: True 
        immediate: True 
      become: True  

正如您所看到的,前两个任务与本章开头示例中的任务相同,最后两个任务用于指示 FirewallD 允许HTTPHTTPS流量通过。

让我们用以下命令运行这个脚本:

$ ansible-playbook -i test01.fale.io, webserver.yaml 

这导致以下结果:

PLAY [all] *************************************************

TASK [Gathering Facts] *************************************
ok: [test01.fale.io]

TASK [Ensure the HTTPd package is installed] ***************
ok: [test01.fale.io]

TASK [Ensure the HTTPd service is enabled and running] *****
ok: [test01.fale.io]

TASK [Ensure HTTP can pass the firewall] *******************
changed: [test01.fale.io]

TASK [Ensure HTTPS can pass the firewall] ******************
changed: [test01.fale.io]

PLAY RECAP *************************************************
test01.fale.io      : ok=5 changed=2 unreachable=0 failed=0 

现在我们有了一个网页服务器,让我们发布一个小型的、单页的、静态的网站。

发布网站

由于我们的网站将是一个简单的单页网站,我们可以很容易地创建它,并使用一个 Ansible 任务发布它。为了使这个页面稍微有趣些,我们将使用一个模板创建它,这个模板将由 Ansible 填充一些关于机器的数据。发布它的脚本将被称为 deploy_website.yaml,并且将具有以下内容:

--- 
- hosts: all 
  remote_user: ansible
  tasks: 
    - name: Ensure the website is present and updated 
      template: 
        src: index.html.j2 
        dest: /var/www/html/index.html 
        owner: root 
        group: root 
        mode: 0644 
      become: True  

让我们从一个简单的模板开始,我们将其称为 index.html.j2

<html> 
    <body> 
        <h1>Hello World!</h1> 
    </body> 
</html>

现在,我们可以通过运行以下命令来测试我们的网站部署:

$ ansible-playbook -i test01.fale.io, deploy_website.yaml 

我们应该收到以下输出:

PLAY [all] ***********************************************

TASK [Gathering Facts] ***********************************
ok: [test01.fale.io]

TASK [Ensure the website is present and updated] *********
changed: [test01.fale.io]

PLAY RECAP ***********************************************
test01.fale.io    : ok=2 changed=1 unreachable=0 failed=0

如果您现在在浏览器中打开 IP/FQDN 测试机,您会找到 Hello World! 页面。

Jinja2 模板

Jinja2 是一个广泛使用的、功能齐全的 Python 模板引擎。让我们看一些语法,这些语法将帮助我们使用 Ansible。本段不是官方文档的替代品,但其目标是教会您在使用 Ansible 时会非常有用的一些组件。

变量

正如我们所见,我们可以通过使用 {{ VARIABLE_NAME }} 语法简单地打印变量内容。如果我们想要打印数组的一个元素,我们可以使用 {{ ARRAY_NAME['KEY'] }},如果我们想要打印对象的一个属性,我们可以使用 {{ OBJECT_NAME.PROPERTY_NAME }}

因此,我们可以通过以下方式改进我们之前的静态页面:

<html> 
    <body> 
        <h1>Hello World!</h1> 
        <p>This page was created on {{ ansible_date_time.date }}.</p> 
    </body> 
</html>

过滤器

有时,我们可能想稍微改变字符串的样式,而不需要为此编写特定的代码;例如,我们可能想要将一些文本大写。为此,我们可以使用 Jinja2 的一个过滤器,比如 {{ VARIABLE_NAME | capitalize }}。Jinja2 有许多可用的过滤器,您可以在 jinja.pocoo.org/docs/dev/templates/#builtin-filters 找到完整的列表。

条件语句

在模板引擎中,您可能经常发现有条件地打印不同的字符串的可能性是有用的,这取决于字符串的内容(或存在)。因此,我们可以通过以下方式改进我们的静态网页:

<html> 
    <body> 
        <h1>Hello World!</h1> 
        <p>This page was created on {{ ansible_date_time.date }}.</p> 
{% if ansible_eth0.active == True %} 
        <p>eth0 address {{ ansible_eth0.ipv4.address }}.</p> 
{% endif %} 
    </body> 
</html> 

正如您所看到的,我们已经添加了打印 eth0 连接的主 IPv4 地址的能力,如果连接是 active 的话。通过条件语句,我们还可以使用测试。

有关内置测试的完整列表,请参阅 jinja.pocoo.org/docs/dev/templates/#builtin-tests

因此,为了获得相同的结果,我们也可以写成以下形式:

<html> 
    <body> 
        <h1>Hello World!</h1> 
        <p>This page was created on {{ ansible_date_time.date }}.</p> 
{% if ansible_eth0.active is equalto True %} 
        <p>eth0 address {{ ansible_eth0.ipv4.address }}.</p> 
{% endif %} 
    </body> 
</html> 

有很多不同的测试可以帮助您创建易于阅读、有效的模板。

循环

jinja2 模板系统还提供了创建循环的功能。让我们为我们的页面添加一个功能,它将打印每个设备的主 IPv4 网络地址,而不仅仅是 eth0。然后我们将有以下代码:

<html> 
    <body> 
        <h1>Hello World!</h1> 
        <p>This page was created on {{ ansible_date_time.date }}.</p> 
        <p>This machine can be reached on the following IP addresses</p> 
        <ul> 
{% for address in ansible_all_ipv4_addresses %} 
            <li>{{ address }}</li> 
{% endfor %} 
        </ul> 
    </body> 
</html> 

正如您所看到的,如果您已经了解 Python 的语法,那么对于循环语句的语法是熟悉的。

这几页关于 Jinja2 模板化编程的内容并不能替代官方文档。实际上,Jinja2 模板比我们在这里看到的要更强大。这里的目标是为您提供最常用于 Ansible 的基本 Jinja2 模板。

摘要

在本章中,我们开始学习 YAML,并了解了 playbook 是什么,它如何工作以及如何使用它来创建 Web 服务器(和静态网站的部署)。我们还看到了多个 Ansible 模块,例如useryumserviceFirewalIDlineinfiletemplate模块。在章节的最后,我们重点关注了模板。

在下一章中,我们将讨论库存,以便我们可以轻松地管理多台机器。

第二节:在生产环境中部署 Playbooks

这一部分将帮助您创建具有多个阶段和多台机器的部署。它还将解释 Ansible 如何与各种云服务集成以及如何通过管理云来简化您的生活。

这一部分包含以下章节:

  • 第三章,扩展到多个主机

  • 第四章,处理复杂部署

  • 第五章,转向云端

  • 第六章,从 Ansible 获取通知

扩展到多个主机

在之前的章节中,我们在命令行中指定了主机。在只有一个主机要处理时,这样做效果很好,但是当管理多个服务器时,效果就不太好了。在本章中,我们将看到如何利用库存来管理多个服务器。此外,我们还将介绍主机变量和组变量等主题,以便轻松快速地设置类似但不同的主机。我们将讨论Ansible中的循环,它允许您减少编写的代码量,同时使代码更易读。

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

  • 使用库存文件

  • 使用变量

技术要求

您可以从本书的 GitHub 仓库下载所有文件,网址为github.com/PacktPublishing/Learning-Ansible-2.X-Third-Edition/tree/master/Chapter03

使用库存文件

库存文件是 Ansible 的真相之源(还有一个名为动态库存的高级概念,我们稍后会介绍)。它遵循 INI 格式,并告诉 Ansible 用户提供的远程主机是否真实。

Ansible 可以并行运行其任务对多个主机进行操作。为了做到这一点,您可以直接将主机列表传递给 Ansible,使用库存文件。对于这样的并行执行,Ansible 允许您在库存文件中对主机进行分组;文件将组的名称传递给 Ansible。Ansible 将在库存文件中搜索该组,并对该组中列出的所有主机运行其任务。

您可以使用 -i--inventory-file 选项将库存文件传递给 Ansible,后跟文件路径。如果您未明确指定任何库存文件给 Ansible,则 Ansible 将从 ansible.cfghost_file 参数的默认路径获取,默认路径为 /etc/ansible/hosts

使用 -i 参数时,如果值是一个列表(至少包含一个逗号),它将被用作库存列表,而如果变量是字符串,则将其用作库存文件路径。

基本库存文件

在深入概念之前,让我们先看一下一个称为hosts的基本库存文件,我们可以在这个文件中使用,而不是在之前的示例中使用的列表:

test01.fale.io

Ansible 可以在库存文件中使用 FQDN 或 IP 地址。

现在,我们可以执行与上一章相同的操作,调整 Ansible 命令参数。

例如,要安装 Web 服务器,我们使用了这个命令:

$ ansible-playbook -i test01.fale.io, webserver.yaml 

相反,我们可以使用以下内容:

$ ansible-playbook -i hosts webserver.yaml 

正如您所看到的,我们已经用库存文件名替换了主机列表。

库存文件中的组

当我们遇到更复杂的情况时,清单文件的优势就会显现出来。假设我们的网站变得更加复杂,现在我们需要一个更复杂的环境。在我们的示例中,我们的网站将需要一个 MySQL 数据库。此外,我们决定有两台 web 服务器。在这种情况下,根据我们的基础设施中的角色对不同的机器进行分组是有意义的。

Ansible 允许我们创建一个类似 INI 文件的文件,其中包含组(INI 部分)和主机。这是我们的主机文件将要更改为的内容:

[webserver] 
ws01.fale.io 
ws02.fale.io 

[database] 
db01.fale.io 

现在我们可以指示播放书只在某个组中的主机上运行。在上一章中,我们为我们的网站示例创建了三个不同的播放书:

  • firstrun.yaml 是通用的,必须在每台机器上运行。

  • common_tasks.yaml 是通用的,必须在每台机器上运行。

  • webserver.yaml 是特定于 web 服务器的,因此不应在任何其他机器上运行。

由于唯一特定于服务器组的文件是 webserver.yaml,所以我们只需要更改它。为此,让我们打开 webserver.yaml 文件并将内容从 - hosts: all 更改为 - hosts: webserver

只有这三个播放书,我们无法继续创建我们的环境,有三个服务器。因为我们还没有一个设置数据库的播放书(我们将在下一章中看到),我们将完全为两台 web 服务器(ws01.fale.iows02.fale.io)提供服务,并且,对于数据库服务器,我们只提供基本系统。

在运行 Ansible 播放书之前,我们需要为环境提供支持。为此,请创建以下 vagrant 文件:

Vagrant.configure("2") do |config|
  config.vm.define "ws01" do |ws01|
    ws01.vm.hostname = "ws01.fale.io"
  end
  config.vm.define "ws02" do |ws02|
    ws02.vm.hostname = "ws02.fale.io"
  end
  config.vm.define "db01" do |db01|
    db01.vm.hostname = "db01.fale.io"
  end
  config.vm.box = "centos/7"
end

通过运行 vagrant up,Vagrant 将为我们生成整个环境。在一段时间后,Vagrant 在 shell 中输出一些内容后,应该会还给你命令提示符。当这种情况发生时,请检查最后几行是否有错误,以确保一切如预期般进行。

现在我们已经为环境提供了支持,我们可以继续执行 firstrun 播放书,这将确保我们的 Ansible 用户存在并且具有正确的 SSH 密钥设置。为此,我们可以使用以下命令运行它:

$ ansible-playbook -i hosts firstrun.yaml 

以下将是结果。完整的输出文件可在 GitHub 上找到:

PLAY [all] ***********************************************************

TASK [Gathering Facts] ***********************************************
ok: [ws01.fale.io]
ok: [ws02.fale.io]
ok: [db01.fale.io]

TASK [Ensure ansible user exists] ************************************
changed: [ws02.fale.io]
changed: [db01.fale.io]
changed: [ws01.fale.io]
...

正如你所看到的,输出与我们收到的单个主机非常相似,但在每个步骤中每个主机都有一行。在这种情况下,所有机器处于相同的状态,并且执行了相同的步骤,所以我们看到它们都表现得一样,但是在更复杂的场景中,你可以看到不同的机器在同一步骤上返回不同的状态。我们也可以执行另外两个播放书,结果类似。

清单文件中的正则表达式

当你有大量服务器时,给它们起一个可预测的名字是常见和有用的,例如,将所有网络服务器命名为wsXYwebXY,或者将数据库服务器命名为dbXY。如果你这样做,你可以减少主机文件中的行数,增加其可读性。例如,我们的主机文件可以简化如下:

[webserver] 
ws[01:02].fale.io 

[database] 
db01.fale.io 

在这个例子中,我们使用了[01:02],它将匹配第一个数字(在我们的例子中是01)和最后一个数字(在我们的例子中是02)之间的所有出现。在我们的案例中,收益并不巨大,但如果你有 40 台网络服务器,你可以从主机文件中减少 39 行。

在这一部分中,我们已经看到了如何创建清单文件,如何向 Ansible 清单添加组,如何利用范围来加快清单创建过程,并如何针对清单运行 Ansible playbook。现在我们将看到如何在清单中设置变量以及如何在我们的 playbooks 中使用它们。

使用变量

Ansible 允许你以多种方式定义变量,从 playbook 中的变量文件,通过使用-e/--extra-vars选项从 Ansible 命令传递它。你也可以通过将其传递给清单文件来定义变量。你可以在清单文件中的每个主机,整个组或在清单文件所在目录中创建一个变量文件来定义变量。

主机变量

可以为特定主机声明变量,在主机文件中声明它们。例如,我们可能希望为我们的网络服务器指定不同的引擎。假设一个需要回复到一个特定的域,而另一个需要回复到不同的域名。在这种情况下,我们会在以下主机文件中执行:

[webserver] 
ws01.fale.io domainname=example1.fale.io 
ws02.fale.io domainname=example2.fale.io 

[database] 
db01.fale.io 

每次我们使用此清单执行 playbook 时,Ansible 将首先读取清单文件,并将根据每个主机分配domainname变量的值。这样,所有在网络服务器上运行的 playbook 将能够引用domainname变量。

组变量

有些情况下,你可能想要设置一个整个组都有效的变量。假设我们想要声明变量https_enabledTrue,并且其值必须对所有网络服务器都相等。在这种情况下,我们可以创建一个[webserver:vars]部分,因此我们将使用以下主机文件:

[webserver] 
ws01.fale.io 
ws02.fale.io 

[webserver:vars] 
https_enabled=True 

[database] 
db01.fale.io 

请记住,如果相同的变量在两个空间中都声明了,主机变量将覆盖组变量。

变量文件

有时,你需要为每个主机和组声明大量的变量,主机文件变得难以阅读。在这些情况下,你可以将变量移动到特定的文件中。对于主机级别的变量,你需要在host_vars文件夹中创建一个与你的主机同名的文件,而对于组变量,你需要使用组名作为文件名,并将它们放置在group_vars文件夹中。

因此,如果我们想要复制先前基于主机变量使用文件的示例,我们需要创建 host_vars/ws01.fale.io 文件,内容如下:

domainname=example1.fale.io 

然后我们创建 host_vars/ws02.fale.io 文件,内容如下:

domainname=example2.fale.io 

而如果我们想要复制基于组变量的示例,我们将需要具有以下内容的 group_vars/webserver 文件:

https_enabled=True 

清单变量遵循一种层次结构;在其顶部是公共变量文件(我们在上一节 使用清单文件 中讨论过),它将覆盖任何主机变量、组变量和清单变量文件。接下来是主机变量,它将覆盖组变量;最后,组变量将覆盖清单变量文件。

用清单文件覆盖配置参数

你可以直接通过清单文件覆盖一些 Ansible 的配置参数。这些配置参数将覆盖所有其他通过 ansible.cfg、环境变量或在 playbooks 中设置的参数。传递给 ansible-playbook/ansible 命令的变量优先级高于清单文件中设置的任何其他变量。

以下是一些可以从清单文件覆盖的参数列表:

  • ansible_user:该参数用于覆盖与远程主机通信所使用的用户。有时,某台机器需要不同的用户;在这种情况下,这个变量会帮助你。例如,如果你是从 ansible 用户运行 Ansible,但在远程机器上需要连接到 automation 用户,设置 ansible_user=automation 就会实现这一点。

  • ansible_port:该参数将用用户指定的端口覆盖默认的 SSH 端口。有时,系统管理员选择在非标准端口上运行 SSH。在这种情况下,你需要通知 Ansible 进行更改。如果在你的环境中 SSH 端口是 22022 而不是 22,你将需要使用 ansible_port=22022

  • ansible_host:该参数用于覆盖别名的主机。如果你想通过 DNS 名称(即:ws01.fale.io)连接到 10.0.0.3 机器,但由于某些原因 DNS 未能正确解析主机,你可以通过设置 ansible_host=10.0.0.3 变量,强制 Ansible 使用该 IP 而不是 DNS 解析出的 IP。

  • ansible_connection:这指定了到远程主机的连接类型。值可以是 SSH、Paramiko 或本地。即使 Ansible 可以使用其 SSH 守护程序连接到本地机器,但这会浪费大量资源。在这些情况下,你可以指定 ansible_connection=local,这样 Ansible 将打开一个标准 shell 而不是 SSH。

  • ansible_private_key_file:此参数将覆盖用于 SSH 的私钥;如果您想为特定主机使用特定密钥,则这将非常有用。常见的用例是,如果您的主机分布在多个数据中心、多个 AWS 区域或不同类型的应用程序中。在这种情况下,私钥可能不同。

  • ansible__type:默认情况下,Ansible 使用 sh shell;您可以使用 ansible_shell_type 参数覆盖此行为。将其更改为 cshksh 等将使 Ansible 使用该 shell 的命令。如果您需要执行一些 cshksh 脚本,并且立即处理它们会很昂贵,那么这可能会有所帮助。

使用动态清单

有些环境中,你有一个系统可以自动创建和销毁机器。我们将在第五章,云之旅中看到如何使用 Ansible 完成这个任务。在这种环境中,机器列表变化非常快,维护主机文件变得复杂。在这种情况下,我们可以使用动态清单来解决这个问题。

动态清单背后的想法是 Ansible 不会读取主机文件,而是执行一个返回主机列表的脚本,并以 JSON 格式返回给 Ansible。这允许您直接查询您的云提供商,询问在任何给定时刻运行的整个基础设施中的机器列表。

通过 Ansible,已经提供了大多数常见云提供商的许多脚本,可以在github.com/ansible/ansible/tree/devel/contrib/inventory找到,但如果您有不同的需求,也可以创建自定义脚本。Ansible 清单脚本可以用任何语言编写,但出于一致性考虑,动态清单脚本应使用 Python 编写。请记住,这些脚本需要直接可执行,因此请记得为它们设置可执行标志(chmod + x inventory.py)。

接下来,我们将查看可以从官方 Ansible 仓库下载的 Amazon Web Services 和 DigitalOcean 脚本。

Amazon Web Services

要允许 Ansible 从Amazon Web ServicesAWS)收集关于您的 EC2 实例的数据,您需要从 Ansible 的 GitHub 仓库下载以下两个文件:github.com/ansible/ansible

  • ec2.py 清单脚本

  • ec2.ini 文件包含了您的 EC2 清单脚本的配置。

Ansible 使用Boto,AWS Python SDK,通过 API 与 AWS 进行通信。为了允许此通信,您需要导出 AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY 变量。

你可以以两种方式使用清单:

  • 通过 -i 选项将其直接传递给 ansible-playbook 命令,并将 ec2.ini 文件复制到您运行 Ansible 命令的当前目录。

  • ec2.py文件复制到/etc/ansible/hosts,并使用chmod +x使其可执行,然后将ec2.ini文件复制到/etc/ansible/ec2.ini

ec2.py文件将根据地区、可用区、标签等创建多个组。您可以通过运行./ec2.py --list来检查清单文件的内容。

让我们看一个使用 EC2 动态清单的示例 playbook,它将简单地 ping 我的帐户中的所有机器:

ansible -i ec2.py all -m ping

由于我们执行了 ping 模块,我们期望配置的帐户中可用的机器会回复我们。由于我当前帐户中只有一台带有 IP 地址 52.28.138.231 的 EC2 机器,我们可以期望它会回复,实际上我的帐户上的 EC2 回复如下:

52.28.138.231 | SUCCESS => { 
    "changed": false, 
    "ping": "pong" 
} 

在上面的示例中,我们使用ec2.py脚本而不是静态清单文件,并使用-i选项和 ping 命令。

类似地,您可以使用这些清单脚本执行各种类型的操作。例如,您可以将它们与您的部署脚本集成,以找出单个区域中的所有节点,并在执行区域部署时部署到这些节点(一个区域表示一个数据中心)在 AWS 中。

如果您只想知道云中的 web 服务器是什么,并且您已经使用某种约定对它们进行了标记,您可以通过使用动态清单脚本来过滤掉标记来实现。此外,如果您有未涵盖的特殊情况,您可以增强它以提供所需的节点集以 JSON 格式,并从 playbooks 中对这些节点进行操作。如果您正在使用数据库来管理您的清单,您的清单脚本可以查询数据库并转储 JSON。它甚至可以与您的云同步,并定期更新您的数据库。

DigitalOcean

正如我们在github.com/ansible/ansible/tree/devel/contrib/inventory中使用 EC2 文件从 AWS 中提取数据一样,我们可以对 DigitalOcean 执行相同的操作。唯一的区别是我们必须获取digital_ocean.inidigital_ocean.py文件。

与以前一样,如果需要,我们需要调整digital_ocean.ini选项,并将 Python 文件设置为可执行。你唯一可能需要更改的选项是api_token

现在我们可以尝试 ping 我在 DigitalOcean 上预配的两台机器,如下所示:

ansible -i digital_ocean.py all -m ping 

正如预期的那样,我帐户中的两个 droplets 响应如下:

188.166.150.79 | SUCCESS => { 
    "changed": false, 
    "ping": "pong" 
} 
46.101.77.55 | SUCCESS => { 
    "changed": false, 
    "ping": "pong" 
} 

我们现在已经看到从许多不同的云提供商检索数据是多么容易。

在 Ansible 中使用迭代器

您可能已经注意到,到目前为止,我们从未使用过循环,因此每次我们必须执行多个相似的操作时,我们都会多次编写代码。其中一个示例是webserver.yaml代码。

实际上,这是webserver.yaml文件的最后一部分:

    - name: Ensure HTTP can pass the firewall 
      firewalld: 
        service: http 
        state: enabled 
        permanent: True 
        immediate: True 
      become: True 
    - name: Ensure HTTPS can pass the firewall 
      firewalld: 
        service: https 
        state: enabled 
        permanent: True 
        immediate: True 
      become: True 

如你所见,webserver.yaml代码的最后两个块执行的操作非常相似:确保防火墙的某个端口是打开的。

使用标准迭代 - with_items

重复的代码本身并非问题,但它不具备可扩展性。

Ansible 允许我们使用迭代来提高代码的清晰度和可维护性。

为了改进上述代码,我们可以使用简单的迭代:with_items

这使我们能够对项目列表进行迭代。在每次迭代中,列表的指定项目将在 item 变量中可用。这使我们能够在单个块中执行多个类似的操作。

因此,我们可以将webserver.yaml代码的最后一部分更改为以下内容:

    - name: Ensure HTTP and HTTPS can pass the firewall 
      firewalld: 
        service: '{{ item }}' 
        state: enabled 
        permanent: True 
        immediate: True 
      become: True
      with_items:
        - http
        - https

我们可以按照以下方式执行它:

ansible-playbook -i hosts webserver.yaml

我们收到以下内容:

PLAY [all] *********************************************************

TASK [Gathering Facts] *********************************************
ok: [ws01.fale.io]
ok: [ws02.fale.io]

TASK [Ensure the HTTPd package is installed] ***********************
ok: [ws02.fale.io]
ok: [ws01.fale.io]

TASK [Ensure the HTTPd service is enabled and running] *************
ok: [ws01.fale.io]
ok: [ws02.fale.io]

TASK [Ensure HTTP and HTTPS can pass the firewall] *****************
ok: [ws01.fale.io] (item=http)
ok: [ws02.fale.io] (item=http)
ok: [ws01.fale.io] (item=https)
ok: [ws02.fale.io] (item=https)

PLAY RECAP *********************************************************
ws01.fale.io                : ok=5 changed=0 unreachable=0 failed=0 
ws02.fale.io                : ok=5 changed=0 unreachable=0 failed=0

如您所见,输出与先前执行略有不同。事实上,在具有循环操作的行上,我们可以看到在Ensure HTTP and HTTPS can pass the firewall块的特定迭代中处理的item

我们现在已经看到我们可以对项目列表进行迭代,但是 Ansible 还允许我们进行其他类型的迭代。

使用嵌套循环 - with_nested

有些情况下,您必须对列表中的所有元素与其他列表中的所有项进行迭代(笛卡尔积)。一个非常常见的情况是当您必须在多个路径中创建多个文件夹时。在我们的例子中,我们将在用户alicebob的主目录中创建文件夹mailpublic_html

我们可以使用with_nested.yaml文件中的以下代码片段来实现;完整的代码可在 GitHub 上找到:


- hosts: all 
  remote_user: ansible
  vars: 
    users: 
      - alice 
      - bob 
    folders: 
      - mail 
      - public_html 
  tasks: 
    - name: Ensure the users exist 
      user: 
        name: '{{ item }}' 
      become: True 
      with_items: 
        - '{{ users }}' 
    ...

使用以下方式运行此命令:

ansible-playbook -i hosts with_nested.yaml 

我们收到以下结果。完整的输出文件可在 GitHub 上找到:

PLAY [all] *******************************************************

TASK [Gathering Facts] *******************************************
ok: [db01.fale.io]
ok: [ws02.fale.io]
ok: [ws01.fale.io]

TASK [Ensure the users exist] ************************************
changed: [db01.fale.io] => (item=alice)
changed: [ws02.fale.io] => (item=alice)
changed: [ws01.fale.io] => (item=alice)
changed: [db01.fale.io] => (item=bob)
changed: [ws02.fale.io] => (item=bob)
changed: [ws01.fale.io] => (item=bob)
...

如您所见,Ansible 在所有目标机器上创建了用户 alice 和 bob,并且还为这两个用户在所有机器上的文件夹$HOME/mail$HOME/public_html

文件通配符循环 - with_fileglobs

有时,我们希望对某个特定文件夹中的每个文件执行操作。如果你想要从一个文件夹复制多个文件到另一个文件夹中,这可能会很方便。为此,你可以创建一个名为with_fileglobs.yaml的文件,其中包含以下代码:

--- 
- hosts: all 
  remote_user: ansible
  tasks: 
    - name: Ensure the folder /tmp/iproute2 is present 
      file: 
        dest: '/tmp/iproute2' 
        state: directory 
      become: True 
    - name: Copy files that start with rt to the tmp folder 
      copy: 
        src: '{{ item }}' 
        dest: '/tmp/iproute2' 
        remote_src: True 
      become: True 
      with_fileglob: 
        - '/etc/iproute2/rt_*' 

我们可以按照以下方式执行它:

ansible-playbook -i hosts with_fileglobs.yaml 

这导致了以下输出。完整的输出文件可在 GitHub 上找到。

PLAY [all] *****************************************************

TASK [Gathering Facts] *****************************************
ok: [db01.fale.io]
ok: [ws02.fale.io]
ok: [ws01.fale.io]

TASK [Ensure the folder /tmp/iproute2 is present] **************
changed: [ws02.fale.io]
changed: [ws01.fale.io]
changed: [db01.fale.io]

TASK [Copy files that start with rt to the tmp folder] *********
changed: [ws01.fale.io] => (item=/etc/iproute2/rt_realms)
changed: [db01.fale.io] => (item=/etc/iproute2/rt_realms)
changed: [ws02.fale.io] => (item=/etc/iproute2/rt_realms)
changed: [ws01.fale.io] => (item=/etc/iproute2/rt_protos)
...

至于我们的目标,我们已经创建了/tmp/iproute2文件夹,并用/etc/iproute2文件夹中文件的副本填充了它。这种模式经常用于创建配置的备份。

使用整数循环 - with_sequence

许多时候,你需要对整数进行迭代。一个例子是创建十个名为fileXY的文件夹,其中XY是从110的连续数字。为此,我们可以创建一个名为with_sequence.yaml的文件,其中包含以下代码:

--- 
- hosts: all 
  remote_user: ansible 
  tasks: 
  - name: Create the folders /tmp/dirXY with XY from 1 to 10 
    file: 
      dest: '/tmp/dir{{ item }}' 
      state: directory 
    with_sequence: start=1 end=10 
    become: True 

与大多数 Ansible 命令不同,我们可以在对象上使用单行符号和标准 YAML 多行符号,with_sequence 只支持单行符号。

然后,我们可以使用以下命令执行它:

ansible-playbook -i hosts with_sequence.yaml 

我们将收到以下输出:

PLAY [all] *****************************************************

TASK [Gathering Facts] *****************************************
ok: [ws02.fale.io]
ok: [ws01.fale.io]
ok: [db01.fale.io]

TASK [Create the folders /tmp/dirXY with XY from 1 to 10] ******
changed: [ws01.fale.io] => (item=1)
changed: [db01.fale.io] => (item=1)
changed: [ws02.fale.io] => (item=1)
changed: [ws01.fale.io] => (item=2)
changed: [db01.fale.io] => (item=2)
changed: [ws02.fale.io] => (item=2)
changed: [ws01.fale.io] => (item=3)
...

Ansible 支持更多类型的循环,但由于它们的使用要少得多,你可以直接参考官方文档了解循环:docs.ansible.com/ansible/playbooks_loops.html

摘要

在本章中,我们探讨了大量的概念,将帮助您将基础设施扩展到单个节点之外。我们从用于指示 Ansible 关于我们机器的清单文件开始,然后我们介绍了如何在运行相同命令的多个异构主机上拥有主机特定和组特定的变量。然后,我们转向由某些其他系统(通常是云提供商)直接填充的动态清单。最后,我们分析了 Ansible playbook 中的多种迭代方式。

在下一章中,我们将以更合理的方式结构化我们的 Ansible 文件,以确保最大的可读性。为此,我们引入了角色,进一步简化了复杂环境的管理。

处理复杂部署

到目前为止,我们已经学习了如何编写基本的 Ansible playbook,与 playbook 相关的选项,使用 Vagrant 开发 playbook 的实践,以及如何在流程结束时测试 playbook。现在我们为你和你的团队提供了一个学习和开始开发 Ansible playbook 的框架。把这看作是从驾校教练那里学开车的类似过程。你首先学习如何通过方向盘控制汽车,然后你慢慢开始控制刹车,最后,你开始操纵换挡器,因此控制你的汽车速度。一段时间后,随着在不同类型的道路(如平坦、多山、泥泞、坑洼等)上进行越来越多的练习,并驾驶不同的汽车,你会获得专业知识、流畅性、速度,基本上,你会享受整个驾驶过程。从本章开始,我们将深入探讨 Ansible,并敦促你练习并尝试更多的示例以熟悉它。

你一定想知道为什么这一章被命名为这样。原因是,到目前为止,我们还没有达到一个能够在生产环境中部署 playbook 的阶段,特别是在复杂情况下。复杂情况包括您需要与数百或数千台机器进行交互的情况,每组机器都依赖于另一组或几组机器。这些组可能彼此依赖于所有或部分交易,以执行与主从服务器的安全复杂数据备份和复制有关的操作。此外,还有几个有趣而相当引人入胜的 Ansible 功能我们还没有探讨过。在本章中,我们将通过示例介绍所有这些功能。我们的目标是,到本章结束时,您应该清楚如何编写可以从配置管理角度部署到生产中的 playbook。接下来的章节将进一步丰富我们所学内容,以增强使用 Ansible 的体验。

本章将涵盖以下主题:

  • local_action功能以及其他任务委派和条件策略一起工作

  • 使用include、处理程序和角色

  • 转换你的 playbook

  • Jinja 过滤器

  • 安全管理提示和工具

技术要求

您可以从本书的 GitHub 存储库下载所有文件,网址为github.com/PacktPublishing/Learning-Ansible-2.X-Third-Edition/tree/master/Chapter04

使用local_action功能

Ansible 的local_action功能是一个强大的功能,特别是当我们考虑编排时。该功能允许您在运行 Ansible 的机器上本地运行某些任务。

考虑以下情况:

  • 生成新的机器或创建 JIRA 工单

  • 管理您的命令中心,包括安装软件包和设置配置

  • 调用负载均衡器 API 以禁用负载均衡器中某个 Web 服务器条目

这些通常是可以在运行 ansible-playbook 命令的同一台机器上运行的任务,而不是登录到远程框并运行这些命令。

让我们举个例子。假设你想在本地系统上运行一个 shell 模块,你正在那里运行你的 Ansible playbook。在这种情况下,local_action 选项就会发挥作用。如果你将模块名称和模块参数传递给 local_action,它将在本地运行该模块。

让我们看看这个选项如何与 shell 模块一起工作。考虑下面的代码,显示了 local_action 选项的输出:

--- 
- hosts: database 
  remote_user: vagrant
  tasks: 
    - name: Count processes running on the remote system 
      shell: ps | wc -l 
      register: remote_processes_number 
    - name: Print remote running processes 
      debug: 
        msg: '{{ remote_processes_number.stdout }}' 
    - name: Count processes running on the local system 
      local_action: shell ps | wc -l 
      register: local_processes_number 
    - name: Print local running processes 
      debug: 
        msg: '{{ local_processes_number.stdout }}' 

现在我们可以将其保存为 local_action.yaml 并使用以下命令运行它:

ansible-playbook -i hosts local_action.yaml

我们会收到以下结果:

PLAY [database] ****************************************************
TASK [Gathering Facts] *********************************************
ok: [db01.fale.io]
TASK [Count processes running on the remote system] ****************
changed: [db01.fale.io]
TASK [Print remote running processes] ******************************
ok: [db01.fale.io] => {
 "msg": "6"
}
TASK [Count processes running on the local system] *****************
changed: [db01.fale.io -> localhost]
TASK [Print local running processes] *******************************
ok: [db01.fale.io] => {
 "msg": "9"
}
PLAY RECAP *********************************************************
db01.fale.io                : ok=5 changed=2 unreachable=0 failed=0 

正如你所看到的,这两个命令提供给我们不同的数字,因为它们在不同的主机上执行。你可以用 local_action 运行任何模块,Ansible 将确保该模块在运行 ansible-playbook 命令的主机上本地运行。另一个你可以(也应该!)尝试的简单例子是运行两个任务:

  • 在远程机器(上述情况中的 db01)上执行 uname

  • 在启用 local_action 的情况下在本地机器上执行 uname

这将进一步阐明 local_action 的概念。

Ansible 提供了另一种方法,可以将某些操作委托给特定(或不同的)机器:delegate_to 系统。

委托任务

有时,你会想在不同的系统上执行某个操作。例如,在你部署应用程序服务器节点时,可能会是数据库节点,也可能是本地主机。为此,你可以简单地将 delegate_to: HOST 属性添加到你的任务中,它将在适当的节点上运行。让我们重新设计上一个例子以实现这一点:

--- 
- hosts: database 
  remote_user: vagrant
  tasks: 
    - name: Count processes running on the remote system 
      shell: ps | wc -l 
      register: remote_processes_number 
    - name: Print remote running processes 
      debug: 
        msg: '{{ remote_processes_number.stdout }}' 
    - name: Count processes running on the local system 
      shell: ps | wc -l 
      delegate_to: localhost 
      register: local_processes_number 
    - name: Print local running processes 
      debug: 
        msg: '{{ local_processes_number.stdout }}' 

将其保存为 delegate_to.yaml,我们可以使用以下命令运行它:

ansible-playbook -i hosts delegate_to.yaml

我们会收到与前一个示例相同的输出:

PLAY [database] **************************************************

TASK [Gathering Facts] *******************************************
ok: [db01.fale.io]
TASK [Count processes running on the remote system] **************
changed: [db01.fale.io]
TASK [Print remote running processes] ****************************
ok: [db01.fale.io] => {
 "msg": "6"
}
TASK [Count processes running on the local system] ***************
changed: [db01.fale.io -> localhost]

TASK [Print local running processes] *****************************
ok: [db01.fale.io] => {
 "msg": "9"
}
PLAY RECAP *******************************************************
db01.fale.io              : ok=5 changed=2 unreachable=0 failed=0 

在这个例子中,我们看到了如何在同一个 playbook 中对远程主机和本地主机执行操作。在复杂的流程中,这变得很方便,其中一些步骤需要由本地机器或你可以连接到的任何其他机器执行。

使用条件

到目前为止,我们只看到了 playbook 的工作原理以及任务是如何执行的。我们也看到了 Ansible 顺序执行所有这些任务。然而,这不会帮助你编写一个包含数十个任务并且只需要执行其中一部分任务的高级 playbook。例如,假设你有一个 playbook,它会在远程主机上安装 Apache HTTPd 服务器。现在,Debian-based 操作系统对 Apache HTTPd 服务器有一个不同的包名称,叫做 apache2;对于基于 Red Hat 的操作系统,它叫做 httpd

在 playbook 中有两个任务,一个用于 httpd 包(适用于基于 Red Hat 的系统),另一个用于 apache2 包(适用于基于 Debian 的系统),这样 Ansible 就会安装这两个包,但是执行会失败,因为如果你在基于 Red Hat 的操作系统上安装,apache2 将不可用。为了克服这样的问题,Ansible 提供了条件语句,帮助仅在满足指定条件时运行任务。在这种情况下,我们执行类似以下伪代码的操作:

If os = "redhat" 
  Install httpd 
Else if os = "debian" 
  Install apache2 
End 

在基于 Red Hat 的操作系统上安装 httpd 时,我们首先检查远程系统是否运行了基于 Red Hat 的操作系统,如果是,则安装 httpd 包;否则,我们跳过该任务。让我们直接进入一个名为 conditional_httpd.yaml 的示例 playbook,其内容如下:

--- 
- hosts: webserver 
  remote_user: vagrant
  tasks: 
    - name: Print the ansible_os_family value 
      debug: 
        msg: '{{ ansible_os_family }}' 
    - name: Ensure the httpd package is updated 
      yum: 
        name: httpd 
        state: latest 
      become: True 
      when: ansible_os_family == 'RedHat' 
    - name: Ensure the apache2 package is updated 
      apt: 
        name: apache2 
        state: latest 
      become: True 
      when: ansible_os_family == 'Debian' 

使用以下方式运行它:

ansible-playbook -i hosts conditional_httpd.yaml

这是结果。完整的代码输出文件可以在 GitHub 上找到:

PLAY [webserver] ***********************************************

TASK [Gathering Facts] *****************************************
ok: [ws03.fale.io]
ok: [ws02.fale.io]
ok: [ws01.fale.io]

TASK [Print the ansible_os_family value] ***********************
ok: [ws01.fale.io] => {
 "msg": "RedHat"
}
ok: [ws02.fale.io] => {
 "msg": "RedHat"
}
ok: [ws03.fale.io] => {
 "msg": "Debian"
}
...

如你所见,我为此示例创建了一个名为 ws03 的新服务器,它是基于 Debian 的。如预期,在两个 CentOS 节点上执行了 httpd 包的安装,而在 Debian 节点上执行了 apache2 包的安装。

Ansible 只区分少数家族(在撰写本书时为 AIX、Alpine、Altlinux、Archlinux、Darwin、Debian、FreeBSD、Gentoo、HP-UX、Mandrake、Red Hat、Slackware、Solaris 和 Suse);因此,CentOS 机器具有一个 ansible_os_family 值:RedHat

同样,你也可以匹配不同的条件。Ansible 支持等于 (==),不等于 (!=),大于 (>),小于 (<),大于或等于 (>=) 和小于或等于 (<=)。

到目前为止,我们见过的运算符将匹配变量的整个内容,但如果你只想检查变量中是否存在特定字符或字符串怎么办?为了执行这些类型的检查,Ansible 提供了 innot 运算符。你还可以使用 andor 运算符匹配多个条件。and 运算符会确保在执行此任务之前所有条件都匹配,而 or 运算符会确保至少有一个条件匹配。

布尔条件

除了字符串匹配外,你还可以检查一个变量是否为 True。当你想要检查一个变量是否被赋值时,这种类型的验证将非常有用。你甚至可以根据变量的布尔值执行任务。

例如,让我们将以下代码放入名为 crontab_backup.yaml 的文件中:

--- 
- hosts: all 
  remote_user: vagrant
  vars: 
    backup: True 
  tasks: 
    - name: Copy the crontab in tmp if the backup variable is true 
      copy: 
        src: /etc/crontab 
        dest: /tmp/crontab 
        remote_src: True 
      when: backup 

我们可以使用以下方式执行它:

ansible-playbook -i hosts crontab_backup.yaml

然后我们得到以下结果:

PLAY [all] ***************************************************

TASK [Gathering Facts] ***************************************
ok: [ws03.fale.io]
ok: [ws01.fale.io]
ok: [db01.fale.io]
ok: [ws02.fale.io]

TASK [Copy the crontab in tmp if the backup variable is true]
changed: [ws03.fale.io]
changed: [ws02.fale.io]
changed: [ws01.fale.io]
changed: [db01.fale.io]

PLAY RECAP ***************************************************
db01.fale.io          : ok=2 changed=1 unreachable=0 failed=0 
ws01.fale.io          : ok=2 changed=1 unreachable=0 failed=0 
ws02.fale.io          : ok=2 changed=1 unreachable=0 failed=0 
ws03.fale.io          : ok=2 changed=1 unreachable=0 failed=0 

我们可以稍微改变命令为这样:

ansible-playbook -i hosts crontab_backup.yaml --extra-vars="backup=False"

然后我们将收到这个输出:

PLAY [all] ***************************************************

TASK [Gathering Facts] ***************************************
ok: [ws03.fale.io]
ok: [ws01.fale.io]
ok: [db01.fale.io]
ok: [ws02.fale.io]

TASK [Copy the crontab in tmp if the backup variable is true]
skipping: [ws01.fale.io]
skipping: [ws02.fale.io]
skipping: [ws03.fale.io]
skipping: [db01.fale.io]

PLAY RECAP ***************************************************
db01.fale.io          : ok=1 changed=0 unreachable=0 failed=0 
ws01.fale.io          : ok=1 changed=0 unreachable=0 failed=0 
ws02.fale.io          : ok=1 changed=0 unreachable=0 failed=0 
ws03.fale.io          : ok=1 changed=0 unreachable=0 failed=0 

正如你所看到的,在第一种情况下,操作被执行了,而在第二种情况下,它被跳过了。我们可以通过配置文件、host 变量或 group 变量覆盖备份值。

如果以这种方式检查并且变量未设置,Ansible 将假定其为 False

检查变量是否设置

有时候,你会发现自己不得不在命令中使用一个变量。每次这样做时,你都必须确保变量是设置的。这是因为一些命令如果用一个未设置的变量调用可能会造成灾难性后果(也就是说,如果你执行 rm -rf $VAR/*$VAR 没有设置或为空,它会清空你的机器)。为了做到这一点,Ansible 提供了一种检查变量是否定义的方式。

我们可以按以下方式改进前面的示例:

--- 
- hosts: all 
  remote_user: ansible 
  vars: 
    backup: True 
  tasks: 
    - name: Check if the backup_folder is set 
      fail: 
        msg: 'The backup_folder needs to be set' 
      when: backup_folder is not defined or backup_folder == “” 
    - name: Copy the crontab in tmp if the backup variable is true 
      copy: 
        src: /etc/crontab 
        dest: '{{ backup_folder }}/crontab' 
        remote_src: True 
      when: backup 

如您所见,我们使用了 fail 模块,它允许我们在 backup_folder 变量未设置时将 Ansible playbook 放入失败状态。

使用 include 进行操作

include 功能帮助您减少编写任务时的重复性。这也允许我们通过将可重用代码包含在单独的任务中来拥有更小的 playbooks,使用不要重复自己DRY)原则。

要触发包含另一个文件的过程,您需要将以下内容放在 tasks 对象下方:

 - include: FILENAME.yaml 

您还可以将一些变量传递给被包含的文件。为此,我们可以按以下方式指定它们:

- include: FILENAME.yaml variable1="value1" variable2="value2"

使用 include 语句保持您的变量清洁和简洁,并遵循 DRY 原则,这将使您能够编写更易于维护和遵循的 Ansible 代码。

处理程序

在许多情况下,您将有一个任务或一组任务,这些任务会更改远程机器上的某些资源,需要触发一个事件才能生效。例如,当您更改服务配置时,您需要重新启动或重新加载服务本身。在 Ansible 中,您可以使用 notify 动作触发此事件。

每个处理程序任务将在通知时在 playbooks 结束时运行。例如,您多次更改了 HTTPd 服务器配置,并且希望重新启动 HTTPd 服务以应用更改。现在,每次更改配置时重新启动 HTTPd 并不是一个好的做法;即使没有对其配置进行任何更改,重新启动服务器也不是一个好的做法。为了处理这种情况,您可以通知 Ansible 在每次配置更改时重新启动 HTTPd 服务,但是 Ansible 会确保,无论您多少次通知它重新启动 HTTPd,它都将在所有其他任务完成后仅调用该任务一次。让我们按照以下方式稍微更改我们在前几章中创建的 webserver.yaml 文件;完整代码可在 GitHub 上找到:

--- 
- hosts: webserver 
  remote_user: vagrant
  tasks: 
    - name: Ensure the HTTPd package is installed 
      yum: 
        name: httpd 
        state: present 
      become: True 
    - name: Ensure the HTTPd service is enabled and running 
      service: 
        name: httpd 
        state: started 
        enabled: True 
      become: True 
    - name: Ensure HTTP can pass the firewall 
      firewalld: 
 service: http 
        state: enabled 
        permanent: True 
        immediate: True 
      become: True 
   ...

使用以下方式运行此脚本:

ansible-playbook -i hosts webserver.yaml

我们将得到以下输出。完整的代码输出文件可在 GitHub 上找到:

PLAY [webserver] *********************************************

TASK [Gathering Facts] ***************************************
ok: [ws01.fale.io]
ok: [ws02.fale.io]

TASK [Ensure the HTTPd package is installed] *****************
ok: [ws01.fale.io]
ok: [ws02.fale.io]

TASK [Ensure the HTTPd service is enabled and running] *******
changed: [ws02.fale.io]
changed: [ws01.fale.io]

...

在这种情况下,处理程序是从配置文件更改中触发的。但是如果我们再运行一次,配置将不会改变,因此,我们将得到以下结果:

PLAY [webserver] *********************************************

TASK [Gathering Facts] ***************************************
ok: [ws01.fale.io]
ok: [ws02.fale.io]

TASK [Ensure the HTTPd package is installed] *****************
ok: [ws01.fale.io]
ok: [ws02.fale.io]

TASK [Ensure the HTTPd service is enabled and running] *******
ok: [ws02.fale.io]
ok: [ws01.fale.io]

TASK [Ensure HTTP can pass the firewall] *********************
ok: [ws02.fale.io]
ok: [ws01.fale.io]

TASK [Ensure HTTPd configuration is updated] *****************
ok: [ws02.fale.io]
ok: [ws01.fale.io]

PLAY RECAP ***************************************************
ws01.fale.io          : ok=5 changed=0 unreachable=0 failed=0 
ws02.fale.io          : ok=5 changed=0 unreachable=0 failed=0

如你所见,这次没有执行任何处理程序,因为所有可能触发它们执行的步骤都没有改变,所以不需要处理程序。记住这个行为,以确保你不会对未执行的处理程序感到惊讶。

处理角色

我们已经看到了如何自动化简单的任务,但我们到目前为止看到的内容不会解决你所有的问题。这是因为 playbook 很擅长执行操作,但不太擅长配置大量的机器,因为它们很快就会变得混乱。为了解决这个问题,Ansible 有角色

我对角色的定义是一组用于实现特定目标的 playbook、模板、文件或变量。例如,我们可以有一个数据库角色和一个 Web 服务器角色,以便这些配置保持清晰分离。

在开始查看角色内部之前,让我们谈谈组织项目的问题。

组织项目

在过去的几年里,我为多个组织的多个 Ansible 仓库工作过,其中许多非常混乱。为了确保您的存储库易于管理,我将给您一个我始终使用的模板。

首先,我总是在 root 文件夹中创建三个文件:

  • ansible.cfg:一个小配置文件,用于告诉 Ansible 在我们的文件夹结构中查找文件的位置。

  • hosts:我们已经在前几章中看到的主机文件。

  • master.yaml:一个将整个基础架构对齐的 playbook。

除了这三个文件外,我还创建了两个文件夹:

  • playbooks:这将包含 playbook 和一个名为 groups 的文件夹,用于组管理。

  • roles:这将包含我们需要的所有角色。

为了澄清这一点,让我们使用 Linux 的 tree 命令来查看一个需要 Web 服务器和数据库服务器的简单 Web 应用程序的 Ansible 仓库的结构:

    ├── ansible.cfg
    ├── hosts
    ├── master.yaml
    ├── playbooks
    │   ├── firstrun.yaml
    │   └── groups
    │       ├── database.yaml
    │       └── webserver.yaml
    └── roles
        ├── common
        ├── database
        └── webserver

如你所见,我也添加了一个 common 角色。这对于将所有应该为每台服务器执行的事情放在一起非常有用。通常,我在这个角色中配置 NTP、motd 和其他类似的服务,以及机器主机名。

现在我们将看看如何结构化一个角色。

角色的解剖

角色中的文件夹结构是标准的,你不能改变它太多。

角色中最重要的文件夹是 tasks 文件夹,因为这是其中唯一必需的文件夹。它必须包含一个 main.yaml 文件,这将是要执行的任务列表。在角色中经常存在的其他文件夹是模板和文件。第一个将用于存储 模板任务 使用的模板,而第二个将用于存储 复制任务 使用的文件。

将你的 playbook 转换成一个完整的 Ansible 项目

让我们看看如何将我们用来设置我们的 Web 基础架构的三个 playbooks(common_tasks.yamlfirstrun.yamlwebserver.yaml)转换为适合这个文件组织的文件。我们必须记住,我们在这些角色中还使用了两个文件(index.html.j2motd),所以我们也必须适当地放置这些文件。

首先,我们将创建我们在前一段中看到的文件夹结构。

最容易转换的 playbook 是firstrun.yaml,因为我们只需要将它复制到playbooks文件夹中。这个 playbook 将保持为一个 playbook,因为它是一组操作,每台服务器只需运行一次。

现在,我们转到common_tasks.yaml playbook,它需要一点重新调整以匹配角色范例。

将一个 playbook 转换为一个角色

我们需要做的第一件事是创建roles/common/tasksroles/common/templates文件夹。在第一个文件夹中,我们将添加以下main.yaml文件。完整的代码在 GitHub 上可用:

---
- name: Ensure EPEL is enabled 
  yum: 
    name: epel-release 
    state: present 
  become: True 
- name: Ensure libselinux-python is present 
  yum: 
    name: libselinux-python 
    state: present 
  become: True 
- name: Ensure libsemanage-python is present 
  yum: 
    name: libsemanage-python 
    state: present 
  become: True 
...

正如你所看到的,这与我们的common_tasks.yaml playbooks 非常相似。事实上,只有两个区别:

  • hostsremote_usertasks行(第 2、3 和 4 行)已被删除。

  • 文件的其余部分的缩进已经相应地修正了。

在这个角色中,我们使用了模板任务在服务器上创建了一个名为motd的文件,其中包含了机器的 IP 和其他有趣的信息。因此,我们需要创建roles/common/templates并把motd模板放在里面。

在这一点上,我们的常规任务将具有这种结构:

common/ 
├── tasks 
│   └── main.yaml 
└── templates 
    └── motd 

现在,我们需要指示 Ansible 在哪些机器上执行common角色中指定的所有任务。为此,我们应该查看 playbooks/groups 目录。在这个目录中,为每组逻辑上相似的机器(即执行相同类型操作的机器)准备一个文件非常方便,就像数据库和 Web 服务器一样。

因此,让我们在playbooks/groups中创建一个名为database.yaml的文件,内容如下:

--- 
- hosts: database 
  user: vagrant 
  roles: 
  - common 

在相同文件夹中创建一个名为webserver.yaml的文件,内容如下:

--- 
- hosts: webserver 
  user: vagrant 
  roles: 
  - common 

如你所见,这些文件指定了我们要操作的主机组、要在这些主机上使用的远程用户以及我们要执行的角色。

辅助文件

当我们在前一章中创建hosts文件时,我们注意到它有助于简化我们的命令行。因此,让我们开始将我们之前在root文件夹中使用的 hosts 文件复制到我们 Ansible 存储库的根目录中。到目前为止,我们总是在命令行上指定这个文件的路径。如果我们创建一个告诉 Ansible 我们的hosts文件位置的ansible.cfg文件,这将不再需要。因此,让我们在我们 Ansible 存储库的根目录中创建一个名为ansible.cfg的文件,并添加以下内容:

[defaults] 
inventory = hosts 
host_key_checking = False 
roles_path = roles 

在这个文件中,除了我们已经谈论过的inventory之外,我们还指定了另外两个变量,它们是host_key_checkingroles_path

host_key_checking标志对于不要求验证远程系统 SSH 密钥非常有用。尽管在生产中不建议使用这种方式,因为建议在这种环境中使用公钥传播系统,但在测试环境中非常方便,因为它将帮助您减少 Ansible 等待用户输入的时间。

roles_path用于告诉 Ansible 在哪里找到我们 playbooks 的角色。

我通常会添加一个额外的文件,即master.yaml。我发现这非常有用,因为你经常需要保持基础架构与你的 Ansible 代码保持一致。为了在单个命令中执行它,你需要一个能运行 playbooks/groups 中所有文件的文件。因此,让我们在 Ansible 仓库的root文件夹中创建一个master.yaml文件,内容如下:

--- 
- import_playbook: playbooks/groups/database.yaml 
- import_playbook: playbooks/groups/webserver.yaml 

此时,我们可以执行以下操作:

ansible-playbook master.yaml 

结果将是以下内容。完整的代码输出文件可在 GitHub 上找到:

PLAY [database] ********************************************** 
TASK [Gathering Facts] ***************************************
ok: [db01.fale.io]

TASK [common : Ensure EPEL is enabled] ***********************
ok: [db01.fale.io]

TASK [common : Ensure libselinux-python is present] **********
ok: [db01.fale.io]

TASK [common : Ensure libsemanage-python is present] *********
ok: [db01.fale.io]

TASK [common : Ensure we have last version of every package] *
ok: [db01.fale.io]
...

如前面的输出所示,列在common角色中的操作首先在database组中的节点上执行,然后在webserver组中的节点上执行。

转换 web 服务器角色

正如我们将common playbook 转换为common角色一样,我们可以将webserver角色也转换为webserver角色。

在角色中,我们需要有带有tasks子文件夹的webserver文件夹。在这个文件夹中,我们必须放置包含从 playbooks 复制的tasksmain.yaml文件。以下是代码片段;完整的代码可以在 GitHub 上找到:

--- 
- name: Ensure the HTTPd package is installed 
  yum: 
    name: httpd 
    state: present 
  become: True 
- name: Ensure the HTTPd service is enabled and running 
  service: 
    name: httpd 
    state: started 
    enabled: True 
  become: True 
- name: Ensure HTTP can pass the firewall 
  firewalld: 
    service: http 
    state: enabled 
    permanent: True 
    immediate: True 
  become: True 
... 

在此角色中,我们使用了多个任务,这些任务需要额外的资源才能正常工作;更具体地说,我们需要执行以下操作:

  • website.conf文件放在roles/webserver/files中。

  • index.html.j2模板放在roles/webserver/templates中。

  • 创建Restart HTTPd处理程序。

前两个应该很简单。实际上,第一个是一个空文件(因为默认配置已经足够我们使用),而index.html.j2文件应包含以下内容:

<html> 
    <body> 
        <h1>Hello World!</h1> 
        <p>This page was created on {{ ansible_date_time.date }}.</p> 
        <p>This machine can be reached on the following IP addresses</p> 
        <ul> 
{% for address in ansible_all_ipv4_addresses %} 
            <li>{{ address }}</li> 
{% endfor %} 
        </ul> 
    </body> 
</html> 

角色中的处理程序

完成此角色的最后一件事是创建Restart HTTPd通知的处理程序。为此,我们需要在roles/webserver/handlers中创建一个main.yaml文件,内容如下:

--- 
- name: Restart HTTPd 
  service: 
    name: httpd 
    state: restarted 
  become: True 

正如您可能已经注意到的,这与我们在 playbook 中使用的处理程序非常相似,只是文件位置和缩进不同。

使我们的角色可应用的唯一还需要做的事情是将条目添加到playbooks/groups/webserver.yaml文件中,以便通知 Ansible 服务器应用 Web 服务器角色以及常见角色。我们的playbooks/groups/webserver.yaml文件应该如下所示:

--- 
- hosts: webserver 
  user: ansible 
  roles: 
  - common 
  - webserver 

现在我们可以再次执行 master.yaml,以将 Web 服务器角色应用于相关服务器,但我们也可以只执行 playbooks/groups/webserver.yaml,因为我们刚刚进行的更改只与此服务器组相关。为此,我们运行以下命令:

ansible-playbook playbooks/groups/webserver.yaml 

我们应该收到类似于以下的输出。完整的代码输出文件可在 GitHub 上找到:

PLAY [webserver] *********************************************

TASK [Gathering Facts] ***************************************
ok: [ws01.fale.io]
ok: [ws02.fale.io]

TASK [common : Ensure EPEL is enabled] ***********************
ok: [ws01.fale.io]
ok: [ws02.fale.io]

TASK [common : Ensure libselinux-python is present] **********
ok: [ws01.fale.io]
ok: [ws02.fale.io]

TASK [common : Ensure libsemanage-python is present] *********
ok: [ws01.fale.io]
ok: [ws02.fale.io]

...

正如您在上述输出中所看到的,commonwebserver 角色都已应用于 webserver 节点。

对于一个特定的节点,应用所有相关角色而不仅仅是您更改的角色非常重要,因为往往情况是,如果一个组中的一个或多个节点出现问题,而同一组中的其他节点没有问题,那么问题可能是该组中的某些角色被不均等地应用了。仅将所有相关角色应用于组将授予您该组节点的平等。

执行策略

在 Ansible 2 之前,每个任务都需要在每台机器上执行(并完成),然后 Ansible 才会在所有机器上发出新任务。这意味着,如果您正在对一百台机器执行任务,其中一台机器性能不佳,所有机器都将以性能不佳的机器的速度运行。

使用 Ansible 2,执行策略已被制作成模块化和可插拔的;因此,您现在可以为您的播放书选择您喜欢的执行策略。您还可以编写自定义执行策略,但这超出了本书的范围。目前(在 Ansible 2.7 中),只有三种执行策略,线性串行自由

  • 线性执行:此策略与 Ansible 版本 2 之前的行为完全相同。这是默认策略。

  • 串行执行:此策略将获取一组主机(默认为五个)并在移动到下一组之前对这些主机执行所有任务,然后从头开始。这种执行策略可以帮助您在有限数量的主机上工作,以便您始终有一些可用于用户的主机。如果您正在寻找这种部署类型,您将需要一个位于主机之前的负载均衡器,该负载均衡器需要在每个给定时刻知道哪些节点正在维护。

  • 自由执行:此策略将为每个主机提供一个新任务,一旦该主机完成了前一个任务。这将允许更快的主机在较慢的节点之前完成播放。如果选择此执行策略,您必须记住,某些任务可能需要在所有节点上完成先前的任务(例如,集群数据库需要所有数据库节点安装并运行数据库),在这种情况下,它们可能会失败。

Ansible 模板 - Jinja 过滤器

我们已经在第二章,自动化简单任务中看到,这些模板允许您动态完成您的 playbook,并根据诸如hostgroup变量等动态数据在服务器上放置文件。在这一节中,我们将进一步看到Jinja2 过滤器如何与 Ansible 协同工作。

Jinja2 过滤器是简单的 Python 函数,它们接受一些参数,处理它们,并返回结果。例如,考虑以下命令:

{{ myvar | filter }}

在上面的示例中,myvar是一个变量;Ansible 将myvar作为参数传递给 Jinja2 过滤器。然后 Jinja2 过滤器会处理它并返回结果数据。Jinja2 过滤器甚至接受额外的参数,如下所示:

{{ myvar | filter(2) }}

在这个例子中,Ansible 现在会传递两个参数,即myvar2。同样,你可以通过逗号分隔传递多个参数给过滤器。

Ansible 支持各种各样的 Jinja2 过滤器,我们将看到一些你在编写 playbook 时可能需要使用的重要 Jinja2 过滤器。

使用过滤器格式化数据

Ansible 支持 Jinja2 过滤器将数据格式化为 JSON 或 YAML。你将一个字典变量传递给这个过滤器,它将把你的数据格式化为 JSON 或 YAML。例如,考虑以下命令行:

{{ users | to_nice_json }}

在前面的示例中,users是变量,to_nice_json是 Jinja2 过滤器。正如我们之前看到的,Ansible 将users作为参数内部传递给 Jinja2 过滤器to_nice_json。同样,你也可以使用以下命令将你的数据格式化为 YAML:

{{ users | to_nice_yaml }}

默认未定义的变量

我们在前面的章节中已经看到,在使用变量之前检查它是否被定义是明智的。我们可以为变量设置一个default值,这样,如果变量没有被定义,Ansible 将使用该值而不是失败。要这样做,我们使用这个:

{{ backup_disk | default("/dev/sdf") }}

这个过滤器不会将default值分配给变量;它只会将default值传递给正在使用它的当前任务。在结束本节之前,让我们看一些 Jinja 过滤器本身的更多示例:

  • 执行此命令以从列表中获取一个随机字符:
{{ ['a', 'b', 'c', 'd'] | random }} 
  • 执行此命令以获取从0100的随机数:
{{ 100 | random }}
  • 执行此命令以获取从1050的随机数:
{{ 50  | random(10) }}
  • 执行此命令以获取从2050,步长为10的随机数:
{{ 50 | random(20, 10) }}
  • 使用过滤器将列表连接为字符串:Jinja2 过滤器允许您使用 join 过滤器将列表连接为字符串。这个过滤器将一个分隔符作为额外参数。如果你不指定分隔符,则该过滤器会将列表的所有元素组合在一起而不进行任何分隔。考虑以下例子:
{{ ["This", "is", "a", "string"] | join(" ") }} 

前述过滤器将产生一个输出This is a string。您可以指定任何分隔符,而不是空格。

当涉及使用过滤器编码或解码数据时,你可以按如下方式使用过滤器编码或解码数据:

  • 使用b64encode过滤器将您的数据编码为base64
{{ variable | b64encode }} 
  • 使用 b64decode 过滤器解码编码的 base64 字符串:
{{ "aGFoYWhhaGE=" | b64decode }} 

安全管理

本章的最后一部分是关于安全管理的。如果你告诉系统管理员你想引入一个新功能或工具,他们会问你的第一个问题之一是:"你的工具有哪些安全功能?"我们将尝试在这一部分从 Ansible 的角度回答这个问题。让我们更详细地看一下。

使用 Ansible Vault

Ansible Vault 是 Ansible 中一个令人兴奋的功能,它在 Ansible 版本 1.5 中引入。这允许你在源代码中使加密密码成为一部分。建议最好不要在你的存储库中以明文形式包含密码(以及其他敏感信息,如私钥和 SSL 证书),因为任何检出你存储库的人都可以查看你的密码。Ansible Vault 可以通过加密和解密来帮助你保护机密信息。

Ansible Vault 支持交互模式,在该模式下它将要求你输入密码,或非交互模式,在该模式下你将需要指定包含密码的文件,Ansible Vault 将直接读取它。

对于这些示例,我们将使用密码 ansible,因此让我们开始创建一个名为 .password 的隐藏文件,并在其中放置字符串 ansible。为此,请执行以下操作:

echo 'ansible' > .password

现在我们可以在交互和非交互模式下都创建 ansible-vault。如果我们想以交互模式进行,我们将需要执行以下操作:

ansible-vault create secret.yaml

Ansible 将要求我们提供保险柜密码,然后确认。稍后,它将打开默认文本编辑器(在我的情况下是vi)以添加明文内容。我已使用密码 ansible 和文本是 This is a password protected file。现在我们可以保存并关闭编辑器,并检查 ansible-vault 是否已加密我们的内容;事实上,我们可以运行以下命令:

cat secret.yaml

它将输出以下内容:

$ANSIBLE_VAULT;1.1;AES256
65396465353561366635653333333962383237346234626265633461353664346532613566393365
3263633761383434363766613962386637383465643130320a633862343137306563323236313930
32653533316238633731363338646332373935353935323133666535386335386437373539393365
3433356539333232650a643737326362396333623432336530303663366533303465343737643739
63373438316435626138646236643663313639303333306330313039376134353131323865373330
6333663133353730303561303535356230653533346364613830

同样,我们可以使用 - vault-password-file=VAULT_PASSWORD_FILE 选项在 ansible-vault 命令上调用非交互模式来指定我们的 .password 文件。例如,我们可以使用以下命令编辑我们的 secret.yaml 文件:

ansible-vault --vault-password-file=.password edit secret.yaml 

这将打开你的默认文本编辑器,你将能够更改文件,就像它是一个普通文件一样。当你保存文件时,Ansible Vault 会在保存之前执行加密,确保你内容的保密性。

有时,你需要查看文件的内容,但你不想在文本编辑器中打开它,所以通常使用 cat 命令。Ansible Vault 有一个类似的功能叫做 view,所以你可以运行以下命令:

ansible-vault --vault-password-file=.password view secret.yaml

Ansible Vault 允许你解密文件,将其加密内容替换为其明文内容。为此,你可以执行以下操作:

ansible-vault --vault-password-file=.password decrypt secret.yaml 

此时,我们可以在 secret.yaml 文件上使用 cat 命令,结果如下:

This is a password protected file

Ansible Vault 还提供了加密已经存在的文件的功能。如果您想要在受信任的机器上(例如,您自己的本地机器)上以明文形式开发所有文件以提高效率,然后在之后加密所有敏感文件,这将特别有用。为此,您可以执行以下操作:

ansible-vault --vault-password-file=.password encrypt secret.yaml

您现在可以检查secret.yaml文件是否再次加密。

Ansible Vault 的最后一个选项非常重要,因为它是一个rekey功能。此功能将允许您在单个命令中更改加密密钥。您可以使用两个命令执行相同的操作(使用旧密钥解密secret.yaml文件,然后使用新密钥加密它),但能够在单个步骤中执行此操作具有重大优势,因为文件以明文形式存储在磁盘上的任何时刻都不会被存储。

为此,我们需要一个包含新密码的文件(在我们的情况下,文件名为.newpassword,包含字符串ansible2),并且您需要执行以下命令:

ansible-vault --vault-password-file=.password --new-vault-password-file=.newpassword rekey secret.yaml 

我们现在可以使用cat命令查看secret.yaml文件,我们将看到以下输出:

$ANSIBLE_VAULT;1.1;AES256
32623466356639646661326164313965313366393935623236323465356265313630353930346135
3730616433353331376537343962366661616363386235330a643261303132336437613464636332
36656564653836616238383836383562633037376533376135663034316263323764656531656137
3462323739653339360a613933633865383837393331616363653765646165363333303232633132
63393237383231393738316465356636396133306132303932396263333735643230316361383339
3365393438636530646366336166353865376139393361396539

这与我们以前的非常不同。

保险库和播放脚本

您还可以使用ansible-playbook与保险库。您需要使用如下命令动态解密文件:

$ ansible-playbook site.yml --vault-password-file .password

还有另一个选项允许您使用脚本解密文件,该脚本然后可以查找其他源并解密文件。这也可以是提供更多安全性的有用选项。但是,请确保get_password.py脚本具有可执行权限:

$ ansible-playbook site.yml --vault-password-file ~/.get_password.py 

在结束本章之前,我想稍微谈谈密码文件。此文件需要存在于执行 playbooks 的机器上,在位置和权限方面,以便由执行 playbook 的用户可读取。您可以在启动时创建.password文件。

.password文件名中的.字符是为了确保文件默认隐藏在查找时。这不直接是一项安全措施,但它可能有助于减轻攻击者不知道他们正在寻找什么的情况。

.password文件内容应该是一个安全且只能由具有运行 Ansible playbooks 权限的人员访问的密码或密钥。

最后,请确保您没有加密每个可用的文件! Ansible Vault 应仅用于需要安全的重要信息。

加密用户密码

Ansible Vault 负责检查已检入的密码,并在运行 Ansible playbooks 或命令时帮助您处理它们。但是,在运行 Ansible play 时,有时您可能需要用户输入密码。您还希望确保这些密码不会出现在详尽的 Ansible 日志(默认/var/log/ansible.log位置)或stdout上。

Ansible 使用 Passlib,这是一个用于 Python 的密码哈希库,用于处理提示密码的加密。您可以使用 Passlib 支持的以下任何算法:

  • des_crypt: DES 加密

  • bsdi_crypt: BSDi 加密

  • bigcrypt: BigCrypt

  • crypt16: Crypt16

  • md5_crypt: MD5 加密

  • bcrypt: BCrypt

  • sha1_crypt: SHA-1 加密

  • sun_md5_crypt: Sun MD5 加密

  • sha256_crypt: SHA-256 加密

  • sha512_crypt: SHA-512 加密

  • apr_md5_crypt: Apache 的 MD5-crypt 变体

  • phpass: PHPass 可移植哈希

  • pbkdf2_digest: 通用 PBKDF2 哈希

  • cta_pbkdf2_sha1: Cryptacular 的 PBKDF2 哈希

  • dlitz_pbkdf2_sha1: Dwayne Litzenberger 的 PBKDF2 哈希

  • scram: SCRAM 哈希

  • bsd_nthash: FreeBSD 的 MCF 兼容 nthash 编码

现在让我们看看如何使用变量提示进行加密:

- name: ssh_password 
  prompt: Enter ssh_password 
  private: True 
  encryption: md5_crypt 
  confirm: True 
  salt_size: 7 

在上述代码片段中,vars_prompt 用于提示用户输入一些数据。

这将提示用户输入密码,类似于 SSH 的方式。

name 键指示 Ansible 将存储用户密码的实际变量名,如下所示:

name: ssh_password  

我们使用 prompt 键提示用户输入密码,如下所示:

prompt: Enter ssh password  

我们通过使用 private 键显式要求 Ansible 隐藏密码不输出到 stdout;这与 Unix 系统上的任何其他密码提示相似。private 键的访问方式如下所示:

private: True  

我们在此处使用 md5_crypt 算法,并使用 7 作为盐的大小:

encrypt: md5_crypt
salt_size: 7  

此外,Ansible 将提示两次密码并比较两个密码:

confirm: True  

隐藏密码

默认情况下,Ansible 过滤包含 login_password 键、password 键和 user:pass 格式的输出。例如,如果您正在使用 login_passwordpassword 键传递密码,则 Ansible 将使用 VALUE_HIDDEN 替换您的密码。现在让我们看看如何使用 password 键隐藏密码:

- name: Running a script
  shell: script.sh
    password: my_password  

在上述 shell 任务中,我们使用 password 键来传递密码。这将使 Ansible 能够隐藏它在 stdout 和其日志文件中。

现在,当您以详细模式运行上述任务时,您不应该看到您的 mypass 密码;相反,Ansible 将使用 VALUE_HIDDEN 替换它,如下所示:

REMOTE_MODULE command script.sh password=VALUE_HIDDEN #USE_SHELL  

使用 no_log

只有在使用特定的键时,Ansible 才会隐藏您的密码。然而,这可能并非每次都是这样;此外,您可能还想隐藏其他一些机密数据。Ansible 的 no_log 功能将隐藏整个任务,防止其记录到 syslog 文件中。它仍将在 stdout 上打印您的任务,并记录到其他 Ansible 日志文件中。

在撰写本书时,Ansible 不支持使用 no_logstdout 隐藏任务。

现在让我们看看如何使用 no_log 隐藏整个任务:

- name: Running a script
  shell: script.sh
    password: my_password
  no_log: True  

通过将 no_log: True 传递给您的任务,Ansible 将防止整个任务被记录到 syslog 中。

概要

在本章中,我们看到了大量 Ansible 的特性。我们从local_actions开始,用于在一台机器上执行操作,然后我们转向委托,在第三台机器上执行任务。然后,我们转向条件语句和 include,使 playbooks 更加灵活。我们学习了角色以及它们如何帮助您保持系统一致,还学会了如何正确组织 Ansible 仓库,充分利用 Ansible 和 Git。接着,我们讨论了执行策略和 Jinja 过滤器,以实现更加灵活的执行。

我们结束了本章对 Ansible Vault 的讲解,并提供了许多其他提示,以使您的 Ansible 执行更安全。

在下一章中,我们将看看如何使用 Ansible 来创建基础设施,更具体地说,如何在云提供商 AWS 和 DigitalOcean 上使用它。

往云端前进

在本章中,我们将看到如何使用 Ansible 在几分钟内配置基础架构。在我看来,这是 Ansible 最有趣和强大的功能之一,因为它允许你快速,一致地重建环境。当你有多个环境用于你的部署流程的不同阶段时,这非常重要。事实上,它允许你创建相等的环境,并在需要进行更改时保持其对齐,而不会感到任何痛苦。

让 Ansible 配置你的设备还有其他的优点,为此,我总是建议执行以下操作:

  • 审计追踪:最近几年来,IT 行业吞食了大量其他行业,作为这个过程的一部分,审计流程现在将 IT 视为流程的关键部分。当审计师来到 IT 部门询问服务器的历史记录,从创建到目前为止,有 Ansible 播放脚本的整个过程将非常有帮助。

  • 多个分阶段的环境:正如我们之前提到的,如果你有多个环境,使用 Ansible 配置服务器将会对你非常有帮助。

  • 迁移服务器:当一家公司使用全球云提供商(如 AWS、DigitalOcean 或 Azure)时,他们通常会选择距离他们办公室或客户最近的区域来创建第一台服务器。这些提供商经常开设新区域,如果他们的新区域更靠近你,你可能会想将整个基础架构迁移到新区域。如果你手动配置了每个资源,这将是一场噩梦!

在本章中,从宏观层面上,我们将涵盖以下主题:

  • 在 AWS 中配置机器

  • 在 DigitalOcean 中配置机器

  • 在 Azure 中配置机器

大多数新设备创建有两个阶段:

  • 配置新机器或一组新机器

  • 运行播放脚本,确保新机器被正确配置以发挥其在你的基础架构中的作用

在最初的章节中,我们已经看过了配置管理方面。在本章中,我们将更加专注于配置新机器,对配置管理的关注较少。

技术要求

你可以从本书的 GitHub 存储库中下载所有文件,网址为 github.com/PacktPublishing/Learning-Ansible-2.X-Third-Edition/tree/master/Chapter05

在云中配置资源

有了这个,让我们跳到第一个主题。管理基础架构的团队今天有很多选择来运行他们的构建、测试和部署。提供商比如亚马逊、Azure 和 DigitalOcean 主要提供基础设施即服务(IaaS)。当我们谈论 IaaS 时,最好谈论资源而不是虚拟机,有几个原因:

  • 这些公司允许您提供的多数产品都不是机器,而是其他关键资源,例如网络和存储。

  • 最近,这些公司开始提供许多不同类型的计算实例,从裸金属机器到容器。

  • 在某些非常简单的环境中,无网络(或存储)的机器设置可能就足够了,但在生产环境中可能不够。

这些公司通常提供 API、CLI、GUI 和 SDK 工具,以创建和管理云资源的整个生命周期。我们更感兴趣的是使用它们的 SDK,因为它在我们的自动化努力中将发挥重要作用。在刚开始时,建立新服务器并进行配置是有趣的,但在某个阶段,它可能变得乏味,因为它是相当重复的。每个配置步骤都会涉及几个类似的步骤,以使它们正常运行。

想象一下,某天早上您收到一封电子邮件,要求为三个新的客户设置安装,其中每个客户设置都有三到四个实例和一堆服务和依赖项。对您来说,这可能是个简单的任务,但它需要多次运行相同的重复命令,然后在服务器启动后监视它们以确认一切顺利。此外,您手动进行的任何操作都有可能引入问题。如果前两个客户设置正确启动,但由于疲劳,您遗漏了第三个客户的一个步骤,从而引入了问题怎么办?为了处理这种情况,就需要自动化。

云配置自动化使工程师能够尽快建立新的服务器,从而使他们能够集中精力处理其他优先事项。使用 Ansible,您可以轻松执行这些操作,并以最少的工作量自动化云配置。Ansible 为您提供了自动化各种不同云平台的能力,如 Amazon、Azure、DigitalOcean、Google Cloud、Rackspace 等,其中涵盖了 Ansible 核心版或扩展模块包中提供的不同服务的模块。

如前所述,启动新机器并不是结束游戏的标志。我们还需要确保我们配置它们以发挥所需的作用。

在接下来的章节中,我们将在以下环境中配置我们在之前章节中使用的环境(两个 Web 服务器和一个数据库服务器):

  • 简单的 AWS 部署:所有机器将放置在相同的可用区(AZs)和相同的网络中。

  • 复杂的 AWS 部署:在此部署中,机器将分割到多个可用区(AZs)和网络中。

  • DigitalOcean:由于 DigitalOcean 不允许我们进行许多网络调整,因此它与第一个相似。

  • Azure:在这种情况下,我们将创建一个简单的部署。

在 AWS 中配置机器

AWS 是被广泛使用的最大的公有云,通常会被选择,因为它有大量可用的服务以及大量的文档、回答问题和相关文章可以在这样一个热门产品周围找到。

由于 AWS 的目标是成为完整的虚拟数据中心提供商(以及更多),我们将需要创建和管理我们的网络,就像我们如果要建立一个真实的数据中心一样。显然,我们不需要为电缆等东西,因为这是一个虚拟数据中心。因此,几行 Ansible playbook 就足够了。

AWS 全球基础设施

亚马逊一直非常谨慎地分享其云实际上由哪些数据中心组成的位置或确切数量。在我写这篇文章时,AWS 拥有 21 个区域(还宣布了四个更多的区域),共 61 个 AZ 和数百个边缘位置。亚马逊将一个区域定义为“我们(亚马逊)拥有多个 AZ 的世界中的物理位置”。查看亚马逊关于 AZ 的文档,它说“一个 AZ 由一个或多个离散的数据中心组成,每个数据中心都有冗余的电力、网络和连接,设立在不同的设施中”。对于边缘位置,没有官方定义。

如你所见,从现实生活的角度来看,这些定义并没有帮助太多。当我尝试解释这些概念时,我通常使用我自己创造的不同定义:

  • 区域:一组物理上靠近的 AZ

  • AZ:一个区域中的数据中心(亚马逊表示它可能不止一个数据中心,但由于没有列出每个 AZ 的具体几何形状的文件,我假定最坏情况)

  • 边缘位置:互联网交换或第三方数据中心,亚马逊在这里拥有 CloudFront 和 Route 53 终端点。

尽管我试图使这些定义尽可能简单和有用,但其中一些仍然很模糊。当我们开始谈论现实世界的差异时,这些定义会立即变得清晰。例如,从网络速度的角度看,当你在同一个 AZ 内移动内容时,带宽非常高。当你在同一区域内使用两个 AZ 进行相同操作时,你会获得高带宽,而如果你在两个不同区域使用两个 AZ,带宽将更低。此外,还有价格差异,因为同一区域内的所有流量是免费的,而不同区域之间的流量是免费的。

AWS 简单存储服务

亚马逊 简单存储服务S3)是推出的第一项 AWS 服务,也是最为人所知的 AWS 服务之一。Amazon S3 是一种对象存储服务,具有公共端点和私有端点。它使用 bucket 的概念,允许您管理不同类型的文件并以简单的方式管理它们。Amazon S3 还提供了用户更高级的功能,例如使用内置 Web 服务器来提供 bucket 内容的能力。这就是许多人决定在 Amazon S3 上托管其网站或网站上的图片的原因之一。

S3 的优点主要有以下几点:

  • 价格方案:您将按照已使用的每 GB/月和已传输的每 GB 计费。

  • 可靠性:亚马逊声称 AWS S3 上的对象在任何一年内有 99.999999999% 的存活率。这比任何硬盘都要高出数量级。

  • 工具:因为 S3 是一个已经存在多年的服务,许多工具已被实现以利用这项服务。

AWS 弹性计算云

AWS 推出的第二项服务是 弹性计算云EC2)服务。该服务允许您在 AWS 基础设施上创建计算机。您可以将这些 EC2 实例视为 OpenStack 计算实例或 VMware 虚拟机。最初,这些机器与 VPS 非常相似,但过了一段时间亚马逊决定赋予这些机器更多的灵活性,并引入了非常先进的网络选项。旧类型的机器仍然在最古老的数据中心中提供,名为 EC2 Classic,而新类型是当前的默认选项,只被称为 EC2。

AWS 虚拟私有云

虚拟私有云VPC)是亚马逊在前面提到的网络实现。VPC 更多的是一组工具而不是单个工具;实际上,它所提供的功能由经典数据中心中的多个金属盒子提供。

您可以通过 VPC 创建以下主要事项:

  • 交换机

  • 路由器

  • DHCP

  • 网关

  • 防火墙

  • 虚拟专用网络VPN

当使用 VPC 时,重要的是要了解您的网络布局不是完全任意的,因为亚马逊创建了一些限制来简化其网络。基本限制如下:

  • 您不能在 AZ 之间生成子网络。

  • 您不能在不同的区域之间生成网络。

  • 您不能直接路由不同区域的网络。

而对于前两者,唯一的解决方案是创建多个网络和子网络,而对于第三者,您实际上可以使用 VPN 服务来实现一个解决方法,该 VPN 服务可以是自我提供的,也可以是使用官方的 AWS VPN 服务提供的。

我们将主要使用 VPC 的交换和路由功能。

AWS Route 53

与许多其他云服务一样,亚马逊提供了 作为服务的 DNSDNSaaS) 功能,而在亚马逊的情况下,它被称为 Route 53。 Route 53 是一个分布式 DNS 服务,在全球各地拥有数百个端点(Route 53 存在于所有 AWS 边缘位置)。

Route 53 允许您为域创建不同的区域,从而允许分割地平线情况,根据请求 DNS 解析的客户端是否在您的 VPC 内或外部,将接收不同的响应。当您希望您的应用程序轻松地在您的 VPC 内外移动而无需更改时,这非常有用,但同时,您希望您的流量尽可能地保留在一个私有(虚拟)网络中。

AWS 弹性块存储

AWS 弹性块存储EBS)是一个块存储提供者,允许您的 EC2 实例保留数据,这些数据将在重新启动后保留,并且非常灵活。从用户的角度来看,EBS 看起来很像任何其他 SAN 产品,只是具有更简单的界面,因为您只需要创建卷并告诉 EBS 需要连接到哪台机器,然后 EBS 会完成其余工作。您可以将多个卷附加到单个服务器,但每个卷一次只能连接到一个服务器。

AWS 身份和访问管理

为了允许您管理用户和访问方法,亚马逊提供了 身份和访问管理IAM) 服务。IAM 服务的主要特点如下:

  • 创建、编辑和删除用户

  • 更改用户密码

  • 创建、编辑和删除组

  • 管理用户和组关联

  • 管理令牌

  • 管理双因素身份验证

  • 管理 SSH 密钥

我们将使用此服务来设置用户及其权限。

亚马逊关系型数据库服务

设置和维护关系数据库是复杂且非常耗时的。为了简化这一过程,亚马逊提供了一些广泛使用的 作为服务的数据库DBaaS),具体如下:

  • Aurora

  • MariaDB

  • MySQL

  • Oracle

  • PostgreSQL

  • SQL Server

对于这些引擎中的每一个,亚马逊提供不同的功能和价格模型,但每个引擎的具体细节超出了本书的目标。

在 AWS 上设置账户

在开始使用 AWS 之前,我们需要的第一件事是账户。在 AWS 上创建账户非常简单,并且由亚马逊官方文档以及多个独立站点进行了很好的记录,因此在这些页面中不会涉及此操作。

创建好 AWS 账户后,需要进入 AWS 并完成以下操作:

  • 在 EC2 | Keypairs 中上传您的 SSH 密钥。

  • 在 Identity & Access Management | Users | 创建新用户 中创建新用户,并在 ~/.aws/credentials 中创建一个文件,其中包含以下行:

[default]
aws_access_key_id = YOUR_ACCESS_KEY
aws_secret_access_key = YOUR_SECRET_KEY

创建好 AWS 密钥并上传 SSH 密钥后,您需要设置 Route 53。在 Route 53 中,您需要为您的域创建两个区域(如果您没有未使用的域,也可以使用子域):一个公共区域和一个私有区域。

如果你只创建公共区域,Route 53 将在全局范围内传播该区域,但如果你创建了一个公共区域和一个私有区域,Route 53 将在创建私有区域时指定的 VPC 以外的所有地方提供你的公共区域服务。如果你在该 VPC 内查询这些 DNS 条目,将使用私有区域。这种方法有多个优点:

  • 只公开公共机器的 IP 地址。

  • 即使对于内部流量,也要始终使用 DNS 名称而不是 IP 地址。

  • 确保你的内部机器直接通信,而无需通过公共网络传递数据。

  • 由于 AWS 中的外部 IP 是由 Amazon 管理的虚拟 IP 地址,并使用 NAT 与你的实例关联,因此这种方法可以提供最少的跳数,从而减少时延。

如果你在公共区域中声明了一个条目,但在私有区域中没有声明该条目,那么 VPC 中的机器将无法解析该条目。

在创建公共区域后,AWS 将给出一些域名服务器 IP 地址,你需要将这些 IP 地址放入你的注册/根区域 DNS 中,以便你实际上可以解析这些 DNS。

简单的 AWS 部署

正如我们之前所说,我们首先需要的是网络连接。对于这个示例,我们只需要一个单独的可用区中的一个网络来容纳所有的机器。

在本节中,我们将在playbooks/aws_simple_provision.yaml文件中工作。

前两行只用于声明将执行命令的主机(localhost)和任务部分的开始:

- hosts: localhost
  tasks:  

首先,我们将确保存在公钥/私钥对:

    - name: Ensure key pair is present
      ec2_key:
        name: fale
        key_material: "{{ lookup('file', '~/.ssh/fale.pub') }}"

在 AWS 中,我们需要有一个 VPC 网络和子网。默认情况下,它们已经存在,但如果需要,可以执行以下步骤创建 VPC 网络:

    - name: Ensure VPC network is present
      ec2_vpc_net:
        name: NET_NAME
        state: present
        cidr_block: 10.0.0.0/16
        region: AWS_REGION
      register: aws_net
    - name: Ensure the VPC subnetwork is present
      ec2_vpc_subnet:
        state: present
        az: AWS_AZ
        vpc_id: '{{ aws_simple_net.vpc_id }}'
        cidr: 10.0.1.0/24
      register: aws_subnet

由于我们使用的是默认的 VPC,我们需要查询 AWS 以了解 VPC 网络和子网的值:

   - 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

现在我们已经获得了关于网络和子网的所有信息,接下来我们可以转向安全组。我们可以使用ec2_group模块来完成。在 AWS 世界中,安全组用于防火墙。安全组与共享相同目标的防火墙规则组非常相似(对于入口规则)或相同目标(对于出口规则)。与标准防火墙规则相比,实际上有三个不同之处值得一提:

  • 多个安全组可以应用于同一个 EC2 实例。

  • 作为源(对于入口规则)或目标(对于出口规则),你可以指定以下之一:

    • 一个实例 ID

    • 另一个安全组

    • 一个 IP 范围

  • 你不需要在链的末尾指定默认拒绝规则,因为 AWS 会默认添加它。

所以,对于我的情况,以下代码将被添加到playbooks/aws_simple_provision.yaml中:

    - 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

现在我们即将为我们的数据库创建另一个安全组。在这种情况下,我们只需要向 Web 安全组中的服务器打开3036端口即可:

    - name: Ensure dbsg Security Group is present
      ec2_group:
        name: dbsg
        description: DB Security Group
        region: eu-west-1
        vpc_id: '{{ aws_simple_net.vpcs.0.id }}'
        rules:
          - proto: tcp
            from_port: 3036
            to_port: 3036
            group_id: '{{ aws_simple_wssg.group_id }}'
        rules_egress:
          - proto: all
            cidr_ip: 0.0.0.0/0

如你所见,我们允许所有出站流量流动。这并不是安全最佳实践建议的做法,因此您可能需要调节出站流量。经常迫使您调节出站流量的情况是,如果您希望目标机器符合 PCI-DSS 标准。

现在我们有了 VPC、VPC 中的子网和所需的安全组,我们现在可以继续实际创建 EC2 实例了:

    - name: Setup instances
      ec2:
        assign_public_ip: '{{ item.assign_public_ip }}'
        image: ami-3548444c
        region: eu-west-1
        exact_count: 1
        key_name: fale
        count_tag:
          Name: '{{ item.name }}'
        instance_tags:
          Name: '{{ item.name }}'
        instance_type: t2.micro
        group_id: '{{ item.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
      register: aws_simple_instances
      with_items:
        - name: ws01.simple.aws.fale.io
          group_id: '{{ aws_simple_wssg.group_id }}'
          assign_public_ip: True
        - name: ws02.simple.aws.fale.io
          group_id: '{{ aws_simple_wssg.group_id }}'
          assign_public_ip: True
        - name: db01.simple.aws.fale.io
          group_id: '{{ aws_simple_dbsg.group_id }}'
          assign_public_ip: False 

当我们创建 DB 机器时,我们没有指定 assign_public_ip: True 行。在这种情况下,该机器将不会收到公共 IP,因此它将无法从 VPC 外部访问。由于我们为此服务器使用了非常严格的安全组,因此它不会从 wssg 外的任何机器访问。

正如你所猜到的那样,我们刚刚看到的代码片段将创建我们的三个实例(两个 Web 服务器和一个 DB 服务器)。

现在,我们可以将这些新创建的实例添加到我们的 Route 53 帐户中,以便解析这些机器的完全限定域名。为了与 AWS Route 53 交互,我们将使用 route53 模块,该模块允许我们创建条目、查询条目和删除条目。要创建新条目,我们将使用以下代码:

    - name: Add route53 entry for server SERVER_NAME
      route53:
        command: create
        zone: ZONE_NAME
        record: RECORD_TO_ADD
        type: RECORD_TYPE
        ttl: TIME_TO_LIVE
        value: IP_VALUES
        wait: True

因此,为我们的服务器创建条目,我们将添加以下代码:

    - name: Add route53 rules for instances
      route53:
        command: create
        zone: aws.fale.io
        record: '{{ item.tagged_instances.0.tags.Name }}'
        type: A
        ttl: 1
        value: '{{ item.tagged_instances.0.public_ip }}'
        wait: True
      with_items: '{{ aws_simple_instances.results }}'
      when: item.tagged_instances.0.public_ip
    - name: Add internal route53 rules for instances
      route53:
        command: create
        zone: aws.fale.io
        private_zone: True
        record: '{{ item.tagged_instances.0.tags.Name }}'
        type: A
        ttl: 1
        value: '{{ item.tagged_instances.0.private_ip }}'
        wait: True
      with_items: '{{ aws_simple_instances.results }}'  

由于数据库服务器没有公共地址,将此机器发布到公共区域是没有意义的,因此我们只在内部区域中创建了此机器条目。

将所有内容整合在一起,playbooks/aws_simple_provision.yaml 将如下所示。完整的代码可在 GitHub 上找到:

---
- 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
   ...

运行 ansible-playbook playbooks/aws_simple_provision.yaml,Ansible 将负责创建我们的环境。

复杂的 AWS 部署

在本节中,我们将稍微修改之前的示例,将其中一个 Web 服务器移至同一地区的另一个可用区。为此,我们将在 playbooks/aws_complex_provision.yaml 中创建一个新文件,该文件与之前的文件非常相似,唯一的区别在于帮助我们配置机器的部分。事实上,我们将使用以下代码片段代替我们上次运行时使用的代码片段。完整的代码可在 GitHub 上找到:

    - name: Setup instances
      ec2:
        assign_public_ip: '{{ item.assign_public_ip }}'
        image: ami-3548444c
        region: eu-west-1
        exact_count: 1
        key_name: fale
        count_tag:
          Name: '{{ item.name }}'
        instance_tags:
          Name: '{{ item.name }}'
        instance_type: t2.micro
        group_id: '{{ item.group_id }}'
        vpc_subnet_id: '{{ item.vpc_subnet_id }}'
        volumes:
          - device_name: /dev/sda1
            volume_type: gp2
            volume_size: 10
            delete_on_termination: True
    ...

如你所见,我们将 vpc_subnet_id 放入一个变量中,这样我们就可以为 ws02 机器使用不同的子网。由于 AWS 已经默认提供了两个子网(每个子网都绑定到不同的可用区),因此只需使用以下的可用区即可。安全组和 Route 53 代码不需要更改,因为它们不在子网/可用区级别工作,而在 VPC 级别(对于安全组和内部 Route 53 区域)或全局级别(对于公共 Route 53)工作。

在 DigitalOcean 中进行机器配置

与 AWS 相比,DigitalOcean 似乎非常不完整。直到几个月前,DigitalOcean 只提供了小水滴、SSH 密钥管理和 DNS 管理。在撰写本文时,DigitalOcean 最近推出了额外的块存储服务。与许多竞争对手相比,DigitalOcean 的优势如下:

  • 价格比 AWS 低。

  • 非常简单的 API。

  • 文档非常完善的 API。

  • 小水滴与标准虚拟机非常相似(它们不进行奇怪的自定义)。

  • 小水滴上下移动速度很快。

  • 由于 DigitalOcean 具有非常简单的网络堆栈,因此比 AWS 更高效。

小水滴

小水滴是 DigitalOcean 提供的主要服务,是非常类似于 Amazon EC2 Classic 的计算实例。DigitalOcean 依赖于Kernel Virtual MachineKVM)来虚拟化机器,确保非常高的性能和安全性。

由于他们没有以任何明智的方式更改 KVM,而且由于 KVM 是开源的,并且可在任何 Linux 机器上使用,这使得系统管理员可以在私有和公共云上创建相同的环境。DigitalOcean 小水滴将具有一个外部 IP,它们最终可以添加到一个虚拟网络中,该虚拟网络将允许您的机器使用内部 IP。

与许多其他可比较的服务不同,DigitalOcean 允许您的小水滴除了 IPv4 地址外,还具有 IPv6 地址。该服务是免费的。

SSH 密钥管理

每次想要创建一个小水滴时,都必须指定是否要为 root 用户分配特定的 SSH 密钥,或者是否要设置一个密码(第一次登录时必须更改)。要能够选择一个 SSH 密钥,您需要一个用于上传的接口。DigitalOcean 允许您使用非常简单的界面进行此操作,该界面允许您列出当前的密钥,以及创建和删除密钥。

私有网络

正如在小水滴部分中所提到的,DigitalOcean 允许我们拥有一个私有网络,我们的机器可以与另一个机器通信。这允许对服务进行隔离(例如数据库服务)仅在内部网络上以提供更高级别的安全性。由于默认情况下,MySQL 绑定到所有可用接口,因此我们将需要稍微调整数据库角色以仅绑定到内部网络。

为了识别内部网络和外部网络,由于一些 DigitalOcean 的特殊性,有许多方法:

  • 私有网络始终位于10.0.0.0/8网络中,而公共 IP 从不在该网络中。

  • 公网始终是eth0,而私网始终是eth1

根据您的可移植性需求,您可以使用这两种策略之一来了解在哪里绑定您的服务。

在 DigitalOcean 中添加 SSH 密钥

我们首先需要一个 DigitalOcean 帐户。一旦我们有了 DigitalOcean 用户、设置了信用卡和 API 密钥,我们就可以开始使用 Ansible 将我们的 SSH 密钥添加到我们的 DigitalOcean 云中。为此,我们需要创建一个名为playbooks/do_provision.yaml的文件,其结构如下:

- hosts: localhost
  tasks:
    - name: Add the SSH Key to Digital Ocean
      digital_ocean:
        state: present
        command: ssh
        name: SSH_KEY_NAME
        ssh_pub_key: 'ssh-rsa AAAA...'
        api_token: XXX
      register: ssh_key

在我这个案例中,这是我的文件内容:

    - name: Add the SSH Key to Digital Ocean
      digital_ocean:
        state: present
        command: ssh
        name: faleKey
        ssh_pub_key: "{{ lookup('file', '~/.ssh/fale.pub') }}"
        api_token: ee02b...2f11d
      register: ssh_key

然后我们可以执行它,你将会得到类似以下的结果:

PLAY [all] ***********************************************************

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

TASK [Add the SSH Key to Digital Ocean] ******************************
changed: [localhost]

PLAY RECAP ***********************************************************
localhost : ok=2 changed=1 unreachable=0 failed=0

此任务是幂等的,因此我们可以多次执行它。如果密钥已经上传,那么每次运行都会返回 SSH 密钥 ID。

在 DigitalOcean 中部署

在撰写本文时,使用 Ansible 创建 droplet 的唯一方法是使用digital_ocean模块,该模块可能很快就会被弃用,因为它的许多功能现在已经由其他模块以更好、更干净的方式完成,而且 Ansible 错误跟踪器上已经有一个 bug 来跟踪它的完全重写和可能的弃用。我猜新模块将被称为digital_ocean_droplet,并且将具有类似的语法,但目前没有代码,所以这只是我的猜测。

要创建 droplets,我们将使用类似以下的digital_ocean模块语法:

 - name: Ensure the ws and db servers are present
   digital_ocean:
     state: present
     ssh_key_ids: KEY_ID
     name: '{{ item }}'
     api_token: DIGITAL_OCEAN_KEY
     size_id: 512mb
     region_id: lon1
     image_id: centos-7-0-x64
     unique_name: True
   with_items:
     - WEBSERVER 1
     - WEBSERVER 2
     - DBSERVER 1

为了确保我们所有的 provisioning 都是完全且健康的,我始终建议为整个基础架构创建一个单独的 provision 文件。因此,在我的情况下,我将在playbooks/do_provision.yaml文件中添加以下任务:

    - name: Ensure the ws and db servers are present
      digital_ocean:
        state: present
        ssh_key_ids: '{{ ssh_key.ssh_key.id }}'
        name: '{{ item }}'
        api_token: ee02b...2f11d
        size_id: 512mb
        region_id: lon1
        image_id: centos-7-x64
        unique_name: True
      with_items:
        - ws01.do.fale.io
        - ws02.do.fale.io
        - db01.do.fale.io
      register: droplets

这之后,我们可以使用digital_ocean_domain模块添加域名:

    - name: Ensure domain resolve properly
      digital_ocean_domain:
        api_token: ee02b...2f11d
        state: present
        name: '{{ item.droplet.name }}'
        ip: '{{ item.droplet.ip_address }}'
      with_items: '{{ droplets.results }}'

因此,将所有这些放在一起,我们的playbooks/do_provision.yaml将如下所示,完整的代码块可在 GitHub 上找到:

---
- hosts: localhost
  tasks:
    - name: Ensure domain is present
      digital_ocean_domain:
        api_token: ee02b...2f11d
        state: present
        name: do.fale.io
        ip: 127.0.0.1
    - name: Add the SSH Key to Digital Ocean
      digital_ocean:
        state: present
        command: ssh
        name: faleKey
        ssh_pub_key: "{{ lookup('file', '~/.ssh/fale.pub') }}"
        api_token: ee02b...2f11d
      register: ssh_key
   ...

因此,我们现在可以用以下命令运行它:

ansible-playbook playbooks/do_provision.yaml 

我们将看到类似以下的结果。完整的代码输出文件可在 GitHub 上找到:

PLAY [localhost] *****************************************************

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

TASK [Ensure domain is present] **************************************
changed: [localhost]

TASK [Add the SSH Key to Digital Ocean] ******************************
changed: [localhost]

TASK [Ensure the ws and db servers are present] **********************
changed: [localhost] => (item=ws01.do.fale.io)
changed: [localhost] => (item=ws02.do.fale.io)
changed: [localhost] => (item=db01.do.fale.io)

...

我们已经看到了如何使用几行 Ansible 在 DigitalOcean 上提供三台机器。我们现在可以使用我们在前几章中讨论过的 playbook 来配置它们。

在 Azure 中提供机器

最近,Azure 正在成为一些公司中最大的云之一。

正如你可能想象的那样,Ansible 有 Azure 特定的模块,可以轻松创建 Azure 环境。

在创建了帐户之后,我们在 Azure 上首先需要做的事情是设置授权。

有几种方法可以做到这一点,但最简单的方法可能是创建以 INI 格式包含[default]部分的~/.azure/credentials文件,其中包含subscription_id和,可选的,client_idsecretad_userpassword

一个示例如下文件:

[default]
subscription_id: __AZURE_SUBSCRIPTION_ID__
client_id: __AZURE_CLIENT_ID__ secret: __AZURE_SECRET__

之后,我们需要一个资源组,然后我们将在其中创建所有资源。

为此,我们可以使用azure_rm_resourcegroup,语法如下:

    - name: Create resource group
      azure_rm_resourcegroup:
        name: myResourceGroup
        location: eastus

现在我们有了资源组,我们可以在其中创建虚拟网络和虚拟子网络:

     - name: Create Azure VM
            hosts: localhost
            tasks:
      - name: Create resource group
            azure_rm_resourcegroup:
            name: myResourceGroup
            location: eastus
      - name: Create virtual network
            azure_rm_virtualnetwork:
            resource_group: myResourceGroup
            name: myVnet
           address_prefixes: "10.0.0.0/16"
    - name: Add subnet
           azure_rm_subnet:
           resource_group: myResourceGroup
          name: mySubnet
          address_prefix: "10.0.1.0/24"
          virtual_network: myVnet

在我们继续创建虚拟机之前,我们仍然需要一些网络项目,更具体地说,需要一个公共 IP、一个网络安全组和一个虚拟网络卡:

    - name: Create public IP address
      azure_rm_publicipaddress:
        resource_group: myResourceGroup
        allocation_method: Static
        name: myPublicIP
      register: output_ip_address
    - name: Dump public IP for VM which will be created
      debug:
        msg: "The public IP is {{ output_ip_address.state.ip_address }}."
    - name: Create Network Security Group that allows SSH 
      azure_rm_securitygroup:
        resource_group: myResourceGroup
        name: myNetworkSecurityGroup
        rules:
          - name: SSH 
            protocol: Tcp 
            destination_port_range: 22
            access: Allow
            priority: 1001
            direction: Inbound
    - name: Create virtual network inteface card
      azure_rm_networkinterface:
        resource_group: myResourceGroup
        name: myNIC
        virtual_network: myVnet
        subnet: mySubnet
        public_ip_name: myPublicIP
        security_group: myNetworkSecurityGroup

现在我们准备创建我们的第一台 Azure 机器,使用以下代码:

    - name: Create VM
      azure_rm_virtualmachine:
        resource_group: myResourceGroup
        name: myVM
        vm_size: Standard_DS1_v2
        admin_username: azureuser
        ssh_password_enabled: false
        ssh_public_keys:
          - path: /home/azureuser/.ssh/authorized_keys
            key_data: "{{ lookup('file', '~/.ssh/fale.pub') }}"
        network_interfaces: myNIC
        image:
          offer: CentOS
          publisher: OpenLogic
          sku: '7.5'
        version: latest

运行 playbook 后,您将在 Azure 上获得一个运行 CentOS 的机器!

摘要

在本章中,我们看到了如何在 AWS 云、DigitalOcean 和 Azure 中配置我们的机器。在 AWS 云的情况下,我们看到了两个不同的示例,一个非常简单,一个稍微复杂一些。

在下一章中,我们将讨论当 Ansible 发现问题时如何通知我们。

从 Ansible 获取通知

与 bash 脚本相比,Ansible 的一个重大优势之一是其幂等性,确保一切井然有序。这是一个非常好的功能,不仅向您保证服务器配置没有变化,而且新配置也将在短时间内生效。

因为这些原因,许多人每天运行他们的 master.yaml 文件一次。当您这样做时(也许您应该!),您希望 Ansible 本身向您发送某种反馈。还有许多其他情况,您可能希望 Ansible 向您或您的团队发送消息。例如,如果您使用 Ansible 部署您的应用程序,您可能希望向开发团队频道发送 IRC 消息(或其他类型的群聊消息),以便他们都了解系统的状态。

有时,你希望 Ansible 通知 Nagios 即将破坏某些东西,这样 Nagios 就不会担心,也不会开始向系统管理员发送电子邮件和消息。在本章中,我们将探讨多种方法,帮助您设置 Ansible Playbooks,既可以与您的监控系统配合工作,又可以最终发送通知。

在本章中,我们将探讨以下主题:

  • 电子邮件通知

  • Ansible XMPP/Jabber

  • Slack 和 Rocket 聊天

  • 向 IRC 频道发送消息(社区信息和贡献)

  • Amazon 简单通知服务

  • Nagios

技术要求

许多示例将需要第三方系统(用于发送消息),您可能正在使用或没有使用。如果您无法访问其中一个系统,则相关示例将无法执行。这并不是一个大问题,因为您仍然可以阅读该部分,并且许多通知模块非常相似。您可能会发现,适用于您环境的模块与另一个模块的功能非常相似。您可以参考 Ansible 通知模块的完整列表:docs.ansible.com/ansible/latest/modules/list_of_notification_modules.html

您可以从本书的 GitHub 存储库下载所有文件:github.com/PacktPublishing/Learning-Ansible-2.X-Third-Edition/tree/master/Chapter06

使用 Ansible 发送电子邮件

经常有用户需要及时通知有关 Ansible Playbook 执行的操作。这可能是因为这个用户知道这一点很重要,或者因为有一个自动化系统必须通知以便(不)启动某个过程。

提醒人们的最简单和最常见的方法是发送电子邮件。Ansible 允许您使用mail模块从您的 playbook 发送电子邮件。您可以在任何任务之间使用此模块,并在需要时通知用户。此外,在某些情况下,您无法自动化每一件事,因为要么您缺乏权限,要么需要进行一些手动检查和确认。如果是这种情况,您可以通知负责人员 Ansible 已经完成了其工作,现在是他们执行其职责的时候了。让我们使用名为uptime_and_email.yaml的非常简单的 playbook 来使用mail模块通知您的用户:

---
- hosts: localhost 
  connection: local
  tasks: 
    - name: Read the machine uptime 
      command: uptime -p 
      register: uptime 
    - name: Send the uptime via e-mail 
      mail: 
        host: mail.fale.io 
        username: ansible@fale.io 
        password: PASSWORD 
        to: me@fale.io 
        subject: Ansible-report 
        body: 'Local system uptime is {{ uptime.stdout }}.' 

前述 playbook 首先读取当前机器的正常运行时间,发出uptime命令,然后通过电子邮件发送到me@fale.io电子邮件地址。要发送电子邮件,我们显然需要一些额外信息,如 SMTP 主机、有效的 SMTP 凭据以及电子邮件的内容。这个例子非常简单,将使我们能够保持示例简短,但显然,你可以以非常类似的方式在非常长且复杂的 playbooks 中生成电子邮件。如果我们稍微专注于mail任务,我们可以看到我们正在使用以下数据:

  • 要用于发送电子邮件的电子邮件服务器(还需登录信息,这是该服务器所必需的)

  • 接收者电子邮件地址

  • 电子邮件主题

  • 电子邮件正文

mail模块支持的其他有趣参数如下:

  • attach参数:这用于向将生成的电子邮件添加附件。例如,当您希望通过电子邮件发送日志时,这非常有用。

  • port参数:这用于指定电子邮件服务器使用的端口。

有关此模块的有趣之处在于,唯一强制性字段是subject,而不是正文,许多人可能期望正文也是必需的。RFC 2822 不强制要求主题或正文的存在,因此即使没有它们,电子邮件仍然有效,但对于人来说,管理这种格式的电子邮件将非常困难。因此,Ansible 将始终发送带有主题和正文的电子邮件,如果正文为空,则会在主题和正文中都使用subject字符串。

我们现在可以继续执行脚本以验证其功能,使用以下命令:

    ansible-playbook -i localhost, uptime_and_email.yaml 

由于uptime-p参数是特定于 Linux 的,可能无法在其他 POSIX 操作系统(如 macOS)上运行,因此此 playbook 可能在某些机器上无法正常工作。

通过运行上述 playbook,我们将得到类似以下的结果:

    PLAY [localhost] *************************************************

    TASK [setup] *****************************************************
    ok: [localhost]

    TASK [Read the machine uptime] ***********************************
    changed: [localhost]

    TASK [Send the uptime via email] ********************************
    changed: [localhost]

    PLAY RECAP *******************************************************
    localhost         : ok=3    changed=2    unreachable=0    failed=0

正如预期的那样,Ansible 已经向我发送了一封带有以下内容的电子邮件:

Local system uptime is up 38 min

该模块可以以许多不同的方式使用。我见过一个实际案例,那就是为了自动化一个非常复杂但顺序流程的一部分,涉及多个人员。每个人在流程的特定点必须开始他们的工作,而链中的下一个人在前一个人完成他们的工作之前不能开始他们的工作。保持该过程运作的关键在于每个人手动向链中的下一个人发送电子邮件,通知他们他们自己的部分已经完成,因此接收者需要开始他们在流程中的工作。在我们开始自动化该程序之前,人们通常手动进行电子邮件通知,但当我们开始自动化该程序的一部分时,没有人注意到该部分已经自动化。

对于这样的复杂、顺序流程,通过电子邮件进行跟踪并不是处理它们的最好方式,因为错误很容易犯,可能导致跟踪丢失。此外,此类复杂顺序流程往往非常缓慢,但它们在组织中被广泛使用,通常您无法进行更改。

有些情况下,流程需要以比电子邮件更实时的方式发送通知,因此 XMPP 是一个好的选择。

XMPP

电子邮件速度慢,不可靠,并且人们通常不会立即对其做出反应。在某些情况下,您希望向您的用户发送实时消息。许多组织依赖 XMPP/Jabber 作为其内部聊天系统,而美妙的事情是 Ansible 能够直接向 XMPP/Jabber 用户和会议室发送消息。

让我们修改之前的示例,在 uptime_and_xmpp_user.yaml 文件中发送可靠性信息给某个用户:

---
- hosts: localhost 
  connection: local
  tasks: 
    - name: Read the machine uptime 
      command: 'uptime -p' 
      register: uptime 
    - name: Send the uptime to user 
      jabber: 
        user: ansible@fale.io 
        password: PASSWORD 
        to: me@fale.io 
        msg: 'Local system uptime is {{ uptime.stdout }}.' 

如果您想要使用 Ansible 的 jabber 任务,需要在执行该任务的系统上安装 xmpppy 库。其中一种安装方法是使用您的软件包管理器。例如,在 Fedora 上,您只需执行 sudo dnf install -y python2-xmpp 即可进行安装。您也可以使用 pip install xmpppy 进行安装。

第一个任务与前一节完全相同,而第二个任务则有一些细微的差别。正如您所看到的,jabber 模块非常类似于 mail 模块,并且需要类似的参数。在 XMPP 的情况下,我们不需要指定服务器主机和端口,因为 XMPP 会从 DNS 自动收集该信息。在需要使用不同服务器主机或端口的情况下,我们可以分别使用 hostport 参数。

现在,我们可以使用以下命令执行该脚本以验证其功能:

    ansible-playbook -i localhost, uptime_and_xmpp_user.yaml

我们将得到类似于以下的结果:

    PLAY [localhost] *************************************************

    TASK [setup] *****************************************************
    ok: [localhost]

    TASK [Read the machine uptime] ***********************************
    changed: [localhost]

    TASK [Send the uptime to user] ***********************************
    changed: [localhost]

    PLAY RECAP *******************************************************
    localhost         : ok=3    changed=2    unreachable=0    failed=0

对于我们想要发消息到会议室而不是单个用户的情况,只需要将接收者在 to 参数中更改为与会议室相应的接收者即可。

to: sysop@conference.fale.io (mailto:sysop@conference.fale.io)/ansiblebot

除了接收者更改和添加 (mailto:sysop@conference.fale.io)/ansiblebot(标识要使用的聊天句柄 ansiblebot,在本例中)之外,XMPP 对用户和会议室的处理方式是相同的,因此从一种切换到另一种非常容易。

尽管 XMPP 相当流行,但并非每家公司都使用它。另一个 Ansible 可以发送消息的协作平台是 Slack。

Slack

在过去几年中,出现了许多新的聊天和协作平台。其中最常用的之一是 Slack。Slack 是一个基于云的团队协作工具,这使得与 XMPP 相比,与 Ansible 的集成更加容易。

让我们将以下行放入 uptime_and_slack.yaml 文件中:

---
- hosts: localhost 
  connection: local
  tasks: 
    - name: Read the machine uptime 
      command: 'uptime -p' 
      register: uptime 
    - name: Send the uptime to slack channel 
      slack: 
        token: TOKEN 
        channel: '#ansible' 
        msg: 'Local system uptime is {{ uptime.stdout }}.' 

正如我们讨论的那样,此模块的语法甚至比 XMPP 更简单。事实上,它只需要知道令牌(您可以在 Slack 网站上生成),要发送消息的频道以及消息本身。

自 Ansible 的 1.8 版本以来,需要新版本的 Slack 令牌,例如 G522SJP14/D563DW213/7Qws484asdWD4w12Md3avf4FeD

使用以下命令运行 Playbook:

    ansible-playbook -i localhost, uptime_and_slack.yaml  

这导致以下输出:

    PLAY [localhost] *************************************************

    TASK [setup] *****************************************************
    ok: [localhost]

    TASK [Read the machine uptime] ***********************************
    changed: [localhost]

    TASK [Send the uptime to slack channel] **************************
    changed: [localhost]

    PLAY RECAP *******************************************************
    localhost         : ok=3    changed=2    unreachable=0    failed=0

由于 Slack 的目标是使通信更有效,它允许我们调整消息的多个方面。从我的角度来看,最有趣的几点是:

  • color:这允许您指定一个颜色条,放在消息开头以标识以下状态:

    • Good: green bar

    • Normal: no bar

    • 警告:黄色条

    • Danger: red bar

  • icon_url:这允许您为该消息更改用户图像。

例如,以下代码将以警告颜色和自定义用户图像发送消息:

    - name: Send the uptime to slack channel 
      slack: 
        token: TOKEN 
        channel: '#ansible' 
        msg: 'Local system uptime is {{ uptime.stdout }}.' 
        color: warning
        icon_url: https://example.com/avatar.png

由于并非每家公司都愿意让 Slack 看到他们的私人对话,因此存在替代方案,例如 Rocket Chat。

Rocket Chat

许多公司喜欢 Slack 的功能,但不想在使用 Slack 时失去本地服务所提供的隐私。Rocket Chat 是一个开源软件解决方案,实现了 Slack 的大多数功能以及其大部分界面。作为开源软件,每家公司都可以将其安装在本地,并以符合其 IT 规则的方式进行管理。

由于 Rocket Chat 的目标是成为 Slack 的即插即用替代方案,从我们的角度来看,几乎不需要做任何更改。事实上,我们可以创建 uptime_and_rocket.yaml 文件,其中包含以下内容:

---
- hosts: localhost 
  connection: local
  tasks: 
    - name: Read the machine uptime 
      command: 'uptime -p' 
      register: uptime 
    - name: Send the uptime to rocketchat channel 
      rocketchat: 
        token: TOKEN 
        domain: chat.example.com 
        channel: '#ansible' 
        msg: 'Local system uptime is {{ uptime.stdout }}.' 

如您所见,仅有第六和第七行发生了变化,其中 slack 一词已被替换为 rocketchat。此外,我们需要添加一个 domain 字段,指定我们的 Rocket Chat 安装位于何处。

使用以下命令运行代码:

    ansible-playbook -i localhost, uptime_and_rocketchat.yaml  

这导致以下输出:

    PLAY [localhost] *************************************************

    TASK [setup] *****************************************************
    ok: [localhost]

    TASK [Read the machine uptime] ***********************************
    changed: [localhost]

    TASK [Send the uptime to rocketchat channel] *********************
    changed: [localhost]

    PLAY RECAP *******************************************************
    localhost         : ok=3    changed=2    unreachable=0    failed=0

另一种自托管公司对话的方式是使用 IRC,这是一个非常古老但仍然常用的协议。Ansible 也能够使用它发送消息。

Internet Relay Chat

互联网中继聊天IRC)可能是 1990 年代最著名和广泛使用的聊天协议,至今仍在使用。它的受欢迎程度和持续使用主要是由于它在开源社区中的使用和其简单性。从 Ansible 的角度来看,IRC 是一个非常直接的模块,我们可以像以下示例一样使用它(放在 uptime_and_irc.yaml 文件中):

---
- hosts: localhost 
  connection: local
  tasks: 
    - name: Read the machine uptime 
      command: 'uptime -p' 
      register: uptime 
    - name: Send the uptime to IRC channel 
      irc: 
        port: 6669 
        server: irc.example.net 
        channel: '#desired_channel'
        msg: 'Local system uptime is {{ uptime.stdout }}.' 
        color: green 

您需要安装 socket Python 库才能使用 Ansible IRC 模块。

在 IRC 模块中,需要以下字段:

  • channel:指定消息将要传送到的频道。

  • msg:这是您要发送的消息。

通常您需要指定的其他配置包括:

  • server:选择要连接的服务器,如果不是localhost

  • port:选择连接的端口,如果不是6667

  • color:指定消息颜色,如果不是黑色

  • nick:指定发送消息的昵称,如果不是ansible

  • use_ssl:使用 SSL 和 TLS 安全性。

  • style:如果要以粗体、斜体、下划线或反向样式发送消息,则使用此选项。

使用以下命令运行代码:

    ansible-playbook uptime_and_irc.yaml  

这将产生以下输出:

    PLAY [localhost] *************************************************

    TASK [setup] *****************************************************
    ok: [localhost]

    TASK [Read the machine uptime] ***********************************
    changed: [localhost]

    TASK [Send the uptime to IRC channel] ****************************
    changed: [localhost]

    PLAY RECAP *******************************************************
    localhost         : ok=3    changed=2    unreachable=0    failed=0

我们已经看到许多不同的通信系统可能在您的公司或项目中使用,但这些通常用于人与人或机器与人的通信。机器对机器的通信通常使用不同的系统,例如亚马逊 SNS。

亚马逊简单通知服务

有时,您希望您的 Playbook 在接收警报的方式上是不可知的。这具有多个优点,主要是灵活性。事实上,在这种模型中,Ansible 将消息交付给一个通知服务,然后通知服务将负责交付消息。亚马逊简单通知服务SNS)并不是唯一可用的通知服务,但它可能是最常用的。SNS 有以下组件:

  • 消息:由 UUID 识别的发布者生成的消息

  • 发布者:生成消息的程序

  • 主题:命名的消息组,可以类比于聊天频道或房间

  • 订阅者:订阅了他们感兴趣主题的所有消息的客户端

因此,在我们的情况下,具体如下:

  • 消息:Ansible 通知

  • 发布者:Ansible 本身

  • 主题:根据系统和/或通知类型(例如存储、网络或计算)对消息进行分组的可能不同主题

  • 订阅者:您团队中必须得到通知的人员

正如我们所说,SNS 的一个重大优势是,您可以将 Ansible 发送消息的方式(即 SNS API)与用户接收消息的方式分离开来。事实上,您将能够为每个用户和每个主题规则选择不同的传送系统,并最终可以动态更改它们,以确保消息以最佳方式发送到任何情况中。目前,SNS 可以发送消息的五种方式如下:

  • Amazon Lambda 函数(用 Python、Java 和 JavaScript 编写的无服务器函数)

  • Amazon Simple Queue ServiceSQS)(一种消息队列系统)

  • 电子邮件

  • HTTP(S) 调用

  • 短信

让我们看看如何使用 Ansible 发送 SNS 消息。为此,我们可以创建一个名为 uptime_and_sns.yaml 的文件,其中包含以下内容:

---
- hosts: localhost 
  connection: local
  tasks: 
    - name: Read the machine uptime 
      command: 'uptime -p' 
      register: uptime 
    - name: Send the uptime to SNS 
      sns: 
        msg: 'Local system uptime is {{ uptime.stdout }}.' 
        subject: "System uptime" 
        topic: "uptime"

在此示例中,我们使用 msg 键来设置将要发送的消息,topic 选择最合适的主题,并且 subject 将用作电子邮件投递的主题。您可以设置许多其他选项。主要用于使用不同的传送方式发送不同的消息。

例如,通过短信发送短消息(最后,SMS 中的第一个 S 意味着 ),通过电子邮件发送更长且更详细的消息是有意义的。为此,SNS 模块为我们提供了以下针对传送的特定选项

  • email

  • http

  • https

  • sms

  • sqs

正如我们在前一章中所看到的,AWS 模块需要凭据,我们可以以多种方式设置它们。运行此模块所需的三个 AWS 特定参数是:

  • aws_access_key:这是 AWS 访问密钥;如果未指定,则将考虑环境变量 aws_access_key~/.aws/credentials 的内容。

  • aws_secret_key:这是 AWS 秘密密钥;如果未指定,则将考虑环境变量 aws_secret_key~/.aws/credentials 的内容。

  • region:这是要使用的 AWS 区域;如果未指定,则将考虑环境变量 ec2_region~/.aws/config 的内容。

使用以下命令运行代码:

    ansible-playbook uptime_and_sns.yaml  

这将导致以下输出:

PLAY [localhost] ************************************************* 

TASK [setup] ***************************************************** 
ok: [localhost] 

TASK [Read the machine uptime] *********************************** 
changed: [localhost] 

TASK [Send the uptime to SNS] ************************************ 
changed: [localhost] 

PLAY RECAP ******************************************************* 
localhost         : ok=3    changed=2    unreachable=0    failed=0 

有时我们希望通知监控系统,以便它不会由于 Ansible 操作而触发任何警报。这种系统的常见示例是 Nagios。

Nagios

Nagios是用于监控服务和服务器状态的最常用工具之一。Nagios 能够定期审计服务器和服务的状态,并在出现问题时通知用户。如果您的环境中有 Nagios,那么在管理机器时必须非常小心,因为在 Nagios 发现服务器或服务处于不健康状态时,它会开始发送电子邮件和短信并向您的团队打电话。当您对由 Nagios 控制的节点运行 Ansible 脚本时,您必须更加小心,因为您会面临在夜间或其他不适当时间触发电子邮件、短信消息和电话的风险。为了避免这种情况,Ansible 能够事先通知 Nagios,以便在那个时间窗口内 Nagios 不会发送通知,即使一些服务处于下线状态(例如,因为它们正在重新启动)或其他检查失败。

在这个例子中,我们将会停止一个服务,等待五分钟,然后再次启动它,因为这实际上会导致在大多数配置中出现 Nagios 故障。事实上,通常情况下,Nagios 被配置为接受最多两次连续的测试失败(通常每分钟执行一次测试),在提升为关键状态之前将服务置于警告状态。我们将创建名为long_restart_service.yaml的文件,这将触发 Nagios 关键状态:

---
- hosts: ws01.fale.io 
  tasks: 
    - name: Stop the HTTPd service 
      service: 
        name: httpd 
        state: stopped 
    - name: Wait for 5 minutes 
      pause: 
        minutes: 5 
    - name: Start the HTTPd service 
      service: 
        name: httpd 
        state: stopped 

运行以下命令来执行代码:

ansible-playbook long_restart_service.yaml

这应该会触发 Nagios 警报,并导致以下输出:

PLAY [ws01.fale.io] ********************************************** 

TASK [setup] ***************************************************** 
ok: [ws01.fale.io] 

TASK [Stop the HTTpd service] ************************************ 
changed: [ws01.fale.io] 

TASK [Wait for 5 minutes] **************************************** 
changed: [ws01.fale.io] 

TASK [Start the HTTpd service] *********************************** 
changed: [ws01.fale.io] 

PLAY RECAP ******************************************************* 
ws01.fale.io      : ok=4    changed=3    unreachable=0    failed=0 

如果没有触发 Nagios 警报,那么可能是因为您的 Nagios 安装没有跟踪该服务,或者五分钟不足以使其提升为关键状态。要检查,请联系管理您 Nagios 安装的人员或团队,因为 Nagios 允许完全配置到一个非常难以预测 Nagios 行为的地步,而不知道其配置是很难的。

现在我们可以创建一个非常相似的 playbook,以确保 Nagios 不会发送任何警报。我们将创建一个名为long_restart_service_no_alert.yaml的文件,其内容如下(完整代码可在 GitHub 上找到):

---
- hosts: ws01.fale.io 
  tasks: 
    - name: Mute Nagios 
      nagios: 
        action: disable_alerts 
        service: httpd 
        host: '{{ inventory_hostname }}' 
      delegate_to: nagios.fale.io 
    - name: Stop the HTTPd service 
      service: 
        name: httpd 
        state: stopped 
   ...

正如你所见,我们添加了两个任务。第一个任务是告诉 Nagios 不要发送有关给定主机上 HTTPd 服务的警报,第二个任务是告诉 Nagios 再次开始发送有关该服务的警报。即使您没有指定服务,因此静音了该主机上的所有警报,我的建议是仅禁用您将要中断的警报,以便 Nagios 仍然能够在大多数基础架构上正常工作。

如果 playbook 运行在恢复警报之前失败,你的警报将保持禁用状态。

该模块的目标是切换 Nagios 警报以及安排停机时间,从 Ansible 2.2 开始,该模块还可以取消安排的停机时间。

使用以下命令来运行代码:

    ansible-playbook long_restart_service_no_alert.yaml  

这将触发 Nagios 警报,并导致以下输出(完整的代码输出可在 GitHub 上获得):

    PLAY [ws01.fale.io] **********************************************

    TASK [setup] *****************************************************
    ok: [ws01.fale.io]

    TASK [Mute Nagios] ***********************************************
    changed: [nagios.fale.io]

    TASK [Stop the HTTpd service] ************************************
    changed: [ws01.fale.io]

  ...

要使用 Nagios 模块,您需要使用 delegate_to 参数将操作委托给 Nagios 服务器,如示例所示。

有时,与 Nagios 集成要实现的目标完全相反。事实上,你并不想把它静音,而是想让 Nagios 处理你的测试结果。一个常见的情况是,如果您想利用您的 Nagios 配置通知您的管理员一个任务的输出。为此,我们可以使用 Nagios 的 nsca 工具,将其集成到我们的 playbook 中。Ansible 还没有一个管理它的特定模块,但您可以使用命令模块来运行它,利用 send_nsca CLI 程序。

总结

在本章中,我们已经学习了如何让 Ansible 发送通知到其他系统和人员。您学会了通过电子邮件和消息服务(如 Slack)发送通知的方法。最后,你学会了如何在你运行 Nagios 时防止它发送关于系统健康状况的不必要通知。

在下一章中,我们将学习如何创建一个模块,以便您可以扩展 Ansible 来执行任何类型的任务。

第三部分:使用 Ansible 部署应用

本节将解释如何调试和测试 Ansible 以确保您的 Playbook 总是正常工作。您还将学习如何使用 Ansible 管理多个层、多个环境和多个部署。

本节包括以下章节:

  • 第七章,创建自定义模块

  • 第八章,调试和错误处理

  • 第九章,复杂环境

创建自定义模块

本章将重点介绍如何编写和测试自定义模块。我们已经讨论了模块的工作原理以及如何在任务中使用它们。为了快速回顾,Ansible 中的一个模块是每次运行 Ansible 任务时传输和执行到你的远程主机上的代码片段(如果你使用了local_action,它也可以在本地运行)。

从我的经验来看,每当需要将某个特定功能暴露为一流任务时,就会编写自定义模块。虽然可以通过现有模块执行相同的功能,但为了实现最终目标,可能需要一系列任务(有时还包括命令和 shell 模块)。例如,假设你想通过预启动执行环境PXE)配置服务器。没有自定义模块,你可能会使用一些 shell 或命令任务来完成这个任务。然而,通过自定义模块,你只需将所需参数传递给它,业务逻辑将嵌入到自定义模块中,以执行 PXE 启动。这使你能够编写更简单易读的 playbook,并提供了更好的可重用性,因为你只需创建一次模块,就可以在角色和 playbook 中的任何地方使用它。

你传递给模块的参数(只要它们以键值格式提供)将与模块一起在一个单独的文件中转发。Ansible 期望你的模块输出至少有两个变量(即,模块运行的结果),无论它是通过还是失败的,以及用户的消息 - 它们都必须以 JSON 格式提供。如果你遵循这个简单的规则,你可以根据自己的需要定制你的模块!

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

  • Python 模块

  • Bash 模块

  • Ruby 模块

  • 测试模块

先决条件

当你选择特定的技术或工具时,通常从它提供的内容开始。你慢慢地了解哲学,然后开始构建工具以及它帮助你解决的问题。然而,只有当你深入了解它的工作原理时,你才真正感到舒适和掌控。在某个阶段,为了充分利用工具的全部功能,你将不得不以适合你特定需求的方式定制它。随着时间的推移,那些提供了方便的方式来插入新功能的工具会留下来,而那些没有的则会从市场上消失。Ansible 也是类似的情况。Ansible playbook 中的所有任务都是某种类型的模块,并且它加载了数百个模块。你几乎可以找到任何你可能需要的模块。然而,总会有例外,这就是通过添加自定义模块来扩展 Ansible 功能的力量所在。

Chef 提供了轻量级资源和提供者LWRPs)来执行此操作,而 Ansible 允许您使用自定义模块扩展其功能。使用 Ansible,您可以使用您选择的任何语言编写模块(前提是您有该语言的解释器),而在 Chef 中,模块必须是 Ruby 编写的。Ansible 开发人员建议对于任何复杂模块都使用 Python,因为有内置支持来解析参数;几乎所有的*nix系统默认都安装了 Python,并且 Ansible 本身也是用 Python 编写的。在本章中,我们还将学习如何使用其他语言编写模块。

要使自定义模块可用于 Ansible,您可以执行以下操作之一:

  • ANSIBLE_LIBRARY环境变量中指定自定义模块的路径。

  • 使用--module-path命令行选项。

  • 将模块放在您的 Ansible 顶层目录中的library目录中,并在ansible.cfg文件的[default]部分中添加library=library

您可以从本书的 GitHub 存储库中下载所有文件,网址为github.com/PacktPublishing/Learning-Ansible-2.X-Third-Edition/tree/master/Chapter07

现在我们已经了解了这些背景信息,让我们来看一些代码吧!

使用 Python 编写模块

Ansible 允许用户使用任何语言编写模块。但是,使用 Python 编写模块具有自己的优势。您可以利用 Ansible 的库来缩短您的代码 - 这是其他语言编写的模块所不具备的优势。借助 Ansible 库的帮助,解析用户参数、处理错误并返回所需值变得更加容易。此外,由于 Ansible 是用 Python 编写的,因此您将在整个 Ansible 代码库中使用相同的语言,使审查更加容易,可维护性更高。我们将看到两个自定义 Python 模块的示例,一个使用 Ansible 库,一个不使用,以便让您了解自定义模块的工作原理。在创建模块之前,请确保按照前一节中提到的方式组织您的目录结构。第一个示例创建了一个名为check_user的模块。为此,我们需要在 Ansible 顶层目录中的library文件夹中创建check_user.py文件。完整的代码可以在 GitHub 上找到:

def main(): 
    # Parsing argument file 
    args = {} 
    args_file = sys.argv[1] 
    args_data = file(args_file).read() 
    arguments = shlex.split(args_data) 
    for arg in arguments: 
        if '=' in arg: 
            (key, value) = arg.split('=') 
            args[key] = value 
    user = args['user'] 

    # Check if user exists 
    try: 
        pwd.getpwnam(user) 
        success = True 
        ret_msg = 'User %s exists' % user 
    except KeyError: 
        success = False 
        ret_msg = 'User %s does not exists' % user 
...

先前的自定义模块check_user,将检查用户是否存在于主机上。该模块期望来自 Ansible 的用户参数。让我们分解前面的模块,看看它的作用。我们首先声明 Python 解释器,并导入解析参数所需的库:

#!/usr/bin/env python 

import pwd 
import sys 
import shlex 
import json 

使用sys库,然后解析由 Ansible 在文件中传递的参数。参数的格式为param1=value1 param2=value2,其中param1param2是参数,value1value2是参数的值。有多种方式可以拆分参数并创建字典,我们选择了一种简单的方式来执行操作。我们首先通过空格字符拆分参数创建参数列表,然后通过=字符拆分参数将键和值分开,并将其赋给 Python 字典。例如,如果您有一个字符串,如user=foo gid=1000,那么您将首先创建一个列表,["user=foo", "gid=1000"],然后循环遍历该列表以创建字典。此字典将是{"user": "foo", "gid": 1000};此操作使用以下行执行:

def main(): 
    # Parsing argument file 
    args = {} 
    args_file = sys.argv[1] 
    args_data = file(args_file).read() 
    arguments = shlex.split(args_data) 
    for arg in arguments: 
        if '=' in arg: 
            (key, value) = arg.split('=') 
            args[key] = value 
    user = args['user'] 

我们根据空格字符分隔参数,因为这是核心 Ansible 模块遵循的标准。您可以使用任何分隔符来代替空格,但我们建议您保持统一性。

一旦我们有了用户参数,我们就会检查该用户是否存在于主机上,具体如下:

    # Check if user exists 
    try: 
        pwd.getpwnam(user) 
        success = True 
        ret_msg = 'User %s exists' % user 
    except KeyError: 
        success = False 
        ret_msg = 'User %s does not exists' % user 

我们使用pwd库来检查用户的passwd文件。为简单起见,我们使用两个变量:一个用于存储成功或失败的消息,另一个用于存储用户的消息。最后,我们使用try-catch块中创建的变量来检查模块是否成功或失败:

    # Error handling and JSON return 
    if success: 
        print json.dumps({ 
            'msg': ret_msg 
        }) 
        sys.exit(0) 
    else: 
        print json.dumps({ 
            'failed': True, 
            'msg': ret_msg 
        }) 
        sys.exit(1) 

如果模块成功,则会以0的退出代码(exit(0))退出执行;否则,它将以非零代码退出。Ansible 将查找失败的变量,如果它设置为True,则退出,除非您已明确要求 Ansible 使用ignore_errors参数忽略错误。您可以像使用 Ansible 的任何其他核心模块一样使用自定义模块。为了测试自定义模块,我们将需要一个 playbook,所以让我们使用以下代码创建playbooks/check_user.yaml文件:

---
- hosts: localhost
  connection: local
  vars:
    user_ok: root
    user_ko: this_user_does_not_exists
  tasks:
    - name: 'Check if user {{ user_ok }} exists'
      check_user:
        user: '{{ user_ok }}'
    - name: 'Check if user {{ user_ko }} exists'
      check_user:
        user: '{{ user_ko }}'

如您所见,我们像使用任何其他核心模块一样使用了check_user模块。Ansible 将通过将模块复制到远程主机并在单独的文件中使用参数执行该模块。让我们看看这个 playbook 如何运行,使用以下代码:

ansible-playbook playbooks/check_user.yaml

我们应该收到以下输出:

PLAY [localhost] ***************************************************

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

TASK [Check if user root exists] ***********************************
ok: [localhost]

TASK [Check if user this_user_does_not_exists exists] **************
fatal: [localhost]: FAILED! => {"changed": false, "msg": "User this_user_does_not_exists does not exists"}
 to retry, use: --limit @playbooks/check_user.retry

PLAY RECAP *********************************************************
localhost                   : ok=2 changed=0 unreachable=0 failed=1

如预期,由于我们有root用户,但没有this_user_does_not_exists,它通过了第一个检查,但在第二个检查失败了。

Ansible 还提供了一个 Python 库来解析用户参数并处理错误和返回值。现在是时候探索 Ansible Python 库如何使您的代码更短、更快且更不容易出错了。为此,让我们创建一个名为library/check_user_py2.py的文件,其中包含以下代码。完整的代码可在 GitHub 上找到:

#!/usr/bin/env python 

import pwd 
from ansible.module_utils.basic import AnsibleModule 

def main(): 
    # Parsing argument file 
    module = AnsibleModule( 
        argument_spec = dict( 
            user = dict(required=True) 
        ) 
    ) 
    user = module.params.get('user') 

    # Check if user exists 
    try: 
        pwd.getpwnam(user) 
        success = True 
        ret_msg = 'User %s exists' % user 
    except KeyError: 
        success = False 
        ret_msg = 'User %s does not exists' % user 

...

让我们分解前面的模块,看看它是如何工作的,具体如下:

#!/usr/bin/env python 

import pwd 
from ansible.module_utils.basic import AnsibleModule 

如您所见,我们不再导入sysshlexjson; 我们不再使用它们,因为所有需要它们的操作现在都由 Ansible 的module_utils模块完成:

    # Parsing argument file 
    module = AnsibleModule( 
        argument_spec = dict( 
            user = dict(required=True) 
        ) 
    ) 
    user = module.params.get('user') 

以前,我们对参数文件进行了大量处理,以获取最终的用户参数。 Ansible 通过提供一个AnsibleModule类来简化这个过程,该类自行处理所有处理并为我们提供最终参数。 required=True参数意味着该参数是必需的,如果未传递该参数,则执行将失败。 默认值requiredFalse,这将允许用户跳过该参数。 然后,您可以通过在module.params字典上调用module.params上的get方法来访问参数的值。 远程主机上检查用户的逻辑将保持不变,但错误处理和返回方面将如下更改:

    # Error handling and JSON return 
    if success: 
        module.exit_json(msg=ret_msg) 
    else: 
        module.fail_json(msg=ret_msg) 

使用AnsibleModule对象的一个优点是你有一个非常好的设施来处理返回值到 playbook。 我们将在下一节中更深入地讨论。

我们本可以将检查用户和返回方面的逻辑压缩在一起,但我们将它们分开以提高可读性。

要验证一切是否按预期工作,我们可以在playbooks/check_user_py2.yaml中创建一个新的 playbook,并使用以下代码:

---
- hosts: localhost
  connection: local
  vars:
    user_ok: root
    user_ko: this_user_does_not_exists
  tasks:
    - name: 'Check if user {{ user_ok }} exists'
      check_user_py2:
        user: '{{ user_ok }}'
    - name: 'Check if user {{ user_ko }} exists'
      check_user_py2:
        user: '{{ user_ko }}'

你可以使用以下代码来运行它:

ansible-playbook playbooks/check_user_py2.yaml

然后,我们应该收到以下输出:

PLAY [localhost] ***************************************************

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

TASK [Check if user root exists] ***********************************
ok: [localhost]

TASK [Check if user this_user_does_not_exists exists] **************
fatal: [localhost]: FAILED! => {"changed": false, "msg": "User this_user_does_not_exists does not exists"}
 to retry, use: --limit @playbooks/check_user_py2.retry

PLAY RECAP *********************************************************
localhost                   : ok=2 changed=0 unreachable=0 failed=1

这个输出与我们的预期一致。

使用 exit_json 和 fail_json

Ansible 通过exit_jsonfail_json方法提供了更快和更短的处理成功和失败的方法,分别。 您可以直接将消息传递给这些方法,Ansible 将处理剩余的部分。 您还可以将其他变量传递给这些方法,并且 Ansible 将这些变量打印到stdout。 例如,除了消息之外,您可能还想打印用户的uidgid参数。 您可以通过将这些变量分隔符传递给exit_json方法来实现这一点。

让我们看看如何将多个值返回到stdout,如下面的代码所示,放置在library/check_user_id.py中。 完整的代码在 GitHub 上可用:

#!/usr/bin/env python 

import pwd 
from ansible.module_utils.basic import AnsibleModule 

class CheckUser: 
    def __init__(self, user): 
        self.user = user 

    # Check if user exists 
    def check_user(self): 
        uid = '' 
        gid = '' 
        try: 
            user = pwd.getpwnam(self.user) 
            success = True 
            ret_msg = 'User %s exists' % self.user 
            uid = user.pw_uid 
            gid = user.pw_gid 
        except KeyError: 
            success = False 
            ret_msg = 'User %s does not exists' % self.user 
        return success, ret_msg, uid, gid 

...

正如你所见,我们返回了用户的uidgid参数,以及消息msg。 你可以有多个值,Ansible 将以字典格式打印所有这些值。 创建一个包含以下内容的 playbook:playbooks/check_user_id.yaml

---
- hosts: localhost
  connection: local
  vars:
    user: root
  tasks:
    - name: 'Retrive {{ user }} data if it exists'
      check_user_id:
        user: '{{ user }}'
      register: user_data
    - name: 'Print user {{ user }} data'
      debug:
        msg: '{{ user_data }}'

你可以使用以下代码来运行它:

ansible-playbook playbooks/check_user_id.yaml

我们应该收到以下输出:

PLAY [localhost] ***************************************************

TASK [Gathering Facts] *********************************************
ok: [localhost] 
TASK [Retrive root data if it exists] ******************************
ok: [localhost]

TASK [Print user root data] ****************************************
ok: [localhost] => {
 "msg": {
 "changed": false, 
 "failed": false, 
 "gid": 0, 
 "msg": "User root exists", 
 "uid": 0
 }
}

PLAY RECAP *********************************************************
localhost : ok=3 changed=0 unreachable=0 failed=0

在这里,我们完成了两种方法的工作,这反过来又帮助我们找到了在 Ansible 中处理成功和失败的更快方式,同时向用户传递参数。

测试 Python 模块

正如你所见,你可以通过创建非常简单的 playbooks 来测试你的模块。 为此,我们需要克隆 Ansible 官方仓库(如果你还没有这样做):

git clone git://github.com/ansible/ansible.git --recursive

接下来,像下面这样来源一个环境文件:

source ansible/hacking/env-setup

现在我们可以使用test-module工具通过将文件名作为命令行参数来运行脚本:

ansible/hacking/test-module -m library/check_user_id.py -a "user=root"

结果将类似于以下输出:

* including generated source, if any, saving to: /home/fale/.ansible_module_generated 
* ansiballz module detected; extracted module source to: /home/fale/debug_dir 
*********************************** 
RAW OUTPUT 

{"msg": "User root exists", "invocation": {"module_args": {"user": "root"}}, "gid": 0, "uid": 0, "changed": false} 

*********************************** 
PARSED OUTPUT 
{ 
    "changed": false, 
    "gid": 0, 
    "invocation": { 
        "module_args": { 
            "user": "root" 
        } 
    }, 
    "msg": "User root exists", 
    "uid": 0 
}

如果你没有使用AnsibleModule,直接执行脚本也很容易。这是因为该模块需要很多 Ansible 特定的变量,所以"模拟" Ansible 运行比实际运行 Ansible 本身更复杂。

使用 bash 模块

Ansible 中的 Bash 模块与任何其他 bash 脚本没有任何区别,唯一不同之处在于它们将数据打印在stdout上。Bash 模块可以是非常简单的,比如检查远程主机上是否有进程正在运行,也可以是运行一些更复杂的命令。

如前所述,一般推荐使用 Python 编写模块。在我看来,第二选择(仅适用于非常简单的模块)是bash 模块,因为它简单易用,用户基础广泛。

让我们创建一个名为library/kill_java.sh的文件,并包含以下内容:

#!/bin/bash 
source $1 

SERVICE=$service_name 

JAVA_PIDS=$(/usr/java/default/bin/jps | grep ${SERVICE} | awk '{print $1}') 

if [ ${JAVA_PIDS} ]; then 
    for JAVA_PID in ${JAVA_PIDS}; do 
        /usr/bin/kill -9 ${JAVA_PID} 
    done 
    echo "failed=False msg=\"Killed all the orphaned processes for ${SERVICE}\"" 
    exit 0 
else 
    echo "failed=False msg=\"No orphaned processes to kill for ${SERVICE}\"" 
    exit 0 
fi

前述的bash模块将使用service_name参数并强制终止所有属于该服务的 Java 进程。正如你所知,Ansible 将参数文件传递给模块。然后我们使用$1 source 来来源参数文件。这将实际上设置一个名为service_name的环境变量。然后我们使用$service_name来访问这个变量,如下所示:

source $1 

SERVICE=$service_name 

然后我们检查我们是否得到了该服务的 PIDS,并遍历这些 PIDS 来强制终止所有与 service_name 匹配的 Java 进程。一旦它们被终止,我们通过failed=False退出模块,并且附上一个退出码为0的消息,如下所示:

if [ ${JAVA_PIDS} ]; then 
    for JAVA_PID in ${JAVA_PIDS}; do 
        /usr/bin/kill -9 ${JAVA_PID} 
    done 
    echo "failed=False msg=\"Killed all the orphaned processes for ${SERVICE}\"" 
    exit 0 

如果我们没有找到该服务的正在运行的进程,我们仍将以退出码0退出模块,因为终止 Ansible 运行可能没有意义:

else 
    echo "failed=False msg=\"No orphaned processes to kill for ${SERVICE}\"" 
    exit 0 
fi 

你也可以通过打印failed=True并将退出码设置为1来终止 Ansible 运行。

如果语言本身不支持 JSON,则 Ansible 允许返回键值输出。这使得 Ansible 更加友好于开发人员和系统管理员,并允许以您选择的任何语言编写自定义模块。让我们通过将参数文件传递给模块来测试bash模块。现在我们可以在/tmp/arguments中创建一个参数文件,将service_name参数设置为jenkins,如下所示:

service_name=jenkins

现在,你可以像运行任何其他 bash 脚本一样运行模块。让我们看看当我们用以下代码运行时会发生什么:

bash library/kill_java.sh /tmp/arguments

我们应该收到以下输出:

failed=False msg="No orphaned processes to kill for jenkins"

如预期,即使本地主机上没有运行 Jenkins 进程,模块也没有失败。

如果你收到 jps command does not exists 错误而不是上述输出,那么你的机器可能缺少 Java。如果是这样,你可以按照操作系统的说明安装它:www.java.com/en/download/help/download_options.xml

使用 Ruby 模块

在 Ruby 中编写模块与在 Python 或 bash 中编写模块一样简单。您只需要注意参数、错误、返回语句,当然还需要了解基本的 Ruby!让我们创建library/rsync.rb文件,其中包含以下代码。完整代码可在 GitHub 上找到:

#!/usr/bin/env ruby 

require 'rsync' 
require 'json' 

src = '' 
dest = '' 
ret_msg = '' 
SUCCESS = '' 

def print_message(state, msg, key='failed') 
    message = { 
        key => state, 
        "msg" => msg 
    } 
    print message.to_json 
    exit 1 if state == false 
    exit 0 
...

在前述模块中,我们首先处理用户参数,然后使用rsync库复制文件,最后返回输出。

要使用这个功能,您需要确保系统上存在用于 Ruby 的rsync库。为此,您可以执行以下命令:

gem install rsync

让我们逐步分解上述代码,看看它是如何工作的。

我们首先编写一个名为print_message的方法,该方法将以 JSON 格式打印输出。通过这样做,我们可以在多个地方重用相同的代码。请记住,如果要使 Ansible 运行失败,则您的模块的输出应包含failed=true;否则,Ansible 将认为模块成功并将继续进行下一个任务。获得的输出如下所示:

#!/usr/bin/env ruby 

require 'rsync' 
require 'json' 

src = '' 
dest = '' 
ret_msg = '' 
SUCCESS = '' 

def print_message(state, msg, key='failed') 
    message = { 
        key => state, 
        "msg" => msg 
    } 
    print message.to_json 
    exit 1 if state == false 
    exit 0 
end

然后我们处理参数文件,其中包含由空格字符分隔的键值对。这类似于我们之前使用 Python 模块时所做的,我们负责解析参数。我们还执行一些检查,以确保用户没有漏掉任何必需的参数。在这种情况下,我们检查是否已指定了srcdest参数,并在未提供参数时打印一条消息。进一步的检查可能包括参数的格式和类型。您可以添加这些检查和您认为重要的任何其他检查。例如,如果您的参数之一是date参数,则需要验证输入是否确实是正确的日期。考虑以下代码片段,其中显示了讨论过的参数:

args_file = ARGV[0] 
data = File.read(args_file) 
arguments = data.split(" ") 
arguments.each do |argument| 
    print_message(false, "Argument should be name-value pairs. Example name=foo") if not argument.include?("=") 
    field, value = argument.split("=") 
    if field == "src" 
        src = value 
    elsif field == "dest" 
        dest = value 
    else print_message(false, "Invalid argument provided. Valid arguments are src and dest.") 
    end 
end 

一旦我们拥有了必需的参数,我们将使用rsync库复制文件,如下所示:

result = Rsync.run("#{src}", "#{dest}") 
if result.success? 
    success = true 
    ret_msg = "Copied file successfully" 
else 
    success = false 
    ret_msg = result.error 
end 

最后,我们检查rsync任务是通过还是失败,然后调用print_message函数将输出打印在stdout上,如下所示:

if success 
    print_message(false, "#{ret_msg}") 
else 
    print_message(true, "#{ret_msg}") 
end

您可以通过简单地将参数文件传递给模块来测试您的 Ruby 模块。为此,我们可以创建/tmp/arguments文件,并包含以下内容:

src=/etc/resolv.conf dest=/tmp/resolv_backup.conf

现在让我们运行模块,如下所示:

ruby library/rsync.rb /tmp/arguments

我们将收到以下输出:

{"failed":false,"msg":"Copied file successfully"} 

我们将留下serverspec测试由您完成。

测试模块

由于对其目的和其为业务带来的好处的理解不足,测试经常被低估。测试模块与测试 Ansible playbook 的任何其他部分一样重要,因为模块中的微小更改可能会破坏整个 playbook。我们将以本章的 使用 Python 编写模块 部分中编写的 Python 模块为例,并使用 Python 的 nose 测试框架编写一个集成测试。虽然也鼓励进行单元测试,但对于我们的场景,即检查远程用户是否存在,集成测试更有意义。

nose 是一个 Python 测试框架;您可以在 nose.readthedocs.org/en/latest/ 上找到有关该测试框架的更多信息。

为了对模块进行测试,我们将先前的模块转换为一个 Python 类,以便我们可以直接将该类导入到我们的测试中,并仅运行模块的主要逻辑。以下代码显示了重组的 library/check_user_py3.py 模块,该模块将检查远程主机上是否存在用户。完整代码可在 GitHub 上找到:

#!/usr/bin/env python 

import pwd 
from ansible.module_utils.basic import AnsibleModule 

class User: 
    def __init__(self, user): 
        self.user = user 

    # Check if user exists 
    def check_if_user_exists(self): 
        try: 
            user = pwd.getpwnam(self.user) 
            success = True 
            ret_msg = 'User %s exists' % self.user 
        except KeyError: 
            success = False 
            ret_msg = 'User %s does not exists' % self.user 
        return success, ret_msg 

 ...

正如您在上述代码中所见,我们创建了一个名为 User 的类。我们实例化了该类并调用了 check_if_user_exists 方法来检查用户是否实际存在于远程计算机上。现在是时候编写集成测试了。我们假设您已经在系统上安装了 nose 包。如果没有,不用担心!您仍然可以使用以下命令安装该包:

pip install nose

现在让我们在 library/test_check_user_py3.py 中编写集成测试文件,如下所示:

from nose.tools import assert_equals, assert_false, assert_true 
import imp 
imp.load_source("check_user","check_user_py3.py") 
from check_user import User 

def test_check_user_positive(): 
    chkusr = User("root") 
    success, ret_msg = chkusr.check_if_user_exists() 
    assert_true(success) 
    assert_equals('User root exists', ret_msg) 

def test_check_user_negative(): 
    chkusr = User("this_user_does_not_exists") 
    success, ret_msg = chkusr.check_if_user_exists() 
    assert_false(success) 
    assert_equals('User this_user_does_not_exists does not exists', ret_msg) 

在上述集成测试中,我们导入了 nose 包和我们的 check_user 模块。我们通过传递我们想要检查的用户来调用 User 类。然后,我们通过调用 check_if_user_exists() 方法来检查用户是否存在于远程主机上。nose 方法 - assert_trueassert_falseassert_equals - 可以用于比较预期值与实际值。只有当 assert 方法通过时,测试也会通过。您可以通过具有以 test_ 开头的多个方法来在同一个文件中拥有多个测试;例如,test_check_user_positive()test_check_user_negative() 方法。nose 测试将获取所有以 test_ 开头的方法并执行它们。

正如您所见,我们实际上为一个函数创建了两个测试。这是测试的一个关键部分。始终尝试您知道会成功的情况,但也不要忘记测试您期望失败的情况。

现在我们可以通过运行以下代码使用 nose 来测试它是否正常工作:

cd library
nosetests -v test_check_users_py3.py

您应该会收到类似以下代码块的输出:

test_check_user_py3.test_check_user_positive ... ok test_check_user_py3.test_check_user_negative ... ok --------------------------------------------------- Ran 2 tests in 0.001sOK

正如您所见,测试通过了,因为主机上存在 root 用户,而 this_user_does_not_exists 用户不存在。

我们使用 nose 测试的 -v 选项来启用 详细模式

对于更复杂的模块,我们建议您编写单元测试和集成测试。您可能会想知道为什么我们没有使用 serverspec 来测试模块。

我们仍然建议将 serverspec 测试作为 playbook 的功能测试的一部分进行运行;然而,对于单元测试和集成测试,建议使用知名框架。同样,如果您编写了 Ruby 模块,我们建议您使用诸如 rspec 等框架为其编写测试。如果您的自定义 Ansible 模块具有多个参数和多个组合,则您将编写更多的测试来测试每个场景。最后,我们建议您将所有这些测试作为您的 CI 系统的一部分运行,无论是 Jenkins、Travis 还是其他任何系统。

摘要

到此,我们结束了这一小而重要的章节,重点介绍了如何通过编写自定义模块来扩展 Ansible。您学会了如何使用 Python、bash 和 Ruby 来编写自己的模块。我们还学习了如何为模块编写集成测试,以便将其集成到您的 CI 系统中。希望未来通过使用模块来扩展 Ansible 的功能将会更加容易!

在下一章中,我们将进入供应、部署和编排的世界,看看当我们为环境中的各种实例提供新的实例或者想要将软件更新部署到各种实例时,Ansible 如何解决我们的基础设施问题。我们承诺这段旅程将会很有趣!

调试和错误处理

像软件代码一样,测试基础架构代码是一项非常重要的任务。在生产环境中最好没有未经测试的代码浮现,尤其是当您有严格的客户 SLA 需要满足时,即使对于基础架构也是如此。在本章中,我们将看到语法检查、在不将代码应用于机器上进行测试(no-op 模式)以及用于 playbook 的功能测试,playbook 是 Ansible 的核心,触发您想在远程主机上执行的各种任务。建议您将其中一些集成到您为 Ansible 设置的持续集成CI)系统中,以更好地测试您的 playbook。我们将看到以下要点:

  • 语法检查

  • 带有和不带有--diff的检查模式

  • 功能测试

作为功能测试的一部分,我们将关注以下内容:

  • 对系统最终状态的断言

  • 使用标签进行测试

  • 使用--syntax-check选项

  • 使用ANSIBLE_KEEP_REMOTE_FILESANSIBLE_DEBUG标志

然后,我们将看看如何处理异常以及如何自愿触发错误。

技术要求

对于本章,除了常规要求外,没有特定要求,例如 Ansible、Vagrant 和一个 shell。

您可以从本书的 GitHub 存储库中下载所有文件,网址为github.com/PacktPublishing/Learning-Ansible-2.X-Third-Edition/tree/master/Chapter08

语法检查

每次运行 playbook 时,Ansible 首先检查 playbook 文件的语法。如果遇到错误,Ansible 会报告语法错误,并且不会继续,除非您修复了该错误。仅当运行ansible-playbook命令时才执行此语法检查。在编写大型 playbook 或包含任务文件时,可能很难修复所有错误;这可能会浪费更多时间。为了应对这种情况,Ansible 提供了一种方式,让您随着 playbook 的编写进度来检查您的 YAML 语法。对于此示例,我们将需要创建名为playbooks/setup_apache.yaml的文件,并包含以下内容:

---
- hosts: all
  tasks: 
    - name: Install Apache 
      yum: 
        name: httpd 
        state: present 
      become: True
    - name: Enable Apache 
    service: 
        name: httpd 
        state: started 
        enabled: True 
      become: True

现在我们有了示例文件,我们需要用--syntax-check参数运行它;因此,您需要按照以下方式调用 Ansible:

ansible-playbook playbooks/setup_apache.yaml --syntax-check

ansible-playbook命令检查了setup_apache.yml playbook 的 YAML 语法,并显示了 playbook 的语法是正确的。让我们看看 playbook 中无效语法导致的结果错误:

ERROR! Syntax Error while loading YAML.
  did not find expected '-' indicator

The error appears to have been in '/home/fale/Learning-Ansible-2.X-Third-Edition/Ch8/playbooks/setup_apache.yaml': line 10, column 5, but may
be elsewhere in the file depending on the exact syntax problem.

The offending line appears to be:

    - name: Enable Apache
    service:
    ^ here

错误显示Enable Apache任务中存在缩进错误。Ansible 还会提供错误的行号、列号和发现错误的文件名(即使这并不是确切错误位置的保证)。这肯定应该是您为 Ansible 的 CI 运行的基本测试之一。

检查模式

检查模式(也称为dry-runno-op 模式)将以无操作模式运行你的 playbook - 也就是说,它不会将任何更改应用于远程主机; 相反,它只会显示运行任务时将引入的更改。检查模式是否实际启用取决于每个模块。有一些命令可能会引起你的兴趣。所有这些命令都必须在/usr/lib/python2.7/site-packages/ansible/modules或你的 Ansible 模块文件夹所在的位置运行(根据你使用的操作系统以及你安装 Ansible 的方式,可能存在不同的路径)。

要计算安装的可用模块数量,你可以执行此命令:

find . -type f | grep '.py$' | grep -v '__init__' | wc -l 

使用 Ansible 2.7.2,此命令的结果是2095,因为 Ansible 有那么多模块。

如果你想看看有多少支持检查模式的模块,你可以运行以下代码:

grep -r 'supports_check_mode=True' | awk -F: '{print $1}' | sort | uniq | wc -l 

使用 Ansible 2.7.2,此命令的结果是1239

你可能还会发现以下命令对于列出支持检查模式的所有模块很有用:

grep -r 'supports_check_mode=True' | awk -F: '{print $1}' | sort | uniq 

这有助于你测试 playbook 的行为方式,并在将其运行在生产服务器之前检查是否可能存在任何故障。你只需简单地在ansible-playbook命令中传递--check选项来运行检查模式下的 playbook。让我们看看检查模式如何与setup_apache.yml playbook 一起运行,运行以下代码:

ansible-playbook --check -i ws01, playbooks/setup_apache.yaml

结果将如下所示:

PLAY [all] ***********************************************************

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

TASK [Install Apache] ************************************************
changed: [ws01]

TASK [Enable Apache] *************************************************
changed: [ws01]

PLAY RECAP ***********************************************************
ws01                          : ok=3 changed=2 unreachable=0 failed=0

在上述运行中,Ansible 不会在目标主机上进行更改,而是会突出显示在实际运行期间将发生的所有更改。从上述运行中,你可以发现httpd服务已经安装在目标主机上。因此,Ansible 对该任务的退出消息是 OK:

TASK [Install Apache] ************************************************
changed: [ws01]

但是,对于第二个任务,它发现httpd服务在目标主机上未运行:

TASK [Enable Apache] *************************************************
changed: [ws01]

当你再次运行上述的 playbook 时,如果没有启用检查模式,Ansible 会确保服务状态正在运行。

使用--diff 指示文件之间的差异

在检查模式下,你可以使用--diff选项来显示将应用于文件的更改。为了能够看到--diff选项的使用,我们需要创建一个playbooks/setup_and_config_apache.yaml playbook,以匹配以下内容:

- hosts: all
  tasks: 
    - name: Install Apache 
      yum: 
        name: httpd 
        state: present 
      become: True
    - name: Enable Apache 
      service: 
        name: httpd 
        state: started 
        enabled: True 
      become: True
    - name: Ensure Apache userdirs are properly configured
      template:
        src: ../templates/userdir.conf
        dest: /etc/httpd/conf.d/userdir.conf
      become: True

正如你所看到的,我们添加了一个任务,将确保/etc/httpd/conf.d/userdir.conf文件处于特定状态。

我们还需要创建一个模板文件,放置在templates/userdir.conf中,并包含以下内容(完整文件可在 GitHub 上找到):

#
# UserDir: The name of the directory that is appended onto a user's home
# directory if a ~user request is received.
#
# The path to the end user account 'public_html' directory must be
# accessible to the webserver userid. This usually means that ~userid
# must have permissions of 711, ~userid/public_html must have permissions
# of 755, and documents contained therein must be world-readable.
# Otherwise, the client will only receive a "403 Forbidden" message.
#
<IfModule mod_userdir.c>
    #
    # UserDir is disabled by default since it can confirm the presence
    # of a username on the system (depending on home directory
    # permissions).
    #
    UserDir enabled

  ...

在此模板中,我们仅更改了UserDir enabled行,默认情况下为UserDir disabled

--diff选项与file模块不兼容;你必须仅使用template模块。

现在我们可以使用以下命令测试此结果:

ansible-playbook -i ws01, playbooks/setup_and_config_apache.yaml --diff --check 

正如你所看到的,我们正在使用--check参数,确保这将是一个干运行。我们将收到以下输出:

PLAY [all] ***********************************************************

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

TASK [Install Apache] ************************************************
ok: [ws01]

TASK [Enable Apache] *************************************************
ok: [ws01]

TASK [Ensure Apache userdirs are properly configured] ****************
--- before: /etc/httpd/conf.d/userdir.conf
+++ after: /home/fale/.ansible/tmp/ansible-local-6756FTSbL0/tmpx9WVXs/userdir.conf
@@ -14,7 +14,7 @@
 # of a username on the system (depending on home directory
 # permissions).
 #
- UserDir disabled
+ UserDir enabled

 #
 # To enable requests to /~user/ to serve the user's public_html

changed: [ws01]

PLAY RECAP ***********************************************************
ws01                          : ok=4 changed=1 unreachable=0 failed=0 

我们可以看到,Ansible 比较远程主机的当前文件与源文件;以 "+" 开头的行表示向文件中添加了一行,而 "-" 表示删除了一行。

你也可以使用 --diff 选项而不用 --check,这将允许 Ansible 进行指定的更改并显示两个文件之间的差异。

在 CI 测试的一部分中,将 --diff--check 模式一起使用作为测试步骤,可以断言有多少步骤在运行过程中发生了变化。另一个可以同时使用这些功能的情况是部署过程的一部分,用于检查运行 Ansible 时会发生什么变化。

有时候会出现这样的情况——尽管不应该出现,但有时候确实会出现——你在一台机器上很长一段时间没有运行 playbook,而你担心再次运行会破坏某些东西。使用这些选项可以帮助你了解到这只是你的担忧,还是一个真正的风险。

Ansible 中的功能测试

维基百科称功能测试是一种质量保证QA)过程和一种基于所测试软件组件的规格的黑盒测试。功能测试通过提供输入并检查输出来测试函数;内部程序结构很少被考虑。在基础设施方面,在代码方面同样重要。

从基础设施的角度来看,就功能测试而言,我们在实际机器上测试 Ansible 运行的输出。Ansible 提供了多种执行 playbook 的功能测试的方式,让我们来看看其中一些最常用的方法。

使用 assert 进行功能测试

仅当你想要检查任务是否会在主机上改变任何内容时,check 模式才会起作用。当你想检查你的模块的输出是否符合预期时,这并没有帮助。例如,假设你编写了一个模块来检查端口是开启还是关闭。为了测试这一点,你可能需要检查你的模块的输出,看它是否与期望的输出匹配。为了执行这样的测试,Ansible 提供了一种将模块的输出与期望的输出直接进行比较的方法。

让我们通过创建 playbooks/assert_ls.yaml 文件,并使用以下内容来查看它是如何工作的:

---
- hosts: all
  tasks: 
    - name: List files in /tmp 
      command: ls /tmp 
      register: list_files 
    - name: Check if file testfile.txt exists 
      assert: 
        that: 
          - "'testfile.txt' in list_files.stdout_lines" 

在上述 playbook 中,我们在目标主机上运行 ls 命令,并将该命令的输出记录在 list_files 变量中。接下来,我们要求 Ansible 检查 ls 命令的输出是否具有期望的结果。我们使用 assert 模块来实现这一点,该模块使用某些条件检查来验证任务的 stdout 值是否符合用户的预期输出。让我们运行上述 playbook,看看 Ansible 返回什么输出,使用以下命令:

ansible-playbook -i ws01, playbooks/assert_ls.yaml

由于我们没有这个文件,所以我们会收到以下输出:

PLAY [all] ***********************************************************

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

TASK [List files in /tmp] ********************************************
changed: [ws01]

TASK [Check if file testfile.txt exists] *****************************
fatal: [ws01]: FAILED! => {
 "assertion": "'testfile.txt' in list_files.stdout_lines", 
 "changed": false, 
 "evaluated_to": false, 
 "msg": "Assertion failed"
}
 to retry, use: --limit @/home/fale/Learning-Ansible-2.X-Third-Edition/Ch8/playbooks/assert_ls.retry

PLAY RECAP ***********************************************************
ws01                          : ok=2 changed=1 unreachable=0 failed=1 

如果我们在创建预期的文件之后重新运行 playbook,这将是结果:

PLAY [all] ***********************************************************

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

TASK [List files in /tmp] ********************************************
changed: [ws01]

TASK [Check if file testfile.txt exists] *****************************
ok: [ws01] => {
 "changed": false, 
 "msg": "All assertions passed"
}

PLAY RECAP ***********************************************************
ws01                          : ok=3 changed=1 unreachable=0 failed=0

这次,任务通过了 OK 信息,因为testfile.txtlist_files 变量中存在。同样,你可以使用andor运算符在变量中匹配多个字符串或多个变量。断言功能非常强大,编写过单元测试或集成测试的用户将非常高兴看到这个功能!

使用标签进行测试

标签是一种在不运行整个 playbook 的情况下测试一堆任务的好方法。我们可以使用标签在节点上运行实际测试,以验证用户在 playbook 中所期望的状态。我们可以将其视为在实际框中运行 Ansible 的另一种方法来运行集成测试。标签测试方法可以在实际运行 Ansible 的机器上运行,并且主要在部署期间用于测试终端系统的状态。在本节中,我们首先看一下如何通用地使用tags,它们可能会帮助我们,不仅仅是用于测试,而且甚至是用于测试目的。

要在 playbook 中添加标签,请使用tags参数,后面跟着一个或多个标签名称,用逗号或 YAML 列表分隔。让我们在playbooks/tag_example.yaml中创建一个简单的 playbook,以查看标签如何与以下内容一起工作:

- hosts: all
  tasks: 
    - name: Ensure the file /tmp/ok exists 
      file: 
        name: /tmp/ok 
        state: touch 
      tags: 
        - file_present 
    - name: Ensure the file /tmp/ok does not exists 
      file: 
        name: /tmp/ok 
        state: absent 
      tags: 
        - file_absent 

如果现在运行 playbook,文件将被创建和销毁。我们可以看到它的运行情况:

ansible-playbook -i ws01, playbooks/tags_example.yaml

它会给我们这个输出:

PLAY [all] ***********************************************************

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

TASK [Ensure the file /tmp/ok exists] ********************************
changed: [ws01]

TASK [Ensure the file /tmp/ok does not exists] ***********************
changed: [ws01]

PLAY RECAP ***********************************************************
ws01                          : ok=3 changed=2 unreachable=0 failed=0 

由于这不是一个幂等的 playbook,如果我们反复运行它,我们将始终看到相同的结果,因为 playbook 将每次都创建和删除文件。

但是,我们添加了两个标签:file_presentfile_absent。你现在可以仅传递file_present 标签或file_absent标签以执行其中一个动作,就像以下示例中所示:

ansible-playbook -i ws01, playbooks/tags_example.yaml -t file_present

由于-t file_present部分,只有带有file_present标签的任务将被执行。事实上,这将是输出:

PLAY [all] ***********************************************************

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

TASK [Ensure the file /tmp/ok exists] ********************************
changed: [ws01]

PLAY RECAP ***********************************************************
ws01                          : ok=2 changed=1 unreachable=0 failed=0 

你还可以使用标签在远程主机上执行一组任务,就像从负载均衡器中取出一个服务器并将其添加回负载均衡器。

你还可以将标签与--check选项一起使用。通过这样做,你可以在不实际在主机上运行任务的情况下测试你的任务。这使你能够直接测试一堆个别任务,而不必将任务复制到临时 playbook 并从那里运行。

理解--skip-tags 选项

Ansible 还提供了一种跳过 playbook 中某些标签的方法。如果你有一个带有多个标签(例如 10 个)的长 playbook,并且你想要执行它们中的全部,但不包括一个,那么向 Ansible 传递九个标签不是一个好主意。如果你忘了传递一个标签,ansible-run命令将失败,情况将变得更加困难。为了克服这种情况,Ansible 提供了一种跳过几个标签而不是传递多个应该运行的标签的方法。它的工作方式非常简单,可以按以下方式触发:

ansible-playbook -i ws01, playbooks/tags_example.yaml --skip-tags file_present 

输出将类似于以下内容:

PLAY [all] ***********************************************************

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

TASK [Ensure the file /tmp/ok exists] ********************************
changed: [ws01]

PLAY RECAP ***********************************************************
ws01                          : ok=2 changed=1 unreachable=0 failed=0 

正如你所见,除了具有 file_present 标签的任务之外,所有任务都已执行。

理解调试命令

Ansible 允许我们使用两个非常强大的变量来帮助我们调试。

ANSIBLE_KEEP_REMOTE_FILES 变量允许我们告诉 Ansible 保留它在远程机器上创建的文件,以便我们可以回过头来调试它们。

ANSIBLE_DEBUG 变量允许我们告诉 Ansible 将所有调试内容打印到 shell。调试输出通常过多,但对于某些非常复杂的问题可能有所帮助。

我们已经看到如何在你的 playbook 中查找问题。有时,你知道特定步骤可能会失败,但没关系。在这种情况下,我们应该适当地管理异常。让我们看看如何做到这一点。

管理异常

有很多情况下,出于各种原因,你希望你的 playbook 和角色在一个或多个任务失败的情况下继续执行。一个典型的例子是你想检查软件是否安装。让我们看一个以下示例,在只有当 Java 8 未安装时才安装 Java 11。在 roles/java/tasks/main.yaml 文件中,我们将输入以下代码:

- name: Verify if Java8 is installed
  command: rpm -q java-1.8.0-openjdk
  args:
    warn: False
  register: java 
  ignore_errors: True 
  changed_when: java is failed 

- name: Ensure that Java11 is installed
  yum:
    name: java-11-openjdk
    state: present
  become: True
  when: java is failed

在继续执行此角色所需的其他部分之前,我想在角色任务列表的各个部分上花几句话,因为有很多新东西。

在此任务中,我们将执行一个 rpm 命令:

- name: Verify if Java8 is installed
  command: rpm -q java-1.8.0-openjdk
  args:
    warn: False
  register: java 
  ignore_errors: True 
  changed_when: java is failed 

此代码可能有两种可能的输出:

  • 失败

  • 返回 JDK 软件包的完整名称

由于我们只想检查软件包是否存在,然后继续执行,我们会记录输出(第五行),并忽略可能的失败(第六行)。

当它失败时,意味着 Java8 未安装,因此我们可以继续安装 Java11

- name: Ensure that Java11 is installed
  yum:
    name: java-11-openjdk
    state: present
  become: True
  when: java is failed

创建角色后,我们将需要包含主机机器的 hosts 文件;在我的情况下,它将如下所示:

ws01

我们还需要一个用于应用角色的 playbook,放置在 playbooks/hosts/j01.fale.io.yaml 中,内容如下:

- hosts: ws01
  roles: 
    - java 

现在我们可以用以下命令执行它:

ansible-playbook playbooks/hosts/ws01.yaml 

我们将得到以下结果:

PLAY [ws01] **********************************************************

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

TASK [java : Verify if Java8 is installed] ***************************
fatal: [ws01]: FAILED! => {"changed": true, "cmd": ["rpm", "-q", "java-1.8.0-openjdk"], "delta": "0:00:00.028358", "end": "2019-02-10 10:56:22.474350", "msg": "non-zero return code", "rc": 1, "start": "2019-02-10 10:56:22.445992", "stderr": "", "stderr_lines": [], "stdout": "package java-1.8.0-openjdk is not installed", "stdout_lines": ["package java-1.8.0-openjdk is not installed"]}
...ignoring

TASK [java : Ensure that Java11 is installed] ************************
changed: [ws01]

PLAY RECAP ***********************************************************
ws01 : ok=3 changed=2 unreachable=0 failed=0

正如你所见,安装检查失败,因为机器上未安装 Java,因此其他任务已按预期执行。

触发失败

有些情况下,您希望直接触发一个失败。这可能发生在多种原因下,即使这样做有一些不利之处,因为当您触发失败时,Playbook 将被强行中断,如果您不小心的话,这可能会导致机器处于不一致的状态。我曾亲眼见过一个情况非常适用的场景,那就是当您运行一个不是幂等的 Playbook 时(例如,构建一个应用的新版本),您需要一个变量(例如,要部署的版本/分支)设置。在这种情况下,您可以在开始运行操作之前检查预期的变量是否正确配置,以确保以后的一切都能正常运行。

让我们将以下代码放入playbooks/maven_build.yaml中:

- hosts: all
  tasks: 
    - name: Ensure the tag variable is properly set
      fail: 'The version needs to be defined. To do so, please add: --extra-vars "version=$[TAG/BRANCH]"' 
      when: version is not defined 
    - name: Get last Project version 
      git: 
        repo: https://github.com/org/project.git 
        dest: "/tmp" 
        version: '{{ version }}' 
    - name: Maven clean install 
      shell: "cd /tmp/project && mvn clean install" 

如您所见,我们期望用户在调用脚本的命令中添加--extra-vars "version=$[TAG/BRANCH]"。我们本可以设置一个默认要使用的分支,但这样做太冒险,因为用户可能分心并忘记自己添加正确的分支名称,这将导致编译(和部署)错误的应用程序版本。fail模块还允许我们指定将显示给用户的消息。

我认为,在手动运行 Playbook 时,fail任务要比失败更加有用,因为自动运行 Playbook 时,管理异常通常比直接失败更好。

使用fail模块,一旦发现问题,您就可以退出 Playbooks。

摘要

在本章中,我们已经学习了如何使用语法检查、带有和不带--diff的检查模式以及功能测试来调试 Ansible Playbooks。

作为功能测试的一部分,我们已经看到如何对系统的最终状态进行断言,如何利用标签进行测试,以及如何使用--syntax-check选项和ANSIBLE_KEEP_REMOTE_FILESANSIBLE_DEBUG标志。然后,我们转向了失败的管理,最后,我们学习了如何故意触发失败。

在下一章中,我们将讨论多层环境以及部署方法论。

复杂环境

到目前为止,我们已经了解了如何开发 Ansible 剧本并对其进行测试。最后一个方面是如何将剧本发布到生产环境。在大多数情况下,发布剧本到生产环境之前,你将需要处理多个环境。这类似于你的开发人员编写的软件。许多公司有多个环境,通常你的剧本将按照以下步骤进行:

  • 开发环境

  • 测试环境

  • 阶段环境

  • 生产

一些公司以不同的方式命名这些环境,有些公司还有额外的环境,比如所有软件都必须在进入生产环境之前通过认证的认证环境。

当你编写你的剧本并设置角色时,我们强烈建议你从一开始就牢记环境的概念。与你的软件和运维团队交流,了解你的设置必须满足多少个环境可能是值得的。我们将列举一些方法,并提供你可以在你的环境中遵循的示例。

本章将涵盖以下主题:

  • 基于 Git 分支的代码

  • 软件分发策略

  • 使用修订控制系统部署 Web 应用程序

  • 使用 RPM 包部署 Web 应用程序

  • 使用 RPM 打包编译软件

技术要求

要能够跟随本章的示例,你需要一台能够构建 RPM 包的 UNIX 机器。我的建议是安装 Fedora 或 CentOS(无论是裸金属还是虚拟机)。

你可以从本书的 GitHub 存储库下载所有文件,网址为github.com/PacktPublishing/Learning-Ansible-2.X-Third-Edition/tree/master/Chapter09

基于 Git 分支的代码

假设你要照顾四个环境,它们如下:

  • 开发

  • 测试

  • 阶段

  • 生产

在基于 Git 分支的方法中,你将每个分支拥有一个环境。你总是首先对开发进行更改,然后将这些更改提升到测试(在 Git 中合并或挑选,以及标记提交),阶段生产。在这种方法中,你将拥有一个单一的清单文件,一组变量文件,最后,针对每个分支的角色和剧本的一堆文件夹。

具有多个文件夹的单一稳定分支

在这种方法中,您将始终保持开发和主分支。初始代码提交到开发分支,一旦稳定,将其推广到主分支。在主分支上存在的相同角色和 playbooks 将在所有环境中运行。另一方面,您将为每个环境有单独的文件夹。让我们看一个例子。我们将展示如何针对两个环境(暂存和生产)拥有单独的配置和清单。您可以根据自己的情况来扩展到您使用的所有环境。首先,让我们看一下playbooks/variables.yaml中的 playbook,它将在这些多个环境中运行,并具有以下内容。完整代码可在 GitHub 上查看:

- hosts: web 
  user: vagrant 
  tasks: 
    - name: Print environment name 
      debug: 
        var: env 
    - name: Print db server url 
      debug: 
        var: db_url 
    - name: Print domain url 
      debug: 
        var: domain 
...

正如您所见,在这个 playbook 中有两组任务:

  • 运行在 DB 服务器上的任务

  • 运行在 Web 服务器上的任务

还有一个额外的任务来打印特定环境中所有服务器共有的环境名称。我们也将有两个不同的清单文件。

第一个将被称为inventory/production,内容如下:

[web] 
ws01.fale.io 
ws02.fale.io 

[db] 
db01.fale.io 

[production:children] 
db 
web 

第二个将被称为inventory/staging,内容如下:

[web] 
ws01.staging.fale.io 
ws02.staging.fale.io 

[db] 
db01.staging.fale.io 

[staging:children] 
db 
web

如您所见,在每个环境中web部分有两台机器,db部分有一台。此外,我们对阶段和生产环境有不同的机器组。附加部分[ENVIRONMENT:children]允许您创建一组组。这意味着在ENVIRONMENT部分定义的任何变量都将应用于dbweb组,除非在各自的部分中覆盖。接下来看看如何在每个环境中分离变量值将是有趣的。

我们先从位于inventory/group_vars/all的所有环境相同的变量开始:

db_user: mysqluser 

两个环境中唯一相同的变量是db_user

现在我们可以查看位于inventory/group_vars/production的特定于生产环境的变量:

env: production 
domain: fale.io 
db_url: db.fale.io 
db_pass: this_is_a_safe_password 

如果我们现在查看位于inventory/group_vars/staging的特定于阶段环境的变量,我们会发现与生产环境中相同的变量,但值不同:

env: staging 
domain: staging.fale.io 
db_url: db.staging.fale.io 
db_pass: this_is_an_unsafe_password 

我们现在可以验证我们收到了预期的结果。首先,我们将对暂存环境运行:

ansible-playbook -i inventory/staging playbooks/variables.yaml

我们应该会收到类似以下的输出。完整代码输出可在 GitHub 上查看:

PLAY [web] ***********************************************************

TASK [Gathering Facts] ***********************************************
ok: [ws01.staging.fale.io]
ok: [ws02.staging.fale.io]

TASK [Print environment name] ****************************************
ok: [ws01.staging.fale.io] => {
 "env": "staging"
}
ok: [ws02.staging.fale.io] => {
 "env": "staging"
}

TASK [Print db server url] *******************************************
ok: [ws01.staging.fale.io] => {
 "db_url": "db.staging.fale.io"
}
ok: [ws02.staging.fale.io] => {
 "db_url": "db.staging.fale.io"
}

...

现在我们可以针对生产环境运行:

ansible-playbook -i inventory/production playbooks/variables.yaml 

我们将收到以下结果:

PLAY [web] ***********************************************************

TASK [Gathering Facts] ***********************************************
ok: [ws02.fale.io]
ok: [ws01.fale.io]

TASK [Print environment name] ****************************************
ok: [ws01.fale.io] => {
 "env": "production"
}
ok: [ws02.fale.io] => {
 "env": "production"
}

TASK [Print db server url] *******************************************
ok: [ws01.fale.io] => {
 "db_url": "db.fale.io"
}
ok: [ws02.fale.io] => {
 "db_url": "db.fale.io"
}

...

您可以看到 Ansible 运行捕获了为暂存环境定义的所有相关变量。

如果您使用这种方法获得多个环境的稳定主分支,最好使用混合环境特定目录,group_vars和清单组来应对这种情况。

软件分发策略

部署应用程序可能是信息与通信技术(ICT)领域中最复杂的任务之一。这主要是因为它经常需要更改作为该应用程序某种程度上组成部分的大多数机器的状态。事实上,通常您会发现自己在部署过程中需要同时更改负载均衡器、分发服务器、应用服务器和数据库服务器的状态。新技术,如容器,正试图简化这些操作,但通常很难或不可能将传统应用程序移至容器中。

现在我们将分析各种软件分发策略以及 Ansible 如何帮助每一种。

从本地机器复制文件

这可能是最古老的软件分发策略了。其思想是将文件放在本地机器上(通常用于开发代码),一旦更改完成,文件的副本就会被放在服务器上(通常通过 FTP)。这种部署代码的方式经常用于 Web 开发,其中的代码(通常是 PHP)不需要任何编译。

由于多个问题,应避免使用这种分发策略:

  • 回滚很困难。

  • 无法跟踪各个部署的更改。

  • 没有部署历史记录。

  • 在部署过程中很容易出错。

尽管这种分发策略可以非常容易地通过 Ansible 自动化,我强烈建议立即转向其他能够让您拥有更安全的分发策略的策略。

带有分支的版本控制系统

许多公司正在使用这种技术来分发他们的软件,主要用于非编译软件。这种技术背后的理念是设置服务器使用代码库的本地副本。通过 SVN,这是可能的,但不是很容易正确管理,而 Git 允许这种技术的简化,使其非常受欢迎。

这种技术与我们刚刚看到的技术相比有很大的优势;其中主要优势如下:

  • 易于回滚

  • 非常容易获取更改历史记录

  • 非常容易的部署(特别是如果使用 Git)

另一方面,这种技术仍然存在多个缺点:

  • 没有部署历史记录

  • 编译软件困难

  • 可能存在安全问题

我想更详细地讨论一下您可能会遇到的这种技术的潜在安全问题。非常诱人的做法是直接在用于分发内容的文件夹中下载您的 Git 存储库,因此,如果这是一个 Web 服务器,那么这将是/var/www/文件夹。这样做有明显的优势,因为要部署,您只需要执行git pull。缺点是 Git 将创建/var/www/.git文件夹,其中包含整个 Git 存储库(包括历史记录),如果没有得到妥善保护,将可以被任何人自由下载。

Alexa 排名前 100 万的网站中约有 1%的网站可以公开访问 Git 文件夹,所以如果您想使用这种分发策略,一定要非常小心。

带有标签的修订控制系统

使用稍微复杂但具有一些优点的修订控制系统的另一种方法是利用标记系统。此方法要求您每次进行新部署时都要打标签,然后在服务器上检查特定的标签。

这具有上一种方法的所有优点,并加入了部署历史记录。已编译的软件问题和可能的安全问题与上一种方法相同。

RPM 软件包

部署软件的一种非常常见的方法(主要用于已编译的应用程序,但对于非编译的应用程序也有优势)是使用某种打包系统。一些语言,如 Java,已经包含了系统(Java 的情况下是 WAR),但也有可以用于任何类型的应用程序的打包系统,比如RPM 软件包管理器RPM)。RPM 最初由 Erik Troan 和 Marc Ewing 开发,并于 1997 年发布用于 Red Hat Linux。自那时起,它已被许多 Linux 发行版采用,并且是 Linux 世界中分发软件的两种主要方式之一,另一种是 DEB。这些系统的缺点是它们比以前的方法稍微复杂一些,但是这些系统可以提供更高级别的安全性,以及版本控制。此外,这些系统很容易嵌入到 CI/CD 流水线中,因此实际复杂性远低于乍看之下所见。

准备环境

要查看我们如何以我们在软件分发策略部分讨论的方式部署代码,我们将需要一个环境,显然我们将使用 Ansible 创建它。首先,为了确保我们的角色被正确加载,我们需要ansible.cfg文件,内容如下:

[defaults] 
roles_path = roles

然后,我们需要playbooks/groups/web.yaml文件来正确引导我们的 Web 服务器:

- hosts: web 
  user: vagrant 
  roles: 
    - common 
    - webserver 

正如您可以从前面的文件内容中想象的那样,我们将需要创建commonwebserver角色,它们与我们在第四章中创建的角色非常相似,处理复杂的部署。我们将从roles/common/tasks/main.yaml文件开始,内容如下。完整的代码可在 GitHub 上找到:

- name: Ensure EPEL is enabled 
  yum: 
    name: epel-release 
    state: present 
  become: True 
- name: Ensure libselinux-python is present 
  yum: 
    name: libselinux-python 
    state: present 
  become: True 
- name: Ensure libsemanage-python is present 
  yum: 
    name: libsemanage-python 
    state: present 
  become: True 
...

这是motd模板在roles/common/templates/motd中的模板:

                This system is managed by Ansible 
  Any change done on this system could be overwritten by Ansible 

OS: {{ ansible_distribution }} {{ ansible_distribution_version }} 
Hostname: {{ inventory_hostname }} 
eth0 address: {{ ansible_eth0.ipv4.address }} 

            All connections are monitored and recorded 
    Disconnect IMMEDIATELY if you are not an authorized user

现在我们可以转移到webserver角色——更具体地说是roles/webserver/tasks/main.yaml文件。完整的代码文件可以在 GitHub 上找到:

--- 
- name: Ensure the HTTPd package is installed 
  yum: 
    name: httpd 
    state: present 
  become: True 
- name: Ensure the HTTPd service is enabled and running 
  service: 
    name: httpd 
    state: started 
    enabled: True 
  become: True 
- name: Ensure HTTP can pass the firewall 
  firewalld: 
    service: http 
    state: enabled 
    permanent: True 
    immediate: True 
  become: True 
...

我们还需要在roles/webserver/handlers/main.yaml中创建处理程序,内容如下:

--- 
- name: Restart HTTPd 
  service: 
    name: httpd 
    state: restarted 
  become: True

我们在roles/webserver/templates/index.html.j2文件中添加以下内容:

<html> 
    <body> 
        <h1>Hello World!</h1> 
        <p>This page was created on {{ ansible_date_time.date }}.</p> 
        <p>This machine can be reached on the following IP addresses</p> 
        <ul> 
{% for address in ansible_all_ipv4_addresses %} 
            <li>{{ address }}</li> 
{% endfor %} 
        </ul> 
    </body> 
</html> 

最后,我们需要触及roles/webserver/files/website.conf文件,暂时将其留空,但它必须存在。

现在我们可以预配一对 CentOS 机器(我预配了ws01.fale.iows02.fale.io),并确保清单正确。我们可以通过运行它们的组播放本来配置这些机器:

ansible-playbook -i inventory/production playbooks/groups/web.yaml 

我们将收到以下输出。完整的代码输出可在 GitHub 上找到:

PLAY [web] ***********************************************************

TASK [Gathering Facts] ***********************************************
ok: [ws01.fale.io]
ok: [ws02.fale.io]

TASK [common : Ensure EPEL is enabled] *******************************
ok: [ws02.fale.io]
ok: [ws01.fale.io]

TASK [common : Ensure libselinux-python is present] ******************
ok: [ws02.fale.io]
ok: [ws01.fale.io]

TASK [common : Ensure libsemanage-python is present] *****************
ok: [ws01.fale.io]
ok: [ws02.fale.io]

TASK [common : Ensure we have last version of every package] *********
changed: [ws02.fale.io]
changed: [ws01.fale.io]

...

现在,我们可以在端口80上指向我们的节点,检查是否如预期显示了 HTTPd 页面。既然我们已经有了基本的 Web 服务器运行,我们现在可以专注于部署 Web 应用程序。

使用修订控制系统部署 Web 应用

现在我们将从修订控制系统(Git)直接将 Web 应用程序首次部署到我们的服务器上,使用 Ansible。因此,我们将部署一个简单的 PHP 应用程序,只由一个单独的 PHP 页面组成。源代码可在以下存储库找到:github.com/Fale/demo-php-app

要部署它,我们将需要将以下代码放置在playbooks/manual/rcs_deploy.yaml中:

- hosts: web 
  user: vagrant 
  tasks:
    - name: Ensure git is installed
      yum:
        name: git
        state: present 
      become: True
    - name: Install or update website 
      git: 
        repo: https://github.com/Fale/demo-php-app.git 
        dest: /var/www/application 
      become: True

现在我们可以使用以下命令运行部署器:

ansible-playbook -i inventory/production/playbooks/manual/rcs_deploy.yaml 

这是预期的结果:

PLAY [web] ***********************************************************

TASK [Gathering Facts] ***********************************************
ok: [ws02.fale.io]
ok: [ws01.fale.io]

TASK [Ensure git is installed] ***************************************
changed: [ws01.fale.io]
changed: [ws02.fale.io]

TASK [Install or update website] *************************************
changed: [ws02.fale.io]
changed: [ws01.fale.io]

PLAY RECAP ***********************************************************
ws01.fale.io                  : ok=3 changed=2 unreachable=0 failed=0 
ws02.fale.io                  : ok=3 changed=2 unreachable=0 failed=0

目前,我们的应用程序还无法访问,因为我们没有 HTTPd 规则来访问该文件夹。为了实现这一点,我们将需要更改roles/webserver/files/website.conf文件,内容如下:

<VirtualHost *:80> 
    ServerName app.fale.io 
    DocumentRoot /var/www/application 
    <Directory /var/www/application> 
        Options None 
    </Directory> 
    <DirectoryMatch ".git*"> 
        Require all denied 
    </DirectoryMatch> 
</VirtualHost>

正如您所见,我们只向通过app.fale.ioURL 到达我们服务器的用户显示此应用程序,而不是向所有人。这将确保所有用户都拥有一致的体验。此外,您可以看到我们阻止所有对.git文件夹(及其所有内容)的访问。这是出于我们在本章的软件分发策略部分提到的安全原因。

现在我们可以重新运行 Web 播放本以确保我们的 HTTPd 配置被传播:

ansible-playbook -i inventory/production playbooks/groups/web.yaml 

这是我们将要收到的结果。完整的代码输出可在 GitHub 上找到:

PLAY [web] ***********************************************************

TASK [Gathering Facts] ***********************************************
ok: [ws01.fale.io]
ok: [ws02.fale.io]

TASK [common : Ensure EPEL is enabled] *******************************
ok: [ws02.fale.io]
ok: [ws01.fale.io]

TASK [common : Ensure libselinux-python is present] ******************
ok: [ws01.fale.io]
ok: [ws02.fale.io]

TASK [common : Ensure libsemanage-python is present] *****************
ok: [ws01.fale.io]
ok: [ws02.fale.io]
 ...

您现在可以检查并确保一切正常运行。

我们已经看到了如何从 Git 获取源代码并将其部署到 Web 服务器,以便及时提供给我们的用户。现在,我们将深入研究另一种分发策略:使用 RPM 软件包部署 Web 应用程序。

使用 RPM 软件包部署 Web 应用程序

要部署 RPM 软件包,我们首先需要创建它。为此,我们需要的第一件事是一个SPEC 文件

创建 SPEC 文件

我们需要做的第一件事是创建一个 SPEC 文件,这是一个指导rpmbuild如何实际创建 RPM 软件包的配方。我们将把 SPEC 文件定位在spec/demo-php-app.spec中。以下是代码片段内容,完整代码可在 GitHub 上找到:

%define debug_package %{nil} 
%global commit0 b49f595e023e07a8345f47a3ad62a6f50f03121e 
%global shortcommit0 %(c=%{commit0}; echo ${c:0:7}) 

Name: demo-php-app 
Version: 0 
Release: 1%{?dist} 
Summary: Demo PHP application 

License: PD 
URL: https://github.com/Fale/demo-php-app 
Source0: %{url}/archive/%{commit0}.tar.gz#/%{name}-%{shortcommit0}.tar.gz 

%description 
This is a demo PHP application in RPM format 
 ...

在继续之前,让我们看看各个部分的作用和含义:

%define debug_package %{nil} 
%global commit0 b49f595e023e07a8345f47a3ad62a6f50f03121e 
%global shortcommit0 %(c=%{commit0}; echo ${c:0:7}) 

前三行是变量声明。

第一步将禁用调试包的生成。默认情况下,rpmbuild每次都会创建一个调试包并包含所有调试符号,但在本例中我们没有任何调试符号,因为我们没有进行任何编译。

第二个将提交的哈希放入commit0变量。第三个计算shortcommit0的值,该值计算为commit0字符串的前八个字符:

Name:       demo-php-app 
Version:    0 
Release:    1%{?dist} 
Summary:    Demo PHP application 

License:    PD 
URL:        https://github.com/Fale/demo-php-app 
Source0:    %{url}/archive/%{commit0}.tar.gz#/%{name}-%{shortcommit0}.tar.gz 

在第一行中,我们声明名称、版本、发布编号和摘要。版本和发布的区别在于版本是上游版本,而发布是上游版本的 SPEC 版本。

许可证是源许可证,而不是 SPEC 许可证。URL 用于跟踪上游网站。source0字段用于rpmbuild查找源文件的名称(如果存在多个文件,我们可以使用source1source2等)。此外,如果源字段是有效的 URI,则可以使用spectool来自动下载它们。这是打包到 RPM 软件包中的软件的description

%description 
This is a demo PHP application in RPM format

prep阶段是源文件解压缩和最终应用补丁的阶段。%autosetup将解压缩第一个源文件,并应用所有补丁。在这部分,您还可以执行在构建阶段前需要执行的其他操作,其目的是为构建阶段准备环境:

%prep 
%autosetup -n %{name}-%{commit0}

在这里,我们将列出所有构建阶段的操作。在我们的情况下,我们的源代码不需要编译,因此为空:

%build

install阶段,我们将文件放入%{buildroot}文件夹,模拟目标文件系统:

%install 
mkdir -p %{buildroot}/var/www/application 
ls -alh 
cp index.php %{buildroot}/var/www/application 

files部分需要声明要放入软件包中的文件:

%files 
%dir /var/www/application 
/var/www/application/index.php 

changelog用于跟踪谁何时发布了新版本以及带有哪些更改:

%changelog  
* Sun Feb 24 2019 Fabio Alessandro Locati - 0.1
- Initial packaging 

现在我们有了 SPEC 文件,我们需要构建它。为此,我们可以使用生产机器,但这样会增加对该机器的攻击面,所以最好避免。构建 RPM 软件的多种方式。主要的四种方式如下:

  • 手动

  • 使用 Ansible 自动化手动方式

  • Jenkins

  • Koji

让我们简要看一下区别。

手动构建 RPM 包

构建 RPM 软件包的最简单方式是以手动方式进行。

最大的优势在于您只需要几个简单的步骤来安装build,因此许多刚开始使用 RPM 的人都从这里开始。缺点是这个过程将是手动的,因此人为的错误可能会破坏结果。此外,手动构建非常难以审计,因为唯一可审计的部分是输出而非过程本身。

要构建 RPM 软件包,您需要一个 Fedora 或 EL(Red Hat Enterprise Linux,CentOS,Scientific Linux,Oracle Enterprise Linux)系统。如果您使用 Fedora,您需要执行以下命令来安装所有必需的软件:

sudo dnf install -y fedora-packager 

如果您正在运行 EL 系统,则需要执行以下命令:

sudo yum install -y mock rpm-build spectool 

无论哪种情况,您都需要将要使用的用户添加到 mock 组中,为此,您需要执行以下操作:

sudo usermod -a -G mock [yourusername] 

Linux 在登录时加载用户,因此要应用组更改,您需要重新启动会话。

此时,我们可以将 SPEC 文件复制到文件夹中(通常情况下,$HOME 是一个不错的选择),然后执行以下操作:

mkdir -p ~/rpmbuild/SOURCES 

这将创建所需的 $HOME/rpmbuild/SOURCES 文件夹。 -p 选项将自动创建路径中缺失的所有文件夹。我们使用 spectool 下载源文件并将其放置在适当的目录中。 spectool 将自动从 SPEC 文件中获取 URL,因此我们不必记住它:

spectool -R -g demo-php-app.spec 

现在我们需要创建一个 src.rpm 文件,为此,我们可以使用 rpmbuild

rpmbuild -bs demo-php-app.spec

此命令将输出类似于以下内容:

Wrote: /home/fale/rpmbuild/SRPMS/demo-php-app-0-1.fc28.src.rpm

名称中可能存在一些小差异;例如,您可能具有不同于 Fedora 24 的 $HOME 文件夹,如果您使用的是 Fedora 24 以外的其他版本,则可能有 fc24 以外的内容。此时,我们可以使用以下代码创建二进制文件:

mock -r epel-7-x86_64 /home/fale/rpmbuild/SRPMS/demo-php-app-0-1.fc28.src.rpm 

Mock 允许我们在干净的环境中构建 RPM 包,并且还由于 -r 选项而允许我们构建不同版本的 Fedora、EL 和 Mageia。该命令将给出非常长的输出,我们在此不涵盖,但在最后几行中有有用的信息。如果一切都构建正确,这是您应该看到的最后几行:

Wrote: /builddir/build/RPMS/demo-php-app-0-1.el7.centos.x86_64.rpm
Executing(%clean): /bin/sh -e /var/tmp/rpm-tmp.d4vPhr
+ umask 022
+ cd /builddir/build/BUILD
+ cd demo-php-app-b49f595e023e07a8345f47a3ad62a6f50f03121e
+ /usr/bin/rm -rf /builddir/build/BUILDROOT/demo-php-app-0-1.el7.centos.x86_64
+ exit 0
Finish: rpmbuild demo-php-app-0-1.fc28.src.rpm
Finish: build phase for demo-php-app-0-1.fc28.src.rpm
INFO: Done(/home/fale/rpmbuild/SRPMS/demo-php-app-0-1.fc28.src.rpm) Config(epel-7-x86_64) 0 minutes 58 seconds
INFO: Results and/or logs in: /var/lib/mock/epel-7-x86_64/result
Finish: run 

倒数第二行包含您可以找到结果的路径。如果您在该文件夹中查找,应该会找到以下文件:

drwxrwsr-x. 2 fale mock 4.0K Feb 24 12:26 .
drwxrwsr-x. 4 root mock 4.0K Feb 24 12:25 ..
-rw-rw-r--. 1 fale mock 4.6K Feb 24 12:26 build.log
-rw-rw-r--. 1 fale mock 3.3K Feb 24 12:26 demo-php-app-0-1.el7.centos.src.rpm
-rw-rw-r--. 1 fale mock 3.1K Feb 24 12:26 demo-php-app-0-1.el7.centos.x86_64.rpm
-rw-rw-r--. 1 fale mock 184K Feb 24 12:26 root.log
-rw-rw-r--. 1 fale mock  792 Feb 24 12:26 state.log

在编译过程中出现问题时,这三个日志文件非常有用。 src.rpm 文件将是我们使用第一个命令创建的 src.rpm 文件的副本,而 x86_64.rpm 文件是我们创建的 mock 文件,也是我们需要在机器上安装的文件。

使用 Ansible 构建 RPM 包

由于手动执行所有这些步骤可能会很长、乏味且容易出错,因此我们可以使用 Ansible 自动化它们。生成的 playbook 可能不是最清晰的,但可以以可重复的方式执行所有操作。

出于这个原因,我们将从头开始构建一个新的机器。我将这台机器称为 builder01.fale.io,我们还将更改 inventory/production 文件以匹配此更改:

[web] 
ws01.fale.io 
ws02.fale.io 

[db] 
db01.fale.io 

[builders] 
builder01.fale.io 

[production:children] 
db 
web 
builders 

在深入研究 builders 角色之前,我们需要对 webserver 角色进行一些更改,以启用新的存储库。首先是在 roles/webserver/tasks/main.yaml 文件末尾添加一个任务,其中包含以下代码:

- name: Install our private repository 
  copy: 
    src: privaterepo.repo 
    dest: /etc/yum.repos.d/privaterepo.repo 
  become: True

第二个更改实际上是使用以下内容创建 roles/webserver/files/privaterepo.repo 文件:

[privaterepo] 
name=Private repo that will keep our apps packages 
baseurl=http://repo.fale.io/ 
skip_if_unavailable=True 
gpgcheck=0 
enabled=1 
enabled_metadata=1

现在我们可以执行 webserver 组 playbook 以使更改生效,命令如下:

ansible-playbook -i inventory/production playbooks/groups/web.yaml 

应该显示如下输出。完整代码输出可在 GitHub 上找到:

PLAY [web] ***********************************************************

TASK [Gathering Facts] ***********************************************
ok: [ws02.fale.io]
ok: [ws01.fale.io]

TASK [common : Ensure EPEL is enabled] *******************************
ok: [ws02.fale.io]
ok: [ws01.fale.io]

TASK [common : Ensure libselinux-python is present] ******************
ok: [ws01.fale.io]
ok: [ws02.fale.io]

TASK [common : Ensure libsemanage-python is present] *****************
ok: [ws01.fale.io]
ok: [ws02.fale.io]

...

如预期的那样,唯一的变化是部署了我们新生成的仓库文件。

我们还需要为builders创建一个角色,其中包含位于roles/builder/tasks/main.yamltasks文件,内容如下。完整代码可在 GitHub 上找到:

- name: Ensure needed packages are present 
  yum: 
    name: '{{ item }}' 
    state: present 
  become: True 
  with_items: 
    - mock 
    - rpm-build 
    - spectool 
    - createrepo 
    - httpd 

- name: Ensure the user ansible is in the mock group 
  user: 
    name: ansible 
    groups: mock 
    append: True 
  become: True 

...

同样作为builder角色的一部分,我们需要roles/builder/handlers/main.yaml处理文件,内容如下:

- name: Restart HTTPd 
  service: 
    name: httpd 
    state: restarted 
  become: True 

如您从tasks文件中可以猜想的,我们还需要roles/builder/files/repo.conf文件,内容如下:

<VirtualHost *:80> 
    ServerName repo.fale.io 
    DocumentRoot /var/www/repo 
    <Directory /var/www/repo> 
        Options Indexes FollowSymLinks 
    </Directory> 
</VirtualHost>

我们还需要一个新的group playbook,位于playbooks/groups/builders.yaml,内容如下:

- hosts: builders 
  user: vagrant 
  roles: 
    - common 
    - builder 

现在我们可以创建主机本身,内容如下:

ansible-playbook -i inventory/production playbooks/groups/builders.yaml 

我们期望得到类似下面的结果:

PLAY [builders] ******************************************************

TASK [Gathering Facts] ***********************************************
ok: [builder01.fale.io]

TASK [common : Ensure EPEL is enabled] *******************************
changed: [builder01.fale.io]

TASK [common : Ensure libselinux-python is present] ******************
ok: [builder01.fale.io]

TASK [common : Ensure libsemanage-python is present] *****************
changed: [builder01.fale.io]

TASK [common : Ensure we have last version of every package] *********
changed: [builder01.fale.io]

TASK [common : Ensure NTP is installed] ******************************
changed: [builder01.fale.io]

TASK [common : Ensure the timezone is set to UTC] ********************
changed: [builder01.fale.io]

...

现在我们已经准备好基础设施的所有部分,可以创建playbooks/manual/rpm_deploy.yaml文件,内容如下。完整代码可在 GitHub 上找到:

- hosts: builders 
  user: vagrant 
  tasks: 
    - name: Copy SPEC file to user folder 
      copy: 
        src: ../../spec/demo-php-app.spec 
        dest: /home/vagrant
    - name: Ensure rpmbuild exists 
      file: 
        name: ~/rpmbuild 
        state: directory 
    - name: Ensure rpmbuild/SOURCES exists 
      file: 
        name: ~/rpmbuild/SOURCES 
        state: directory 
   ...

正如我们讨论过的,此 playbook 有许多不太干净的命令和 shell。将来可能有可能编写一个具有相同功能但使用模块的 playbook。大多数操作与我们在前一节讨论过的相同。新操作在最后; 实际上,在这种情况下,我们将生成的 RPM 文件复制到特定文件夹,我们调用createrepo在该文件夹中生成一个仓库,然后强制所有 Web 服务器更新生成的软件包至最新版本。

为确保应用程序的安全性,重要的是仓库仅在内部可访问,而不是公开的。

现在我们可以用以下命令运行 playbook:

ansible-playbook -i inventory/production playbooks/manual/rpm_deploy.yaml 

我们期望得到类似以下的结果。完整代码输出在 GitHub 上找到:

PLAY [builders] ******************************************************

TASK [setup] *********************************************************
ok: [builder01.fale.io]

TASK [Copy SPEC file to user folder] *********************************
changed: [builder01.fale.io]

TASK [Ensure rpmbuild exists] ****************************************
changed: [builder01.fale.io]

TASK [Ensure rpmbuild/SOURCES exists] ********************************
changed: [builder01.fale.io]

TASK [Download the sources] ******************************************
changed: [builder01.fale.io]

TASK [Ensure no SRPM files are present] ******************************
changed: [builder01.fale.io]

TASK [Build the SRPM file] *******************************************
changed: [builder01.fale.io]
...

使用 CI/CD 流水线构建 RPM 软件包

虽然本书未涉及此内容,但在更复杂的情况下,您可能希望使用 CI/CD 流水线来创建和管理 RPM 软件包。这两个主要的流水线基于两种不同类型的软件:

  • Koji

  • Jenkins

Koji 软件由 Fedora 社区和 Red Hat 开发。它根据 LGPL 2.1 许可证发布。这是目前由 Fedora、CentOS 以及许多其他公司和社区用来创建所有他们的 RPM 软件包(包括官方测试,也称为临时构建)的流水线。 Koji 默认情况下不会由提交触发;需要通过用户(通过 Web 界面或 CLI)进行手动调用。 Koji 将自动从 Git 下载 SPEC 文件的最新版本,从侧边缓存(这是可选的,但建议的)或原始位置下载源代码,并触发模拟构建。 Koji 仅支持模拟构建,因为它是唯一支持一致和可重复构建的系统。 Koji 可以永久存储所有输出的构件或根据配置的设置存储一段时间。这是为了确保非常高的审计级别。

Jenkins 是最常用的 CI/CD 管理器之一,也可以用于 RPM 流水线。其主要缺点是需要从头开始配置,这意味着需要更多的时间,但这意味着它具有更高的灵活性。此外,Jenkins 的一个重大优势是许多公司已经有了 Jenkins 实例,这使得设置和维护基础设施更容易,因为您可以重用您已经拥有的安装,因此您总体上不必管理较少的系统。

使用 RPM 打包构建编译软件

RPM 打包对于非二进制应用程序非常有用,并且对于二进制应用程序几乎是必需的。这也是因为非二进制和二进制情况之间的复杂性差异非常小。事实上,构建和安装将以完全相同的方式工作。唯一会改变的是 SPEC 文件。

让我们看看编写一个简单的用 C 编写的Hello World!应用程序所需的 SPEC 文件:

%global commit0 7c288b9d80a6ef525c0cca8a744b32e018eaa386 
%global shortcommit0 %(c=%{commit0}; echo ${c:0:7}) 

Name:           hello-world 
Version:        1.0 
Release:        1%{?dist} 
Summary:        Hello World example implemented in C 

License:        GPLv3+ 
URL:            https://github.com/Fale/hello-world 
Source0:        %{url}/archive/%{commit0}.tar.gz#/%{name}-%{shortcommit0}.tar.gz 

BuildRequires:  gcc 
BuildRequires:  make 

%description 
The description for our Hello World Example implemented in C 

%prep 
%autosetup -n %{name}-%{commit0} 

%build 
make %{?_smp_mflags} 

%install 
%make_install 

%files 
%license LICENSE 
%{_bindir}/hello 

%changelog 
* Sun Feb 24 2019 Fabio Alessandro Locati - 1.0-1 
- Initial packaging

正如你所看到的,这与我们在 PHP 演示应用程序中看到的非常相似。让我们看看其中的区别。

让我们稍微深入了解 SPEC 文件的各个部分:

%global commit0 7c288b9d80a6ef525c0cca8a744b32e018eaa386 
%global shortcommit0 %(c=%{commit0}; echo ${c:0:7}) 

正如你所看到的,我们没有禁用调试包的行。每次打包编译应用程序时,都应该让rpm创建调试符号包,这样在崩溃的情况下,调试和理解问题会更容易。

SPEC 文件的以下部分显示在这里:

Name:           hello-world 
Version:        1.0 
Release:        1%{?dist} 
Summary:        Hello World example implemented in C 

License:        GPLv3+ 
URL:            https://github.com/Fale/hello-world 
Source0:        %{url}/archive/%{commit0}.tar.gz#/%{name}-%{shortcommit0}.tar.gz 

正如你所看到的,此部分的更改仅是由于新包具有不同的名称和URL,但它们与这是一个可编译应用程序的事实无关联:

BuildRequires:  gcc 
BuildRequires:  make

在非编译应用程序中,我们不需要任何构建时存在的软件包,而在这种情况下,我们将需要makegcc(编译器)应用程序。不同的应用程序可能需要不同的工具和/或库在构建时存在于系统中:

%description 
The description for our Hello World Example implemented in C 

%prep 
%autosetup -n %{name}-%{commit0} 

%build 
make %{?_smp_mflags} 

description是特定于包的,并不受包的编译影响。同样,%prep阶段也是如此。

%build阶段,我们现在必须制作%{?_smp_mflags}。这是需要告诉rpmbuild实际运行make来构建我们的应用程序。_smp_mflags变量将包含一组参数,以优化编译为多线程:

%install 
%make_install 

%install阶段,我们将发出%make_install命令。此宏将调用make install,并带有一组额外的参数,以确保库位于正确的文件夹中,以及二进制文件等等:

%files 
%license LICENSE 
%{_bindir}/hello 

在这种情况下,我们只需要将hello二进制文件放置在%install阶段的buildroot正确文件夹中,并添加包含许可证的LICENSE文件:

%changelog 
* Sun Feb 24 2019 Fabio Alessandro Locati - 1.0-1 
- Initial packaging 

%changelog与我们看到的其他 SPEC 文件非常相似,因为它不受编译的影响。

完成后,您可以将其放在 spec/hello-world.spec 中,并通过以下代码段将 playbooks/manual/rpm_deploy.yaml 调整为 playbooks/manual/hello_deploy.yaml 保存。全部代码可在 GitHub 上找到:

- hosts: builders 
  user: vagrant 
  tasks: 
    - name: Copy SPEC file to user folder 
      copy: 
        src: ../../spec/hello-world.spec 
        dest: /home/ansible 
    - name: Ensure rpmbuild exists 
      file: 
        name: ~/rpmbuild 
        state: directory 
    - name: Ensure rpmbuild/SOURCES exists 
      file: 
        name: ~/rpmbuild/SOURCES 
        state: directory 
    ...

正如您所见,我们唯一更改的是所有对 demo-php-app 的引用都替换为 hello-world。使用以下命令运行它:

ansible-playbook -i inventory/production playbooks/manual/hello_deploy.yaml

我们将得到以下结果。全部代码输出可在 GitHub 上找到:

PLAY [builders] ******************************************************

TASK [setup] *********************************************************
ok: [builder01.fale.io]

TASK [Copy SPEC file to user folder] *********************************
changed: [builder01.fale.io]

TASK [Ensure rpmbuild exists] ****************************************
ok: [builder01.fale.io]

TASK [Ensure rpmbuild/SOURCES exists] ********************************
ok: [builder01.fale.io]

TASK [Download the sources] ******************************************
changed: [builder01.fale.io]

TASK [Ensure no SRPM files are present] ******************************
changed: [builder01.fale.io]

TASK [Build the SRPM file] *******************************************
changed: [builder01.fale.io]

TASK [Execute mock] **************************************************
changed: [builder01.fale.io]

...

您最终可以创建一个接受要构建的包的名称作为参数的 Playbook,这样您就不需要为每个包创建不同的 Playbook。

部署策略

我们已经看到如何在您的环境中分发软件,所以现在,我们将谈论部署策略,即如何升级您的应用程序而不会使您的服务受到影响。

在更新期间可能遇到三种不同的问题:

  • 更新推出期间的停机时间。

  • 新版本存在问题。

  • 新版本似乎工作正常,直到它失败。

第一个问题是每个系统管理员都知道的。在更新期间,您可能会重新启动一些服务,而在服务启动和结束之间的时间内,您的应用程序将无法在该机器上使用。为了解决这个问题,您需要具有智能负载均衡器的多台机器,该负载均衡器将在执行特定节点的升级之前,从可用节点池中删除指定节点,然后在节点升级后尽快将它们添加回去。

第二个问题可以通过多种方式预防。最清洁的方法是在 CI/CD 流水线中进行测试。事实上,这些问题很容易通过简单的测试找到。我们即将看到的方法也可以预防这种情况。

第三个问题迄今为止是最复杂的。许多次,甚至是全球范围内的问题,都是由这些问题引起的。通常,问题在于新版本存在一些性能问题或内存泄漏。由于大多数部署是在服务器负载最轻的时期完成的,一旦负载增加,性能问题或内存泄漏可能会导致服务器崩溃。

要能够正确使用这些方法,您必须能够确保您的软件可以接受回滚。有些情况下这是不可能的(即,在更新中删除了一个数据库表)。我们不会讨论如何避免这种情况,因为这是开发策略的一部分,与 Ansible 无关。

为了解决这些问题,通常使用两种常见的部署模式:金丝雀部署蓝绿部署

金丝雀部署

金丝雀部署是一种技术,涉及将你的一小部分机器(通常为 5%)更新到新版本,并指示负载均衡器仅将等量的流量发送到它。这有几个优点:

  • 在更新期间,你的容量永远不会低于 95%

  • 如果新版本完全失败,你会损失 5% 的容量。

  • 由于负载均衡器在新旧版本之间分配流量,如果新版本有问题,只有你的用户的 5% 将看到问题。

  • 只需要比预期负载多 5% 的容量

金丝雀部署能够避免我们提到的所有三个问题,而且额外开销很小(5%),并且在回滚时成本低廉(5%)。因此,许多大型公司都广泛使用这种技术。通常,为了确保用户在相近地理位置的体验相似,会根据地理位置选择用户是使用旧版本还是新版本。

当测试看起来成功时,可以逐步增加百分比,直到达到 100%。

可以以多种方式在 Ansible 中实现金丝雀部署。我建议的方式是最干净的方式,即使用清单文件,这样你会有如下内容:

[web-main] 
ws[00:94].fale.io 

[web-canary] 
ws[95:99].fale.io 

[web:children] 
web-main 
web-canary

通过这种方式,你可以在 web 组上设置所有变量(变量将是相同的,无论是什么版本的操作系统,或者至少应该是相同的),但你可以很容易地对金丝雀组、主要组或同时对两个组运行 playbook。另一个选项是创建两个不同的清单文件,一个用于金丝雀组,另一个用于主要组,组的名称相同,以便共享变量。

蓝/绿部署

蓝/绿部署与金丝雀部署非常不同,它有一些优点和一些缺点。主要优点如下:

  • 更容易实现

  • 允许更快的迭代

  • 所有用户同时转移

  • 回滚不会有性能下降

缺点中,主要的是需要比应用程序所需的机器多一倍。如果应用程序在云上运行(无论是私有、公共还是混合),这个缺点可以很容易地缓解,为部署扩展应用程序资源,然后再缩减它们。

在 Ansible 中实现蓝/绿部署非常简单。最简单的方法是创建两个不同的清单(一个用于蓝色,一个用于绿色),然后简单地管理你的基础设施,就像它们是不同的环境,如生产、暂存、开发等。

优化

有时,Ansible 感觉很慢,主要是因为要执行一个非常长的任务列表和/或有大量的机器。有多种原因和方法可以避免这种情况,我们将看一下其中的三种方式。

流水线

Ansible 默认较慢的原因之一是,对于每个模块的执行和每个主机,Ansible 将执行以下操作:

  • SSH 握手

  • 执行任务

  • 关闭 SSH 连接

正如你所看到的,这意味着如果你有 10 个任务要在单个远程服务器上执行,Ansible 将会打开(并关闭)10 次连接。由于 SSH 协议是一种加密协议,这使得 SSH 握手过程变得更长,因为两个部分必须每次都要协商密码。

Ansible 允许我们通过在 playbook 开始时初始化连接并在整个执行过程中保持连接处于活动状态来大幅减少执行时间,这样就不需要在每个任务中重新打开连接。在 Ansible 的发展过程中,这个特性已经多次改名,以及启用方式也有所变化。从 1.5 版本开始,它被称为pipelining,启用它的方式是在你的 ansible.cfg 文件中添加以下行:

pipelining=True 

这个功能默认没有启用的原因是许多发行版都带有 sudo 中的 requiretty 选项。Ansible 中的 pipelining 模式和 sudo 中的 requiretty 选项会冲突,并且会导致你的 playbook 失败。

如果你想要启用 pipelining 模式,请确保你的目标机器上已禁用了 sudo requiretty 模式。

使用 with_items 进行优化

如果你想要多次执行类似的操作,可以多次使用相同的任务并带有不同的参数,或者使用 with_items 选项。除了使你的代码更易于阅读和理解之外,with_items 还可以提高你的性能。一个例子是在安装软件包(即 aptdnfyumpackage 模块)时,如果使用 with_items,Ansible 将执行一个命令,而不是如果不使用则为每个软件包执行一个命令。你可以想象,这可以帮助提高你的性能。

理解任务执行时发生的情况

即使你已经实施了我们刚刚讨论过的加快 playbook 执行速度的方法,你可能仍然会发现一些任务需要很长时间。即使对许多其他模块来说可能是可能的,对一些任务来说这是非常普遍的。通常会给你带来这种问题的模块如下:

  • 包管理(即 aptdnfyumpackage

  • 云机器创建(即 digital_oceanec2

这种慢的原因通常不是特定于 Ansible 的。一个示例情况可能是,如果你使用了一个包管理模块来更新你的机器。这需要在每台机器上下载几十甚至几百兆的软件并安装大量软件。加快这种操作的方法是在你的数据中心中拥有一个本地仓库,并让所有的机器指向它,而不是你的发行版仓库。这将允许你的机器以更高的速度下载,并且不使用通常带宽有限或计量的公共连接。

了解模块在后台执行的操作对优化 playbook 的执行至关重要。

在云机器创建的情况下,Ansible 只需向所选的云提供商执行 API 调用,并等待机器准备就绪。DigitalOcean 的机器可能需要长达一分钟才能创建(其他云可能需要更长时间),因此 Ansible 将等待该时间。一些模块具有异步模式,以避免此等待时间,但您必须确保机器准备就绪后才能使用它;否则,使用创建的机器的模块将失败。

总结

在本章中,我们看到了如何使用 Ansible 部署应用程序,以及您可以使用的各种分发和部署策略。我们还看到了如何使用 Ansible 创建 RPM 包以及如何使用不同的方法优化 Ansible 的性能。

在下一章中,我们将学习如何在 Windows 机器上使用 Ansible,以及如何找到其他人编写的角色并如何使用它们,还有一个用于 Ansible 的用户界面。

第四部分:使用 Ansible 部署应用

本节解释了如何从 Ansible 管理 Windows 节点以及如何利用 Ansible Galaxy 和 Ansible Tower 来最大化您的生产力。

本节包含以下章节:

  • 第十章,为企业介绍 Ansible

  • 第十一章,开始使用 AWX

  • 第十二章,与 AWX 用户、权限和组织合作

为企业介绍 Ansible

在前一章中,我们看到了 Ansible 的工作原理以及如何利用它。到目前为止,我们一直在假定我们的目标是 Unix 机器,我们将自己编写所有 playbook,并且 Ansible CLI 是我们要寻找的。现在我们将摆脱这些假设,看看如何超越典型的 Ansible 用法。

在本章中,我们将探讨以下主题:

  • Windows 上的 Ansible

  • Ansible Galaxy

  • Ansible Tower

技术要求

除了 Ansible 本身之外,为了能够在您的机器上按本章示例操作,您需要一个 Windows 机器。

Windows 上的 Ansible

Ansible 版本 1.7 开始能够通过一些基本模块管理 Windows 机器。在 Ansible 被 Red Hat 收购后,Microsoft 和许多其他公司以及个人都为此付出了大量努力。到 2.1 版本发布之时,Ansible 管理 Windows 机器的能力已接近完整。一些模块已扩展以在 Unix 和 Windows 上无缝工作,而在其他情况下,Windows 逻辑与 Unix 有很大差异,因此需要创建新的模块。

在撰写本文时,尚不支持将 Windows 作为控制机器,尽管某些用户已调整了代码和环境使其能够运行。

从控制机器到 Windows 机器的连接不是通过 SSH 进行的;而是通过Windows 远程管理WinRM)进行的。您可以访问微软的网站以获取详细解释和实施方法:msdn.microsoft.com/en-us/library/aa384426(v=vs.85).aspx

在控制机器上,一旦安装了 Ansible,重要的是安装 WinRM。您可以通过以下命令使用 pip 安装:

pip install "pywinrm>=0.3.0"  

您可能需要使用 sudoroot 帐户来执行此命令。

在每台远程 Windows 机器上,您需要安装 PowerShell 版本 3.0 或更高。Ansible 提供了一些有用的脚本来设置它:

您还需要通过防火墙允许端口5986,因为这是默认的 WinRM 连接端口,并确保它可以从命令中心访问。

为确保可以远程访问服务,请运行 curl 命令:

curl -vk -d `` -u "$USER:$PASSWORD" "https://<IP>:5986/wsman".  

如果基本身份验证可用,则可以开始运行命令。设置完成后,您就可以开始运行 Ansible!让我们通过运行 win_ping 来运行 Ansible 中 Windows 版本的 Hello, world! 程序的等效程序。为此,让我们设置我们的凭据文件。

可以使用 ansible-vault 完成此操作,如下所示:

$ ansible-vault create group_vars/windows.yml  

正如我们已经看到的,ansible-vault 会要求您设置 password

Vault password:
Confirm Vault password:  

此时,我们可以添加我们需要的变量:

ansible_ssh_user: Administrator 
ansible_ssh_pass: <password> 
ansible_ssh_port: 5986 
ansible_connection: winrm 

让我们设置我们的 inventory 文件,如下所示:

[windows] 
174.129.181.242 

在此之后,让我们运行 win_ping

ansible windows -i inventory -m win_ping --ask-vault-pass  

Ansible 将要求我们输入 Vault 密码,然后打印运行结果,如下所示:

Vault password: 
174.129.181.242 | success >> { 
    "changed": false, 
    "ping": "pong" 
} 

我们已经看到了如何连接到远程计算机。现在,您可以以与管理 Unix 计算机相同的方式管理 Windows 计算机。需要注意的是,由于 Windows 操作系统和 Unix 系统之间存在巨大差异,不是每个 Ansible 模块都能正常工作。因此,许多 Unix 模块已经被从头开始重写,以具有与 Unix 模块相似的行为,但具有完全不同的实现方式。这些模块的列表可以在 docs.ansible.com/ansible/latest/modules/list_of_windows_modules.html 找到。

Ansible Galaxy

Ansible Galaxy 是一个免费网站,您可以在该网站上下载由社区开发的 Ansible 角色,并在几分钟内启动自动化。您可以分享或审查社区角色,以便其他人可以轻松找到 Ansible Galaxy 上最值得信赖的角色。您可以通过简单地注册 Twitter、Google 和 GitHub 等社交媒体应用程序,或者在 Ansible Galaxy 网站 galaxy.ansible.com/ 上创建新帐户,并使用 ansible-galaxy 命令下载所需的角色,该命令随 Ansible 版本 1.4.2 及更高版本一起提供。

如果您想要托管自己的本地 Ansible Galaxy 实例,可以通过从 github.com/ansible/galaxy 获取代码来实现。

要从 Ansible Galaxy 下载 Ansible 角色,请使用以下命令:

ansible-galaxy install username.rolename  

您也可以按照以下步骤指定版本:

ansible-galaxy install username.rolename[,version]  

如果您不指定版本,则 ansible-galaxy 命令将下载最新可用的版本。您可以通过以下两种方式安装多个角色;首先,通过将多个角色名称用空格分隔,如下所示:

ansible-galaxy install username.rolename[,version] username.rolename[,version]  

其次,您可以通过在文件中指定角色名称,并将该文件名传递给 -r/--role-file 选项来完成此操作。例如,您可以创建以下内容的 requirements.txt 文件:

user1.rolename,v1.0.0 
user2.rolename,v1.1.0 
user3.rolename,v1.2.1 

您可以通过将文件名传递给 ansible-galaxy 命令来安装角色,如下所示:

ansible-galaxy install -r requirements.txt  

让我们看看如何使用 ansible-galaxy 下载 Apache HTTPd 的角色:

ansible-galaxy install geerlingguy.apache  

您将看到类似以下内容的输出:

- downloading role 'apache', owned by geerlingguy
- downloading role from https://github.com/geerlingguy/ansible-role-apache/archive/3.0.3.tar.gz
- extracting geerlingguy.apache to /home/fale/.ansible/roles/geerlingguy.apache
- geerlingguy.apache (3.0.3) was installed successfully

前述的 ansible-galaxy 命令将把 Apache HTTPd 角色下载到 ~/.ansible/roles 目录中。您现在可以直接在您的 playbook 中使用前述的角色,并创建 playbooks/galaxy.yaml 文件,并填写以下内容:

- hosts: web 
  user: vagrant 
  become: True 
  roles: 
    - geerlingguy.apache 

如您所见,我们创建了一个带有 geerlingguy.apache 角色的简单 playbook。现在我们可以测试它:

ansible-playbook -i inventory playbooks/galaxy.yaml 

这应该给我们以下输出:

PLAY [web] ***********************************************************

TASK [Gathering Facts] ***********************************************
ok: [ws01.fale.io]

TASK [geerlingguy.apache : Include OS-specific variables.] ***********
ok: [ws01.fale.io]

TASK [geerlingguy.apache : Include variables for Amazon Linux.] ******
skipping: [ws01.fale.io]

TASK [geerlingguy.apache : Define apache_packages.] ******************
ok: [ws01.fale.io]

TASK [geerlingguy.apache : include_tasks] ****************************
included: /home/fale/.ansible/roles/geerlingguy.apache/tasks/setup-RedHat.yml for ws01.fale.io

TASK [geerlingguy.apache : Ensure Apache is installed on RHEL.] ******
changed: [ws01.fale.io]

TASK [geerlingguy.apache : Get installed version of Apache.] *********
ok: [ws01.fale.io]

...

正如您可能已经注意到的,由于该角色设计用于在许多不同的 Linux 发行版上工作,因此跳过了许多步骤。

现在您知道如何利用 Ansible Galaxy 角色,您可以花更少的时间重写其他人已经写过的代码,并花更多的时间编写对您的架构特定且给您带来更多价值的部分。

将角色推送到 Ansible Galaxy

由于 Ansible Galaxy 是社区驱动的工作,您还可以将自己的角色添加到其中。在我们可以开始发布它的流程之前,我们需要对其进行准备。

Ansible 为我们提供了一个工具,可以从模板中引导一个新的 Galaxy 角色。为了利用它,我们可以运行以下命令:

ansible-galaxy init ansible-role-test

这将创建 ansible-role-test 文件夹,以及通常具有的所有文件夹的 Ansible 角色。

唯一对您新的文件将是 meta/main.yaml,即使没有 Ansible Galaxy 也可以使用,但包含了很多关于角色的信息,这些信息可被 Ansible Galaxy 读取。

可用于设置的主要信息在该文件中都可以找到,以满足您的需求,如下所示:

  • author:您的名字。

  • description:在此处放置角色的描述。

  • company:在此处放置您所工作公司的名称(或删除该行)。

  • license:设置您的模块将具有的许可证。一些建议的许可证包括 BSD(也是默认的),MIT,GPLv2,GPLv3,Apache 和 CC-BY。

  • min_ansible_version:设置您已测试过角色的最低 Ansible 版本。

  • galaxy_tags:在此部分中,放置您的模块适用的平台和版本。

  • dependencies:列出执行您的角色所需的角色。

要进行发布,您需要使用 GitHub 账户登录 Galaxy,然后您可以转到“我的内容”开始添加内容。

按下“添加内容”后,将会出现一个窗口,其中显示您可以选择的存储库,如下截图所示:

在选择正确的存储库后,然后点击“确定”按钮,Ansible Galaxy 将开始导入给定的角色。

如果您在执行此操作几分钟后返回到“我的内容”页面,您将看到您的角色及其状态,如下所示:

您现在可以像其他人一样使用该角色。记得在需要更改时更新它!

Ansible Tower 和 AWX

Ansible Tower 是由 Red Hat 开发的基于 Web 的 GUI。Ansible Tower 提供了一个易于使用的仪表板,您可以在其中管理节点和基于角色的身份验证以控制对 Ansible Tower 仪表板的访问。Ansible Tower 的主要特点如下:

  • LDAP/AD 集成:您可以基于 Ansible Tower 对 LDAP/AD 服务器执行的查询结果导入(并授予权限给)用户。

  • 基于角色的访问控制:它限制用户只能运行他们被授权运行的 Playbook,并/或者仅针对有限数量的主机。

  • REST API:所有 Ansible Tower 的功能都通过 REST API 暴露出来。

  • 作业调度:Ansible Tower 允许我们调度作业(Playbook 执行)。

  • 图形化清单管理:Ansible Tower 对清单的管理方式比 Ansible 更加动态。

  • 仪表盘:Ansible Tower 允许我们查看所有当前和之前作业执行的情况。

  • 日志记录:Ansible Tower 记录每次作业执行的所有结果,以便在需要时进行查看。

在 Red Hat 收购 Ansible Inc. 期间,承诺过将使 Ansible Tower 成为开源项目。2017 年,这一承诺得以实现,并且以 AWX 的名字回归。

AWX 和 Ansible Tower 在企业版中经常被使用,因为它为 Ansible 生态系统提供了非常方便的功能。我们将在接下来的章节中更详细地讨论这些功能。

概要

在本章中,我们已经了解了如何通过查看如何控制 Windows 主机将 Ansible 移出 Unix 世界。然后我们转向 Ansible Galaxy,在那里您可以找到许多其他人编写的角色,您可以简单地重用。最后,我们提到了 Ansible Tower,它是 AWX 的开源化身。在接下来的章节中,我们将更多地讨论关于 AWX 的内容,从安装过程到运行您的第一个作业。

开始使用 AWX

正如我们在前面的章节中所看到的,Ansible 是一个非常强大的工具。但这还不足以使其无处不在。事实上,要使一个工具无处不在,它需要在任何用户级别上都易于使用,并且易于以各种方式与现有环境集成。

Ansible 公司认识到了这一点,并创建了一个名为 Ansible Tower 的工具,它基本上是围绕 Ansible 构建的 Web UI 和 API 集。 Ansible Tower 是一个闭源工具,也是该公司的主要收入来源。 当红帽公司宣布收购 Ansible 时,其管理层也承诺将 Ansible Tower 开源化。 几年后,红帽公司开源了 Ansible Tower,创建了 AWX 项目,它现在是 Ansible Tower 的上游项目,就像 Fedora 是 Red Hat Enterprise Linux 的上游项目一样。

在 AWX 之前,开源社区中还开发了其他 Web UI 和 API 集,例如 Semaphore。 AWX 和 Ansible Tower 并不是今天 Ansible 的唯一 Web UI 和 API 集,但它们是更活跃的解决方案。

在本章中,我们将看到如何设置 AWX 并学习如何使用它。 更具体地说,我们将讨论以下内容:

  • 设置 AWX

  • 理解 AWX 项目是什么以及如何利用它

  • 理解 AWX 清单是什么以及与 Ansible 清单的区别

  • 理解 AWX 作业模板是什么以及如何创建一个

  • 理解 AWX 作业是什么以及如何执行您的第一个作业

技术要求

对于本章,您需要一台可以运行 ansibledocker 并且已安装 docker-py 的机器。

设置 AWX

与 Ansible 不同,安装 AWX 不仅涉及一个单一命令,但仍然相当快速和简单。

首先,您需要安装 ansibledockerdocker-py。之后,您需要给所需用户运行 Docker 的权限。最后,您需要下载 AWX Git 仓库并执行一个 ansible playbook。

在 Fedora 中安装 Ansible、Docker 和 Docker-py

让我们从在 Fedora 中安装 dockeransibledocker-py 包开始:

sudo dnf install ansible docker python-docker-py

要启动并启用 Docker 服务,请使用以下命令:

sudo systemctl start docker
sudo systemctl enable docker

现在我们已经安装了 ansibledockerdocker-py,让我们继续授予用户访问 Docker 的权限。

在 Fedora 中给当前用户授权使用 Docker

为确保当前用户可以使用 Docker(默认情况下,Fedora 仅允许 root 使用它),您需要创建一个新的 Docker 组,将当前用户分配到其中,并重新启动 Docker:

sudo groupadd docker && sudo gpasswd -a ${USER} docker && sudo systemctl restart docker

由于组只在会话开始时分配,所以您需要重新启动您的会话,但我们可以通过执行以下命令来强制 Linux 将新组添加到当前会话中:

newgrp docker

现在我们已经准备好了所有的先决条件,我们可以开始真正的 AWX 安装了。

安装 AWX

我们首先需要做的是通过执行以下命令来检出 git 代码库:

git clone https://github.com/ansible/awx.git

一旦 Git 完成了它的任务,我们就可以将目录更改为包含安装程序的目录并运行它:

cd awx/installer/
ansible-playbook -i inventory install.yml

这将在 Docker 容器和默认配置中安装 AWX。您可以通过更改相同文件夹中的inventory文件来调整配置(在运行最后一个命令之前)。

安装过程完成后,您可以打开浏览器,并指向https://localhost,然后使用admin用户名和password密码登录。

登录后,您应该会看到类似以下的页面:

设置了 AWX 后,您现在将能够执行 Ansible playbooks 而不再使用 Ansible CLI。要开始这个过程,我们首先需要一个项目,所以让我们看看如何设置它。

创建新的 AWX 项目

AWX 假设您已经将您的 playbooks 保存在某个地方,为了能够在 AWX 中使用它们,我们需要创建一个项目。

项目基本上是包含 Ansible 资源(角色和 playbooks)的存储库的 AWX 占位符。

当您进入项目部分时,在左侧菜单栏中,您将看到类似以下的内容:

如您所见,演示项目已经就位(安装程序为我们创建了它!)并且由一个 Git 存储库支持。

项目名称的左侧有一个白色圆圈,表示该特定项目尚未被拉取。如果有一个绿色圆圈,意味着项目已成功拉取。脉动的绿色圆圈表示拉取正在进行中,而红色停止标志表示出现了问题。

在项目的同一行,有三个按钮:

  • 获取 SCM 最新修订版本:获取代码的当前最新版本

  • 复制:创建项目的副本

  • 删除:删除项目

在卡片的右上角,您可以看到一个绿色的加号按钮。这是一个允许我们添加更多项目的按钮。

通过选择它,一个新的新项目卡片将出现在项目卡片的顶部,您可以在其中添加新项目。

新项目卡片将如下所示:

它正在请求有关您要创建的项目的信息:

  • 名称:这是您项目的显示名称。这是为了人类使用,所以要做到人性化!

  • 描述:一个额外的显示(仍然是给人看的),以理解项目目标。

  • 组织:将拥有该项目的组织。这将在下一章中介绍。现在,让我们保持默认设置。

  • SCM 类型:您的代码所包含的 SCM 类型。在撰写本文时,受支持的选项有:手动、Git、Mercurial、Subversion 和 Red Hat Insights。

  • 根据您选择的 SCM 类型,将出现更多字段,例如 SCM URL 和 SCM 分支。

当您填写完所有必填字段后,您可以保存并看到已添加一个新项目。

使用 AWX 清单

AWX 清单是 AWX 世界中 Ansible 清单的等价物。由于 AWX 是一个图形工具,清单不像 Ansible 中那样存储为文件(如在 Ansible 中所做),而是可通过 AWX 用户界面进行管理。不绑定到文件还使 AWX 清单相对于 Ansible 清单具有更多的灵活性。

AWX 有不同的方式来管理清单。

您可以通过点击左侧菜单上的 Inventories 项目来查看,您将找到类似于此的内容:

至于项目,AWX 自带演示清单。

从左到右看,我们可以找到以下列:

  • 云符号 - 用于清单同步状态

  • 显示状态(正常或失败)的常规圆圈

  • 清单名称

  • 清单类型

  • 拥有清单的组织

  • 编辑符号

  • 复制符号

  • 删除符号

与之前一样,绿色 + 按钮将允许您创建新项目。点击它,它会询问您想要创建清单还是智能清单。

我们现在可以选择 Inventories 选项,它将允许您添加名称和组织(仅两个强制选项)以及其他非强制选项。一旦保存,您将能够添加主机、组和权限。

如果您不愿手工指定主机、组、变量等,还有一个 Sources 标签可供您使用。

点击 Sources 标签上的 +,您将能够从可用类型列表或使用自定义脚本添加来源。

撰写时可用的来源类型如下:

  • 从项目中获取:基本上,它将从存储库导入一个 Ansible 核心清单文件。

  • 亚马逊 EC2:它将使用 AWS API 来发现在您的环境中运行的所有 EC2 机器及其特性。

  • 谷歌计算引擎(GCE):它将使用 Google API 来发现您环境中运行的所有 GCE 机器及其特性。

  • Microsoft Azure 资源管理器:它将使用 Azure API 来发现在您的环境中运行的所有机器及其特性。

  • VMWare vCenter:它将使用 VMWare API 来发现由您的 vCenter 管理的所有机器及其特性。

  • 红帽 Satellite 6:它将使用卫星 API 来发现由您的卫星管理的所有机器及其特性。

  • 红帽 CloudForms:它将使用 CloudForms API 来发现由其管理的所有机器及其特性。

  • OpenStack:它将使用 OpenStack API 来发现在您的 OpenStack 环境中运行的所有机器及其特性。

  • 红帽虚拟化:它将使用 RHEV API 来发现所有正在运行的机器及其特性。

  • Ansible Tower:它将使用另一个 Ansible Tower/AWX 安装 API 来发现其管理的所有机器及其特性。

  • 自定义脚本:它将使用您在清单脚本部分上传的脚本。

我们现在已经看到如何设置 AWX 清单,这将在下一部分中需要:设置 AWX 作业模板。

理解 AWX 作业模板

在 AWX 中,我们有一个作业模板的概念,它基本上是对 playbook 的封装。

要管理作业模板,您必须转到左侧菜单中的“模板”部分,然后会发现类似以下内容:

查看包含作业模板的表格,我们会找到以下内容:

  • 作业模板名称

  • 模板类型(AWX 还支持工作流模板,这是一组作业模板的模板)

  • 火箭按钮

  • 复制按钮

  • 删除按钮

通过点击火箭按钮,我们可以执行它。这样做会自动将您带入到不同的视图中,在下一节中我们会发现。

使用 AWX 作业

AWX 作业是 AWX 作业模板的执行,就像 Ansible 运行是 Ansible playbooks 的执行一样。

当您启动一个作业时,您会看到一个窗口,就像下面这个:

这是在命令行上运行 Ansible 时的 AWX 版本的输出。

几秒钟后,在右侧的灰色框中,一个非常熟悉的输出将开始弹出,因为它完全相同于 Ansible 的stdout,只是重定向到那里。

如果稍后您在左侧菜单栏上点击“作业”,您会发现自己处于一个不同的屏幕上,列出了所有先前运行的作业:

正如您所注意到的,我们有两个已经执行的作业,而我们只执行了演示作业模板。这是因为在演示作业模板执行之前已经拉取了演示项目。这使得操作员始终可以放心地运行作业,知道它将始终是 SCM 中可用的最新版本要执行的作业。

摘要

在本章中,您已经学会了如何在 Fedora 上设置 AWX,并学会了使用 AWX 项目、清单、作业模板和作业。正如您可以想象的那样,由于 AWX 中存在的选项、标志和项目数量,这只是冰山一角,并不打算对其进行完整的解释,因为需要一个专门的书籍来解释。

在接下来的章节中,我们将稍微讨论一下 AWX 用户、用户权限和组织。

与 AWX 用户、权限和组织一起工作

在阅读上一章节时,您可能会对 AWX 的安全性产生疑问。

AWX 非常强大,并且要如此强大,它需要对目标机器有很多访问权限,这意味着它可能成为安全链中的一个潜在弱点。

在本章中,我们将讨论一些 AWX 用户、权限和组织的问题;具体来说,我们将涵盖以下主题:

  • AWX 用户和权限

  • AWX 组织

技术要求

为了完成本章,我们只需要 AWX,这是我们在上一章中设置的。

AWX 用户和权限

首先,如果您还记得第一次打开 AWX 时,您将记得您必须输入用户名和密码。

当然你可以想象,那些是默认凭据,但您可以创建组织所需的所有用户。

为此,您可以转到左侧菜单中的用户部分,如下屏幕截图所示:

正如您可能期望的那样,管理员用户已存在,并且是唯一存在的用户。

我们可以通过点击带有+ 符号绿色按钮来创建其他用户。

当我们创建新用户时,需要填写以下字段:

  • 名字:这是用户的名字。

  • 姓氏:用户的姓氏。

  • 组织:用户所属的组织(我们稍后将在本章更多地讨论这个问题)。

  • 电子邮件:这是用户的电子邮件。

  • 用户名:这是用户的用户名。将用于登录,并将在用户界面中弹出。

  • 密码:这是用户的密码。

  • 确认密码:重新输入密码以确保没有拼写错误。

  • 用户类型:用户可以是普通用户、系统审计员或系统管理员。默认情况下,普通用户无法访问任何内容,除非明确授予权限。系统审计员可以以只读模式查看整个系统中的所有内容。系统管理员可以完全读写访问整个系统。

创建了一个普通用户后,您可以转至模板。如果您进入演示作业模板的编辑模式,您会注意到一个权限部分,您可以在其中查看和设置能够查看和操作此作业模板的用户。您应该看到以下屏幕截图中显示的内容:

通过点击带有+ 符号绿色按钮,将会出现一个模态框,您可以在其中选择(或搜索)要启用的用户,并选择访问级别,如下所示的屏幕截图:

AWX 允许您在三种不同的访问级别之间进行选择:

  • 管理员:这种类型的用户能够查看作业模板和以前使用它创建的作业,执行将来的作业模板,以及编辑作业模板。

  • 执行:这种用户可以看到作业模板和以前使用它创建的作业,并在未来执行作业模板,但不能编辑作业模板。

  • 读取:这种用户可以看到作业模板和以前使用它创建的作业,但不能执行它也不能更改它。

类似于作业模板可以被用户看到、使用和管理,AWX 中的所有其他对象都可以有权限。

如您所想象的那样,如果您开始有数十个作业和数十个用户,您将花费大量时间来管理权限。为了帮助您,AWX 提供了团队的概念。

团队可以在左侧菜单中的团队项中进行管理,基本上只是用户的分组,因此您可以从自主访问控制DAC)方法转变为基于角色的访问控制RBAC)方法,这样在组织变化和需求方面更快地跟上。

通过使用用户、团队和权限,您将能够以非常精细的级别决定谁能够做什么。

AWX 组织

在更复杂的组织中,经常出现很多来自非常不同团队和业务单元的人共享同一个 AWX 安装的情况。

在这些情况下,建立不同的 AWX 组织是有意义的。这可以更轻松地管理权限,并将一些权限管理委托给核心系统管理员团队之外的组织管理员。此外,组织允许垂直权限于组织资源,比如清单管理员(即,拥有该组织所有清单的自动管理员)或项目管理员(即,拥有该组织所有项目的自动管理员),除了组织范围的角色(比如组织管理员和组织审核员)。

如果您在一个有多个网站的公司,您可以决定将所有网站集群到同一个 AWX 组织中(如果它们是由同一批人管理的,比如,“web group”),或者您可以决定将它们分成多个 AWX 组织,每个网站一个。

这些组织带来的优势如下:

  • 更简单的权限管理

  • 团队经理(即,“web group” 管理员或单个网站管理员)能够随着时间推移招聘和解雇成员

  • 更容易和更快的审核,因为只需要审核与特定组织相关的权限,而不是 Tower 中的所有权限

凭借这些优势,我总是建议您考虑如何在 AWX 中使用 AWX 组织。

此外,根据我的经验,我始终注意到 AWX 组织结构与公司结构越相似,用户体验越好,因为对所有用户来说都会感觉自然。另一方面,如果你试图强行将 AWX 组织结构与公司结构完全不同,这将感觉陌生,会减慢 AWX 的采用速度,并且在某些情况下甚至可能导致平台失败。

总结

在这本书中,我们从一些非常基本的自动化概念开始,通过将 Ansible 与其他常见选项如手动流程、bash 脚本、Puppet 和 Chef 进行比较。然后,我们看了如何编写 YAML 文件,因为这是 Ansible 使用的格式,以及如何安装 Ansible。然后,我们进行了第一个由 Ansible 驱动的安装(基本的一对 HTTP 服务器,支持数据库服务器)。然后,我们添加了利用 Ansible 特性的功能,例如变量、模板和任务委派。接着,我们看到了 Ansible 如何在 AWS、Digital Ocean 和 Azure 等云环境中帮助您。然后,我们继续分析 Ansible 如何用于触发通知,以及在各种部署场景中的应用。最后,我们总结了官方 Ansible 图形界面的概述:AWX/Ansible Tower。

通过这个内容,现在你应该能够自动化你在使用 Ansible 过程中遇到的所有可能情景。

posted @ 2024-05-20 11:58  绝不原创的飞龙  阅读(14)  评论(0编辑  收藏  举报