We have this Ansible inventory with dozens of servers, being grouped in servers per microservice. So say we have several application groups in the inventory with servers in it.
Say:
[group1]
server1
server2
server3
server20
server27
server38
[group2]
server4
server5
[group3]
server7
server8
server9
server6
This inventory is being used for dozens of plays, so just changing it is not an option. I need to deal with this setup.
What I need to know if it is somehow possible to have a play run in parallel on one server in each group without naming them explicitly in the plays? (groups and servers can be added by others and I need to play to be able to cope with that)
So when the play starts it may process in parallel on server1, server4 and server7. Processing on server2 may start when server1 is finished, processing of server5 may start when server4 is finished, etc, etc. You get what I mean I guess. This will mean that, in the beginning, one server of every group is processed, but as time runs by smaller groups will be done whereas in larger group processing still takes place.
Are there ways to achieve this?
Thia
Q: "Run in parallel on 1 server in each group ... process in parallel on server1, server4, and server7. Processing on server2 may start when server1 is finished, processing of server5 may start when server4 is finished, etc ... Iterate over a number of groups and servers unknown at the time of writing the code."
A: As an incomplete hint the playbook below
- hosts: all
gather_facts: false
vars:
_groups: "{{ groups.keys()|difference(['all', 'ungrouped']) }}"
# _hosts: [0, 1, 2] # Range of minimal common leght perhaps?
_groups_len_min: "{{ _groups|map('extract', groups)|map('length')|min }}"
_hosts: "{{ range(_groups_len_min|int) }}"
my_hosts: "{{ query('cartesian', _hosts, _groups) }}"
tasks:
- add_host:
name: "{{ groups[item.1][item.0] }}"
groups: my_group
loop: "{{ my_hosts }}"
run_once: true
- hosts: my_group
gather_facts: false
serial: 3 # TODO: Number of the groups involved
order: inventory
tasks:
- debug:
var: inventory_hostname
gives
PLAY [my_group] ***************************************************
TASK [debug] ******************************************************
ok: [server4] =>
inventory_hostname: server4
ok: [server1] =>
inventory_hostname: server1
ok: [server7] =>
inventory_hostname: server7
PLAY [my_group] ***************************************************
TASK [debug] ******************************************************
ok: [server2] =>
inventory_hostname: server2
ok: [server5] =>
inventory_hostname: server5
ok: [server8] =>
inventory_hostname: server8
PLAY [my_group] ***************************************************
TASK [debug] ******************************************************
ok: [server6] =>
inventory_hostname: server6
ok: [server3] =>
inventory_hostname: server3
ok: [server9] =>
inventory_hostname: server9
If you want to include all hosts fill the missing hosts with dummies, e.g. given the inventory
shell> cat hosts
[group1]
server1
server2
server3
[group2]
server4
server5
[group3]
server7
the playbook
- hosts: all
gather_facts: false
vars:
_groups: "{{ groups.keys()|difference(['all', 'ungrouped']) }}"
_groups_len_max: "{{ _groups|map('extract', groups)|map('length')|max }}"
_hosts: "{{ range(_groups_len_max|int) }}"
my_hosts: "{{ query('cartesian', _hosts, _groups) }}"
tasks:
- add_host:
name: "{{ groups[item.1][item.0]|default('dummy-' ~ ansible_loop.index) }}"
groups: my_group
loop: "{{ my_hosts }}"
loop_control:
extended: true
run_once: true
- hosts: my_group
gather_facts: false
serial: 3 # TODO: Number of the groups involved
order: inventory
tasks:
- name: Proceed if not dummy
block:
- debug:
var: inventory_hostname
when: inventory_hostname is not match('^dummy-\d*$')
gives
PLAY [my_group] *****************************************************
TASK [debug] ********************************************************
ok: [server1] =>
inventory_hostname: server1
ok: [server4] =>
inventory_hostname: server4
ok: [server7] =>
inventory_hostname: server7
PLAY [my_group] *****************************************************
TASK [debug] ********************************************************
skipping: [dummy-6]
ok: [server2] =>
inventory_hostname: server2
ok: [server5] =>
inventory_hostname: server5
PLAY [my_group] *****************************************************
TASK [debug] ********************************************************
ok: [server3] =>
inventory_hostname: server3
skipping: [dummy-8]
skipping: [dummy-9]
NOTES
A simple option would be creating dynamic groups, e.g.
- hosts: all
gather_facts: false
vars:
my_groups:
my_group1: [[group1,0], [group2,0], [group3,0]]
my_group2: [[group1,1], [group2,1], [group3,1]]
my_group3: [[group1,2], [group2,2], [group3,2]]
tasks:
- add_host:
name: "{{ groups[item.1.0][item.1.1] }}"
groups: "{{ item.0.key }}"
with_subelements:
- "{{ my_groups|dict2items }}"
- value
run_once: true
- hosts: my_group1
gather_facts: false
tasks:
- debug:
var: inventory_hostname
- hosts: my_group2
gather_facts: false
tasks:
- debug:
var: inventory_hostname
- hosts: my_group3
gather_facts: false
tasks:
- debug:
var: inventory_hostname
gives (abridged)
PLAY [my_group1] *******************************************************
TASK [debug] ***********************************************************
ok: [server1] =>
inventory_hostname: server1
ok: [server7] =>
inventory_hostname: server7
ok: [server4] =>
inventory_hostname: server4
PLAY [my_group2] *******************************************************
TASK [debug] ***********************************************************
ok: [server5] =>
inventory_hostname: server5
ok: [server2] =>
inventory_hostname: server2
ok: [server8] =>
inventory_hostname: server8
PLAY [my_group3] *******************************************************
TASK [debug] ***********************************************************
ok: [server3] =>
inventory_hostname: server3
ok: [server6] =>
inventory_hostname: server6
ok: [server9] =>
inventory_hostname: server9
The next option would be creating a single group with the required order of the hosts and set serial and order, e.g.
- hosts: all
gather_facts: false
vars:
my_groups:
my_group1: [[group1,0], [group2,0], [group3,0]]
my_group2: [[group1,1], [group2,1], [group3,1]]
my_group3: [[group1,2], [group2,2], [group3,2]]
tasks:
- add_host:
name: "{{ groups[item.1.0][item.1.1] }}"
groups: my_group
with_subelements:
- "{{ my_groups|dict2items }}"
- value
run_once: true
- hosts: my_group
gather_facts: false
serial: 3
order: inventory
tasks:
- debug:
var: inventory_hostname
gives (abridged)
PLAY [my_group] ****************************************************
TASK [debug] *******************************************************
ok: [server1] =>
inventory_hostname: server1
ok: [server7] =>
inventory_hostname: server7
ok: [server4] =>
inventory_hostname: server4
PLAY [my_group] ****************************************************
TASK [debug] *******************************************************
ok: [server2] =>
inventory_hostname: server2
ok: [server5] =>
inventory_hostname: server5
ok: [server8] =>
inventory_hostname: server8
PLAY [my_group] ****************************************************
TASK [debug] *******************************************************
ok: [server3] =>
inventory_hostname: server3
ok: [server6] =>
inventory_hostname: server6
ok: [server9] =>
inventory_hostname: server9
To avoid the tedious job of writing manually a large matrix, it's possible to create the structure dynamically, e.g. the play below creates the same dynamic group
- hosts: all
gather_facts: false
vars:
_groups: [group1, group2, group3]
_hosts: [0, 1, 2]
_dgroups: [my_group1, my_group2, my_group3]
_groups_hosts: "{{ query('cartesian', _hosts, _groups)|
batch(_dgroups|length) }}"
my_groups: "{{ dict(_dgroups|zip(_groups_hosts)) }}"
tasks:
- add_host:
name: "{{ groups[item.1.1][item.1.0] }}"
groups: my_group
with_subelements:
- "{{ my_groups|dict2items }}"
- value
run_once: true