Runbook: Configure a new inventory
1 · Prerequisites
Confirm every item is in place before any state change.
2 · Pre-checks
Read-only diagnostic commands. If any of these don't match expected output, stop and investigate further.
- · The source of truth for this host list is named, and it is a system rather than a person
- · The environment this inventory represents is decided, and it is exactly one environment
- · The group naming convention in use elsewhere in the repository has been read, not guessed
- · Whether any host in the list already appears in another inventory has been checked
- · A non-production target exists in the list that can absorb the first real run
3 · Procedure
Execute each step in order. Verify the expected output of a step before moving to the next.
- 1Create the inventory directory with the standard layout and an empty group_vars tree
- 2Write the host and group definitions, one environment per inventory root
- 3Render the inventory and read the graph: groups, membership, host counts
- 4Resolve variables per host and confirm each value comes from the layer you intended
- 5Check for hosts that appear in more than one group whose variables conflict
- 6Run --list-hosts for every play that will use this inventory and compare counts against the source of truth
- 7Prove reachability with a connecting module, not with debug
- 8Run one playbook in check mode against a single host from the new inventory
- 9Commit the inventory, open a review, and record the source-of-truth reconciliation in the change record
4 · Verification
Confirm the procedure actually fixed the problem.
- ✓ansible-inventory --graph shows every expected group, and no unexpected group
- ✓The total host count matches the source of truth exactly, and any difference is explained in writing
- ✓ansible-inventory --host returns the intended value for a sampled host in every group
- ✓ansible-playbook --list-hosts against the new inventory returns the host set you expected for each play
- ✓ansible <group> -m ping succeeds for every host, or every failure is recorded with a reason
- ✓No host in this inventory is also defined in another inventory root, or the duplication is deliberate and documented
5 · Rollback
If verification fails, undo the procedure in reverse order.
- ↶The inventory is a file, so rollback is a revert: git revert the commit, or delete the directory before it is merged
- ↶If the inventory was already used for a real run, reverting the file does not undo the run - identify what was changed with the audit runbook first
- ↶If a wrong inventory caused a change on hosts that should not have been targeted, stop and use the configuration rollback runbook; do not attempt to fix it by editing the inventory
- ↶Remove any ansible.cfg change that made the new inventory the default until it has passed verification
6 · Escalation
When the runbook isn't enough, contact:
- · Escalate to the service owner if the source of truth and the rendered host count disagree and the difference cannot be explained
- · Escalate before adding a host that already appears in another environment inventory - a host in two environments is a design problem, not an inventory problem
- · Escalate to the platform owner if the naming convention here would have to differ from the rest of the repository
- · Escalate if reachability fails for a group rather than for individual hosts - that is usually a network or credential boundary, not an inventory defect
The inventory is the blast-radius map. Every hosts: line, every
--limit, every group pattern resolves through it, and none of them can
be safer than the file they resolve against. A new inventory is
therefore not a data-entry task - it is a change to what your automation
is capable of touching.
This runbook brings one into service and proves it before anything runs against it in anger.
When to use this runbook
- A new environment, region or tenant is being brought under management.
- An existing static inventory is being restructured or split.
- A team is taking over hosts that were previously managed elsewhere.
- A dynamic inventory source is being replaced by, or supplemented with, a static one.
Blast radius
Writing the inventory: none. Steps 1 to 6 are file edits and read-only resolution.
Step 7 connects to every host in the new file. Step 8 runs a playbook against one of them in check mode. The number that matters throughout is the host count in Step 3 - if that number is larger than you expected, every later step is larger than you expected too.
Inputs
- The authoritative list of hosts, exported from the source of truth (CMDB, cloud API, provisioning system) with a timestamp.
- The environment name and the repository’s convention for it.
- The connection details: user, port, bastion, key.
- The group structure this estate uses for roles and for lifecycle.
Step 1: Create the layout
mkdir -p inventories/staging/group_vars/all
mkdir -p inventories/staging/host_vars
touch inventories/staging/hosts.ymlOne directory per environment, and the environment name in the path.
This is what makes --limit mistakes survivable: a play run against
inventories/staging cannot reach production hosts, because they are
not in the file.
Step 2: Write the hosts and groups
all:
children:
web:
hosts:
web01.example.com:
ansible_host: 192.0.2.11
web02.example.com:
ansible_host: 192.0.2.12
db:
hosts:
db01.example.com:
ansible_host: 192.0.2.21
staging:
children:
web:
db:Two kinds of group, and it is worth keeping them visibly separate:
- Function groups (
web,db,cache) say what a host is. Roles target these. - Lifecycle groups (
staging,canary,wave1) say how a host is treated. Rollouts target these.
A host belongs to exactly one function group and to as many lifecycle
groups as the process needs. Mixing the two - a group called
web-staging-wave1 - produces a combinatorial explosion that nobody
maintains, and the group that is missing on the day you need it is the
one you have to invent under pressure.
Step 3: Render the graph and read it
ansible-inventory -i inventories/staging --graphVerified output shape on 2.21.3:
@all:
|--@ungrouped:
|--@web:
| |--web01.example.com
| |--web02.example.com
|--@db:
| |--db01.example.com
Three things to read, in this order:
@ungroupedmust be empty. A host that landed there matches no function group, so no role targets it, and it will silently receive nothing while appearing to be managed.- Every group you expected exists, and no group you did not expect does. A group that appears from nowhere usually means an indentation error promoted a host list into a child group.
- The host count. Count it and compare against the source of truth.
ansible-inventory -i inventories/staging --list \
| python3 -c 'import json,sys; print(len(json.load(sys.stdin)["_meta"]["hostvars"]))'If that number does not match the export you started from, do not proceed. An inventory that is missing hosts fails quietly - those hosts just never get patched. An inventory with extra hosts fails loudly, at 3am, on a machine that belongs to another team.
Step 4: Resolve variables and check where each came from
ansible-inventory -i inventories/staging --host web01.example.comSample one host per group. For each variable that matters, ask which layer it came from and whether that was deliberate.
Step 5: Look for conflicting group membership
A host in two groups that both set the same variable gets one of them, and which one depends on group depth and on alphabetical order of group names at the same depth. That is a rule, but it is not a rule anyone should be relying on in production.
# Which groups is each host in?
ansible-inventory -i inventories/staging --graph --vars
# Which groups define the same key?
grep -rn 'app_port' inventories/staging/group_vars/If two group_vars files set the same key and a host is in both, either move the value to one layer or make the intent explicit with a host_vars override. Leaving it to resolution order is how a value changes because someone renamed a group.
Step 6: Prove what a play would target
ansible-playbook -i inventories/staging site.yml --list-hosts
ansible-playbook -i inventories/staging site.yml --limit web --list-hosts
ansible-playbook -i inventories/staging patch.yml --list-tasksDo this for every playbook that will use this inventory, not just the
first one. Each play has its own hosts: line, and a play whose pattern
does not match anything in the new inventory is a play that will be
skipped silently on the day it matters.
Verified on 2.21.3: a --limit that matches nothing does not silently
run against everything. It produces
[WARNING]: Could not match supplied host pattern, ignoring: nosuchhost*
[ERROR]: Specified inventory, host pattern and/or --limit leaves us with no hosts to target.
and exit code 1. That is the desired behaviour and it is worth knowing
it is the behaviour, because it means a mistyped limit stops rather than
widens.
Step 7: Prove reachability
ansible -i inventories/staging all -m ping -oping here is Ansible’s module, not ICMP: it opens the connection,
runs a tiny module on the target and returns pong. That is the point -
it proves transport, authentication and the remote Python at once.
Do not build this check out of debug. debug is an action plugin that
runs on the controller and never connects, so a genuinely unreachable
host reports unreachable=0 and the check passes on a fleet you cannot
reach. Exit code 4 is what an unreachable host produces; check for it.
Step 8: One check-mode run
ansible-playbook -i inventories/staging site.yml \
--limit web01.example.com --check --diffOne host, check mode, diff on. You are testing that the inventory feeds the playbook sensible values, so read the diff for variables that rendered wrong - an empty string where a hostname should be, a default that should have been overridden - rather than for whether the change itself is correct.
Step 9: Commit and reconcile
Commit the inventory with the source-of-truth export referenced in the message, and record in the change record:
- The export timestamp and the host count it contained.
- The rendered host count from Step 3.
- Any difference, with a reason.
- Which playbooks were
--list-hostschecked against it.
A reconciliation that was done and not written down has to be done again by the next person, who will not know it already agreed.
Rollback
The inventory is a file. Before merge, delete the directory. After
merge, git revert the commit.
git revert --no-edit <inventory-commit>
ansible-inventory -i inventories/staging --graphPoint of no return: the revert is only a clean rollback while the inventory has never been used for a run that changed something. Once a play has executed against it, reverting the file changes nothing on the hosts. If the inventory targeted hosts it should not have, the file revert is step zero and the configuration rollback runbook is the actual recovery.
Common patterns
| Symptom | Likely cause | Resolution |
|---|---|---|
Hosts appear under @ungrouped | They are defined at all level, or an indentation error dropped them out of a group | Fix the nesting; re-render the graph |
| Host count is higher than the export | A group was included twice, or a range expression expanded wider than intended | Read --graph; check [01:06] style ranges |
| A group exists that nobody wrote | Indentation promoted a hosts: mapping to children: | Re-indent; groups only appear under children: |
| Variable resolves differently than the file suggests | Two groups set it and resolution order picked the other | Move the value to one layer, or set it explicitly in host_vars |
--list-hosts returns nothing for a play | The play’s hosts: pattern names a group this inventory does not have | Align the group name with the convention, or fix the play |
| Reachability fails for an entire group | Bastion, firewall or credential boundary, not the inventory | Test one host by hand with ssh -v before editing YAML |
| The same host is in two environments | A real design problem | Escalate; do not resolve it by deleting one entry |
Escalation
Escalate when:
- The rendered count and the source of truth disagree and you cannot explain the difference. An inventory you cannot reconcile is an inventory whose blast radius you cannot state.
- A host would be present in two environment inventories.
- The convention here would have to differ from the rest of the repository. Convention drift compounds; get it decided once.
- Reachability fails at group granularity. That is infrastructure, and the inventory change should wait behind it.