<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>
<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 |
<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