Introduction to Molecule# Molecule is a testing framework for Ansible Roles maintained by the ansible-community. With Molecule, you can systematically test Ansible Roles, enabling comprehensive testing using multiple instances, operating systems, virtualization providers, test frameworks, and test scenarios.
Why Do You Need Molecule?# When developing Ansible Roles, you face the following challenges:
Limitations of Manual Testing : Manually running and verifying Roles each time is time-consuming.Multi-Environment Support : You need to ensure Roles work correctly on various operating systems like Ubuntu, CentOS, and Debian.Continuous Integration : Tests need to run automatically in CI/CD pipelines.Code Quality : Ansible code quality must be maintained consistently.Molecule provides the following features to address these challenges:
Support for various drivers (Docker, Podman, Vagrant, EC2, etc.) Automated test lifecycle management Integration with multiple test frameworks (Ansible, Testinfra, Goss) Lint tool integration (yamllint, ansible-lint) Installation# System Requirements# Python 3.8 or higher Docker, Podman, or Vagrant (for test environments) Ansible 2.10 or higher Installation Methods# When installing with Pip, it’s recommended to create a virtual environment with Virtualenv or use dependency management tools like Pipenv or Poetry to avoid corrupting system Python dependencies.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Create virtual environment (recommended)
$ python -m venv .venv
$ source .venv/bin/activate
# Install with docker, yamllint, and ansible-lint packages
# podman, vagrant, azure, hetzner are also supported
$ pip install molecule[ docker,lint]
# Verify installation
$ molecule --version
molecule 24.9.0 using python 3.11
ansible:2.17.0
delegated:24.9.0 from molecule
docker:24.9.0 from molecule_docker
Installing Additional Drivers# 1
2
3
4
5
6
7
8
# Podman driver
$ pip install molecule[ podman]
# Vagrant driver
$ pip install molecule[ vagrant]
# EC2 driver
$ pip install molecule[ ec2]
Core Concepts of Molecule# Scenario# A scenario defines the test lifecycle. The default scenario is named default, and you can create multiple scenarios as needed. For example:
default: Basic Docker-based testingcentos: CentOS-specific testingubuntu: Ubuntu-specific testingDriver# A driver defines how test instances are created:
Driver Use Case Advantages Disadvantages Docker Container-based testing Fast and lightweight Limited systemd support Podman Container-based testing Rootless execution Docker compatibility considerations Vagrant VM-based testing Complete OS environment Relatively slow EC2 Cloud VM testing Real production environment Cost incurred
Provisioner# Defines how Roles are applied. Ansible is used by default.
Verifier# Defines how test results are validated:
Ansible : Validate with Ansible playbooksTestinfra : Python-based testing frameworkGoss : YAML-based lightweight testing toolWriting Tests# 1. Creating a New Role# If you don’t have an existing Role, you can initialize a new one with Molecule:
1
$ molecule init role myrole
This command creates the following structure:
m ├ │ ├ │ ├ │ ├ │ │ │ │ │ │ ├ │ ├ │ │ └ y ─ ─ ─ ─ ─ ─ ─ r ─ ─ ─ ─ ─ ─ ─ o l d └ h └ m └ m └ t └ t ├ └ v └ e e ─ a ─ e ─ o ─ a ─ e ─ ─ a ─ / f ─ n ─ t ─ l ─ s ─ s ─ ─ r ─ a d a e k t s u m l m / m c d ├ ├ ├ └ s m s i t / m l a e a a u e ─ ─ ─ ─ / a / n e a t i r i i l f ─ ─ ─ ─ i v s i s n s n n e a n e t n / . / . . / u c m v t └ . n . . y y y l o o e a ─ y t y y m m m t n l r s ─ m o m m l l l / v e i k l r l l e c f s m y r u y a g l . i e e y n . . m . y y l y m m m l l l 2. Adding Molecule to an Existing Role# To add Molecule to an existing Role:
1
2
3
4
$ cd /path/to/role
$ molecule init scenario
--> Initializing new scenario default...
Initialized scenario in /path/to/role/molecule/default successfully.
3. Configuring molecule.yml# Create the /path/to/role/molecule/default/molecule.yml file with the following content:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
---
dependency :
name : galaxy
options :
requirements-file : ../../requirements.yml
driver :
name : docker
platforms :
- name : instance-ubuntu
image : docker.io/geerlingguy/docker-ubuntu2204-ansible:latest
pre_build_image : true
privileged : true
volumes :
- /sys/fs/cgroup:/sys/fs/cgroup:rw
cgroupns_mode : host
- name : instance-centos
image : docker.io/geerlingguy/docker-rockylinux9-ansible:latest
pre_build_image : true
privileged : true
volumes :
- /sys/fs/cgroup:/sys/fs/cgroup:rw
cgroupns_mode : host
provisioner :
name : ansible
config_options :
defaults :
host_key_checking : false
verifier :
name : ansible
lint : |
set -e
yamllint -c ../../.yamllint .
ansible-lint -c ../../.ansible-lint
Key Configuration Explanations:
dependency.galaxy: Manages Ansible Galaxy dependencies.driver.name: Uses Docker to create test instances.platforms: List of platforms to test. Multiple OS can be tested simultaneously.privileged: true: Required for features like systemd.provisioner.config_options: Customizes Ansible settings.4. Writing converge.yml# Create /path/to/role/molecule/default/converge.yml and add environment configuration code. After creating Docker containers, execute the Role to configure the environment:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
---
- name : Converge
hosts : all
become : true
vars :
myrole_custom_var : "test_value"
pre_tasks :
- name : Update apt cache (Ubuntu)
apt :
update_cache : true
cache_valid_time : 600
when : ansible_os_family == 'Debian'
changed_when : false
- name : Install dependencies
package :
name :
- curl
- wget
state : present
tasks :
- name : Include myrole
include_role :
name : myrole
vars :
myrole_param : "{{ myrole_custom_var }}"
Writing Tips:
Use pre_tasks to perform necessary setup before Role execution. Define test variables using vars. Enable privilege escalation with become: true. 5. Writing verify.yml# Create /path/to/role/molecule/default/verify.yml and add verification code. This is where actual testing is performed:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
---
- name : Verify
hosts : all
become : true
gather_facts : true
vars :
expected_packages :
- nginx
- curl
tasks :
- name : Check if nginx is installed
package_facts :
manager : auto
- name : Assert nginx is installed
assert :
that :
- "'nginx' in ansible_facts.packages"
fail_msg : "nginx is not installed"
success_msg : "nginx is installed successfully"
- name : Check nginx service status
service_facts :
register : services_state
- name : Assert nginx service is running
assert :
that :
- "'nginx.service' in services_state.ansible_facts.services"
- "services_state.ansible_facts.services['nginx.service'].state == 'running'"
fail_msg : "nginx service is not running"
success_msg : "nginx service is running"
- name : Check if port 80 is listening
wait_for :
port : 80
timeout : 30
register : port_check
- name : Assert port 80 is available
assert :
that :
- port_check is success
fail_msg : "Port 80 is not listening"
success_msg : "Port 80 is listening"
- name : Test HTTP response
uri :
url : "http://localhost"
return_content : true
register : http_response
failed_when : "'Welcome' not in http_response.content"
- name : Verify configuration files exist
stat :
path : "/etc/nginx/nginx.conf"
register : config_file
- name : Assert configuration file exists
assert :
that :
- config_file.stat.exists
fail_msg : "nginx.conf does not exist"
success_msg : "nginx.conf exists"
6. Advanced Verification with Testinfra# Using Testinfra instead of Ansible allows you to write more powerful tests:
1
2
3
# molecule.yml
verifier :
name : testinfra
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# tests/test_default.py
import pytest
def test_nginx_is_installed (host):
"""Check if nginx is installed"""
nginx = host. package("nginx" )
assert nginx. is_installed
assert nginx. version. startswith("1." )
def test_nginx_running_and_enabled (host):
"""Check if nginx service is running and enabled"""
nginx = host. service("nginx" )
assert nginx. is_running
assert nginx. is_enabled
def test_nginx_listening_on_port_80 (host):
"""Check if listening on port 80"""
socket = host. socket("tcp://0.0.0.0:80" )
assert socket. is_listening
def test_nginx_config_exists (host):
"""Check if nginx config file exists"""
config = host. file("/etc/nginx/nginx.conf" )
assert config. exists
assert config. is_file
assert config. user == "root"
assert config. group == "root"
assert oct (config. mode) == "0o644"
def test_nginx_response (host):
"""Check HTTP response"""
response = host. run("curl -s http://localhost" )
assert response. rc == 0
assert "Welcome" in response. stdout
Molecule Commands# Basic Commands# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ cd /path/to/role
# Create test instances
$ molecule create
# Run converge.yml (apply Role)
$ molecule converge
# Check changes to instances
$ molecule side-effect
# Run verify.yml (test verification)
$ molecule verify
# Remove test instances
$ molecule destroy
# Run all the above steps at once
$ molecule test
Useful Commands# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Test with specific scenario
$ molecule test -s centos
# Debug in interactive mode
$ molecule create
$ molecule login
$ molecule destroy
# Run lint checks only
$ molecule lint
# Install dependencies only
$ molecule dependency
# View instance list
$ molecule list
# Execute command on instance
$ molecule exec -- ls -la /etc/nginx
Test Lifecycle# Molecule’s test command executes the following stages sequentially:
lint : Code quality checkdestroy : Clean up existing instancesdependency : Install dependenciessyntax : Check playbook syntaxcreate : Create test instancesprepare : Prepare instances (prepare.yml)converge : Apply Role (converge.yml)idempotence : Idempotence testside-effect : Side effect testverify : Verify results (verify.yml)cleanup : Clean up (cleanup.yml)destroy : Remove instancesCI/CD Integration# GitHub Actions# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# .github/workflows/molecule.yml
name : Molecule Test
on :
push :
branches : [main, develop]
pull_request :
branches : [main]
jobs :
test :
runs-on : ubuntu-latest
strategy :
matrix :
distro :
- docker.io/geerlingguy/docker-ubuntu2204-ansible:latest
- docker.io/geerlingguy/docker-rockylinux9-ansible:latest
steps :
- name : Checkout code
uses : actions/checkout@v4
- name : Set up Python
uses : actions/setup-python@v5
with :
python-version : '3.11'
- name : Install dependencies
run : |
pip install molecule[docker,lint]
pip install ansible-core
- name : Run Molecule tests
run : molecule test
env :
MOLECULE_DISTRO : ${{ matrix.distro }}
GitLab CI# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
# .gitlab-ci.yml
molecule :
image : docker:latest
services :
- docker:dind
variables :
DOCKER_TLS_CERTDIR : ""
before_script :
- apk add --no-cache python3 py3-pip
- pip3 install molecule[docker,lint] ansible-core
script :
- molecule test
tags :
- docker
Best Practices# 1. Idempotence Testing# Roles should guarantee the same result when executed multiple times:
1
2
3
# Run converge twice to ensure no changes on second run
$ molecule converge
$ molecule converge
2. Test Multiple Operating Systems# 1
2
3
4
5
6
7
platforms :
- name : ubuntu-22.04
image : docker.io/geerlingguy/docker-ubuntu2204-ansible:latest
- name : rocky-9
image : docker.io/geerlingguy/docker-rockylinux9-ansible:latest
- name : debian-12
image : docker.io/geerlingguy/docker-debian12-ansible:latest
3. Clear Verification# 1
2
3
4
5
6
- name : Verify with clear messages
assert :
that :
- result.rc == 0
fail_msg : "Command failed: {{ result.stderr }}"
success_msg : "Command succeeded: {{ result.stdout }}"
4. Test Isolation# Each test should be independently executable:
1
2
3
4
- name : Clean up before test
file :
path : /tmp/test_data
state : absent
5. Enable Debugging# 1
2
3
4
5
# Run with verbose logging
$ MOLECULE_DEBUG = true molecule test
# Debug while keeping instances
$ molecule test --destroy= never
Troubleshooting# Docker Permission Issues# 1
2
$ sudo usermod -aG docker $USER
$ newgrp docker
systemd Support Issues# To use systemd in Docker containers:
1
2
3
4
5
6
7
8
9
10
platforms :
- name : instance
image : docker.io/geerlingguy/docker-ubuntu2204-ansible:latest
privileged : true
volumes :
- /sys/fs/cgroup:/sys/fs/cgroup:rw
command : /sbin/init
tmpfs :
- /run
- /tmp
Memory Issues# 1
2
3
4
5
6
7
8
platforms :
- name : instance
docker_networks :
- name : molecule
networks :
- name : molecule
ulimits :
- nofile:262144:262144
Conclusion# Molecule is an essential tool for ensuring the quality of Ansible Roles. Through systematic testing, it provides:
Reliability : Verify that Roles work as intendedPortability : Ensure compatibility across various OS and environmentsMaintainability : Continuous quality management through CI/CD integrationProductivity : Reduce manual testing timeAdopt Molecule in your projects to take Ansible Role quality to the next level.
References#