如何在Ubuntu16.04上使用Molecule测试Ansible角色
作为 Write for DOnations 计划的一部分,作者选择了 Mozilla 基金会 来接受捐赠。
介绍
Ansible 中的单元测试是确保角色按预期运行的关键。 Molecule 允许您指定针对不同环境测试角色的场景,从而简化了此过程。 在后台使用 Ansible,Molecule 将角色卸载到配置程序中,该配置程序在配置的环境中部署角色并调用验证程序(例如 Testinfra)来检查配置漂移。 这可确保您的角色在该特定场景中对环境进行了所有预期的更改。
在本指南中,您将构建一个 Ansible 角色,将 Apache 部署到主机并在 CentOS 7 上配置 Firewalld。 要测试此角色是否按预期工作,您将使用 Docker 作为驱动程序和 Testinfra(用于测试服务器状态的 Python 库)在 Molecule 中创建一个测试。 Molecule 将提供 Docker 容器来测试角色,Testinfra 将验证服务器是否已按预期配置。 完成后,您将能够为跨环境的构建创建多个测试用例,并使用 Molecule 运行这些测试。
先决条件
在开始本指南之前,您需要以下内容:
- 一台 Ubuntu 16.04 服务器。 按照 Initial Server Setup with Ubuntu 16.04 指南中的步骤创建非 root sudo 用户,并确保无需密码即可连接到服务器。
- Docker 安装在您的服务器上。 按照 如何在 Ubuntu 16.04 上安装和使用 Docker 中的步骤 1 和 2,并确保将您的非 root 用户添加到
docker
组。 - 熟悉 Ansible 剧本。 如需查看,请参阅 配置管理 101:编写 Ansible Playbooks。
第 1 步 — 准备环境
为了创建我们的角色和测试,让我们首先创建一个虚拟环境并安装 Molecule。 安装 Molecule 也将安装 Ansible,允许使用 playbook 来创建角色和运行测试。
首先以非 root 用户身份登录并确保您的存储库是最新的:
sudo apt-get update
这将确保您的包存储库包含最新版本的 python-pip
包,它将安装 pip
和 Python 2.7。 我们将使用 pip
创建一个虚拟环境并安装额外的包。 要安装 pip
,请运行:
sudo apt-get install -y python-pip
使用 pip
安装 virtualenv
Python 模块:
python -m pip install virtualenv
接下来,让我们创建并激活虚拟环境:
python -m virtualenv my_env
激活它以确保您的操作仅限于该环境:
source my_env/bin/activate
使用 pip
安装 molecule
和 docker
:
python -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
您将看到列出每个默认测试操作的输出:
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 ...
在开始测试之前,Molecule 会验证配置文件 molecule.yml
以确保一切正常。 它还打印这个测试矩阵,它指定了测试动作的顺序。
创建角色并自定义测试后,我们将详细讨论每个测试操作。 现在,请注意每个测试的 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 索引页面。
使用 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 linux2 -- Python 2.7.12, pytest-3.8.0, py-1.6.0, 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 56.73 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 官方文档 是学习如何使用 Molecule 的最佳资源。