Skip to main content
RunBook Academy

AnsibleIV · Inventory FundamentalsInventory mechanics

Groups, children, and the two groups you never wrote

Foundation⏱ ~20 minbash

What you'll learn

  • Name the two implicit groups and state which hosts are in each
  • Explain upward membership inheritance through children and its effect on targeting
  • Predict which hosts a pattern reaches in a nested group tree, then verify it
  • Distinguish the groups magic variable from group_names and use each correctly

Prerequisites

Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11

Not yet marked complete on this device.

Every host in every Ansible inventory is in at least two groups, and one of them you did not write.

That sounds like trivia. It is the reason hosts: all is the most dangerous line in the language, and it is the reason a play targeting production can change a machine whose name appears nowhere near the word.

The two groups nobody writes

all contains every host in the inventory, without exception. There is no way to exclude a host from it, no flag, no variable. A host that exists is in all.

ungrouped contains every host that belongs to no other group. Its membership shrinks as you organise the inventory, and it is a genuinely useful signal: a host in ungrouped is a host nobody has classified.

Here is an inventory with one grouped host and one loose one:

# inventory/hosts.ini
jump01.example.com

[webservers]
web01.example.com
Read-only / Safethe group tree, including what you did not declare
$ ansible-inventory -i inventory/hosts.ini --graph
@all:
|--@ungrouped:
|  |--jump01.example.com
|--@webservers:
|  |--web01.example.com

jump01.example.com was never assigned anywhere, so it landed in ungrouped. It is nonetheless in all, and therefore a play with hosts: all reaches it.

Membership inherits upward

children nests one group inside another. The consequence that matters is directional, and it is the opposite of what people expect from directory trees: a host belongs to its group’s parents, all the way up.

Take the fleet from the previous lesson:

# inventory/hosts.yml
all:
  children:
    cache:
      hosts:
        cache01.example.com:
    production:
      children:
        webservers:
          hosts:
            web01.example.com:
            web02.example.com:
        databases:
          hosts:
            db01.example.com:
            db02.example.com:
Read-only / Safethe nested tree
$ ansible-inventory -i inventory/hosts.yml --graph
@all:
|--@ungrouped:
|--@cache:
|  |--cache01.example.com
|--@production:
|  |--@webservers:
|  |  |--web01.example.com
|  |  |--web02.example.com
|  |--@databases:
|  |  |--db01.example.com
|  |  |--db02.example.com

The word production appears exactly once in that file, and no host is listed under it. Yet:

Read-only / Safewho does production actually reach?
$ ansible -i inventory/hosts.yml production --list-hosts
  hosts (4):
  web01.example.com
  web02.example.com
  db01.example.com
  db02.example.com

And the same fact from the host’s point of view:

Read-only / Safewhich groups does this host think it is in?
$ ansible -i inventory/hosts.yml web01.example.com -m debug -a 'var=group_names' --connection=local
web01.example.com | SUCCESS => {
  "group_names": [
      "production",
      "webservers"
  ]
}

web01 is in production because webservers is, and web01 is in webservers.

Reading membership from either end

Two commands, two directions, and knowing which one to reach for is half the skill.

Read-only / Safefrom the group down, and from the host up
ansible -i inventory/hosts.yml production --list-hosts
ansible -i inventory/hosts.yml web01.example.com \
  -m debug -a 'var=group_names' --connection=local

Use the first before you run anything: it answers “how big is this change”. Use the second when a host received a variable you did not expect: it answers “where could that have come from”.

A group with no hosts is still a group

Declaring a group and leaving it empty is legal, and it is often the right thing to do. An empty maintenance_hold group that a play excludes is a working control from the day it exists, even before any host is in it — and adding a host to it later is a one-line, reviewable change rather than an edit to a play.

The cost is that an empty group is invisible in a pattern. hosts: maintenance_hold matches nothing and the run exits successfully having done nothing, which is fine when that is what you meant and confusing when it is not.

Groups you cannot name

Valid group names are letters, digits and underscores. No hyphens, no dots, no spaces. Host names have no such restriction — web-01.example.com is a perfectly ordinary host — so this catches people exactly once, while writing their first group.

What is easy to miss is that Ansible does not refuse an invalid name. By default it warns and creates the group anyway:

Read-only / Safean inventory declaring [web-servers] and [eu.west]
$ ansible-inventory -i inventory/hosts.ini --graph
[WARNING]: Invalid characters were found in group names but not replaced, use -vvvv to see details
@all:
|--@ungrouped:
|--@web-servers:
|  |--web01.example.com
|--@eu.west:
|  |--web02.example.com

The behaviour is controlled by force_valid_group_names in ansible.cfg (ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS in the environment), which defaults to never — allow the name, warn about it. Setting it to always replaces the offending characters with underscores instead, which is the more useful choice for a dynamic inventory whose group names come from cloud tags you do not control.

The reason to care is Jinja. A group named eu.west cannot be written as groups.eu.west in a template, because the dot means attribute access; you need groups['eu.west']. A name with a hyphen has the same problem for the same reason. The inventory parses, the graph looks fine, and the failure appears later in a template that reads like a typo.

Use web_servers and eu_west. The naming conventions that survive at fleet scale are the subject of the inventory design part; this is only the syntactic floor.

Knowledge check

Knowledge check · 4 questions

  1. Q1. An inventory declares a group `production` with a `children` entry naming `webservers`, and lists no hosts under `production` itself. What does `hosts: production` reach?

  2. Q2. The `group_names` magic variable for a host includes `all`.

  3. Q3. Which of these are true of the `ungrouped` implicit group? Select all that apply.

  4. Q4. A play targeting `monitoring_target` unexpectedly restarted a service on your web servers. What is the most likely explanation?

Passing score: 75%. Answers are checked in this browser.