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.

Key Testing Benefits:
  • 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:

  1. Create: Spin up test instances (Docker, VM, etc.)
  2. Prepare: Configure instances for testing
  3. Converge: Run your role against the instances
  4. Verify: Run verification tests (Ansible, Testinfra, etc.)
  5. 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

Common Debugger Commands:
  • p task - Print the task
  • p task.args - Print task arguments
  • p task_vars - Print task variables
  • p result - Print task result
  • task.args['key'] = 'value' - Modify task arguments
  • task_vars['key'] = 'value' - Modify task variables
  • u or update_task - Update task with changes
  • r or redo - Re-execute task
  • c or continue - Continue to next task
  • q or quit - 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
Pro Tip: Use environment variable 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

Common Testing Problems:
  • 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

  1. Start Small: Write tests as you develop, not after
  2. Test Idempotence: Every role should pass idempotence tests
  3. Use Multiple Platforms: Test on all target OSes
  4. Automate in CI: Run tests on every commit
  5. Document Test Requirements: Specify dependencies and prerequisites
  6. Keep Tests Fast: Use minimal containers and parallel execution
  7. Test Edge Cases: Include failure scenarios and rollback tests
  8. 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