Ansible Lint & Molecule

What are Lint and Molecule? Ansible Lint and Molecule are essential testing tools. Ansible Lint checks playbook code quality and best practices, while Molecule provides a complete testing framework for roles and playbooks with automated testing scenarios.

Understanding Code Quality Tools

Why Use Testing Tools?

Testing tools ensure automation quality:

  • Code Quality: Catch errors before production
  • Best Practices: Enforce Ansible standards
  • Consistency: Maintain uniform code across team
  • CI/CD Integration: Automated testing in pipelines
  • Documentation: Tests serve as usage examples

Ansible Lint

What is Ansible Lint?

Ansible Lint is a command-line tool that checks playbooks, roles, and collections for common issues and enforces best practices.

Installation

# Install with pip
pip install ansible-lint

# Install specific version
pip install ansible-lint==6.22.0

# Install with pipx (isolated environment)
pipx install ansible-lint

# Verify installation
ansible-lint --version

Basic Usage

# Lint a playbook
ansible-lint playbook.yml

# Lint all files in directory
ansible-lint .

# Lint a role
ansible-lint roles/myrole/

# Lint with specific ruleset
ansible-lint --profile production playbook.yml

# Show all rules
ansible-lint --list-rules

# Lint and show tags
ansible-lint --list-tags

Common Rules and Fixes

# BAD - Using command when module exists
- name: Install package
  command: yum install httpd

# GOOD - Use proper module
- name: Install package
  ansible.builtin.yum:
    name: httpd
    state: present

# BAD - Task without name
- ansible.builtin.debug:
    msg: "Hello"

# GOOD - Always name tasks
- name: Display greeting
  ansible.builtin.debug:
    msg: "Hello"

# BAD - Using bare variables
- name: Install packages
  ansible.builtin.yum:
    name: {{ package_name }}

# GOOD - Quote template expressions
- name: Install packages
  ansible.builtin.yum:
    name: "{{ package_name }}"

# BAD - Not using FQCN
- name: Copy file
  copy:
    src: file.txt
    dest: /tmp/

# GOOD - Use fully qualified collection names
- name: Copy file
  ansible.builtin.copy:
    src: file.txt
    dest: /tmp/

Configuration File

Create .ansible-lint in project root:

---
# .ansible-lint
profile: production  # or: min, basic, moderate, safety, shared

# Exclude specific paths
exclude_paths:
  - .cache/
  - .github/
  - test/
  - venv/

# Skip specific rules
skip_list:
  - yaml[line-length]  # Allow long lines
  - name[casing]       # Allow flexible naming

# Warn only for specific rules
warn_list:
  - experimental

# Enable specific rules
enable_list:
  - args
  - empty-string-compare
  - no-log-password

# Override rule severity
kinds:
  - playbook: "**/deploy.yml"
  - tasks: "**/tasks/**/*.yml"
  - vars: "**/vars/**/*.yml"

# Custom rules directory
rulesdir:
  - ~/.ansible-lint/custom-rules/

Profiles

Ansible Lint includes built-in profiles:

  • min: Minimal rules (syntax errors only)
  • basic: Basic best practices
  • moderate: Balanced ruleset
  • safety: Safety and security focused
  • shared: For shared/public content
  • production: Strict production-ready standards

Auto-Fixing Issues

# Auto-fix issues (experimental)
ansible-lint --fix playbook.yml

# Fix specific rule
ansible-lint --fix --fix-only yaml[line-length] playbook.yml

# Preview fixes without applying
ansible-lint --fix --dry-run playbook.yml

CI/CD Integration

# .github/workflows/ansible-lint.yml
name: Ansible Lint
on: [push, pull_request]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'

      - name: Install ansible-lint
        run: pip install ansible-lint

      - name: Run ansible-lint
        run: ansible-lint --profile production

Molecule

What is Molecule?

Molecule is a testing framework designed for developing and testing Ansible roles and collections. It provides:

  • Automated test scenarios
  • Multiple infrastructure providers (Docker, Vagrant, EC2, etc.)
  • Verification with different tools (Ansible, Testinfra, etc.)
  • Complete role lifecycle testing

Installation

# Install Molecule with Docker driver
pip install molecule molecule-plugins[docker]

# Install with Vagrant driver
pip install molecule molecule-plugins[vagrant]

