Get Rewarded! We will reward you with up to €50 credit on your account for every tutorial that you write and we publish!

Unit testing Ansible roles with Molecule

profile picture
Author
Harshavardhan Musanalli
Published
2024-05-06
Time to read
12 minutes reading time

Introduction

Ansible Molecule is a powerful testing framework that simplifies the process of testing Ansible roles in different scenarios. In this tutorial, we will guide you through the process of setting up and using Ansible Molecule to test your Ansible roles effectively.

Why use Molecule to test Ansible?

Reasons why you should consider using Molecule:

  • Automated Testing: Molecule automates the testing process for Ansible roles, allowing you to define and execute test scenarios easily. This automation helps catch issues early in the development cycle, ensuring that roles behave as expected.
  • Isolation of Environments: Molecule uses different drivers (e.g., Docker, Vagrant) to create isolated environments for testing. This ensures that tests are consistent and reproducible across different systems, reducing the likelihood of environment-related issues.
  • Multiple Testing Scenarios: Molecule supports the definition of multiple scenarios, allowing you to test roles under various conditions. For example, you can test different operating systems, Ansible versions, or configurations to ensure role compatibility.
  • Continuous Integration (CI): Molecule is designed to work seamlessly with CI systems like Travis CI, Jenkins, or GitLab CI. This enables you to automate the testing process every time changes are pushed to your version control system, ensuring continuous quality assurance.
  • Role Development Support: Molecule includes tools for role development, such as role initialization, linting, and dependency management. This streamlines the development workflow and helps maintain a consistent codebase.
  • Parallel Execution: Molecule supports parallel execution of tests, allowing you to save time when testing multiple scenarios simultaneously. This is crucial for speeding up the feedback loop during development.
  • Documentation and Best Practices: Molecule encourages best practices in role development and testing. The tool's documentation provides clear guidance on structuring roles, writing effective test cases, and following recommended conventions.
  • Community Adoption: Molecule is widely adopted within the Ansible community. Many Ansible roles and projects use Molecule for testing, making it a well-supported and reliable choice for role developers.

Prerequisites

Before we dive into the demo, make sure you have the following prerequisites in place. For CI workloads, docker is highly recommended without the need of virtualenv.


Example installation on Ubuntu 20.04 / 22.04
    sudo apt update && sudo apt install -y python3 python3-pip libssl-dev
    python3 -m pip install molecule ansible-core
    export PATH="$PATH:$HOME/.local/bin"
    pip3 install python-dateutil
    ansible-galaxy collection install hetzner.hcloud

  • python: Ansible is built on top of Python. Hence, it is the first prerequisite. You can install python either with OS package manager or by compiling from sources. Python3 is highly recommended.
  • pip: Pip is the package manager of Python. We install Molecule with pip as it is not yet available as an OS rpm.
  • Ansible: We need Ansible to be installed. Just like python this could be installed either with OS package manager or from pip.
    pip3 install ansible
  • Molecule: Install Molecule with pip:
    pip3 install --no-cache-dir  molecule[ansible]
  • hetzner.hcloud collections (Optional) : This depends on your use case. For the sake of this demo, we will be testing the Ansible role on hetzner infrastructure by provisioning a VM, connecting over SSH & executing the role on the target VM. We install the collections by running the ansible-galaxy collection install hetzner.hcloud command.

Step 1 - Add an example Ansible rule

A usual Ansible role directory structure would be something like below:

holu@<your_host>:~/ansible_example_role$ ls -l
total 56
drwxr-xr-x   3 holu  staff    96 21 Nov 12:24 defaults
drwxr-xr-x   3 holu  staff    96 21 Nov 12:24 files
drwxr-xr-x   3 holu  staff    96 21 Nov 12:24 meta
drwxr-xr-x   3 holu  staff    96 21 Nov 12:24 tasks
drwxr-xr-x   3 holu  staff    96 21 Nov 12:24 vars

In this demo we will apply an OS hardening Ansible role on our VM infrastructure. Let's assume our VM infrastructure has a mix of different OS distributions, major/minor versions. A tiny change made to the hardening role needs to be tested thoroughly on each OS flavor as part of Continuous Integration (CI).

This tutorial will use a very simple example that looks like this:

holu@<your_host>:~/os_hardening_role$ ls -l
drwxr-xr-x   3 holu  staff    96 21 Nov 12:24 tasks
holu@<your_host>:~/os_hardening_role/tasks$ ls -l
-rw-rw-r--   1 holu  staff   208 21 Nov 12:30 main.yml

Content of os_hardening_role/tasks/main.yml:

This is a very basic example that you can use to follow the tutorial.

