← All TILs · ansible

Hosts and groups: the two groups every inventory has, all and ungrouped

ansible - 2026-10-03

First entry in the Ansible inventory from scratch series. Ansible runs tasks on remote machines, its managed hosts, from one machine, the controller, where ansible-playbook runs. It finds the hosts in the inventory: a list of host names, sorted into named groups, that a play targets with hosts:. This entry writes a small inventory in YAML and shows what Ansible builds from it, including two groups nobody wrote: all and ungrouped. Everything below ran with ansible-core 2.21.4.

The inventory

A PostgreSQL server, three application servers, and a jump host, the machine people connect through to reach the others:

all:
  hosts:
    jump1:              # in no group of its own
  children:
    app:
      hosts:
        app1:
        app2:
        app3:
    postgresql:         # a parent group: every host of db is in it too
      children:
        db:
          hosts:
            db1:
    backup:             # db1 again: a host can be in several groups
      hosts:
        db1:

The YAML format nests three keys:

The file, inventory/hosts.yml, holds no variables at all. The connection settings sit in inventory/group_vars/all/, the files Ansible reads for every host of the group all; later entries cover variables.

What Ansible builds from it

ansible-inventory --graph prints the groups as a tree:

@all:
  |--@ungrouped:
  |  |--jump1
  |--@app:
  |  |--app1
  |  |--app2
  |  |--app3
  |--@postgresql:
  |  |--@db:
  |  |  |--db1
  |--@backup:
  |  |--db1

A host listed directly under all, in no other group, went into ungrouped, a group the file never names. db1 appears twice, once per group; it's still one host.

groups and group_names

Two magic variables, which Ansible sets itself, give the same information to a playbook:

From the example's playbook, which writes both out:

groups:
  all: ['jump1', 'app1', 'app2', 'app3', 'db1']
  app: ['app1', 'app2', 'app3']
  backup: ['db1']
  db: ['db1']
  postgresql: ['db1']
  ungrouped: ['jump1']
group_names, per host:
  app1: ['app']
  db1: ['backup', 'db', 'postgresql']
  jump1: ['ungrouped']

(app2 and app3 are like app1.) Three things show:

localhost is not in all

The playbook runs on localhost, the controller itself, and the inventory doesn't define it. Ansible then creates an implicit localhost (the guide's Implicit 'localhost' page): a host that works with hosts: localhost and delegate_to: localhost, but isn't in any group. groups['all'] above lists five hosts, not six, and hosts: all doesn't run on the controller. Defining localhost in the inventory makes it an ordinary host, in all and ungrouped like jump1, and drops the implicit behaviour.

In short

The example repository

The series' companion repository, abdelhousni/ansible-inventory-series, holds this inventory, with every host connecting locally so that nothing needs a real server. run.sh prints the graph and runs groups.yml, which writes groups and each host's group_names to its out/ directory. Its CI runs it on every push and compares the output with the expected one.

Sources

Created 2026-10-03T04:00:07+02:00 · Edit