自 2013 年以来,我一直在使用 Ansible,并且至今仍在维护我最初编写的一些 playbook。它们随着 Ansible 从 1.4 版本发展到当前版本(截至本文撰写时为 2.9 版本)。
一路走来,随着 Ansible 从几十个模块增长到几百个,再到现在的数千个模块,我学到了很多关于如何确保我的 playbook 能够随着系统的增长而保持可维护性和可扩展性的知识。即使对于简单的项目(例如我用来管理自己笔记本电脑的 playbook),避免常见陷阱并做出让未来的你感激而不是后悔的决定,也会带来回报。
从这次经验中得出的三个主要结论是
- 保持组织性
- 尽早并经常测试
- 简化,优化
我学到的每个经验教训的重要性也遵循这个顺序;尝试优化已经组装得很差的东西(第 1 点)是没有用的(第 3 点)。每个步骤都建立在前一个步骤的基础上,所以我将引导您完成每个步骤。
保持组织性

至少,您应该将您的 Ansible playbook 存储在 Git 仓库中。这在很多方面都有帮助
- 一旦您有了已知的工作状态,您就可以提交工作(理想情况下,使用标签标记主要版本,例如第一个稳定版本的 1.0.0 和升级或重写的 2.0.0)。
- 如果需要,您可以随时将更改回退到以前的已知工作状态(例如,通过使用
git reset
或git checkout <tag>
)。 - 大规模更改(例如,功能添加或重大升级)可以在分支中进行,因此您仍然可以维护现有的 playbook,并有足够的时间来处理重大更改。
将 playbook 存储在 Git 中也有助于第二个重要的组织技术:从构建服务器运行您的 playbook。
无论您使用 Ansible Tower、Jenkins 还是其他一些构建系统,使用中央界面来运行 playbook 可以为您提供一致性和稳定性——您不会冒着一个管理员以一种方式运行 playbook(例如,使用错误的角色版本或旧的检出),而另一个人以另一种方式运行它,从而破坏您的服务器的风险。
它也有帮助,因为它迫使您确保您的 playbook 的所有资源都封装在 playbook 的仓库和构建配置中。理想情况下,整个构建(包括作业配置)都将捕获在仓库中(例如,通过使用 Jenkinsfile
或其等效物)。
组织性的另一个重要方面是文档;至少,我在每个 playbook 仓库中都有一个包含以下内容的 README
- playbook 的用途
- 相关资源的链接(CI 构建状态、外部文档、问题跟踪、主要联系人)
- 本地测试和开发的说明
即使您通过构建服务器自动化了 playbook,但对于如何以其他方式运行 playbook(例如,在本地测试环境中),拥有完整且正确的文档也很重要。我喜欢确保我的项目易于上手——不仅对于最终可能需要使用它们的人,也包括我自己!我在运行 playbook 时经常忘记细微差别或依赖关系,而 README 是概述任何特殊性的完美位置。
最后,Ansible 任务本身的结构也很重要,我喜欢通过拥有小的、可读的任务文件并将相关的任务集提取到 Ansible 角色中来确保我有一个可维护的结构。
通常,如果单个 playbook 达到大约 100 行 YAML 代码,我将开始将其分解为单独的任务文件,并使用 include_tasks
来包含这些文件。如果我发现一组可以独立运行并且可以分解成自己的 Ansible 角色 的任务集,我将致力于提取这些任务以及相关的处理程序、变量和模板。
使用角色是加速 Ansible playbook 维护的最佳方法;我经常需要在许多(如果不是大多数)playbook 中执行类似的任务,例如管理用户帐户或安装和配置 Web 服务器或数据库。将这些任务抽象到 Ansible 角色中意味着我可以维护一组任务,以便在许多 playbook 中使用,并使用变量来提供所需的灵活性。
如果您能够使 Ansible 角色通用化并使用开源许可证提供代码,则还可以通过 Ansible Galaxy 将其贡献回社区。我已经向 Galaxy 贡献了一百多个角色,并且由于数千个其他 playbook(除了我自己的)依赖于它们,并且如果角色中存在错误就会崩溃,因此它们变得更好。
关于角色的最后一点说明:如果您选择使用外部角色(来自 Galaxy 或私有 Git 仓库),我建议将角色提交到您的仓库(而不是将其添加到 .gitignore
文件并在每次运行 playbook 时下载角色),因为我喜欢避免每次 playbook 运行时都依赖从 Ansible Galaxy 下载。您仍然应该使用 requirements.yml
文件来定义角色依赖项,并为角色定义特定版本,以便您可以选择何时升级您的依赖项。
尽早并经常测试

