AnsibleIV · Inventory FundamentalsInventory mechanics
Groups, children, and the two groups you never wrote
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
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
$ ansible-inventory -i inventory/hosts.ini --graph@all:
|--@ungrouped:
| |--jump01.example.com
|--@webservers:
| |--web01.example.comjump01.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:
$ 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.comThe word production appears exactly once in that file, and no host is
listed under it. Yet:
$ ansible -i inventory/hosts.yml production --list-hosts hosts (4):
web01.example.com
web02.example.com
db01.example.com
db02.example.comAnd the same fact from the host’s point of view:
$ ansible -i inventory/hosts.yml web01.example.com -m debug -a 'var=group_names' --connection=localweb01.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.
ansible -i inventory/hosts.yml production --list-hosts
ansible -i inventory/hosts.yml web01.example.com \
-m debug -a 'var=group_names' --connection=localUse 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:
$ 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.comThe 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
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?
Q2. The `group_names` magic variable for a host includes `all`.
Q3. Which of these are true of the `ungrouped` implicit group? Select all that apply.
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.