Testing Ansible Roles with Molecule and Docker

I was working on writing some generic ansible playbook’s for modifying the infrastructure and we developed a lot for creating and modifying infrastructure for us. Initially the journey was smooth but as we proceeded and created more and more roles. we came across issues as when we are not touching a role for a longer time and when we dare to dust it off to make some changes, we usually get’s issues as the role is not working or the changes we did might break for any other roles. This is not a new problem that we face when we maintain the infrastructure as code and its part of Software Engineering. Now the big question is how can we solve this or get rid of these situations?

To overcome this problem/situation we wanted to implement some way so that we can test our ansible roles and can easily push to production infrastructure. To solve this we wanted to implement Continuous Integration(CI) for our playbooks and also wanted to introduce the unit and integration tests for our ansible playbooks. To solve this problem we came through Molecule. Molecule helps in testing and development of Ansible roles. Molecule gives provision for testing with multiple instances, operating systems and distributions, virtualization providers, test frameworks and testing scenarios. That’s all we wanted and we started utilizing this for our Infrastructure.

Today we will do some hand’s on with Molecule and see how we can test our roles against different distributions. This will also give idea about how you can test the same against different OS’s and distributions. To start of with molecule we need to install python-pip, as its based on Python.

# I will be taking this example on CentOS
# IT requires Ansible >= 2.2 and Python 2.7
sudo yum install -y epel-release
sudo yum install -y gcc python-pip python-devel openssl-devel

Once you are done with installing python pip, lets just install molecule.

sudo python-pip install molecule

To start of implementing and testing we will create one role named molecule-demo and after this we will initialize molecule role. To initialize, we just have to run the following command.

# This will initialize molecule-demo role and directory structure

molecule init role --driver-name docker --role-name molecule-demo --verifier-name testinfra

This command will create a molecule project for us and we can see this by running “” command. This is just a simple template for creating a ansible role with default molecule files.

root@Ansible:/home/ubuntu/molecule-demo# tree
.
├── defaults
│ └── main.yml
├── handlers
│ └── main.yml
├── meta
│ └── main.yml
├── molecule
│ └── default
│ ├── create.yml
│ ├── destroy.yml
│ ├── Dockerfile.j2
│ ├── INSTALL.rst
│ ├── molecule.yml
│ ├── playbook.yml
│ └── tests
│ ├── test_default.py
│ └── test_default.pyc
├── README.md
├── tasks
│ └── main.yml
└── vars
└── main.yml
8 directories, 14 files

Once you are done with this, you can run a simple test which was default in molecule. To test if the setup is working or not , you can run “” command, this will run default tests.

