关于 Ansible 模块,你需要了解的

了解如何以及何时为 Ansible 开发自定义模块。
249 位读者喜欢这个。
Automated provisioning in Kubernetes

Opensource.com

Ansible 的工作原理是连接到节点并发送称为模块的小程序以进行远程执行。这使其成为一种推送架构,其中配置从 Ansible 推送到没有代理的服务器,而不是拉取模型,这在基于代理的配置管理系统中很常见,配置是被拉取的。

这些模块映射到资源及其各自的状态,这些状态在 YAML 文件中表示。它们使您能够管理几乎所有具有 API、CLI 或您可以与之交互的配置文件的事物,包括网络设备(如负载均衡器、交换机、防火墙)、容器编排器、容器本身,甚至虚拟机实例(在虚拟机监控程序或公共云(例如,AWS、GCE、Azure)和/或私有云(例如,OpenStack、CloudStack)中),以及存储和安全设备以及系统配置。

借助 Ansible 的自带电池模型,包含了数百个模块,并且剧本中的任何任务背后都有一个模块。

构建模块的约定很简单:stdout 中的 JSON。YAML 文件中声明的配置通过 SSH/WinRM(或任何其他连接插件)通过网络作为小型脚本传递,以便在目标服务器中执行。模块可以使用任何能够返回 JSON 的语言编写,尽管大多数 Ansible 模块(Windows PowerShell 除外)都是使用 Ansible API 用 Python 编写的(这简化了新模块的开发)。

模块是扩展 Ansible 功能的一种方式。其他替代方案,如动态清单和插件,也可以增强 Ansible 的功能。了解它们很重要,这样您就知道何时使用一种而不是另一种。

插件分为几个具有不同目标的类别,如 Action、Cache、Callback、Connection、Filters、Lookup 和 Vars。最流行的插件是

  • 连接插件: 这些插件实现了一种与清单中的服务器通信的方式(例如,SSH、WinRM、Telnet);换句话说,自动化代码如何通过网络传输以供执行。
  • 过滤器插件: 这些插件允许您在剧本中操作数据。这是 Jinja2 的一项功能,Ansible 利用它来解决基础设施即代码的问题。
  • 查找插件: 这些插件从外部源(例如,env、file、Hiera、数据库、HashiCorp Vault)获取数据。

Ansible 的官方文档是关于开发插件的良好资源。

何时应该开发模块?

虽然 Ansible 附带了许多模块,但您的需求可能尚未被覆盖,或者是一些太具体的东西——例如,可能仅在您的组织中才有意义的解决方案。幸运的是,官方文档提供了关于开发模块的优秀指南。

重要提示: 在您开始开发新东西之前,请务必检查未完成的拉取请求,在 #ansible-devel (IRC/Freenode) 上询问开发人员,或搜索开发列表和/或现有的工作组,以查看模块是否存在或正在开发中。

表明您需要新模块而不是使用现有模块的迹象包括

  • 传统的配置管理方法(例如,templates、file、get_url、lineinfile)无法正确解决您的问题。
  • 您必须使用复杂的命令组合、shell、过滤器、使用神奇正则表达式的文本处理以及使用 curl 的 API 调用才能实现您的目标。
  • 您的剧本复杂、命令式、非幂等,甚至是非确定性的。

在理想情况下,工具或服务已经具有用于管理的 API 或 CLI,并且它返回某种结构化数据(JSON、XML、YAML)。

识别好的和坏的剧本

“Make love, but don't make a shell script in YAML.” (意译:享受编程,但不要在 YAML 中写 shell 脚本。)

那么,是什么让剧本变坏呢?

- name: Read a remote resource
   command: "curl -v http://xpto/resource/abc"
 register: resource
 changed_when: False

 - name: Create a resource in case it does not exist
   command: "curl -X POST http://xpto/resource/abc -d '{ config:{ client: xyz, url: http://beta, pattern: *.* } }'"
   when: "resource.stdout | 404"

 # Leave it here in case I need to remove it hehehe
 #- name: Remove resource
 #  command: "curl -X DELETE http://xpto/resource/abc"
 #  when: resource.stdout == 1