Ansible 允许您将基础设施定义为代码。并且像任何软件一样,能够验证您编写的代码是否符合您的预期至关重要。
像任何软件一样,最好测试您的 Ansible playbook。当考虑为我构建的任何单个 Ansible 项目进行测试时,我会想到一系列我可以使用的 CI 测试选项,按从最容易到最难实现的顺序排列
yamllint
ansible-playbook --syntax-check
ansible-lint
- Molecule 测试(集成测试)
ansible-playbook --check
(针对生产环境的测试)- 构建并行基础设施
前三个选项(linting 和对您的 playbook 运行语法检查)基本上是免费的;它们运行速度非常快,可以帮助您避免 playbook 的任务结构和格式方面最常见的问题。
它们提供了一些价值,但除非 playbook 非常简单,否则我喜欢超越基本的 linting,并使用 Molecule 运行测试。我通常使用 Molecule 的内置 Docker 集成,针对本地 Docker 实例运行我的 playbook,该实例运行与我的生产服务器相同的基本操作系统。对于我在不同 Linux 发行版(例如 CentOS 和 Debian)上运行的某些角色,我为每个发行版运行一次 Molecule 测试 playbook,有时还会为更复杂的角色添加额外的测试场景。
如果您有兴趣了解如何使用 Molecule 测试角色,我在几年前写了一篇关于这个主题的博客文章,名为 使用 Molecule 测试您的 Ansible 角色。测试完整 playbook 的过程是类似的,在这两种情况下,测试都可以在大多数 CI 环境中运行(例如,我的 geerlingguy.apache 角色通过 Travis CI 运行一套 Molecule 测试)。
最后两个测试选项,在 --check
模式下运行 playbook 或构建并行生产基础设施,需要更多的设置工作,并且通常超出高效测试过程的必要范围。但在 playbook 管理对业务收入至关重要的服务器的情况下,它们可能是必要的。
在运行测试和定期检查或更新 playbook 时,还有一些其他需要注意的重要事项
- 确保跟踪(并修复)您在 Ansible 的输出中看到的任何
DEPRECATION WARNING
。通常,您有一到两年的时间,警告才会导致最新 Ansible 版本出现故障,因此您可以越早更新您的 playbook 代码越好。 - 每个 Ansible 版本都有一个 移植指南),当您从一个版本更新到下一个版本时,它非常有用。
- 如果您在使用像
command
这样的模块时在 playbook 输出中看到令人讨厌的WARN
消息,并且您知道可以安全地忽略它们,您可以在任务中的args
下添加warn: no
。最好消除这些警告,以便更具可操作性的警告(如弃用警告)可以一目了然地被注意到。
最后,我喜欢确保我的 CI 环境始终运行最新的 Ansible 版本(而不是锁定到我知道可以与我的 playbook 一起使用的特定版本),因为我知道 playbook 是否会在新版本发布后立即崩溃。我的构建服务器锁定到一个特定的 Ansible 版本,它可能比最新版本落后一到两个版本,因此这给了我时间来确保我在将构建服务器升级到最新版本之前修复 CI 测试中发现的任何新问题。
简化,优化