# To run simple(default) test by molecule run following command$ molecule test
--> Test matrix
└── default
├── destroy
├── dependency
├── syntax
├── create
├── converge
├── idempotence
├── lint
├── side_effect
├── verify
└── destroy
--> Scenario: 'default'
--> Action: 'destroy'
PLAY [Destroy] *****************************************************************TASK [Destroy molecule instance(s)] ********************************************
ok: [localhost] => (item=(censored due to no_log))
PLAY RECAP *********************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0
--> Scenario: 'default'
--> Action: 'dependency'
Skipping, missing the requirements file.
--> Scenario: 'default'
--> Action: 'syntax'
playbook: /home/ubuntu/molecule-demo/molecule/default/playbook.yml--> Scenario: 'default'
--> Action: 'create'
PLAY [Create] ******************************************************************TASK [Create Dockerfiles from image names] *************************************
changed: [localhost] => (item=(censored due to no_log))
TASK [Discover local Docker images] ********************************************
ok: [localhost] => (item=(censored due to no_log))
TASK [Build an Ansible compatible image] ***************************************
changed: [localhost] => (item=(censored due to no_log))
TASK [Create molecule instance(s)] *********************************************
changed: [localhost] => (item=(censored due to no_log))
PLAY RECAP *********************************************************************
localhost : ok=4 changed=3 unreachable=0 failed=0
--> Scenario: 'default'
--> Action: 'converge'
PLAY [Converge] ****************************************************************TASK [Gathering Facts] *********************************************************
ok: [instance]
PLAY RECAP *********************************************************************
instance : ok=1 changed=0 unreachable=0 failed=0
--> Scenario: 'default'
--> Action: 'idempotence'
Idempotence completed successfully.
--> Scenario: 'default'
--> Action: 'lint'
--> Executing Yamllint on files found in /home/ubuntu/molecule-demo/...
Lint completed successfully.
--> Executing Flake8 on files found in /home/ubuntu/molecule-demo/molecule/default/tests/...
Lint completed successfully.
--> Executing Ansible Lint on /home/ubuntu/molecule-demo/molecule/default/playbook.yml...
Lint completed successfully.
--> Scenario: 'default'
--> Action: 'side_effect'
Skipping, side effect playbook not configured.
--> Scenario: 'default'
--> Action: 'verify'
--> Executing Testinfra tests found in /home/ubuntu/molecule-demo/molecule/default/tests/...
============================= test session starts ==============================
platform linux2 -- Python 2.7.12, pytest-3.2.2, py-1.4.34, pluggy-0.4.0
rootdir: /home/ubuntu/molecule-demo/molecule/default, inifile:
plugins: testinfra-1.6.3
collected 1 item
tests/test_default.py .=============================== warnings summary ===============================
None
Module already imported so can not be re-written: testinfra
-- Docs: http://doc.pytest.org/en/latest/warnings.html
===================== 1 passed, 1 warnings in 4.39 seconds =====================
Verifier completed successfully.
--> Scenario: 'default'
--> Action: 'destroy'
PLAY [Destroy] *****************************************************************TASK [Destroy molecule instance(s)] ********************************************
changed: [localhost] => (item=(censored due to no_log))
PLAY RECAP *********************************************************************
localhost : ok=1 changed=1 unreachable=0 failed=0

You will see this output, where you can see that we have executed default test present at . Before we go ahead and see what test that we actually ran, i would like to give you an idea what actually this does in background.

Basically it will check the file to get the details about the platform on which we want to test it. For us we have configured to run it on docker containers( as per the initialize command). we wanted to test this playbook on CentOS 7. This will download the official CentOS 7 image and consider it as base image and then try to run your playbook inside the container. Once the playbook execution is done, it will execute the test cases defined and give you the result.

Let’s just create a role and then we will write a simple test case to validate it. To start will open the file in any of your favorite editor and paste the following code.

# put this code in task/main.yml
---
- name: "Installing httpd service"
yum:
name: httpd
state: installed

The above code will simply installing the httpd service in CentOS box/machine. Now to test if this installs the httpd service, we will write a simple test to verify it. Now, open and paste the below code to it.

import osimport testinfra.utils.ansible_runnertestinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
os.environ['MOLECULE_INVENTORY_FILE']).get_hosts('all')
def test_package(Package):
service = Package("httpd")
assert service.is_installed

So now we have written our Ansible playbook to install httpd service and also wrote the test case to check if the service is installed or not. Now lets just run it and check if our test passes or not. To run the test simply run the and you will see the following output showing test passed.