# Install with EC2 driver
pip install molecule molecule-plugins[ec2]

# Install all
pip install molecule molecule-plugins[docker,vagrant,ec2]

# Verify installation
molecule --version

Initialize New Role with Molecule

# Create new role with Molecule tests
molecule init role myrole --driver-name docker

# Creates structure:
# myrole/
# ├── README.md
# ├── defaults/
# ├── handlers/
# ├── meta/
# ├── molecule/
# │   └── default/
# │       ├── converge.yml
# │       ├── molecule.yml
# │       └── verify.yml
# ├── tasks/
# ├── templates/
# ├── tests/
# └── vars/

Add Molecule to Existing Role

# Navigate to role directory
cd roles/existing-role

# Initialize Molecule scenario
molecule init scenario --driver-name docker

# Initialize multiple scenarios
molecule init scenario staging --driver-name docker
molecule init scenario production --driver-name vagrant

Molecule Configuration

Configure Molecule in molecule/default/molecule.yml:

---
# molecule/default/molecule.yml
dependency:
  name: galaxy
  options:
    requirements-file: requirements.yml

driver:
  name: docker

platforms:
  - name: ubuntu-22
    image: geerlingguy/docker-ubuntu2204-ansible:latest
    pre_build_image: true
    privileged: true
    volumes:
      - /sys/fs/cgroup:/sys/fs/cgroup:ro
    command: /lib/systemd/systemd

  - name: centos-9
    image: geerlingguy/docker-centos9-ansible:latest
    pre_build_image: true
    privileged: true
    volumes:
      - /sys/fs/cgroup:/sys/fs/cgroup:ro
    command: /usr/lib/systemd/systemd

provisioner:
  name: ansible
  config_options:
    defaults:
      callbacks_enabled: profile_tasks
      stdout_callback: yaml
  inventory:
    host_vars:
      ubuntu-22:
        ansible_python_interpreter: /usr/bin/python3

verifier:
  name: ansible

scenario:
  test_sequence:
    - dependency
    - cleanup
    - destroy
    - syntax
    - create
    - prepare
    - converge
    - idempotence
    - side_effect
    - verify
    - cleanup
    - destroy

Test Playbooks

converge.yml - Main test playbook:

---
# molecule/default/converge.yml
- name: Converge
  hosts: all
  become: true
  roles:
    - role: myrole
      vars:
        nginx_port: 8080
        nginx_user: www-data

verify.yml - Verification tests:

---
# molecule/default/verify.yml
- name: Verify
  hosts: all
  tasks:
    - name: Check if nginx is installed
      ansible.builtin.package:
        name: nginx
        state: present
      check_mode: true
      register: nginx_installed
      failed_when: nginx_installed is changed

    - name: Check if nginx is running
      ansible.builtin.service:
        name: nginx
        state: started
      check_mode: true
      register: nginx_running
      failed_when: nginx_running is changed

    - name: Test nginx is responding
      ansible.builtin.uri:
        url: http://localhost:8080
        status_code: 200
      register: nginx_response

    - name: Verify configuration file exists
      ansible.builtin.stat:
        path: /etc/nginx/nginx.conf
      register: config_file
      failed_when: not config_file.stat.exists

Running Molecule Tests

# Full test sequence
molecule test

# Individual commands
molecule create        # Create test instances
molecule converge      # Run role on instances
molecule verify        # Run verification tests
molecule destroy       # Destroy test instances

# Test specific scenario
molecule test -s staging

# Test with specific platform
molecule test --platform-name ubuntu-22

# Keep instances after test for debugging
molecule test --destroy never

# Run only idempotence test
molecule idempotence

Molecule Test Sequence

Default test sequence stages:

  1. dependency: Install role dependencies
  2. cleanup: Clean up from previous runs
  3. destroy: Destroy test instances
  4. syntax: Check playbook syntax
  5. create: Create test instances
  6. prepare: Prepare instances (install prerequisites)
  7. converge: Apply role to instances
  8. idempotence: Verify idempotency (run again, expect no changes)
  9. side_effect: Run side effect playbook
  10. verify: Run verification tests
  11. cleanup: Clean up after tests
  12. destroy: Destroy test instances

Multiple Test Scenarios

