Case Study: Multi-Environment Inventory¶
Problem¶
Three environments — dev, staging, production — need the same roles applied with different variables (different database hosts, different replica counts, different feature flags), and a mistake in one environment must not be able to reach another.
Requirements¶
- One set of roles, reused unmodified across all three environments
- Environment-specific variables cleanly separated
- Structurally impossible (not just "please be careful") to target production by accident
Architecture¶
flowchart TD
R[Shared roles/] --> D[inventories/dev]
R --> S[inventories/staging]
R --> P[inventories/production]
D --> DR[ansible-playbook -i inventories/dev]
S --> SR[ansible-playbook -i inventories/staging]
P --> PR[ansible-playbook -i inventories/production]
Repository Structure¶
ansible-project/
├── inventories/
│ ├── dev/
│ │ ├── hosts.ini
│ │ └── group_vars/all.yml
│ ├── staging/
│ │ ├── hosts.ini
│ │ └── group_vars/all.yml
│ └── production/
│ ├── hosts.ini
│ └── group_vars/all.yml
├── playbooks/site.yml
└── roles/app/
Inventory and Variables Per Environment¶
env_name: dev
db_host: dev-db.internal
replica_count: 1
feature_new_dashboard: true
[app]
prod-app01 ansible_host=10.20.1.10
prod-app02 ansible_host=10.20.1.11
prod-app03 ansible_host=10.20.1.12
env_name: production
db_host: prod-db.internal
replica_count: 3
feature_new_dashboard: false
Notice dev-app01 and prod-app01 are different hostnames in entirely separate files — there is no shared inventory with an env: variable to get wrong.
Playbook (Identical Across Every Environment)¶
Execution¶
# Dev — safe to run freely
ansible-playbook -i inventories/dev/hosts.ini playbooks/site.yml
# Production — same playbook, same roles, different inventory
ansible-playbook -i inventories/production/hosts.ini playbooks/site.yml --check --diff
ansible-playbook -i inventories/production/hosts.ini playbooks/site.yml
Expected Output¶
$ ansible-playbook -i inventories/production/hosts.ini playbooks/site.yml --check --diff
PLAY [Configure application servers] ***
TASK [app : Deploy application config] ***
--- before: /etc/app/config.yml
+++ after: config.yml.j2
@@ -1,3 +1,3 @@
-db_host: dev-db.internal
+db_host: prod-db.internal
changed: [prod-app01]
--check --diff against production shows exactly what would change — including confirming db_host resolved to the production database, not dev's, before anything is actually applied.
Failure Scenario (Near Miss, Caught)¶
An engineer means to test a change against dev and runs:
--limit app01 matches nothing — app01 doesn't exist in the dev inventory (it's dev-app01) — so Ansible reports zero hosts matched and the run does nothing, rather than silently matching a same-named host in a different environment. Compare this to a single shared inventory using generic hostnames like app01 across environments distinguished only by a variable: the same typo there could easily have matched a host in the wrong environment and applied a change silently.
Troubleshooting¶
confirms exactly which hosts a given inventory file resolves to, before running anything against it — the fastest sanity check before any production run.
Production Hardening¶
- Add a CI gate requiring
--check --diffoutput to be reviewed (as a PR comment or artifact) before any production apply is allowed to run. - Name hosts with an environment prefix (
dev-,prod-) even though they already live in separate files — a second layer of defense against a copy-pasted--limitpattern. - Restrict which CI credentials/runners can even reach the production inventory's target network, so a wrong
-iflag fails at the network layer too, not just the inventory layer.
Interview Questions¶
- Why is separate inventory directories per environment safer than one inventory with an
env:variable? - What would
ansible-playbook -i inventories/dev/hosts.ini site.yml --limit app01do ifapp01doesn't exist in that inventory, and why is that the safe behavior?
What You Learned¶
Environment isolation that depends on "remembering to pass the right flag" is not isolation — structuring inventory so the wrong flag simply can't match a real host is.