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 + pytest:molecule init
(在 roles/ 中)
它提供了更大的灵活性来选择
- 简化的设置
- 如何启动您的基础设施:例如,Vagrant、Docker、OpenStack、EC2
- 如何验证您的基础设施测试:Testinfra 和 Goss
但是您的测试必须使用带有 Testinfra 或 Goss 的 pytest 而不是纯 Ansible 编写。如果您想了解更多关于测试 Ansible 角色的信息,请参阅我关于使用 Molecule 的文章。
2 条评论