Anti-Pattern #
Setting up an Ansible inventory file at the start of a project feels like a trivial task. You just write a few server IPs under a group name, then immediately start writing playbooks. However, neglecting to design the inventory structure from the start is one of the main sources of operational accidents in production environments. Problems like wrong execution targets (a playbook runs in production when it was meant for staging), secret key leaks in Git, and variables mysteriously overridden due to misunderstanding precedence often trace back to poor inventory design. This article breaks down six fatal anti-patterns in Ansible inventory management along with tactical mitigation steps.
Why Avoiding Design Mistakes Matters #
Avoiding design mistakes in inventory management is part of enforcing security discipline and system stability. A poorly designed inventory acts like a time bomb: the system looks normal while the server count is small, but when infrastructure scales to dozens or hundreds of servers, design errors trigger system failures that are hard to debug.
By recognizing these common error patterns, you can build a solid automation foundation, minimize the risk of human error, and protect your automation project’s secret data from the danger of leaks.
Groups Based on Location Instead of Function #
It’s very intuitive to group servers by their physical location (like data center names or cloud regional zones). However, this pattern is a classic design mistake.
# ANTI-PATTERN: Grouping servers by physical location
[datacenter_jakarta]
web-prod-01.example.com
db-prod-01.example.com
cache-prod-01.example.com
[datacenter_surabaya]
web-prod-02.example.com
db-prod-02.example.com
Why Is This Pattern Difficult? #
Imagine you want to run a playbook to update Nginx security packages on all web servers. Under the anti-pattern structure above, you have no easy way to target all web servers globally. You’re forced to list hostnames manually on the CLI command line, or write new overlapping (redundant) groups.
The Correct Modular Solution: #
You should group servers by their primary function first (software role), then inject location data as group or host variables, or use child groups:
# CORRECT: Grouping by function
[webservers]
web-prod-01.example.com datacenter=jakarta
web-prod-02.example.com datacenter=surabaya
[dbservers]
db-prod-01.example.com datacenter=jakarta
db-prod-02.example.com datacenter=surabaya
[cacheservers]
cache-prod-01.example.com datacenter=jakarta
With this design, if you want to trigger web server automation globally, just point the playbook at the webservers group. If you only want to target web servers in Jakarta, use the limit filter expression on the CLI: --limit "webservers:&jakarta".
Sensitive Credentials in Plaintext #
Storing secret keys like admin passwords, cloud API tokens, database connection strings, or private key passphrases directly in the inventory hosts file or group_vars/ variable files in plaintext.
# ANTI-PATTERN: Storing secrets in plaintext in the hosts file
[dbservers:vars]
db_root_password=SuperSecretPassword123
aws_secret_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
# ANTI-PATTERN: Storing a plaintext token in group_vars/webservers.yml
---
github_oauth_token: "ghp_example1234567890abcdefghij"
The Danger of Credential Leaks: #
Once an inventory file containing plaintext passwords is committed to a Git repository, those credentials are recorded permanently in the commit history (git history). Even if you remove them in the next commit, the old file remains accessible to anyone with access to the repository.
The Ansible Vault Encryption Solution: #
Always use Ansible Vault to encrypt sensitive data. The best practice is splitting your group-level variable files into two files inside the group subdirectory:
group_vars/dbservers/vars.yml(Contains common non-sensitive configuration variables).group_vars/dbservers/vault.yml(Contains encrypted sensitive variables).
There are two main ways to secure sensitive data with Ansible Vault:
- Method A: Encrypting the Entire
vault.ymlFile: Create avault.ymlfile with plaintext content, then encrypt it entirely using the CLI:# Encrypt the file entirely ansible-vault encrypt group_vars/dbservers/vault.yml # Reopen the file for safe editing ansible-vault edit group_vars/dbservers/vault.yml - Method B: Encrypting Only Specific Variables (Encrypt String):
If you want only the variable value to be encrypted while the variable name stays visible as plaintext documentation:The command output is copied and placed into the YAML file like this:
# Encrypt a single password text string ansible-vault encrypt_string 'MySuperSecretDBPassword123' --name 'db_root_password'# File: group_vars/dbservers/vault.yml --- db_root_password: !vault | $ANSIBLE_VAULT;1.1;AES256 35336531383564616238613437633261386162326532393261343538663836366535313531376662 3965653139366537653733633630633866366562306230300a333966376566653133616262333161 61383737636662653965623238646132646263303164626136363032333835373866396565306363
Automating Vault Key Reading #
So you don’t have to type the Vault password manually every time you run an automation command, you can configure the secret key file path in ansible.cfg:
# File: ansible.cfg
[defaults]
# Point the vault password lookup to a hidden local file
vault_password_file = .vault_pass
[!WARNING] Always make sure the
.vault_passfile is in your project’s.gitignoreignore list so it doesn’t get uploaded to public Git repositories. Losing this key means you can never read production variable data again.
One Inventory for All Environments #
Combining development, staging, and production target servers in a single inventory file, distinguishing them only by group names.
# ANTI-PATTERN: All environments merged into one hosts.ini file
[webservers_development]
dev-web-01.internal
[webservers_staging]
stage-web-01.internal
[webservers_production]
prod-web-01.unisbadri.com
prod-web-02.unisbadri.com
Why Is This Pattern Very Dangerous? #
The blast radius of a command typo is enormous. If you want to run an application release deployment playbook to the staging environment but accidentally type the limit flag as --limit webservers_production, Ansible immediately executes your production servers with no safety protection.
The diagram below illustrates the risk level (blast radius) comparison between a monolithic inventory and a separated inventory:
flowchart TD
subgraph "Scenario A: Single Monolithic Inventory (High Risk)"
A["Run Release Playbook"] --> B["Typo Target --limit prod-web-01"]
B --> C["Accidentally Connects to the Entire Production Group"]
C --> D["DOWN or Production Service Outage (Maximum Blast Radius)"]
end
subgraph "Scenario B: Physically Separated Inventory (Safe)"
E["Run Release Playbook"] --> F["Point to Staging Folder -i environments/staging/"]
F --> G["Try typo --limit prod-web-01"]
G --> H["Ansible Returns 'No hosts matched' Message"]
H --> I["Production Services Stay Safe & Protected"]
endThe Physical Isolation Solution: #
You must physically separate inventories into distinct subdirectories under the environments/ folder. This forces you to point to a specific folder during execution:
# Run the playbook with the staging inventory
ansible-playbook -i environments/staging/hosts.ini playbooks/deploy.yml
# Run the playbook with the production inventory (Controlled & Explicit)
ansible-playbook -i environments/production/hosts.ini playbooks/deploy.yml
Piling Up Variables in the hosts File #
Defining dozens of application configuration variables inline right next to the server name or under the [group:vars] block in the hosts.ini / hosts.yml inventory file.
# ANTI-PATTERN: Inventory file filled with configuration variables
# hosts.ini
[webservers]
web-01.example.com http_port=80 max_conn=200 keepalive=65 worker=4 log_dir=/var/log/nginx ssl_cert=/etc/ssl/app.crt ssl_key=/etc/ssl/app.key app_root=/opt/app cache_timeout=300 proxy_read=120
The Bad Effects of Piling: #
- Hard to Read: The inventory file becomes very long horizontally, forcing exhausting horizontal scrolling.
- Code Duplication: If you have 10 homogeneous web servers, you’re forced to rewrite those inline variables on every host line, triggering potential data inconsistency if a variable is missed during updates.
- Ruins Project Orderliness: The inventory file violates the separation of concerns principle by mixing host identity data with internal application tuning details.
The File Separation Solution: #
Move all those variables into the group_vars/webservers/ subdirectory and store them in neat YAML format files organized by topic (for example nginx.yml, app.yml, and ssl.yml). Your hosts.ini inventory file should only contain pure host identifier lines:
# CORRECT: Clean hosts file
[webservers]
web-01.example.com
web-02.example.com
Inconsistent Group Names #
Using haphazard server group naming formats, mixing uppercase and lowercase (CamelCase), using singular nouns, or mixing dash and underscore symbols.
# ANTI-PATTERN: Inconsistent group names
[WebServers] # CamelCase
[db_server] # Singular, uses underscore
[cache-servers] # Plural, uses dash
[App] # Capitalized, too short
The Impact of Inconsistency: #
Group names are declared and used in many important files: in the hosts: declaration in playbooks, YAML file names in the group_vars/ folder, and --limit arguments on the CLI.
Naming inconsistency will often trigger execution errors because Ansible is very sensitive to character differences (case-sensitive). Database servers won’t get configured just because you made a group_vars/db_servers.yml file while the inventory says [db_server].
The Convention Standard Solution: #
Adopt one standard convention for your entire project. The most recommended industry standard is: all group names written in lowercase, in plural form, and using underscores if more than one word.
# CORRECT: Consistent and standardized group names
[webservers]
[dbservers]
[cacheservers]
[application_servers]
Neglecting Global Variable Usage #
Repeatedly defining identical-valued configuration parameters in every group variable file, instead of putting them in one unified global file.
# ANTI-PATTERN: Duplicating homogeneous variables
# group_vars/webservers.yml
ansible_user: ubuntu
timezone: "Asia/Jakarta"
ntp_server: "time.unisbadri.com"
# group_vars/dbservers.yml
ansible_user: ubuntu # DUPLICATE
timezone: "Asia/Jakarta" # DUPLICATE
ntp_server: "time.unisbadri.com" # DUPLICATE
Why Is This Hard to Maintain? #
If one day the infrastructure team decides to move the NTP server to a new address, you’re forced to open and change the NTP parameter value in every existing group variable file. If you have 20 server groups, that’s wasted work time and triggers inconsistency if a group file is missed.
The all.yml Consolidation Solution: #
Consolidate all global parameters (applying to all server types) into the one special file Ansible natively provides: group_vars/all.yml:
# CORRECT: Consolidating global parameters
# group_vars/all.yml
---
ansible_user: ubuntu
timezone: "Asia/Jakarta"
ntp_server: "time.unisbadri.com"
The specific group files (group_vars/webservers.yml or group_vars/dbservers.yml) now only need to contain variables truly unique to their group.
Project Structure Comparison #
To make the file layout easier to understand, here’s a side-by-side comparison of a wrong design (anti-pattern) and a correct design (best practice) in managing Ansible project folders.
// ANTI-PATTERN: Mixing everything in the main folder (Monolithic & Messy)
ansible-project-messy/
├── hosts.ini # Combined dev and prod inventory (Very Dangerous)
├── ansible.cfg
├── setup-nginx-db-app.yml # One playbook containing hundreds of mixed tasks
├── nginx.conf # Static configuration file without environment variation
├── variables.yml # All variables mixed together (dev, prod, db, nginx)
└── db-key.pem # Sensitive credentials stored in plain text without encryption
// BEST PRACTICE: Modular, Isolated, and Using Encryption (Safe & Scalable)
ansible-project-clean/
├── ansible.cfg
├── requirements.yml
├── environments/ # Physically isolated environments
│ ├── staging/
│ │ ├── hosts.ini
│ │ └── group_vars/
│ │ └── webservers.yml
│ └── production/
│ ├── hosts.ini
│ └── group_vars/
│ └── webservers.yml # Sensitive variables encrypted with Ansible Vault
├── playbooks/ # Separate modular playbooks by tier
│ ├── site.yml
│ ├── webservers.yml
│ └── dbservers.yml
├── roles/ # Uses reusable self-contained roles
│ ├── common/
│ ├── nginx/
│ └── postgresql/
└── group_vars/ # Project-level global variables
└── all.yml
Summary #
- Function-Based Design — Avoid creating groups based on physical data center locations; group servers by role (function) then use variables for location details.
- Credential Encryption — Storing secret keys in plaintext in inventory files is strictly forbidden; use Ansible Vault to protect tokens and passwords.
- Physical Folder Isolation — Separate staging and production inventories into distinct directories to cut the risk of execution accidents from limit command typos.
- Separation of Concerns — Clean the hosts inventory file of piled-up inline variables; move configuration parameters to YAML files under the
group_vars/directory.- Group Name Standardization — Adopt a uniform group naming convention (lowercase, plural, underscore) to avoid variable matching failures.
- all.yml Utilization — Avoid duplicating homogeneous parameters in every group variable file by consolidating them into the global
group_vars/all.ymlfile.- Git Lifecycle Security — Make sure private key files, plaintext vault passwords, and retry files are in the
.gitignoreignore list before pushing to Git repositories.