Ansible-配置管理第二版-全-
Ansible 配置管理第二版(全)
原文:
annas-archive.org/md5/80dce39ac81b8756c5ce4c845d8ba3f1译者:飞龙
前言
自从 1993 年 Mark Burgess 创建了 CFEngine 以来,配置管理工具一直在不断发展。紧随其后的是 Puppet 和 Chef 等现代工具的出现,现在系统管理员可选择的工具种类繁多。
Ansible 是配置管理领域中新出现的工具之一。其他工具侧重于完整性和可配置性,而 Ansible 则打破了这一趋势,专注于简单性和易用性。
本书的目标是向你展示如何使用 Ansible,从它的命令行工具的基础开始,到编写 playbooks,再到管理大型复杂的环境。最后,我们将教你如何构建自己的模块并通过编写插件扩展 Ansible,增加新功能。
本书内容
第一章,Ansible 入门,教你 Ansible 的基础知识,如何在 Windows 和 Linux 上安装它,如何构建库存,如何使用模块,最重要的是,如何获取帮助。
第二章,简单 Playbooks,教你如何将多个模块结合起来创建 Ansible playbooks 来管理主机,同时也介绍了一些有用的模块。
第三章,高级 Playbooks,深入讲解 Ansible 的脚本语言,并教授更复杂的语言构造;在这里我们还解释了如何调试 playbooks。
第四章,更大规模的项目,教你如何将 Ansible 的配置扩展到大规模部署,使用多个复杂的系统,包括如何管理你可能用来配置系统的各种机密。
第五章,自定义模块,教你如何通过编写模块和插件将 Ansible 扩展到现有功能之外。
本书所需的内容
要使用本书,你至少需要以下内容:
- 
一款文本编辑器 
- 
一台运行 Linux 操作系统的计算机 
- 
Python 2.6.x 或 Python 2.7.x 
然而,为了充分发挥 Ansible 的作用,你应该至少拥有几台 Linux 机器进行管理。如果需要,你可以使用虚拟化平台来模拟多个主机。要使用 Windows 模块,你需要一台 Windows 机器作为被管理主机,并且需要一台 Linux 机器作为控制主机。
本书适用人群
本书面向希望了解 Ansible 工作原理基础的人。预计读者具备一定的 Linux 系统设置和配置知识。在本书的某些部分,我们将介绍 BIND、MySQL 以及其他 Linux 守护进程的配置文件;了解这些内容将非常有帮助,但并非必需。
约定
在本书中,你会发现一些文本样式,用以区分不同种类的信息。以下是这些样式的一些示例,以及它们的含义说明。
文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名会以如下方式显示:“这与使用vars_files指令的方式类似。”
一块代码会如下所示:
[group]
machine1
machine2
machine3
当我们希望引起你对某段代码块中特定部分的注意时,相关的行或项会以粗体显示:
tasks:
  - name: install apache
    action: yum name=httpd state=installed
  - name: configure apache
    copy: src=files/httpd.conf dest=/etc/httpd/conf/httpd.conf
所有命令行输入或输出都会如下所示:
ansible machinename -u root -k -m ping
新术语和重要词汇以粗体显示。你在屏幕上看到的、菜单或对话框中的词语例如,会以这样的方式出现在文本中:“点击下一步按钮会将你带到下一个屏幕。”
注意
警告或重要注释以这样的框显示。
提示
提示和技巧以这种方式出现。
读者反馈
我们始终欢迎读者的反馈。告诉我们你对本书的看法——你喜欢的部分或可能不喜欢的部分。读者反馈对我们来说很重要,它帮助我们开发出你能真正受益的书籍。
要向我们提供一般反馈,只需发送电子邮件至<feedback@packtpub.com>,并通过邮件主题提到书名。
如果你在某个领域具有专业知识,并且有兴趣撰写或贡献一本书,可以查看我们在www.packtpub.com/authors上的作者指南。
客户支持
现在你是一本 Packt 书籍的自豪拥有者,我们提供了许多帮助你充分利用购买的资源。
下载示例代码
你可以从你的账户在www.packtpub.com下载你购买的所有 Packt 书籍的示例代码文件。如果你在其他地方购买了本书,你可以访问www.packtpub.com/support并注册,将文件直接通过电子邮件发送给你。
勘误
尽管我们已经尽力确保内容的准确性,但错误仍然会发生。如果你在我们的书中发现错误——无论是文本还是代码中的错误——我们将非常感激你能报告这个问题。通过这样做,你可以帮助其他读者避免困扰,并帮助我们改进后续版本的书籍。如果你发现任何勘误,请访问www.packtpub.com/submit-errata报告,选择你的书籍,点击勘误提交表单链接,输入你的勘误详细信息。一旦你的勘误被验证,你的提交将被接受,并且勘误将被上传到我们的网站,或者添加到现有勘误列表中,列在该书籍的勘误部分。你可以通过选择你的书名访问www.packtpub.com/support查看现有勘误。
盗版
互联网上的版权盗版问题是所有媒体面临的持续性问题。在 Packt,我们非常重视版权和许可证的保护。如果您在互联网上发现我们作品的任何非法复制品,无论形式如何,请立即提供相关地址或网站名称,以便我们采取相应的措施。
请通过 <copyright@packtpub.com> 与我们联系,并提供涉嫌侵权材料的链接。
我们感谢您为保护我们的作者以及帮助我们为您提供有价值内容所做的贡献。
问题
如果您在书籍的任何方面遇到问题,您可以通过 <questions@packtpub.com> 与我们联系,我们会尽力解决您的问题。
第一章:开始使用 Ansible
Ansible 与目前市面上其他配置管理工具有着根本的不同。它的设计目的就是在几乎所有方面都让配置变得简单,从它简单的英文配置语法到易于设置的特点。你会发现,Ansible 让你不再需要编写自定义的配置和部署脚本,而是可以直接专注于你的工作。
Ansible 只需要在你用来管理基础设施的机器上安装。它不需要在被管理的机器上安装客户端,也不需要在使用之前设置任何服务器基础设施。你甚至可以在安装后几分钟内就开始使用它,正如我们在本章中将要展示的那样。
本章涵盖的主题如下:
- 
安装 Ansible 
- 
配置 Ansible 
- 
从命令行使用 Ansible 
- 
使用 Ansible 管理 Windows 机器 
- 
如何获取帮助 
所需的硬件和软件
你将从一台机器的命令行使用 Ansible,这台机器我们称之为 控制机器,然后使用它配置另一台机器,我们称之为 被管理机器。Ansible 目前只支持 Linux 或 OS X 控制机器;然而,被管理机器可以是 Linux、OS X、其他类 Unix 机器或 Windows。Ansible 对控制机器的要求不高,对被管理机器的要求更少。
控制机器的要求如下:
- 
Python 2.6 或更高版本 
- 
paramiko 
- 
PyYAML 
- 
Jinja2 
- 
httplib2 
- 
基于 Unix 的操作系统 
被管理的机器需要 Python 2.4 或更高版本以及 simplejson;然而,如果你的 Python 版本是 2.5 或更高,你只需要 Python。被管理的 Windows 机器需要开启 Windows 远程管理,并且 PowerShell 版本要大于 3.0。虽然 Windows 机器的要求更多,但所有这些工具都是免费的,Ansible 项目甚至包括了帮助你轻松设置依赖项的脚本。
安装方法
如果你想使用 Ansible 管理一组现有的机器或基础设施,你可能希望使用这些系统上附带的包管理器。这意味着你将随着分发版的更新而获得 Ansible 的更新,可能会落后于其他方法的几个版本。然而,这也意味着你将运行一个已经过测试并能在你使用的系统上正常工作的版本。
如果你正在运行一个现有的基础设施,但需要一个更新版本的 Ansible,你可以通过 pip 安装 Ansible。Pip 是一个用于管理 Python 软件包和库的工具。Ansible 的发布版本会在发布后立即推送到 pip,因此如果你使用的是最新的 pip,你应该总是运行最新版本。
如果你打算开发大量模块并可能为 Ansible 贡献代码,你应该使用从源代码安装的版本。由于你将运行最新且未经充分测试的版本,可能会遇到一些小问题。
从发行版安装
大多数现代发行版都包括一个包管理器,可以自动管理包的依赖关系和更新。这使得通过包管理器安装 Ansible 成为启动 Ansible 的最简单方式;通常只需要一个命令即可完成安装。它还会随着你的机器更新而自动更新,尽管可能会落后一两个版本。以下是在最常见的发行版上安装 Ansible 的命令。如果你使用的是其他发行版,请参考你的包管理工具的用户指南或发行版的包列表:
- 
Fedora、RHEL、CentOS 及兼容系统: $ yum install ansible
- 
Ubuntu、Debian 及兼容系统: $ apt-get install ansible
注意
请注意,RHEL 和 CentOS 需要先安装 EPEL 仓库。有关 EPEL 的详细信息,包括如何安装,可以参考fedoraproject.org/wiki/EPEL。
如果你使用的是 Ubuntu,并希望使用最新版本而不是操作系统提供的版本,你可以使用 Ansible 提供的 Ubuntu PPA。有关如何设置的详细信息,可以参考launchpad.net/~ansible/+archive/ubuntu/ansible。
从 pip 安装
Pip,像发行版的包管理器一样,会处理你请求的包及其依赖项的查找、安装和更新。这使得通过 pip 安装 Ansible 和通过包管理器安装一样简单。然而,需要注意的是,它不会随着操作系统的更新而更新。此外,更新操作系统可能会破坏你的 Ansible 安装;不过,这种情况不太可能发生。如果你是 Python 用户,可能希望在隔离环境(虚拟环境)中安装 Ansible:但这不被支持,因为 Ansible 尝试将其模块安装到系统中。你应该使用 pip 在全系统范围内安装 Ansible。
以下是通过 pip 安装 Ansible 的命令:
$ pip install ansible
从源代码安装
从源代码安装是获取最新版本的好方法,但它可能没有经过像正式发布版本那样的测试。此外,你还需要自行管理更新到新版本,并确保 Ansible 在操作系统更新后依然能够正常工作。要克隆 git 仓库并安装,请运行以下命令。你可能需要系统的 root 权限才能执行此操作:
$ git clone git://github.com/ansible/ansible.git
$ cd ansible
$ sudo make install
提示
下载示例代码
你可以从 www.packtpub.com 的帐户中下载你购买的所有 Packt 书籍的示例代码文件。如果你是从其他地方购买的本书,可以访问 www.packtpub.com/support 并注册,文件将通过电子邮件直接发送给你。
设置 Ansible
Ansible 需要能够获取你想要配置的机器的清单,才能管理它们。由于有清单插件,这可以通过多种方式实现。基础安装包含了几个不同的清单插件。我们将在本书后面介绍这些插件。现在,我们将介绍简单的主机文件清单。
默认的 Ansible 清单文件名为 hosts,存放在 /etc/ansible 目录下。它的格式类似于 INI 文件。组名用方括号括起来,所有位于组名下的内容,直到下一个组名,都会分配给该组。机器可以同时属于多个组。组的作用是让你可以一次配置多个机器。在接下来的示例中,你可以使用组名代替主机名作为主机模式,Ansible 将对整个组同时运行模块。
在以下示例中,我们有三台机器在名为 webservers 的组中,分别是 site01、site02 和 site01-dr。我们还有一个名为 production 的组,包含了 site01、site02、db01 和 bastion。
[webservers]
site01
site02
site01-dr
[production]
site01
site02
db01
bastion
一旦你将主机添加到 Ansible 清单中,你就可以开始对其运行命令。Ansible 包含一个简单的模块叫做 ping,可以让你测试自己与主机之间的连接。让我们从命令行使用 Ansible 对我们的其中一台机器进行测试,确认我们可以配置它们。
Ansible 设计上追求简洁,而开发者通过使用 SSH 来连接被管理的机器实现了这一点。然后,它通过 SSH 连接发送代码并执行。这意味着你不需要在被管理的机器上安装 Ansible。这也意味着 Ansible 使用的是你已经用来管理机器的通道。这使得设置更简单,因为在大多数情况下,通常不需要额外的配置,也不需要在防火墙中打开端口。
首先,我们使用 Ansible ping 模块检查与服务器的连接。这个模块只是连接到以下服务器:
$ ansible site01 -u root -k -m ping
这应该会要求输入 SSH 密码,然后输出如下结果:
site01 | success >> {
 "changed": false,
 "ping": "pong"
}
如果你为远程系统设置了 SSH 密钥,你将能够省略 -k 参数,跳过提示并使用密钥。你还可以通过在清单中为每个主机单独配置,或者在全局 Ansible 配置中配置,来让 Ansible 始终使用特定的用户名。
要全局设置用户名,请编辑/etc/ansible/ansible.cfg并更改在[defaults]部分设置remote_user的行。您还可以更改remote_port以更改 Ansible 将 SSH 连接到的默认端口。这将更改所有机器的默认设置,但可以在清单文件中按服务器或组进行覆盖。
要在清单文件中设置用户名,只需在清单的相应行后附加ansible_ssh_user。例如,以下代码部分显示了一个清单,其中site01主机使用用户名root,site02主机使用用户名daniel。还有其他变量可以使用。ansible_ssh_host变量允许您设置不同的主机名,ansible_ssh_port变量允许您设置不同的端口,这在site01-dr主机上有所示。最后,db01主机使用用户名fred,并使用ansible_ssh_private_key_file设置私钥。
[webservers]      #1
site01 ansible_ssh_user=root     #2
site02 ansible_ssh_user=daniel      #3
site01-dr ansible_ssh_host=site01.dr ansible_ssh_port=65422      #4
[production]      #5
site01      #6
site02      #7
db01 ansible_ssh_user=fred ansible_ssh_private_key_file=/home/fred/.ssh.id_rsa     #8
bastion      #9
如果您不希望将 Ansible 直接访问受管机器上的 root 帐户,或者您的机器不允许 SSH 访问 root 帐户(例如 Ubuntu 的默认配置),您可以配置 Ansible 使用sudo来获取 root 访问权限。使用sudo的 Ansible 意味着您可以强制进行与否则相同的审核。配置 Ansible 使用sudo与配置端口一样简单,只是它要求在受管机器上配置sudo。
第一步是向/etc/sudoers文件添加一行;在受管节点上,如果选择使用自己的帐户,可能已经设置了这一点。您可以使用sudo与密码,也可以使用无密码的sudo。如果决定使用密码,您将需要对 Ansible 使用-k参数,或者在/etc/ansible/ansible.cfg中将ask_sudo_pass值设置为true。要使 Ansible 使用 sudo,请像这样在命令行中添加--sudo:
ansible site01 -s -m command -a 'id -a'
如果正常工作,它应返回类似于:
site01 | success | rc=0 >>
uid=0(root) gid=0(root) groups=0(root) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
在 Windows 上设置它
Ansible 最近添加了管理 Windows 机器的功能。现在,您可以使用 Ansible 轻松管理 Windows 机器,就像管理 Linux 机器一样。
这与在 Linux 机器上使用 SSH 执行模块的方式相同使用 Windows PowerShell Remoting 工具远程执行。已添加了几个明确支持 Windows 的新模块,但也使一些现有模块能够与 Windows 受管机器一起使用。
要开始管理您的 Windows 机器,您需要执行一些复杂的设置。您需要遵循以下步骤:
- 
在清单中创建一些 Windows 机器 
- 
安装 Python-winrm 以允许 Ansible 连接到 Windows 机器 
- 
升级到 PowerShell 3.0+ 以支持 Windows 模块 
- 
启用 Windows 远程管理以便 Ansible 连接 
Windows 机器的创建方式与清单中其他所有机器相同。它们通过 ansible_connection 变量的值来区分。当 ansible_connection 设置为 winrm 时,它将尝试通过 winrm 连接到远程机器上的 Windows PowerShell。Ansible 还像在其他机器上一样使用 ansible_ssh_user、ansible_ssh_pass 和 ansible_ssh_port 的值。尽管这些名字里包含 ssh,但它们是用来提供连接 Windows PowerShell 远程服务所需的端口和凭证。以下是一个 Windows 机器示例:
[windows]
dc.ad.example.com
web01.ad.example.com
web02.ad.example.com
[windows:vars]
ansible_connection=winrm
ansible_ssh_user=daniel
ansible_ssh_pass=s3cr3t
ansible_ssh_port=5986
出于安全考虑,你可能不希望将密码存储在清单文件中。你可以像我们之前对 Unix 系统所展示的那样让 Ansible 提示输入密码,只需要去掉 ansible_ssh_user 和 ansible_ssh_pass 变量,而改为使用 -k 和 -u 参数传递给 Ansible。如果你愿意,也可以选择将密码存储在 Ansible 密库中,后续书中将详细介绍这个方法。
创建完清单文件后,你需要在控制器机器上安装 winrm Python 库。这个库将赋予 Ansible 连接 Windows 远程管理服务并配置远程 Windows 系统的能力。
目前,这个库还处于实验阶段,它与 Ansible 的连接并不完美,因此你需要安装与当前使用的 Ansible 版本匹配的特定版本。随着 Ansible 1.8 版本的发布,这个问题应该会有所解决。大多数发行版尚未打包该库,因此你可能需要通过 pip 安装。作为 root 用户,你需要运行以下命令:
$ pip install https://github.com/diyan/pywinrm/archive/df049454a9309280866e0156805ccda12d71c93a.zip
然而,对于较新的版本,你只需要运行以下命令:
pip install http://github.com/diyan/pywinrm/archive/master.zip
这将安装与 Ansible 1.7 兼容的特定版本 winrm。对于其他较新的 Ansible 版本,你可能需要不同的版本,最终 winrm Python 库应该会被不同的发行版打包。现在,你的机器应该能够使用 Ansible 连接和管理 Windows 机器。
接下来,你需要在将要管理的机器上进行一些设置。第一步是确保你已安装 PowerShell 3.0 或更高版本。你可以使用以下命令检查已安装的版本:
$PSVersionTable.PSVersion.Major
如果你得到的返回值不是 3 或者大于 3,那么你需要升级你的 PowerShell 版本。你可以选择手动下载并安装适合你系统的最新 Windows 管理框架,或者使用 Ansible 项目提供的脚本。为了节省空间,我们将在这里解释脚本化安装;手动安装留给读者自行操作。
Invoke-WebRequest https://raw.githubusercontent.com/ansible/ansible/release1.7.0/examples/scripts/upgrade_to_ps3.ps1 -OutFile upgrade_to_ps3.ps1
.\upgrade_to_ps3.ps1
第一个命令从 Ansible 项目的 GitHub 存储库下载升级脚本并保存到磁盘。第二个命令将检测您的操作系统,以下载适合的 Windows 管理框架版本并安装它。
接下来,您需要配置 Windows 远程管理服务。Ansible 项目提供了一个脚本,该脚本会自动按 Ansible 期望的方式配置 Windows 远程管理。虽然您可以手动设置,但强烈建议您使用此脚本,以避免配置错误。要下载并运行此脚本,请打开 PowerShell 终端并运行以下命令:
Invoke-WebRequest https://raw.githubusercontent.com/ansible/ansible/release1.7.0/examples/scripts/ConfigureRemotingForAnsible.ps1 -OutFile ConfigureRemotingForAnsible.ps1
.\ConfigureRemotingForAnsible.ps1
第一个命令从 Ansible 项目的 GitHub 上下载配置脚本,第二个命令运行该脚本。如果一切正常,第二个脚本应该返回Ok的输出。
您现在应该能够连接到您的机器并使用 Ansible 进行配置。像之前一样,我们运行一个 ping 命令来确认 Ansible 是否能够远程执行其模块。虽然 Unix 机器可以使用ping模块,但 Windows 机器使用win_ping模块。用法几乎完全相同;不过,由于我们已经将密码添加到库存文件中,因此无需使用-k选项。
$ ansible web01.ad.example.com -u daniel -m win_ping
如果一切正常,您应该看到以下输出:
web01.ad.example.com | success >> {
 "changed": false,
 "ping": "pong"
}
输出表示 Ansible 成功连接到 Windows 远程管理服务,成功登录并在远程主机上执行了一个模块。如果一切正常,那么您应该能够使用所有其他 Windows 模块来管理您的机器。
使用 Ansible 的第一步
Ansible 模块以类似key=value的键值对形式接收参数,执行远程服务器上的任务,并以JSON格式返回任务信息。键值对允许模块在请求时知道应该做什么。它们可以是硬编码的值,或者在播放书中,它们可以使用变量,关于这一点将在第二章,简单播放书中介绍。模块返回的数据让 Ansible 知道是否在受管理的主机中发生了任何更改,或者是否之后应该更改 Ansible 保存的任何信息。
模块通常在播放书中运行,因为这样可以将多个模块串联起来,但也可以在命令行中使用。之前,我们使用了ping命令来检查 Ansible 是否已正确设置并能够访问配置的节点。ping模块仅检查 Ansible 核心是否能够在远程机器上运行,但实际上什么都不做。
一个稍微更有用的模块名为setup。该模块连接到配置的节点,收集有关系统的数据,然后返回这些值。虽然在命令行运行时这并不特别方便,但在播放书中,您可以稍后在其他模块中使用收集到的值。
要从命令行运行 Ansible,你需要传递两个参数,通常是三个。第一个是一个主机模式,用于匹配你想要应用模块的机器。第二个是你需要提供你希望运行的模块名称,和可选的任何参数。对于主机模式,你可以使用一个组名、一个机器名、一个 glob 模式,或一个波浪符(~),后面跟一个正则表达式匹配主机名。或者,为了表示所有这些,你可以使用all这个词或简单地使用*。以这种方式在命令行运行 Ansible 模块被称为临时 Ansible 命令。
要在你的节点上运行 setup 模块,你需要以下命令行:
$ ansible machinename -u root -k -m setup
然后,setup模块将连接到机器并返回许多有用的信息。setup模块提供的所有事实信息都以ansible_为前缀,以便与变量区分开来。
该模块可以在 Windows 和 Unix 机器上工作。目前,Unix 机器会提供比 Windows 机器更多的信息。然而,随着 Ansible 新版本的发布,你可以期待看到更多 Windows 功能被加入到 Ansible 中。
machinename | success >> {
 "ansible_facts": {
 "ansible_distribution": "Microsoft Windows NT 6.3.9600.0",
 "ansible_distribution_version": "6.3.9600.0",
 "ansible_fqdn": "ansibletest",
 "ansible_hostname": "ANSIBLETEST",
 "ansible_ip_addresses": [
 "100.72.124.51",
 "fe80::1fd:fc3b:1eff:350d"
 ],
 "ansible_os_family": "Windows",
 "ansible_system": "Win32NT",
 "ansible_totalmem": "System.Object[]"
 },
 "changed": false
}
以下是你将使用的最常见值的表格;并非所有这些值都在所有机器上可用。尤其是 Windows 机器从 setup 模块返回的数据要少得多。
| 字段 | 示例 | 描述 | 
|---|---|---|
| ansible_architecture | x86_64 | 这是受管机器的架构 | 
| ansible_distribution | CentOS | 这是受管机器上的 Linux 或 Unix 发行版 | 
| ansible_distribution_version | 6.3 | 这是前述发行版的版本 | 
| ansible_domain | example.com | 这是服务器主机名中的域名部分 | 
| ansible_fqdn | machinename.example.com | 这是受管机器的完全合格域名 | 
| ansible_interfaces | ["lo", "eth0"] | 这是该机器所有接口的列表,包括回环接口 | 
| ansible_kernel | 2.6.32-279.el6.x86_64 | 这是受管机器上安装的内核版本 | 
| ansible_memtotal_mb | 996 | 这是受管机器上可用的总内存,以兆字节为单位 | 
| ansible_processor_count | 1 | 这是受管机器上可用的 CPU 总数 | 
| ansible_virtualization_role | guest | 这决定了机器是客机还是主机 | 
| ansible_virtualization_type | kvm | 这是受管机器上虚拟化的类型 | 
在 Unix 机器上,这些变量通过 Python 从受管机器收集;如果远程节点上安装了 facter 或 ohai,setup 模块将执行它们并返回它们的数据。与其他事实一样,ohai 的事实前缀是 ohai_,facter 的事实前缀是 facter_。尽管在命令行中 setup 模块看起来没什么用处,但一旦开始编写 playbook,它会变得非常有用。请注意,facter 和 ohai 在 Windows 主机上不可用。
如果 Ansible 中的所有模块像 setup 和 ping 模块那样仅做很少的事情,我们将无法更改远程机器上的任何内容。Ansible 提供的几乎所有其他模块,如 file 模块,都允许我们实际配置远程机器。
file 模块可以通过单个路径参数调用;这将使其返回有关该文件的信息。如果你提供更多参数,它将尝试更改文件的属性,并告诉你是否已进行更改。Ansible 模块会告诉你是否进行了更改,这在你编写 playbooks 时变得更加重要。
你可以调用 file 模块,如下所示,查看 /etc/fstab 的详细信息:
$ ansible machinename -u root -k -m file -a 'path=/etc/fstab'
前述命令应该产生类似以下内容的响应:
machinename | success >> {
 "changed": false,
 "group": "root",
 "mode": "0644",
 "owner": "root",
 "path": "/etc/fstab",
 "size": 779,
 "state":
 "file"
}
另外,响应可能是类似以下命令的内容,用于在 /tmp 创建一个新的测试目录:
$ ansible machinename -u root -k -m file -a 'path=/tmp/teststate=directory mode=0700 owner=root'
前述命令应该返回类似以下内容:
machinename | success >> {
 "changed": true,
 "group": "root",
 "mode": "0700",
 "owner": "root",
 "path": "/tmp/test",
 "size": 4096,
 "state": "directory"
}
我们可以看到响应中 changed 变量被设置为 true,因为目录不存在或具有不同的属性,需要进行更改以使其与提供的参数所指定的状态匹配。如果第二次使用相同的参数运行该命令,changed 的值将设置为 false,意味着该模块没有对系统做出任何更改。
有几个模块接受与 file 模块类似的参数,其中一个例子是 copy 模块。copy 模块会将控制机上的文件复制到受管机器,并按要求设置其属性。例如,要将 /etc/fstab 文件复制到受管机器的 /tmp,你可以使用以下命令:
$ ansible machinename -m copy -a 'src=/etc/fstab dest=/tmp/fstab'
前述命令在第一次运行时,应该返回类似以下内容:
machinename | success >> {
 "changed": true,
 "dest": "/tmp/fstab",
 "group": "root",
 "md5sum": "fe9304aa7b683f58609ec7d3ee9eea2f",
 "mode": "0700",
 "owner": "root",
 "size": 637,
 "src": "/root/.ansible/tmp/ansible-1374060150.96- 77605185106940/source",
 "state": "file"
}
还有一个名为 command 的模块,可以在受管机器上运行任何任意命令。这使得你可以使用任何任意命令进行配置,比如一个 preprovided 安装程序或自编写的脚本;它对于重启机器也非常有用。请注意,这个模块不会在 shell 中运行命令,因此你无法执行重定向、使用管道、展开 shell 变量或在后台运行命令。
Ansible 模块致力于在不需要更改时避免进行修改。这被称为幂等性,它可以使得在多个服务器上运行命令时变得更快。不幸的是,Ansible 无法知道你的命令是否做出了更改,因此为了帮助它更具幂等性,你需要提供一些帮助。它可以通过creates或removes参数来实现这一点。如果你给出creates参数,则如果文件名参数存在,命令将不会运行。removes参数则相反;如果文件名存在,命令将会执行。
你可以按以下方式运行该命令:
$ ansible machinename -m command -a 'rm -rf /tmp/testing removes=/tmp/testing'
如果没有名为/tmp/testing的文件或目录,命令输出将指示该文件已被跳过,如下所示:
machinename | skipped
否则,如果文件确实存在,它将像以下代码一样显示:
ansibletest | success | rc=0 >>
通常情况下,使用其他模块代替command模块会更好。其他模块提供更多选项,并且能够更好地捕获它们所工作的问题领域。例如,在这个例子中,使用file模块对于 Ansible 和编写配置的人员来说都会减少工作量,因为file模块如果状态设置为absent,会递归地删除某些东西。所以,前面的命令相当于以下命令:
$ ansible machinename -m file -a 'path=/tmp/testing state=absent'
如果你需要在运行命令时使用通常在 shell 中可用的功能,你需要使用shell模块。这样你就可以使用重定向、管道或后台作业。你可以使用executable参数来选择要使用的 shell。你可以按以下方式使用shell模块:
$ ansible machinename -m shell -a '/opt/fancyapp/bin/installer.sh >/var/log/fancyappinstall.log creates=/var/log/fancyappinstall.log'
模块帮助
不幸的是,我们没有足够的空间来涵盖 Ansible 中所有可用的模块;不过幸运的是,Ansible 包含了一个名为ansible-doc的命令,可以用来检索帮助信息。Ansible 中包含的所有模块都有这个数据,但从其他地方收集的模块可能帮助信息较少。ansible-doc命令还允许你查看所有可用模块的列表。
要获取所有可用模块的列表,并附带每种类型的简短描述,可以使用以下命令:
$ ansible-doc -l
要查看某个特定模块的帮助文件,你需要将模块作为ansible-doc命令的唯一参数。例如,要查看file模块的帮助信息,可以使用以下命令:
$ ansible-doc file
总结
在本章中,我们讨论了选择哪种安装类型来安装 Ansible,以及如何构建一个清单文件来反映你的环境。接着,我们展示了如何以临时方式使用 Ansible 模块来执行简单任务。最后,我们讨论了如何了解系统中可用的模块,并如何使用命令行获取使用模块的指令。
在接下来的章节中,你将学习如何在 Playbook 中一起使用多个模块。这使得你能够执行比单独使用模块时更加复杂的任务。
第二章。简单的剧本
Ansible 可以用作命令行工具来进行小改变。但是,它的真正力量在于其脚本能力。在设置机器时,我们几乎总是需要同时做多件事情。Ansible 使用名为剧本的概念来实现此目的。使用剧本,我们可以一次执行多个操作,并跨多个系统执行。它们提供了一种协调部署、确保一致配置或简单执行常见任务的方法。
剧本以YAML表示,大部分情况下,Ansible 使用标准的 YAML 解析器。这意味着我们在编写时可以利用 YAML 提供给我们的所有功能。例如,在剧本中我们可以像在 YAML 中一样使用相同的注释系统。许多剧本的行也可以用 YAML 数据类型编写和表示。更多信息请参阅 www.yaml.org/。
剧本还打开了许多机会。它们允许我们在一个命令到另一个命令之间传递状态。例如,我们可以在一台机器上获取文件的内容,将其注册为变量,然后在另一台机器上使用该值。这使我们能够创建使用 Ansible 命令单独无法实现的复杂部署机制。此外,由于每个模块都试图是幂等的,我们应该能够多次运行剧本,只有在需要时才会进行更改。
执行剧本的命令是 ansible-playbook。它接受类似 Ansible 命令行工具的参数。例如,-k(--ask-pass)和-K(--ask-sudo)使 Ansible 分别提示 SSH 和 sudo 密码;-u 可用于设置用于 SSH 的用户。但是,这些选项也可以在剧本自身的目标部分中设置。例如,要使用名为 example-play.yml 的播放,我们可以使用以下命令:
$ ansible-playbook example-play.yml
Ansible 剧本由一个或多个播放组成。一个播放包括三个部分:
- 
目标部分 定义了将在其上运行播放的主机以及如何运行。这是我们设置 SSH 用户名和其他 SSH 相关设置的地方。 
- 
变量部分 定义了在运行时将提供给剧本的变量。 
- 
任务部分 按我们希望 Ansible 运行的模块顺序列出所有模块。 
我们可以在单个 YAML 文件中包含尽可能多的剧本。YAML 文件以---开头,包含许多键值和列表。在 YAML 中,行缩进用于指示变量的嵌套给解析器,这也使文件更易于阅读。
完整的 Ansible 播放示例如下代码片段所示:
---
- hosts: webservers
  user: root
  vars:
    apache_version: 2.6
    motd_warning: 'WARNING: Use by ACME Employees ONLY'
    testserver: yes
  tasks:
    - name: setup a MOTD
      copy:
        dest: /etc/motd
        content: "{{ motd_warning }}"
