Back to Linux guides

Mastering Ansible Inventory Files: A Beginner's Guide

Ayooluwa Isaiah
Updated on March 19, 2025

Ansible is an open-source automation tool that simplifies complex tasks like configuration management, application deployment, and orchestration.

Unlike many other automation solutions, Ansible requires no agents on managed nodes and operates over standard SSH connections, making it lightweight and easy to implement.

At the heart of Ansible's functionality lies the inventory file - a seemingly simple component that holds immense power when properly utilized.

This guide will walk you through everything you need to know about Ansible inventory files, from basic concepts to advanced techniques, with practical examples you can try on your local machine.

By the end of this article, you'll have a solid understanding of how to structure and manage Ansible inventories effectively.

Inventory file basics

An Ansible inventory file is essentially a list of hosts that Ansible can communicate with and manage. In its most basic form, it's simply a text file containing hostnames or IP addresses. However, it can be extended to include grouping information, connection details, and variables that control how Ansible interacts with each host.

By default, Ansible looks for the inventory file at /etc/ansible/hosts. For local testing and development, however, you'll typically supply your own inventory file using the -i parameter when running Ansible commands.

Consider this basic inventory file:

inventory.ini
web1.example.com
web2.example.com
db1.example.com

This simple inventory lists three hosts that Ansible can connect to. However, without any additional organization, you'd have to manage each host individually, which quickly becomes unwieldy as your infrastructure grows.

Static vs dynamic inventories

Ansible supports two types of inventories:

  1. Static inventories: Text files that explicitly list all hosts and groups. These are straightforward and version-controllable, making them ideal for stable environments.

  2. Dynamic inventories: Scripts or plugins that generate inventory information on the fly, often by querying external sources like cloud providers or CMDBs. Dynamic inventories are particularly useful for rapidly changing environments, but for this article, we'll focus primarily on static inventories since they're easier to work with locally.

Supported formats

Ansible inventory files can be written in two primary formats:

  1. INI format: The traditional and more commonly used format that resembles Windows INI files. This format is simple to understand and edit, making it accessible for beginners.

  2. YAML format: Provides more structural clarity for complex inventories. YAML offers better readability and organization for larger inventories, though it requires stricter syntax.

Both formats offer the same capabilities, so the choice largely comes down to preference and the complexity of your inventory. For most examples in this article, we'll use the INI format for simplicity, but we'll also show YAML equivalents where relevant.

Creating your first inventory file

Let's start by creating a simple inventory file in INI format. This will help you understand the basic structure before we add more advanced features.

In the INI format, hosts are specified either individually or under group headers in square brackets. Groups allow you to organize hosts logically and target them collectively.

inventory.ini
# Simple inventory example

# Individual hosts listed without a group are part of the 'ungrouped' group
server1.example.com
192.168.1.10

# A group of web servers
[webservers]
web1.example.com
web2.example.com
web3.example.com

# A group of database servers
[dbservers]
db1.example.com
db2.example.com

This basic inventory separates servers into logical groups based on their function. The square brackets define group names, and the hosts listed under each group belong to that group. This organization enables you to run commands or playbooks against all web servers or all database servers with a single command.

The same inventory in YAML format would look like this:

inventory.yml
all:
 hosts:
   server1.example.com:
   192.168.1.10:
 children:
   webservers:
     hosts:
       web1.example.com:
       web2.example.com:
       web3.example.com:
   dbservers:
     hosts:
       db1.example.com:
       db2.example.com:

While the YAML format requires more indentation and structure, it can be more readable for complex inventories and integrates well with other YAML-based Ansible files.

For local testing purposes, you can create an inventory that targets your localhost:

local_inventory.ini
localhost ansible_connection=local

[test_servers]
localhost ansible_connection=local

In this example, we've specified ansible_connection=local which tells Ansible to execute commands directly on the local machine rather than trying to establish an SSH connection.

To test this inventory, save it as local_inventory.ini and run a simple ping command:

 
ansible -i local_inventory.ini all -m ping
Output
localhost | SUCCESS => {
   "changed": false,
   "ping": "pong"
}

The successful pong response confirms that your inventory is working correctly. This simple test verifies that Ansible can find your hosts in the inventory file and communicate with them. The ping module is a lightweight way to test connectivity without making any changes to the systems.

Ansible inventory output

Understanding host patterns