“YAML 不是一种编程语言。”
— Jeff Geerling
playbook 的简洁性使维护和未来的更改变得更加容易。有时我会查看一个 playbook,并对正在发生的事情感到困惑,因为有多个 when
和 until
条件,其中混杂着一堆 Python 和 Jinja 过滤器。
如果我开始看到超过一个或两个链接过滤器或 Python 方法调用(尤其是任何与正则表达式有关的东西),我将其视为将所需功能重写为 Ansible 模块的主要候选对象。该模块可以在 Python 中维护并独立测试,并且更容易作为严格的 Python 代码维护,而不是将所有 Python 与 YAML 任务定义内联混合。
所以我的第一点是:尽可能坚持使用 Ansible 的模块和简单的任务定义。尽可能尝试使用 Jinja 过滤器,并避免一次在一个变量上链接超过一个或两个过滤器。如果您有很多复杂的内联 Python 或 Jinja,那么是时候考虑将其重构为自定义 Ansible 模块了。
我看到人们做的另一个常见的事情,尤其是在第一次构建角色时,是使用复杂的 dict 变量,而单独的“扁平”变量可能更灵活。
例如,与其在一个巨大的 dict 中拥有许多选项的 apache 角色,像这样
apache:
startservers: 2
maxclients: 2
并考虑使用单独的扁平变量
apache_startservers: 2
apache_maxclients: 2
这样做的原因很简单:使用扁平变量允许 playbook 轻松覆盖一个特定值,而无需重新定义整个字典。当您在一个角色中有几十个(或在某些罕见情况下,数百个)默认变量时,这尤其有用。
一旦 playbook 和角色代码看起来不错,就该开始考虑优化了。
我首先查看的几件事是
- 我可以禁用
gather_facts
吗?并非每个 playbook 都需要所有事实,并且它会在每次运行和每台服务器上增加一点开销。 - 我可以增加 Ansible 使用的
forks
数量吗?默认值为 5,但如果我有 50 台服务器,我可以一次操作 20 或 25 台服务器,以大大减少 Ansible 在所有服务器上运行 playbook 所需的时间吗? - 在 CI 中,我可以并行化测试场景吗?如果我可以一次启动所有测试,而不是运行一个测试,然后再运行下一个测试,这将使我的 CI 测试周期更快。如果 CI 速度很慢,您可能会倾向于忽略它或不等到测试运行完成,因此确保您的测试周期短非常重要。
当我在角色或 playbook 中查找任务时,我还会查找某些模块常见的几个明显的性能问题
- 当使用
package
(或apt
、yum
、dnf
等)时,如果要管理多个软件包,则应将列表直接传递给name
参数,而不是通过with_items
或loop
——这样 Ansible 可以有效地一次性操作整个列表,而不是逐个软件包进行操作。 - 当使用
copy
时,正在复制多少个文件?如果只有一个文件甚至几十个文件,那可能没问题,但是如果您有数百或数千个文件要复制,则copy
模块非常慢(最好使用像synchronize
这样的模块或其他策略,例如复制 tarball 并在服务器上展开它)。 - 如果在循环中使用
lineinfile
,则使用template
代替并在一次传递中控制整个文件可能更有效(有时也更容易维护)。
一旦我解决了大部分唾手可得的问题,我喜欢分析我的 playbook,Ansible 有一些内置工具可以做到这一点。您可以通过在 ansible.cfg
的 defaults
下设置 callback_whitelist
选项来配置额外的回调插件,以测量角色和任务性能
[defaults]
callback_whitelist = profile_roles, profile_tasks, timer
现在,当您运行 playbook 时,您会在最后获得最慢的角色和任务的摘要
Monday 10 September 22:31:08 -0500 (0:00:00.851) 0:01:08.824 ******
===============================================================================
geerlingguy.docker ------------------------------------------------------ 9.65s
geerlingguy.security ---------------------------------------------------- 9.33s
geerlingguy.nginx ------------------------------------------------------- 6.65s
geerlingguy.firewall ---------------------------------------------------- 5.39s
geerlingguy.munin-node -------------------------------------------------- 4.51s
copy -------------------------------------------------------------------- 4.34s
geerlingguy.backup ------------------------------------------------------ 4.14s
geerlingguy.htpasswd ---------------------------------------------------- 4.13s
geerlingguy.ntp --------------------------------------------------------- 3.94s
geerlingguy.swap -------------------------------------------------------- 2.71s
template ---------------------------------------------------------------- 2.64s
...
如果任何事情花费超过几秒钟,最好弄清楚它为什么花费这么长时间。
总结
我希望您学到了一些使您的 Ansible Playbook 更易于维护的方法;正如我在一开始所说,三个要点(保持组织性、测试,然后简化和优化)都建立在前一个要点的基础上,因此首先要确保您拥有干净、文档齐全的代码,然后确保它经过充分测试,最后查看如何使其更好更快!
本文是 Jeff 在 AnsibleFest 2018 上所做的演讲 使您的 Ansible playbook 灵活、可维护和可扩展 的后续文章,您可以在 此处观看。
1 条评论