Ansible Inventory: the complete guide

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:

BASH
# 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.yml
Click to expand and view more

Inventory solves three key tasks:

  1. Defines targets — which hosts exist and are available for automation.
  2. Organizes structure — how hosts are logically grouped.
  3. 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 format

The INI format is simple and clear for small inventories:

PLAINTEXT
# 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=production
Click to expand and view more

Important 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.

YAML
---
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: production
Click to expand and view more

Advantage 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:

YAML
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]:
Click to expand and view more

Let’s check the result:

BASH
# 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.5
Click to expand and view more

This 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:

Nested groups (children)

YAML
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"
Click to expand and view more

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:

BASH
# 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]+"
Click to expand and view more

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

YAML
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: ssh
Click to expand and view more

Key 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:

PLAINTEXT
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 db1
Click to expand and view more

Ansible looks for group_vars/ and host_vars/ in two places:

  1. Relative to the inventory file.
  2. 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:

YAML
# group_vars/webservers.yml
---
nginx_port: 80
ssl_enabled: true

application_config:
  database_host: "db.example.com"
  database_port: 5432
Click to expand and view more

Advanced organization with subdirectories

For a complex architecture, variables can be split across files:

PLAINTEXT
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.yml
Click to expand and view more

Ansible 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:

PrioritySourceScope
1command line values (e.g. -u my_user)CLI options (NOT variables)
2role defaultsrole/defaults/main.yml
3inventory file group varsGroup variables in the inventory
4inventory group_vars/allgroup_vars/all relative to inventory
5playbook group_vars/allgroup_vars/all relative to playbook
6inventory group_vars/*group_vars of groups relative to inventory
7playbook group_vars/*group_vars of groups relative to playbook
8inventory file host varsHost variables in the inventory
9inventory host_vars/*host_vars relative to inventory
10playbook host_vars/*host_vars relative to playbook
11host facts / cached set_factsCollected system facts
12play varsVariables in a play
13play vars_promptInteractive input
14play vars_filesVariable files
15role varsrole/vars/main.yml
16block varsBlock variables
17task varsTask variables
18include_varsDynamically loaded
19set_facts / registered varsSet at runtime
20role (and include_role) paramsRole parameters
21include paramsInclude parameters
22extra 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:

YAML
a_group:
  vars:
    test_var: a
b_group:
  vars:
    test_var: b
Click to expand and view more

Result: testvar == b

But priority can also be controlled via ansible_group_priority:

YAML
a_group:
  vars:
    testvar: a
    ansible_group_priority: 10
b_group:
  vars:
    testvar: b
Click to expand and view more

Now 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:

BASH
# 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]"
Click to expand and view more

Or in a playbook:

YAML
- 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: 2
Click to expand and view more

Dynamic 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:

AWS EC2 Plugin

The most popular plugin for working with AWS. First let’s install the collection:

BASH
ansible-galaxy collection install amazon.aws
pip3 install boto3 botocore
Click to expand and view more

Create the configuration file aws_ec2.yml (the name MUST end with aws_ec2.yml or aws_ec2.yaml):

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_zone
Click to expand and view more

If 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:

YAML
---
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-role
Click to expand and view more

GCP Compute Plugin

A similar approach for Google Cloud:

BASH
ansible-galaxy collection install google.cloud
pip3 install requests google-auth
Click to expand and view more

Create a service account in GCP and download the JSON key. Then the configuration:

YAML
---
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].natIP
Click to expand and view more

Azure Plugin

BASH
ansible-galaxy collection install azure.azcollection
pip3 install azure-cli
Click to expand and view more

Authentication via Azure CLI:

BASH
az login
Click to expand and view more

Configuration azure_rm.yml:

YAML
---
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]
Click to expand and view more

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.

BASH
ansible-galaxy collection install netbox.netbox
pip3 install pynetbox
Click to expand and view more

Create the configuration file netbox_inventory.yml:

YAML
---
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: id
Click to expand and view more

Composing sources

Ansible is a tool for the lazy, but free ones, so it offers combining different types of inventories:

BASH
# Combine several sources
ansible-playbook -i static-hosts.yml -i aws_ec2.yml -i azure_rm.yml deploy.yml
Click to expand and view more

Or create an inventory directory:

PLAINTEXT
inventory/
├── 01-static.yml
├── 02-aws_ec2.yml
├── 03-azure_rm.yml
└── 04-gcp.yml
Click to expand and view more

As mentioned earlier, prefixes like 01-, 02- help control the order — the last loaded source can override variables from previous ones.

BASH
ansible-playbook -i inventory/ deploy.yml
Click to expand and view more

Caching 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:

PLAINTEXT
[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 = 3600
Click to expand and view more

Or in the plugin configuration:

YAML
---
plugin: amazon.aws.aws_ec2
regions:
  - us-east-1

# Caching
cache: true
cache_timeout: 3600
cache_connection: /tmp/aws_inventory_cache
Click to expand and view more

Debugging and troubleshooting

Even with a well-organized inventory, sometimes things go wrong. Here’s my golden set of debugging commands:

BASH
# 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 setup
Click to expand and view more

Some 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

PLAINTEXT
[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 = True
Click to expand and view more

Parallelism

PLAINTEXT
[defaults]
# Number of parallel processes (default: 5)
forks = 50
Click to expand and view more

Smart fact gathering

PLAINTEXT
[defaults]
# Gather facts only when needed
gathering = smart

# Fact caching
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 86400
Click to expand and view more

In a playbook you can disable or limit facts:

YAML
- hosts: all
  gather_facts: false  # Don't gather at all
  tasks:
    - setup:
        gather_subset:
          - '!all'
          - '!any'
          - min  # Only the minimal set
Click to expand and view more

Execution strategies

YAML
- 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: 25
Click to expand and view more

Best Practices: checklist

✅ Organization

✅ Variables

✅ Dynamic inventories

❌ What to avoid

Copyright Notice

Author: Ivan Cherniy

Link: https://r4ven.me/en/clippings/ansible-inventory-polnoe-rukovodstvo/

License: CC BY-NC-SA 4.0

Blog materials may be used with attribution to the author and source, for non-commercial purposes, and under the same license.

Start searching

Enter keywords to search articles

↑↓
ESC
⌘K Shortcut