Ansible Host Groups — Organize Inventory for Targeted Automation

Introduction

Host groups are how you organize servers in Ansible inventory. Instead of listing individual hosts in every playbook, you group them by function (webservers, databases), environment (production, staging), location (us-east, eu-west), or any criteria that makes sense for your infrastructure. This guide covers group structure, nesting, variables, and targeting patterns.

Basic Group Structure

INI Format

# inventory.ini
[webservers]
web01.example.com
web02.example.com
web03.example.com

[databases]
db01.example.com
db02.example.com

[loadbalancers]
lb01.example.com

[monitoring]
mon01.example.com

YAML Format

# inventory.yml
all:
  children:
    webservers:
      hosts:
        web01.example.com:
        web02.example.com:
        web03.example.com:
    databases:
      hosts:
        db01.example.com:
        db02.example.com:
    loadbalancers:
      hosts:
        lb01.example.com:
    monitoring:
      hosts:
        mon01.example.com:

Nested Groups (Children)

# inventory.yml
all:
  children:
    # Environment groups
    production:
      children:
        prod_web:
          hosts:
            web01.example.com:
            web02.example.com:
        prod_db:
          hosts:
            db01.example.com:

    staging:
      children:
        stage_web:
          hosts:
            web-stage01.example.com:
        stage_db:
          hosts:
            db-stage01.example.com:

    # Function groups (same hosts, different grouping)
    webservers:
      children:
        prod_web:
        stage_web:

    databases:
      children:
        prod_db:
        stage_db:

A host can belong to multiple groups:

web01.example.com is in:
  - prod_web
  - production (parent of prod_web)
  - webservers (parent of prod_web)
  - all (always)

Group Variables

Directory Structure

project/
├── inventory.yml
├── group_vars/
│   ├── all.yml              # Applied to every host
│   ├── webservers.yml       # Applied to webservers group
│   ├── databases.yml        # Applied to databases group
│   ├── production.yml       # Applied to production group
│   └── staging.yml          # Applied to staging group
├── host_vars/
│   ├── web01.example.com.yml
│   └── db01.example.com.yml
└── playbooks/
    └── site.yml

Variable Files

# group_vars/all.yml
---
ntp_servers:
  - ntp1.example.com
  - ntp2.example.com
dns_servers:
  - 10.0.0.53
  - 10.0.0.54
admin_email: ops@example.com

# group_vars/webservers.yml
---
nginx_worker_processes: auto
nginx_worker_connections: 4096
app_port: 8080
ssl_certificate: /etc/ssl/certs/web.pem

# group_vars/databases.yml
---
postgresql_version: 16
postgresql_max_connections: 500
backup_schedule: "0 2 * * *"
backup_retention_days: 30

# group_vars/production.yml
---
env_name: production
log_level: warn
monitoring_enabled: true
backup_enabled: true

# group_vars/staging.yml
---
env_name: staging
log_level: debug
monitoring_enabled: false
backup_enabled: false

Variable Precedence with Groups

Priority (low → high):
1. all group vars
2. parent group vars
3. child group vars
4. host_vars
5. play vars
6. extra vars (-e)

Example for web01 in prod_web → production → webservers:
  all.yml          →  ntp_servers, dns_servers
  production.yml   →  env_name=production, log_level=warn
  webservers.yml   →  nginx_worker_processes, app_port
  web01.yml        →  override anything for this specific host

Targeting Groups in Playbooks

# Run on specific group
- name: Configure web servers
  hosts: webservers
  tasks:
    - name: Install Nginx
      ansible.builtin.package:
        name: nginx
        state: present

# Run on multiple groups
- name: Update all servers
  hosts: webservers:databases:loadbalancers
  tasks:
    - name: Update packages
      ansible.builtin.package:
        name: "*"
        state: latest

# Run on intersection (hosts in BOTH groups)
- name: Production web servers only
  hosts: webservers:&production
  tasks:
    - name: Deploy app
      ansible.builtin.copy:
        src: app.tar.gz
        dest: /opt/app/

# Exclude a group
- name: All except staging
  hosts: all:!staging
  tasks:
    - name: Production task
      ansible.builtin.debug:
        msg: "Not staging"

Command Line Targeting

# Target specific group
ansible webservers -m ping
ansible-playbook site.yml --limit webservers

# Multiple groups
ansible 'webservers:databases' -m ping

# Intersection
ansible 'webservers:&production' -m ping

# Exclusion
ansible 'all:!staging' -m ping

# Regex pattern
ansible '~web\d+' -m ping

# Specific hosts within a group
ansible 'webservers[0]' -m ping      # First host
ansible 'webservers[0:2]' -m ping    # First three hosts

Built-in Groups

Ansible provides two groups automatically:

# 'all' — every host in inventory
- hosts: all

# 'ungrouped' — hosts not in any group (except 'all')
- hosts: ungrouped

Multi-Environment Inventory

Separate Files

inventories/
├── production/
│   ├── hosts.yml
│   └── group_vars/
│       ├── all.yml
│       └── webservers.yml
├── staging/
│   ├── hosts.yml
│   └── group_vars/
│       ├── all.yml
│       └── webservers.yml
└── development/
    ├── hosts.yml
    └── group_vars/
        └── all.yml
# Target specific environment
ansible-playbook site.yml -i inventories/production/
ansible-playbook site.yml -i inventories/staging/

Dynamic Group Membership

# Add hosts to groups dynamically
- name: Classify hosts
  hosts: all
  tasks:
    - name: Add to docker_hosts group
      ansible.builtin.group_by:
        key: "docker_hosts"
      when: "'docker' in ansible_facts.packages"

    - name: Group by OS
      ansible.builtin.group_by:
        key: "os_{{ ansible_distribution | lower }}"

- name: Configure Docker hosts
  hosts: docker_hosts
  tasks:
    - name: Docker-specific config
      ansible.builtin.debug:
        msg: "This host has Docker installed"

Troubleshooting

IssueSolution
"No hosts matched"Check group name spelling; run ansible-inventory --list
Variables not appliedCheck group_vars/ directory name matches group name exactly
Wrong variable valueCheck precedence: child group vars override parent
Host in wrong groupRun ansible-inventory --graph to visualize
group_vars not loadedDirectory must be next to inventory file or in project root
# Debug inventory structure
ansible-inventory -i inventory.yml --graph
ansible-inventory -i inventory.yml --list
ansible-inventory -i inventory.yml --host web01.example.com

Best Practices

  1. Group by function AND environment — use nested groups so hosts belong to both
  2. Use group_vars/all.yml for truly global settings — NTP, DNS, admin contacts
  3. Keep host_vars minimal — most config should come from groups
  4. Separate inventories per environment — inventories/production/, inventories/staging/
  5. Name groups descriptively — prod_webservers not group1
  6. Use ansible-inventory --graph — visualize before running playbooks

Conclusion

Host groups are the foundation of organized Ansible automation. By grouping servers by function and environment, you write playbooks once and target them precisely. Combined with group_vars, each group gets its own configuration without duplicating variables across hosts. Start simple (one group per function), add environment groups as you grow, and use nested groups to combine both dimensions.