Static inventory files become unmanageable as your infrastructure grows and changes. Dynamic inventory scripts and plugins query your cloud provider's API to automatically discover servers, their IPs, and metadata. This guide covers building and using dynamic inventory for Ansible with cloud providers and custom APIs.
Why Dynamic Inventory?
- Auto-discovery: New servers appear automatically — no manual inventory updates
- Accurate: Always reflects the current state of your infrastructure
- Metadata as groups: Automatically group servers by tags, regions, or roles
- Scalable: Works whether you have 5 or 5,000 servers
Hetzner Cloud Dynamic Inventory
# Install the Hetzner collection
ansible-galaxy collection install hetzner.hcloud
# inventory/hcloud.yml
plugin: hetzner.hcloud.hcloud
token: "{{ lookup('env', 'HCLOUD_TOKEN') }}"
# Group servers by labels
keyed_groups:
- key: labels.environment
prefix: env
separator: "_"
- key: labels.role
prefix: role
separator: "_"
- key: location
prefix: location
separator: "_"
# Filter servers
filters:
- labels.managed_by == "ansible"
# Set connection variables
compose:
ansible_host: ipv4_address
ansible_user: "'deploy'"
# Cache for performance
cache: true
cache_plugin: jsonfile
cache_connection: /tmp/ansible-inventory-cache
cache_timeout: 300
# Test it
# HCLOUD_TOKEN=your-token ansible-inventory -i inventory/hcloud.yml --list
# HCLOUD_TOKEN=your-token ansible-inventory -i inventory/hcloud.yml --graph
AWS EC2 Dynamic Inventory
# inventory/aws_ec2.yml
plugin: amazon.aws.aws_ec2
regions:
- us-east-1
- us-west-2
# Filter by tags
filters:
tag:ManagedBy: ansible
instance-state-name: running
# Group by tags and attributes
keyed_groups:
- key: tags.Environment
prefix: env
- key: tags.Role
prefix: role
- key: placement.availability_zone
prefix: az
- key: instance_type
prefix: type
# Set host variables
compose:
ansible_host: public_ip_address | default(private_ip_address)
ansible_user: "'ubuntu'"
# Use Name tag as hostname
hostnames:
- tag:Name
- private-ip-address
# Cache
cache: true
cache_plugin: jsonfile
cache_connection: /tmp/aws-inventory-cache
cache_timeout: 600
Custom Dynamic Inventory Script
#!/usr/bin/env python3
# inventory/custom_inventory.py
"""
Custom dynamic inventory script for internal API.
Usage: ansible-playbook -i inventory/custom_inventory.py playbook.yml
"""
import json
import sys
import requests
import os
from argparse import ArgumentParser
def get_inventory():
"""Fetch server inventory from internal API."""
api_url = os.environ.get('INVENTORY_API_URL', 'https://api.example.com/servers')
api_token = os.environ.get('INVENTORY_API_TOKEN', '')
headers = {'Authorization': f'Bearer {api_token}'}
response = requests.get(api_url, headers=headers)
servers = response.json()
inventory = {
'_meta': {'hostvars': {}},
'all': {'children': ['webservers', 'databases', 'monitoring']},
'webservers': {'hosts': []},
'databases': {'hosts': []},
'monitoring': {'hosts': []},
}
for server in servers:
hostname = server['hostname']
role = server.get('role', 'webservers')
env = server.get('environment', 'production')
# Add to role group
if role not in inventory:
inventory[role] = {'hosts': []}
inventory['all']['children'].append(role)
inventory[role]['hosts'].append(hostname)
# Add to environment group
env_group = f'env_{env}'
if env_group not in inventory:
inventory[env_group] = {'hosts': []}
inventory['all']['children'].append(env_group)
inventory[env_group]['hosts'].append(hostname)
# Set host variables
inventory['_meta']['hostvars'][hostname] = {
'ansible_host': server['ip_address'],
'ansible_user': server.get('ssh_user', 'deploy'),
'server_id': server['id'],
'datacenter': server.get('datacenter', 'us-east'),
'os': server.get('os', 'ubuntu-24.04'),
}
return inventory
def get_host(hostname):
"""Get variables for a specific host."""
inventory = get_inventory()
return inventory['_meta']['hostvars'].get(hostname, {})
def main():
parser = ArgumentParser()
parser.add_argument('--list', action='store_true')
parser.add_argument('--host', type=str)
args = parser.parse_args()
if args.list:
print(json.dumps(get_inventory(), indent=2))
elif args.host:
print(json.dumps(get_host(args.host), indent=2))
else:
parser.print_help()
sys.exit(1)
if __name__ == '__main__':
main()
# Make executable and test
chmod +x inventory/custom_inventory.py
./inventory/custom_inventory.py --list | python -m json.tool
# Use with Ansible
ansible-playbook -i inventory/custom_inventory.py playbook.yml
Combining Static and Dynamic Inventory
# Use a directory as inventory source — Ansible merges all files
inventory/
├── static_hosts.yml # Static entries (network devices, etc.)
├── hcloud.yml # Hetzner Cloud dynamic inventory
├── aws_ec2.yml # AWS dynamic inventory
└── custom_inventory.py # Custom API inventory
# ansible.cfg
[defaults]
inventory = ./inventory
# Ansible merges all sources automatically
ansible-inventory --list # Shows combined inventory
Inventory Plugins vs Scripts
| Feature | Inventory Plugin (YAML) | Inventory Script (Python) |
|---|---|---|
| Caching | Built-in | Manual implementation |
| Configuration | YAML file | Environment variables |
| Maintenance | Community-maintained | You maintain it |
| Flexibility | Provider-specific | Unlimited — any API |
| Best for | Standard cloud providers | Custom/internal systems |
Best Practices
- Enable caching: Dynamic inventory queries APIs on every run — cache to avoid rate limits and latency
- Use keyed_groups: Automatically group servers by tags, regions, and roles
- Combine static and dynamic: Use an inventory directory for mixed environments
- Tag your servers: Good tagging in your cloud provider = good Ansible groups automatically
- Set reasonable cache timeouts: 5-10 minutes for development, 30-60 minutes for stable production
- Test with
ansible-inventory --graphto visualize your inventory structure