Ansible Molecule: Role Testing with Docker and Podman Drivers

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:

  1. Create — spin up test instances (Docker containers, VMs, cloud instances)
  2. Converge — run your Ansible role against those instances
  3. Verify — run tests to confirm the role did what it should
  4. 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-podman

Verify:

molecule --version
# molecule 6.x.x using python 3.x

Initializing 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 docker

This 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 setup

Configuring 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-lint

Testing 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: 2

Keep 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 == 200

Option 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: true

Running 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 destroy

Using 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_ADMIN

Podman 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: or shell: without creates: or changed_when:
  • File permissions that get reset by other tasks
  • notify handlers 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 exists

Multiple 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_ssl

CI/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 test

Practical Tips

Use pre-built Ansible imagesgeerlingguy/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 developmentmolecule 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.

Read more

Start now free