Host patterns allow you to target specific hosts or groups when running Ansible commands. This provides fine-grained control over which machines receive what commands. Host patterns are specified as arguments to Ansible commands and playbooks, determining which hosts will execute the requested tasks.

Let's expand our earlier inventory example to demonstrate how host patterns work:

expanded_inventory.ini
# Web servers
[webservers]
web1.example.com
web2.example.com
web3.example.com

# Database servers
[dbservers]
db1.example.com
db2.example.com

# Load balancers
[loadbalancers]
lb1.example.com
lb2.example.com

# Application grouping
[application:children]
webservers
dbservers

# Environment grouping
[production]
web1.example.com
db1.example.com
lb1.example.com

[staging]
web2.example.com
db2.example.com
lb2.example.com

[development]
web3.example.com

Notice how we've organized servers in multiple ways - by function (webservers, dbservers, loadbalancers) and by environment (production, staging, development). We've also created a parent group called application that includes both webservers and dbservers. This multi-dimensional grouping gives us flexibility in how we target our automation.

With this inventory, you can target hosts in multiple ways such as:

  • Target a specific host: ansible -i inventory.ini web1.example.com -m ping
  • Target a specific group: ansible -i inventory.ini webservers -m ping
  • Target multiple groups: ansible -i inventory.ini webservers:dbservers -m ping
  • Target all hosts: ansible -i inventory.ini all -m ping
  • Use wildcards: ansible -i inventory.ini 'web*.example.com' -m ping
  • Use exclusions: ansible -i inventory.ini 'webservers:!web3.example.com' -m ping (all webservers except web3)
  • Use intersections: ansible -i inventory.ini 'webservers:&production' -m ping (servers that are both webservers and in production)

These pattern expressions give you precise control over which hosts receive your commands. The ability to combine patterns with colons (:), exclamation points (!), and ampersands (&) lets you create complex selections without having to define explicit groups for every possible combination.

For local testing, you could create multiple localhost entries with different aliases to simulate a more complex environment:

multi_local.ini
localhost ansible_connection=local
local-web ansible_host=127.0.0.1 ansible_connection=local
local-db ansible_host=127.0.0.1 ansible_connection=local

[webservers]
local-web

[dbservers]
local-db

[development]
local-web
local-db

Even though all hosts in this inventory are actually the same machine (your local computer), Ansible treats them as separate entities based on their inventory names. The ansible_host=127.0.0.1 parameter tells Ansible that the hostname resolves to your local machine's loopback address.

This allows you to test patterns like:

 
ansible -i multi_local.ini webservers -m debug -a "msg='This targets the web server'"
Output
local-web | SUCCESS => {
   "msg": "This targets the web server"
}

And you can verify that your pattern selections work as expected:

 
ansible -i multi_local.ini 'webservers:!dbservers' --list-hosts
Output
 hosts (1):
   local-web

This approach lets you practice and test complex host patterns locally before applying them to your production infrastructure.

Understanding host variables

Variables allow you to customize how Ansible interacts with each host or group of hosts. They can be specified directly in the inventory file, making it easy to maintain host-specific configuration details. Variables defined in the inventory can be used in playbooks, templates, and command-line operations.

Here's an example of an inventory file with host and group variables:

inventory_with_vars.ini
# Host variables example

# Single host with connection variables
web1.example.com ansible_host=192.168.1.101 ansible_user=admin ansible_port=2222

[webservers]
web1.example.com
web2.example.com http_port=8080 max_connections=1000
web3.example.com http_port=8081 max_connections=500

[dbservers]
db1.example.com db_port=5432 backup_time=midnight
db2.example.com db_port=5433 backup_time=2am

[webservers:vars]
app_environment=production
deployment_user=deploy
http_port=80

In this example, we've defined variables in three different ways:

  1. Host connection variables: For web1.example.com, we've set connection-specific variables using the ansible_ prefix (ansible_host, ansible_user, ansible_port). These special variables control how Ansible connects to the host.

  2. Inline host variables: For web2.example.com and others, we've set application-specific variables (http_port, max_connections) directly on the same line as the host. This is convenient for setting a few variables per host.

  3. Group variables: For the entire webservers group, we've set default values using the [groupname:vars] syntax. These variables apply to all hosts in the group unless overridden by host-specific variables.