---
- name: OS Hardening
  hosts: localhost
  tasks:
    - name: OS Hardening task
      debug:
        msg: "Performing OS hardening tasks for {{ ansible_distribution }} {{ ansible_distribution_version }}"

Step 2 - Adding Molecule testing to an existing role

Inside the role directory (in this example ~/os_hardening_role), run the molecule init scenario command. This will create the new subdirectory molecule. The subdirectory molecule includes a default directory with example files. You will find them in the following structure:

os_hardening_role/
├── tasks/
│       └── main.yml
└── molecule/
    └── <scenario_name>/
        ├── converge.yml
        ├── create.yml
        ├── destroy.yml
        └── molecule.yml

Scenarios serve as the foundation for a variety of powerful functionalities within Molecule. Consider a scenario as a dedicated test suite for roles or playbooks within a collection. You can create multiple scenarios, and Molecule will execute them sequentially.

Step 3 - The Scenario Layout

File Purpose
molecule.yml This is the key configuration entry point for Molecule per scenario. With this file, you can configure each tool that Molecule will employ when testing your role.
create.yml This playbook file is used for creating the test environment before running your ansible role. This can involve creating instances, Docker containers, or other infrastructure required for testing your Ansible role.
converge.yml This playbook file contains the call for your role. Molecule will invoke this playbook in the test environment you set up with create.yml using ansible-playbook <your_playbook_file>.
verify.yml This playbook file validates your role in the test environment. It is more or less equivalent to assert in unit tests.
destroy.yml This playbook file is used for destroying and removing the test environment after the role was tested, e.g. delete an instance, a Docker container, or other infrastructure created during the testing process.

Example files:

The example files below create a new Hetzner cloud server for the test environment, test an example Ansible role called os_hardening_role, and delete the Hetzner cloud server once the test finished.

  • molecule.yml

    Provide information about required settings and configurations for the test environment.

    ---
    dependency:
      name: galaxy
    driver:
      name: default
    platforms:
      - name: molecule-test-vm-ubuntu22
        image: "ubuntu-22.04"
      - name: molecule-test-vm-ubuntu24
        image: "ubuntu-24.04"
    provisioner:
      name: ansible
      connection_options:
        ansible_ssh_common_args: " -o ControlMaster=auto -o ControlPersist=60s -o PreferredAuthentications=publickey,password -F /tmp/ssh_config -o StrictHostKeychecking=no"
    verifier:
      name: ansible
    scenario:
      name: default
      ## below test sequence starts with `destroy` to ensure we have clean infrastructure state.
      test_sequence:
        - destroy
        - create
        - converge
        - verify
        - destroy
    # you can disable the lint if you want but it is highly recommended to have it enabled as YAML linting is very essential to improve code quality checks
    lint: |
     ansible-lint --exclude molecule/default/
    dependency By default, Molecule relies on the Galaxy development guide to handle role dependencies.
    driver By default, Molecule employs the Delegated driver to offload instance creation tasks.
    platforms Molecule uses this information to determine instance creation, naming, and grouping. If you want to test your role against various Ubuntu flavors (e.g., 18.04, 20.04, 22.04, 24.04 or even with other distros like CentOS etc.,), you can define them in this section.
    provisioner Molecule exclusively offers an Ansible provisioner, which governs the instance life cycle according to this configuration.
    verifier By default, Molecule utilizes Ansible for creating targeted state-checking tests (e.g., deployment smoke tests) on the target instance.
    scenario This configuration determines the order in which Molecule executes scenarios.
  • create.yml

    This will create a new Hetzner cloud server.

    It will also generate ssh_config (more information follows in "Step 4"). Ansible needs this information to connect to the newly created Hetzner cloud server via SSH.

    In the code below:

    • Replace <your_ssh_key> with the name of the SSH key you saved in your Cloud Console.
    • Replace <your_hetzner_api_token> with your actual API token.
    ---
    - name: Create the test servers
      hosts: localhost
      connection: local
      tasks:
        - name: Create Hetzner cloud server with Ubuntu 24.04 and 22.04
          hetzner.hcloud.server:
            name: "{{ item.name }}"
            server_type: cx11
            image: "{{ item.image }}"
            location: fsn1
            ssh_keys:
              - <your_ssh_key>
            api_token: <your_api_token>
            state: present
          register: vm_output
          with_items:
            - { name: molecule-test-vm-ubuntu24, image: ubuntu-24.04 }
            - { name: molecule-test-vm-ubuntu22, image: ubuntu-22.04 }
    
        - name: Generate ssh_config
          template:
            src: ssh_config.j2
            dest: /tmp/ssh_config
          vars:
            hosts: "{{ vm_output.results }}"
    
        - name: Pause for 1 minute to wait for the SSH process to come up
          pause:
            seconds: 60
  • converge.yml

    This will apply the example Ansible role os_hardening_role to the test environment. With the example file create.yml above, the test environment is are one Hetzner cloud server with Ubuntu 24.04 and one Hetzner cloud server with Ubuntu 22.04.

    If you used a different path, replace ../../../os_hardening_role/tasks respectively.

    ---
    - name: Converge
      hosts: all
      become: yes
      gather_facts: true
      tasks:
        - name: "Include os_hardening_role"
          include_role:
            name: "../../../os_hardening_role/tasks"
  • verify.yml

    You could add more checks here. For the sake of this demo, we ensure root login is disabled as part of hardening.

    ---
    - name: Verify OS Hardening Control
      hosts: all
      become: true
      tasks:
        - name: Ensure root login is disabled
          command: grep '^\s*#*PermitRootLogin' /etc/ssh/sshd_config
          register: root_login_config
          failed_when: "'yes' in root_login_config.stdout"
    
        - name: Print root login status
          debug:
            msg: "{{ root_login_config.stdout_lines[0] }}"
  • destroy.yml

    This will remove the server that was created previously. This is more or less the same as create.yml except that the "state" has to be marked "absent".

    In the code below, replace <your_api_token> with your own API token.

    ---
    - name: Destroy the test servers
      hosts: all
      connection: local
      tasks:
        - name: Delete Hetzner cloud server with Ubuntu {{ item.image }}
          hetzner.hcloud.server:
            name: "{{ item.name }}"
            api_token: <your_api_token>
            state: absent
          with_items:
            - { name: molecule-test-vm-ubuntu24, image: ubuntu-24.04 }
            - { name: molecule-test-vm-ubuntu22, image: ubuntu-22.04 }

