Skip to main content
RunBook Academy

AnsibleV · Inventory Design at Fleet ScaleFleet taxonomy

Reading an inventory as a blast-radius map

Advanced⏱ ~17 minansible-corepython3

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

Not yet marked complete on this device.

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:

  1. What does hosts: all actually reach? A number, and a distribution.
  2. Which single group is the largest? That is where a fleet-wide mistake lands.
  3. 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:

Read-only / Safethe fleet-wide number
$ 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.com

Six 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:

Read-only / Safegroup sizes, descending
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.

Read-only / Safeplay targets, ranked
grep -rhoE '^\s+hosts:\s+.*' --include='*.yml' playbooks/ roles/ \
| sed -E 's/^\s+hosts:\s+//' | sort | uniq -c | sort -rn

A 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:

Read-only / Safeper-play blast radius
$ ansible-playbook -i inventory/production site.yml --list-hosts
playbook: 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.com

This 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:

Read-only / Safewhat would run
$ ansible-playbook -i inventory/production site.yml --list-tasks
playbook: 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

  1. Q1. Which command gives the authoritative per-play host list for an unfamiliar playbook without touching any managed node?

  2. 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.

  3. 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.

  4. 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.