Debug Ansible Playbooks with debug, assert, check mode and diff mode

Debug Ansible playbooks by printing variables with debug, capturing task output with register, validating assumptions with assert, inspecting files and facts, and previewing changes with --check, --diff, and verbosity.

Published

Updated

Read time 13 min read

Reviewed byDeepak Prasad

Debug Ansible playbooks with debug, register, assert, check mode and diff on Rocky Linux 10

When a playbook fails on a template line or a when: condition, guessing variable values wastes time. Debug Ansible playbooks by inspecting runtime data first: print variables with debug, capture module output with register, validate assumptions with assert, check files and services with facts modules, and preview file changes with --check and --diff.

This guide is a practical debugging workflow—not a repeat of inventory, SSH, or sudo diagnosis. If you do not yet know which layer failed, start with the Ansible troubleshooting hub. For full variable precedence or register semantics, see Ansible variables and register and magic variables.

Tested on: Rocky Linux 10.2 (Red Quartz); kernel 6.12.0-211.16.1.el10_2.0.1.x86_64; ansible-core 2.16.16.

NOTE
This chapter is part of the GoLinuxCloud Ansible tutorial (RHCE EX294). Follow along from ~/ansible-project, inventory group lab, and playbooks in playbooks/. Use your own host names and paths if yours differ.
IMPORTANT
This page covers debugging tools and workflowdebug, register, assert, facts modules, verbosity, check mode, and diff mode. It does not replace the troubleshooting hub for connectivity errors, the collection and variable errors guide for FQCN issues, or the dedicated facts and template tutorials.


Why Debugging Ansible Playbooks Matters

Ansible failures are easier to fix when you look at actual runtime values instead of rewriting tasks at random. A template error often means a variable is undefined on one host. A when: that never runs may mean a registered key is not where you expect. Check mode shows whether a file task would change the host before you apply it in production.

Debugging helps with idempotency checks, conditionals, templates, facts, and registered output. Use the tools in this guide before you change task logic.


Quick Ansible Debugging Workflow

Work through these steps when a playbook misbehaves:

Step What to check Tool
1 Is playbook syntax valid? --syntax-check
2 Are correct hosts targeted? --list-hosts
3 Which tasks will run? --list-tasks
4 What is the variable value? debug
5 What did a task return? register + debug
6 Is a file, service, or package present? stat, service_facts, package_facts
7 Will this task change the host? --check --diff
8 Where exactly does it fail? --start-at-task, --step, -vvv

Those CLI flags are documented in how to run Ansible playbooks. For expression bugs inside when:, see operators and tests.

Limit output to one host with -l rocky2 while you debug so the screen stays readable. Avoid -vvv or higher when playbooks pass passwords, tokens, or vaulted variables—verbosity can print module arguments and return data that leak secrets into your terminal and CI logs. Use no_log: true on sensitive tasks and reserve high verbosity for non-secret failures.


Use Verbose Mode with -v, -vvv and -vvvv

Verbosity adds detail without editing the playbook. Increase it when you need to see what Ansible sent to a module or how SSH connected.

Verbosity Use
-v Basic extra output; shows which config file Ansible loaded
-vv More task and module detail, including task paths
-vvv Module arguments, return values, and task internals—a good default for task debugging
-vvvv Connection debugging: SSH user, key, port, and remote temp paths

From the project directory, run a playbook with basic verbosity:

bash
cd ~/ansible-project
bash
ansible-playbook playbooks/debug-demo.yml -l rocky2 -v

Sample output:

output
Using /home/ansible/ansible-project/ansible.cfg as config file

PLAY [Debug Ansible playbook demo] *********************************************

TASK [Run command] *************************************************************
changed: [rocky2] => {"changed": true, "cmd": ["hostname"], "rc": 0, "stdout": "rocky2", ...}

The Using ... ansible.cfg line confirms which configuration file this shell is using—useful when a playbook works locally but fails in CI.

WARNING
-vvvv and high verbosity can print secrets, tokens, or rendered template content in logs. Avoid piping verbose CI output to public log stores without filtering. For tasks that handle passwords, tokens, or private keys, set no_log: true to hide arguments and results from normal output—and still avoid unnecessary debug tasks around secret values.

Use the debug Module

The ansible.builtin.debug module prints values during execution. Use it to confirm variables, registered results, and expressions before a risky task runs.

var: prints the structured value of a variable:

yaml
- name: Print a simple variable
  ansible.builtin.debug:
    var: app_port

msg: accepts Jinja2 expressions in a string:

