☝️ Important
This material is borrowed from a third-party source.
What does inventory rest on?
Let’s sort out the basic concepts. Ansible uses a push model: the control node (the machine on which we run the playbook) connects to managed nodes (target hosts) and executes tasks on them. Inventory is a roadmap that reflects where, how, and when we can go.
By default Ansible looks for inventory at /etc/ansible/hosts, but we can specify an alternative source via the -i flag:
# Use a specific inventory file
ansible-playbook -i inventory.yml site.yml
# Use a directory with multiple inventories
ansible-playbook -i inventory/ site.yml
# Combine several sources
ansible-playbook -i static-hosts.yml -i aws_ec2.yml site.ymlInventory solves three key tasks:
- Defines targets — which hosts exist and are available for automation.
- Organizes structure — how hosts are logically grouped.
- Stores configuration — variables for connecting to and configuring each host.
Static inventories: INI vs YAML
Static inventories are plain files containing lists of target hosts. Ansible supports two main formats:
- INI,
- YAML.
INI format
The INI format is simple and clear for small inventories:
# Hosts without a group (ungrouped)
noname.example.com ansible_host=192.0.2.50 ansible_port=5555
# Web servers group
[webservers]
web1.example.com
web2.example.com ansible_host=10.0.1.11
web[3:5].example.com # Range: web3, web4, web5
# Database group with variables
[dbservers]
db1.example.com http_port=80 maxconn=808
db2.example.com http_port=8080 maxconn=909
# Variables for the whole group
[webservers:vars]
ansible_user=deploy
nginx_worker_processes=4
# Group of groups (children)
[production:children]
webservers
dbservers
[production:vars]
env=productionImportant INI quirk: values declared next to a host (e.g. http_port=80) are interpreted as Python variables. That is, http_port=80 will become the number 80, and debug=True will become the boolean True. But in the :vars section everything is interpreted as strings, so debug=True will become the string "True".
YAML format
YAML, familiar to all of us, has a tree structure that is more readable and flexible.
---
all:
children:
webservers:
hosts:
web1.example.com:
web2.example.com:
ansible_host: 10.0.1.11
web3.example.com:
web4.example.com:
web5.example.com:
vars:
ansible_user: deploy
nginx_worker_processes: 4
dbservers:
hosts:
db1.example.com:
http_port: 80
maxconn: 808
db2.example.com:
http_port: 8080
maxconn: 909
production:
children:
webservers:
dbservers:
vars:
env: productionAdvantage of YAML: Data types are handled predictably, there’s no confusion between regular and :vars values. I strongly recommend using YAML for all new projects.
Ranges and patterns
Ansible supports convenient patterns for defining sets of hosts:
all:
children:
webservers:
hosts:
# Numeric ranges
web[01:10].example.com: # web01, web02, ..., web10
# Numeric ranges with a step
web[11:20:3].example.com: # web11, web14, web17, web20
# Alphabetic ranges
web[a:f].example.com: # weba, webb, webc, webd, webe, webf
# IPv4 ranges
192.168.1.[1:5]:Let’s check the result:
# Show all hosts in the inventory
ansible-inventory -i inventory.yml --list
{
"_meta": {
"hostvars": {},
"profile": "inventory_legacy"
},
"all": {
"children": [
"ungrouped",
"webservers"
]
},
"webservers": {
"hosts": [
"web01.example.com",
"web02.example.com",
"web03.example.com",
"web04.example.com",
"web05.example.com",
"web06.example.com",
"web07.example.com",
"web08.example.com",
"web09.example.com",
"web10.example.com",
"web11.example.com",
"web14.example.com",
"web17.example.com",
"web20.example.com",
"weba.example.com",
"webb.example.com",
"webc.example.com",
"webd.example.com",
"webe.example.com",
"webf.example.com",
"192.168.1.1",
"192.168.1.2",
"192.168.1.3",
"192.168.1.4",
"192.168.1.5"
]
}
}
# Graphical representation of groups
ansible-inventory -i inventory.yml --graph
@all:
|--@ungrouped:
|--@webservers:
| |--web01.example.com
| |--web02.example.com
| |--web03.example.com
| |--web04.example.com
| |--web05.example.com
| |--web06.example.com
| |--web07.example.com
| |--web08.example.com
| |--web09.example.com
| |--web10.example.com
| |--web11.example.com
| |--web14.example.com
| |--web17.example.com
| |--web20.example.com
| |--weba.example.com
| |--webb.example.com
| |--webc.example.com
| |--webd.example.com
| |--webe.example.com
| |--webf.example.com
| |--192.168.1.1
| |--192.168.1.2
| |--192.168.1.3
| |--192.168.1.4
| |--192.168.1.5This already looks decent, but if we want to tackle serious tasks, we need more serious mechanisms.
Groups and hierarchy
Ansible lets us organize hosts through groups and subgroups.
Special groups
Ansible has two built-in groups:
**all**— includes absolutely all hosts in the inventory,**ungrouped**— hosts that don’t belong to any group (exceptall).
Nested groups (children)
all:
children:
# Geographic groups
usa:
children:
southeast:
children:
atlanta:
hosts:
host1.example.com:
host2.example.com:
raleigh:
hosts:
host3.example.com:
vars:
timezone: "America/New_York"
regional_server: foo.southeast.example.com
west:
children:
california:
hosts:
host4.example.com:
vars:
timezone: "America/Los_Angeles"Inheritance rule: variables of child groups have higher priority and override parent group variables.
Host selector patterns
When we run a playbook, hosts can be used very flexibly:
# One group
ansible-playbook -i inventory.yml site.yml --limit webservers
# Multiple groups (OR)
ansible-playbook site.yml --limit "webservers,dbservers"
# Intersection of groups (AND)
ansible-playbook site.yml --limit "webservers:&production"
# Exclusion (NOT)
ansible-playbook site.yml --limit "production:!dbservers"
# Combined patterns
ansible-playbook site.yml --limit "webservers:&production:!web1"
# Ranges within a group
ansible-playbook site.yml --limit "webservers[0:5]"
# By regular expressions
ansible-playbook site.yml --limit "~web[0-9]+"Our roadmap already includes many objects, but they clearly lack a description, which is perfectly implemented through variables.
Variables: hierarchy and priority
Variables in Ansible are a very powerful tool, but one that requires attention. Because it’s very hard to guess right away where exactly we got a certain value from, and at what level it was last overridden.
Defining variables in the inventory
all:
children:
webservers:
hosts:
web1:
# Host-level variables
ansible_host: 192.168.1.10
http_port: 8080
max_connections: 1000
web2:
ansible_host: 192.168.1.11
http_port: 8081
vars:
# Group-level variables
ansible_user: deploy
ansible_connection: sshKey rule: Host variables override group variables. In general this rule is easy to remember, since more general variables always have a lower priority.
group_vars and host_vars
For large projects, storing variables directly in the inventory is inconvenient. Ansible supports external variable files:
ansible_project/
├── inventory/
│ └── production
├── group_vars/
│ ├── all.yml # Variables for all hosts
│ ├── webservers.yml # Variables for the webservers group
│ └── dbservers.yml # Variables for the dbservers group
└── host_vars/
├── web1.yml # Variables for the specific host web1
└── db1.yml # Variables for host db1Ansible looks for group_vars/ and host_vars/ in two places:
- Relative to the inventory file.
- Relative to the playbook.
Important: if variables are defined in both places, the ones defined at the playbook level take higher priority.
Let’s look at an example:
# group_vars/webservers.yml
---
nginx_port: 80
ssl_enabled: true
application_config:
database_host: "db.example.com"
database_port: 5432Advanced organization with subdirectories
For a complex architecture, variables can be split across files:
group_vars/
├── all/
│ ├── 00_main.yml # Loaded first
│ ├── 10_network.yml
│ ├── 20_security.yml
│ └── 99_vault.yml # Loaded last
└── webservers/
├── nginx.yml
├── app.yml
└── vault.ymlAnsible loads files in lexicographic order. If the same variables appear in multiple files, the last loaded file wins, which is why prefixes like 00_, 10_, 20_ are useful for controlling load order.
Variable priority: the full picture
Now for the most interesting part. Ansible has 22 levels of variable priority. From lowest to highest:
| Priority | Source | Scope |
|---|---|---|
| 1 | command line values (e.g. -u my_user) | CLI options (NOT variables) |
| 2 | role defaults | role/defaults/main.yml |
| 3 | inventory file group vars | Group variables in the inventory |
| 4 | inventory group_vars/all | group_vars/all relative to inventory |
| 5 | playbook group_vars/all | group_vars/all relative to playbook |
| 6 | inventory group_vars/* | group_vars of groups relative to inventory |
| 7 | playbook group_vars/* | group_vars of groups relative to playbook |
| 8 | inventory file host vars | Host variables in the inventory |
| 9 | inventory host_vars/* | host_vars relative to inventory |
| 10 | playbook host_vars/* | host_vars relative to playbook |
| 11 | host facts / cached set_facts | Collected system facts |
| 12 | play vars | Variables in a play |
| 13 | play vars_prompt | Interactive input |
| 14 | play vars_files | Variable files |
| 15 | role vars | role/vars/main.yml |
| 16 | block vars | Block variables |
| 17 | task vars | Task variables |
| 18 | include_vars | Dynamically loaded |
| 19 | set_facts / registered vars | Set at runtime |
| 20 | role (and include_role) params | Role parameters |
| 21 | include params | Include parameters |
| 22 | extra vars (-e "var=value") | Absolute champion |
It’s very important to remember that -e is the last word of truth — this information can really save you when debugging a role.
Priority of groups at the same level
If a host belongs to several groups at the same level, Ansible loads them in alphabetical order. The last loaded group wins:
a_group:
vars:
test_var: a
b_group:
vars:
test_var: bResult: testvar == b
But priority can also be controlled via ansible_group_priority:
a_group:
vars:
testvar: a
ansible_group_priority: 10
b_group:
vars:
testvar: bNow testvar == a
Important: ansible_group_priority can only be set in the inventory source; it doesn’t work in group_vars/.
Debugging variables
A number of commands useful for debugging:
# Show all variables for a specific host
ansible-inventory -i inventory.yml --host web1
# Output in YAML format (more readable)
ansible-inventory -i inventory.yml --host web1 --yaml
# Show a variable via an ad-hoc command
ansible -i inventory.yml web1 -m debug -a "var=http_port"
# Show all variables of a host
ansible -i inventory.yml web1 -m debug -a "var=hostvars[inventory_hostname]"Or in a playbook:
- hosts: webservers
tasks:
- name: Debug a specific variable
debug:
msg: "HTTP port is {{ http_port | default('not set') }}"
- name: Debug all host variables
debug:
var: hostvars[inventory_hostname]
verbosity: 2Dynamic inventories — an escape route for the laziest
If you’re already too lazy to write the inventory yourself, you can offload this duty to a plugin. But seriously speaking, in a world of constantly changing cloud infrastructure, this is simply the best way to reach all the nodes you’re interested in.
Inventory Plugins
Inventory plugins are the modern and recommended way to work with dynamic inventories. Advantages:
- Standardized configuration format (YAML).
- Built-in caching.
- Filtering and grouping out of the box.
- Support from cloud providers.
- No need to write code.
AWS EC2 Plugin
The most popular plugin for working with AWS. First let’s install the collection:
ansible-galaxy collection install amazon.aws
pip3 install boto3 botocoreCreate the configuration file aws_ec2.yml (the name MUST end with aws_ec2.yml or aws_ec2.yaml):
---
plugin: amazon.aws.aws_ec2
# Regions to scan
regions:
- us-east-1
- eu-west-1
# Filtering instances
filters:
# Only running instances
instance-state-name: running
# By tags
tag:Environment:
- production
- staging
# Grouping by tags
keyed_groups:
# Group by the Name tag: tag_Name_webserver
- key: tags.Name
prefix: tag_Name
separator: "_"
# Group by the Role tag: role_frontend
- key: tags.Role
prefix: role
# Group by instance type: instance_type_t2_micro
- prefix: instance_type
key: instance_type
# Group by region: aws_region_us_east_1
- key: placement.region
prefix: aws_region
# Grouping via conditions
groups:
# development group for hosts tagged env=devel
development: "'devel' in (tags.Environment|lower)"
# webservers group for hosts tagged Role=web
webservers: "tags.Role == 'web'"
# Hostname configuration
hostnames:
- dns-name # Prefer the DNS name
- private-ip-address # Fallback to private IP
# Creating variables
compose:
# ansible_host will be the private IP
ansible_host: private_ip_address
# Custom variables
ec2_region: placement.region
ec2_az: placement.availability_zoneIf your Ansible control node is inside AWS with an IAM role, the plugin will automatically use the role. If it’s outside — we use credentials:
---
plugin: amazon.aws.aws_ec2
# Explicit credentials
aws_access_key: ...
aws_secret_key: ...
# Or use a profile
boto_profile: production
# Or assume a role
iam_role_arn: arn:aws:iam::123456789:role/ansible-roleGCP Compute Plugin
A similar approach for Google Cloud:
ansible-galaxy collection install google.cloud
pip3 install requests google-authCreate a service account in GCP and download the JSON key. Then the configuration:
---
plugin: google.cloud.gcp_compute
# GCP project
projects:
- my-gcp-project-id
# Authentication via service account
auth_kind: serviceaccount
service_account_file: /path/to/service-account.json
# Filtering
filters:
- status = RUNNING
- labels.environment = production
# Grouping by labels
keyed_groups:
- key: labels.role
prefix: role
- key: labels.environment
prefix: env
- key: zone
prefix: zone
# Hostname
hostnames:
- name
compose:
ansible_host: networkInterfaces[0].accessConfigs[0].natIPAzure Plugin
ansible-galaxy collection install azure.azcollection
pip3 install azure-cliAuthentication via Azure CLI:
az loginConfiguration azure_rm.yml:
---
plugin: azure.azcollection.azure_rm
# Include all subscriptions
include_vm_resource_groups:
- my-resource-group
# Filtering
exclude_host_filters:
- powerstate != 'running'
# Grouping
keyed_groups:
- prefix: tag
key: tags
- prefix: azure_loc
key: location
- prefix: azure_os
key: os_profile.system
# Hostname
hostnames:
- name
- public_ip_name
compose:
ansible_host: public_ipv4_addresses[0]NetBox Plugin
NetBox is a great open source option for the SSOT of your infrastructure, so it would be a sin not to use it as a source of information for your inventory.
ansible-galaxy collection install netbox.netbox
pip3 install pynetboxCreate the configuration file netbox_inventory.yml:
---
plugin: netbox.netbox.nb_inventory
# NetBox API endpoint and token
api_endpoint: https://netbox.example.com
token: your-netbox-api-token
# Or via environment variables
# export NETBOX_API=https://netbox.example.com
# export NETBOX_TOKEN=your-token
# SSL verification
validate_certs: true
# Filtering devices
device_query_filters:
# By status: active, offline, planned, staged, failed
- status: active
# By device roles
- role:
- server
- network
# By sites/locations
- site:
- datacenter-1
- datacenter-2
# By tags
- tag:
- production
- ansible-managed
# Filtering virtual machines
virtual_machine_query_filters:
- status: active
- cluster: production-cluster
# Grouping by NetBox attributes
group_by:
- site
- device_role
- platform
- manufacturer
- tags
- tenant
- cluster
- device_type
# Grouping via keyed_groups
keyed_groups:
# By custom fields: cf_environment_production
- key: custom_fields.environment
prefix: cf_environment
separator: "_"
# By region from a custom field: region_eu_west
- key: custom_fields.region
prefix: region
# Combination site + role: dc1_webservers
- key: 'site.slug ~ "_" ~ device_role.slug'
prefix: ""
# By rack: rack_a01
- key: rack.name
prefix: rack
separator: "_"
# Grouping via conditions
groups:
# Production servers
production_servers: >-
'production' in tags and device_role.slug == 'server'
# Critical systems
critical: >-
custom_fields.criticality == 'high' or 'critical' in tags
# Physical servers
physical: "device_role is defined"
# Virtual machines
virtual: "cluster is defined"
# Need updating
needs_update: >-
custom_fields.os_version is defined and
custom_fields.os_version < '22.04'
# Hostname configuration
compose:
# Primary IP as ansible_host (strip the subnet mask)
ansible_host: primary_ip4.address | regex_replace('/.*', '')
# Alternative via interfaces
# ansible_host: interfaces[0].ip_addresses[0].address | regex_replace('/.*', '')
# Custom fields as variables
ansible_user: custom_fields.ansible_user | default('ansible')
ansible_port: custom_fields.ssh_port | default(22)
# Additional variables
environment: custom_fields.environment | default('development')
os_version: custom_fields.os_version
backup_enabled: custom_fields.backup_enabled | default(true)
monitoring_enabled: custom_fields.monitoring | default(true)
# Site-specific settings
ntp_server: '"ntp." ~ site.slug ~ ".company.com"'
# NetBox ID for reverse integration
netbox_device_id: idComposing sources
Ansible is a tool for the lazy, but free ones, so it offers combining different types of inventories:
# Combine several sources
ansible-playbook -i static-hosts.yml -i aws_ec2.yml -i azure_rm.yml deploy.ymlOr create an inventory directory:
inventory/
├── 01-static.yml
├── 02-aws_ec2.yml
├── 03-azure_rm.yml
└── 04-gcp.ymlAs mentioned earlier, prefixes like 01-, 02- help control the order — the last loaded source can override variables from previous ones.
ansible-playbook -i inventory/ deploy.ymlCaching dynamic inventories
Dynamic inventories make API calls that can’t always boast speed and stability. To solve this problem we can enable caching:
ansible.cfg:
[defaults]
inventory = ./inventory/
[inventory]
enable_plugins = amazon.aws.aws_ec2, azure.azcollection.azure_rm, google.cloud.gcp_compute
cache = True
cache_plugin = jsonfile
cache_connection = /tmp/ansible_inventory_cache
cache_timeout = 3600Or in the plugin configuration:
---
plugin: amazon.aws.aws_ec2
regions:
- us-east-1
# Caching
cache: true
cache_timeout: 3600
cache_connection: /tmp/aws_inventory_cacheDebugging and troubleshooting
Even with a well-organized inventory, sometimes things go wrong. Here’s my golden set of debugging commands:
# Show the whole inventory in JSON
ansible-inventory -i inventory.yml --list
# Show the group graph
ansible-inventory -i inventory.yml --graph
# Show the graph with variables
ansible-inventory -i inventory.yml --graph --vars
# Show variables of a specific host
ansible-inventory -i inventory.yml --host web1
# Output in YAML (more readable)
ansible-inventory -i inventory.yml --list --yaml
# Check connectivity
ansible -i inventory.yml all -m ping
# Check a specific group
ansible -i inventory.yml webservers -m ping
# Ad-hoc command to check a variable
ansible -i inventory.yml web1 -m debug -a "var=http_port"
# Gather facts about a host
ansible -i inventory.yml web1 -m setupSome of these have already come up earlier, but this is more of a general cheat sheet
Performance optimization
For large inventories, performance becomes an important aspect, especially when a role runs on a schedule. All the settings mentioned here are applied in ansible.cfg
SSH optimization
[ssh_connection]
# ControlMaster to reuse SSH connections
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
control_path = /tmp/ansible-ssh-%%h-%%p-%%r
# SSH pipelining (fewer SSH connections)
pipelining = TrueParallelism
[defaults]
# Number of parallel processes (default: 5)
forks = 50Smart fact gathering
[defaults]
# Gather facts only when needed
gathering = smart
# Fact caching
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 86400In a playbook you can disable or limit facts:
- hosts: all
gather_facts: false # Don't gather at all
tasks:
- setup:
gather_subset:
- '!all'
- '!any'
- min # Only the minimal setExecution strategies
- hosts: webservers
# Linear: run the task on all hosts, then move to the next
strategy: linear
- hosts: appservers
# Free: hosts execute tasks independently
strategy: free
- hosts: dbservers
# In batches: 5 hosts at a time
serial: 5
- hosts: production
# In batches: 20% of hosts at a time
serial: "20%"
# Abort if >25% of hosts fail
max_fail_percentage: 25Best Practices: checklist
✅ Organization
- Use the YAML format.
- Structure by the What-Where-When principle (webservers/atlanta/production).
- Separate environments into distinct directories for large projects (
inventories/dev/,inventories/prod/). - Use
group_vars/andhost_vars/instead of variables in the inventory. - Document the structure.
✅ Variables
- Host vars override group vars (the more general is always less important).
- Playbook vars override inventory vars.
- Extra vars (
-e) override everything. - Use
{{ var | default('value') }}for optional variables. - Document variables in roles’
defaults/main.yml.
✅ Dynamic inventories
- Enable caching for speed.
- Prefix files with numbers to control load order.
- Combine static and dynamic sources.
❌ What to avoid
- Don’t mix variables from different environments.
- Don’t store everything in one huge file.
- Don’t use the same names for different concepts.
- Don’t ignore warnings from ansible-inventory.
👨💻And…
Don’t forget about our Telegram channel 📱 and chat
Or maybe you want to become a co-author? Then click here🔗
💬 All the best ✌️
That should be it. If not, check the logs 🙂


