测试策略

将测试与 Ansible Playbooks 集成

很多人经常问:“我该如何最好地将测试与 Ansible playbooks 集成?”这里有许多选择。Ansible 在设计上就是一个“快速失败”且有序的系统,因此很容易将测试直接嵌入到 Ansible playbooks 中。在本章中,我们将介绍一些集成基础设施测试的模式,并讨论什么样的测试级别较为合适。

注意

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

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

合适的测试级别

Ansible 资源是期望状态的模型。因此,没有必要去测试服务是否已启动、软件包是否已安装或类似的事情。Ansible 本身就是确保这些声明为真的系统。相反,您应该在 playbook 中通过断言(assert)来确认这些事情。

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

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

将检查模式作为漂移测试

在上述设置中,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

如果您使用了角色(您应该使用,角色非常棒!),由 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 playbooks 中。

然而,在 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 通信指南