Testing & Debugging Ansible
Why Test Ansible Code?
Testing Ansible playbooks and roles ensures reliable automation, catches errors before production deployment, and maintains code quality as infrastructure evolves. Proper testing strategies reduce downtime and increase confidence in automation workflows.
- Early Error Detection: Catch syntax and logic errors before deployment
- Regression Prevention: Ensure changes don't break existing functionality
- Documentation: Tests serve as executable documentation
- Confidence: Deploy changes knowing they've been validated
Testing Types in Ansible
1. Sanity Tests
Sanity tests perform static code analysis to enforce Ansible coding standards and requirements. They check for common issues like syntax errors, coding style violations, and documentation problems.
# Run all sanity tests
ansible-test sanity
# Run specific sanity tests
ansible-test sanity --test pep8
ansible-test sanity --test validate-modules
# Test specific files
ansible-test sanity plugins/modules/my_module.py
2. Unit Tests
Unit tests verify individual components of your code in isolation. They test specific functions or modules without external dependencies.
# Run all unit tests
ansible-test units
# Run tests for specific module
ansible-test units --test ping
# Run with coverage report
ansible-test units --coverage
3. Integration Tests
Integration tests validate that modules and playbooks work correctly in real scenarios, testing the interaction between components.
# Run integration test for ping module
ansible-test integration -v ping
# Run multiple integration tests
ansible-test integration -v ping user file
# Run against specific platform
ansible-test integration --docker ubuntu2004 ping
Molecule: Role Testing Framework
Molecule is the de facto standard for testing Ansible roles. It provides a complete testing framework with support for multiple platforms and test scenarios.
Installation
# Install Molecule with Docker support
pip install molecule molecule-plugins[docker]
# Verify installation
molecule --version
Molecule Workflow
Molecule follows a standardized test sequence:
- Create: Spin up test instances (Docker, VM, etc.)
- Prepare: Configure instances for testing
- Converge: Run your role against the instances
- Verify: Run verification tests (Ansible, Testinfra, etc.)
- Destroy: Tear down test instances
Creating a New Role with Molecule
# Initialize new role with Molecule
molecule init role my_role --driver-name docker
# Navigate to role directory
cd my_role
# Run complete test sequence
molecule test
# Run individual stages
molecule create # Create test instances
molecule converge # Apply role
molecule verify # Run tests
molecule destroy # Clean up
Example Molecule Scenario
# molecule/default/molecule.yml
---
dependency:
name: galaxy
driver:
name: docker
platforms:
- name: instance-ubuntu
image: geerlingguy/docker-ubuntu2004-ansible:latest
pre_build_image: true
- name: instance-centos
image: geerlingguy/docker-centos8-ansible:latest
pre_build_image: true
provisioner:
name: ansible
playbooks:
converge: converge.yml
verifier:
name: ansible
Verification with Testinfra
# Install Testinfra
pip install molecule-plugins[docker] pytest-testinfra
# molecule/default/tests/test_default.py
import os
import testinfra.utils.ansible_runner
testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
os.environ['MOLECULE_INVENTORY_FILE']
).get_hosts('all')
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):
assert host.socket('tcp://0.0.0.0:80').is_listening
Ansible Debugger
The Ansible debugger allows you to pause playbook execution and interactively troubleshoot issues.
Enabling the Debugger
1. Using the debugger Keyword
- name: Install package
ansible.builtin.apt:
name: "{{ package_name }}"
state: present
debugger: on_failed # Options: always, never, on_failed, on_unreachable, on_skipped
2. Global Configuration
# ansible.cfg
[defaults]
enable_task_debugger = True
# Or via environment variable
export ANSIBLE_ENABLE_TASK_DEBUGGER=True
3. Debug Strategy
- hosts: all
strategy: debug
tasks:
- name: This will use debug strategy
command: /bin/false
Debugger Commands
p task- Print the taskp task.args- Print task argumentsp task_vars- Print task variablesp result- Print task resulttask.args['key'] = 'value'- Modify task argumentstask_vars['key'] = 'value'- Modify task variablesuorupdate_task- Update task with changesrorredo- Re-execute taskcorcontinue- Continue to next taskqorquit- Quit debugger
Debugger Example Session
[webserver] TASK: Install package (debug)> p task.args
{'name': '{{ pkg_name }}', 'state': 'present'}
[webserver] TASK: Install package (debug)> p task_vars['pkg_name']
ERROR! 'pkg_name' is undefined
[webserver] TASK: Install package (debug)> task.args['name'] = 'nginx'
[webserver] TASK: Install package (debug)> r
ok: [webserver]
[webserver] TASK: Install package (debug)> c
PLAY RECAP ****************************************************************
Debugging Techniques
1. Debug Module
- name: Print variable value
debug:
var: my_variable
- name: Print custom message
debug:
msg: "The value is {{ my_variable }}"
- name: Conditional debugging
debug:
msg: "Only visible with -v or higher"
verbosity: 1
- name: Pretty print complex data
debug:
var: ansible_facts
verbosity: 2
2. Verbosity Levels
# No extra output
ansible-playbook playbook.yml
# -v: Show task results
ansible-playbook playbook.yml -v
# -vv: Show task input (variables)
ansible-playbook playbook.yml -vv
# -vvv: Show connection debugging
ansible-playbook playbook.yml -vvv
# -vvvv: Show SSH protocol debugging
ansible-playbook playbook.yml -vvvv
ANSIBLE_DISPLAY_ARGS_TO_STDOUT=True to see module arguments even without verbosity flags.
3. Check Mode (Dry Run)
# Run in check mode (no changes)
ansible-playbook playbook.yml --check
# Check mode with diff output
ansible-playbook playbook.yml --check --diff
# Force tasks to run in check mode
- name: Always check this
command: /bin/true
check_mode: yes
# Skip tasks in check mode
- name: Skip in check mode
command: /bin/dangerous
check_mode: no
4. Step-by-Step Execution
# Confirm each task before execution
ansible-playbook playbook.yml --step
# Start at specific task
ansible-playbook playbook.yml --start-at-task "Install packages"
5. Syntax Validation
# Syntax check playbook
ansible-playbook playbook.yml --syntax-check
# Validate YAML
python -c "import yaml; yaml.safe_load(open('playbook.yml'))"
# Use yamllint for stricter checking
pip install yamllint
yamllint playbook.yml
6. Task Debugging with register
- name: Run command
command: whoami
register: result
- name: Show result structure
debug:
var: result
- name: Show specific fields
debug:
msg: |
Command: {{ result.cmd }}
Exit Code: {{ result.rc }}
Output: {{ result.stdout }}
Error: {{ result.stderr }}
Changed: {{ result.changed }}
Playbook Testing Strategies
1. Idempotence Testing
Ensure playbooks can be run multiple times without changes after the first run:
# Run playbook twice
ansible-playbook playbook.yml
ansible-playbook playbook.yml
# Second run should show: changed=0
# Molecule idempotence test
molecule test --destroy never
molecule converge
molecule idempotence
2. Tag-Based Testing
- name: Install packages
apt:
name: nginx
state: present
tags: [install, packages]
- name: Configure service
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
tags: [config, nginx]
# Test only specific tags
ansible-playbook playbook.yml --tags "install"
ansible-playbook playbook.yml --skip-tags "config"
3. Limit Testing to Specific Hosts
# Test on single host first
ansible-playbook playbook.yml --limit web01
# Test on subset of hosts
ansible-playbook playbook.yml --limit "webservers[0:2]"
# Test on specific group
ansible-playbook playbook.yml --limit staging
Continuous Integration Testing
GitHub Actions Example
# .github/workflows/ansible-test.yml
name: Ansible Tests
on: [push, pull_request]
jobs:
ansible-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run ansible-lint
uses: ansible/ansible-lint-action@main
molecule:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: |
pip install molecule molecule-plugins[docker] ansible-lint
- name: Run Molecule tests
run: molecule test
GitLab CI Example
# .gitlab-ci.yml
stages:
- lint
- test
ansible-lint:
stage: lint
image: python:3.10
before_script:
- pip install ansible-lint
script:
- ansible-lint
molecule-test:
stage: test
image: python:3.10
services:
- docker:dind
before_script:
- pip install molecule molecule-plugins[docker]
script:
- molecule test
Ansible Lint
Ansible Lint checks playbooks for practices and behavior that could potentially be improved.
# Install ansible-lint
pip install ansible-lint
# Lint a playbook
ansible-lint playbook.yml
# Lint entire role
ansible-lint roles/my_role/
# Auto-fix issues (use with caution)
ansible-lint --fix playbook.yml
# Use specific rules
ansible-lint --enable-list rule-id
# Configuration file: .ansible-lint
skip_list:
- '403' # Package installs should not use latest
- '701' # meta/main.yml should contain relevant info
exclude_paths:
- .cache/
- .github/
- molecule/
warn_list:
- experimental
Common Testing Patterns
1. Pre-flight Checks
- name: Pre-flight checks
block:
- name: Check required variables
assert:
that:
- db_password is defined
- db_password | length > 8
fail_msg: "db_password must be defined and at least 8 characters"
- name: Verify connectivity
wait_for:
host: "{{ db_host }}"
port: 5432
timeout: 10
delegate_to: localhost
- name: Check disk space
assert:
that: ansible_mounts | selectattr('mount', 'equalto', '/') | map(attribute='size_available') | first > 5000000000
fail_msg: "Insufficient disk space"
2. Smoke Tests
- name: Smoke tests
block:
- name: Check service is running
service_facts:
- name: Verify nginx is active
assert:
that: ansible_facts.services['nginx.service'].state == 'running'
- name: Test HTTP response
uri:
url: http://localhost
status_code: 200
return_content: yes
register: response
- name: Verify content
assert:
that: "'Welcome' in response.content"
3. Rollback Testing
- name: Deploy with rollback capability
block:
- name: Backup current version
copy:
src: /var/www/app
dest: /var/backups/app.{{ ansible_date_time.epoch }}
remote_src: yes
- name: Deploy new version
git:
repo: https://github.com/example/app.git
dest: /var/www/app
version: "{{ deploy_version }}"
- name: Run tests
command: /var/www/app/test.sh
register: test_result
rescue:
- name: Rollback on failure
copy:
src: "{{ lookup('fileglob', '/var/backups/app.*') | sort | last }}"
dest: /var/www/app
remote_src: yes
- name: Restart service
systemd:
name: app
state: restarted
- fail:
msg: "Deployment failed, rolled back to previous version"
Performance Profiling
# Enable profiling callbacks
# ansible.cfg
[defaults]
callback_whitelist = timer, profile_tasks, profile_roles
# Or via environment
export ANSIBLE_CALLBACKS_ENABLED=timer,profile_tasks,profile_roles
# Output will show task execution times
TASK [Install nginx] *******************************************************
ok: [webserver] => (elapsed: 0:00:12.345)
# Profile all tasks at end
PLAY RECAP *****************************************************************
webserver: ok=10 changed=5 unreachable=0 failed=0 skipped=2
Playbook run took 0 days, 0 hours, 2 minutes, 34 seconds
Troubleshooting Common Issues
- Tests Pass Locally, Fail in CI: Check Python versions, Ansible versions, and environment variables
- Molecule Fails to Create: Verify Docker is running and user has permissions
- Idempotence Failures: Look for tasks using shell/command without changed_when
- Slow Tests: Use fact caching, parallel execution, and minimal containers
- Debugger Won't Start: Ensure strategy is not async and task actually failed
Testing Best Practices
- Start Small: Write tests as you develop, not after
- Test Idempotence: Every role should pass idempotence tests
- Use Multiple Platforms: Test on all target OSes
- Automate in CI: Run tests on every commit
- Document Test Requirements: Specify dependencies and prerequisites
- Keep Tests Fast: Use minimal containers and parallel execution
- Test Edge Cases: Include failure scenarios and rollback tests
- Version Pin: Lock Ansible and dependency versions in CI
Quick Reference Commands
# Syntax and validation
ansible-playbook playbook.yml --syntax-check
yamllint playbook.yml
ansible-lint playbook.yml
# Dry-run and debugging
ansible-playbook playbook.yml --check --diff
ansible-playbook playbook.yml -v
ansible-playbook playbook.yml --step
# Testing with ansible-test
ansible-test sanity
ansible-test units --coverage
ansible-test integration ping
# Molecule workflow
molecule init role my_role
molecule create
molecule converge
molecule verify
molecule test
molecule test --destroy never # Keep containers for debugging
# Debugging
ANSIBLE_ENABLE_TASK_DEBUGGER=True ansible-playbook playbook.yml
ansible-playbook playbook.yml --start-at-task "task name"
ansible-playbook playbook.yml --tags debug
Next Steps
- Learn about Error Handling strategies
- Explore CI/CD Integration for automated testing
- Master Best Practices for maintainable code
- Try the Playground to experiment with debugging