<aside> 🧭

Module 13 · Troubleshooting & Debugging

A systematic method for diagnosing Ansible failures, rather than guessing. Everything here is the accumulated "why is this broken" from the previous twelve modules, organised into a procedure you can follow under pressure.

🧠 concept → 🧪 exercise → ✅ expected result (hidden) → 🎯 interview questions (answers hidden)

Prerequisite: Modules 01–12.

</aside>


Part A · Classify the failure first

A1 · The four categories

<aside> 🩺

The analogy. Think of a doctor before running any tests at all. The first thing they ask is where it hurts — because a headache and a broken ankle do not get the same first move, and starting the wrong examination wastes the whole appointment. The question sounds far too simple to be useful, and it is the single thing that saves the most time.

Ansible failures come in four kinds, and each has a completely different first move. Debugging your playbook logic when the error says UNREACHABLE is examining the ankle when the pain is in the head — your module never even ran, so nothing in your YAML can possibly be the cause. Classify first, investigate second.

</aside>

<aside> 📖

Official docs: Playbook debugger · Common Ansible errors (FAQ) · Logging

</aside>

flowchart TD
    A["Something failed"] --> B{"What does the<br>output actually say?"}
    B -->|"ERROR! before any PLAY"| C["PARSE ERROR<br>YAML or playbook structure"]
    B -->|"UNREACHABLE!"| D["CONNECTION<br>the module never ran"]
    B -->|"FAILED! with a module msg"| E["EXECUTION<br>the module ran and objected"]
    B -->|"Runs fine, wrong result"| F["LOGIC<br>variables, conditionals, ordering"]
    C --> C1["--syntax-check<br>yamllint<br>read the ^ here marker"]
    D --> D1["Leave Ansible.<br>Reproduce with ssh -vvv"]
    E --> E1["Read the msg field.<br>Run the command by hand<br>on the target"]
    F --> F1["debug: var=...<br>type_debug<br>--list-tasks, --start-at-task"]
    style D fill:#FEE2E2,stroke:#DC2626
    style F fill:#FEF3C7,stroke:#D97706

<aside> 🔑

Classify before you investigate. The single most common waste of time is debugging a playbook when the failure is UNREACHABLE — which means the module never executed, so nothing in your playbook can be the cause.

Saying "first I work out which of the four categories it is, because each has a different first move" is a far better interview answer than listing flags.

</aside>

Category Signature First move
Parse ERROR! before any PLAY [...] header appears --syntax-check, read the ^ here marker, look above it
Connection UNREACHABLE! Stop using Ansible. ssh -vvv user@host
Execution FAILED! with a msg from the module Read msg, then run the equivalent by hand on the target
Logic Green run, wrong outcome debug: var=, type_debug, check precedence and ordering

A2 · Reading an error properly

<aside> 🧾

The analogy. Think of arguing about a bill. The total is wrong, and staring at the total tells you absolutely nothing — the answer is always further down the receipt, in the itemised lines showing what was actually rung through. And very often the item is right while the quantity is blank, which is a data-entry problem rather than a pricing one.

In a failure object, msg is the total; cmd and invocation.module_args are the itemised lines. That is why a cmd reading /opt//deploy.sh — with the tell-tale double slash — means a variable rendered empty, not that a file is missing. Read the itemised lines first and most Ansible errors stop being mysterious.

</aside>

fatal: [web01]: FAILED! => {
    "changed": false,
    "cmd": "/opt/deploy.sh",
    "msg": "[Errno 2] No such file or directory: b'/opt/deploy.sh'",
    "rc": 2,
    "stderr": "",
    "stdout": ""
}
Field Read it for
msg ⭐ The actual reason. Read this before anything else
rc Exit code — command/shell only
stderr / stderr_lines ⭐ What the command itself complained about
cmd ⭐ The command after templating — often reveals an empty variable
invocation.module_args ⭐ The resolved arguments. Where templating mistakes become visible

<aside> 🎯

cmd and invocation.module_args are the fields people skip and should not. They show the values after Jinja2 rendering. A task that fails with "no such file" and a cmd of /opt//deploy.sh tells you instantly that a variable rendered empty — which is a variable problem, not a file problem.

</aside>

🧪 Exercise A2.1 — Produce all four failure types deliberately

# 1 - PARSE: missing space after the colon
printf -- '---\n- hosts: all\n  tasks:\n    - debug:\n        msg:broken\n' > f1.yml
ansible-playbook f1.yml 2>&1 | head -12

# 2 - CONNECTION: a host that does not exist
ansible-playbook -i 'nosuchhost.invalid,' -m ping all 2>&1 | head -6

# 3 - EXECUTION: a command that is not there
ansible localhost -m ansible.builtin.command -a "/opt/definitely-missing.sh" 2>&1 | head -8

# 4 - LOGIC: green run, wrong answer
cat > f4.yml <<'EOF'
---
- hosts: localhost
  gather_facts: false
  vars:
    should_run: "false"
  tasks:
    - name: This runs, and it should not
      ansible.builtin.debug: {msg: "I ran"}
      when: should_run
EOF
ansible-playbook f4.yml

🎯 Interview questions — Classifying failures