In an Ansible play running against your web servers, how do you use a value belonging to a different host — say the IP address of a database server in another group — and what has to be true for that to work?
answer
- a dictionary keyed by inventory name
- groups gives you the member list
- inventory vars always present, facts not
- --limit is how it breaks in CI
- prefer a declared address over a discovered one
basics
~20 sRead it through the hostvars magic variable, indexed by the other host's inventory name, and find the hosts with groups. Inventory-defined values are always there, but another host's facts only exist if that host was played in this run or its facts are cached.
solid answer
~50 sYou go through `hostvars`, a dictionary of every host's variables keyed by inventory name: `hostvars['db1']['ansible_default_ipv4']['address']`. To avoid hardcoding a name, combine it with `groups`, which maps each inventory group to its list of members, and loop `groups['db']` inside the task or template. The catch is what is actually in `hostvars` at that moment. Inventory-supplied variables — anything from `group_vars` or `host_vars` — are present for every host in the inventory from the start. *Facts* are only present for hosts that Ansible has actually gathered from in this run, or whose facts come from the fact cache. So a play run with `--limit web` will happily render an undefined database address, because db1 was never touched. The fixes are to include the db hosts in an earlier play (even one that only gathers facts), enable fact caching, or use an inventory variable rather than a fact for the address.
code
jinja · 8 linesupstream app_db {
{% for host in groups['db'] %}
server {{ hostvars[host]['ansible_default_ipv4']['address'] }}:5432;
{% endfor %}
}
# rendered on behalf of {{ inventory_hostname }}
# {{ ansible_managed }}go deeper
Know that hostvars holds every host's variables keyed by inventory name, and that groups gives you the list of hosts in a group so you can loop over them in a template.
Explain the difference between inventory-supplied variables, which are always in hostvars, and facts, which only appear once that host has been gathered — and name inventory_hostname versus ansible_host.
Show that you have hit this in production: the --limit run that renders a blank backend list, the choice between gathering the other group first, caching facts, or declaring the address in inventory, and asserting rather than rendering something wrong.
Own the estate rule for cross-host coupling — which facts a play may depend on across groups, whether service discovery belongs in the config-management run at all, and what the fallback is when a dependency group is out of scope.
## The magic variables involved Ansible injects several variables that describe the run itself rather than the machine: - `inventory_hostname` — the name of the host the current task is executing for, as written in the inventory. Not the machine's own hostname (that is the `ansible_hostname` fact) and not the address used to connect (that is `ansible_host`). - `hostvars` — a dictionary of every inventory host's variables, keyed by inventory name. - `groups` — a dictionary mapping every group name to the list of hosts in it. - `group_names` — the list of groups the *current* host belongs to. - `ansible_play_hosts` — the hosts still active in the current play (those that have not failed or been skipped). Together these are how one host's task reads another host's data. The canonical shape: ```yaml - name: Point the app at the database ansible.builtin.template: src: app.conf.j2 dest: /etc/app/app.conf ``` and in `app.conf.j2`: ```jinja {% for host in groups['db'] %} server {{ hostvars[host]['ansible_default_ipv4']['address'] }}:5432 {% endfor %} ``` That pattern — iterate a group, index `hostvars`, pull an address — is how load-balancer configs, cluster member lists and `/etc/hosts` files are generated across the whole industry. ## What is actually in hostvars, and when This is where the interview lives. `hostvars` contains, for every host in the inventory: - everything inventory supplied — `group_vars`, `host_vars`, inventory-file variables — from the very beginning of the run; and - that host's **facts**, only once they have been gathered in this run, or restored from the fact cache. So `hostvars['db1']['db_port']` (set in `group_vars/db.yml`) works even if db1 is never touched, while `hostvars['db1']['ansible_default_ipv4']` is undefined until db1 has been gathered. The failure is intermittent in exactly the way that makes it hard to debug: it works when someone runs the whole playbook and breaks when CI runs `--limit web`, or when the db play was skipped, or when the db host failed earlier in the run. Remedies, in the order I would reach for them: 1. **Gather from the other group first.** A cheap first play does it: ```yaml - hosts: db gather_facts: yes tasks: [] - hosts: web tasks: - ansible.builtin.debug: msg: "{{ hostvars[groups['db'][0]].ansible_default_ipv4.address }}" ``` This is explicit and reviewable, but it still breaks under a `--limit` that excludes db. 2. **Enable fact caching**, so previously gathered facts are available without touching the host — at the cost of staleness, which for an IP address is a real risk after a rebuild. 3. **Do not use a fact at all.** If the address is something you decided rather than discovered, put it in `group_vars/db.yml` or use `ansible_host` from inventory. Inventory data is always in `hostvars`, is under version control, and cannot be stale in the way a cached fact can. This is usually the right answer and the one that separates a candidate who has operated this from one who has only read about it. ## Delegation is the other direction `hostvars` reads another host's data while executing locally. When you need to *act* on another host mid-play — register a backend with a load balancer, add a DNS record, take a node out of rotation — that is `delegate_to`, and `delegate_facts: true` if the resulting facts should be stored against the delegated host rather than the current one. Mentioning the pair shows you know reading and acting are different problems. ## Practical hygiene - Guard the lookup: `hostvars[h].ansible_default_ipv4.address | default('')` inside a template turns a hard failure into a visibly wrong file — sometimes better, sometimes worse, so decide deliberately. Asserting early is often the better choice: fail the play with a clear message if the group is empty or the fact is missing, rather than rendering a config with a blank server address. - Remember that `groups['db']` is ordered by inventory, and taking `[0]` silently designates a primary. If you mean "the primary", say so with a group or a variable rather than relying on file order. - Both bracket and dot access work — `hostvars['db1']['x']` and `hostvars.db1.x` — but bracket form is mandatory when the host name contains a dash or a dot, which most real inventory names do.
- The template works locally but renders an empty server list in CI. What do you check first?The host selection. CI almost certainly runs with `--limit` or a narrower `hosts:` pattern, so the database hosts were never played and their facts were never gathered — `hostvars` still lists them, but without the ansible_ fact keys. Confirm with a debug of `groups['db']` and of one hostvars entry, then either gather from that group first or stop depending on a fact.
- What is the difference between inventory_hostname, ansible_host and ansible_hostname?`inventory_hostname` is the name as written in the inventory and is what indexes `hostvars`. `ansible_host` is the address Ansible actually connects to, which may differ. `ansible_hostname` is a gathered fact reporting what the machine calls itself. Mixing them up is a common source of lookups that quietly return nothing.
- When would you use delegate_to instead of reading hostvars?When you need to perform an action somewhere else rather than read a value. Registering the current host with a load balancer, or adding a DNS record, is a task that must run against another endpoint while logically belonging to this host's play — that is delegate_to, with delegate_facts if the returned facts should be stored against the delegated host.
saying these in an interview costs you the question
- Assumes hostvars always contains other hosts' facts
- Confuses inventory_hostname with ansible_hostname
- Thinks group_names lists all inventory groups
- Takes groups['db'][0] as a guaranteed primary
- Believes --limit only affects which hosts are changed