Testing Ansible Automation with Molecule Pt. 1
There is a lot of Ansible out there. Let’s look at how to test roles using Molecule, Docker, and Testinfra.
What is Molecule?
Molecule is a RedHat project designed to aid in testing Ansible roles. It provides support for testing with various operating systems and distributions. Molecule is also very versatile, allowing for an array of virtualization providers, test frameworks, and test scenarios. This approach encourages consistency in developing and maintaining roles.
Getting Started
Setting up your environment
Python3 and pip are requirements for Ansible and Molecule. The installation and setup of those tools are outside the scope of this guide.
The following commands will install Ansible and Molecule using pip. For other installation options, visit their respective documentation pages, Ansible and Molecule.
You will also need docker-ce installed along with the Docker plugin for Molecule. The last command in the above script installs the docker plugin. This method is how you install any molecule plugins you plan to use. Once done, we can Docker can now serve as the driver in Molecule.
Initializing a new role with Molecule
Now that the prerequisites are ready, let’s start with some basics. Begin by running molecule init role sample.demo_role --driver-name docker --verifier-name testinfra
. Boom, Molecule creates a new role already configured with the test directory. Let’s look at the directory, and then we’ll dive into that command in more detail.
How awesome is that? It created everything we need to build a role and test it simultaneously.
Examining the command and what it creates
Here is a description of the command and options we used:
molecule init role sample.demo_roleinit role
leverages ansible-galaxy
to create a templated directory for <NAME_SPACE>.<ROLE_NAME>
. The command also preps the Molecule directory to help you get started. Alternatively, if you already have a role, you can initialize Molecule as a scenario with molecule init scenario -r <ROLE_NAME>
--driver-name Docker
This tells Molecule what driver to use for your test environment. The driver specifies what framework or infrastructure to deploy for the tests. Several available drivers include podman, libvirt, Azure, and many others. Regardless of your driver, you will need to install the module using pip. For this post, we are going to focus on using Docker.
--verifier-name testinfra
This specifies what verifier you want to use for your default scenario. The verifier is the mechanism used for testing your roles. Some available options include Ansible, testinfra, and inspec. While Ansible is the default verifier, we will use testinfra for this demo.
Here is a description of the created Molecule files:
./molecule/default/molecule.yml
This is the core file for Molecule. Used to define your testing steps, scenarios, dependencies, and other configuration options. The generated file should look like this:
./molecule/default/converge.yml
This is the playbook that Molecule will run to provision the targets for testing. Below is the file that Molecule creates. If you can use this file in a playbook, you can use it here.
./molecule/default/tests/test_default.py
This is the initial test file generated for testinfra. This is where we will be putting our tests for this demo. However, you are not restricted to this file or even this directory for your tests. The default test is shown below.
Getting Practical
A role has to do something
We have a role that currently doesn’t do anything, let’s fix that by adding some tasks to ./tasks/main.yml
. For this post, we’ll stick to the basics, only adding a few things we can write tests for. In this case, some yum packages, a user, and a config file. Provided in the code below.
Create and Converge
We have a role that will process some tasks, so now what? First, we can run the command molecule create
in the ./demo_role
directory. Create uses the settings defined in the molecule.yml
config to determine the driver and the platform. For this example, Molecule will use the following lines in the molecule.yml
.
It will use Ansible to launch a Docker container based on the quay.io/centos/centos:stream8
image, pulling it from the container registry if needed.
Now that we have a target and a role, let’s bring them together with molecule converge
. The converge
command runs the converge.yml
playbook against the target platform.
INFO default scenario test matrix: dependency, create, prepare, converge
INFO Performing prerun with role_name_check=0...
INFO Set ANSIBLE_LIBRARY=/home/pcritchfield/.cache/ansible-compat/54dfe2/modules:/home/pcritchfield/.ansible/plugins/modules:/usr/share/ansible/plugins/modules
INFO Set ANSIBLE_COLLECTIONS_PATH=/home/pcritchfield/.cache/ansible-compat/54dfe2/collections:/home/pcritchfield/.ansible/collections:/usr/share/ansible/collections
INFO Set ANSIBLE_ROLES_PATH=/home/pcritchfield/.cache/ansible-compat/54dfe2/roles:/home/pcritchfield/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles
INFO Using /home/pcritchfield/.cache/ansible-compat/54dfe2/roles/sample.demo_role symlink to current repository in order to enable Ansible to find the role using its expected full name.
INFO Running default > dependency
WARNING Skipping, missing the requirements file.
WARNING Skipping, missing the requirements file.
INFO Running default > create
WARNING Skipping, instances already created.
INFO Running default > prepare
WARNING Skipping, prepare playbook not configured.
INFO Running default > converge
INFO Sanity checks: 'docker'PLAY [Converge] ****************************************************************TASK [Gathering Facts] *********************************************************
ok: [instance]TASK [Include sample.demo_role] ************************************************TASK [sample.demo_role : install some packages] ********************************
changed: [instance] => (item=epel-release)
changed: [instance] => (item=htop)
changed: [instance] => (item=nginx)
changed: [instance] => (item=git)TASK [sample.demo_role : add webapp user] **************************************
changed: [instance]TASK [sample.demo_role : create an app directory owned by webapp] **************
changed: [instance]TASK [sample.demo_role : create an app directory owned by webapp] **************
changed: [instance]PLAY RECAP *********************************************************************
instance : ok=5 changed=4 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Looking at that output, we can see the Molecule steps being run. On the line showing INFO default scenario test matrix: dependency, create, prepare, converge
. This means that running molecule create
isn’t always required. Often you can just run the converge command since it already includes create.
Write tests and Verify
Now that we have run converge and deployed the containers, let’s run some tests. Use the command molecule verify
to run the default test in test_default.py
. You should see output similar to:
INFO default scenario test matrix: verify
INFO Performing prerun with role_name_check=0...
INFO Set ANSIBLE_LIBRARY=/home/pcritchfield/.cache/ansible-compat/54dfe2/modules:/home/pcritchfield/.ansible/plugins/modules:/usr/share/ansible/plugins/modules
INFO Set ANSIBLE_COLLECTIONS_PATH=/home/pcritchfield/.cache/ansible-compat/54dfe2/collections:/home/pcritchfield/.ansible/collections:/usr/share/ansible/collections
INFO Set ANSIBLE_ROLES_PATH=/home/pcritchfield/.cache/ansible-compat/54dfe2/roles:/home/pcritchfield/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles
INFO Using /home/pcritchfield/.cache/ansible-compat/54dfe2/roles/sample.demo_role symlink to current repository in order to enable Ansible to find the role using its expected full name.
INFO Running default > verify
INFO Executing Testinfra tests found in /home/pcritchfield/Projects/ansible_molecule/roles/demo_role/molecule/default/tests/...
============================= test session starts ==============================
platform linux -- Python 3.8.10, pytest-7.1.2, pluggy-1.0.0
rootdir: /home/pcritchfield
plugins: testinfra-6.8.0
collected 1 itemmolecule/default/tests/test_default.py . [100%]============================== 1 passed in 1.50s ===============================
INFO Verifier completed successfully.
Success! Our default test works and confirms that root is a user. molecule verify
also allows us to iterate over our tests on the existing container. No need to rerun create
or converge
.
Now let’s write some tests to confirm our role is doing what it should. We’ll start by testing that the proper packages and versions are installed. Add the following to ./molecule/default/tests/test_default.py
:
For those new to testing, this may look like a lot. Fortunately, it is pretty straightforward. We are using pytest to parameterize a map of name and version. Then passing that into test_packages
and checking for the package name and its version. Using the map allows us to write one test for all packages. Go ahead and save that file and run molecule verify
. You should see that four tests have run successfully.
======================= test session starts ========================
platform linux -- Python 3.8.10, pytest-7.1.2, pluggy-1.0.0
rootdir: /home/pcritchfield
plugins: testinfra-6.8.0
collected 4 itemsmolecule/default/tests/test_default.py .... [100%]======================== 4 passed in 2.77s =========================
INFO Verifier completed successfully.
Next, we can test for the user webapp
and the file app.conf
. Add the following to test_default.py
:
Run molecule verify
one more time, and you will now see six passing tests:
======================= test session starts ========================
platform linux -- Python 3.8.10, pytest-7.1.2, pluggy-1.0.0
rootdir: /home/pcritchfield
plugins: testinfra-6.8.0
collected 6 itemsmolecule/default/tests/test_default.py ...... [100%]======================== 6 passed in 4.09s =========================
Finally, to clean up, we can run molecule destroy
. This removes the containers that we deployed and provisioned with create
or converge
. Putting us into a great place to start again. Providing confidence that we will get consistent deployments with every use of our demo_role
.
One command to rule them all
One last Molecule command to look at is molecule test
. The test
command will run the entire scenario; creating, converging, verifying, and more. Let’s look at what test
does as the default scenario. Run the command molecule matrix test
to list all of the steps that test
will go through:
INFO Test matrix
---
default:
- dependency
- lint
- cleanup
- destroy
- syntax
- create
- prepare
- converge
- idempotence
- side_effect
- verify
- cleanup
- destroy
Notice that there is much more happening than just running the tests, including linting, provisioning the environment, and testing for idempotence. Molecule will run the tests in the verify step and clean up the test environment. Give it a try and run molecule test
.
You can find the code demoed in this post over on GitHub
Next Time
In the next post, I’ll dive into the molecule.yml
config more deeply. We’ll look at testing multiple operating systems, linting, and idempotence.