测试策略

将测试与 Ansible Playbook 集成

很多人经常问:“我该如何最好地将测试与 Ansible playbook 集成?”这里有许多选择。Ansible 的设计初衷就是一个“快速失败”且有序的系统,因此它可以非常方便地将测试直接嵌入到 Ansible playbook 中。在本章中,我们将探讨一些集成基础设施测试的模式,并讨论什么样的测试级别才是合适的。

注意

本章是关于测试你所部署的应用程序的,而不是关于在开发过程中如何测试 Ansible 模块的。关于后者的内容,请跳转到“开发(Development)”部分。

通过在部署工作流中引入一定程度的测试,当代码进入生产环境时,意外情况会减少。在许多情况下,测试可以在生产环境中使用,以防止失败的更新迁移到整个安装环境。由于 Ansible 是基于推送(push-based)的,因此在 localhost 或测试服务器上运行这些步骤也非常简单。Ansible 允许你在升级工作流中插入尽可能多的检查与平衡机制。

合适的测试级别

Ansible 资源是期望状态(desired-state)的模型。因此,没有必要去测试服务是否已启动、软件包是否已安装或其他类似事项。Ansible 就是那个确保这些事项在声明式意义上成立的系统。相反,你应该在 playbook 中对这些结果进行断言(assert)。

tasks:
  - ansible.builtin.service:
      name: foo
      state: started
      enabled: true

如果你认为服务可能没有启动,最好的做法是请求启动它。如果服务启动失败,Ansible 会发出相应的报错。(这不应与服务是否在执行某些功能性操作相混淆,我们稍后将详细介绍如何进行功能性测试)。

将检查模式(Check Mode)作为漂移测试

在上述设置中,Ansible 的 --check 模式也可以作为一层测试。如果在现有系统上运行部署 playbook,在 ansible 命令中使用 --check 标志将报告 Ansible 是否认为需要进行任何更改才能使系统达到期望状态。

这可以让您提前知道给定系统是否有部署需求。通常,脚本和命令不在检查模式下运行,因此如果您希望某些步骤即使在使用了 --check 标志时也能在正常模式下执行(例如调用 script 模块),请为这些任务禁用检查模式。

roles:
  - webserver

tasks:
  - ansible.builtin.script: verify.sh
    check_mode: false

对测试有用的模块

某些 playbook 模块特别适合用于测试。下面是一个确保端口开放的示例:

tasks:

  - ansible.builtin.wait_for:
      host: "{{ inventory_hostname }}"
      port: 22
    delegate_to: localhost

这是一个使用 URI 模块确保 Web 服务有返回响应的示例:

tasks:

  - action: uri url=https://www.example.com return_content=yes
    register: webpage

  - fail:
      msg: 'service is not happy'
    when: "'AWESOME' not in webpage.content"

将任意脚本(任何语言)推送到远程主机非常简单,如果脚本返回非零状态码,则会自动失败。

tasks:

  - ansible.builtin.script: test_script1
  - ansible.builtin.script: test_script2 --parameter value --parameter2 value

如果使用了角色(roles,你应该使用,角色非常棒!),由 script 模块推送的脚本可以存放在角色的 ‘files/’ 目录下。

而 assert 模块使得验证各种真值变得非常简单。

tasks:

   - ansible.builtin.shell: /usr/bin/some-command --parameter value
     register: cmd_result

   - ansible.builtin.assert:
       that:
         - "'not ready' not in cmd_result.stderr"
         - "'gizmo enabled' in cmd_result.stdout"

如果你觉得有必要测试那些不由 Ansible 配置声明设置的文件是否存在,‘stat’ 模块是一个绝佳的选择。

tasks:

   - ansible.builtin.stat:
       path: /path/to/something
     register: p

   - ansible.builtin.assert:
       that:
         - p.stat.exists and p.stat.isdir

如前所述,无需检查诸如命令返回码之类的事情。Ansible 会自动检查它们。与其检查用户是否存在,不如考虑使用 user 模块来使其存在。

Ansible 是一个快速失败的系统,因此当创建该用户出错时,它将停止 playbook 的运行。你不需要在它之后再次检查。

测试生命周期

如果你在 playbook 中编写了一定程度的基础验证,它们将在每次部署时运行。

因此,部署到本地开发 VM 和暂存(staging)环境都将在生产部署之前验证各项事宜是否符合计划。

你的工作流可能是这样的:

- Use the same playbook all the time with embedded tests in development
- Use the playbook to deploy to a staging environment (with the same playbooks) that simulates production
- Run an integration test battery written by your QA team against staging
- Deploy to production, with the same integrated tests.

如果你经营的是生产级 Web 服务,集成测试套件应由你的 QA 团队编写。这将包括 Selenium 测试或自动化 API 测试,通常不会嵌入到 Ansible playbook 中。

然而,在 playbook 中包含一些基础的健康检查是有意义的,而且在某些情况下,可能可以在远程节点上运行 QA 套件的一个子集。这就是下一节涵盖的内容。

将测试与滚动更新集成

如果你阅读了 控制任务运行位置:委托和本地操作,你很快就会发现滚动更新模式可以扩展,你可以根据 playbook 运行的成功或失败来决定是否将机器添加到负载均衡器中。