Variable precedence is important to understand: host-specific variables take precedence over group variables. For example, web1.example.com will use the default http_port=80 from the group variables, while web2.example.com will use its own setting of http_port=8080.

You can verify variables using the following command:

 
ansible -i inventory_with_vars.ini web2.example.com -m debug -a "var=http_port"
Output
web2.example.com | SUCCESS => {
   "http_port": "8080"
}

For local testing, you can define different roles for your localhost:

local_vars.ini
[local_web]
localhost ansible_connection=local http_port=8080 app_path=/var/www

[local_db]
localhost ansible_connection=local db_port=5432 db_path=/var/lib/postgresql

[local_web:vars]
server_role=web
environment=development

[local_db:vars]
server_role=database
environment=development

This setup allows you to test variable assignments and their effects on playbooks locally. You can verify which variables are assigned to a specific host with:

 
ansible -i local_vars.ini local_web -m debug -a "var=hostvars[inventory_hostname]"
Output
localhost | SUCCESS => {
    "hostvars[inventory_hostname]": {
        "ansible_check_mode": false,
        "ansible_config_file": "/etc/ansible/ansible.cfg",
        "ansible_connection": "local",
        "ansible_diff_mode": false,
        "ansible_facts": {},
        "ansible_forks": 5,
        "ansible_inventory_sources": [
            "/home/ayo/dev/betterstack/demo/ansible-inventory/local_vars.ini"
        ],
        . . .
    }
}

Ansible inventory variables

You can use these variables in your playbooks with the {{ variable_name }} syntax. For example:

webserver_playbook.yml
---
- name: Configure web servers
 hosts: local_web
 tasks:
   - name: Print configuration message
     debug:
       msg: "Configuring web server with port {{ http_port }} and environment {{ environment }}"

   - name: Create document root
     file:
       path: "{{ app_path }}"
       state: directory
       mode: '0755'
     when: server_role == "web"

This demonstrates how inventory variables make your playbooks more flexible and reusable across different environments and server configurations.

Group variables and hierarchy

As your inventory grows, you'll want to create a more structured hierarchy to simplify management. Ansible allows you to define parent-child relationships between groups using the [groupname:children] syntax. This creates nested groups where properties can be inherited from parent to child groups.

Let's look at a more complex hierarchical inventory:

hierarchical_inventory.ini
# Define base server types
[web]
web1.example.com
web2.example.com
web3.example.com

[db]
db1.example.com
db2.example.com

# Define environments
[dev]
web1.example.com
db1.example.com

[staging]
web2.example.com
db2.example.com

[prod]
web3.example.com

# Create parent groups
[application:children]
web
db

[test_environments:children]
dev
staging

# Define variables for groups
[application:vars]
ansible_user=deploy
backup_enabled=yes

[web:vars]
http_port=80
document_root=/var/www/html

[db:vars]
db_port=5432
backup_frequency=daily

[dev:vars]
environment_type=development
debug_enabled=yes

[staging:vars]
environment_type=staging
debug_enabled=yes

[prod:vars]
environment_type=production
debug_enabled=no

This hierarchical structure provides several powerful advantages:

  • Multi-dimensional organization: Servers can belong to multiple logical groups simultaneously (by function and by environment).
  • Variable inheritance: Variables cascade down through the hierarchy, with more specific groups or hosts overriding values from parent groups.
  • Targeted operations: Commands can target broad categories (application) or specific intersections (web:&dev).

Let's break down how this works:

  1. We first define basic functional groups (web, db) containing our servers.
  2. We then define environment groups (dev, staging, prod) containing the same servers organized differently.
  3. We create parent groups that combine related groups (application includes both web and db).
  4. We set variables at different levels of the hierarchy.

A host like web1.example.com is a member of both the web group and the dev group, which means it inherits variables from both. If there are conflicts, the most specific assignment wins.

For example, you could run a command on all web servers in the development environment:

 
ansible -i hierarchical_inventory.ini 'web:&dev' -m debug -a "msg='Dev web server with port {{ http_port }} and environment type {{ environment_type }}'"
Output
web1.example.com | SUCCESS => {
   "msg": "Dev web server with port 80 and environment type development"
}

The special group all is always available and encompasses every host in your inventory. You can set variables for all hosts using [all:vars].

Setting up connection parameters