除了非常脆弱(如果资源状态在某处包含 404 会怎样?)并且需要额外的代码才能实现幂等性之外,当资源状态更改时,此剧本无法更新资源。

以这种方式编写的剧本不尊重许多基础设施即代码的原则。它们对人类来说不可读,难以重用和参数化,并且不遵循大多数配置管理工具鼓励的声明式模型。它们也未能实现幂等性并收敛到声明的状态。

糟糕的剧本可能会危及您对自动化的采用。它们没有利用配置管理工具来提高速度,而是与基于脚本和命令执行的命令式自动化方法具有相同的问题。这创建了一种场景,您只是将 Ansible 用作传递旧脚本的手段,将您已有的内容复制到 YAML 文件中。

以下是如何重写此示例以遵循基础设施即代码原则。

- name: XPTO
  xpto:
    name: abc
    state: present
    config:
      client: xyz
      url: http://beta
      pattern: "*.*"

这种基于自定义模块的方法的优点包括

  • 它是声明式的——资源在 YAML 中得到了正确的表示。
  • 它是幂等的。
  • 它从声明的状态收敛到当前状态
  • 它对人类来说是可读的。
  • 它易于参数化或重用。

实现自定义模块

让我们使用 WildFly,一个开源 Java 应用服务器,作为示例来介绍我们不太好的剧本的自定义模块

 - name: Read datasource
   command: "jboss-cli.sh -c '/subsystem=datasources/data-source=DemoDS:read-resource()'"
   register: datasource

 - name: Create datasource
   command: "jboss-cli.sh -c '/subsystem=datasources/data-source=DemoDS:add(driver-name=h2, user-name=sa, password=sa, min-pool-size=20, max-pool-size=40, connection-url=.jdbc:h2:mem:demo;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE..)'"
   when: 'datasource.stdout | outcome => failed'

问题

  • 它不是声明式的。
  • JBoss-CLI 以类似 JSON 语法的纯文本形式返回;因此,这种方法非常脆弱,因为我们需要一种针对此表示法的解析器。即使看似简单的解析器也可能过于复杂,无法处理许多异常
  • JBoss-CLI 只是一个向管理 API(端口 9990)发送请求的接口。
  • 发送 HTTP 请求比打开新的 JBoss-CLI 会话、连接和发送命令更有效。
  • 它不会收敛到所需的状态;它仅在资源不存在时创建资源。

此模块的自定义模块将如下所示

- name: Configure datasource
      jboss_resource:
        name: "/subsystem=datasources/data-source=DemoDS"
        state: present
        attributes:
          driver-name: h2
          connection-url: "jdbc:h2:mem:demo;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"
          jndi-name: "java:jboss/datasources/DemoDS"
          user-name: sa
          password: sa
          min-pool-size: 20
          max-pool-size: 40

此剧本是声明式的、幂等的、更易读的,并且无论当前状态如何,都会收敛到所需的状态。

为什么要学习构建自定义模块?

学习如何构建自定义模块的充分理由包括

  • 改进现有模块
  • 您有糟糕的剧本并想改进它们,或者……
  • 您没有,但想避免拥有糟糕的剧本。
  • 了解如何构建模块可以大大提高您调试剧本问题的能力,从而提高您的工作效率。

“……抽象节省了我们工作的时间,但并没有节省我们学习的时间。” —— Joel Spolsky,《泄漏抽象法则

自定义 Ansible 模块 101

  • stdout 中的 JSON:这就是约定!
  • 它们可以用任何语言编写,但是……
  • Python 通常是最佳选择(或次佳选择)
  • Ansible 附带的大多数模块(lib/ansible/modules)都是用 Python 编写的,并且应该支持兼容的版本。

Ansible 之道

  • 第一步
    git clone https://github.com/ansible/ansible.git
  • lib/ansible/modules/ 中导航并阅读现有的模块代码。
  • 您的工具是:Git、Python、virtualenv、pdb(Python 调试器)
  • 有关全面的说明,请查阅官方文档

另一种选择:将其放在库目录中