yaml
- name: Print a custom message
  ansible.builtin.debug:
    msg: "Application will listen on port {{ app_port }}"

After register, print one key or the whole result:

yaml
- name: Show command output
  ansible.builtin.debug:
    var: hostname_result.stdout

The verbosity parameter runs the task only when playbook verbosity meets the threshold—handy for noisy detail you do not want on every run:

yaml
- name: Print debug output only when verbose mode is used
  ansible.builtin.debug:
    msg: "Detailed value is {{ app_config }}"
    verbosity: 2

Run the demo playbook from the project directory:

bash
cd ~/ansible-project
bash
ansible-playbook playbooks/debug-demo.yml -l rocky2

Sample output:

output
TASK [Print a simple variable] *************************************************
ok: [rocky2] => {
    "app_port": 8080
}

TASK [Print a custom message] **************************************************
ok: [rocky2] => {
    "msg": "Application will listen on port 8080"
}

TASK [Print debug output only when verbose mode is used] ***********************
skipping: [rocky2]

The verbosity-gated task is skipped at default verbosity. Rerun with -vv and it prints the detailed app_config value.

Remove temporary debug tasks after you fix the playbook—leftover debug clutters logs and may expose sensitive data.


Use register to Capture Task Results

register stores a task's return dictionary in a variable you name. That output drives conditionals, loops, and follow-up debug tasks.

yaml
- name: Run command
  ansible.builtin.command: hostname
  register: hostname_result

- name: Show command output
  ansible.builtin.debug:
    var: hostname_result.stdout

Registered variables usually include keys such as changed, failed, rc, stdout, stderr, and stdout_lines. Module-specific keys appear on top of that structure. For loop output and magic variables, see register and magic variables.

Registered variables are host-specific. If a task runs on multiple hosts, each host gets its own hostname_result. When you debug the value, limit to one host with -l rocky2 or the output becomes noisy.


Understand Registered Variable Output

Many guides show register but skip the result shape. These keys appear on most command-style results:

Key Meaning
changed Whether Ansible thinks the task changed the host
failed Whether the task failed
rc Return code for command / shell-like tasks
stdout Standard output as a string
stdout_lines Standard output split into lines
stderr Error output
msg Module message
results List of per-item results when using loops

With -v, the registered command task shows the full hash—including stdout, stderr, and rc—in one line of JSON.


Use assert to Validate Assumptions

ansible.builtin.assert fails early when expressions are false, with a message you control. Use it to validate required variables, port ranges, or OS family before package or service tasks run.

yaml
- name: Validate app_port is defined
  ansible.builtin.assert:
    that:
      - app_port is defined
      - app_port | int > 0
      - app_port | int < 65536
    fail_msg: "app_port must be defined and must be a valid TCP port"

When validation passes, Ansible reports all assertions succeeded:

output
TASK [Validate app_port is defined] ********************************************
ok: [rocky2] => {
    "changed": false,
    "msg": "All assertions passed"
}

When a value is out of range, the failure names the failing expression:

output
TASK [Validate app_port is defined] ********************************************
fatal: [rocky2]: FAILED! => {
    "assertion": "app_port | int < 65536",
    "changed": false,
    "evaluated_to": false,
    "msg": "app_port must be defined and must be a valid TCP port"
}

Place assertions before risky tasks so obscure template errors do not appear ten steps later.

To gate package or service tasks by platform:

yaml
- name: Confirm target is Red Hat family
  ansible.builtin.assert:
    that:
      - ansible_os_family == "RedHat"
    fail_msg: "This play supports Red Hat family hosts only"

Use stat to Check Files Before Running Tasks

ansible.builtin.stat retrieves file or filesystem status—existence, type, owner, mode, and checksum—without shelling out to test or ls. It supports check mode fully.

yaml
- name: Check if config file exists
  ansible.builtin.stat:
    path: /etc/ssh/sshd_config
  register: config_stat

- name: Show file status
  ansible.builtin.debug:
    var: config_stat.stat.exists

Sample output:

output
TASK [Show file status] ********************************************************
ok: [rocky2] => {
    "config_stat.stat.exists": true
}

Use config_stat.stat.isreg, isdir, or checksum when a copy or service task depends on file state.


Use service_facts to Check Service State

ansible.builtin.service_facts gathers service state as facts—useful before restart or enable tasks.

yaml
- name: Gather service facts
  ansible.builtin.service_facts:

- name: Show sshd service facts
  ansible.builtin.debug:
    var: ansible_facts.services['sshd.service']

Sample output:

