Skip to main content
RunBook Academy

AnsibleV · Inventory Design at Fleet ScaleFleet taxonomy

Environment separation you can rely on

Intermediate⏱ ~16 minansible-core

What you'll learn

  • Separate environments at the inventory-source level rather than by group membership
  • Explain why an absent host is a stronger control than an excluded one
  • Distinguish a pattern that matches nothing from a --limit that leaves nothing
  • Identify the three ways environment separation is accidentally undone

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.

There are two ways to keep a staging change away from production, and they are not equally strong.

The common one is to put both environments in one inventory, in groups called production and staging, and rely on everyone always targeting the right group. The other is to put them in separate inventory sources, so that when the staging run is loaded, production hosts do not exist.

The second one costs a directory. It is worth far more than it costs.

Excluded is not the same as absent

Under a single shared inventory, safety is a property of the command line. ansible-playbook site.yml --limit staging is safe; the same command with the limit forgotten is a fleet-wide production change. The inventory permitted both. Something outside the inventory — a habit, a wrapper script, a code review — was the only thing standing between them.

Under separate sources, safety is a property of the data. The staging inventory contains staging hosts. A production hostname typed into a staging run does not resolve, because it is not in the file that was loaded.

This distinction has a name in security engineering and it is worth importing: exclusion is a filter, absence is a boundary. Filters are applied by whoever remembers to apply them.

estate/
  ansible.cfg
  site.yml
  roles/                     # shared: the automation itself
  inventory/
    production/
      hosts.yml              # only production hosts
      group_vars/
    staging/
      hosts.yml              # only staging hosts
      group_vars/
    development/
      hosts.yml
      group_vars/

Roles and playbooks are shared, because the whole point of staging is to run the same automation against different machines. Inventory is not shared, because the whole point of an environment is that its machines are different machines.

What the boundary actually does

Load the staging inventory and ask for everything:

Read-only / Safeall means all of staging
$ ansible -i inventory/staging 'all' --list-hosts
  hosts (2):
  web-s1.example.com
  db-s1.example.com

Now try to reach a production host from that run — the mistake this design exists to stop:

Read-only / Safethe host is not there to be named
$ ansible -i inventory/staging 'all' --limit web-a1.example.com --list-hosts
[WARNING]: Could not match supplied host pattern, ignoring: web-a1.example.com
[ERROR]: Specified inventory, host pattern and/or --limit leaves us with no hosts to target.

Exit status 1, no tasks, nothing attempted. There is no version of that command that reaches production, because the name means nothing to the loaded inventory.

The honest comparison

Separate inventories are not free, and pretending otherwise makes the argument weaker than it is.

Single inventory, environment groupsSeparate inventory sources
Cost of a fleet-wide questionOne command over everythingOne command per environment, or an explicit merge
Group definitionsWritten onceWritten once per environment
Cross-environment reportingTrivialRequires passing -i twice on purpose
A forgotten --limitChanges productionChanges only the loaded environment
A typo in an environment group nameSilently widens the targetCannot widen past the file
Onboarding a new engineerThey must learn the disciplineThe tool enforces it

The first three rows are real costs. Row four is why you pay them.

The duplication in row two is smaller than it looks, because what is duplicated is membership, which is genuinely different per environment, not behaviour, which lives in roles and is shared. If you find yourself copying the same group_vars content between environments, that content is not environment-specific and belongs in the role’s defaults/ instead.

The three ways separation gets undone

Every one of these has happened in real estates, and none of them announce themselves.

1. -i pointed at the parent directory. Shown above. Five hosts appear where two were expected. The tell is the host count in --list-hosts; if you never look at it, there is no tell.

2. A default inventory in ansible.cfg that spans environments. Setting inventory = inventory/ in the project config means every command that omits -i silently loads everything. The fix is to make the default either the safest environment or nothing at all, and to require -i for production. A default that includes production is a default that turns a forgotten flag into an incident.

3. A shared group_vars/all.yml outside the environment directories. Ansible looks for group_vars/ beside the inventory source and beside the playbook. A group_vars/all.yml sitting next to site.yml applies to every environment, so a variable that was meant to be a staging convenience — a shortened timeout, a relaxed check, a test endpoint — reaches production too. Keep group_vars/ inside the environment directory, and treat a playbook-adjacent group_vars/ as a finding in review.

Promotion works the same either way

A concern people raise about separate inventories is promotion: if the environments are separate, how do you know the staging test proves anything about production?

The answer is that promotion has never been about inventory. What gets promoted is the automation — the roles, the playbook, the pinned collection versions, the variable defaults — and that is shared already. The inventory is deliberately different, because staging hosts are deliberately different machines.

What you do owe is structural equivalence: if production has web, database and cache groups, staging should have all three, even if each holds one host. A staging environment missing a group is a staging environment that cannot exercise the play that targets it, and you will discover that during the production run.

Knowledge check

Knowledge check · 4 questions

  1. Q1. Why is a separate inventory source per environment a stronger control than a production group in a shared inventory?

  2. Q2. Pointing -i at the parent directory that contains both inventory/production and inventory/staging merges every environment into a single inventory.

  3. Q3. Which of these silently undo environment separation? Select all that apply.

  4. Q4. A nightly job runs ansible-playbook -i inventory/production site.yml --limit canary. The canary group was renamed last month and no longer exists. What does the operator see?

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