Ansible Molecule: Role Testing with Docker and Podman Drivers
Writing Ansible roles without tests is a recipe for broken playbooks. Molecule is the official testing framework for Ansible roles and collections. It lets you spin up containers, run your role against them, verify the results, and tear everything down — all with a single command.
This guide covers Molecule with Docker and Podman drivers, which are the most practical options for local development and CI.
What Molecule Does
Molecule provides a complete test lifecycle for Ansible roles:
- Create — spin up test instances (Docker containers, VMs, cloud instances)
- Converge — run your Ansible role against those instances
- Verify — run tests to confirm the role did what it should
- Destroy — clean up test instances
It also tests idempotency — running your role twice should produce no changes on the second run. Non-idempotent Ansible is a silent bug that bites you later.
Installation
pip install molecule molecule-docker
# If using Podman instead of Docker
pip install molecule molecule-podmanVerify:
molecule --version
# molecule 6.x.x using python 3.xInitializing Molecule in an Existing Role
If you have an existing role at roles/nginx/, initialize Molecule in it:
cd roles/nginx
molecule init scenario --driver-name dockerThis creates:
roles/nginx/
molecule/
default/
molecule.yml # scenario configuration
converge.yml # playbook that applies the role
verify.yml # verification tests
prepare.yml # optional pre-role setupConfiguring molecule.yml
The molecule.yml file defines your test environment:
# roles/nginx/molecule/default/molecule.yml
---
dependency:
name: galaxy
driver:
name: docker
platforms:
- name: ubuntu-22
image: "geerlingguy/docker-ubuntu2204-ansible:latest"
pre_build_image: true
command: ""
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:ro
privileged: true
- name: debian-12
image: "geerlingguy/docker-debian12-ansible:latest"
pre_build_image: true
command: ""
provisioner:
name: ansible
playbooks:
converge: converge.yml
verify: verify.yml
verifier:
name: ansible
lint: |
set -e
yamllint .
ansible-lintTesting Multiple Platforms
The platforms list lets you test against multiple OS versions simultaneously. The geerlingguy/docker-*-ansible images are pre-configured for Ansible testing with systemd support.
The converge.yml Playbook
This is the playbook Molecule runs to apply your role:
# molecule/default/converge.yml
---
- name: Converge
hosts: all
become: true
pre_tasks:
- name: Update apt cache
ansible.builtin.apt:
update_cache: true
when: ansible_os_family == "Debian"
roles:
- role: nginx
vars:
nginx_port: 80
nginx_worker_processes: 2Keep converge.yml simple — just apply the role with representative variables.
Writing Verification Tests
Option 1: Ansible Verifier (Default)
The Ansible verifier uses a standard playbook with assertions:
# molecule/default/verify.yml
---
- name: Verify
hosts: all
become: true
tasks:
- name: Check nginx is running
ansible.builtin.service_facts:
- name: Assert nginx service is active
ansible.builtin.assert:
that:
- "'nginx' in services"
- "services['nginx'].state == 'running'"
- "services['nginx'].status == 'enabled'"
fail_msg: "nginx service is not running or not enabled"
- name: Check nginx config file exists
ansible.builtin.stat:
path: /etc/nginx/nginx.conf
register: nginx_conf
- name: Assert nginx config exists
ansible.builtin.assert:
that:
- nginx_conf.stat.exists
- nginx_conf.stat.mode == "0644"
- name: Check nginx is listening on port 80
ansible.builtin.wait_for:
host: localhost
port: 80
timeout: 10
msg: "nginx is not listening on port 80"
- name: Verify nginx responds to HTTP requests
ansible.builtin.uri:
url: "http://localhost:80"
return_content: true
register: result
- name: Assert nginx returns 200
ansible.builtin.assert:
that:
- result.status == 200Option 2: Testinfra (Python-based)
Testinfra gives you a more expressive Python test syntax:
pip install molecule-plugins[testinfra]# molecule/default/tests/test_nginx.py
import pytest
def test_nginx_is_installed(host):
nginx = host.package("nginx")
assert nginx.is_installed
assert nginx.version.startswith("1.")
def test_nginx_running_and_enabled(host):
nginx = host.service("nginx")
assert nginx.is_running
assert nginx.is_enabled
def test_nginx_config_file(host):
config = host.file("/etc/nginx/nginx.conf")
assert config.exists
assert config.mode == 0o644
assert config.user == "root"
def test_nginx_listening(host):
nginx = host.socket("tcp://0.0.0.0:80")
assert nginx.is_listening
def test_nginx_http_response(host):
cmd = host.run("curl -s -o /dev/null -w '%{http_code}' http://localhost")
assert cmd.stdout == "200"Configure Testinfra in molecule.yml:
verifier:
name: testinfra
options:
verbose: trueRunning Molecule
# Full test cycle: create → converge → verify → destroy
molecule test
# Just apply the role (for development iteration)
molecule converge
# Just run verification
molecule verify
# Open a shell in the test container
molecule login --host ubuntu-22
# Run specific scenario
molecule test --scenario-name alternative
# Destroy test instances
molecule destroyUsing the Podman Driver
If you use Podman instead of Docker (common on RHEL/Fedora systems or rootless environments):
pip install molecule-podman# molecule/default/molecule.yml
driver:
name: podman
platforms:
- name: centos-stream9
image: "quay.io/centos/centos:stream9"
pre_build_image: true
command: /sbin/init
tmpfs:
- /run
- /tmp
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:ro
capabilities:
- SYS_ADMINPodman works rootlessly, which is better for security in CI environments. GitHub Actions runners support Podman natively on Ubuntu.
Testing Idempotency
Molecule runs your role twice and checks for changes on the second run. If your role is not idempotent, the second run will show changed tasks — which Molecule flags as a failure.
molecule test
# ...
# TASK [nginx : Install nginx] ****
# ok: [ubuntu-22] <-- idempotent (was "changed" first run)Common idempotency issues:
- Using
command:orshell:withoutcreates:orchanged_when: - File permissions that get reset by other tasks
notifyhandlers that always run
Fix with:
# Non-idempotent
- name: Generate SSL cert
ansible.builtin.command: openssl req -x509 -newkey rsa:4096 ...
# Idempotent
- name: Generate SSL cert
ansible.builtin.command: openssl req -x509 -newkey rsa:4096 ...
args:
creates: /etc/ssl/certs/app.crt # skip if file existsMultiple Scenarios
A role can have multiple scenarios for different configurations:
molecule/
default/ # standard setup
with_ssl/ # test with SSL enabled
minimal/ # test with minimum required variables
upgrade/ # test upgrade from previous version# Initialize a new scenario
molecule init scenario with_ssl --driver-name docker
# Test all scenarios
molecule test --all
# Test a specific scenario
molecule test --scenario-name with_sslCI/CD Integration
GitHub Actions
name: Molecule Tests
on: [pull_request]
jobs:
molecule:
runs-on: ubuntu-latest
strategy:
matrix:
scenario:
- default
- with_ssl
steps:
- 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 molecule-docker ansible ansible-lint
- name: Run Molecule tests
run: molecule test --scenario-name ${{ matrix.scenario }}
working-directory: roles/nginx
env:
PY_COLORS: '1'
ANSIBLE_FORCE_COLOR: '1'GitLab CI
molecule-test:
image: python:3.11
services:
- docker:dind
variables:
DOCKER_HOST: tcp://docker:2376
DOCKER_TLS_CERTDIR: "/certs"
before_script:
- pip install molecule molecule-docker ansible
script:
- cd roles/nginx && molecule testPractical Tips
Use pre-built Ansible images — geerlingguy/docker-*-ansible images have Python and systemd configured correctly. Building your own is painful.
Set command: "" and privileged: true for systemd support — otherwise service tests fail.
Write verification before the role — know what success looks like before you write the role. It forces clarity on what the role should actually do.
Test failure scenarios — what happens when config is invalid? Does the role fail loudly or silently?
Use molecule converge during development — molecule test destroys and recreates on every run. During development, converge keeps the container alive and re-runs the role, which is much faster.
Molecule turns Ansible role testing from a manual "deploy and check" process into an automated, reproducible pipeline. Set it up for your most critical roles first, add Docker and Podman platform coverage, and you'll catch breaking changes before they reach production.