Ansible Debugging Techniques
Introduction
When working with Ansible, you'll inevitably encounter situations where your playbooks don't behave as expected. Effective debugging is a crucial skill that separates beginners from experienced Ansible users. This guide explores various techniques to identify, diagnose, and resolve common issues in your Ansible automation workflows.
Debugging in Ansible involves systematically isolating problems, examining variable states, tracking execution flow, and interpreting error messages. With the right techniques, you can quickly pinpoint issues and fix them without extensive trial and error.
Basic Debugging with Increased Verbosity
Understanding Ansible Verbosity Levels
Ansible offers different verbosity levels when running playbooks, which can be enabled using the -v
flag. Each additional "v" provides more detailed output.
# Basic verbosity
ansible-playbook playbook.yml -v
# More detailed output
ansible-playbook playbook.yml -vv
# Connection debugging
ansible-playbook playbook.yml -vvv
# Extremely verbose output (includes module arguments)
ansible-playbook playbook.yml -vvvv
Example: Running a Playbook with Increased Verbosity
Input:
ansible-playbook deploy.yml -vv
Output:
PLAY [Deploy application] *******************************************************
TASK [Gathering Facts] **********************************************************
ok: [web01]
TASK [Install dependencies] *****************************************************
changed: [web01] => {"changed": true, "cmd": ["apt", "install", "-y", "python3-pip"], "delta": "0:00:03.456789", "end": "2025-03-15 12:34:56.123456", "rc": 0, "start": "2025-03-15 12:34:52.666667", "stderr": "", "stderr_lines": [], "stdout": "Reading package lists...
Building dependency tree...
Reading state information...
python3-pip is already the newest version (20.0.2-5).
0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.", "stdout_lines": ["Reading package lists...", "Building dependency tree...", "Reading state information...", "python3-pip is already the newest version (20.0.2-5).", "0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded."]}
With increased verbosity, you can see detailed information about each task's execution, including command outputs, return values, and timing information.
Using the debug
Module
The debug
module is one of the most straightforward ways to inspect variables and expressions during playbook execution.
Basic Variable Inspection
- name: Display a variable
debug:
var: inventory_hostname
- name: Show a custom message with variable
debug:
msg: "The application version is: {{ app_version }}"
Conditional Debugging
You can use conditions to only show debug output in specific circumstances:
- name: Debug when a condition is met
debug:
msg: "Warning: Available disk space is {{ available_disk_space }}GB"
when: available_disk_space < 5
Example: Debugging a Complex Variable Structure
For a dictionary or list, you might want to see its structure:
- name: Get user information
user_info:
name: admin
register: user_data
- name: Display user data
debug:
var: user_data
verbosity: 1 # Only show when -v or higher is used
Output with -v
:
TASK [Display user data] ********************************************************
ok: [server1] => {
"user_data": {
"changed": false,
"failed": false,
"user": {
"name": "admin",
"uid": 1001,
"home": "/home/admin",
"shell": "/bin/bash",
"groups": ["admin", "sudo"]
}
}
}
Registering and Examining Variables
The register
keyword allows you to capture the output of a task for later inspection.
- name: Check service status
command: systemctl status nginx
register: service_result
failed_when: false # Prevent task failure if service is down
- name: Display service status
debug:
var: service_result.stdout_lines
Example: Using Registered Variables for Decision Making
- name: Check if configuration file exists
stat:
path: /etc/myapp/config.ini
register: config_file
- name: Display status of config file
debug:
msg: "Config file exists: {{ config_file.stat.exists }}"
- name: Create configuration file if missing
template:
src: templates/config.ini.j2
dest: /etc/myapp/config.ini
when: not config_file.stat.exists
Using the --check
and --diff
Options
Ansible's check mode allows you to do a dry run without making actual changes to your systems.
# Dry run to see what would change
ansible-playbook deploy.yml --check
# Show differences in modified files
ansible-playbook deploy.yml --diff
# Combine both options
ansible-playbook deploy.yml --check --diff
Example: Checking Configuration File Changes
Input:
ansible-playbook configure_nginx.yml --check --diff
Output:
TASK [Update Nginx configuration] ***********************************************
--- before: /etc/nginx/nginx.conf
+++ after: /etc/nginx/nginx.conf
@@ -8,7 +8,7 @@
events {
- worker_connections 768;
+ worker_connections 1024;
}
http {
This output shows exactly what would be changed without actually applying the changes, making it a safe way to verify your playbook's behavior.
Task-Level Debugging with Tags
Tags allow you to run specific parts of a playbook, which is useful for testing and debugging.
- name: Install application dependencies
apt:
name: "{{ item }}"
state: present
loop: "{{ dependencies }}"
tags: [dependencies]
- name: Configure application
template:
src: app_config.j2
dest: /etc/app/config.yml
tags: [configuration]
You can then run specific tagged tasks:
# Only run tasks tagged with "configuration"
ansible-playbook deploy.yml --tags configuration
# Skip certain tags
ansible-playbook deploy.yml --skip-tags dependencies
Creating an Ansible Debug Playbook
A dedicated debug playbook can help you inspect the state of a system:
---
- name: Debug playbook for system inspection
hosts: all
gather_facts: yes
tasks:
- name: Display system information
debug:
msg: "{{ ansible_distribution }} {{ ansible_distribution_version }}"
- name: Show available memory
debug:
msg: "Available memory: {{ ansible_memtotal_mb }} MB"
- name: List all environment variables
debug:
var: ansible_env
- name: Check disk space
command: df -h
register: disk_space
changed_when: false
- name: Display disk space
debug:
var: disk_space.stdout_lines
The assert
Module for Validation
The assert
module helps you validate assumptions about your environment and fail early with clear messages.
- name: Ensure minimum Python version
assert:
that:
- ansible_python_version is version('3.6', '>=')
fail_msg: "Python 3.6 or higher is required. Found version: {{ ansible_python_version }}"
success_msg: "Python version requirement met: {{ ansible_python_version }}"
- name: Validate database connection settings
assert:
that:
- db_host is defined
- db_port is defined and db_port | int > 0
- db_name is defined and db_name != ""
fail_msg: "Database configuration is incomplete or invalid"
Advanced Debugging Techniques
Using the meta
Module to Control Execution
The meta
module allows you to control playbook execution flow:
- name: Critical task
command: some_command
register: result
failed_when: result.rc != 0
- name: Stop execution on failure
meta: end_play
when: result.rc != 0
- name: This will not run if previous task failed
debug:
msg: "Continuing with deployment"
Creating Debugging Callbacks
Ansible callbacks can provide custom output formatting. You can enable built-in callbacks like profile_tasks
to get timing information:
# Enable the profile_tasks callback in ansible.cfg
[defaults]
callback_whitelist = profile_tasks
# Or via environment variable
ANSIBLE_CALLBACK_WHITELIST=profile_tasks ansible-playbook playbook.yml
Using ansible-playbook --step
The --step
option allows you to interactively confirm each task before execution:
ansible-playbook deploy.yml --step
This prompts you with options like:
- (y)es: execute the task
- (n)o: skip the task
- (c)ontinue: stop asking and run all remaining tasks
Visualizing Playbook Execution Flow
Mermaid diagrams can help visualize the flow of complex playbooks:
Remote Debugging
SSH to Managed Node for Direct Inspection
When remote debugging is necessary, you can use the ansible
command to quickly SSH into a problematic node:
ansible webserver -m shell -a "whoami"
ansible database -m shell -a "systemctl status postgresql"
Creating Debug Files on Remote Systems
Sometimes you need to leave debug artifacts on remote systems for inspection:
- name: Capture environment state for debugging
copy:
content: |
Timestamp: {{ ansible_date_time.iso8601 }}
Hostname: {{ inventory_hostname }}
OS: {{ ansible_distribution }} {{ ansible_distribution_version }}
Kernel: {{ ansible_kernel }}
Python: {{ ansible_python_version }}
Environment Variables: {{ ansible_env | to_nice_yaml }}
dest: "/tmp/ansible_debug_{{ inventory_hostname }}.log"
changed_when: false
Common Debugging Patterns
Using Blocks for Error Handling
- name: Transaction with error handling
block:
- name: Update application
git:
repo: https://github.com/myorg/myapp.git
dest: /opt/myapp
register: git_result
- name: Build application
command: make build
args:
chdir: /opt/myapp
when: git_result.changed
rescue:
- name: Log failure
debug:
msg: "Application update failed, reverting to previous version"
- name: Restore from backup
command: cp -R /opt/myapp.bak /opt/myapp
always:
- name: Restart application service
service:
name: myapp
state: restarted
Using ignore_errors and failed_when
- name: Check if service exists (may fail on some systems)
command: systemctl status custom-service
register: service_status
ignore_errors: yes
- name: Custom failure condition
command: /opt/app/healthcheck.sh
register: health_result
failed_when: "'CRITICAL' in health_result.stdout"
Practical Example: Debugging a Deployment Issue
Let's walk through a real-world debugging scenario:
---
- name: Debug application deployment
hosts: web_servers
gather_facts: yes
vars:
app_name: my_web_app
app_version: 1.2.3
required_memory_mb: 1024
tasks:
- name: Check system requirements
assert:
that:
- ansible_memtotal_mb >= required_memory_mb
- ansible_distribution in ['Ubuntu', 'Debian', 'CentOS']
fail_msg: "System does not meet requirements: {{ ansible_memtotal_mb }}MB RAM, {{ ansible_distribution }} {{ ansible_distribution_version }}"
- name: Check if application is already deployed
stat:
path: "/opt/{{ app_name }}/VERSION"
register: version_file
- name: Read current version if exists
command: cat "/opt/{{ app_name }}/VERSION"
register: current_version
changed_when: false
when: version_file.stat.exists
- name: Debug version comparison
debug:
msg:
- "Installed version: {{ current_version.stdout | default('not installed') }}"
- "Target version: {{ app_version }}"
- "Update needed: {{ (not version_file.stat.exists) or (current_version.stdout is defined and current_version.stdout != app_version) }}"
- name: Deploy application
block:
- name: Download application package
get_url:
url: "https://example.com/packages/{{ app_name }}-{{ app_version }}.tar.gz"
dest: "/tmp/{{ app_name }}-{{ app_version }}.tar.gz"
register: download_result
- name: Extract application
unarchive:
src: "/tmp/{{ app_name }}-{{ app_version }}.tar.gz"
dest: "/opt/"
remote_src: yes
when: download_result is changed
register: extract_result
- name: Create VERSION file
copy:
content: "{{ app_version }}"
dest: "/opt/{{ app_name }}/VERSION"
when: extract_result is changed
rescue:
- name: Debug deployment failure
debug:
msg: "Deployment failed. See registered variables for details."
- name: Collect system information for troubleshooting
command: "{{ item }}"
loop:
- "df -h"
- "free -m"
- "ls -la /opt/"
register: system_checks
changed_when: false
- name: Display system checks
debug:
var: system_checks.results
This playbook includes several debugging techniques:
- System requirement verification with clear error messages
- Version comparison with debug output
- Block-based error handling
- Automated collection of troubleshooting information
Summary
Effective Ansible debugging requires a combination of techniques:
- Increase verbosity with
-v
,-vv
,-vvv
, or-vvvv
to get more detailed output - Use the
debug
module to inspect variables and expressions - Register task outputs to variables for inspection
- Employ
--check
and--diff
for safe, dry-run testing - Use tags to isolate and test specific parts of playbooks
- Create dedicated debug playbooks for system inspection
- Validate assumptions with the
assert
module - Use block/rescue/always constructs for structured error handling
- Apply conditional logic with
when
,failed_when
, andchanged_when
- Create debug artifacts on remote systems when necessary
Mastering these debugging techniques will save you time and frustration as you develop and maintain your Ansible automation.
Additional Resources
- Ansible Documentation: Debugging Playbooks
- Ansible Documentation: Debug Module
- Ansible Documentation: Error Handling
Exercises
- Create a debug playbook that gathers information about your target systems and displays it in a structured format.
- Modify an existing playbook to include proper error handling with the block/rescue/always pattern.
- Practice using
--check
and--diff
on a configuration management playbook to see what changes would be made. - Write a playbook that uses the
assert
module to validate at least three different system requirements. - Create a troubleshooting playbook that collects logs, configuration files, and system information from remote hosts.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)