Step 4 - Build an SSH configuration

Ansible primarily relies on SSH to connect to a target machine to execute tasks. As we are provisioning crash & burn VMs for our testing, we create an SSH configuration for each VM to build the inventory.

  • In create.yml, you will replace the variables in ssh_config.j2 and save this information in /tmp/ssh_config.

  • In molecule.yml, you will find that the connection options refer to this /tmp/ssh_config file. Ansible uses this information to connect to the new server.


A simple jinja2 template would look like this:

Path: <role_name>/molecule/<your-scenario>/ssh_config.j2

Replace ~/.ssh/id_ed25519 with your own private SSH key.

holu@<your_host>:~/os_hardening_role$ cat molecule/default/ssh_config.j2 
{% for host in hosts -%}
Host {{ host.hcloud_server.name }}
    HostName {{ host.hcloud_server.ipv4_address }}
    User root
    IdentityFile ~/.ssh/id_ed25519
{% endfor %}

For more information about the hcloud_server variable (such as name & ipv4_address), check out the Ansible documentation.

Step 5 - Execute Molecule

Molecule provides commands for manually managing the lifecycle of the instance, scenario, development and testing tools. However, we can also tell Molecule to manage this automatically within a scenario sequence.

Molecule full lifecycle sequence
└── default
    ├── dependency
    ├── cleanup
    ├── destroy
    ├── syntax
    ├── create
    ├── prepare
    ├── converge
    ├── idempotence
    ├── side_effect
    ├── verify
    ├── cleanup
    └── destroy

For the default scenario, you can invoke the full lifecycle sequence within the role directory (in this example ~/os_hardening_role) with the molecule test command.

If your scenario has an actual name, you can use molecule test --scenario-name <your_scenario>.

Additional lifecycle scenarios

The lifecycle sequence above includes idempotence. In some cases, this is quite important & you could plugin such a scenario in molecule.yml. This way we tell ansible to execute the ansible once again to ensure our role meets the idempotence criteria. For more scenario related details you could visit molecule-scenarios.

Conclusion

Congratulations! You've successfully set up and tested an Ansible role using Molecule. We covered the basics & testing scenarios on various Ubuntu flavours. Explore further and apply Molecule to your Ansible projects for robust and reliable roles.

Additional Resources:

License: MIT
Want to contribute?

Get Rewarded: Get up to €50 in credit! Be a part of the community and contribute. Do it for the money. Do it for the bragging rights. And do it to teach others!

Report Issue
Try Hetzner Cloud

Get 20€ free credit!

Valid until: 31 December 2024 Valid for: 3 months and only for new customers
Get started
Want to contribute?

Get Rewarded: Get up to €50 credit on your account for every tutorial you write and we publish!

Find out more