Ansible 基础知识,没有它你的剧本就会变成一坨粘糊糊的意大利面

我对其他人的 Ansible 代码做了很多审查,并且自己也写了很多。 在分析错误(包括其他人的和我自己的)以及多次采访的过程中,我意识到 Ansible 用户所犯的主要错误 - 他们在没有掌握基本知识的情况下就陷入了复杂的事物。

为了纠正这种普遍的不公正,我决定为那些已经了解 Ansible 的人写一篇介绍。 我警告你,这不是男人的重述,这是一本长读物,有很多字母,没有图片。

读者的预期水平是已经写了几千行 yamla,有些东西已经在生产中,但“不知怎的,一切都是歪的”。

Ansible 用户犯的主要错误是不知道某些东西叫什么。 如果您不知道名称,就无法理解文档的内容。 一个活生生的例子:在一次采访中,一个似乎说自己用 Ansible 写了很多东西的人无法回答“剧本由哪些元素组成?”的问题。 当我提出“答案是预期的,剧本由游戏组成”时,随之而来的是“我们不使用它”的严厉评论。 人们编写 Ansible 是为了钱,而不是为了玩。 他们实际上使用它,但不知道它是什么。

让我们从简单的事情开始:它叫什么? 也许你知道这一点,也可能你不知道,因为你阅读文档时没有注意。

ansible-playbook 执行剧本。 playbook 是一个扩展名为 yml/yaml 的文件,其中包含如下内容:

---
- hosts: group1
  roles:
    - role1

- hosts: group2,group3
  tasks:
    - debug:

我们已经意识到整个文件是一个剧本。 我们可以显示角色在哪里以及任务在哪里。 但玩在哪里呢? 戏剧和角色或剧本之间有什么区别?

这一切都在文档中。 他们很怀念它。 初学者——因为内容太多,你不可能一下子记住所有内容。 经验丰富——因为“琐事”。 如果您有经验,请至少每六个月重新阅读一次这些页面,您的代码将成为一流的。

所以,请记住:Playbook 是一个包含 play 和 import_playbook.
这是一场戏:

- hosts: group1
  roles:
    - role1

这也是另一出戏:

- hosts: group2,group3
  tasks:
    - debug:

什么是玩? 为什么是她?

游戏是剧本的关键元素,因为游戏且仅游戏将角色和/或任务列表与必须在其上执行的主机列表相关联。 在文档的深处,您可以找到提到的 delegate_to、本地查找插件、特定于网络 CLI 的设置、跳转主机等。 它们允许您稍微更改执行任务的位置。 但是,忘记它吧。 这些聪明的选择中的每一个都有非常具体的用途,而且它们绝对不是通用的。 我们正在谈论每个人都应该了解和使用的基本知识。

如果你想在“某处”表演“某事”,你就写剧本。 不是一个角色。 不是具有模块和委托的角色。 你拿去写剧本。 其中,在主机字段中列出要执行的位置,在角色/任务中列出要执行的内容。

很简单,对吧? 不然怎么可能呢?

当人们不通过游戏而渴望做到这一点时,典型的时刻之一就是“设定一切的角色”。 我想要一个既配置第一类型服务器又配置第二类型服务器的角色。

一个典型的例子是监控。 我想要一个可以配置监控的监控角色。 监控角色分配给监控主机(根据角色)。 但事实证明,为了进行监控,我们需要将包传送到我们正在监控的主机上。 为什么不使用委托? 您还需要配置 iptables。 代表? 您还需要编写/更正 DBMS 的配置以启用监控。 代表! 如果缺乏创造力,那么你可以派一个代表团 include_role 在嵌套循环中,在组列表上使用棘手的过滤器,并在内部 include_role 你可以做更多 delegate_to 再次。 然后我们就走了...

一个美好的愿望 - 拥有一个“包办一切”的单一监控角色 - 会导致我们陷入彻底的地狱,而大多数情况下只有一种出路:从头开始重写一切。

这里哪里发生了错误? 当您发现要在主机 X 上执行任务“x”时,您必须转到主机 Y 并在那里执行“y”,您必须做一个简单的练习:去编写 play,在主机 Y 上执行 y。 不要在“x”上添加任何东西,而是从头开始编写。 即使使用硬编码变量。

看起来上面段落中的所有内容都说得正确。 但这不是你的情况! 因为您想要编写 DRY 且类似库的可重用代码,并且您需要寻找一种方法来实现它。

这是另一个严重错误潜伏的地方。 这个错误使许多项目从写得还算可以的(可能会更好,但一切正常并且很容易完成)变成了连作者都无法理解的完全恐怖。 它有效,但上帝禁止你改变任何事情。