output
TASK [Show sshd service facts] *************************************************
ok: [rocky2] => {
    "ansible_facts.services['sshd.service']": {
        "name": "sshd.service",
        "source": "systemd",
        "state": "running",
        "status": "enabled"
    }
}

Service names follow the unit name on systemd hosts—often httpd.service or sshd.service. Inspect ansible_facts.services selectively; printing the entire dict is rarely useful.


Use package_facts to Check Installed Packages

ansible.builtin.package_facts records installed packages so you can branch without running rpm -q or dnf list.

yaml
- name: Gather package facts
  ansible.builtin.package_facts:
    manager: auto

- name: Check whether openssh is installed
  ansible.builtin.debug:
    msg: "openssh is installed"
  when: "'openssh' in ansible_facts.packages"

Package names differ by manager and distribution. Debug the keys under ansible_facts.packages once per platform rather than guessing.


Use --syntax-check, --list-hosts and --list-tasks

These CLI options validate structure before execution. They bridge playbook editing and runtime debugging.

Option Use
--syntax-check Validate YAML and playbook structure
--list-hosts Confirm target hosts for each play
--list-tasks Show task order and statically imported tasks
--list-tags List tags when you run plays selectively

Confirm syntax from the project directory:

bash
cd ~/ansible-project
bash
ansible-playbook playbooks/ping-lab.yml --syntax-check

Sample output:

output
playbook: playbooks/ping-lab.yml

List hosts matched by a play:

bash
ansible-playbook playbooks/debug-demo.yml --list-hosts

Sample output:

output
play #1 (lab): Debug Ansible playbook demo	TAGS: []
    pattern: ['lab']
    hosts (1):
      rocky2

List tasks in order:

bash
ansible-playbook playbooks/debug-demo.yml --list-tasks

Sample output:

output
play #1 (lab): Debug Ansible playbook demo	TAGS: []
    tasks:
      Print a simple variable	TAGS: []
      Print a custom message	TAGS: []
      Run command	TAGS: []
      ...

Deeper inventory and host-pattern failures belong in the troubleshooting hub, not here.


Use Check Mode with --check

Check mode runs tasks without making changes where modules support it. Modules report what they would change instead of applying it.

bash
ansible-playbook playbooks/file-demo.yml -l rocky2 --check
Situation What to know
Module supports check mode Reports predicted changes
Module does not support check mode May skip or return limited data
Task must run even in check mode Set check_mode: false on that task
Task should only run in check mode Set check_mode: true if needed

command and shell cannot safely predict arbitrary command effects. In check mode, use creates or removes so Ansible can predict whether the command would run; otherwise these tasks may be skipped or return limited information. Prefer state modules such as file, package, copy, or template when testing idempotency—see Ansible idempotency.


Use Diff Mode with --diff

Diff mode shows file content changes for modules such as copy, template, lineinfile, and blockinfile. Combine it with check mode to preview rendered files.

bash
ansible-playbook playbooks/debug-template-demo.yml -l rocky2 --check --diff

Sample output:

output
TASK [Deploy application config] ***********************************************
--- before
+++ after: /home/ansible/.ansible/tmp/.../app.conf.j2
@@ -0,0 +1,3 @@
+# demo-app configuration
+listen_port=8080
+host=rocky2

changed: [rocky2]

The unified diff shows exactly what the template would write to /tmp/demo-app.conf.

WARNING
Do not use --diff carelessly on templates or files that contain passwords, tokens, private keys, or Vault-rendered secrets—rendered content appears in the log.

Use --start-at-task and --step

After you fix an error mid-playbook, rerun from a named task instead of replaying everything:

bash
ansible-playbook playbooks/debug-demo.yml -l rocky2 --start-at-task "Gather service facts"

Ansible still runs fact gathering, then starts at the named task. Task names must match exactly—unique, descriptive name: values make this reliable.

Use --start-at-task only when earlier tasks are already applied or not required for the failing task. If skipped tasks created variables, files, packages, or registered results needed later, the resumed run may fail differently.

--step prompts before each task—useful on long playbooks when you want to confirm state interactively:

bash
ansible-playbook playbooks/debug-demo.yml -l rocky2 --step

Debug Variables, Facts and Templates

For template failures, print inputs immediately before the template task:

yaml
- name: Show template inputs
  ansible.builtin.debug:
    msg:
      app_name: "{{ app_name | default('undefined') }}"
      app_port: "{{ app_port | default('undefined') }}"
      inventory_hostname: "{{ inventory_hostname }}"

