Search code examples
parallel-processingansibleexecutioninventory

Play and task execution with multiple groups and servers with ansible


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


Solution

  • 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