在接下来的几节中,我们将检查每个部分,并详细解释它们的工作方式。
目标部分
目标部分看起来像以下的代码片段:
- hosts: webservers
  user: root
这是一个非常简单的版本,但在大多数情况下可能足够使用。每个播放都存在于一个列表中。根据 YAML 语法,该行必须以破折号开始。播放将运行的主机必须在hosts的值中设置。该值使用与在前一章中讨论的 Ansible 命令行中选择主机时相同的语法。Ansible 的主机模式匹配功能也在前一章中讨论过。在下一行中,用户告诉 Ansible 剧本以哪个用户身份连接到机器。
我们可以在此部分提供的其他行如下:
| 名称 | 描述 | 
|---|---|
| sudo | 如果希望 Ansible 在连接到播放中的机器后使用 sudo成为 root 用户,请将其设置为yes。 | 
| user | 这定义了最初连接到机器的用户名,然后如果已配置 sudo,将运行sudo。 | 
| sudo_user | 这是 Ansible 尝试使用 sudo切换为的用户。例如,如果我们将sudo设置为yes,并将user设置为daniel,将sudo_user设置为kate,则会导致 Ansible 在登录后使用sudo从daniel切换到kate。如果你在交互式 SSH 会话中进行此操作,我们可以在以daniel登录时使用sudo -u kate。 | 
| connection | 这允许我们告诉 Ansible 使用何种传输方式连接远程主机。对于远程主机,我们通常使用 ssh或paramiko。不过,我们也可以使用local来避免在运行localhost时产生连接开销。通常,我们会在此使用local、winrm或ssh。 | 
| gather_facts | 除非我们告诉它不要,否则 Ansible 将自动在远程主机上运行 setup 模块。如果我们不需要来自 setup 模块的变量,可以现在设置此项并节省一些时间。 | 
变量部分
在这里,我们可以定义适用于所有机器的整个播放的变量。如果在命令行中未提供变量,Ansible 还可以提示用户输入变量。这使得我们可以轻松维护播放,避免在多个地方更改相同的内容。它还允许我们将整个配置存储在播放的顶部,在这里我们可以轻松阅读和修改它,而不必担心播放的其他部分。
该部分的变量可以被机器事实(由模块设置的事实)覆盖,但它们本身会覆盖我们在清单中设置的事实。因此,它们用于定义我们可能在以后通过模块收集的默认值,但不能用于保持清单变量的默认值,因为它们会覆盖这些默认值。
变量声明发生在vars部分,类似目标部分中的值,并包含一个 YAML 字典或列表。示例如下所示的代码片段:
vars:
  apache_version: 2.6
  motd_warning: 'WARNING: Use by ACME Employees ONLY'
  testserver: yes
变量也可以通过提供要加载的变量文件列表从外部 YAML 文件加载。这通过类似的方式使用 vars_files 指令完成。然后简单地提供另一个包含自己字典的 YAML 文件的名称。这意味着,我们可以将变量存储并分别分发,而不是将它们存储在同一个文件中,这样我们就可以与他人共享我们的 playbook。
使用 vars_files,这些文件在我们的 playbook 中看起来类似于以下代码片段:
vars_files:
  conf/country-AU.yml
  conf/datacenter-SYD.yml
  conf/cluster-mysql.yml
在之前的示例中,Ansible 会在相对于 playbook 路径的 conf 文件夹中查找 country-AU.yml、datacenter-SYD.yml 和 cluster-mysql.yml。每个 YAML 文件看起来类似于以下代码片段:
---
ntp: ntp1.au.example.com
TZ: Australia/Sydney
最后,我们可以让 Ansible 以交互方式向用户询问每个变量。这在我们有一些不希望用于自动化的变量时非常有用,而是需要人工输入。一个有用的例子是当需要提示用户输入解密 HTTPS 服务器的密钥密码时。
我们可以通过以下代码片段指示 Ansible 提示输入变量:
vars_prompt:
  - name: https_passphrase
    prompt: Key Passphrase
    private: yes