Ansible needs to know how to connect to each host in your inventory. While SSH is the default connection method, you can customize various parameters to suit your environment. Proper connection settings are crucial for Ansible to securely and reliably communicate with your managed nodes.

Here's an inventory file with various connection parameters:

connection_inventory.ini
# Basic connection parameters
web1.example.com ansible_host=192.168.1.101
web2.example.com ansible_host=192.168.1.102 ansible_port=2222
web3.example.com ansible_host=192.168.1.103 ansible_user=admin

# Advanced connection parameters
db1.example.com ansible_host=192.168.1.201 ansible_connection=ssh ansible_ssh_private_key_file=/path/to/private_key
db2.example.com ansible_host=192.168.1.202 ansible_connection=ssh ansible_ssh_common_args='-o StrictHostKeyChecking=no'

# Local connection
localhost ansible_connection=local

Let's look at these connection parameters in detail:

  • ansible_host: Specifies the actual hostname or IP address that Ansible should connect to. This is useful when the inventory hostname is different from the actual network address, such as when using aliases or when host resolution isn't available.

  • ansible_port: Defines the port for the SSH connection (defaults to 22). This is necessary when the SSH service is running on a non-standard port.

  • ansible_user: Specifies the username Ansible should use when connecting to the host. Different servers might require different user accounts for management.

  • ansible_password: The password to use for authentication. Note that storing passwords in plain text is not recommended for production; consider using Ansible Vault instead.

  • ansible_connection: Specifies the connection type. Common values include:

    • ssh: The default, uses SSH for remote connections
    • local: Executes commands on the control node itself
    • docker: Connects to Docker containers
    • winrm: For Windows Remote Management (Windows hosts)
  • ansible_ssh_private_key_file: Path to the SSH private key file to use for authentication. This enables key-based authentication instead of password authentication.

  • ansible_ssh_common_args: Additional arguments to pass to the SSH command. This allows you to customize SSH behavior, such as disabling strict host key checking for testing environments.

For local testing, you can simulate different connection methods:

local_connections.ini
# Local connections for testing
local1 ansible_connection=local
local2 ansible_connection=local ansible_python_interpreter=/usr/bin/python3

The ansible_python_interpreter variable specifies which Python executable Ansible should use on the target host. This is particularly useful when testing with different Python versions.

You can verify that the correct connection parameters are being used:

 
ansible -i local_connections.ini local2 -m debug -a "var=ansible_python_interpreter"
Output
local2 | SUCCESS => {
   "ansible_python_interpreter": "/usr/bin/python3"
}

Understanding these connection parameters allows you to configure Ansible to work with diverse environments, from standard SSH servers to containers, cloud instances with specific security requirements, or even the local system for testing.

Organizing complex inventories

As your infrastructure grows, maintaining a single inventory file becomes unwieldy. Ansible allows you to split your inventory into multiple files and directories, making it easier to manage at scale. This approach keeps your inventory organized and maintainable even as it grows to hundreds or thousands of hosts.

A common approach is to organize inventory files by environment:

 
inventory/
├── production/
│   ├── hosts.ini
│   ├── group_vars/
│   └── host_vars/
├── staging/
│   ├── hosts.ini
│   ├── group_vars/
│   └── host_vars/
└── development/
├── hosts.ini
├── group_vars/
└── host_vars/

With this structure, each environment has its own hosts file and variable directories. This separation makes it easy to manage different environments independently and prevents accidental changes to production when modifying development settings.

You can then target a specific environment by pointing Ansible to the appropriate directory:

 
ansible-playbook -i inventory/development site.yml

For local testing, you might structure your inventory like this:

 
local_inventory/
├── inventory.ini
├── group_vars/
│   ├── all.yml
│   ├── web.yml
│   └── db.yml
└── host_vars/
└── localhost.yml

With the following content:

inventory.ini
[web]
localhost ansible_connection=local

[db]
localhost ansible_connection=local
all.yml
environment: development
debug_mode: true
web.yml
server_role: web
http_port: 8080
document_root: /var/www/html
db.yml
server_role: database
db_port: 5432
db_path: /var/lib/postgresql
localhost.yml
ansible_python_interpreter: /usr/bin/python3
local_user: "{{ lookup('env', 'USER') }}"

This approach separates your inventory structure from its configuration data, making both easier to maintain. The group_vars directory contains files named after groups, and any variables defined in these files are automatically available to all hosts in the corresponding group. Similarly, the host_vars directory contains files named after individual hosts.