library/                  # if any custom modules, put them here (optional)
module_utils/             # if any custom module_utils to support modules, put them here (optional)
filter_plugins/           # if any custom filter plugins, put them here (optional)

site.yml                  # master playbook
webservers.yml            # playbook for webserver tier
dbservers.yml             # playbook for dbserver tier

roles/
    common/               # this hierarchy represents a "role"
        library/          # roles can also include custom modules
        module_utils/     # roles can also include custom module_utils
        lookup_plugins/   # or other types of plugins, like lookup in this case
  • 更容易上手。
  • 除了 Ansible 和您最喜欢的 IDE/文本编辑器之外,不需要任何其他东西。
  • 如果它是将在内部使用的东西,这是您的最佳选择。

提示: 如果您需要修补模块,例如,您可以使用此目录布局来覆盖现有模块。

第一步

您可以使用自己的语言(包括使用另一种语言)来完成它,或者您可以使用 AnsibleModule 类,因为它更容易以 Ansible 期望的方式将 JSON 放入 stdout (exit_json(), fail_json()) (msg, meta, has_changed, result),并且也更容易处理输入 (params[]) 并记录其执行 (log(), debug())。

def main():

  arguments = dict(name=dict(required=True, type='str'),
                  state=dict(choices=['present', 'absent'], default='present'),
                  config=dict(required=False, type='dict'))

  module = AnsibleModule(argument_spec=arguments, supports_check_mode=True)
  try:
      if module.check_mode:
          # Do not do anything, only verifies current state and report it
          module.exit_json(changed=has_changed, meta=result, msg='Fez alguma coisa ou não...')

      if module.params['state'] == 'present':
          # Verify the presence of a resource
          # Desired state `module.params['param_name'] is equal to the current state?
          module.exit_json(changed=has_changed, meta=result)

      if module.params['state'] == 'absent':
          # Remove the resource in case it exists
          module.exit_json(changed=has_changed, meta=result)

  except Error as err:
      module.fail_json(msg=str(err))

注意: check_mode(“试运行”)允许执行剧本或仅验证是否需要更改,但不执行更改。 此外,module_utils 目录可用于不同模块之间的共享代码。

有关完整的 Wildfly 示例,请查看此拉取请求

运行测试

Ansible 之道

Ansible 代码库经过大量测试,并且每次提交都会在其持续集成 (CI) 服务器 Shippable 中触发构建,其中包括 linting、单元测试和集成测试。

对于集成测试,它使用容器和 Ansible 本身来执行设置和验证阶段。这是一个针对我们自定义模块示例代码的测试用例(用 Ansible 编写)

- name: Configure datasource
 jboss_resource:
   name: "/subsystem=datasources/data-source=DemoDS"
   state: present
   attributes:
     connection-url: "jdbc:h2:mem:demo;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"
     ...
 register: result

- name: assert output message that datasource was created
 assert:
   that:
      - "result.changed == true"
      - "'Added /subsystem=datasources/data-source=DemoDS' in result.msg"

另一种选择:将模块与您的角色捆绑在一起

这是一个完整示例,在一个简单的角色中

Molecule + Vagrant + pytestmolecule init(在 roles/ 中)

它提供了更大的灵活性来选择

  • 简化的设置
  • 如何启动您的基础设施:例如,Vagrant、Docker、OpenStack、EC2
  • 如何验证您的基础设施测试:Testinfra 和 Goss

但是您的测试必须使用带有 Testinfra 或 Goss 的 pytest 而不是纯 Ansible 编写。如果您想了解更多关于测试 Ansible 角色的信息,请参阅我关于使用 Molecule 的文章。

User profile image.
DevOps 会议的开发者、演讲者、开源贡献者、偶尔的作家,以及痴迷于测试和自动化。没有 CLI 工具就无法生活。

2 条评论

很好的文章,但是……据我所知,Ansible 现在不提供您编写的嵌套属性 - config{client='',patern='' 等}

- name: XPTO
xpto
name: abc
state: present
config
client: xyz
url: http://beta
pattern: "*.*"

Creative Commons License本作品根据 Creative Commons Attribution-Share Alike 4.0 International License 获得许可。
© . All rights reserved.