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:
- dependency: Install role dependencies
- cleanup: Clean up from previous runs
- destroy: Destroy test instances
- syntax: Check playbook syntax
- create: Create test instances
- prepare: Prepare instances (install prerequisites)
- converge: Apply role to instances
- idempotence: Verify idempotency (run again, expect no changes)
- side_effect: Run side effect playbook
- verify: Run verification tests
- cleanup: Clean up after tests
- 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
- Roles - Create testable roles
- Ansible Galaxy - Publish tested roles
- CI/CD - Automate testing pipelines
- Best Practices - Code quality standards
- Try the Playground - Practice Ansible concepts