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

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. Writing var: "{{ 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, pick ansible-template-defaults, and fix the real broken project - Ansible really does SSH into four managed hosts and converge them.