Best Practice #
Managing an Ansible inventory in a small-scale environment is very easy. However, when your infrastructure grows to include hundreds of virtual servers, multi-cloud setups, and is managed by several platform engineering teams in parallel, an unstructured inventory becomes a heavy maintenance burden. This best practice article doesn’t lay down rigid, constraining rules — it presents proven design principles that keep your inventory files clean, secure, easy to understand (even for new team members), and scalable for future needs.
1. One Inventory per Environment #
Physical isolation is the first security principle in inventory management. You must separate target server data for Development, Staging, and Production environments into physically different folders or files.
# Recommended Environment Isolation Structure:
environments/
├── development/
│ ├── hosts.ini
│ └── group_vars/
├── staging/
│ ├── hosts.ini
│ └── group_vars/
└── production/
├── hosts.ini
└── group_vars/
Merging all environments into one monolithic inventory file is strictly forbidden because it enlarges the blast radius of limit command typos in the terminal. By physically separating directories, every playbook execution requires explicitly pointing to an inventory argument:
# Controlled execution to production servers
ansible-playbook -i environments/production/hosts.ini playbooks/site.yml
Git Branching Strategy and Access Control #
Besides physical folder separation, you’re recommended to implement access control (permissions) using Git rules. You can lock the environments/production/ folder in the Git repository using a CODEOWNERS file. This way, every change to the production server list or production variables must go through a review process (pull request review) by senior administrators before being merged into the main branch.
2. Use Descriptive Host Names #
Using raw IP addresses or cloud provider default hostnames (like ip-10-0-1-24.ap-southeast-1.compute.internal) as server identifiers in the inventory file is an anti-pattern that makes monitoring harder.
You should adopt a consistent, informative naming pattern that directly reflects the three dimensions of a target server: Environment, Function (Role), and Sequence Number.
# ANTI-PATTERN: Random host names and raw IP addresses
[webservers]
10.0.1.15
server-v3-instance
vm-web-staging-01
# CORRECT: Descriptive naming following the {env}-{role}-{number}.{domain} pattern
[webservers]
prod-web-01.unisbadri.com
prod-web-02.unisbadri.com
[dbservers]
prod-db-primary-01.unisbadri.com
prod-db-replica-01.unisbadri.com
Handling Hostnames in Autoscaling #
If you use cloud autoscaling services where virtual server instances are born and die with random names from the AWS API, you can use internal caching mappings or leverage cloud provider metadata tags as dynamic hostnames in Ansible. You can set up the hostnames parameter in your AWS EC2 dynamic plugin configuration file to map instance names based on the Name tag assigned automatically by your cloud provisioning library (like Terraform).
3. Group Hierarchically #
Use the group-within-group (nested groups) feature with the :children keyword to build your infrastructure topology structure. This provides high flexibility when targeting playbook execution.
# File: inventory/production/hosts.ini
# Lowest-level groups (Granular)
[web_primary]
prod-web-01.example.com
[web_cdn]
prod-cdn-01.example.com
# Combined mid-level groups
[webservers:children]
web_primary
web_cdn
[db_primary]
prod-db-01.example.com
[db_replica]
prod-db-02.example.com
[dbservers:children]
db_primary
db_replica
# Top-level group
[production:children]
webservers
dbservers
This hierarchical structure makes it easy to choose the scope of action:
- If you want to update CDN SSL certificates: target
web_cdn. - If you want to update Nginx configurations globally: target
webservers. - If you want to trigger a global production system backup: target
production.
Variable inheritance in this structure flows downward. Properties defined in production:vars automatically flow to webservers and dbservers, but variables defined in web_primary:vars won’t affect other servers outside that child group.
4. Separate Variables by Concern #
Don’t let group-level variable files (group_vars/all.yml or group_vars/webservers.yml) balloon into giant files with hundreds of YAML code lines. Split those variables into meaningful subdirectories aligned with their service topic.
# Recommended Variable Storage Structure:
group_vars/
├── all/
│ ├── system.yml # Basic settings: timezone, ntp, dns
│ └── security.yml # Firewall security hardening settings
├── webservers/
│ ├── nginx.yml # Nginx web server parameter configuration
│ ├── php.yml # PHP-FPM runtime configuration
│ └── ssl.yml # SSL/TLS encryption key path configuration
└── dbservers/
├── postgresql.yml # PostgreSQL database configuration
└── backup.yml # Database backup automation schedule
Example of Variable Separation Implementation #
Here’s an illustration of the YAML file contents inside the modularly separated group_vars/webservers/ folder:
nginx.yml:--- nginx_version: "1.24" nginx_worker_processes: 4 nginx_client_max_body_size: "10m"php.yml:--- php_version: "8.2" php_memory_limit: "256M" php_max_execution_time: 60ssl.yml:--- ssl_cert_path: "/etc/ssl/certs/app.crt" ssl_key_path: "/etc/ssl/private/app.key"
Applying this separation makes your code more modular and speeds up troubleshooting because parameter definition locations are very easy to find.
5. Document Non-Obvious Variables #
Documenting the reasoning behind non-standard (uncommon) parameter values using comments above the YAML variable lines greatly helps maintain project continuity when other engineers manage it in the future.
# File: group_vars/webservers/nginx.yml
---
# Nginx Tuning Parameters:
# worker_processes is set to 8 to match the vCPU count of the c5.2xlarge instance type
# DO NOT raise this value if the managed node instance type in AWS is downgraded
nginx_worker_processes: 8
# Capped at 15M to limit the upload size of PDF document files
nginx_client_max_body_size: "15m"
# Timeout raised to 180 seconds specifically to accommodate legacy API financial report query responses
# Refactor target: change back to 30s after the report endpoint migrates to microservices Q4 2026
nginx_proxy_read_timeout: 180
Contextual comments like these spare the team from recurring debates about why a parameter is set to a certain number.
6. Use Variables to Distinguish Environments #
When writing playbooks, you often face situations where one task must run differently depending on the target environment (for example enabling debug logging in staging but turning it off in production).
You are strictly forbidden from writing imperative hostname checking conditions using the when conditional inside playbook task files. This dirties your playbook workflow with dynamic server name details.
# ANTI-PATTERN: Checking the target hostname inside a playbook task
- name: Enable debug log visualization
template:
src: app.conf.j2
dest: /etc/app/app.conf
when: inventory_hostname in groups['staging'] # Rigid and hard to maintain
The best practice is delegating those behavioral differences to each environment’s inventory variables, then writing the playbook purely declaratively using those variables:
# 1. Define the variable in the staging environment file:
# environments/staging/group_vars/all.yml
enable_app_debug: true
# 2. Define the variable in the production environment file:
# environments/production/group_vars/all.yml
enable_app_debug: false
# 3. Write the playbook cleanly without caring about the target environment name:
# playbooks/deploy.yml
- name: Configure the application
template:
src: app.conf.j2
dest: /etc/app/app.conf
# The enable_app_debug variable is absorbed dynamically from the active inventory
7. Store the Inventory in Git #
The inventory is an inseparable part of Infrastructure as Code (IaC). You must store the entire inventory directory structure (including the environments/ folder, requirements.yml file, and constructed plugin) in the Git repository along with your playbook and role code.
This provides a clear change history (audit trail): who added a new server, when variables were updated, and makes system recovery easier if a configuration error occurs (rollback).
However, you must configure the .gitignore file strictly to prevent private authentication key leaks or local plaintext password files:
# File: .gitignore
# Ignore Ansible automatic backup files
*.retry
# Ignore local SSH private authentication keys accidentally placed here
*.pem
*.key
id_rsa
id_ed25519
# Ignore local Vault reader password files
.vault_pass
vault_password.txt
Ansible Runtime Assembly #
To wrap up all these best practices in Ansible architecture, the diagram below illustrates how all inventory data sources, variable files, playbooks, and roles are dynamically assembled by the Ansible Engine at execution runtime before being sent to target machines via SSH:
flowchart TD
subgraph "Inventory Sources (Target Data)"
A["hosts.ini / hosts.yml or Cloud Plugin"] -->|"1. Load Hosts & Groups"| E["Ansible Runtime Engine"]
end
subgraph "Variable Sources (Parameter Data)"
B["group_vars/all/*.yml"] -->|"2. Load Global Variables"| E
C["group_vars/webservers/*.yml"] -->|"3. Load Group Variables"| E
D["host_vars/web-01.yml"] -->|"4. Load Host Variables"| E
end
subgraph "Execution Logic (Actions)"
F["Playbook (playbooks/webservers.yml)"] -->|"5. Define Workflows & Roles"| E
G["Ansible Roles (roles/nginx/)"] -->|"6. Provide Templates & Tasks"| E
end
E -->|"7. Assemble the Final Combination (hostvars)"| H["Managed Nodes via SSH"]Ansible’s 22-Level Variable Precedence #
To understand how Ansible assembles the final variable in step 7 of the diagram above, you need to be aware of the global precedence order. Here are the 22 variable priority levels in Ansible (sorted from lowest to highest priority):
- role defaults: Default variables in the
defaults/main.ymlfolder inside a role. - inventory file or script group vars: Variables defined in the inventory file for a group.
- inventory group_vars/all: Global variables in the
group_vars/all.ymlfile. - playbook group_vars/all: Global
group_vars/all.ymlvariables located near the playbook. - inventory group_vars/: Group variables defined adjacent to the inventory.
- playbook group_vars/: Group variables defined adjacent to the playbook file.
- inventory file or script host vars: Host variables defined directly in the inventory file.
- inventory host_vars/: Host variables defined adjacent to the inventory.
- playbook host_vars/: Host variables defined adjacent to the playbook file.
- host facts / cached facts: Variables from the target system collection by the setup module.
- play vars: Variables defined directly in the
vars:block within a playbook. - play vars_files: Variables imported using the
vars_files:parameter within a playbook. - role vars: Variables defined in the
vars/main.ymlfolder inside a role. - block vars: Variables defined at the block level within tasks.
- task vars: Variables defined directly at the single task level.
- include_vars: Variables loaded dynamically during task execution using the
include_varsmodule. - set_facts / registered vars: Variables created dynamically using the
set_factmodule orregistermodule captures. - role params: Parameters passed when calling a role.
- include params: Parameters passed when including a task file.
- handler vars: Variables used inside handlers.
- extra vars (CLI): Variables defined in the terminal using the
-eor--extra-varsflag. This is the highest priority that nothing can override.
Understanding this precedence order helps you design a safe, predictable variable placement strategy.
Inventory Review Checklist #
Use this checklist as a mandatory guideline when doing code reviews (pull request reviews) on your team’s Ansible automation repository:
1. Directory Structure & Isolation #
- Physical Isolation: Inventory files are physically separated by target environment (dev, staging, prod) into their own folders to minimize wrong execution targets.
- Folder Separation: The inventory folder is neatly separated from the playbooks/ and roles/ folders to keep the repository structure tidy.
- No Inline Variables: There are no long inline variables piled up in the hosts.ini file that could ruin file neatness.
2. Naming Standards #
- Consistent Group Names: Group names are written in lowercase, plural, and with underscores (e.g.:
dbservers,application_servers) to avoid parser mismatches. - Descriptive Host Names: Descriptive host names include env, role, and index information (e.g.:
prod-web-01.example.com) to make log tracing easier. - Variable Names: Custom variable names use snake_case format consistently across all YAML files.
3. Variable Management #
- Global Consolidation: Homogeneous global parameters are consolidated in
group_vars/all.ymlto prevent code duplication. - Concern Separation: group_vars files are split by concern (like
nginx.yml,postgresql.yml) if the variable count exceeds 20 lines. - Context Comments: Explanatory comments exist above non-standard custom parameter values to maintain knowledge transfer between engineers.
4. Security & Git #
- Secret Encryption: There are no passwords, private keys, or API tokens in plaintext form that could leak publicly.
- Ansible Vault: All sensitive credential data is securely encrypted using Ansible Vault.
- Git Ignores: Local secret files (.vault_pass, *.pem, *.key) are registered in the project’s
.gitignorefile. - Execution Verification: The
ansible-inventory --graphcommand runs 100% successfully without triggering warnings or errors.
Summary #
- Physical Environment Isolation — Physically separating inventories per environment folder avoids the fatal risk of deploying to production due to limit typos.
- Descriptive Naming Convention — Adopt the
{env}-{role}-{number}.{domain}format to give target servers instant visual identity in audit logs.- Modular group_vars Abstraction — Split parameter files by concern in the group_vars subdirectory for easier troubleshooting and parameter auditing.
- Declarative, Not Conditional — Avoid manual target hostname checks in playbooks; move logic differences to inventory variable data.
- Design Decision Documentation — Always include explanatory comments above variables using non-standard values to maintain team knowledge continuity.
- Git as Source of Truth — Store inventory configuration in Git, and protect credentials with very strict
.gitignorerules.- Checklist Verification — Use the inventory review checklist diligently before merging new automation code into the repository’s main branch.