The advantage of this structure is that you can keep sensitive or frequently changing variables separate from your inventory structure. It's also easier to manage version control, as different team members might be responsible for different parts of the configuration.

You can even combine this directory-based approach with dynamic inventories or inventory plugins for maximum flexibility.

Validating and testing inventories

Before deploying your inventory in production, it's important to validate its structure and test that it works as expected. Ansible provides several tools to help with this process, allowing you to catch errors before they impact your automation.

The ansible-inventory command is a powerful tool for examining and visualizing your inventory:

 
ansible-inventory -i inventory.ini --list
Output
{
    "_meta": {
        "hostvars": {
            "db1.example.com": {
                "ansible_host": "192.168.1.201",
                "db_port": 5432
            },
            "web1.example.com": {
                "ansible_host": "192.168.1.101",
                "http_port": 80
            }
        }
    },
    "all": {
        "children": [
            "db",
            "ungrouped",
            "web"
        ]
    },
    "db": {
        "hosts": [
            "db1.example.com"
        ]
    },
    "web": {
        "hosts": [
            "web1.example.com"
        ]
    }
}

This command outputs the complete inventory in JSON format, including all hosts, groups, and variables. This is particularly useful for verifying that your inventory structure matches your expectations and that variables are being set correctly.

You can also generate a visual graph of your inventory hierarchy:

 
ansible-inventory -i inventory.ini --graph
Output
@all:
  |--@db:
  |  |--db1.example.com
  |--@ungrouped:
  |--@web:
  |  |--web1.example.com

This graphical representation makes it easy to visualize the relationships between groups and hosts, which is especially valuable for complex hierarchical inventories.

To test if variables are being properly assigned, use the debug module:

 
ansible -i inventory.ini all -m debug -a "var=hostvars[inventory_hostname]"

This command will show all variables defined for each host, including those inherited from groups. It's a great way to verify that variable precedence is working as expected and that hosts have the correct configuration.

For testing connectivity to your hosts:

 
ansible -i inventory.ini all -m ping

The ping module verifies that Ansible can successfully connect to each host and that Python is correctly installed and accessible. This basic connectivity test should be your first step when troubleshooting inventory issues.

If you're working with a complex inventory, you can test specific group membership:

 
ansible -i inventory.ini 'web:&production' --list-hosts
Output
  hosts (1):
    web1.example.com

This command lists all hosts that are members of both the "web" and "production" groups, allowing you to verify that your group definitions and patterns work as expected.

By thoroughly testing your inventory before deployment, you can avoid many common issues and ensure that your automation targets the correct hosts with the appropriate configuration.

Final thoughts

Ansible inventory files form the foundation of effective infrastructure automation. They define not just what servers you manage, but how they're organized, accessed, and configured. Throughout this guide, we've explored how to build inventories from simple host lists to complex hierarchical structures with variables, patterns, and organization strategies.

The true power of Ansible inventories lies in their flexibility. You can start with a basic setup and gradually refine it as your needs evolve. Whether you're managing a handful of local servers or thousands of cloud instances, the principles remain the same: group logically, structure hierarchically, and separate hosts from their configuration.

As you implement these concepts in your own environments, remember that a well-designed inventory makes your automation more precise, maintainable, and scalable. Take time to plan your structure, test thoroughly, and refactor when necessary. Your future self—and teammates—will thank you when it comes time to expand or troubleshoot your automation.

Ansible inventories may seem simple on the surface, but as we've seen, they provide powerful capabilities that grow alongside your infrastructure. Master them, and you've mastered a fundamental component of successful automation.

Thanks for reading!

Author's avatar
Article by
Ayooluwa Isaiah
Ayo is a technical content manager at Better Stack. His passion is simplifying and communicating complex technical ideas effectively. His work was featured on several esteemed publications including LWN.net, Digital Ocean, and CSS-Tricks. When he's not writing or coding, he loves to travel, bike, and play tennis.
Got an article suggestion? Let us know
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Make your mark

Join the writer's program

Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.

Write for us
Writer of the month
Marin Bezhanov
Marin is a software engineer and architect with a broad range of experience working...
Build on top of Better Stack

Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.

community@betterstack.com

or submit a pull request and help us build better products for everyone.

See the full list of amazing projects on github