在之前的示例中,https_passphrase 是输入的数据存储的位置。系统会提示用户输入 Key Passphrase,由于 private 设置为 yes,用户输入的值将不会在屏幕上显示。
我们可以使用 {{ variablename }} 来引用变量、事实和库存变量。我们甚至可以通过点表示法引用复杂的变量,例如字典。例如,一个名为 httpd 的变量,其键为 maxclients,可以通过 {{ httpd.maxclients }} 进行访问。这对于来自 setup 模块的事实也同样适用。例如,我们可以使用 {{ ansible_eth0.ipv4.address }} 获取名为 eth0 的网络接口的 IPv4 地址。
在变量部分设置的变量不会在同一个 playbook 中的不同 plays 之间保留。然而,通过 setup 模块收集的事实或通过 set_fact 设置的事实会保留。这意味着,如果我们在同一台机器或先前 play 中的一部分机器上运行第二个 play,我们可以在目标部分将 gather_facts 设置为 false。setup 模块有时需要较长时间运行,因此这可以显著加快 plays 的速度,特别是在 serial 设置为较小值的 plays 中。
任务部分
任务部分是每个 play 的最后一个部分。它包含我们希望 Ansible 按顺序执行的操作列表。我们可以以几种不同的方式表示每个模块的参数。建议尽可能坚持使用一种方式,只有在需要时才使用其他方式。这样可以使我们的 playbooks 更容易阅读和维护。以下代码片段展示了任务部分的三种风格:
tasks:
  - name: install apache
    action: yum name=httpd state=installed
  - name: configure apache
    copy: src=files/httpd.conf dest=/etc/httpd/conf/httpd.conf
  - name: restart apache
    service:
      name: httpd
      state: restarted
在这里,我们看到使用三种不同的语法样式来安装、配置并启动 Apache Web 服务器的方式,所展示的是在 CentOS 机器上的表现。第一个任务展示了如何使用原始语法安装 Apache,这需要我们将模块作为 action 键中的第一个关键字调用。第二个任务使用第二种任务样式将 Apache 配置文件复制到位。在这种样式中,模块名称取代 action 关键字,其值直接成为模块的参数。最后,第三个任务展示了如何使用服务模块重启 Apache。在这种样式中,我们像往常一样将模块名称用作键,但我们将参数提供为 YAML 字典。当我们向单个模块提供大量参数,或模块需要复杂形式的参数时(如云形成模块),这种样式会特别有用。随着越来越多的模块需要复杂的参数,后一种样式正迅速成为编写 playbook 的首选方式。在本书中,为了节省示例空间并避免行换行,我们将使用这种样式。
请注意,任务名称不是必需的。然而,它们有助于良好的文档记录,并允许我们在需要时引用每个任务。尤其是在处理程序部分,这将变得非常有用。当 playbook 执行时,任务名称也会输出到控制台,用户可以看到正在发生什么。如果我们没有提供名称,Ansible 将使用任务或处理程序的操作行。
注意
与其他配置管理工具不同,Ansible 不提供完整的依赖系统。这既是一个祝福也是一个诅咒;拥有完整的依赖系统,我们可能会陷入一个困境,即始终不确定哪些更改会应用到特定机器上。然而,Ansible 确保我们的更改会按照编写的顺序执行。因此,如果一个模块依赖于另一个在其之前执行的模块,只需在 playbook 中将一个模块放在另一个之前即可。
处理程序部分
处理程序部分在语法上与任务部分相同,并支持相同的调用模块格式。只有当调用该处理程序的任务记录了执行过程中某些内容发生了变化时,处理程序才会被调用。要触发处理程序,在任务中添加一个 notify 键,并将值设置为任务的名称。
在 Ansible 完成任务列表执行后,如果之前触发过的处理程序(handlers)会被运行。它们的执行顺序是根据在 handlers 部分中列出的顺序,尽管它们在任务部分可能被多次调用,但每个处理程序只会执行一次。这通常用于在升级和配置完守护进程后重启它们。以下的 playbook 示例演示了如何将 ISC DHCP(动态主机配置协议)服务器升级到最新版本,进行配置并设置为开机启动。如果此 playbook 在一个已运行最新版本 ISC DHCP 守护进程且配置文件未更改的服务器上运行,则处理程序不会被调用,DHCP 也不会重启。考虑以下代码示例:
---
- hosts: dhcp
  tasks:
  - name: update to latest DHCP
    yum
      name: dhcp
      state: latest
    notify: restart dhcp
  - name: copy the DHCP config
    copy:
      src: dhcp/dhcpd.conf
      dest: /etc/dhcp/dhcpd.conf
    notify: restart dhcp
  - name: start DHCP at boot
    service:
      name: dhcpd
      state: started
      enabled: yes
  handlers:
  - name: restart dhcp
    service:
      name: dhcpd
      state: restarted
每个处理程序只能是一个单独的模块,但我们可以从单个任务中通知多个处理程序。这允许我们从任务列表中的一个步骤触发多个处理程序。例如,如果我们刚刚检出了任何 Django 应用程序的更新版本,我们可以设置一个处理程序来迁移数据库、部署静态文件并重启 Apache。我们只需在 notify 动作中使用 YAML 列表即可实现。这可能类似于以下代码片段:
---
- hosts: qroud
  tasks:
  - name: checkout Qroud
    git:
      repo:git@github.com:smarthall/Qroud.git
      dest: /opt/apps/Qroud force=no
    notify:
      - migrate db
      - generate static
      - restart httpd
  handlers:
  - name: migrate db
    command: ./manage.py migrate –all
    args:
      chdir: /opt/apps/Qroud
  - name: generate static
    command: ./manage.py collectstatic -c –noinput
    args:
       chdir: /opt/apps/Qroud
  - name: restart httpd
    service:
      name: httpd
      state: restarted
