Finding Out What a Variable Actually Is
Scenario: A template renders port = with nothing after it. The variable is defined - in a file that this play never loads.
New words, in plain English
debugmodule - prints a message or a variable's value during a run.var:vsmsg:-var:takes a bare variable NAME;msg:takes a string you can interpolate into.- Undefined - the variable has no value at all. Referencing it is an error unless a default is supplied.
| default()- the filter that supplies a fallback for an undefined variable.
When a value is wrong, guessing is slow. Three tools answer it directly.
ansible-inventory --host <name> prints every variable that host gets from the inventory - group vars, host vars, and inline inventory settings. It cannot see play vars, role vars or extra vars, which is precisely what makes it useful: if the value is right here but wrong in the run, something above the inventory is overriding it.
A debug task prints the fully-resolved value at exactly the point the play would use it. Use var: app_port for a bare name (note: no braces) or msg: "port is {{ app_port }}" to build a sentence. var: hostvars[inventory_hostname] dumps everything that host can see, which is a lot, but sometimes exactly what you need.
| default() is how you stop the run failing on a genuinely optional value. {{ retries | default(3) }} supplies a fallback. {{ upstream | default('', true) }} treats an empty string as absent too. And when a value is genuinely required, {{ token | mandatory }} fails loudly with a clear message rather than rendering an empty string into a config file.
Analogy: Debugging variables by reading files is searching a library for the book someone is currently holding. debug asks the person directly: what is in your hand, right now?
A worked example
- name: What is this host actually going to use?
ansible.builtin.debug:
var: app_port # bare name - NO braces
- name: Same thing, as a sentence
ansible.builtin.debug:
msg: "{{ inventory_hostname }} -> {{ app_port }} ({{ app_env }})"
- name: Everything this host can see (verbose but definitive)
ansible.builtin.debug:
var: hostvars[inventory_hostname]
when: debug_dump | default(false)
# Supplying fallbacks in a template
# {{ retries | default(3) }} -> 3 when undefined
# {{ upstream | default('', true) }} -> also when empty
# {{ api_token | mandatory }} -> fail loudly if missing
There is a related class of bug worth naming: a template that renders an empty value instead of failing. {{ port }} on an undefined variable raises an error, which is good - you find out immediately. But {{ port | default('') }} silently writes nothing, and you discover it when the service will not start. Use default() for values that are genuinely optional, and let required values fail.
Finally, remember which layer a value should have come from. If debug shows the right value but the wrong layer supplied it, the immediate bug is fixed and the structural bug is not - and it will resurface the next time someone adds a host.
Warning:var:takes a bare variable name. Writingvar: "{{ app_port }}"resolves the variable and then tries to look up a variable named after its value, which produces a confusing error.
Goal: Put this to work in the ansible-template-defaults lab. Open/labs/ansible, pickansible-template-defaults, and fix the real broken project - Ansible really does SSH into four managed hosts and converge them.