Ansible group_by — Create Dynamic Groups at Runtime
Introduction
ansible.builtin.group_by creates host groups dynamically during playbook execution based on facts or variables. Instead of maintaining static inventory groups for every OS, datacenter, or role combination, group_by builds them automatically — then you target those groups in subsequent plays.
Basic Usage
---
- name: Organize hosts by OS
hosts: all
tasks:
- name: Group by OS family
ansible.builtin.group_by:
key: "os_{{ ansible_os_family | lower }}"
# Creates groups: os_debian, os_redhat, os_suse, etc.
- name: Configure Debian hosts
hosts: os_debian
tasks:
- name: Configure APT
ansible.builtin.template:
src: sources.list.j2
dest: /etc/apt/sources.list
- name: Configure RedHat hosts
hosts: os_redhat
tasks:
- name: Configure DNF
ansible.builtin.template:
src: dnf.conf.j2
dest: /etc/dnf/dnf.conf
Common Grouping Patterns
By Distribution
- name: Group by distribution
ansible.builtin.group_by:
key: "distro_{{ ansible_distribution | lower | replace(' ', '_') }}"
# Creates: distro_ubuntu, distro_centos, distro_rocky, etc.
- name: Group by distribution version
ansible.builtin.group_by:
key: "distro_{{ ansible_distribution | lower }}_{{ ansible_distribution_major_version }}"
# Creates: distro_ubuntu_22, distro_ubuntu_24, distro_rocky_9
By Architecture
- name: Group by CPU architecture
ansible.builtin.group_by:
key: "arch_{{ ansible_architecture }}"
# Creates: arch_x86_64, arch_aarch64
By Memory/Capacity
- name: Group by memory tier
ansible.builtin.group_by:
key: >-
mem_{{ 'small' if ansible_memtotal_mb < 4096
else 'medium' if ansible_memtotal_mb < 16384
else 'large' }}
# Creates: mem_small, mem_medium, mem_large
By Network
- name: Group by subnet
ansible.builtin.group_by:
key: "subnet_{{ ansible_default_ipv4.network | replace('.', '_') }}"
# Creates: subnet_192_168_1_0, subnet_10_0_0_0
- name: Group by datacenter
ansible.builtin.group_by:
key: "dc_{{ datacenter | default('unknown') }}"
By Custom Variable
- name: Group by application role
ansible.builtin.group_by:
key: "role_{{ app_role | default('unassigned') }}"
# Creates: role_web, role_db, role_cache, role_unassigned
- name: Group by environment
ansible.builtin.group_by:
key: "env_{{ env | default('dev') }}"
# Creates: env_dev, env_staging, env_production
Multi-Play Pattern
# Play 1: Gather facts and group
- name: Classify all hosts
hosts: all
tasks:
- ansible.builtin.group_by:
key: "os_{{ ansible_os_family | lower }}"
- ansible.builtin.group_by:
key: "{{ 'needs_reboot' if ansible_reboot_pending | default(false) else 'no_reboot' }}"
# Play 2: OS-specific packages
- name: Debian packages
hosts: os_debian
tasks:
- ansible.builtin.apt:
name: [curl, wget, jq]
state: present
- name: RedHat packages
hosts: os_redhat
tasks:
- ansible.builtin.dnf:
name: [curl, wget, jq]
state: present
# Play 3: Reboot hosts that need it
- name: Reboot pending hosts
hosts: needs_reboot
serial: 1
tasks:
- ansible.builtin.reboot:
reboot_timeout: 300
group_by vs Static Groups
| Feature | group_by | Static Groups |
|---|---|---|
| Defined in | Playbook (runtime) | Inventory file |
| Based on | Facts, variables | Manual assignment |
| Maintenance | Automatic | Manual updates needed |
| Available | After the group_by task | From start |
| Persistence | Current run only | Permanent |
Combining with add_host
- name: Provision and classify
hosts: localhost
tasks:
- name: Create instances
amazon.aws.ec2_instance:
name: "server-{{ item }}"
instance_type: "{{ item.type }}"
state: running
loop:
- { name: web1, type: t3.micro }
- { name: db1, type: r6g.large }
register: instances
- name: Register instances
ansible.builtin.add_host:
name: "{{ item.item.name }}"
ansible_host: "{{ item.instances[0].public_ip_address }}"
server_type: "{{ item.item.type }}"
loop: "{{ instances.results }}"
- name: Classify by instance type
hosts: all
tasks:
- ansible.builtin.group_by:
key: "{{ 'compute' if 't3' in server_type | default('') else 'memory' }}"
Troubleshooting
| Issue | Solution |
|---|---|
| Group is empty | Check fact/variable value; use debug to verify |
| Invalid group name | Key must be valid group name (alphanumeric, underscore, hyphen) |
| Group not available | group_by must run in an earlier play |
| Special characters in key | Use replace filter: {{ value | replace('.', '_') }} |
| Always reports "changed" | Add changed_when: false |
Best Practices
- Group early — run
group_byin the first play - Use lowercase —
{{ value | lower }}for consistent group names - Sanitize names — replace dots, spaces, special chars
- Combine groupings — multiple
group_bycalls create multiple groupings - Default values —
{{ var | default('unknown') }}prevents errors - Document groups — comment what groups are created for readability
Conclusion
group_by eliminates the need to manually maintain OS-specific, region-specific, or capacity-specific inventory groups. Let Ansible discover facts and build groups automatically — then target those groups in subsequent plays. It's the foundation for truly portable playbooks that work across mixed environments without inventory changes.