In order to complete some configuration tasks, it may be necessary for actions to be taken on a different server than the one being configured. Some examples of this might include an action that requires waiting for the server to be restarted, adding a server to a load balancer or a monitoring server, or making changes to the DHCP or DNS database needed for the server being configured.
Delegating tasks to the local machine
When any action needs to be performed on the node running Ansible, it can be delegated to localhost by using delegate_to: localhost
.
Here is a sample playbook which runs the command ps
on the localhost using the delegate_to
keyword, and displays the output using the debug
module:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
[miro@controlnode ansible]$ mkdir delegation [miro@controlnode ansible]$ cd dele* [miro@controlnode delegation]$ vi delegation10.yml [miro@controlnode delegation]$ cat delegation10.yml --- - name: delegate_to:localhost example hosts: managedhost1 tasks: - name: remote running process command: ps register: remote_process - debug: msg="{{ remote_process.stdout }}" - name: Running Local Process command: ps delegate_to: localhost register: local_process - debug: msg: "{{ local_process.stdout }}" |
The local_action
keyword is a shorthand syntax replacing delegate_to: localhost
, and can be used on a per-task basis.
The previous playbook can be re-written using this shorthand syntax to delegate the task to the node running Ansible (localhost):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
[miro@controlnode delegation]$ cat delegation11.yml --- - name: delegate_to:localhost example hosts: managedhost1 tasks: - name: remote running process command: ps register: remote_process - debug: msg="{{ remote_process.stdout }}" - name: Running Local Process local_action: command 'ps' register: local_process - debug: msg: "{{ local_process.stdout }}" |
Delegating task to a host outside the play
Ansible can be configured to run a task on a host other than the one that is part of the play with delegate_to
. The delegated module will still run once for every machine, but instead of running on the target machine, it will run on the host specified by delegate_to
. The facts available will be the ones applicable to the original host and not the host the task is delegated to. The task has the context of the original target host, but it gets executed on the host the task is
delegated to.
The following example shows Ansible code that will delegate a task to an outside machine (in this case, loadbalancer-host). This example runs a command on the local balancer host to remove the managed hosts from the load balancer before deploying the latest version of the web stack. After that task is finished, a script is run to add the managed hosts back into the load balancer pool.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
--- - hosts: labservers tasks: - name: Remove server from load balancer command: remove-from-lb {{ inventory_hostname }} delegate_to: loadbalancer-host - name: deploy the latest version of web stack git:repo=git://foosball.example.org/path/to/repo.git dest=/srv/checkout - name: Add server to load balancer pool command: add-to-lb {{ inventory_hostname }} delegate_to: loadbalancer-host |
Delegating a task to a host that exists in the inventory
When delegating to a host listed in the inventory, the inventory data will be used when creating the connection to the delegation target. This would include settings for ansible_connection
, ansible_host
, ansible_port
, ansible_user
and so on. Only the connection-related variables are used; the rest are read from the managed host originally targeted.
Delegating a task to a host that does not exist in the inventory
When delegating a task to a host that does not exist in the inventory, Ansible will use the same connection type and details used for the managed host to connect to the delegating host. To adjust the connection details, use the add_host
module to create an ephemeral host in your inventory with connection data defined.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
[miro@controlnode delegation]$ cat delegation30.yml --- - name: test play hosts: localhost tasks: - name: add delegation host add_host: name=demo ansible_host=172.30.9.52 ansible_user=miro - name: echo Hello command: echo "Hello from {{ inventory_hostname }}" delegate_to: demo register: output - debug: msg: "{{ output.stdout }}" |
When the preceding playbook is executed, Ansible will use the connection details for the ephemeral host demo while executing the task echo Hello on the delegate_to host. The inventory_hostname will be read from the targeted managed host which in this case is localhost. The following example shows the playbook execution. Note that the output contains the add_host line and the variable expansion to localhost.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
[miro@controlnode delegation]$ ansible-playbook delegation30.yml -vv ansible-playbook 2.4.2.0 config file = /etc/ansible/ansible.cfg configured module search path = [u'/home/miro/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules'] ansible python module location = /usr/lib/python2.7/site-packages/ansible executable location = /bin/ansible-playbook python version = 2.7.5 (default, Nov 6 2016, 00:28:07) [GCC 4.8.5 20150623 (Red Hat 4.8.5-11)] Using /etc/ansible/ansible.cfg as config file PLAYBOOK: delegation30.yml ********************************************************************************************************* 1 plays in delegation30.yml PLAY [test play] ******************************************************************************************************************* TASK [Gathering Facts] ************************************************************************************************************* ok: [localhost] META: ran handlers TASK [add delegation host] ********************************************************************************************************* task path: /home/miro/ansible/delegation/delegation30.yml:5 creating host via 'add_host': hostname=demo changed: [localhost] => {"add_host": {"groups": [], "host_name": "demo", "host_vars": {"ansible_host": "172.30.9.52", "ansible_user": "miro"}}, "changed": true} TASK [echo Hello] ****************************************************************************************************************** task path: /home/miro/ansible/delegation/delegation30.yml:8 changed: [localhost -> 172.30.9.52] => {"changed": true, "cmd": ["echo", "Hello from localhost"], "delta": "0:00:00.006048", "end": "2020-05-19 19:37:06.111409", "rc": 0, "start": "2020-05-19 19:37:06.105361", "stderr": "", "stderr_lines": [], "stdout": "Hello from localhost", "stdout_lines": ["Hello from localhost"]} TASK [debug] *********************************************************************************************************************** task path: /home/miro/ansible/delegation/delegation30.yml:13 ok: [localhost] => { "msg": "Hello from localhost" } META: ran handlers META: ran handlers PLAY RECAP ************************************************************************************************************************* localhost : ok=4 changed=2 unreachable=0 failed=0 |
Task execution concurrency with delegation
Delegated tasks run for each managed host targeted. But Ansible tasks can run on multiple managed hosts in parallel. This can create issues with race conditions on the delegated host. This is particularly likely when using conditionals in the task, or when multiple concurrent tasks are run in parallel. This can also create a “thundering herd” problem where too many connections are being opened at once on the delegated host. SSH servers have a MaxStartups configuration option that can limit the number of concurrent connections allowed.
Delegated Facts
Any facts gathered by a delegated task are assigned by default to the delegate_to
host, instead of the host which actually produced the facts. The following example shows a task file that will loop through a list of inventory servers to gather facts.
1 2 3 4 5 6 7 8 9 10 |
[miro@controlnode delegation]$ cat delegation40.yml --- - hosts: servers1 tasks: - name: gather facts from app servers setup: delegate_to: "{{item}}" with_items: "{{groups['servers2']}}" - debug: var="ansible_all_ipv4_addresses" |
1 2 3 4 5 6 |
[miro@controlnode delegation]$ cat inventory [servers1] managedhost1.example.com [servers2] managedhost2.example.com |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
[miro@controlnode delegation]$ ansible managedhost1 -m setup -a "filter=ansible_all_ipv4_addresses" managedhost1 | SUCCESS => { "ansible_facts": { "ansible_all_ipv4_addresses": [ "172.30.9.51" ] }, "changed": false } [miro@controlnode delegation]$ ansible managedhost2 -m setup -a "filter=ansible_all_ipv4_addresses" managedhost2 | SUCCESS => { "ansible_facts": { "ansible_all_ipv4_addresses": [ "172.30.9.52" ] }, "changed": false } |
When the previous playbook is run, the output shows the gathered facts of
managedhost2.example.com as the task delegated to the host from the servers2 inventory group instead of managedhost1.example.com.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
[miro@controlnode delegation]$ ansible-playbook -i inventory delegation40.yml PLAY [servers1] ****************************************************************************************************************** TASK [Gathering Facts] *********************************************************************************************************** ok: [managedhost1.example.com] TASK [gather facts from app servers] ********************************************************************************************* ok: [managedhost1.example.com -> managedhost2.example.com] => (item=managedhost2.example.com) TASK [debug] ********************************************************************************************************************* ok: [managedhost1.example.com] => { "ansible_all_ipv4_addresses": [ "172.30.9.52" ] } PLAY RECAP *********************************************************************************************************************** managedhost1.example.com : ok=3 changed=0 unreachable=0 failed=0 |
The delegate_facts
directive can be set to True to assign the gathered facts from the task to the delegated host instead of the current host.
1 2 3 4 5 6 7 8 9 10 11 |
[miro@controlnode delegation]$ cat delegation41.yml --- - hosts: servers1 tasks: - name: gather facts from app servers setup: delegate_to: "{{item}}" delegate_facts: True with_items: "{{groups['servers2']}}" - debug: var="ansible_all_ipv4_addresses" |
On running the above playbook, the output now shows the facts gathered from managedhost1.example.com instead of the facts from the current managed host.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
[miro@controlnode delegation]$ ansible-playbook -i inventory delegation41.yml PLAY [servers1] ****************************************************************************************************************** TASK [Gathering Facts] *********************************************************************************************************** ok: [managedhost1.example.com] TASK [gather facts from app servers] ********************************************************************************************* ok: [managedhost1.example.com -> managedhost2.example.com] => (item=managedhost2.example.com) TASK [debug] ********************************************************************************************************************* ok: [managedhost1.example.com] => { "ansible_all_ipv4_addresses": [ "172.30.9.51" ] } |
Example.
In this exercise, you will configure the delegation of tasks in an Ansible playbook. The playbook will configure workstation as a proxy server and servera as an Apache web server. During the deployment of the website on servera, you will delegate the task of stopping the traffic coming to servera to workstation proxy server and later after deployment you will start the
traffic coming to servera by delegating task to workstation.
Create an inventory file named hosts under ~/ansible/delegation
.
The inventory file should have two groups defined: webservers and proxyservers. The managedhost1.example.com host should be part of the webservers group and managedhost2.example.com should be part of the proxyservers group.
1 2 3 4 5 6 |
[miro@controlnode delegation]$ cat hosts [webservers] managedhost1.example.com [proxyservers] managedhost2.example.com |
Create managedhost1.example.com-httpd.conf.j2, template file that configures the virtual host in the ~/delegation/templates directory. Later you will use an Ansible variable (inventory_hostname) to list the source of this file.
Create the ~/delegation/managedhost2.example.comhttpd.conf.j2 template file for configuring reverse proxy in the ~/delegation/templates directory.
Create a template file, named index.html.j2
, for the website to be hosted on managedhost1.example.com under the templates
directory. The file should contain the following content:
1 |
The webroot is {{ ansible_fqdn }}. |
Create a site.yml
playbook under the lab project directory, ~/ansible/delegation/
. Define a play inside the playbook that will execute the tasks on all hosts. Use miro for remote connection and use privilege escalation to root using sudo. Define a task to install the httpd package and start and enable the httpd service on all hosts. Define a task to enable the firewall to allow web traffic on managedhost1.example.com. Define a task in the site.yml
playbook to copy the managedhost2.example.com-httpd.conf.j2
and managedhost1.example.com-httpd.conf.j2
template files to the /etc/httpd/conf.d/myconfig.conf
directory on their respective hosts. After copying the configuration file, use the notify:
keyword to invoke the restart httpd handler defined in the next step. Define a handler to restart the httpd service when it is invoked.
Define another play that will have tasks to deploy a website that needs to be run on managedhost1.example.com of the webservers inventory group.
In the site.yml
playbook, define a task to stop the incoming web traffic
to managedhost1.example.com
by stopping the proxy server running on
managedhost2.example.com
. Since the hosts keyword of the play points to webservers host group, use delegate_to
keyword to delegate this task to managedhost2.example.com of the proxyservers host group. Define a task to copy the index.html.j2
template present under templates directory to /var/www/html/index.html
on managedhost1.example.com, part of the webservers group. Change the owner and group to apache and file permission to 0644. Add another task to the site.yml
playbook that starts the proxy server by delegating the task to managedhost2.example.com.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
[miro@controlnode delegation]$ cat site.yml --- - name: Install and configure httpd hosts: all remote_user: miro become: yes tasks: - name: Install httpd yum: name: httpd state: installed - name: Start and enable httpd service: name: httpd state: started enabled: yes - name: Install firewalld yum: name: firewalld state: installed - name: Start and enable firewalld service: name: firewalld state: started enabled: yes - name: Enable firewall firewalld: zone: public service: http permanent: true state: enabled immediate: true when: inventory_hostname in groups['webservers'] - name: template server configs template: src: "templates/{{ inventory_hostname }}-httpd.conf.j2" dest: /etc/httpd/conf.d/myconfig.conf owner: root group: root mode: 0644 notify: - restart httpd handlers: - name: restart httpd service: name: httpd state: restarted - name: Deploy web service and disable proxy server hosts: webservers remote_user: miro become: yes tasks: - name: Stop Apache proxy server service: name: httpd state: stopped delegate_to: "{{ item }}" with_items: "{{ groups['proxyservers'] }}" - name: Deploy webpages template: src: templates/index.html.j2 dest: /var/www/html/index.html owner: apache group: apache mode: 0644 - name: Start Apache proxy server service: name: httpd state: started delegate_to: "{{ item }}" with_items: "{{ groups['proxyservers'] }}" |
Check the syntax of the site.yml playbook. Resolve any syntax errors you find.
1 |
[miro@controlnode delegation]$ ansible-playbook -i hosts --syntax-check site.yml |