Use default('undefined') only for optional values—required variables should fail loudly via assert or an undefined error you can trace. Print hostvars or entire ansible_facts only when necessary; filtered keys keep output readable. For template syntax and filters, see template module and Jinja2; for fact gathering depth, see Ansible facts.


Common Debugging Examples

Debug a variable before using it in a task

yaml
- name: Confirm port before template
  ansible.builtin.debug:
    var: app_port

Debug output from a command task

yaml
- name: Run command
  ansible.builtin.command: hostname
  register: hostname_result

- name: Show command output
  ansible.builtin.debug:
    var: hostname_result.stdout

Debug loop results

yaml
- name: Create demo directories
  ansible.builtin.file:
    path: "/tmp/debug-loop-{{ item }}"
    state: directory
    mode: "0755"
  loop:
    - alpha
    - beta
  register: loop_result

- name: Show loop results
  ansible.builtin.debug:
    var: loop_result.results

Loop output lives under results—each element has its own item, changed, and failed keys.

Validate required variables with assert

yaml
- name: Validate app_port is defined
  ansible.builtin.assert:
    that:
      - app_port is defined
      - app_port | int > 0
    fail_msg: "app_port must be defined and positive"

Check if a file exists before copying

yaml
- name: Check if config file exists
  ansible.builtin.stat:
    path: /etc/myapp/myapp.conf
  register: config_stat

- name: Copy default config when missing
  ansible.builtin.copy:
    src: files/myapp.conf
    dest: /etc/myapp/myapp.conf
  when: not config_stat.stat.exists

Preview template changes with --check --diff

bash
ansible-playbook playbooks/debug-template-demo.yml -l rocky2 --check --diff

Common Mistakes

Mistake Why it causes problems
Printing entire ansible_facts without filtering Output becomes huge and hard to read
Leaving debug tasks in production playbooks Logs become noisy and may expose sensitive values
Using debug instead of fixing variable scope Hides the real variable-loading problem
Running -vvvv in CI logs with secrets Verbose output can expose credentials or tokens
Omitting no_log: true on secret-handling tasks Passwords and tokens may appear in playbook output
Using --diff on secret templates Rendered secrets may appear in output
Ignoring registered variable structure Using result.stdout when data is under result.results
Debugging all hosts at once Output becomes unreadable; use -l to limit
Using command only to check state Prefer stat, service_facts, or package_facts where possible
Trusting check mode blindly Some modules do not fully support check mode
Making assertions too late Fail early before risky tasks

Best Practices

Practice Why
Start with --syntax-check, --list-hosts, and --list-tasks Confirms playbook structure before execution
Limit debugging to one host with -l Keeps output readable
Use debug: var= for variables and registered results Prints structured output clearly
Use register on important tasks Makes output reusable for debug and conditions
Use assert before risky tasks Fails early with a useful message
Prefer facts modules over shell checks More idempotent and structured
Use --check --diff before file or config changes Shows planned changes where supported
Avoid printing secrets Protects credentials in logs
Remove temporary debug tasks Keeps playbooks clean
Keep task names unique Helps --start-at-task and log reading

Summary

Debug Ansible playbooks by inspecting runtime data before you rewrite tasks: print values with debug, capture output with register, validate assumptions with assert, probe state with stat, service_facts, and package_facts, and preview file changes with --check and --diff. Use -vvv for module-level detail and --start-at-task to resume long plays. When the failure is inventory, SSH, or sudo—not variable inspection—use the troubleshooting hub first.


References

  • ansible.builtin.debug — print variables during execution
  • ansible.builtin.assert — fail when expressions are false
  • Check mode — dry-run behavior
  • Command line tools — verbosity and playbook options
  • Ansible register and magic variables — registered output structure
  • Ansible troubleshooting hub — diagnose connectivity and parsing layers first

Frequently Asked Questions

1. How do I debug a variable in Ansible?

Use ansible.builtin.debug with var: variable_name or msg: "{{ variable_name }}" to print the value during playbook execution.

2. How do I debug a registered variable in Ansible?

Register the task result with register, then print it with ansible.builtin.debug using var: result_name or a specific key such as result_name.stdout.

3. What is the difference between check mode and diff mode in Ansible?

Check mode predicts changes without applying them where modules support it. Diff mode shows content differences for supported file-related modules. They are often used together with --check --diff.

4. How do I run an Ansible playbook from a specific task?

Use ansible-playbook playbook.yml --start-at-task "Task name". Unique task names make this easier.

5. Is it safe to use -vvvv or --diff in production logs?

Use them carefully because verbose and diff output can expose variables, rendered templates, tokens, passwords, or private data.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive …