让您的 Ansible Playbook 灵活、可维护且可扩展
让您的 Ansible Playbook 灵活、可维护且可扩展
在过去的几年里,我学习了许多技巧,帮助减轻我的工作维护负担。对我来说,拥有可维护的项目非常重要,因为我的许多项目(例如托管 Apache Solr)已经运行了十多年!如果项目难以维护或难以进行重大架构变更,我可能会失去客户,从而失去收入,最重要的是,我可能会失去理智!
我将在今年的 AnsibleFest 奥斯汀上发表一个名为“让您的 Ansible Playbook 灵活、可维护且可扩展”的演讲,我想在这里总结一些主要主题。
保持组织
我喜欢摄影和自动化,因此我花了很多时间构建涉及 Raspberry Pis 和相机的电子项目。如果没有我使用的组织系统,组装项目所需的正确组件将非常令人沮丧。
同样,在 Ansible 中,我喜欢将任务组织起来,以便我可以更容易地组合它们、测试它们并管理它们,而不会花费太多精力。
我通常从一个文件中包含所有任务的 playbook 开始。当我的 YAML 代码行数达到 100 行左右时,我会将相关的任务组分解成单独的文件,并在 playbook 中使用 include_tasks
将它们包含进来。
在 playbook 变得更加完整之后,我经常会发现一些相关的任务集可以被隔离出来,比如安装软件、复制软件配置,然后启动(或重新启动)守护程序。因此,我使用 ansible-galaxy init ROLE_NAME
创建一个新的角色,然后将这些任务放入该角色中。
如果该角色足够通用,我会将其放在 GitHub 上并提交给 Ansible Galaxy,或者将其放在一个单独的私有 Git 仓库中。现在,我可以为该角色添加一组通用的测试(使用 Molecule 或其他一些测试设置),并且可以与多个项目共享该角色,甚至可以与完全由独立团队管理的项目共享该角色!
然后,我通过 requirements.yml
文件将外部角色包含在我的项目中。对于一些以稳定性为最重要的特征的项目,我还将为每个包含的 Ansible 角色定义版本(git 引用或标签)。对于其他一些项目,我可以为了更方便的维护而牺牲一些稳定性(比如测试 playbook 或一次性服务器配置),我只需要提供角色名称(以及如果不在 Galaxy 上的仓库详细信息)。
对于大多数项目,我不会将外部角色(在 requirements.yml
中定义的那些角色)提交到仓库中——我的 CI 系统中有一个任务会在每次运行时安装新鲜的角色。但是,在某些情况下,最好将所有角色提交到代码库中。例如,由于开发人员可以每天运行我的 Drupal VM playbook,而这些开发人员通常并不靠近 Ansible Galaxy 的服务器,他们在安装大量所需的 Ansible Galaxy 角色时遇到了麻烦。因此,我将这些角色提交到了代码库中,现在他们不再需要在每次构建新的 Drupal VM 实例时等待所有角色安装。
如果你确实将角色提交到代码库中,你需要有一个完善的更新角色流程——确保你的 requirements.yml
文件不会与已安装的角色不同步!我经常运行 ansible-galaxy install -r requirements.yml --force
强制替换代码库中的所有必需角色,以保证自己没有遗漏任何东西!
简化和优化
> YAML is not a programming language. > > ---Jeff Geerling
人们喜欢使用 Ansible 的原因之一是它使用 YAML,并且具有声明式语法。你想安装一个包,所以你有一个包任务:name=httpd state=present
。你想运行一个服务,所以你有一个服务任务:name=httpd state=started
。
在许多情况下,你需要添加更多智能。例如,如果你使用相同的角色来构建 VM 和容器,并且不想在容器中启动该服务,你需要添加一个 when 条件,比如
- name: Ensure Apache is started. service: name: httpd state: started when: 'server_type != "container"'
这种逻辑很简单,在阅读任务并了解其作用时是有意义的。但是有些人可能会尝试在 when 条件或其他地方塞入大量精妙的逻辑,而 Ansible 在这些地方提供了对 Jinja2 和 Python 的一些暴露,这时事情就会失控。
作为一个经验法则,如果你花超过 10 分钟的时间在 playbook 的 when 条件中处理转义引号,那么可能需要考虑编写一个单独的模块来执行任务所需的逻辑。Python 通常应该在一个单独的模块中,而不是与其他 YAML 代码混在一起。虽然也有例外情况(例如,当比较更复杂的字典和字符串时),但我尽量避免在我的 Ansible playbook 中编写任何复杂的代码。
除了避免复杂逻辑之外,让 playbook 运行得更快也很有帮助。很多时候,我会在 ansible.cfg
文件 defaults 部分中设置 playbook 定时器,并运行 playbook,发现一两个任务或角色与 playbook 的其他部分相比需要很长时间。
例如,一个 playbook 使用 copy 模块来处理包含数十个文件的巨大目录。由于 Ansible 内部执行文件复制的方式,这意味着浪费了许多秒来等待 Ansible 将每个文件通过 SSH 连接传送。
将该任务转换为使用 synchronize
而不是 copy 模块可以节省每 playbook 运行的许多秒。对于一次运行来说,这似乎并不多;但是,当 playbook 按照计划运行(例如,为了强制执行服务器上的特定配置)或作为 CI 套件的一部分运行时,帮助提高效率非常重要。否则,这会导致在低效代码上浪费额外的 CPU 周期,而开发人员通常不喜欢等待 CI 测试很长时间才能知道他们的代码是否破坏了什么东西。