Ansible Testing & Linting (Molecule)

Untested Ansible roles are liabilities. A role that has never been run against a clean system, verified to be idempotent, or checked against a target OS other than the one you developed on will eventually fail in production at the worst possible time. Molecule is the standard testing framework for Ansible roles. Ansible Lint is the standard code quality checker for playbooks and roles. Together, they bring software engineering discipline to infrastructure automation. This lesson gives you the skills to test confidently and ship automation you trust.

What Molecule Does

Molecule automates the full testing lifecycle for an Ansible role:

  1. Create: Spin up test instances (Docker containers, VMs, or cloud instances)
  2. Prepare: Run any prerequisite configuration on the instances
  3. Converge: Apply your role to the instances
  4. Idempotency: Run converge again and verify zero changes are reported
  5. Verify: Run assertions to confirm the role achieved the desired state
  6. Destroy: Tear down all test instances

This entire sequence runs automatically with molecule test. Failed tests report exactly which step failed and why.

Installing Molecule

pip3 install molecule molecule-docker ansible-lint

The molecule-docker plugin uses Docker containers as test instances — the fastest and most resource-efficient option for local development. Molecule also supports Vagrant, AWS, GCE, Azure, and others.

Initialising Molecule in a Role

cd roles/nginx
molecule init scenario --driver-name docker

This creates the molecule/default/ directory structure:

molecule/
  default/
    molecule.yml        # Scenario configuration
    converge.yml        # The test playbook (applies your role)
    verify.yml          # Assertions to run after converge
    prepare.yml         # (Optional) pre-role setup

Configuring molecule.yml

---
dependency:
  name: galaxy

driver:
  name: docker

platforms:
  - name: ubuntu-22.04
    image: geerlingguy/docker-ubuntu2204-ansible
    pre_build_image: true
  - name: ubuntu-20.04
    image: geerlingguy/docker-ubuntu2004-ansible
    pre_build_image: true
  - name: centos-8
    image: geerlingguy/docker-centos8-ansible
    pre_build_image: true

provisioner:
  name: ansible
  playbooks:
    converge: converge.yml
    verify: verify.yml

verifier:
  name: ansible

The geerlingguy/docker-*-ansible Docker images are pre-built containers with Python and systemd support — ideal for Ansible testing. Testing across multiple platforms simultaneously ensures cross-OS compatibility.

Writing converge.yml

---
- name: Converge
  hosts: all
  become: true
  roles:
    - role: nginx
      vars:
        nginx_http_port: 80
        nginx_vhosts:
          - server_name: testapp.local
            document_root: /var/www/testapp

Writing verify.yml with Assertions

---
- name: Verify
  hosts: all
  gather_facts: false

  tasks:
    - name: Ensure Nginx package is installed
      ansible.builtin.package:
        name: nginx
        state: present
      check_mode: true
      register: pkg_check
      failed_when: pkg_check.changed

    - name: Ensure Nginx service is running
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true
      check_mode: true
      register: svc_check
      failed_when: svc_check.changed

    - name: Ensure Nginx responds on port 80
      ansible.builtin.uri:
        url: http://localhost:80
        status_code: 200
      register: response

    - name: Verify HTTP response
      ansible.builtin.assert:
        that:
          - response.status == 200
        fail_msg: "Nginx is not responding with HTTP 200"
        success_msg: "Nginx is responding correctly"

    - name: Verify virtual host config exists
      ansible.builtin.stat:
        path: /etc/nginx/sites-available/testapp.local.conf
      register: vhost_file
      failed_when: not vhost_file.stat.exists

Running the Full Test Suite

# Full test lifecycle (create, converge, idempotency, verify, destroy)
molecule test

# Just apply the role (faster iteration during development)
molecule converge

# Run only the verify assertions
molecule verify

# Drop into the test container for debugging
molecule login --host ubuntu-22.04

# Destroy test instances
molecule destroy

Ansible Lint: Code Quality Enforcement

Ansible Lint checks playbooks and roles against a comprehensive set of best practice rules:

# Install
pip3 install ansible-lint

# Lint the current directory
ansible-lint

# Lint a specific file
ansible-lint site.yml

# Lint with a specific profile
ansible-lint --profile production

Ansible Lint profiles range from min (minimal rules) to production (strict professional standards). Common lint rules it enforces:

  • All tasks must have a name
  • Use the FQCN (Fully Qualified Collection Name) for modules: ansible.builtin.apt not just apt
  • Variables should be named with lowercase and underscores
  • No deprecated module syntax
  • Line length limits
  • YAML formatting standards

Configuring ansible-lint with .ansible-lint

# .ansible-lint
profile: production

exclude_paths:
  - .cache/
  - molecule/

skip_list:
  - yaml[line-length]    # Skip line length rule for complex tasks

warn_list:
  - experimental

Integrating Molecule into CI/CD

# GitHub Actions workflow fragment
- name: Run Molecule tests
  run: |
    pip install molecule molecule-docker ansible-lint
    cd roles/nginx
    molecule test
  env:
    PY_COLORS: '1'
    ANSIBLE_FORCE_COLOR: '1'

Try This: Write a Complete Molecule Test Suite for Your Nginx Role

Initialise Molecule in your roles/nginx directory. Write a verify.yml with at least five assertions: package installed, service running, service enabled on boot, config file exists with correct permissions, and HTTP response returns 200. Run molecule test and confirm all assertions pass. Then introduce a deliberate bug in your role (remove the service start task) and run molecule test again to see the failure. Fix the bug and re-run. This debugging cycle builds the test-driven reflex that distinguishes professional Ansible work.

Summary

Molecule automates the full role testing lifecycle: create, converge, idempotency check, verify, destroy. Testing across multiple platform containers simultaneously ensures cross-OS compatibility. The verify.yml playbook uses Ansible itself to assert that the role achieved the desired state. Ansible Lint enforces code quality and best practices against configurable rule sets. Integrating both into CI/CD pipelines makes quality enforcement automatic and non-negotiable.

Leave a Comment