AnsibleV · Inventory Design at Fleet ScaleFleet taxonomy
Reading an inventory as a blast-radius map
What you'll learn
- Answer the three blast-radius questions about an unfamiliar repository from read-only commands
- Use ansible-playbook --list-hosts to get per-play host counts before any run
- Rank inventory groups by size to find where a fleet-wide mistake would land
- Treat an inventory nobody can quickly characterise as a defect to be fixed
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
You have been handed an Ansible repository you did not write. It might be a code review, a new job, a due-diligence exercise, or an incident at 02:00 where the only person who understands it is unreachable.
Three questions tell you most of what you need, and all three are answerable from read-only commands in about five minutes:
- What does
hosts: allactually reach? A number, and a distribution. - Which single group is the largest? That is where a fleet-wide mistake lands.
- Which group is named in the most plays? That is the group with the most ways to be changed.
If the repository was designed well, these are obvious. If it takes an afternoon, that is the finding — and it is a finding about safety, not about tidiness.
Question one: what does all reach?
Start with the count, because the count is the shape of the problem:
$ ansible -i inventory/production all --list-hosts hosts (6):
db-a1.example.com
db-a2.example.com
web-a1.example.com
web-a4.example.com
web-a2.example.com
web-a3.example.comSix is a number you can reason about. Six hundred is a number that tells
you every play in this repository targeting all is a fleet-wide change,
and that you should find out how many of them there are.
The order is not sorted and does not indicate execution order — the
default linear strategy runs a task across hosts in parallel batches
governed by forks, which the configuration part covers.
Question two: which group is the largest?
Group sizes tell you where the damage would be. A single command gets you the ranking:
ansible-inventory -i inventory/production --list | python3 -c '
import json, sys
data = json.load(sys.stdin)
rows = [(len(v.get("hosts", [])), k)
for k, v in data.items()
if k != "_meta" and "hosts" in v]
for count, name in sorted(rows, reverse=True):
print(f"{count:5d} {name}")' 6 production
5 live
4 web
2 database
1 draining
Two things to read here, neither of which is the biggest number.
Read the ratio. production at 6 and web at 4 means the role
families partition the fleet reasonably. A repository where the largest
role group holds 95% of the fleet does not have role groups; it has one
group and some rounding.
Read the absences. Groups you expected and did not find are the interesting entries. No lifecycle family means there is no way to say “skip the host that is draining”. No exception groups means exceptions live somewhere less visible.
Question three: which group is targeted most?
The inventory tells you what exists. The playbooks tell you what gets used, and the gap between them is where the surprises are.
grep -rhoE '^\s+hosts:\s+.*' --include='*.yml' playbooks/ roles/ \
| sed -E 's/^\s+hosts:\s+//' | sort | uniq -c | sort -rnA group appearing in twenty plays is a group whose behaviour is defined in twenty places, which is a maintenance question. A group appearing in no play is either a variable carrier or a leftover.
But the authoritative answer for any single playbook comes from Ansible itself, and it resolves patterns rather than matching text:
$ ansible-playbook -i inventory/production site.yml --list-hostsplaybook: site.yml
play #1 (all): Baseline every managed host TAGS: []
pattern: ['all']
hosts (6):
db-a1.example.com
db-a2.example.com
web-a1.example.com
web-a4.example.com
web-a2.example.com
web-a3.example.com
play #2 (web:&live): Configure the web tier TAGS: []
pattern: ['web:&live']
hosts (3):
web-a2.example.com
web-a3.example.com
web-a1.example.comThis is the single most useful command in the lesson. It gives you, per play, the pattern as written and the hosts it resolves to right now against this inventory. Run it before every unfamiliar playbook, and run it in CI so that a pattern change shows up as a host-count change in the diff.
Pair it with --list-tasks to see what those hosts would have done:
$ ansible-playbook -i inventory/production site.yml --list-tasksplaybook: site.yml
play #1 (all): Baseline every managed host TAGS: []
tasks:
placeholder TAGS: []
play #2 (web:&live): Configure the web tier TAGS: []
tasks:
placeholder TAGS: []Design so the answers are obvious
The three questions are a review technique. They are also a design target: an inventory should be built so that the answers do not have to be derived.
Make group sizes visible in the layout. One file per environment, one group per section, membership grouped by family with comment headers. A reviewer scrolling the file should see the shape without running anything.
Do not compute membership where a reader cannot see it. A dynamic
inventory or a heavily templated static one can be correct and still be
unreadable, because the file no longer shows who is in what. That is a
real trade-off — dynamic inventory has its own part in this course — and
the mitigation is the same either way: commit a periodically refreshed
ansible-inventory --graph snapshot so the shape is reviewable even when
the source is generated.
Keep hosts: all rare and deliberate. Every play targeting all is
a fleet-wide change by construction. Some genuinely should be — a baseline
role, an inventory audit — and those should say so in the play name. A
play called “Configure the web tier” that targets all is a defect
regardless of whether it currently does the right thing.
Name plays after their blast radius. “Baseline every managed host” and “Configure the web tier” tell a reader the intended scope, so a mismatch between the name and the pattern is visible in review. “site.yml” and “main play” tell them nothing.
A five-minute assessment, in order
1. ansible --version
Which config file is in effect? (Part VI explains why this is first.)
2. ansible-inventory -i <source> --graph
What groups exist? Is ungrouped empty?
3. ansible -i <source> all --list-hosts | head -1
How many hosts does a fleet-wide play reach?
4. <the group-size ranking above>
Where would a mistake land?
5. ansible-playbook -i <source> <playbook> --list-hosts
Per play: what is the pattern, and what does it resolve to?
6. ansible-playbook -i <source> <playbook> --list-tasks
What would those hosts do?
Every step is read-only and none of them connect to a managed node. That property is what makes this usable during an incident rather than only during a review.
Knowledge check
Knowledge check · 4 questions
Q1. Which command gives the authoritative per-play host list for an unfamiliar playbook without touching any managed node?
Q2. A host listed directly under hosts: at the top of an inventory file, outside every children: block, will be reached by a play targeting all but by no role-based or environment-based play.
Q3. A group-size ranking shows production 6, live 5, web 4, database 2, draining 1. Which observations are worth recording? Select all that apply.
Q4. Reviewing an unfamiliar repository, you find a play named "Configure the web tier" whose pattern is hosts: all. It currently behaves correctly because every host in the inventory happens to be a web server. What is the right assessment?
Passing score: 75%. Answers are checked in this browser.