错误是:角色是库函数。 这个类比毁掉了如此多的良好开端,让人看着很伤心。 该角色不是库函数。 她无法进行计算,也无法做出游戏层面的决定。 提醒我游戏会做出什么决定?

谢谢,你是对的。 Play 做出关于在哪些主机上执行哪些任务和角色的决定(更准确地说,它包含信息)。

如果您将这一决定委托给某个角色,即使进行了计算,您也注定会陷入悲惨的境地(以及试图解析您的代码的人)。 该角色并不决定其在何处执行。 这个决定是通过游戏做出的。 这个角色按照它所告诉的去做,在它所告诉的地方做。

为什么在 Ansible 中编程很危险以及为什么 COBOL 比 Ansible 更好,我们将在有关变量和 jinja 的章节中讨论。 现在,让我们说一件事——你的每一次计算都会留下全局变量变化的不可磨灭的痕迹,而你对此无能为力。 当两条“痕迹”相交时,一切都消失了。

谨记:该角色肯定会影响控制流。 吃 delegate_to 并且它有合理的用途。 吃 meta: end host/play。 但! 还记得我们教的基础知识吗? 忘记了 delegate_to。 我们正在谈论最简单、最美丽的 Ansible 代码。 易于阅读、易于编写、易于调试、易于测试和易于完成。 所以,再一次:

play 和 only play 决定在哪些主机上执行什么。

在本节中,我们讨论了游戏和角色之间的对立。 现在我们来谈谈任务与角色的关系。

任务和角色

考虑玩:

- hosts: somegroup
  pre_tasks:
    - some_tasks1:
  roles:
     - role1
     - role2
  post_tasks:
     - some_task2:
     - some_task3:

假设您需要执行 foo. 它看起来像 foo: name=foobar state=present。 我应该在哪里写这个? 在预? 邮政? 创建角色?

......任务去哪里了?

我们再次从基础开始——播放设备。 如果你在这个问题上犹豫不决,你就不能把游戏作为其他一切的基础,你的结果就会“摇摇欲坠”。

播放设备:hosts 指令、播放本身和 pre_tasks、任务、角色、post_tasks 部分的设置。 剩下的游戏参数现在对我们来说并不重要。

其任务和角色部分的顺序: pre_tasks, roles, tasks, post_tasks。 由于从语义上讲,执行顺序介于 tasks и roles 不清楚,那么最佳实践说我们正在添加一个部分 tasks,只有当不是 roles。 如果有的话 roles,然后所有附加任务都放置在部分中 pre_tasks/post_tasks.

剩下的就是一切在语义上都是清楚的:首先 pre_tasks,然后 roles,然后 post_tasks.

但我们仍然没有回答这个问题:模块调用在哪里? foo 写? 我们需要为每个模块编写一个完整的角色吗? 还是所有事情都扮演厚重的角色会更好? 如果不是角色,那么我应该在哪里写——前置还是后置?

如果这些问题没有合理的答案,那么这就是缺乏直觉的表现,也就是说,同样是“基础不稳固”。 让我们弄清楚一下。 首先,一个安全问题:如果游戏有 pre_tasks и post_tasks (并且没有任务或角色),那么如果我执行第一个任务,可能会出现问题 post_tasks 我会把它移到最后 pre_tasks?

当然,问题的措辞暗示它会崩溃。 但到底是什么?

... 处理程序。 阅读基础知识揭示了一个重要的事实:所有处理程序都会在每个部分之后自动刷新。 那些。 所有任务来自 pre_tasks,然后是所有收到通知的处理程序。 然后执行所有角色以及角色中通知的所有处理程序。 后 post_tasks 和他们的处理者。

因此,如果您将任务从 post_tasks в pre_tasks,那么您可能会在执行处理程序之前执行它。 例如,如果在 pre_tasks Web 服务器已安装并配置,并且 post_tasks 有东西发送给它,然后将此任务转移到该部分 pre_tasks 将导致这样一个事实:在“发送”时,服务器尚未运行,一切都会中断。

现在我们再想一想,为什么我们需要 pre_tasks и post_tasks? 例如,为了在履行角色之前完成所需的一切(包括处理程序)。 A post_tasks 将允许我们处理执行角色(包括处理程序)的结果。

精明的 Ansible 专家会告诉我们它是什么。 meta: flush_handlers,但是如果我们可以依赖播放中各部分的执行顺序,为什么我们需要flush_handlers呢? 此外,使用 meta:flush_handlers 可能会给我们带来意想不到的结果,即重复的处理程序,在使用时给我们带来奇怪的警告 when у block ETC。 您对 ansible 的了解越多,您可以为“棘手”解决方案命名的细微差别就越多。 一个简单的解决方案 - 使用前/角色/后之间的自然划分 - 不会造成细微差别。