# Directory structure with scenarios
molecule/
├── default/          # Standard test
│   ├── converge.yml
│   ├── molecule.yml
│   └── verify.yml
├── with-ssl/         # Test with SSL enabled
│   ├── converge.yml
│   ├── molecule.yml
│   └── verify.yml
└── cluster/          # Test cluster setup
    ├── converge.yml
    ├── molecule.yml
    └── verify.yml

# Run specific scenario
molecule test -s with-ssl

# List scenarios
molecule list

# Run all scenarios
molecule test --all

Debugging Failed Tests

# Create instances and login
molecule create
molecule login

# Inside container, manually test
# root@instance:/# ansible-playbook ...

# Converge and keep running
molecule converge

# Check instances
molecule list

# View logs
molecule --debug converge

Advanced Testing Patterns

Testinfra Verification

# Install testinfra
pip install molecule-plugins[testinfra]

# Configure in molecule.yml
verifier:
  name: testinfra
  options:
    v: 1

# Create tests/test_default.py
def test_nginx_installed(host):
    nginx = host.package("nginx")
    assert nginx.is_installed

def test_nginx_running(host):
    nginx = host.service("nginx")
    assert nginx.is_running
    assert nginx.is_enabled

def test_nginx_listening(host):
    socket = host.socket("tcp://0.0.0.0:80")
    assert socket.is_listening

def test_nginx_config(host):
    config = host.file("/etc/nginx/nginx.conf")
    assert config.exists
    assert config.user == "root"
    assert config.group == "root"

Multi-Platform Testing

---
# molecule/default/molecule.yml
platforms:
  - name: ubuntu-22
    image: geerlingguy/docker-ubuntu2204-ansible
    groups:
      - debian_family

  - name: ubuntu-20
    image: geerlingguy/docker-ubuntu2004-ansible
    groups:
      - debian_family

  - name: centos-9
    image: geerlingguy/docker-centos9-ansible
    groups:
      - rhel_family

  - name: centos-8
    image: geerlingguy/docker-centos8-ansible
    groups:
      - rhel_family

# Use groups in converge.yml
- name: Debian family specific tasks
  hosts: debian_family
  tasks:
    - name: Install apt packages
      ansible.builtin.apt:
        name: nginx
        state: present

- name: RHEL family specific tasks
  hosts: rhel_family
  tasks:
    - name: Install yum packages
      ansible.builtin.yum:
        name: nginx
        state: present

CI/CD Integration

GitHub Actions with Molecule

# .github/workflows/molecule.yml
name: Molecule Test
on: [push, pull_request]

jobs:
  molecule:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        scenario:
          - default
          - with-ssl
        platform:
          - ubuntu-22
          - centos-9
    steps:
      - uses: actions/checkout@v3

      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: |
          pip install molecule molecule-plugins[docker] ansible-lint

      - name: Run Molecule test
        run: |
          molecule test -s ${{ matrix.scenario }} --platform-name ${{ matrix.platform }}
        env:
          PY_COLORS: '1'
          ANSIBLE_FORCE_COLOR: '1'

GitLab CI/CD

# .gitlab-ci.yml
image: python:3.11

stages:
  - lint
  - test

before_script:
  - pip install molecule molecule-plugins[docker] ansible-lint

lint:
  stage: lint
  script:
    - ansible-lint

molecule:
  stage: test
  services:
    - docker:dind
  variables:
    DOCKER_HOST: tcp://docker:2375
    DOCKER_TLS_CERTDIR: ""
  script:
    - molecule test

Best Practices

Testing Best Practices:
  • Test Early: Run lint and tests during development
  • Multiple Platforms: Test on all supported OS versions
  • Idempotence: Always verify tasks are idempotent
  • CI Integration: Automate tests in CI/CD pipelines
  • Fix Warnings: Address lint warnings promptly
  • Document Tests: Explain what tests verify
  • Keep Tests Fast: Use Docker for speed
  • Version Control: Commit test configurations

Common Issues

Docker Permission Issues

# Add user to docker group
sudo usermod -aG docker $USER
newgrp docker

# Or run with sudo
sudo molecule test

Systemd in Docker

Use images with systemd support:

  • geerlingguy/docker-ubuntu2204-ansible
  • geerlingguy/docker-centos9-ansible
  • Configure privileged mode and mount cgroup

Slow Tests

  • Use pre-built images (pre_build_image: true)
  • Cache dependencies
  • Reduce test matrix size
  • Run only changed scenarios in CI

Next Steps