Ansible Guide
Flat isometric illustration of a pink server cabinet beside a blue grid platform holding twelve glowing pink cylinder nodes wired into a branching tree.
Inventory

Ansible Inventory: Groups, Variables, and Patterns

How to structure an Ansible inventory: group hierarchies, where variables belong, how same-level groups merge, and the pattern syntax that scopes a run.

By Ansible Guide Editorial · · 6 min read

Inventory is where an Ansible estate is either legible or not. It answers two questions that every run depends on: which machines exist, and what is true about each of them. Get the structure right and playbooks stay short, because a play can name a group and inherit everything else. Get it wrong and the playbooks fill up with conditionals that test hostnames, which is the classic sign that inventory is doing too little work.

Formats and sources

An inventory can be written in INI or YAML. Both express the same thing: hosts, groups, and variables attached to either. A single -i flag points at one source, the flag can be repeated, and it also accepts a directory, in which case every file inside is parsed and merged. That is how most real estates are organised: a dynamic cloud source for the machines that come and go, plus a small static file for the fixed ones and for group definitions.

Two groups always exist without being declared. all contains every host in the inventory. ungrouped contains every host that belongs to no other group, which makes it a useful audit target: hosts appearing there are usually the ones nobody has classified yet.

Host ranges avoid repetition. A pattern like web[01:50].example.com in an inventory file expands to fifty hosts, and db-[a:f].example.com expands alphabetically. Nesting is expressed with children, so a production group can contain web, db and cache without listing a single host itself.

Connection details belong in inventory, not in plays

Behavioural inventory parameters tell Ansible how to reach a host, and they are ordinary variables:

  • ansible_host when the inventory name is not resolvable, or when a machine is reached through a different address than it is called by.
  • ansible_port for a non-standard SSH port.
  • ansible_user for the login account.
  • ansible_connection to select a connection plugin, such as local for the control node.
  • ansible_python_interpreter when discovery picks the wrong interpreter, which is common on minimal or mixed-distribution images.

Putting these in inventory rather than in the play keeps the play portable. A play that hardcodes a user cannot be reused against an estate with a different convention.

group_vars and host_vars

Variables can be written inline in the inventory, but past a handful they belong in group_vars/ and host_vars/ directories placed next to the inventory file or next to the playbook. A file named group_vars/webservers.yml applies to every host in webservers; host_vars/web01.example.com.yml applies to one machine. Either can be a directory instead of a file, so group_vars/webservers/ holding tuning.yml and packages.yml splits a large set of variables into readable pieces.

The convention that scales is to keep defaults in group_vars/all, override them in progressively more specific groups, and use host_vars only for genuine per-machine facts such as an address or a disk layout. A host_vars file full of settings that ought to describe a role is a sign that a group is missing.

How variables merge, and the rule that catches people

Before a play runs, Ansible flattens variables down to each host. The documented order of precedence for inventory entities, lowest to highest, is: all, then the parent group, then the child group, then the host. More specific wins, which is what most people expect.

The part that is not obvious concerns groups at the same level. Ansible merges same-level groups in alphabetical order, and variables from the last group loaded overwrite those from earlier ones. If a_group and b_group both set testvar, the host ends up with b_group’s value purely because of the letter it starts with. That is a fragile thing to depend on, and renaming a group can silently change a value.

The documented fix is ansible_group_priority. It overrides alphabetical sorting for groups at the same level after the parent and child order is resolved. A larger number means the group is merged later and therefore wins, and the default is 1 when it is not set. Two constraints are worth memorising: it only affects groups at the same level, and it can only be set in an inventory source, not in a group_vars/ file, because Ansible reads it while loading group_vars/.

There is one more default to know about. Duplicate dictionary variables are overwritten rather than merged. The configuration setting that changes this exists but is a global behaviour change with wide blast radius; the safer pattern is to leave it alone and merge explicitly in a template or task with the combine filter, so the merge is visible where it happens.

Command-line --extra-vars sit above all of this and override everything, which makes them useful for a one-off and dangerous as a habit. The full flag set is covered in ansible-playbook options.

Patterns: scoping a run

A pattern is how a play or an ad hoc command names its targets. It is the content of the hosts: line, the unflagged argument to ansible, and the argument to --limit. The documented operators are small enough to memorise:

  • all or * targets everything.
  • host1:host2 or host1,host2 targets a list. Comma is preferred when ranges or IPv6 addresses are involved, since colons are ambiguous there.
  • webservers:dbservers is a union: every host in either group.
  • webservers:!atlanta excludes, targeting hosts in webservers that are not in atlanta.
  • webservers:&staging intersects, targeting only hosts in both.

These combine. The documentation’s own example, webservers:dbservers:&staging:!phoenix, targets machines in webservers or dbservers that are also in staging, excluding anything in phoenix. Wildcards work against FQDNs and IP addresses when hosts are named that way in inventory, so 192.0.* and *.example.com are valid, and wildcards can be mixed with group names.

Design groups so patterns do the combining

Intersection is the reason to model an estate along several independent axes rather than one. Give each host a function group, an environment group and a location group, and any combination is addressable without inventing a new group:

--limit 'webservers:&production:&eu-west'

The alternative, a group per combination such as prod-web-eu, doubles in size every time an axis is added and drifts out of date the moment a machine moves. Independent axes also make variable placement obvious: tuning belongs to the function group, credentials and endpoints to the environment group, and mirrors or time zones to the location group.

Verify inventory instead of assuming it

Inventory bugs are quiet. A misplaced host does not throw an error; it just receives the wrong configuration. Two commands make the structure visible:

  • ansible-inventory --graph prints the group tree with host membership, and adding --vars shows the variables resolved onto each entry. --list produces the full JSON, and --host <name> shows what one machine ends up with after every merge.
  • ansible-playbook --list-hosts resolves the pattern a play would actually run against, without running it.

Running the graph command after any inventory change, especially one that renames or reparents a group, catches the alphabetical merge problem before a playbook does.

Dynamic inventory

Inventory plugins generate hosts from a source of truth rather than a file: cloud provider APIs, a container platform, or a CMDB. They are configured with a small YAML file that the -i flag points at, and they can be mixed with static files in the same inventory directory.

The constructed plugin is the piece that makes dynamic inventory usable, because raw cloud output rarely has the groups you want. It builds groups from existing facts and tags, so instances carrying a role tag become function groups automatically. That keeps the estate self-classifying: a new machine tagged correctly at build time lands in the right groups without an inventory edit.

Common mistakes

Writing conditionals in tasks that test hostnames instead of creating a group. Depending on the alphabetical merge of two same-level groups without setting ansible_group_priority. Setting ansible_group_priority in group_vars/, where it has no effect. Filling host_vars with settings that describe a role. Putting connection details such as ansible_user in plays, making them non-portable. Building combinatorial groups like prod-web-eu instead of intersecting independent axes. Changing group names without re-running the inventory graph.

Once scope is right, the next two questions are how many hosts run at once and in what order. Concurrency and idempotence are covered in how Ansible runs a playbook, execution ordering in linear vs free vs host_pinned, and the inventory sizing calculator models what a given host count implies for forks and runtime.

Sources

  1. Ansible documentation: How to build your inventory
  2. Ansible documentation: Patterns: targeting hosts and groups
  3. Ansible documentation: Using Variables
  4. Ansible documentation: ansible-inventory command line tool

Related