作为 Write for DOnations 计划的一部分,作者选择了 Mozilla 基金会 来接受捐赠。
介绍
Ansible 中的单元测试是确保角色按预期运行的关键。 Molecule 允许您指定针对不同环境测试角色的场景,从而简化了此过程。 在后台使用 Ansible,Molecule 将角色卸载到配置程序中,该配置程序在配置的环境中部署角色并调用验证程序(例如 Testinfra)来检查配置漂移。 这可确保您的角色在该特定场景中对环境进行了所有预期的更改。
在本指南中,您将构建一个 Ansible 角色,将 Apache 部署到主机并在 CentOS 7 上配置 firewalld。 要测试此角色是否按预期工作,您将使用 Docker 作为驱动程序和 Testinfra(用于测试服务器状态的 Python 库)在 Molecule 中创建一个测试。 Molecule 将提供 Docker 容器来测试角色,Testinfra 将验证服务器是否已按预期配置。 完成后,您将能够为跨环境的构建创建多个测试用例,并使用 Molecule 运行这些测试。
先决条件
在开始本指南之前,您需要以下内容:
- 一台 Ubuntu 18.04 服务器。 按照 Initial Server Setup with Ubuntu 18.04 指南中的步骤创建非 root sudo 用户,并确保无需密码即可连接到服务器。
- Docker 安装在您的服务器上。 按照 如何在 Ubuntu 18.04 上安装和使用 Docker 中的步骤 1 和 2,包括将您的非 root 用户添加到
docker
组。 - 在您的服务器上安装并配置了 Python 3 和
venv
。 遵循 如何在 Ubuntu 18.04 服务器 上安装 Python 3 和设置编程环境以获取指导。 - 熟悉 Ansible 剧本。 如需查看,请参阅 配置管理 101:编写 Ansible Playbooks。
第 1 步 — 准备环境
如果您已满足先决条件,则应安装并正确配置 Python 3、venv
和 Docker。 让我们首先创建一个虚拟环境来使用 Molecule 测试 Ansible。
首先以非 root 用户身份登录并创建一个新的虚拟环境:
python3 -m venv my_env
激活它以确保您的操作仅限于该环境:
source my_env/bin/activate
接下来,在您激活的环境中,安装 wheel
包,它提供了 bdist_wheel
setuptools
扩展,pip
用于安装 Ansible:
python3 -m pip install wheel
您现在可以使用 pip
安装 molecule
和 docker
。 Ansible 将作为 Molecule 的依赖项自动安装:
python3 -m pip install molecule docker
以下是每个软件包的作用:
molecule
:这是您将用于测试角色的主要分子包。 安装molecule
会自动安装 Ansible 以及其他依赖项,并允许使用 Ansible 剧本来执行角色和测试。docker
:这个 Python 库被 Molecule 用来与 Docker 交互。 由于您使用 Docker 作为驱动程序,因此您将需要它。
接下来,让我们在 Molecule 中创建一个角色。
第 2 步 — 在分子中创建角色
设置好环境后,您可以使用 Molecule 创建一个基本角色,用于测试 Apache 的安装。 该角色将创建目录结构和一些初始测试,并将 Docker 指定为驱动程序,以便 Molecule 使用 Docker 运行其测试。
创建一个名为 ansible-apache
的新角色:
molecule init role -r ansible-apache -d docker
-r
标志指定角色的名称,而 -d
指定驱动程序,它为 Molecule 提供用于测试的主机。
切换到新创建角色的目录:
cd ansible-apache
测试默认角色以检查 Molecule 是否已正确设置:
molecule test
您将看到列出每个默认测试操作的输出。 在开始测试之前,Molecule 会验证配置文件 molecule.yml
以确保一切正常。 它还打印这个测试矩阵,它指定了测试操作的顺序:
Output--> Validating schema /home/sammy/ansible-apache/molecule/default/molecule.yml. Validation completed successfully. --> Test matrix └── default ├── lint ├── destroy ├── dependency ├── syntax ├── create ├── prepare ├── converge ├── idempotence ├── side_effect ├── verify └── destroy ...
创建角色并自定义测试后,我们将详细讨论每个测试操作。 现在,请注意每个测试的 PLAY_RECAP
,并确保没有任何默认操作返回 failed
状态。 例如,默认 'create'
动作的 PLAY_RECAP
应该如下所示:
Output... PLAY RECAP ********************************************************************* localhost : ok=5 changed=4 unreachable=0 failed=0
让我们继续修改角色以配置 Apache 和 firewalld。
第 3 步 — 配置 Apache 和 Firewalld
要配置 Apache 和 firewalld,您将为角色创建一个任务文件,指定要安装的包和要启用的服务。 这些详细信息将从变量文件和模板中提取,您将使用它来替换默认的 Apache 索引页面。
仍然在 ansible-apache
目录中,使用 nano
或您喜欢的文本编辑器为角色创建一个任务文件:
nano tasks/main.yml
您会看到该文件已经存在。 删除那里的内容并将其替换为以下代码以安装所需的包并启用正确的服务、HTML 默认值和防火墙设置:
~/ansible-apache/tasks/main.yml
--- - name: "Ensure required packages are present" yum: name: "{{ pkg_list }}" state: present - name: "Ensure latest index.html is present" template: src: index.html.j2 dest: /var/www/html/index.html - name: "Ensure httpd service is started and enabled" service: name: "{{ item }}" state: started enabled: true with_items: "{{ svc_list }}" - name: "Whitelist http in firewalld" firewalld: service: http state: enabled permanent: true immediate: true
该剧本包括 4 个任务:
"Ensure required packages are present"
:此任务将安装pkg_list
下的变量文件中列出的包。 变量文件将位于~/ansible-apache/vars/main.yml
,您将在此步骤结束时创建它。"Ensure latest index.html is present"
:此任务将复制模板页面index.html.j2
,并将其粘贴到由 Apache 生成的默认索引文件/var/www/html/index.html
上。 您还将在此步骤中创建新模板。"Ensure httpd service is started and enabled"
:此任务将启动并启用变量文件中svc_list
中列出的服务。"Whitelist http in firewalld"
:此任务会将firewalld
中的http
服务列入白名单。 Firewalld 是一个完整的防火墙解决方案,默认出现在 CentOS 服务器上。 要使http
服务正常工作,您需要公开所需的端口。 指示firewalld
将服务列入白名单可确保它将服务所需的所有端口列入白名单。
完成后保存并关闭文件。
接下来,让我们为index.html.j2
模板页面创建一个templates
目录:
mkdir templates
创建页面本身:
nano templates/index.html.j2
粘贴以下样板代码:
~/ansible-apache/templates/index.html.j2
<div style="text-align: center"> <h2>Managed by Ansible</h2> </div>
保存并关闭文件。
完成角色的最后一步是编写变量文件,它为我们的主要角色手册提供包和服务的名称:
nano vars/main.yml
使用以下代码粘贴默认内容,指定 pkg_list
和 svc_list
:
~/ansible-apache/vars/main.yml
--- pkg_list: - httpd - firewalld svc_list: - httpd - firewalld
这些列表包含以下信息:
pkg_list
:这包含角色将安装的包的名称:httpd
和firewalld
。svc_list
:这包含角色将启动和启用的服务的名称:httpd
和firewalld
。
注意: 确保您的变量文件没有任何空行,否则您的测试将在 linting 期间失败。
现在您已经完成了角色的创建,让我们配置 Molecule 以测试它是否按预期工作。
第 4 步 — 修改运行测试的角色
在我们的例子中,配置 Molecule 涉及修改 Molecule 配置文件 molecule.yml
以添加平台规范。 因为您正在测试配置和启动 httpd
systemd 服务的角色,所以您需要使用配置了 systemd 并启用特权模式的映像。 在本教程中,您将使用 Docker Hub 上可用的 milcom/centos7-systemd
映像 。 特权模式允许容器使用其主机的几乎所有功能运行。
让我们编辑 molecule.yml
以反映这些变化:
nano molecule/default/molecule.yml
添加突出显示的平台信息:
~/ansible-apache/molecule/default/molecule.yml
--- dependency: name: galaxy driver: name: docker lint: name: yamllint platforms: - name: centos7 image: milcom/centos7-systemd privileged: true provisioner: name: ansible lint: name: ansible-lint scenario: name: default verifier: name: testinfra lint: name: flake8
完成后保存并关闭文件。
现在您已经成功配置了测试环境,让我们继续编写 Molecule 在执行角色后将对您的容器运行的测试用例。
第五步——编写测试用例
在对该角色的测试中,您将检查以下条件:
- 已安装
httpd
和firewalld
软件包。 httpd
和firewalld
服务正在运行并启用。- 在您的防火墙设置中启用了
http
服务。 index.html
包含在模板文件中指定的相同数据。
如果所有这些测试都通过,则该角色按预期工作。
要为这些条件编写测试用例,让我们在 ~/ansible-apache/molecule/default/tests/test_default.py
中编辑默认测试。 使用 Testinfra,我们将测试用例编写为使用 Molecule 类的 Python 函数。
打开test_default.py
:
nano molecule/default/tests/test_default.py
删除文件的内容,以便您可以从头开始编写测试。
注意: 在编写测试时,请确保它们被两个新行分隔,否则它们将失败。
首先导入所需的 Python 模块:
~/ansible-apache/molecule/default/tests/test_default.py
import os import pytest import testinfra.utils.ansible_runner
这些模块包括:
os
:这个内置的 Python 模块启用了依赖于操作系统的功能,使 Python 可以与底层操作系统进行交互。pytest
:pytest 模块启用测试写入。testinfra.utils.ansible_runner
:这个Testinfra模块使用Ansible作为后端来执行命令。
在模块导入下,添加以下代码,该代码使用 Ansible 后端返回当前主机实例:
~/ansible-apache/molecule/default/tests/test_default.py
... testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner( os.environ['MOLECULE_INVENTORY_FILE']).get_hosts('all')
将您的测试文件配置为使用 Ansible 后端,让我们编写单元测试来测试主机的状态。
第一个测试将确保安装了 httpd
和 firewalld
:
~/ansible-apache/molecule/default/tests/test_default.py
... @pytest.mark.parametrize('pkg', [ 'httpd', 'firewalld' ]) def test_pkg(host, pkg): package = host.package(pkg) assert package.is_installed
测试从 pytest.mark.parametrize 装饰器 开始,它允许我们参数化测试的参数。 第一个测试将 test_pkg
作为参数来测试 httpd
和 firewalld
包的存在。
下一个测试检查 httpd
和 firewalld
是否正在运行和启用。 它以 test_svc
作为参数:
~/ansible-apache/molecule/default/tests/test_default.py
... @pytest.mark.parametrize('svc', [ 'httpd', 'firewalld' ]) def test_svc(host, svc): service = host.service(svc) assert service.is_running assert service.is_enabled
最后一个测试检查传递给 parametrize()
的文件和内容是否存在。 如果文件不是由您的角色创建的并且内容设置不正确,assert
将返回 False
:
~/ansible-apache/molecule/default/tests/test_default.py
... @pytest.mark.parametrize('file, content', [ ("/etc/firewalld/zones/public.xml", "<service name=\"http\"/>"), ("/var/www/html/index.html", "Managed by Ansible") ]) def test_files(host, file, content): file = host.file(file) assert file.exists assert file.contains(content)
在每个测试中,assert
将根据测试结果返回 True
或 False
。
完成的文件如下所示:
~/ansible-apache/molecule/default/tests/test_default.py
import os import pytest import testinfra.utils.ansible_runner testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner( os.environ['MOLECULE_INVENTORY_FILE']).get_hosts('all') @pytest.mark.parametrize('pkg', [ 'httpd', 'firewalld' ]) def test_pkg(host, pkg): package = host.package(pkg) assert package.is_installed @pytest.mark.parametrize('svc', [ 'httpd', 'firewalld' ]) def test_svc(host, svc): service = host.service(svc) assert service.is_running assert service.is_enabled @pytest.mark.parametrize('file, content', [ ("/etc/firewalld/zones/public.xml", "<service name=\"http\"/>"), ("/var/www/html/index.html", "Managed by Ansible") ]) def test_files(host, file, content): file = host.file(file) assert file.exists assert file.contains(content)
现在您已经指定了测试用例,让我们测试角色。
第 6 步 — 用分子测试角色
启动测试后,Molecule 将执行您在场景中定义的操作。 现在让我们再次运行默认的 molecule
场景,执行默认测试序列中的操作,同时更仔细地查看每个操作。
再次运行默认场景的测试:
molecule test
这将启动测试运行。 初始输出打印默认测试矩阵:
Output--> Validating schema /home/sammy/ansible-apache/molecule/default/molecule.yml. Validation completed successfully. --> Test matrix └── default ├── lint ├── destroy ├── dependency ├── syntax ├── create ├── prepare ├── converge ├── idempotence ├── side_effect ├── verify └── destroy
让我们看看每个测试操作和预期的输出,从 linting 开始。
linting 动作执行 yamllint
、flake8
和 ansible-lint
:
yamllint
:此 linter 在角色目录中存在的所有 YAML 文件上执行。flake8
:这个 Python 代码 linter 检查为 Testinfra 创建的测试。ansible-lint
:这个用于 Ansible 剧本的 linter 在所有场景中执行。
Output... --> Scenario: 'default' --> Action: 'lint' --> Executing Yamllint on files found in /home/sammy/ansible-apache/... Lint completed successfully. --> Executing Flake8 on files found in /home/sammy/ansible-apache/molecule/default/tests/... Lint completed successfully. --> Executing Ansible Lint on /home/sammy/ansible-apache/molecule/default/playbook.yml... Lint completed successfully.
下一个动作 destroy 使用 destroy.yml
文件执行。 这样做是为了在新创建的容器上测试我们的角色。
默认情况下,destroy 会被调用两次:在测试运行开始时,删除所有预先存在的容器,最后,删除新创建的容器:
Output... --> Scenario: 'default' --> Action: 'destroy' PLAY [Destroy] ***************************************************************** TASK [Destroy molecule instance(s)] ******************************************** changed: [localhost] => (item=None) changed: [localhost] TASK [Wait for instance(s) deletion to complete] ******************************* ok: [localhost] => (item=None) ok: [localhost] TASK [Delete docker network(s)] ************************************************ skipping: [localhost] PLAY RECAP ********************************************************************* localhost : ok=2 changed=1 unreachable=0 failed=0
销毁操作完成后,测试将转到 dependency。 如果您的角色需要,此操作允许您从 ansible-galaxy 中提取依赖项。 在这种情况下,我们的角色不会:
Output... --> Scenario: 'default' --> Action: 'dependency' Skipping, missing the requirements file.
下一个测试动作是 syntax 检查,在默认的 playbook.yml
playbook 上执行。 它的工作方式类似于命令 ansible-playbook --syntax-check playbook.yml
中的 --syntax-check
标志:
Output... --> Scenario: 'default' --> Action: 'syntax' playbook: /home/sammy/ansible-apache/molecule/default/playbook.yml
接下来,测试继续进行 create 操作。 这将使用您角色的 Molecule 目录中的 create.yml
文件来创建具有您的规范的 Docker 容器:
Output... --> Scenario: 'default' --> Action: 'create' PLAY [Create] ****************************************************************** TASK [Log into a Docker registry] ********************************************** skipping: [localhost] => (item=None) skipping: [localhost] TASK [Create Dockerfiles from image names] ************************************* changed: [localhost] => (item=None) changed: [localhost] TASK [Discover local Docker images] ******************************************** ok: [localhost] => (item=None) ok: [localhost] TASK [Build an Ansible compatible image] *************************************** changed: [localhost] => (item=None) changed: [localhost] TASK [Create docker network(s)] ************************************************ skipping: [localhost] TASK [Create molecule instance(s)] ********************************************* changed: [localhost] => (item=None) changed: [localhost] TASK [Wait for instance(s) creation to complete] ******************************* changed: [localhost] => (item=None) changed: [localhost] PLAY RECAP ********************************************************************* localhost : ok=5 changed=4 unreachable=0 failed=0
创建后,测试继续进行 prepare 操作。 此操作执行准备剧本,在运行收敛之前将主机带入特定状态。 如果您的角色需要在执行角色之前对系统进行预配置,这将非常有用。 同样,这不适用于我们的角色:
Output... --> Scenario: 'default' --> Action: 'prepare' Skipping, prepare playbook not configured.
准备好之后,converge 动作通过运行 playbook.yml
剧本在容器上执行你的角色。 如果在 molecule.yml
文件中配置了多个平台,Molecule 将收敛于所有这些:
Output... --> Scenario: 'default' --> Action: 'converge' PLAY [Converge] **************************************************************** TASK [Gathering Facts] ********************************************************* ok: [centos7] TASK [ansible-apache : Ensure required packages are present] ******************* changed: [centos7] TASK [ansible-apache : Ensure latest index.html is present] ******************** changed: [centos7] TASK [ansible-apache : Ensure httpd service is started and enabled] ************ changed: [centos7] => (item=httpd) changed: [centos7] => (item=firewalld) TASK [ansible-apache : Whitelist http in firewalld] **************************** changed: [centos7] PLAY RECAP ********************************************************************* centos7 : ok=5 changed=4 unreachable=0 failed=0
覆盖后,测试继续 幂等。 此操作测试剧本的幂等性,以确保在多次运行中不会发生意外更改:
Output... --> Scenario: 'default' --> Action: 'idempotence' Idempotence completed successfully.
下一个测试动作是 side-effect 动作。 这使您可以产生能够测试更多事物的情况,例如 HA 故障转移。 默认情况下,Molecule 不会配置副作用剧本,并且会跳过该任务:
Output... --> Scenario: 'default' --> Action: 'side_effect' Skipping, side effect playbook not configured.
然后,Molecule 将使用默认验证程序 Testinfra 运行 verifier 操作。 此操作执行您之前在 test_default.py
中编写的测试。 如果所有测试都成功通过,您将看到一条成功消息,并且 Molecule 将继续下一步:
Output... --> Scenario: 'default' --> Action: 'verify' --> Executing Testinfra tests found in /home/sammy/ansible-apache/molecule/default/tests/... ============================= test session starts ============================== platform linux -- Python 3.6.5, pytest-3.7.3, py-1.5.4, pluggy-0.7.1 rootdir: /home/sammy/ansible-apache/molecule/default, inifile: plugins: testinfra-1.14.1 collected 6 items tests/test_default.py ...... [100%] ========================== 6 passed in 41.05 seconds =========================== Verifier completed successfully.
最后,Molecule destroys 在测试期间完成的实例并删除分配给这些实例的网络:
Output... --> Scenario: 'default' --> Action: 'destroy' PLAY [Destroy] ***************************************************************** TASK [Destroy molecule instance(s)] ******************************************** changed: [localhost] => (item=None) changed: [localhost] TASK [Wait for instance(s) deletion to complete] ******************************* changed: [localhost] => (item=None) changed: [localhost] TASK [Delete docker network(s)] ************************************************ skipping: [localhost] PLAY RECAP ********************************************************************* localhost : ok=2 changed=2 unreachable=0 failed=0
测试操作现已完成,验证您的角色是否按预期工作。
结论
在本文中,您创建了一个 Ansible 角色来安装和配置 Apache 和 firewalld。 然后,您使用 Testinfra 编写单元测试,Molecule 用来断言角色运行成功。
您可以对高度复杂的角色使用相同的基本方法,也可以使用 CI 管道进行自动化测试。 Molecule 是一个高度可配置的工具,可用于测试 Ansible 支持的任何提供程序的角色,而不仅仅是 Docker。 还可以针对您自己的基础架构进行自动化测试,以确保您的角色始终是最新的并且可以正常工作。 您可以使用 Molecule 和 Travis CI 将持续测试集成到您的工作流程中,以及 如何在 Ubuntu 18.04 上使用 Molecule 和 Travis CI 实现 Ansible 角色的持续测试教程。
Molecule 官方文档 是学习如何使用 Molecule 的最佳资源。