这就是嵌入式测试的极致体现。

---

- hosts: webservers
  serial: 5

  pre_tasks:

    - name: take out of load balancer pool
      ansible.builtin.command: /usr/bin/take_out_of_pool {{ inventory_hostname }}
      delegate_to: 127.0.0.1

  tasks:

    - ansible.builtin.include_role:
        name: "{{ item }}"
      loop:
        - common
        - webserver

    - name: run any notified handlers
      ansible.builtin.meta: flush_handlers

    - name: test the configuration
      ansible.builtin.include_role:
        name: apply_testing_checks

  post_tasks:

    - name: add back to load balancer pool
      ansible.builtin.command: /usr/bin/add_back_to_pool {{ inventory_hostname }}
      delegate_to: 127.0.0.1

当然,在上述示例中,“移出池”和“重新加入”步骤将被替换为对 Ansible 负载均衡器模块或相应 shell 命令的调用。你可能还会使用监控模块来为该机器启动和结束停机窗口。

然而,从上述内容你可以看到,测试被用作一个“门禁”——如果 “apply_testing_checks” 步骤没有执行,机器将不会回到池中。

阅读关于 “max_fail_percentage” 的委托章节,你还可以控制多少个测试失败将导致滚动更新停止推进。

上述方法也可以修改为从一台测试机远程地对一台目标机运行步骤。

---

- hosts: webservers
  serial: 5

  pre_tasks:

    - name: take out of load balancer pool
      ansible.builtin.command: /usr/bin/take_out_of_pool {{ inventory_hostname }}
      delegate_to: 127.0.0.1

  roles:

     - common
     - webserver

  tasks:
     - ansible.builtin.script: /srv/qa_team/app_testing_script.sh --server {{ inventory_hostname }}
       delegate_to: testing_server

  post_tasks:

    - name: add back to load balancer pool
      ansible.builtin.command: /usr/bin/add_back_to_pool {{ inventory_hostname }}
      delegate_to: 127.0.0.1

在上述示例中,在将远程节点重新加入池之前,从测试服务器运行一个脚本对其进行测试。

如果出现问题,可以使用 Ansible 自动生成的重试文件(retry file)修复少数失败的服务器,仅在这些服务器上重复部署。

实现持续部署

如果需要,上述技术可以扩展以支持持续部署(Continuous Deployment)实践。

工作流可能如下所示:

- Write and use automation to deploy local development VMs
- Have a CI system like Jenkins deploy to a staging environment on every code change
- The deploy job calls testing scripts to pass/fail a build on every deploy
- If the deploy job succeeds, it runs the same deploy playbook against production inventory

一些 Ansible 用户使用上述方法每小时部署十几次,而无需关闭所有基础设施。如果你希望达到这个水平,自动化 QA 文化至关重要。

如果你仍然在进行大量的手动 QA,你仍然应该决定是否手动部署,但采用前一节的滚动更新模式并使用 ‘script’、‘stat’、‘uri’ 和 ‘assert’ 等模块结合一些基础健康检查仍然会有所帮助。

结论

Ansible 认为,你不应该需要另一个框架来验证基础设施的基础事项是否属实。这是因为 Ansible 是一个基于顺序的系统,在遇到主机的未处理错误时会立即失败,并阻止该主机的进一步配置。这使得错误能够迅速显现,并在 Ansible 运行结束时的摘要中显示。

然而,由于 Ansible 被设计为多层编排系统,它使得在 playbook 运行结束时集成测试变得非常简单,可以通过简单的任务或角色来实现。当与滚动更新结合使用时,测试步骤可以决定是否将机器重新放入负载均衡池中。

最后,因为 Ansible 的错误会一直传递到 Ansible 程序本身的返回码,且 Ansible 默认以简单的推送模式运行,因此如果你希望在持续集成/持续交付(CI/CD)流水线中将其用于系统部署,Ansible 是构建环境中的绝佳步骤,正如上述章节所涵盖的那样。

重点不应放在基础设施测试上,而应放在应用程序测试上。因此,我们强烈建议你与 QA 团队沟通,询问在每次部署开发 VM 时运行什么样的测试有意义,以及在每次部署暂存环境时他们希望运行什么样的测试。显然,在开发阶段,单元测试也非常棒。但不要对你的 playbook 进行单元测试。Ansible 以声明方式描述资源状态,所以你不需要这样做。不过,如果你有某些必须确定的情况,那是很好的,stat/assert 等模块就是为此目的而设计的绝佳工具。

总而言之,测试是非常依赖组织架构和具体站点的情况。每个人都应该进行测试,但对你的环境最合理的方式会根据你部署的内容和使用者而有所不同——但每个人都能从更健壮、更可靠的部署系统中获益。

另请参阅

集合索引

浏览现有的集合、模块和插件

使用 Playbook

Playbook 简介

控制任务运行位置:委托与本地操作

委托(Delegation),适用于负载均衡器、云平台和本地执行步骤的工作。

交流方式

有疑问?需要帮助?想分享你的想法?请访问 Ansible 通信指南