root@Ansible:/home/ubuntu/molecule-demo# molecule test
--> Test matrix
└── default
├── destroy
├── dependency
├── syntax
├── create
├── converge
├── idempotence
├── lint
├── side_effect
├── verify
└── destroy
--> Scenario: 'default'
--> Action: 'destroy'
PLAY [Destroy] *****************************************************************TASK [Destroy molecule instance(s)] ********************************************
ok: [localhost] => (item=(censored due to no_log))
PLAY RECAP *********************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0
--> Scenario: 'default'
--> Action: 'dependency'
Skipping, missing the requirements file.
--> Scenario: 'default'
--> Action: 'syntax'
playbook: /home/ubuntu/molecule-demo/molecule/default/playbook.yml--> Scenario: 'default'
--> Action: 'create'
PLAY [Create] ******************************************************************TASK [Create Dockerfiles from image names] *************************************
ok: [localhost] => (item=(censored due to no_log))
TASK [Discover local Docker images] ********************************************
ok: [localhost] => (item=(censored due to no_log))
TASK [Build an Ansible compatible image] ***************************************
changed: [localhost] => (item=(censored due to no_log))
TASK [Create molecule instance(s)] *********************************************
changed: [localhost] => (item=(censored due to no_log))
PLAY RECAP *********************************************************************
localhost : ok=4 changed=2 unreachable=0 failed=0
--> Scenario: 'default'
--> Action: 'converge'
PLAY [Converge] ****************************************************************TASK [Gathering Facts] *********************************************************
ok: [instance]
TASK [molecule-demo : Installing httpd service] ********************************
changed: [instance]
PLAY RECAP *********************************************************************
instance : ok=2 changed=1 unreachable=0 failed=0
--> Scenario: 'default'
--> Action: 'idempotence'
Idempotence completed successfully.
--> Scenario: 'default'
--> Action: 'lint'
--> Executing Yamllint on files found in /home/ubuntu/molecule-demo/...
Lint completed successfully.
--> Executing Flake8 on files found in /home/ubuntu/molecule-demo/molecule/default/tests/...
Lint completed successfully.
--> Executing Ansible Lint on /home/ubuntu/molecule-demo/molecule/default/playbook.yml...
Lint completed successfully.
--> Scenario: 'default'
--> Action: 'side_effect'
Skipping, side effect playbook not configured.
--> Scenario: 'default'
--> Action: 'verify'
--> Executing Testinfra tests found in /home/ubuntu/molecule-demo/molecule/default/tests/...
============================= test session starts ==============================
platform linux2 -- Python 2.7.12, pytest-3.2.2, py-1.4.34, pluggy-0.4.0
rootdir: /home/ubuntu/molecule-demo/molecule/default, inifile:
plugins: testinfra-1.6.3
collected 1 item
tests/test_default.py .=============================== warnings summary ===============================
None
Module already imported so can not be re-written: testinfra
Package fixture is deprecated. Use host fixture and get Package module with host.package
TestinfraBackend fixture is deprecated. Use host fixture and get backend with host.backend
-- Docs: http://doc.pytest.org/en/latest/warnings.html
===================== 1 passed, 3 warnings in 4.42 seconds =====================
Verifier completed successfully.
--> Scenario: 'default'
--> Action: 'destroy'
PLAY [Destroy] *****************************************************************TASK [Destroy molecule instance(s)] ********************************************
changed: [localhost] => (item=(censored due to no_log))
PLAY RECAP *********************************************************************
localhost : ok=1 changed=1 unreachable=0 failed=0

So, from the above command output, its clearly shown that the test that we wrote passed which means the httpd package is installed successfully. This way we can test our roles before executing them on Production Infrastructure. As we know the infrastructure consists of different platforms and different OS. We should also be able to test if our playbook supports the different OS versions or not. How can we do this with molecule. Let’ see.

Open the file file. you will see the configuration for the platforms on which you want to test. Current configuration suggests that we are testing this on CentOS 7, what if i want to check if my playbook works on CentOS 6.9 or not . Lets enter the platform details in file and will check with .

---
dependency:
name: galaxy
driver:
name: docker
lint:
name: yamllint
platforms:
- name: instance
image: centos:7
- name: instance2
image: centos:6.9
provisioner:
name: ansible
lint:
name: ansible-lint
scenario:
name: default
verifier:
name: testinfra
lint:
name: flake8

So, this way you can test if your role works or not and also for which all platforms this can support. This way we have overcome the issue of testing and keeping all our playbooks working and adding CI(Continuous Integration)to the workflow. Currently molecule is not supporting windows but still you can use it(not with docker) with vagrant to test your roles for windows.

DevOps Consultant, ALM Consultant, CloudOps Consultant