The first time I saw hostvars inside a Jinja expression I thought Ansible had become much cleverer than I was. It was useful, but it also made the playbook harder to read. After a few such expressions, I went back and learned what Ansible calls magic variables.
They are variables Ansible creates to describe the current run. They let one playbook adapt to its inventory, but they can also hide the important part of the design if used without restraint.
The current host#
These variables answer basic identity questions:
- name: Show the current host
ansible.builtin.debug:
msg:
- "inventory name: {{ inventory_hostname }}"
- "short name: {{ ansible_hostname | default('not gathered') }}"
- "groups: {{ group_names }}"Illustrative output:
TASK [Show the current host] ****************************************
ok: [web01] => {
"msg": [
"inventory name: web01",
"short name: web01",
"groups: ['web']"
]
}inventory_hostname is the name used in the inventory. ansible_hostname is a fact gathered from the remote machine, so it is unavailable until facts have been gathered.
Groups and hostvars#
groups contains the hosts in each inventory group. hostvars lets a task inspect variables belonging to another host:
- name: Show the broker addresses
ansible.builtin.debug:
msg: "{{ groups['kafka_brokers'] | map('extract', hostvars, 'ansible_host') | list }}"
run_once: trueThis is useful for building a configuration file from a cluster inventory. It also creates a dependency on variable availability. I learned to check that dependency early. If ansible_host is not defined, use a deliberate fallback or fail with a useful message rather than producing a half-correct configuration.
Facts and fact caching#
Facts are gathered at the start of a play by default:
- name: Gather platform details
hosts: all
gather_facts: true
tasks:
- name: Show operating system
ansible.builtin.debug:
var: ansible_facts.distributionFor a play that only manages a known service, disabling fact gathering can reduce runtime:
gather_facts: falseDo this only when no task or role needs facts. Fact caching can speed up multi-play workflows, but cached information has a freshness problem. Treat it as data with a lifecycle, not as an always-current source of truth.
Avoid clever templates#
This is compact but hard to review:
{{ hostvars[groups['kafka_brokers'][0]]['ansible_host'] }}Prefer a named variable or a preparatory task:
- name: Select the first broker
ansible.builtin.set_fact:
seed_broker: "{{ hostvars[groups['kafka_brokers'][0]].ansible_host }}"
run_once: trueDocument why the first host is a valid seed. Magic variables should provide context, not conceal architecture.
A useful rule#
Use ordinary variables for decisions that belong to your application. Use magic variables for facts about the Ansible run and inventory. When a template needs several nested lookups, pause and ask whether a clearer inventory variable, group, or role interface would communicate the intent better.