我们可以看到 git 模块用于检出一些公共 GitHub 代码,如果这导致了任何更改,它将触发 migrate db、generate static 和 restart httpd 动作。
playbook 模块
在 playbook 中使用模块与在命令行中使用它们略有不同。这主要是因为我们有很多来自先前模块和 setup 模块的事实(facts)。某些模块在 Ansible 命令行中无法使用,因为它们需要访问这些变量。其他模块则可以在命令行版本中使用,但在 playbook 中使用时,能够提供更强大的功能。
模板模块
一个经常使用的需要 Ansible 事实的模块示例是 template 模块。这个模块允许我们设计配置文件的框架,然后让 Ansible 在正确的位置插入值。为了实现这一点,Ansible 使用 Jinja2 模板语言。实际上,Jinja2 模板可以比这更复杂,包括条件判断、for 循环和宏等。以下是一个用于配置 BIND 的 Jinja2 配置模板示例:
# {{ ansible_managed }}
options {
  listen-on port 53 {
    127.0.0.1;
    {% for ip in ansible_all_ipv4_addresses %}
      {{ ip }};
    {% endfor %}
  };
  listen-on-v6 port 53 { ::1; };
  directory       "/var/named";
  dump-file       "/var/named/data/cache_dump.db";
  statistics-file "/var/named/data/named_stats.txt";
  memstatistics-file "/var/named/data/named_mem_stats.txt";
};
zone "." IN {
  type hint;
  file "named.ca";
};
include "/etc/named.rfc1912.zones";
include "/etc/named.root.key";
{# Variables for zone config #}
{% if 'authorativenames' in group_names %}
  {% set zone_type = 'master' %}
  {% set zone_dir = 'data' %}
{% else %}
  {% set zone_type = 'slave' %}
  {% set zone_dir = 'slaves' %}
{% endif %}
zone "internal.example.com" IN {
  type {{ zone_type }};
  file "{{ zone_dir }}/internal.example.com";
  {% if 'authorativenames' not in group_names %}
    masters { 192.168.2.2; };
  {% endif %}
};
按约定,Jinja2 模板的文件扩展名为 .j2,但这并非严格要求。现在让我们将这个示例拆解成各个部分。示例从以下代码行开始:
# {{ ansible_managed }}
这一行在文件顶部添加了注释,显示该文件来自哪个模板、主机、模板的修改时间以及所有者。将这些信息作为注释放在模板中是一个好习惯,它确保了人们知道如果想要永久修改它,应该编辑哪些部分。
接下来,在第五行有一个 for 循环:
    {% for ip in ansible_all_ipv4_addresses %}
      {{ ip }};
    {% endfor %}
For 循环会遍历列表中的所有元素,对于列表中的每一项都会执行一次循环。它可以选择将当前项赋值给我们选择的变量,这样我们就可以在循环内使用它。这个循环遍历的是 ansible_all_ipv4_addresses 中的所有值,这是 setup 模块提供的一个列表,包含了机器的所有 IPv4 地址。在 for 循环内,它会将每个地址添加到配置中,确保 BIND 会在该接口上监听。
模板中也可以添加注释,例如第 24 行的示例:
{# Variables for zone config #}
{# 和 #} 之间的内容会被 Jinja2 模板处理器忽略。这使我们能够在模板中添加注释,而这些注释不会出现在最终的文件中。如果我们正在做一些复杂的操作、在模板中设置变量,或者如果配置文件不允许注释,这特别有用。
接下来的几行是一个 if 语句的一部分,它设置了 zone_type 和 zone_dir 变量,以便在模板中后续使用:
{% if 'authorativenames' in group_names %}
  {% set zone_type = 'master' %}
  {% set zone_dir = 'data' %}
{% else %}
  {% set zone_type = 'slave' %}
  {% set zone_dir = 'slaves' %}
{% endif %}
{% if %} 和 {% else %} 之间的内容会在 if 标签中的语句为 false 时被忽略。在这里,我们检查 authorativenames 是否在适用于该主机的组名列表中。如果为 true,接下来的两行将分别设置两个自定义变量。zone_type 被设置为 master,zone_dir 被设置为 data。如果该主机不在 authorativenames 组中,zone_type 和 zone_dir 将分别设置为 slave 和 slaves。
最后,从第 33 行开始,我们提供了区域的实际配置:
zone "internal.example.com" IN {
  type {{ zone_type }};
  file "{{ zone_dir }}/internal.example.com";
  {% if zone_type == 'slave' %}
    masters { 192.168.2.2; };
  {% endif %}
};
我们将类型设置为之前创建的 zone_type 变量,并将位置设置为 zone_dir。最后,我们检查 zone 类型是否为 slave,如果是的话,我们会将它的主服务器配置为特定的 IP 地址。
为了让这个模板设置一个权威的 DNS 服务器,我们需要在清单文件中创建一个名为 authorativenames 的组,并在其下添加一些主机。如何做到这一点在第一章,开始使用 Ansible 中有讨论过。
我们可以简单地调用 templates 模块,机器的事实信息会被传递过来,包括该机器所在的组。这和调用其他任何模块一样简单。template 模块也接受与 copy 模块类似的参数,比如 owner、group 和 mode。例如,考虑以下代码:
---
- name: Setup BIND
  host: allnames
  tasks:
  - name: configure BIND
    template: src=templates/named.conf.j2 dest=/etc/named.conf owner=root group=named mode=0640
set_fact 模块
set_fact 模块允许我们在 Ansible play 中为机器构建我们自己的事实。然后这些事实可以在模板中使用,或者作为 playbook 中的变量。事实就像是来自 setup 模块等模块的参数一样,在主机级别起作用。我们应该使用这个模块来避免将复杂的逻辑写入模板中。例如,如果我们要配置一个缓冲区来占用一定比例的内存,我们应该在 playbook 中进行计算。
以下示例展示了如何使用 set_fact 配置 MySQL 服务器,使其 InnoDB 缓冲区大小约为机器总内存的一半:
---
- name: Configure MySQL
  hosts: mysqlservers
  tasks:
  - name: install MySql
    yum:
      name: mysql-server
      state: installed
  - name: Calculate InnoDB buffer pool size
    set_fact:
      innodb_buffer_pool_size_mb="{{ansible_memtotal_mb/2}}"
  - name: Configure MySQL
    template:
      src: templates/my.cnf.j2
      dest: /etc/my.cnf
      owner: root
      group: root
      mode: 0644
    notify: restart mysql
  - name: Start MySQL
    service:
      name: mysqld
      state: started
      enabled: yes
  handlers:
  - name: restart mysql
    service:
      name: mysqld
      state: restarted
这里的第一个任务仅仅是通过 yum 安装 MySQL。第二个任务通过获取管理机的总内存,将其除以二,去掉任何非整数的余数,并将结果存入名为 innodb_buffer_pool_size_mb 的事实中。接下来的一行将一个模板加载到 /etc/my.cnf 中来配置 MySQL。最后,MySQL 被启动并设置为开机自启。还包括了一个处理程序,用于在 MySQL 配置更改时重新启动 MySQL。
模板只需要获取 innodb_buffer_pool_size 的值,并将其放入配置中。这意味着我们可以在需要将缓冲池大小设置为内存的五分之一或八分之一的地方重用相同的模板,并仅仅改变这些主机的剧本。在这种情况下,模板可能会像以下代码片段一样:
# {{ ansible_managed }}
[mysqld]
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock
# Disabling symbolic-links is recommended to prevent assorted security risks
symbolic-links=0
# Settings user and group are ignored when systemd is used.
# If we need to run mysqld under a different user or group,
# customize our systemd unit file for mysqld according to the
# instructions in http://fedoraproject.org/wiki/Systemd
# Configure the buffer pool
innodb_buffer_pool_size = {{ innodb_buffer_pool_size_mb|default(128) }}M
[mysqld_safe]
log-error=/var/log/mysqld.log
pid-file=/var/run/mysqld/mysqld.pid
我们可以看到,在之前的模板中,我们只是将从剧本中获得的变量放入模板中。如果模板没有看到 innodb_buffer_pool_size_mb 事实,它将使用默认值 128。
pause 模块
pause 模块会停止剧本的执行一段时间。我们可以配置它等待特定的时间,或者让它提示用户继续。虽然在从 Ansible 命令行使用时它基本上没有用,但在剧本内部使用时它非常方便。
通常,当我们希望用户提供确认继续,或者在某个特定点需要人工干预时,会使用 pause 模块。例如,如果我们刚刚将新版本的 Web 应用程序部署到服务器上,并且需要让用户手动检查确保一切正常,然后再配置它们接收生产流量,我们可以在这里加入暂停。它还可以用来警告用户可能存在的问题,并让他们选择是否继续。这将使 Ansible 打印出服务器名称并要求用户按 Enter 键继续。如果与目标部分的 serial 键一起使用,它会对 Ansible 正在运行的每组主机询问一次。通过这种方式,我们可以让用户在互动监控进度的同时,以自己的节奏运行部署。
不太有用的是,这个模块可以简单地等待指定的时间。通常它并不是很有用,因为我们通常不知道某个特定操作需要多长时间,猜测可能会导致灾难性的后果。我们不应该使用它来等待网络守护进程启动;相反,我们应该使用 wait_for 模块(在下一节中描述)来完成这个任务。以下剧本展示了先在用户交互模式下使用 pause 模块,然后在定时模式下使用的例子:
---
- hosts: localhost
  tasks:
  - name: wait on user input
    pause:
      prompt: "Warning! Press ENTER to continue or CTRL-C to quit."
  - name: timed wait
    pause:
      seconds: 30
wait_for 模块
wait_for模块用于轮询特定的 TCP 端口,并且直到该端口接受远程连接时才会继续。轮询是从远程机器执行的。如果我们只提供端口,或者将 host 参数设置为localhost,轮询会尝试连接到管理机。我们可以利用local_action从控制机运行命令,并使用ansible_hostname变量作为主机参数,让它尝试从控制机连接到管理机。
这个模块对于需要较长时间才能启动的守护进程,或我们希望在后台运行的程序特别有用。Apache Tomcat 附带一个初始化脚本,在我们尝试启动它时会立即返回,Tomcat 则在后台启动。根据 Tomcat 配置加载的应用程序,它的启动时间可能从两秒到十分钟不等,直到完全启动并准备好接受连接。我们可以为应用程序的启动时间设置计时,并使用pause模块。然而,下次部署可能会需要更长或更短的时间,这会破坏我们的部署机制。使用wait_for模块,Ansible 能够识别 Tomcat 何时准备好接受连接。下面是一个完全实现这一点的 play:
---
- hosts: webapps
  tasks:
  - name: Install Tomcat
    yum:
      name: tomcat7
      state: installed
  - name: Start Tomcat
    service:
      name: tomcat7
      state: started
  - name: Wait for Tomcat to start
    wait_for:
      port: 8080
      state: started
完成此 play 后,Tomcat 应该已安装、启动并准备好接受请求。我们可以在这个例子中添加更多模块,依赖于 Tomcat 已经可用并在监听。
assemble 模块
assemble模块将管理机上的多个文件合并,并保存到管理机上的另一个文件中。在 playbook 中,当我们有一个config文件,该文件不允许在其包含的内容中使用 includes 或 glob 时,这个功能非常有用。比如,对于 root 用户的authorized_keys文件,这个模块就很有用。下面的 play 将把一组 SSH 公钥发送到管理机,然后将它们组合在一起并放到 root 用户的主目录中:
---
- hosts: all
  tasks:
  - name: Make a Directory in /opt
    file:
      path: /opt/sshkeys
      state: directory
      owner: root
      group: root
      mode: 0700
  - name: Copy SSH keys over
    copy:
      src: "keys/{{ item }}.pub"
      dest: "/opt/sshkeys/{{ item }}.pub"
      owner: root
      group: root
      mode: 0600
    with_items:
      - dan
      - kate
      - mal
  - name: Make the root users SSH config directory
    file:
      path: /root/.ssh
      state: directory
      owner: root
      group: root
      mode: 0700
  - name: Build the authorized_keys file
    assemble:
      src: /opt/sshkeys
      dest: /root/.ssh/authorized_keys
      owner: root
      group: root
      mode: 0700
到目前为止,这一切应该看起来很熟悉。我们可以注意到在复制密钥的任务中有with_items键和{{ items }}变量。这些将在第三章中进一步解释,高级 Playbook,但现在我们需要知道的是,我们提供给with_items键的任何项都会替换成{{ items }}变量,类似于for循环的工作方式。这让我们能够轻松地一次性将多个文件复制到远程主机。
最后的任务展示了assemble模块的用法。我们将包含要合并文件的目录作为src参数传递给输出,然后将dest作为输出文件。它还接受许多与其他创建文件的模块相同的参数(owner、group和mode)。它还会按ls -1命令列出的顺序合并文件。这意味着我们可以像udev和rc.d那样使用相同的方法,给文件前加上数字,以确保它们按正确的顺序排列。
add_host 模块
add_host模块是 playbooks 中最强大的模块之一。add_host让我们可以动态地在 play 中添加新机器。我们可以使用uri模块从我们的配置管理数据库(CMDB)中获取一个主机,然后将其添加到当前 play 中。该模块还将我们的主机添加到一个组中,如果该组尚不存在,则会动态创建该组。
该模块简单地接受name和groups参数,这些参数相当直观,并设置主机名和组。我们还可以传递额外的参数,这些参数的处理方式与 inventory 文件中的额外值处理方式相同。这意味着我们可以设置ansible_ssh_user、ansible_ssh_port等。
如果我们使用云服务提供商,如 RackSpace 或 Amazon EC2,Ansible 中有可用的模块可以让我们管理计算资源。如果我们在 inventory 中找不到它们,我们可能决定在 play 开始时创建机器。如果这样做,我们可以使用该模块将机器添加到 inventory 中,以便稍后进行配置。下面是一个使用 Google Compute 模块的示例:
---
- name: Create infrastructure
  hosts: localhost
  connection: local
  tasks:
    - name: Make sure the mailserver exists
      gce:
        image: centos-6
        name: mailserver
        tags: mail
        zone: us-central1-a
      register: mailserver
      when: '"mailserver" not in groups.all'
    - name: Add new machine to inventory
      add_hosts:
        name: mailserver
        ansible_ssh_host: "{{ mailserver.instance_data[0].public_ip }}"
        groups: tag_mail
      when: not mailserver|skipped
group_by模块
除了在 play 中动态创建主机外,我们还可以创建组。group_by模块可以根据机器的事实信息来创建组,包括我们使用前面提到的add_fact模块自行设置的事实。group_by模块接受一个参数key,它接受机器将被添加到的组的名称。通过将其与变量的使用结合起来,我们可以使该模块根据主机的操作系统、虚拟化技术或任何其他可访问的事实将服务器添加到一个组中。然后,我们可以在任何后续 play 的目标部分或模板中使用该组。
所以,如果我们想要创建一个按操作系统对主机进行分组的组,我们将按照以下方式调用该模块:
---
- name: Create operating system group
  hosts: all
  tasks:
    - group_by: key=os_{{ ansible_distribution }}
- name: Run on CentOS hosts only
  hosts: os_CentOS
  tasks:
  - name: Install Apache
    yum: name=httpd state=latest
- name: Run on Ubuntu hosts only
  hosts: os_Ubuntu
  tasks:
  - name: Install Apache
    apt: pkg=apache2 state=latest
然后我们可以使用这些组来通过正确的打包工具安装软件包。在实际操作中,这通常用于避免 Ansible 在执行时输出大量的“跳过”消息。我们可以通过创建一个组来为需要执行操作的机器配置,而不是为每个需要跳过的任务添加when条件。然后,我们可以使用一个单独的 play 来分别配置这些机器。下面是一个在 Debian 和 RedHat 机器上安装 SSL 私钥的示例,而没有使用when条件:
---
- name: Catergorize hosts
  hosts: all
  tasks:
    - name: Gather hosts by OS
      group_by:
        key: "os_{{ ansible_os_family }}"
- name: Install keys on RedHat
  hosts: os_RedHat
  tasks:
    - name: Install SSL certificate
      copy:
        src: sslcert.pem
        dest: /etc/pki/tls/private/sslcert.pem
- name: Install keys on Debian
  hosts: os_Debian
  tasks:
    - name: Install SSL certificate
      copy:
        src: sslcert.pem
        dest: /etc/ssl/private/sslcert.pem
slurp 模块
slurp 模块从远程系统获取文件,使用 base64 编码后返回结果。我们可以利用 register 关键字将文件内容放入 fact 中。当使用 slurp 模块获取文件时,应注意文件的大小。此模块会将整个文件加载到内存中,因此使用 slurp 处理大文件可能会消耗所有可用的内存,导致系统崩溃。文件还需要从被管理机器传输到控制机,对于大文件,这可能需要相当长的时间。
将该模块与 copy 模块结合使用,可以实现两台机器之间的文件复制。以下是该 playbook 的示例:
---
- name: Fetch a SSH key from a machine
  hosts: bastion01
  tasks:
    - name: Fetch key
      slurp:
        src: /root/.ssh/id_rsa.pub
      register: sshkey
- name: Copy the SSH key to all hosts
  hosts: all
  tasks:
    - name: Make directory for key
      file:
        state: directory
        path: /root/.ssh
        owner: root
        group: root
        mode: 0700
    - name: Install SSH key
      copy:
        contents: "{{ hostvars.bastion01.sshkey|b64decode }}"
        dest: /root/.ssh/authorized_keys
        owner: root
        group: root
        mode: 0600
注意
请注意,由于 slurp 模块使用 base64 编码数据,我们必须使用名为 b64decode 的 jinja2 过滤器解码数据,才能让 copy 模块使用它。过滤器将在 第三章 高级 Playbooks 中更详细地介绍。
Windows playbook 模块
Windows 支持是 Ansible 的新特性,因此目前可用的模块不多。专门针对 Windows 的模块以 win_ 开头命名。此外,还有一些既适用于 Windows 又适用于 Unix 系统的模块,例如我们之前提到的 slurp 模块。
在使用 Windows 模块时,应该特别小心路径字符串的引号。反斜杠在 YAML 中是一个重要字符,用于转义字符,在 Windows 路径中也表示目录。因此,YAML 可能会将路径的一部分误认为转义序列。为避免这种情况,我们在字符串中使用单引号。此外,如果路径本身是一个目录,我们应去掉结尾的反斜杠,以免 YAML 将字符串的结尾误认为转义序列。如果必须以反斜杠结束路径,应使用双反斜杠,第二个反斜杠会被忽略。以下是一些正确和错误字符串的示例:
# Correct
'C:\Users\Daniel\Documents\secrets.txt'
'C:\Program Files\Fancy Software Inc\Directory'
'D:\\' # \\ becomes \
# Incorrect
"C:\Users\Daniel\newcar.jpg" # \n becomes a new line
'C:\Users\Daniel\Documents\' # \' becomes '
云基础设施模块
基础设施模块不仅可以管理我们机器的配置,还可以创建这些机器本身。除此之外,我们还可以自动化它们周围的大部分基础设施。这可以作为 Amazon Cloud Formation 等服务的简单替代方案。
在创建我们希望在同一个 playbook 中后续 play 中进行管理的机器时,我们需要使用前面章节中讨论的 add_hosts 模块,将机器添加到内存中的库存中,以便它成为后续 play 的目标。我们还可能希望运行 group_by 模块,将它们按组排列,就像我们排列其他机器一样。还应该使用 wait_for 模块检查机器是否响应 SSH 连接,以便在尝试管理它之前确认连接是否正常。
云基础设施模块可能有点复杂,因此我们将展示如何设置和安装 Amazon 模块。有关如何配置其他模块的详细信息,请使用ansible-doc查看它们的文档。
AWS 模块
AWS 模块的工作方式类似于大多数 AWS 工具的工作方式。这是因为它们使用 python boto 库,许多其他工具也使用该库,并且遵循 Amazon 发布的原始 AWS 工具的惯例。
最好以与安装 Ansible 相同的方式安装 boto。对于大多数使用场景,我们将在受管机器上运行模块,因此只需要在那里安装 boto 模块即可。我们可以通过以下方式安装 boto 库:
- 
Centos/RHEL/Fedora: yum install python-boto
- 
Ubuntu: apt-get install python-boto
- 
Pip: pip install boto
然后我们需要设置正确的环境变量。最简单的方法是通过在本地机器上使用 localhost 连接运行模块。如果我们这样做,shell 中的变量将被传递并自动对 Ansible 模块可用。以下是 boto 库用于连接 AWS 的变量:
| 变量名称 | 描述 | 
|---|---|
| AWS_ACCESS_KEY | 这是有效 IAM 账户的访问密钥 | 
| AWS_SECRET_KEY | 这是与上面的访问密钥对应的密钥 | 
| AWS_REGION | 这是默认的区域,除非被覆盖 | 
我们可以在示例中使用以下代码设置这些环境变量:
export AWS_ACCESS_KEY="AKIAIOSFODNN7EXAMPLE"
export AWS_SECRET_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
export AWS_REGION="us-east-1"
这些只是示例凭证,无法使用。设置好这些后,我们可以开始使用 AWS 模块。在接下来的代码块中,我们结合了本章中的几个模块来创建一台机器并将其添加到清单中。以下示例使用了尚未讨论的几个功能,如 register 和 delegate_to,这些将在第三章 高级 Playbooks 中讨论:
---
- name: Setup an EC2 instance
  hosts: localhost
  connection: local
  tasks:
    - name: Create an EC2 machine
      ec2:
        key_name: daniel-keypair
        instance_type: t2.micro
        image: ami-b66ed3de
        wait: yes
        group: webserver
        vpc_subnet_id: subnet-59483
        assign_public_ip: yes
      register: newmachines
    - name: Wait for SSH to start
      wait_for:
        host: "{{ newmachines.instances[0].public_ip }}"
        port: 22
        timeout: 300
      delegate_to: localhost
    - name: Add the machine to the inventory
      add_host:
        hostname: "{{ newmachines.instances[0].public_ip }}"
        groupname: new
- name: Configure the new machines
  hosts: new
  sudo: yes
  tasks:
    - name: Install a MOTD
      template:
        src: motd.j2
        dest: /etc/motd
总结
在本章中,我们覆盖了 playbook 文件中可用的部分。我们还学习了如何使用变量使 playbook 可维护,如何在做出更改时触发处理程序,最后,我们查看了某些模块在 playbook 中使用时的优势。你可以通过官方文档进一步探索 Ansible 提供的其他模块,docs.ansible.com/modules_by_category.html。
在下一章中,我们将深入研究 playbook 的更复杂功能。这将使我们能够构建更复杂的 playbook,能够部署和配置整个系统。
第三章:高级剧本
到目前为止,我们所查看的剧本都很简单,只是按顺序运行了一些模块。Ansible 允许对剧本的执行进行更多控制。通过以下技巧,你应该能够执行即使是最复杂的部署:
- 
并行执行操作 
- 
循环 
- 
条件执行 
- 
任务委派 
- 
额外变量 
- 
查找带变量的文件 
- 
环境变量 
- 
外部数据查找 
- 
存储数据 
- 
处理数据 
- 
调试剧本 
并行执行操作
默认情况下,Ansible 只会进行最多五次分叉,因此它一次只会在五台不同的机器上运行操作。如果你有大量的机器,或者你已经降低了这个最大分叉值,那么你可能希望异步启动任务。Ansible 执行此操作的方法是启动任务,然后轮询等待它完成。这允许 Ansible 在所有需要的机器上启动作业,同时仍然使用最大分叉数。
要并行执行操作,请使用 async 和 poll 关键字。async 关键字触发 Ansible 并行执行任务,其值将是 Ansible 等待命令完成的最大时间。poll 的值告诉 Ansible 每隔多长时间轮询一次,以检查命令是否完成。
如果你想在整个集群上运行 updatedb,它可能看起来像以下代码:
- hosts: all
  tasks:
    - name: Install mlocate
      yum: name=mlocate state=installed
    - name: Run updatedb
      command: /usr/bin/updatedb
      async: 300
      poll: 10
你会注意到,当你在超过五台机器上运行前面的示例时,yum 模块与 command 模块的行为不同。yum 模块会在前五台机器上运行,然后是接下来的五台,依此类推。然而,command 模块会在所有机器上并行运行,并在完成后显示状态。
如果你的命令启动了一个守护进程,最终会在某个端口监听,你可以在不进行轮询的情况下启动它,这样 Ansible 就不会检查它是否完成。然后,你可以继续进行其他操作,并稍后使用 wait_for 模块检查是否完成。要配置 Ansible 不等待任务完成,将 poll 的值设置为 0。
最后,如果你的任务需要极长的时间来执行,你可以告诉 Ansible 等待任务完成,无论它需要多长时间。为此,将 async 的值设置为 0。
在以下情况中,你需要使用 Ansible 的轮询功能:
- 
你有一个长期运行的任务,可能会触发超时 
- 
你需要在大量机器上执行操作 
- 
你有一个操作,不需要等待其完成 
也有一些情况,你不应使用 async 或 poll:
- 
如果你的任务获取了锁,阻止其他操作的运行 
- 
你的任务只需要很短的时间来执行 
循环
Ansible 允许你使用不同的输入多次重复运行一个模块,例如,如果你有多个文件需要设置相似的权限。这可以节省你大量的重复工作,并允许你对事实和变量进行迭代。
要做到这一点,您可以在操作上使用with_items关键字,并将值设置为您将要迭代的项目列表。这将为模块创建一个名为item的变量,该变量将逐个设置为您的模块正在迭代的每个项目。某些模块,例如yum,将优化此过程,以便不是为每个软件包执行单独的事务,而是一次性操作它们。
使用with_items,代码看起来像这样:
tasks:
- name: Secure config files file:
    path: "/etc/{{ item }}"
    mode: 0600
    owner: root
    group: root with_items: - my.cnf - shadow - fstab
除了循环固定项目或变量外,Ansible 还为我们提供了一个称为查找插件的工具。这些插件允许您告诉 Ansible 从外部某处获取数据。例如,您可能希望找到所有与特定模式匹配的文件,然后上传它们。
在此示例中,我们上传目录中的所有公钥,然后将它们组合成authorized_keys文件,如下例所示:
tasks: - name: Make key directory file:
    path: /root/.sshkeys
    ensure: directory
    mode: 0700
    owner: root
    group: root - name: Upload public keys copy:
    src: "{{ item }}"
    dest: /root/.sshkeys
    mode: 0600
    owner: root
    group: root with_fileglob: - keys/*.pub - name: Assemble keys into authorized_keys file assemble:
    src: /root/.sshkeys
    dest: /root/.ssh/authorized_keys
    mode: 0600
    owner: root
    group: root
可以在以下情况下使用重复模块:
- 
多次重复一个模块,并使用类似的设置 
- 
迭代列表的所有值 
- 
创建许多文件,稍后使用 assemble模块将它们合并成一个大文件
- 
当与 with_fileglob查找插件结合使用时,可以复制文件目录
条件执行
一些模块,例如copy模块,提供配置机制以跳过模块的执行。您还可以配置自己的跳过条件,只有当它们解析为true时才执行模块。如果您的服务器使用不同的打包系统或具有不同的文件系统布局,这将非常方便。它还可与set_fact模块一起使用,允许您计算许多不同的内容。
要跳过一个模块,您可以使用when关键字;这允许您提供一个条件。如果您设置的条件解析为假,则会跳过该模块。您分配给when的值是一个 Python 表达式。您可以在此时使用任何可用的变量或事实。
注意
如果您想根据条件处理列表中的某些项,则简单地使用when子句。when子句会分别处理列表中的每个项;正在处理的项可以通过{{ item }}作为变量使用。
以下代码是一个示例,显示如何在 Debian 和 Red Hat 系统中选择apt和yum之间的选择。
---
- name: Install VIM
  hosts: all
  tasks:
    - name: Install VIM via yum
      yum:
        name: vim-enhanced
        state: installed
      when: ansible_os_family == "RedHat"
    - name: Install VIM via apt
      apt:
        name: vim
        state: installed
      when: ansible_os_family == "Debian"
    - name: Unexpected OS family
      debug:
        msg: "OS Family {{ ansible_os_family }} is not supported"
        fail: yes
      when: ansible_os_family != "RedHat" and ansible_os_family != "Debian"
还有第三个子句,如果未识别操作系统,则打印消息并失败。
注意
这个功能可以在特定点暂停,并等待用户干预后继续。通常,当 Ansible 遇到错误时,它会停止当前操作而不运行任何处理程序。使用此功能,你可以添加带有条件的pause模块,该条件在意外情况下触发。这样,在正常情况下pause模块会被忽略;但在意外情况下,它将允许用户干预并在安全时继续。任务会像这样:
name: pause for unexpected conditions
pause: prompt="Unexpected OS"
when: ansible_os_family != "RedHat"
跳过某些操作有很多用法,以下是其中的一些:
- 
解决操作系统之间的差异 
- 
提示用户并仅在他们请求时执行相应操作 
- 
通过避免使用你知道不会改变任何内容但可能需要较长时间的模块来提高性能 
- 
拒绝修改具有特定文件存在的系统 
- 
检查自定义脚本是否已经执行 
任务委派
默认情况下,Ansible 会在配置的机器上同时运行所有任务。这对于需要配置大量独立机器,或者每台机器都负责与其他远程机器通信其状态的情况非常有效。然而,如果你需要在与 Ansible 当前操作的主机不同的主机上执行操作,你可以使用委派功能。
Ansible 可以配置为在与当前配置的主机不同的主机上运行任务,使用delegate_to键。该模块仍然会针对每台机器运行一次,但它将不再在目标机器上运行,而是在委派的主机上运行。可用的事实将是适用于当前主机的事实。这里,我们展示一个使用get_url选项的 playbook,从一堆 Web 服务器下载配置。
---
- name: Fetch configuration from all webservers
  hosts: webservers
  tasks:
    - name: Get config
      get_url:
        dest: "configs/{{ ansible_hostname }}"
        force: yes
        url: "http://{{ ansible_hostname }}/diagnostic/config"
      delegate_to: localhost
如果你要委派到localhost,在定义操作时可以使用快捷方式,自动使用本地机器。如果你将操作行的键定义为local_action,则意味着将委派到localhost。如果我们在前面的示例中使用这个,它会稍微简短一些,看起来会是这样:
--- #1
- name: Fetch configuration from all webservers     #2
  hosts: webservers     #3
  tasks:     #4
    - name: Get config     #5
      local_action: get_url dest=configs/{{ ansible_hostname }}.cfg url=http://{{ ansible_hostname }}/diagnostic/config     #6
委派不仅限于本地机器。你可以委派给清单中的任何主机。你可能希望进行委派的其他原因包括:
- 
在部署前将主机从负载均衡器中移除 
- 
更改 DNS 以将流量引导到你即将更改的服务器之外 
- 
在存储设备上创建 iSCSI 卷 
- 
使用外部服务器检查是否能正常访问网络外部 
附加变量
你可能已经在上一章的模板示例中看到了我们使用了一个名为group_names的变量。这是 Ansible 本身提供的魔法变量之一。在写这篇文章时,共有七个这样的变量,接下来的章节将描述这些变量。
主机变量
hostvars 变量允许你检索当前 play 所处理的所有主机的变量。如果当前 play 尚未在该托管主机上运行 setup 模块,则只会提供该主机的变量。你可以像访问其他复杂变量一样访问它,例如 ${hostvars.hostname.fact},因此,要获取名为 ns1 的服务器上运行的 Linux 发行版,可以使用 ${hostvars.ns1.ansible_distribution}。以下示例将名为 ns1 的服务器设置为名为 zone master 的变量。然后它调用 template 模块,利用此信息为每个区域设置主机。
---
- name: Setup DNS Servers
  hosts: allnameservers
  tasks:
    - name: Install BIND
      yum:
        name: named
        state: installed
- name: Setup Slaves
  hosts: slavenamesservers
  tasks:
    - name: Get the masters IP
      set_fact:
        dns_master: "{{ hostvars.ns1.ansible_default_ipv4.address }}"
    - name: Configure BIND
      template:
        dest: /etc/named.conf src: templates/named.conf.j2
注意
使用 hostvars,你可以进一步抽象环境中的模板。如果你嵌套变量调用,那么你就可以在 play 的变量部分中添加主机名,而不是直接放入 IP 地址。要找到名为 the_machine 的机器的地址,你可以使用 {{ hostvars.[the_machine].default_ipv4.address }}。
groups 变量
groups 变量包含按清单组分组的所有主机的列表。它让你访问你配置的所有主机。这是一个非常强大的工具,它允许你遍历整个组,并为每个主机应用一个动作。
---
- name: Configure the database
  hosts: dbservers
  user: root
  tasks:
    - name: Install mysql
      yum:
        name: "{{ item }}"
        state: installed
      with_items:
      - mysql-server
      - MySQL-python
    - name: Start mysql
      service:
        name: mysqld
        state: started
        enabled: true
    - name: Create a user for all app servers
      with_items: groups.appservers
      mysql_user:
        name: kate
        password: test
        host: "{{ hostvars.[item].ansible_eth0.ipv4.address }}" state: present
注意
groups 变量不包含组中的实际主机;它包含表示主机名的字符串,这些字符串来自清单。这意味着,如果需要,你必须使用嵌套变量扩展来访问 hostvars 变量。
你甚至可以使用这个变量为所有机器创建 known_hosts 文件,其中包含所有其他机器的 host 密钥。这将允许你在一台机器与另一台机器之间进行 SSH 连接,而无需确认远程主机的身份。它还会在机器退出服务时自动删除它们,或在机器被替换时进行更新。以下是一个模板,用于创建一个 known_hosts 文件:
{% for host in groups['all'] %}
{{ hostvars[host]['ansible_hostname'] }}
{{ hostvars[host]['ansible_ssh_host_key_rsa_public'] }}
{% endfor %}
使用此模板的 playbook 将如下所示:
---
hosts: all
tasks:
- name: Setup known hosts
  hosts: all
  tasks:
    - name: Create known_hosts
      template:
        src: templates/known_hosts.j2 dest: /etc/ssh/ssh_known_hosts
        owner: root
        group: root mode: 0644
group_names 变量
group_names 变量包含一个字符串列表,列出了当前主机所属的所有组。这不仅对于调试有用,还可以用于检测组成员身份的条件。在上一章中,我们使用这个变量设置了一个名称服务器。
这个变量主要用于跳过某个任务或在模板中作为条件。例如,如果你有两种 SSH 守护进程配置,一种是安全的,另一种是较不安全的,但你只希望在安全组中的机器上使用安全配置,你可以这样做:
- name: Setup SSH
  hosts: sshservers
  tasks:
    - name: For secure machines
      set_fact:
        sshconfig: files/ssh/sshd_config_secure
      when: "'secure' in group_names"
    - name: For non-secure machines
      set_fact:
        sshconfig: files/ssh/sshd_config_default
      when: "'secure' not in group_names"
    - name: Copy over the config
      copy:
        src: "{{ sshconfig }}"
        dest: /tmp/sshd_config
注意
在之前的示例中,我们使用了 set_fact 模块来为每个情况设置事实值,然后使用了 copy 模块。我们本可以用 copy 模块代替 set_fact 模块,从而减少一个任务。这么做的原因是 set_fact 模块在本地运行,而 copy 模块在远程运行。当你首先使用 set_fact 模块并仅调用一次 copy 模块时,所有机器上的复制任务会并行执行。如果你使用两个带有条件的 copy 模块,那么每个模块会分别在相关机器上执行。由于 copy 是这两者中更长的任务,最适合并行执行。
inventory_hostname 变量
inventory_hostname 变量存储了作为清单中记录的服务器主机名。如果你选择不在当前主机上运行 setup 模块,或者由于各种原因 setup 模块检测到的值不正确,那么应该使用此变量。这在你进行机器初始设置并更改主机名时非常有用。
inventory_hostname_short 变量
inventory_hostname_short 变量与之前的变量相同;然而,它仅包括第一个点之前的字符。因此,对于 host.example.com,它将返回 host。
inventory_dir 变量
inventory_dir 变量是包含清单文件的目录的路径名。
inventory_file 变量
inventory_file 变量与之前的变量相同,唯一的不同是它还包含了文件名。
使用变量查找文件
所有模块都可以通过解引用 {{ 和 }} 来将变量作为其参数的一部分。你可以利用这一点根据变量加载特定文件。例如,你可能想根据正在使用的架构选择不同的 config 文件来配置 NRPE(一个 Nagios 检查守护进程)。以下是这样的实现方式:
---
- name: Configure NRPE for the right architecture
  hosts: ansibletest
  user: root
  tasks:
    - name: Copy in the correct NRPE config file
      copy:
        src: "files/nrpe.{{ ansible_architecture }}.conf" dest: "/etc/nagios/nrpe.cfg"
在 copy 和 template 模块中,你还可以配置 Ansible 查找一组文件,并通过第一个文件来找到它们。这允许你配置文件查找路径;如果未找到该文件,将使用第二个文件,以此类推,直到找到文件或到达列表末尾。如果文件未找到,则模块将失败。此功能通过 first_available_file 键触发,并在操作中引用 {{ item }}。以下代码是该功能的示例:
---
- name: Install an Apache config file
  hosts: ansibletest
  user: root
  tasks:
   - name: Get the best match for the machine
     copy:
       dest: /etc/apache.conf
       src: "{{ item }}"
     first_available_file:
      - "files/apache/{{ ansible_os_family }}-{{ ansible_architecture }}.cfg"
      - "files/apache/default-{{ ansible_architecture }}.cfg"
      - files/apache/default.cfg
注意
请记住,你可以从 Ansible 命令行工具运行 setup 模块。当你在 playbook 或模板中大量使用变量时,这非常有用。要检查某个 play 将可用的事实,只需复制主机模式的值并运行以下命令:
ansible [host pattern] -m setup
在 CentOS x86_64 机器上,此配置会在浏览files/apache/时首先查找RedHat-x86_64.cfg文件。如果该文件不存在,则会在浏览file/apache/时查找default-x86_64.cfg文件,最后如果什么都没有找到,它会尝试使用default.cfg。
环境变量
通常,Unix 命令会利用某些环境变量。常见的例子有 C Makefile、安装程序和 AWS 命令行工具。幸运的是,Ansible 使这变得非常简单。如果你想将文件从远程机器上传到 Amazon S3,你可以按如下方式设置 Amazon 访问密钥。你还会看到我们安装了 EPEL,以便能够安装 pip,pip 用于安装 AWS 工具。
---
- name: Upload a remote file via S3
  hosts: ansibletest
  user: root
  tasks:
    - name: Setup EPEL
      command: >
        rpm -ivh http://download.fedoraproject.org/pub/epel/6/i386/ epel-release-6-8.noarch.rpm
        creates=/etc/yum.repos.d/epel.repo
    - name: Install pip
      yum:
        name: python-pip
        state: installed
    - name: Install the AWS tools
      pip:
        name: awscli
        state: present
    - name: Upload the file
      shell: >
        aws s3 put-object
        --bucket=my-test-bucket
        --key={{ ansible_hostname }}/fstab
        --body=/etc/fstab
        --region=eu-west-1
      environment:
        AWS_ACCESS_KEY_ID: XXXXXXXXXXXXXXXXXXX
        AWS_SECRET_ACCESS_KEY: XXXXXXXXXXXXXXXXXXXXX
注释
在内部,Ansible 将环境变量设置到 Python 代码中;这意味着任何已经使用环境变量的模块都可以利用这里设置的变量。如果你编写自己的模块,你应该考虑某些参数是否应该作为环境变量而不是参数使用。
一些 Ansible 模块,如get_url、yum和apt,也将使用环境变量来设置它们的代理服务器。以下是一些你可能想要设置环境变量的其他情况:
- 
运行应用程序安装程序 
- 
在使用 shell模块时向路径中添加额外的项
- 
从系统库搜索路径中未包含的位置加载库 
- 
在运行模块时使用 LD_PRELOAD黑客技术
外部数据查找
Ansible 在版本 0.9 中引入了查找插件。这些插件允许 Ansible 从外部源获取数据。Ansible 提供了多个插件,但你也可以编写自己的插件。这为配置的灵活性打开了大门。
查找插件是用 Python 编写的,并且在控制机器上运行。它们有两种执行方式:直接调用和with_*键。直接调用在你想像使用变量一样使用它们时很有用。使用with_*键在你想将它们作为循环使用时很有用。在前面的部分中,我们讨论了with_fileglob,它就是这种情况的一个例子。
在下一个示例中,我们直接使用查找插件从environment中获取http_proxy值,并将其传递到配置的机器。这确保了我们正在配置的机器将使用相同的代理服务器来下载文件。
---
- name: Downloads a file using a proxy
  hosts: all
  tasks:
    - name: Download file
      get_url:
        dest: /var/tmp/file.tar.gz url: http://server/file.tar.gz
      environment:
        http_proxy: "{{ lookup('env', 'http_proxy') }}"
注释
你也可以在变量部分使用查找插件。这不会立即查找结果并将其放入变量中,就像你可能想的那样;相反,它会将结果作为宏存储,并在每次使用时查找它。如果你正在使用某些值可能随时间变化的内容,这一点很有用。
使用 with_* 形式的 lookup 插件将允许你遍历一些通常无法遍历的内容。你可以像这样使用任何插件,但返回列表的插件最为有用。以下代码演示了如何动态地注册一个 webapp farm。
---
- name: Registers the app server farm
  hosts: localhost
  connection: local
  vars:
    hostcount: 5
  tasks:
   - name: Register the webapp farm
      local_action: add_host name={{ item }} groupname=webapp
      with_sequence: start=1 end={{ hostcount }} format=webapp%02x
如果你使用这个例子,你可以添加一个任务来创建每个虚拟机,然后创建一个新的 play 来配置它们。
lookup 插件有用的场景如下:
- 
将整个 Apache 配置目录复制到 conf.d风格的目录中
- 
使用环境变量来调整 playbook 的行为 
- 
从 DNS TXT 记录中获取配置 
- 
将命令输出提取到变量中 
存储结果
几乎每个模块都会输出一些内容,甚至 debug 模块也是如此。大多数情况下,唯一使用的变量是名为 changed 的变量。changed 变量帮助 Ansible 决定是否运行处理程序,以及以什么颜色打印输出。然而,如果你愿意,你可以存储返回的值并在 playbook 中稍后使用它们。在这个示例中,我们查看 /tmp 目录的模式,并创建一个新的目录 /tmp/subtmp,其模式与之相同,如下所示。
---
- name: Using register
  hosts: ansibletest
  user: root
  tasks:
    - name: Get /tmp info
      file:
        dest: /tmp
        state: directory
      register: tmp
    - name: Set mode on /var/tmp
      file:
        dest: /tmp/subtmp
        mode: "{{ tmp.mode }}"
        state: directory
一些模块,如前面例子中的 file 模块,可以配置为仅提供信息。通过将其与 register 功能结合,你可以创建能够检查环境并计算如何继续的 playbook。
注意
将 register 功能与 set_fact 模块结合使用,可以对从模块返回的数据进行处理。这让你能够计算值并对这些值进行数据处理,使得你的 playbook 比以往更加智能和灵活。
Register 允许你基于已有的模块为主机创建自定义的事实。这在许多不同情况下都非常有用:
- 
获取远程目录中的文件列表,并使用 fetch 下载所有文件 
- 
当前一个任务发生变化时,在处理程序运行之前执行任务 
- 
获取远程主机的 SSH 密钥内容,并构建一个 known_hosts文件
处理数据
Ansible 使用 Jinja2 过滤器来允许你以基本模板无法实现的方式转换数据。当 playbook 中可用的数据格式不符合需求,或者需要在与模块或模板配合使用之前进行复杂的处理时,我们会使用过滤器。过滤器可以在我们通常使用变量的地方使用,例如在模板中、作为模块的参数或在条件语句中。使用过滤器时,只需提供变量名称、一个管道符号,然后是过滤器名称。我们还可以使用多个过滤器名称,过滤器通过管道符分隔,按从左到右的顺序应用。以下是一个示例,确保所有用户都使用小写的用户名创建:
---
- name: Create user accounts
  hosts: all
  vars:
    users:
  tasks:
    - name: Create accounts
      user: name={{ item|lower }} state=present
      with_items:
        - Fred
        - John
        - DanielH
以下是一些你可能会发现有用的常见过滤器:
| 过滤器 | 描述 | 
|---|---|
| min | 当参数为列表时,返回列表中最小的值。 | 
| max | 当参数为列表时,返回列表中最大的值。 | 
| random | 当参数为列表时,从列表中随机选择一个项目。 | 
| changed | 当用于使用 register 关键字创建的变量时,如果任务更改了任何内容则返回 true;否则返回false。 | 
| failed | 当用于使用 register 关键字创建的变量时,如果任务失败则返回 true;否则返回false。 | 
| skipped | 当用于使用 register 关键字创建的变量时,如果任务未更改任何内容则返回 true;否则返回false。 | 
| default(X) | 如果变量不存在,则使用 X 的值。 | 
| unique | 当参数为列表时,返回一个没有重复项的列表。 | 
| b64decode | 将变量中的 base64 编码字符串转换为其二进制表示。这在与 slurp 模块一起使用时非常有用,因为它将其数据作为 base64 编码的字符串返回。 | 
| replace(X, Y) | 返回字符串的副本,其中任何出现的 X都被Y替换。 | 
| join(X) | 当变量为列表时,返回所有条目用 X分隔的字符串。 | 
调试 playbook
有几种方法可以调试 playbook。Ansible 包括详细模式和专门用于调试的debug模块。您还可以使用诸如fetch和get_url等模块来获取帮助。这些调试技术还可以用于检查模块在学习如何使用它们时的行为。
debug 模块
使用debug模块非常简单。它接受两个可选参数,msg和fail.msg,用于设置模块将打印的消息和fail,如果设置为yes,则表示对 Ansible 的失败,这将导致它停止处理该主机的 playbook。我们在跳过模块部分早些时候使用了此模块,以便在操作系统不被识别时退出 playbook。
在以下示例中,我们将展示如何使用debug模块列出机器上所有可用的接口:
---
- name: Demonstrate the debug module
  hosts: ansibletest
  user: root
  vars:
    hostcount: 5
  tasks:
    - name: Print interface
      debug:
        msg: "{{ item }}"
      with_items: ansible_interfaces
前述代码输出如下:
PLAY [Demonstrate the debug module] *********************************
GATHERING FACTS *****************************************************
ok: [ansibletest]
TASK: [Print interface] *********************************************
ok: [ansibletest] => (item=lo) => {"item": "lo", "msg": "lo"}
ok: [ansibletest] => (item=eth0) => {"item": "eth0", "msg": "eth0"}
PLAY RECAP **********************************************************
ansibletest                : ok=2    changed=0    unreachable=0    failed=0
正如您所看到的,使用debug模块来查看 play 期间变量的当前值非常容易。
详细模式
调试的另一个选项是详细选项。在使用详细选项运行 Ansible 时,它会打印每个模块运行后返回的所有值。如果您使用前面介绍的register关键字,这尤其有用。要在详细模式下运行ansible-playbook,只需在命令行中添加--verbose如下:
ansible-playbook --verbose playbook.yml
检查模式
除了详细模式外,Ansible 还包括检查模式和差异模式。你可以通过在命令行中添加 --check 来使用检查模式,使用 --diff 来启用差异模式。检查模式指示 Ansible 在不对远程系统进行实际更改的情况下遍历剧本。这使你能够获取 Ansible 计划对配置系统进行的更改列表。
注意
这里需要注意的是,Ansible 的检查模式并不完美。任何未实现检查功能的模块将被跳过。此外,如果跳过的模块提供了更多的变量,或者这些变量依赖于模块实际更改某些内容(例如文件大小),那么这些变量将无法使用。使用 command 或 shell 模块时,这是一个明显的局限性。
差异模式显示由 template 模块所做的更改。这一局限性是因为 template 文件只适用于文本文件。如果你尝试为 copy 模块中的二进制文件提供差异,结果几乎是无法读取的。差异模式还可以与检查模式一起使用,展示由于处于检查模式而未做的计划更改。
暂停模块
另一种技巧是使用 pause 模块,在你检查正在运行的配置机器时暂停剧本。这样,你可以看到模块在当前剧本位置所做的更改,然后继续观察剧本的其余部分。
摘要
在本章中,我们探讨了编写剧本的更多高级细节。现在,你应该能够使用委派、循环、条件语句和事实注册等功能,使你的剧本更容易维护和编辑。我们还研究了如何访问其他主机的信息、为模块配置环境以及从外部来源收集数据。最后,我们介绍了一些调试剧本的技巧,帮助解决剧本执行异常的问题。
在下一章,我们将讨论如何在更大规模的环境中使用 Ansible。内容将包括提高执行时间较长的剧本性能的方法。我们还将介绍一些能够使剧本更易于维护的特性,特别是通过目的将其拆分成多个部分。
第四章. 更大的项目
到目前为止,我们一直在查看单个剧本中的单个剧本文件。这种方法适用于简单的基础设施,或者在将 Ansible 用作简单的部署机制时。然而,如果你有一个庞大而复杂的基础设施,那么你需要采取措施以防止局面失控。本章将包括以下主题:
- 
将剧本拆分成不同的文件,并从其他位置包含它们 
- 
使用角色包含多个执行相似功能的文件 
- 
提高 Ansible 配置机器速度的方法 
包含
你在面对复杂的基础设施时会遇到的第一个问题是,剧本文件的大小会迅速增加。大的剧本文件会变得难以阅读和维护。Ansible 允许你通过包含来解决这个问题。
包含允许你将剧本拆分为多个部分。然后,你可以从其他剧本中包含每个部分。这使得你可以为不同的目的构建多个不同的部分,并将它们全部包含在一个主剧本中。
包含有四种类型,分别是变量包含、剧本包含、任务包含和处理器包含。关于从外部vars_file文件包含变量的内容,已经在第二章,简单剧本中讨论过了。以下是每种包含的描述:
- 
变量包含:它们允许你将变量放在外部 YAML 文件中 
- 
剧本包含:它们用于将其他文件中的剧本包含到单个剧本中 
- 
任务包含:它们允许你将公共任务放到其他文件中,并在需要的地方包含它们 
- 
处理器包含:它们允许你将所有的处理器放在一个地方 
我们将在接下来的章节中探讨这些包含;不过,关于从外部vars_file文件包含变量的内容,已经在第二章,简单剧本中讨论过了,因此我们不再详细讨论。
任务包含
当你有许多重复的公共任务时,可以使用任务包含。例如,你可能有一组任务,在配置机器之前,将其从监控和负载均衡器中移除。你可以将这些任务放在一个单独的 YAML 文件中,然后从主任务中包含它们。
任务包含会继承它们所包含的剧本中的事实。你还可以提供自己的变量,这些变量会传递到任务中,并可供使用。
最后,任务包含可以应用条件判断。如果你这样做,Ansible 将自动为每个包含的任务单独添加条件判断。这些任务仍然会被包含。在大多数情况下,这是不重要的区别;然而,在变量可能发生变化的情况下,这是很重要的。
作为任务包含的文件包含一系列任务。如果你假设某些变量、主机或组的存在,那么应该在文件顶部的注释中说明它们。这可以帮助后来想要重用该文件的人。
因此,如果你想创建一批用户并设置他们的环境和公钥,你可以将每个用户的任务拆分到一个单独的文件中。这个文件将类似于以下代码:
---
# Requires a user variable to specify user to setup
- name: Create user account
  user:
    name: "{{ user }}"
    state: present
- name: Make user SSH config dir
  file:
    path: "/home/{{ user }}/.ssh"
    owner: "{{ user }}"
    group: "{{ user }}"
    mode: 0600
    state: directory
- name: Copy in public key
  copy:
    src: "keys/{{ user }}.pub"
    dest: "/home/{{ user }}/.ssh/authorized_keys"
    mode: 0600
    owner: "{{ user }}"
    group: "{{ user }}"
我们预计将会传入一个名为user的变量,并且他们的公钥会存放在keys目录中。会创建帐户,生成ssh config目录,最后我们可以将他们的公钥复制到其中。使用这个config文件的最简单方法是通过你在第三章中学习的with_items关键字来包含它,高级 Playbooks。这将类似于以下代码:
---
- hosts: ansibletest
  user: root
  tasks:
    - include: usersetup.yml user={{ item }}
      with_items:
        - mal
        - dan
        - kate
处理器包含
在编写 Ansible playbook 时,你会发现自己多次重复使用相同的处理器。例如,用于重启 MySQL 的处理器在任何地方看起来都一样。为了简化这一过程,Ansible 允许你在处理器部分包含其他文件。处理器包含的写法与任务包含类似。你应该确保为每个处理器提供一个名称,否则你将无法在任务中轻松引用它们。一个处理器包含文件看起来类似于以下代码:
---
- name: config sendmail
  command: make -C /etc/mail
  notify: reload sendmail
- name: config aliases
  command: newaliases
  notify: reload sendmail
- name: reload sendmail
  service:
    name: sendmail
    state: reloaded
- name: restart sendmail
  service:
    name: sendmail
    state: restarted
本文件提供了在配置sendmail后你可能需要处理的几项常见任务。通过将以下处理器包含在各自的文件中,你可以在需要更改sendmail配置时轻松地重用它们:
- 
第一个处理器重新生成 sendmail数据库的config文件,并在稍后触发sendmail的reload文件。
- 
第二个处理器初始化 aliases数据库,并且还会安排一个sendmail的reload文件。
- 
第三个处理器重新加载 sendmail;它可能由前两个任务触发,或者也可以直接通过任务触发。
- 
第四个处理器在被触发时重启 sendmail;如果你升级了sendmail到新版本,这将非常有用。
注意
处理器可以触发其他处理器,只要它们仅触发后面指定的处理器,而不是已触发的处理器。这意味着你可以设置一系列相互调用的级联处理器。这将避免你在任务的 notify 部分写出一长串处理器。
使用前面的处理器文件现在变得简单了。我们只需要记住,如果我们更改了sendmail配置文件,则应触发config sendmail,如果更改了aliases文件,则应触发config aliases。以下代码为我们展示了这个示例:
---
  hosts: mailers
  tasks:
    - name: update sendmail
      yum:
        name: sendmail
        state: latest
      notify: restart sendmail
    - name: configure sendmail
      template:
        src: templates/sendmail.mc.j2 dest: /etc/mail/sendmail.mc
      notify: config sendmail
  handlers:
    - include: sendmailhandlers.yml
这个 playbook 确保 sendmail 已安装。如果没有安装,或者没有运行最新版本,则会安装或更新它。更新后,它会安排重启,以便我们可以确信在 playbook 执行完毕后,最新版本将会运行。在下一步中,我们用模板替换 sendmail 配置文件。如果配置文件被模板更改,则会重新生成 sendmail 配置文件,最后 sendmail 会被重新加载。
Playbook includes
Playbook includes 用于在你想要为一组机器包含一整套任务时使用。例如,你可能有一个 play,它收集几个机器的主机密钥,并构建一个 known_hosts 文件,将其复制到所有机器上。
虽然任务包含允许你包含任务,但 playbook includes 允许你包含完整的 play。这使你能够选择你希望执行的主机,并为通知事件提供处理程序。由于你包含的是完整的 playbook 文件,你还可以包含多个 plays。
Playbook includes 允许你嵌入完全自包含的文件。因此,你需要提供它所需的任何变量。如果它们依赖于某些特定的主机或组,应该在文件顶部的注释中注明。
当你希望一次执行多个不同的操作时,这非常方便。例如,假设我们有一个名为 drfailover.yml 的 playbook,用于切换到我们的灾难恢复站点,另一个名为 upgradeapp.yml 的 playbook 用于升级应用,另一个名为 drfailback.yml 的 playbook 用于故障恢复,最后一个名为 drupgrade.yml。这些 playbook 分别使用可能都是有效的;然而,在执行站点升级时,你可能希望一次执行所有这些操作。你可以像下面的代码那样做到这一点:
---
- include "drfailover.yml"
- include "upgradeapp.yml"
- include "drfailback.yml"
- name: Notify management
  hosts: local
  tasks:
    - mail
        to: "mgmt-team@example.com"
        msg: 'The application has been upgraded and is now live'
- include "drupgrade.yml"
正如你所看到的,你可以在包含其他 playbook 的 playbooks 中放入完整的 plays。
角色
如果你的 playbook 开始超出 includes 能解决的问题,或者你开始收集大量模板,你可能需要使用角色。Ansible 中的角色允许你将文件按定义的结构组合在一起。它们本质上是对 includes 的扩展,能够自动处理一些事情,这有助于你在代码库中组织这些文件。
角色允许你将变量、文件、任务、模板和处理程序放在一个文件夹中,然后轻松地将它们包含进去。你还可以在角色中包含其他角色,这实际上创建了一个依赖树。与任务包含类似,它们也可以传递变量给角色。使用这些功能,你应该能够构建自包含的角色,方便与他人共享。
角色通常用于管理机器提供的服务,但它们也可以是守护进程、选项或简单的特性。你可能希望在角色中配置的内容如下:
- 
Web 服务器,如 Nginx 或 Apache 
- 
根据机器的安全级别定制的每日信息 | 
- 
运行 PostgreSQL 或 MySQL 的数据库服务器 | 
要管理 Ansible 中的角色,请执行以下步骤: |
- 
创建一个名为 roles的文件夹,并将你的 playbooks 放入其中。 |
- 
在 roles文件夹中,为你想要的每个角色创建一个文件夹。 |
- 
在每个角色的文件夹中,创建名为 files、handlers、meta、tasks、templates和vars的文件夹。如果你不打算使用所有这些文件夹,可以省略不需要的部分。当使用角色时,Ansible 会默默忽略任何缺失的文件或目录。 |
- 
在你的 playbooks 中,添加关键字 roles,后跟你希望应用于主机的角色列表。 |
- 
例如,如果你有 common、apache、website1和website2角色,你的目录结构看起来会像以下示例。site.yml文件用于重新配置整个站点,而webservers1.yml和webservers2.yml文件用于配置每个 Web 服务器集群。![Roles]() | |
以下文件可能出现在 website1.yml 中。它展示了一个 playbook,将 common、apache 和 website1 角色应用到清单中的 website1 组。website1 角色使用更详细的格式进行包含,允许我们将变量传递给角色,如下所示: |
---
- name: Setup servers for website1.example.com
  hosts: website1
  roles:
    - common
    - apache
    - { role: website1, port: 80 }
对于名为 common 的角色,Ansible 将尝试加载 roles/common/tasks/main.yml 作为任务包含,roles/common/handlers/main.yml 作为处理程序包含,roles/common/vars/main.yml 作为变量文件包含。如果这些文件都缺失,Ansible 会抛出错误;但是,如果其中一个文件存在,则缺失的其他文件将被忽略。以下目录是 Ansible 默认安装时使用的目录(其他目录可能被不同的模块使用): |
| 目录 | 描述 | 
|---|---|
| tasks | tasks文件夹应包含一个main.yml文件,该文件应包括该角色的任务列表。任何包含在这些角色中的任务也将在此文件夹中查找其文件。这允许你将大量任务拆分到单独的文件中,并使用任务包含的其他功能。 | 
| files | files文件夹是用于存放由复制或脚本模块使用的角色的默认文件位置。 | 
| templates | templates目录是模板模块将自动查找角色中包含的 jinja2 模板的位置。 | 
| handlers | handlers文件夹应包含一个main.yml文件,该文件指定角色的处理程序,并且该文件夹中的任何包含文件也会在相同位置查找文件。 | 
| vars | vars文件夹应包含一个main.yml文件,该文件包含此角色的变量。 | 
| meta | meta文件夹应包含一个main.yml文件。此文件可以包含角色的设置以及其依赖项的列表。此功能仅在 Ansible 1.3 及以上版本中可用。 | 
| default | 如果你希望将变量传递给该角色并希望使其可选,则应使用 default文件夹。该文件夹中的main.yml文件将被读取,以获取可以被从调用角色的 playbook 中传递的变量覆盖的初始变量值。此功能仅适用于 Ansible 1.3 及以上版本。 | 
使用角色时,copy、template 和 script 模块的行为会略有改变。除了从 playbook 文件所在的目录中查找文件外,Ansible 还会在角色的位置查找文件。例如,如果你使用名为 common 的角色,这些模块的行为将变为如下:
- 
copy模块将查找roles/common/files中的文件。
- 
template模块将首先在roles/common/templates中查找模板。
- 
script模块将首先在roles/common/files中查找文件。
- 
其他模块可能会决定在 roles/common/内的其他文件夹中查找其数据。模块的文档可以通过ansible-doc获取,正如在 第一章 的 模块帮助 部分中讨论的那样,开始使用 Ansible。
角色元数据
使用角色元数据允许我们指定我们的角色依赖于其他角色。例如,如果你正在部署的应用程序需要发送电子邮件,你的角色可能依赖于 Postfix 角色。这意味着在应用程序设置和安装之前,Postfix 将被安装和设置。
meta/main.yml 文件将类似于以下代码:
---
allow_duplicates: no
dependencies:
  - apache
allow_duplicates 行设置为 no,这是默认值。如果你将其设置为 no,那么如果该角色在相同的参数下被包含两次,Ansible 将不会再次运行该角色。如果将其设置为 yes,即使该角色之前已经运行过,也会重复执行。你可以将其保持为 off,而不是设置为 no。
依赖项以与角色相同的格式指定。这意味着你可以在此传递变量;可以是静态值或传递给当前角色的变量。
角色默认值
Ansible 1.3 版本新增的第二个功能是变量默认值。如果你在角色的 defaults 目录中放置一个 main.yml 文件,这些变量将被读取到角色中;然而,它们可以被 vars/main.yml 文件中的变量,或者在包含角色时传递给角色的变量覆盖。这使得向角色传递变量变得可选。这些文件与其他变量文件完全相同。例如,如果你在角色中使用了一个名为 port 的变量,并且你希望将其默认设置为端口 80,那么你的 defaults/main.yml 文件将类似于以下代码:
---
port: 80
加速执行
随着你在 Ansible 配置中添加越来越多的机器和服务,你会发现系统运行得越来越慢。幸运的是,有几种技巧可以让 Ansible 在更大规模下运行。
配置
Ansible 不仅仅限于配置我们的机器;我们还可以用它来创建我们要配置的机器。我们不仅限于创建要配置的机器,还可以创建网络、负载均衡器、DNS 条目,甚至是整个基础设施。你甚至可以在配置机器之前,通过使用group、group_by和add_host模块让这一切自动发生。
在以下示例中,我们使用 Google Compute 创建了两台机器,并在其上安装和启动了 MySQL 服务器:
---
- name: Setup MySQL Infrastructure
  hosts: localhost
  connection: local
  tasks:
    - name: Start GCE Nodes
      gce:
        image: centos-6
        name: "mysql-{{ item }}"
        tags: mysql
        zone: us-central1-a
      with_sequence: count=2
      register: nodes
      when: '"mysql-{{ item }}" not in groups.all'
    - name: Wait for the nodes to start
      wait_for:
          host: "{{ item.instance_data[0].public_ip }}"
          port: 22
      with_items: nodes.results
      when: not item|skipped
    - name: Register the hosts in a group
      add_host:
          name: "{{ item.instance_data[0].name }}"
          ansible_ssh_host: "{{ item.instance_data[0].public_ip }}"
          groups: "tag_mysql"
      with_items: nodes.results
      when: not item|skipped
- name: Setup MySQL
  hosts: tag_mysql
  tasks:
    - name: Install MySQL
      yum:
        name: mysql
        state: present
    - name: Start MySQL
      service:
        name: mysqld
        state: started
        enabled: yes
标签
Ansible 标签是一个特性,它允许你选择剧本中需要执行的部分以及应跳过的部分。虽然 Ansible 模块是幂等的,并且在没有更改时会自动跳过,但这通常需要与远程主机建立连接。yum 模块在确定模块是否为最新时通常比较慢,因为它需要刷新所有的仓库。
如果你知道不需要执行某些操作,可以选择只运行那些已被标记特定标签的任务。这甚至不尝试运行任务,它只是跳过它。这将节省几乎所有模块的时间,即使没有任何操作需要执行。
假设你有一台机器上有大量的 Shell 账户,同时也配置了多个服务在其上运行。现在,假设某个用户的 SSH 密钥被泄露,需要立即移除。你可以不必运行整个剧本,或者重新编写剧本以只包括移除该密钥所需的步骤,而是可以通过已存在的剧本和 SSH 密钥标签来运行,这样它只会运行必要的步骤来复制新的密钥,立即跳过其他步骤。
这在你拥有一个包含整个基础设施的剧本时特别有用。有了这个设置,你可以迅速部署安全补丁、修改密码,并在整个基础设施中撤销密钥,尽可能快地进行操作。
给任务打标签非常简单;只需添加一个名为tag的键,并将其值设置为你希望给予的标签列表。以下代码展示了我们如何做到这一点:
---
- name: Install and setup our webservers
  hosts: webservers
  tasks:
  - name: install latest software
    yum
      name: "{{ item }}"
      state: latest
    notify: restart apache
    tags:
      - patch
    with_items:
    - httpd
    - webalizer
  - name: Create subdirectories
    file
      dest: "/var/www/html/{{ item }}"
      state: directory
      mode: 755 owner: apache
      group: apache
    tags:
      - deploy
    with_items:
      - pub
  - name: Copy in web files
    copy
      src: "website/{{ item }}"
      dest: "/var/www/html/{{ item }}"
      mode: 0755
      owner: apache
      group: apache
    tags:
      - deploy
    with_items:
      - index.html
      - logo.png
      - style.css
      - app.js
      - pub/index.html
  - name: Copy webserver config
    tags:
      - deploy
      - config
    copy
      src: website/httpd.conf
      dest: /etc/httpd/conf/httpd.conf
      mode: 0644
      owner: root
      group: root
    notify: reload apache
  - name: set apache to start on startup
    service
      name: httpd
      state: started
      enabled: yes
  handlers:
  - name: reload apache
    service: name=httpd state=reloaded
  - name: restart apache
    service: name=httpd state=restarted
这个剧本定义了patch、deploy和config标签。如果你事先知道要执行哪个操作,可以通过提供正确的参数来运行 Ansible,只执行你选择的操作。如果在命令行中没有提供标签,则默认会运行所有任务。例如,如果你只想让 Ansible 执行标记为deploy的任务,你可以运行以下命令:
$ ansible-playbook webservers.yml --tags deploy
除了适用于离散任务,标签也可以应用于角色,这使得 Ansible 只应用在命令行中提供的标签对应的角色。你可以像应用标签到任务一样应用它们。例如,参考以下代码:
---
- hosts: website1
  roles:
    - common
    - { role: apache, tags: ["patch"] }
    - { role: website2, tags: ["deploy", "patch"] }
在前面的代码中,common 角色没有任何标签,如果应用了任何标签,它将不会被运行。如果应用了 patch 标签,则会应用 apache 和 website2 角色,但不会应用 common。如果应用了 deploy 标签,则只会运行 website2 角色。这样可以缩短修补服务器或运行部署所需的时间,因为不必要的步骤将被完全跳过。
Ansible 的拉取模式
Ansible 包含一个拉取模式,可以显著提升你的 playbook 的可扩展性。到目前为止,我们只讲了如何通过 SSH 使用 Ansible 配置另一台机器。这与 Ansible 的拉取模式不同,拉取模式在你想要配置的主机上运行。由于 ansible-pull 在配置的机器上运行,它不需要与其他机器建立连接,因此运行速度更快。在这种模式下,你将配置放在一个 Git 仓库中,Ansible 会下载并使用这些配置来配置你的机器。
你应该在以下情况使用 Ansible 的拉取模式:
- 
在配置节点时,你的节点可能无法访问,比如自动扩展服务器集群中的成员。 
- 
你有大量机器需要配置,即使使用较大的 fork 值,也会花费很长时间来配置它们。 
- 
你希望机器在仓库更新时自动更新它们的配置。 
- 
你可能想在一台可能没有网络访问权限的机器上运行 Ansible,比如在启动后安装的 kickstart 环境中。 
然而,拉取模式有以下一些缺点,这使得它不适用于某些特定的场景:
- 
要连接到其他机器并收集变量,或复制文件,你需要在管理节点上拥有凭证。 
- 
你需要在服务器集群中协调 playbook 的运行;例如,如果你一次只能将三台服务器下线进行维护。 
- 
这些服务器位于严格的防火墙后面,防火墙不允许从你用来配置它们的节点发起 SSH 连接。 
拉取模式不需要在 playbook 中做任何特殊设置,但需要在你想要配置的节点上进行一些配置。在某些情况下,你可以使用 Ansible 的正常推送模式来完成这项工作。这里有一个小的 play 用于在机器上设置拉取模式:
---
- name: Ansible Pull Mode
  hosts: pullhosts
  tasks:
    - name: Setup EPEL
      command: "rpm -ivh http://download.fedoraproject.org/pub/epel/6/i386/epel-release-6-8.noarch.rpm"
      args: creates=/etc/yum.repos.d/epel.repo
    - name: Install Ansible + Dependencies
      yum:
        name: "{{ item }}"
        state: latest
        enablerepo: epel
      with_items:
      - ansible
      - git-core
    - name: Make directory to put downloaded playbooks in
      file:
        state: directory
        path: /opt/ansiblepull
    - name: Setup cron
      cron:
        name: "ansible-pull"
        user: root
        minute: "*/5"
        state: present
        job: "ansible-pull -U https://git.int.example.com.com/gitrepos/ansiblepull.git -D /opt/ansiblepull {{ inventory_hostname_short }}.yml"
在这个示例中,我们执行了以下步骤:
- 
首先,我们安装并设置了 EPEL。这是一个包含 CentOS 额外软件的仓库,Ansible 可以在 EPEL 仓库中找到。 
- 
接下来我们安装了 Ansible,并确保启用了 EPEL 仓库。 
- 
然后,我们为 Ansible 的拉取模式创建了一个目录来存放 playbook。保留这些文件意味着你不需要每次都下载整个 Git 仓库;只需要更新即可。 
- 
最后,我们设置了一个定时任务,每五分钟尝试运行 ansible-pull模式配置。
注意
上面的代码会从内部 HTTPS git 服务器下载仓库。如果你想通过 SSH 下载仓库,你需要添加一个步骤来安装 SSH 密钥,或者生成密钥并将其复制到 git 服务器。
存储机密信息
最终,你将需要在 Ansible 配方中包含敏感数据。到目前为止,我们讨论的所有配方都必须以纯文本的形式存储在磁盘上;如果你还将其存储在源代码控制中,第三方可能也能访问这些数据。这是有风险的,可能违反公司政策。
这可以通过使用 Ansible 密钥库来避免。密钥库是加密文件,Ansible 可以透明地解密它们。你可以将它们用于包括、变量文件、角色中的任务列表以及 Ansible 使用的任何其他 YAML 格式文件。你还可以在使用ansible-playbook时,配合-e命令行参数使用包含 JSON 和 YAML 文件的密钥库文件。密钥库文件由ansible-vault命令管理,可以像没有加密一样使用。
ansible-vault命令有多个模式,作为第一个参数传递。此表描述了这些模式:
| 模式 | 操作 | 
|---|---|
| 创建 | 这将启动默认编辑器以创建一个新的加密文件 | 
| 加密 | 这将加密一个现有文件,将其转变为密钥库 | 
| 编辑 | 这将编辑密钥库,允许你更改内容 | 
| 重设密码 | 这将更改用于加密密钥库的密码 | 
| 解密 | 这将解密密钥库,将其恢复为普通文件 | 
例如,要为你的暂存环境创建一个新的变量文件,你可以运行:
$ ansible-vault create vars/staging.yml
该命令会提示你输入密码,要求确认密码,然后打开编辑器让你添加内容;最后,加密的内容将保存在vars/staging.yml中。
使用密钥库文件时,你需要提供密码以便解密。这可以通过三种方式完成。你可以将--ask-vault-pass参数传递给 Ansible,这会导致 Ansible 在每次启动时都提示输入密码。你还可以使用--vault-password-file参数,它指向包含密码的文件。最后,你可以将vault_password_file添加到ansible.cfg文件中,使 Ansible 在每个命令中自动使用密钥库密码文件。需要注意的是,每次运行 Ansible 时只能提供一个密码,因此不能包含多个不同密码的文件。
为了让 Ansible 在运行加密的 playbook 时提示输入密码,你需要执行以下操作:
$ ansible-playbook --ask-vault-pass encrypted.yml
注意
密码文件也可以是可执行文件。要输出到屏幕上,你可以将内容输出到标准错误;要从用户处读取密码,你可以像往常一样使用stdin,最后脚本需要在退出之前将密码输出到stdout。
总结
在本章中,我们介绍了从简单的配置到更大规模部署时所需的技巧。我们讨论了如何使用 include 将你的 playbook 分成多个部分。接着,我们探讨了如何将相关的 include 打包,并通过角色一次性自动包含它们。最后,我们讨论了拉取模式,它允许你在远程节点本身上自动化 playbook 的部署。
在下一章中,我们将介绍如何编写你自己的模块。我们从使用 bash 脚本构建一个简单的模块开始。然后,我们将了解 Ansible 如何查找模块,以及如何让它找到你自己的自定义模块。接下来,我们看看如何使用 Python 编写更高级的模块,利用 Ansible 提供的功能。最后,我们将编写一个脚本,将 Ansible 配置为从外部源拉取其库存。
第五章:自定义模块
直到现在,我们一直在使用 Ansible 提供的工具。虽然这赋予了我们很大的能力,并使许多事情成为可能,但如果你遇到特别复杂的情况,或者经常使用脚本模块,那么你很可能希望学习如何扩展 Ansible。
在本章中,你将学习以下主题:
- 
如何在 Bash 脚本或 Python 中编写模块 
- 
使用你开发的自定义模块 
- 
编写一个脚本,将外部数据源作为库存 
当你在 Ansible 中处理复杂的任务时,通常会编写一个脚本模块。脚本模块的问题在于,你不能轻松地处理它们的输出或根据输出触发处理程序。因此,尽管脚本模块在某些情况下有效,但使用模块可能会更好。
当出现以下情况时,请使用模块而不是编写脚本:
- 
你不想每次都运行脚本 
- 
你需要处理输出 
- 
你的脚本需要生成事实 
- 
你需要将复杂的变量作为参数传递 
如果你想开始编写模块,应该查看 Ansible 的代码库。如果你希望你的模块与某个特定版本兼容,也应该切换到该版本以确保兼容性。以下命令将帮助你为 Ansible 1.3.0 开发模块。
$ git clone (https://github.com/ansible/ansible.git)
$ cd ansible
$ git checkout v1.3.0
$ chmod +x hacking/test-module
查看 Ansible 代码库可以让你获得一个方便的脚本,我们将在后面用它来测试我们的模块。我们还将使这个脚本可执行,以便在本章后续部分使用。
使用 Bash 编写模块
Ansible 允许你使用任何你喜欢的语言编写模块。虽然 Ansible 中的大多数模块使用 JSON,但如果你没有 JSON 解析工具,你可以使用快捷方式。Ansible 会以原始键值对的形式传递参数给你,如果它们是以这种格式提供的。如果提供了复杂的参数,你将收到 JSON 编码的数据。你可以使用 jsawk (github.com/micha/jsawk) 或 jq (stedolan.github.io/jq/) 等工具解析这些数据,但前提是它们已经安装在你的远程机器上。
Ansible 已经有一个模块,可以让你更改系统的主机名,但它只适用于基于 systemd 的系统。所以我们来编写一个适用于标准hostname命令的模块。我们将从打印当前主机名开始,然后再逐步扩展脚本。下面是这个简单模块的样子:
#!/bin/bash
HOSTNAME="$(hostname)"
echo "hostname=${HOSTNAME}"
如果你之前写过 Bash 脚本,这应该显得非常基础。基本上,我们做的就是获取主机名并以键值对的形式打印出来。现在我们已经编写了模块的初步版本,接下来我们应该进行测试。
为了测试 Ansible 模块,我们使用之前运行过chmod命令的脚本。该命令简单地运行你的模块,记录输出并返回给你。它还展示了 Ansible 如何解释模块的输出。我们将使用的命令如下所示:
ansible/hacking/test-module -m ./hostname
上一个命令的输出应如下所示:
* module boilerplate substitution not requested in module, line numbers will be unaltered
***********************************
RAW OUTPUT
hostname=admin01.int.example.com
***********************************
PARSED OUTPUT
{
    "hostname": "admin01.int.example.com"
}
忽略顶部的通知;它与使用 bash 构建的模块无关。你可以看到我们脚本发送的原始输出,完全符合我们的预期。测试脚本还会显示解析后的输出。在我们的例子中,我们使用的是简短的输出格式,我们可以看到 Ansible 正确地将其解释为模块通常接受的 JSON 格式。
让我们扩展模块以允许设置hostname。我们应该编写它,以便仅在需要时才进行更改,并让 Ansible 知道是否进行了更改。对于我们编写的小命令来说,这实际上是相当简单的。新的脚本应该类似于以下内容:
#!/bin/bash
set -e
# This is potentially dangerous
source ${1}
OLDHOSTNAME="$(hostname)"
CHANGED="False"
if [ ! -z "$hostname" -a "${hostname}x" != "${OLDHOSTNAME}x" ]; then
  hostname $hostname
  OLDHOSTNAME="$hostname"
  CHANGED="True"
fi
echo "hostname=${OLDHOSTNAME} changed=${CHANGED}"
exit 0
上一个脚本的工作原理如下:
- 
我们设置了 Bash 的错误退出模式,这样我们就不必处理 hostname方法的错误。Bash 将在失败时自动退出并返回退出代码。这将通知 Ansible 出现了问题。
- 
我们加载参数文件。这个文件作为第一个参数从 Ansible 传递给脚本。它包含发送给我们模块的参数。因为我们加载了这个文件,它可以用来运行任意命令;然而,Ansible 已经可以做到这一点,所以这并不会造成太大的安全问题。 
- 
我们收集旧的主机名并将默认值 CHANGED设为False。这使我们能够查看模块是否需要执行任何更改。
- 
我们检查是否收到新的主机名,并确认该主机名是否与当前设置的不同。 
- 
如果这两个测试都为真,我们尝试更改主机名,并将 CHANGED设置为True。
- 
最后,我们输出结果并退出。结果包括当前的主机名以及是否进行了更改。 
在 Unix 机器上更改主机名需要 root 权限。因此,在测试此脚本时,你需要确保以 root 用户身份运行它。我们使用sudo来测试此脚本,看看它是否有效。你将使用以下命令:
sudo ansible/hacking/test-module -m ./hostname -a 'hostname=test.example.com'
如果test.example.com不是当前机器的主机名,你应该看到以下输出:
* module boilerplate substitution not requested in module, line numbers will be unaltered
***********************************
RAW OUTPUT
hostname=test.example.com changed=True
***********************************
PARSED OUTPUT
{
    "changed": true,
    "hostname": "test.example.com"
}
如你所见,我们的输出被正确解析,模块声称已经对系统进行了更改。你可以通过hostname命令自己验证这一点。现在,再次使用相同的主机名运行该模块。你应该会看到如下输出:
* module boilerplate substitution not requested in module, line numbers will be unaltered
***********************************
RAW OUTPUT
hostname=test.example.com changed=False
***********************************
PARSED OUTPUT
{
    "changed": false,
    "hostname": "test.example.com"
}
再次,我们看到输出被正确解析了。然而,这一次,模块声称没有进行任何更改,这正是我们预期的结果。你也可以通过hostname命令来验证这一点。
使用自定义模块
现在我们已经为 Ansible 编写了第一个模块,我们应该在 playbook 中试一试。Ansible 会在多个位置查找模块——首先,它会查看config文件中library键指定的地方(/etc/ansible/ansible.cfg),然后它会查看命令行中使用--module-path参数指定的位置,接着它会查看与 playbook 同目录下的library目录,最后它会在library目录中查找任何可能设置的角色。
让我们创建一个使用新模块的 playbook,并将其放置在相同位置的library目录中,以便我们可以看到它的实际效果。以下是一个使用hostname模块的 playbook:
---
- name: Test the hostname file
  hosts: testmachine
  tasks:
    - name: Set the hostname
      hostname: hostname=testmachine.example.com
然后在与 playbook 文件相同的目录下创建一个名为library的目录。将hostname模块放在 library 目录中。你的目录结构应该如下所示:

现在,当你运行 playbook 时,它会在library目录中找到hostname模块并执行它。你应该能看到类似这样的输出:
PLAY [Test the hostname file] ***************************************
GATHERING FACTS *****************************************************
ok: [ansibletest]
TASK: [Set the hostname] ********************************************
changed: [ansibletest]
PLAY RECAP **********************************************************
ansibletest                : ok=2    changed=1    unreachable=0    failed=0
再次运行时,结果应该从changed变为ok。恭喜!你现在已经创建并执行了你的第一个模块。这个模块现在非常简单,但你可以扩展它,支持hostname文件,或其他方法来在启动时配置主机名。
用 Python 编写模块
所有随 Ansible 分发的模块都是用 Python 编写的。因为 Ansible 本身也是用 Python 编写的,所以这些模块可以直接与 Ansible 集成。以下是你应该用 Python 编写模块的一些理由:
- 
用 Python 编写的模块可以使用模板代码,这样可以减少所需代码量。 
- 
Python 模块可以提供文档供 Ansible 使用。 
- 
模块的参数会自动处理。 
- 
输出会自动转换为 JSON 格式。 
- 
Ansible 上游只接受使用 Python 并包含模板代码的插件。 
你仍然可以在没有此集成的情况下构建 Python 模块,通过解析参数并自行输出 JSON。然而,考虑到你可以免费获得的所有功能,要为这种做法辩护是很困难的。
让我们构建一个 Python 模块,允许我们更改系统当前运行的初始化级别。我们有一个名为pyutmp的 Python 模块,它可以让我们解析utmp文件。不幸的是,由于 Ansible 模块必须包含在一个单独的文件中,除非我们知道它会被安装到远程系统上,否则我们无法使用它。因此,我们将使用runlevel命令并解析其输出。设置运行级别可以通过init命令来完成。
第一步是弄清楚该模块支持哪些参数和功能。为了简化起见,我们的模块只接受一个参数。我们将使用runlevel参数来获取用户想要切换的运行级别。为此,我们将按如下方式实例化AnsibleModule类并传入数据:
module = AnsibleModule(
  argument_spec = dict(
    runlevel=dict(default=None, type='str')
  )
)
现在,我们需要实现模块的实际内容。我们之前创建的模块对象为我们提供了一些快捷方式。接下来的步骤中我们将使用三种快捷方式。由于有太多方法无法在此文档中一一说明,你可以查看整个AnsibleModule类以及在lib/ansible/module_common.py中所有可用的帮助函数。
- 
run_command:此方法用于启动外部命令并获取返回代码、stdout的输出以及stderr的输出。
- 
exit_json:此方法用于在模块成功完成时将数据返回给 Ansible。
- 
fail_json:此方法用于向 Ansible 报告失败,带有错误信息和返回代码。
以下代码实际上管理了系统的初始化级别,并附有注释来解释它的作用:
def main():     #1
  module = AnsibleModule(    #2
    argument_spec = dict(    #3
      runlevel=dict(default=None, type='str')     #4
    )     #5
  )     #6
  # Ansible helps us run commands     #7
  rc, out, err = module.run_command('/sbin/runlevel')     #8
  if rc != 0:     #9
    module.fail_json(msg="Could not determine current runlevel.", rc=rc, err=err)     #10
  # Get the runlevel, exit if its not what we expect     #11
  last_runlevel, cur_runlevel = out.split(' ', 1)     #12
  cur_runlevel = cur_runlevel.rstrip()     #13
  if len(cur_runlevel) > 1:     #14
    module.fail_json(msg="Got unexpected output from runlevel.", rc=rc)     #15
  # Do we need to change anything     #16
  if module.params['runlevel'] is None or module.params['runlevel'] == cur_runlevel:     #17
    module.exit_json(changed=False, runlevel=cur_runlevel)     #18
  # Check if we are root     #19
  uid = os.geteuid()     #20
  if uid != 0:     #21
    module.fail_json(msg="You need to be root to change the runlevel")     #22
  # Attempt to change the runlevel     #23
  rc, out, err = module.run_command('/sbin/init %s' % module.params['runlevel'])     #24
  if rc != 0:     #25
    module.fail_json(msg="Could not change runlevel.", rc=rc, err=err)     #26
  # Tell ansible the results     #27
  module.exit_json(changed=True, runlevel=cur_runlevel)     #28
最后需要在模板中添加一行代码,告诉 Ansible 需要动态地将集成代码添加到我们的模块中。这就是让我们能够使用AnsibleModule类并实现与 Ansible 紧密集成的魔法。模板代码需要放在文件的最底部,后面不能有任何代码。实现此功能的代码如下所示:
# include magic from lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
main()
So, finally, we have the code for our module built. Putting it all together, it should look like the following code:
#!/usr/bin/python     #1
# -*- coding: utf-8 -*-    #2
import os     #3
def main():     #4
  module = AnsibleModule(    #5
    argument_spec = dict(    #6
      runlevel=dict(default=None, type='str'),     #7
    ),     #8
  )     #9
  # Ansible helps us run commands     #10
  rc, out, err = module.run_command('/sbin/runlevel')     #11
  if rc != 0:     #12
    module.fail_json(msg="Could not determine current runlevel.", rc=rc, err=err)     #13
  # Get the runlevel, exit if its not what we expect     #14
  last_runlevel, cur_runlevel = out.split(' ', 1)     #15
  cur_runlevel = cur_runlevel.rstrip()     #16
  if len(cur_runlevel) > 1:     #17
    module.fail_json(msg="Got unexpected output from runlevel.", rc=rc)     #18
  # Do we need to change anything     #19
  if (module.params['runlevel'] is None or module.params['runlevel'] == cur_runlevel):     #20
    module.exit_json(changed=False, runlevel=cur_runlevel)     #21
  # Check if we are root     #22
  uid = os.geteuid()     #23
  if uid != 0:     #24
    module.fail_json(msg="You need to be root to change the runlevel")     #25
  # Attempt to change the runlevel     #26
  rc, out, err = module.run_command('/sbin/init %s' % module.params['runlevel'])     #27
  if rc != 0:     #28
    module.fail_json(msg="Could not change runlevel.", rc=rc, err=err)     #29
  # Tell ansible the results     #30
  module.exit_json(changed=True, runlevel=cur_runlevel)     #31
# include magic from lib/ansible/module_common.py     #32
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>     #33
main()     #34
你可以使用test-module脚本以与测试 Bash 模块相同的方式测试这个模块。然而,你需要小心,因为如果你使用sudo运行它,可能会重启你的机器或者将初始化级别更改为你不希望的状态。这个模块可能通过在远程测试机器上使用 Ansible 本身来进行更好的测试。我们按照本章前面编写 Bash 模块部分中描述的相同过程进行操作。我们创建一个使用该模块的 playbook,然后将该模块放置在与 playbook 相同目录下的库目录中。以下是我们需要使用的 playbook:
---
- name: Test the new init module
  hosts: testmachine
  user: root
  tasks:
    - name: Set the init level to 5
      init: runlevel=5
现在你应该能够尝试在远程机器上运行它。第一次运行时,如果机器的运行级别尚未是 5 级,你应该看到它更改运行级别。然后你可以再次运行它,看到没有任何变化。你可能还想检查一下,确保模块在未以 root 身份运行时会正确失败。
外部库存
在第一章,Ansible 入门中,我们看到 Ansible 需要一个库存文件,这样它就能知道主机的位置以及如何访问它们。Ansible 还允许你指定一个脚本,通过它从其他来源获取库存。外部库存脚本可以使用任何你喜欢的语言编写,只要它输出有效的 JSON。
外部清单脚本必须接受 Ansible 的两个不同调用。如果使用–list调用,它必须返回所有可用组和主机的列表。此外,它也可以使用--host调用。在这种情况下,第二个参数将是主机名,脚本应该返回该主机的变量列表。所有输出都应以 JSON 格式返回,因此你应该使用自然支持它的语言。
让我们编写一个模块,接受一个包含所有机器的 CSV 文件,并将其作为清单呈现给 Ansible。如果你有一个配置管理数据库(CMDB),允许你将机器列表导出为 CSV,或者如果有人将其机器记录保存在电子表格中,这将非常有用。此外,它不需要 Python 以外的任何依赖,因为 Python 已经包含了一个 CSV 处理模块。这实际上只是将 CSV 文件解析成正确的数据结构,并以 JSON 数据结构的形式输出。以下是我们希望处理的 CSV 文件示例;你可能希望根据你环境中的机器自定义它:
Group,Host,Variables
test,example,ansible_ssh_user=root
test,localhost,connection=local
该文件需要转换为两种不同的 JSON 输出。当调用--list时,我们需要以如下格式输出整个内容:
{"test": ["example", "localhost"]}
当使用参数--host example调用时,它应返回以下内容:
{"ansible_ssh_user": "root"}
这是一个脚本,它打开一个名为machines.csv的文件,并在给定--list时生成组的字典:此外,当给定--host和主机名时,它解析该主机的变量并将其作为字典返回。脚本有详细注释,你可以看到它的工作过程。你可以手动运行脚本,使用--list和--host参数来确认其行为是否正确。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
import csv
import json
def getlist(csvfile):
  # Init local variables
  glist = dict()
  rowcount = 0
  # Iterate over all the rows
  for row in csvfile:
    # Throw away the header (Row 0)
    if rowcount != 0:
      # Get the values out of the row
      (group, host, variables) = row
      # If this is the first time we've
      # read this group create an empty
      # list for it
      if group not in glist:
        glist[group] = list()
      # Add the host to the list
      glist[group].append(host)
    # Count the rows we've processed
    rowcount += 1
  return glist
def gethost(csvfile, host):
  # Init local variables
  rowcount = 0
  # Iterate over all the rows
  for row in csvfile:
    # Throw away the header (Row 0)
    if rowcount != 0 and row[1] == host:
      # Get the values out of the row
      variables = dict()
      for kvpair in row[2].split():
        key, value = kvpair.split('=', 1)
        variables[key] = value
      return variables
    # Count the rows we've processed
    rowcount += 1
command = sys.argv[1]
#Open the CSV and start parsing it
with open('machines.csv', 'r') as infile:
  result = dict()
  csvfile = csv.reader(infile)
  if command == '--list':
    result = getlist(csvfile)
  elif command == '--host':
    result = gethost(csvfile, sys.argv[2])
  print json.dumps(result)
现在,你可以使用这个清单脚本在使用 Ansible 时提供清单。测试一切是否正常工作的一个快速方法是使用ping模块来测试与所有机器的连接。此命令不会测试主机是否在正确的组中;如果你想做这个,你可以使用相同的ping模块命令,但不仅仅是对所有主机运行它,你可以简单地使用你想要测试的组。如果你的清单文件是可执行的,Ansible 将运行它并使用输出。你还可以使用目录,Ansible 会包括其中的所有文件,如果它们是可执行的,将运行它们。
$ ansible -i csvinventory –list-hosts -m ping all
类似于你在第一章中使用ping模块时,Ansible 入门,你应该看到如下输出:
localhost | success >> {
  "changed": false,
  "ping": "pong"
}
example | success >> {
  "changed": false,
  "ping": "pong"
}
这表明你可以连接并在所有来自你清单的主机上使用 Ansible。你可以使用相同的-i参数与ansible-playbook一起运行你的剧本,并使用相同的清单。
扩展 Ansible
除了编写模块和外部清单脚本外,你还可以扩展 Ansible 本身的核心功能。这允许你通过 Python 向 Ansible 添加更多功能。通过为 Ansible 编写插件,你可以做到以下几点:
- 
为了通过连接插件控制其他机器,可以添加新的方法 
- 
使用查找插件在循环或查找中使用来自 Ansible 外部的数据 
- 
使用过滤插件为变量或模板添加新的过滤器 
- 
使用回调插件包含在 Ansible 内部某些操作发生时运行的回调 
要将额外的插件添加到你的 Ansible 项目中,我们需要在 ansible.cfg 文件中指定的插件目录下创建一个 Python 文件。或者,我们可以将包含插件的新目录添加到现有目录列表中。
注意
请不要删除任何现有的目录,因为删除这些目录会移除提供核心 Ansible 功能的插件,正如我们在本书前面提到的那些插件。
在为 Ansible 编写插件时,你应该尽量使插件灵活且可重用。这样,你可以将一些复杂性从剧本和模板中移到少数几个复杂的 Python 文件中。专注于插件的重用性还意味着你有可能将插件提交回 Ansible 项目,通过 GitHub pull 请求的方式。如果你将插件提交回 Ansible,那么每个人都能利用你的插件,而你也为 Ansible 的发展做出了贡献。关于如何为 Ansible 做贡献的更多信息,可以在 Ansible 源代码中的 CONTRIBUTORS.md 文件中找到。
连接插件
连接插件负责将文件传输到远程机器并执行模块。你无疑已经在本书前面的剧本中使用过 SSH、本地插件,可能还使用过 winrm 插件。
除了常规的 __init__() 方法外,连接插件还必须实现以下方法:
| 方法 | 目的 | 
|---|---|
| connect() | 这个方法打开与我们正在管理的主机的连接 | 
| exec_command() | 这个方法在被管理的主机上执行一个命令 | 
| put_file() | 这个方法将文件复制到被管理的主机上 | 
| fetch_file() | 这个方法从被管理的主机下载一个文件 | 
| close() | 这个方法关闭与我们正在管理的主机的连接 | 
查找插件
查找插件有两种使用方式:通过 lookup() 包含来自外部的数据,或者以 with_ 风格遍历项目。你甚至可以将两者结合使用,像在 with_fileglob 查找插件中那样遍历外部数据。本书前面特别是在 第三章的 循环 部分展示了几个查找插件。
查找插件很容易编写,除了常规的__init__()方法外,它们只需要你实现一个run()方法。这个方法使用 Ansible utils包中的listify_lookup_plugin_terms()方法来收集传入的参数列表,并返回结果。作为示例,我们现在演示一个查找插件,从 JSON 编码文件中读取数据:
import json
class LookupModule(object):
    def __init__(self, basedir=None, **kwargs):
        pass
    def run(self, terms, inject=None, **kwargs):
        with open(terms, 'r') as f:
            json_obj = json.load(f)
        return json_obj
这可以用作查找插件来提取复杂数据,或者如果文件包含 JSON 列表,则可以使用with_jsonfile作为循环。将上述示例保存为jsonfile.py,并放在你其中一个查找插件目录中。你可以看到我们声明了一个新的类LookupModule;这是 Ansible 在 Python 文件中要查找的类名,因此你必须使用这个名称。然后,我们创建一个构造函数(命名为__init__),以便 Ansible 可以创建我们的类。最后,我们编写一个小方法,它只会打开一个 JSON 文件,解析它,并将结果返回给 Ansible。
我们应该注意到,这个示例其实是简化过的,它只会在当前工作目录中查找文件。以后可以扩展为在角色文件目录或其他地方查找,以便更好地符合其他 Ansible 模块设定的惯例。
然后,你可以在剧本中像这样使用这个查找插件:
- name: Print the JSON data we received
  debug:
    msg: "{{ lookup('json', 'file.json') }}"
过滤器插件
过滤器插件是 Ansible 用来处理变量并从模板生成文件的 Jinja2 模板引擎的扩展。这些扩展可以在剧本中使用,对变量进行数据处理,或者它们可以在模板内使用,处理数据后再包含到文件中。它们通过将复杂的处理逻辑移到 Python 文件中,从而简化了数据处理,避免了在模板或 Ansible 配置中增加复杂度。
过滤器插件与其他插件有些不同。要实现一个过滤器插件,首先你需要写一个简单的函数,它只需要接收所需的输入并返回结果。接着,你创建一个名为FilterModule的类,并在其中实现一个filters方法,该方法返回一个 Python 字典,其中键是过滤器名称,值是要调用的函数。
这是一个插件的示例实现,可以用来计算在任何组中避免脑裂情况所需的最小服务器数:在大多数系统中,这个数字是节点总数的 50%以上,再加 1。
def quorum(list_of_machines):
    n = len(list_of_machines)
    quorum = n / 2 + 1
    return quorum
class FilterModule(object):
    def filters(self):
        return {
            'quorum': quorum,
        }
简单来说,这个模块统计传入列表中有多少个项目,将其除以二,然后加一。所有操作都以整数数学计算,因此余数会被忽略,所有的操作都以整数进行,这符合我们的目的。
这个过滤器可以在剧本或模板中使用。例如,如果我们想配置一个 Elasticsearch 集群以确保有法定人数并避免脑裂问题,我们将使用以下代码行:
discovery.zen.minimum_master_nodes: {{ play_hosts|quorum }}
这将获取当前正在运行的主机列表(来自play_hosts变量),然后计算其中需要多少个才能获得法定人数。
回调插件
回调插件用于向外部系统提供有关 Ansible 中正在发生的操作的信息。如果在 Ansible 配置中指定的callback_plugins目录下找到回调插件,它们会自动激活。它们在自动化任务中运行剧本时尤其有用,因为它们可以通过标准输出之外的其他渠道提供反馈。回调插件有广泛的用途,具体如下:
- 
在剧本结束时,发送包含更改统计信息的电子邮件 
- 
记录 syslog中正在进行的更改的运行日志
- 
在剧本任务失败时通知聊天频道 
- 
在系统配置发生变化时,更新 CMDB 以确保每个系统的配置视图准确。 
- 
当所有主机都失败导致剧本提前退出时,通知管理员 
回调插件是最复杂的插件之一,因为它们可以钩入 Ansible 的大多数功能。然而,选项虽然很多,但并不意味着你需要实现所有的功能。你只需实现回调插件将使用的那些方法。以下是你可以实现的方法列表及其描述:
- 
def on_any(self, *args, **kwargs):在其他回调方法调用之前会调用此方法。由于不同回调方法的参数不同,它将参数展开为args和kwargs。此方法适用于日志记录,若用于其他用途会变得比较复杂。
- 
runner_on_failed(self, host, res, ignore_errors=False):任务失败后运行。host参数包含任务运行的主机,res包含来自剧本的任务数据以及返回的任何数据,ignore_errors包含一个布尔值,指定是否忽略剧本中指示的错误。
- 
runner_on_ok(self, host, res):在任务成功完成或异步任务轮询成功时运行。参数host包含任务运行的主机,res包含来自剧本的任务数据以及返回的任何数据。
- 
runner_on_skipped(self, host, item=None):在任务被跳过后运行。参数host包含如果任务没有被跳过的话将运行的主机,item参数包含当前正在迭代的循环项。
- 
runner_on_unreachable(self, host, res):当某个主机不可达时运行。host参数包含不可达的主机,res包含连接插件返回的错误信息。
- 
runner_on_no_hosts(self):当任务在没有任何主机的情况下启动时,这个回调会被调用。它没有任何变量。
- 
runner_on_async_poll(self, host, res, jid, clock):每当异步任务轮询状态时,这个方法会被调用。变量host包含正在被轮询的主机,res包含轮询的详细信息,jid包含任务 ID,clock包含任务失败之前剩余的时间。
- 
runner_on_async_ok(self, host, res, jid):当轮询完成且没有错误时,会运行此回调。host参数包含正在轮询的主机,res存储任务的结果,jid包含作业 ID。
- 
runner_on_async_failed(self, host, res, jid):当轮询完成并出现错误时,会运行此回调。host参数包含正在轮询的主机,res存储任务的结果,jid包含作业 ID。
- 
playbook_on_start(self):此回调在通过ansible-playbook启动 playbook 时执行。它不使用任何变量。
- 
playbook_on_notify(self, host, handler):每当一个 handler 被通知时,此回调函数会被触发。由于它在通知发生时运行,而不是在 handler 执行时运行,因此每个 handler 可能会多次触发。它有两个变量:host存储通知任务的主机名,handler存储被通知的 handler 名称。
- 
playbook_on_no_hosts_matched(self):如果一个 play 开始时没有匹配任何主机,则会运行此回调。它没有任何变量。
- 
playbook_on_no_hosts_remaining(self):当 play 中所有主机都有错误,且 play 无法继续时,会运行此回调。
- 
playbook_on_task_start(self, name, is_conditional):每个任务开始前都会运行此回调,即使该任务将被跳过。name变量设置为任务的名称,is_conditional设置为 when 条件的结果——如果任务将执行,True;如果不会执行,False。
- 
playbook_on_setup(self):此回调会在 setup 模块在所有主机上执行之前运行。无论包含多少主机,它只会运行一次。它不包含任何变量。
- 
playbook_on_play_start(self, name):每个 play 开始时都会运行此回调。name变量包含即将开始的 play 的名称。
- 
playbook_on_stats(self, stats):此回调会在 playbook 结束时、即将打印统计信息之前运行。stats变量包含 playbook 的详细信息。
总结
阅读完本章后,你应该能够使用 Bash 或任何你熟悉的语言构建模块。你应该能够安装从互联网获取或自己编写的模块。我们还介绍了如何通过使用 Python 中的模板代码更高效地编写模块,并编写了一个库存脚本,允许你从外部来源提取库存。最后,我们介绍了如何通过编写连接、查找、过滤和回调插件来向 Ansible 本身添加新功能。
我们已经尽力涵盖了你了解 Ansible 时所需的大部分内容,但我们不可能涵盖所有内容。如果你想继续学习 Ansible,可以访问官方 Ansible 文档:docs.ansible.com/。
Ansible 项目目前正在进行重写,最终将作为 2.0 版本发布。本书应与该版本及未来的其他版本保持兼容,但将会有一些新功能未在此涵盖。在 Ansible 2.0 版本中,您可以期待以下功能,这些功能可能会在未来发生变化(因为该版本尚未发布):
- 
在剧本中的故障恢复能力 
- 
允许您并行运行大量任务 
- 
兼容 Python 3 
- 
更容易的调试,因为错误信息将包含行号 

 
                    
                     
                    
                 
                    
                
 |
 | 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号