然后,回到我们的“foo”。 我应该把它放在哪里? 在前、后或角色中? 显然,这取决于我们是否需要 foo 处理程序的结果。 如果它们不存在,则 foo 不需要放置在 pre 或 post 中 - 这些部分具有特殊含义 - 在代码主体之前和之后执行任务。

现在,“角色或任务”问题的答案归结为已经在起作用的内容 - 如果那里有任务,那么您需要将它们添加到任务中。 如果有角色,您需要创建一个角色(即使是从一项任务)。 让我提醒您,任务和角色不能同时使用。

了解 Ansible 的基础知识可以为看似品味的问题提供合理的答案。

任务和角色(第二部分)

现在我们来讨论一下刚开始写剧本时的情况。 您需要制作 foo、bar 和 baz。 这三个任务、一个角色还是三个角色? 总结一下这个问题:你应该从什么时候开始写角色? 当你可以写任务时,写角色还有什么意义?...什么是角色?

最大的错误之一(我已经讨论过这一点)是认为角色就像程序库中的函数。 通用函数描述是什么样的? 它接受输入参数,与副作用交互,产生副作用,并返回一个值。

现在,注意。 从这个角色中可以做什么? 随时欢迎您调用副作用,这就是整个 Ansible 的本质——创建副作用。 有副作用吗? 初级。 但是“传递一个值并返回它”——这就是它不起作用的地方。 首先,您不能将值传递给角色。 您可以在角色的 vars 部分中设置一个具有生命周期大小的全局变量。 您可以在角色内部设置一个具有生命周期的全局变量。 或者甚至是剧本的生命周期(set_fact/register)。 但你不能有“局部变量”。 你不能“获取一个值”并“返回它”。

主要的事情是这样的:你不能在 Ansible 中编写一些东西而不引起副作用。 更改全局变量始终是函数的副作用。 例如,在 Rust 中,更改全局变量是 unsafe。 在 Ansible 中,它是影响角色值的唯一方法。 注意使用的词语:不是“将值传递给角色”,而是“更改角色使用的值”。 角色之间不存在隔离。 任务和角色之间不存在隔离。

合计: 角色不是函数.

这个角色有什么好处呢? 首先,角色有默认值(/default/main.yaml),其次,角色有额外的目录用于存储文件。

默认值有什么好处? 因为在马斯洛金字塔中,Ansible 相当扭曲的变量优先级表,角色默认值是最低优先级的(减去 Ansible 命令行参数)。 这意味着,如果您需要提供默认值并且不担心它们会覆盖库存或组变量中的值,那么角色默认值是唯一适合您的地方。 (我撒了一点谎——还有更多 |d(your_default_here),但如果我们谈论固定位置,则只有角色默认值)。

角色还有哪些精彩之处? 因为他们有自己的目录。 这些是变量的目录,包括常量(即为角色计算)和动态(有模式或反模式 - include_vars{{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml.)。 这些是目录 files/, templates/。 此外,它还允许您拥有自己的模块和插件(library/)。 但是,与剧本中的任务(也可以拥有所有这些)相比,这里唯一的好处是文件不会转储到一堆,而是转储到几个单独的堆中。

还有一个细节:您可以尝试创建可重复使用的角色(通​​过 Galaxy)。 随着集合的出现,角色分配几乎被遗忘了。

因此,角色有两个重要的功能:它们有默认值(一个独特的功能),并且允许您构建代码。

回到最初的问题:什么时候做任务,什么时候做角色? 剧本中的任务最常用作角色之前/之后的“粘合剂”,或者作为独立的构建元素(那么代码中不应该有角色)。 一堆正常的任务与角色混合在一起是毫不含糊的马虎行为。 您应该遵循特定的风格 - 任务或角色。 角色提供了实体和默认值的分离,任务允许您更快地阅读代码。 通常,更多“固定”(重要且复杂)的代码被放入角色中,辅助脚本以任务风格编写。

可以将 import_role 作为一项任务来执行,但是如果您编写此内容,请准备好向您自己的美感解释为什么要执行此操作。

精明的读者可能会说,角色可以导入角色,角色可以通过 Galaxy.yml 拥有依赖关系,还有一个可怕可怕的 include_role — 我提醒您,我们正在提高基本 Ansible 的技能,而不是花样体操的技能。

处理程序和任务

让我们讨论另一个明显的事情:处理程序。 知道如何正确使用它们几乎是一门艺术。 处理程序和拖动之间有什么区别?

由于我们记住了基础知识,因此这里有一个示例:

- hosts: group1
  tasks:
    - foo:
      notify: handler1
  handlers:
     - name: handler1
       bar:

角色的处理程序位于 rolename/handlers/main.yaml 中。 处理程序在所有游戏参与者之间进行翻查:pre/post_tasks 可以拉取角色处理程序,并且角色可以从游戏中拉取处理程序。 然而,对处理程序的“跨角色”调用比重复一个简单的处理程序更麻烦。 (最佳实践的另一个要素是尽量不要重复处理程序名称)。

主要区别在于任务始终执行(幂等)(加/减标签和 when),以及处理程序 - 按状态更改(仅当已更改时才触发通知)。 这是什么意思? 例如,当您重新启动时,如果没有更改,则不会有处理程序。 为什么在生成任务没有变化的情况下我们需要执行handler? 例如,因为某些内容发生了损坏并发生了变化,但执行并未到达处理程序。 例如,因为网络暂时关闭。 配置已更改,服务尚未重新启动。 下次启动时,配置不再更改,服务仍保留旧版本的配置。

配置的情况无法解决(更准确地说,您可以使用文件标志等为自己发明一个特殊的重启协议,但这不再是任何形式的“基本ansible”)。 但还有另一个常见的故事:我们安装了该应用程序,并记录了它 .service-file,现在我们想要它 daemon_reload и state=started。 而处理这个问题的自然地点似乎是处理程序。 但是,如果您将其设置为任务列表或角色末尾的任务而不是处理程序,则每次都会以幂等方式执行。 即使剧本中途破裂了。 这根本不能解决重启问题(你不能用restarted属性做任务,因为幂等性丢失了),但是state=started绝对值得做,playbooks的整体稳定性增加了,因为连接数和动态减少。

处理程序的另一个积极特性是它不会阻塞输出。 没有任何变化 - 输出中没有额外的跳过或确定 - 更易于阅读。 这也是一个负面属性 - 如果您在第一次运行时在线性执行任务中发现拼写错误,那么处理程序将仅在更改时执行,即在某些情况下 - 很少。 例如,五年后,这是我人生中的第一次。 当然,如果名称出现拼写错误,一切都会崩溃。 如果您不第二次运行它们,则不会发生任何变化。

另外,我们需要谈谈变量的可用性。 例如,如果您用循环通知任务,变量中会包含什么? 您可以通过分析进行猜测,但这并不总是微不足道的,特别是当变量来自不同的地方时。

……因此,处理程序的用处远不如看上去那么有用,而且问题也比看上去多得多。 如果你可以在没有处理程序的情况下写出漂亮的东西(没有多余的装饰),那么最好不要使用它们。 如果效果不理想,最好还是和他们一起。

腐蚀性的读者正确地指出我们还没有讨论过 listen一个处理程序可以调用另一个处理程序的通知,一个处理程序可以包含 import_tasks(可以使用 with_items 执行 include_role),Ansible 中的处理程序系统是图灵完备的,来自 include_role 的处理程序以一种奇怪的方式与游戏中的处理程序相交,等等.d. - 这一切显然不是“基础”)。

尽管有一个特定的 WTF 实际上是您需要牢记的功能。 如果你的任务是用 delegate_to 并且有notify,则执行对应的handler,无需 delegate_to, IE。 在分配播放的主机上。 (当然,尽管处理程序可能有 delegate_to 也)。

另外,我想谈谈可重用角色。 在集合出现之前,有一个想法是可以制作通用角色 ansible-galaxy install 然后就走了。 适用于所有情况下所有变体的所有操作系统。 所以,我的意见是:这不起作用。 任何具有质量的角色 include_vars支持 100500 个案例,注定会出现极端案例错误的深渊。 它们可以通过大量测试来覆盖,但与任何测试一样,要么你有输入值和总函数的笛卡尔积,要么你有“覆盖的个别场景”。 我的观点是,如果角色是线性的(圈复杂度为 1),那就更好了。

更少的 if(显式或声明性 - 形式为 when 或形式 include_vars 通过一组变量),作用就越好。 有时你必须建立分支,但是,我再说一遍,分支越少越好。 所以这对于银河系来说似乎是一个很好的角色(它有效!) when 可能不如五项任务中“自己”的角色更可取。 当你开始写一些东西的时候,银河系的角色就变得更好了。 当事情变得更糟的时候,你会怀疑这是因为“与星系的作用”。 打开它,里面有五个内含物、八个任务表和一叠 when'ov...我们需要解决这个问题。 不是 5 个任务,而是一个没有什么可打破的线性列表。

在以下部分

  • 关于清单、组变量、host_group_vars 插件、hostvars 的一些信息。 如何与意大利面快刀斩乱麻。 范围和优先级变量,Ansible 内存模型。 “那么我们在哪里存储数据库的用户名呢?”
  • jinja: {{ jinja }} — nosql notype nosense 软橡皮泥。 它无处不在,甚至在你意想不到的地方。 一点关于 !!unsafe 和美味的山药。

来源: habr.com

添加评论