{"id":3607,"date":"2020-03-09T17:59:49","date_gmt":"2020-03-09T16:59:49","guid":{"rendered":"http:\/\/miro.borodziuk.eu\/?p=3607"},"modified":"2020-05-20T17:26:12","modified_gmt":"2020-05-20T15:26:12","slug":"ansible-delegation","status":"publish","type":"post","link":"http:\/\/miro.borodziuk.eu\/index.php\/2020\/03\/09\/ansible-delegation\/","title":{"rendered":"Ansible Delegation"},"content":{"rendered":"<p>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.<\/p>\n<p><!--more--><\/p>\n<p><span style=\"color: #3366ff;\"> Delegating tasks to the local machine<\/span><br \/>\nWhen any action needs to be performed on the node running Ansible, it can be delegated to localhost by using <code>delegate_to: localhost<\/code>.<br \/>\nHere is a sample playbook which runs the command <code>ps<\/code> on the localhost using the <code>delegate_to<\/code> keyword, and displays the output using the <code>debug<\/code> module:<\/p>\n<pre class=\"lang:sh decode:true \">[miro@controlnode ansible]$ mkdir delegation\r\n[miro@controlnode ansible]$ cd dele*\r\n[miro@controlnode delegation]$ vi delegation10.yml\r\n[miro@controlnode delegation]$ cat delegation10.yml\r\n---\r\n- name: delegate_to:localhost example\r\n  hosts: managedhost1\r\n  tasks:\r\n    - name: remote running process\r\n      command: ps\r\n      register: remote_process\r\n\r\n    - debug: msg=\"{{ remote_process.stdout }}\"\r\n\r\n    - name: Running Local Process\r\n      command: ps\r\n      delegate_to: localhost\r\n      register: local_process\r\n\r\n    - debug:\r\n      msg: \"{{ local_process.stdout }}\"<\/pre>\n<p>The <code>local_action<\/code> keyword is a shorthand syntax replacing <code>delegate_to: localhost<\/code>, and can be used on a per-task basis.<\/p>\n<p>The previous playbook can be re-written using this shorthand syntax to delegate the task to the node running Ansible (localhost):<\/p>\n<pre class=\"lang:sh decode:true\">[miro@controlnode delegation]$ cat delegation11.yml\r\n---\r\n- name: delegate_to:localhost example\r\n  hosts: managedhost1\r\n  tasks:\r\n    - name: remote running process\r\n      command: ps\r\n      register: remote_process\r\n\r\n    - debug: msg=\"{{ remote_process.stdout }}\"\r\n\r\n    - name: Running Local Process\r\n      local_action: command 'ps'\r\n      register: local_process\r\n\r\n    - debug:\r\n        msg: \"{{ local_process.stdout }}\"<\/pre>\n<p>&nbsp;<\/p>\n<p><span style=\"color: #3366ff;\">Delegating task to a host outside the play<\/span><br \/>\nAnsible can be configured to run a task on a host other than the one that is part of the play with <code>delegate_to<\/code>. 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 <code>delegate_to<\/code>. 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<br \/>\ndelegated to.<br \/>\nThe 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.<\/p>\n<pre class=\"lang:sh decode:true\">---\r\n- hosts: labservers\r\n  tasks:\r\n    - name: Remove server from load balancer\r\n      command: remove-from-lb {{ inventory_hostname }}\r\n      delegate_to: loadbalancer-host\r\n\r\n    - name: deploy the latest version of web stack\r\n      git:repo=git:\/\/foosball.example.org\/path\/to\/repo.git dest=\/srv\/checkout\r\n\r\n    - name: Add server to load balancer pool\r\n      command: add-to-lb {{ inventory_hostname }}\r\n      delegate_to: loadbalancer-host\r\n<\/pre>\n<p>&nbsp;<\/p>\n<p><span style=\"color: #3366ff;\">Delegating a task to a host that exists in the inventory<\/span><br \/>\nWhen 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 <code>ansible_connection<\/code>, <code>ansible_host<\/code>, <code>ansible_port<\/code>, <code>ansible_user<\/code> and so on. Only the connection-related variables are used; the rest are read from the managed host originally targeted.<\/p>\n<p>&nbsp;<\/p>\n<p><span style=\"color: #3366ff;\">Delegating a task to a host that does not exist in the inventory<\/span><br \/>\nWhen 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 <code>add_host<\/code> module to create an ephemeral host in your inventory with connection data defined.<\/p>\n<pre class=\"lang:sh decode:true\">[miro@controlnode delegation]$ cat delegation30.yml\r\n---\r\n- name: test play\r\n  hosts: localhost\r\n  tasks:\r\n    - name: add delegation host\r\n      add_host: name=demo ansible_host=172.30.9.52 ansible_user=miro\r\n\r\n    - name: echo Hello\r\n      command: echo \"Hello from {{ inventory_hostname }}\"\r\n      delegate_to: demo\r\n      register: output\r\n\r\n    - debug:\r\n        msg: \"{{ output.stdout }}\"<\/pre>\n<p>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.<\/p>\n<pre class=\"lang:sh decode:true \">[miro@controlnode delegation]$ ansible-playbook delegation30.yml -vv\r\nansible-playbook 2.4.2.0\r\nconfig file = \/etc\/ansible\/ansible.cfg\r\nconfigured module search path = [u'\/home\/miro\/.ansible\/plugins\/modules', u'\/usr\/share\/ansible\/plugins\/modules']\r\nansible python module location = \/usr\/lib\/python2.7\/site-packages\/ansible\r\nexecutable location = \/bin\/ansible-playbook\r\npython version = 2.7.5 (default, Nov 6 2016, 00:28:07) [GCC 4.8.5 20150623 (Red Hat 4.8.5-11)]\r\nUsing \/etc\/ansible\/ansible.cfg as config file\r\n\r\nPLAYBOOK: delegation30.yml *********************************************************************************************************\r\n1 plays in delegation30.yml\r\n\r\nPLAY [test play] *******************************************************************************************************************\r\n\r\nTASK [Gathering Facts] *************************************************************************************************************\r\nok: [localhost]\r\nMETA: ran handlers\r\n\r\nTASK [add delegation host] *********************************************************************************************************\r\ntask path: \/home\/miro\/ansible\/delegation\/delegation30.yml:5\r\ncreating host via 'add_host': hostname=demo\r\nchanged: [localhost] =&gt; {\"add_host\": {\"groups\": [], \"host_name\": \"demo\", \"host_vars\": {\"ansible_host\": \"172.30.9.52\", \"ansible_user\": \"miro\"}}, \"changed\": true}\r\n\r\nTASK [echo Hello] ******************************************************************************************************************\r\ntask path: \/home\/miro\/ansible\/delegation\/delegation30.yml:8\r\nchanged: [localhost -&gt; 172.30.9.52] =&gt; {\"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\"]}\r\n\r\nTASK [debug] ***********************************************************************************************************************\r\ntask path: \/home\/miro\/ansible\/delegation\/delegation30.yml:13\r\nok: [localhost] =&gt; {\r\n\"msg\": \"Hello from localhost\"\r\n}\r\nMETA: ran handlers\r\nMETA: ran handlers\r\n\r\nPLAY RECAP *************************************************************************************************************************\r\nlocalhost : ok=4 changed=2 unreachable=0 failed=0\r\n\r\n\r\n\r\n<\/pre>\n<p>&nbsp;<\/p>\n<p><span style=\"color: #3366ff;\"> Task execution concurrency with delegation<\/span><br \/>\nDelegated 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 &#8220;thundering herd&#8221; problem where too many connections are being opened at once on the delegated host. SSH servers have a <strong>MaxStartups<\/strong> configuration option that can limit the number of concurrent connections allowed.<\/p>\n<p>&nbsp;<\/p>\n<p><span style=\"color: #3366ff;\"> Delegated Facts<\/span><br \/>\nAny facts gathered by a delegated task are assigned by default to the <code>delegate_to<\/code> 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.<\/p>\n<pre class=\"lang:sh decode:true\">[miro@controlnode delegation]$ cat delegation40.yml\r\n---\r\n- hosts: servers1\r\n  tasks:\r\n    - name: gather facts from app servers\r\n      setup:\r\n      delegate_to: \"{{item}}\"\r\n      with_items: \"{{groups['servers2']}}\"\r\n\r\n    - debug: var=\"ansible_all_ipv4_addresses\"<\/pre>\n<pre class=\"lang:sh decode:true \">[miro@controlnode delegation]$ cat inventory\r\n[servers1]\r\nmanagedhost1.example.com\r\n\r\n[servers2]\r\nmanagedhost2.example.com<\/pre>\n<pre class=\"lang:sh decode:true\">[miro@controlnode delegation]$ ansible managedhost1 -m setup -a \"filter=ansible_all_ipv4_addresses\"\r\nmanagedhost1 | SUCCESS =&gt; {\r\n\"ansible_facts\": {\r\n\"ansible_all_ipv4_addresses\": [\r\n\"172.30.9.51\"\r\n]\r\n},\r\n\"changed\": false\r\n}\r\n[miro@controlnode delegation]$ ansible managedhost2 -m setup -a \"filter=ansible_all_ipv4_addresses\"\r\nmanagedhost2 | SUCCESS =&gt; {\r\n\"ansible_facts\": {\r\n\"ansible_all_ipv4_addresses\": [\r\n\"172.30.9.52\"\r\n]\r\n},\r\n\"changed\": false\r\n}<\/pre>\n<p>When the previous playbook is run, the output shows the gathered facts of<br \/>\n<em>managedhost2.example.com<\/em> as the task delegated to the host from the <em>servers2<\/em> inventory group instead of <em>managedhost1.example.com<\/em>.<\/p>\n<pre class=\"lang:sh decode:true\">[miro@controlnode delegation]$ ansible-playbook -i inventory delegation40.yml\r\n\r\nPLAY [servers1] ******************************************************************************************************************\r\n\r\nTASK [Gathering Facts] ***********************************************************************************************************\r\nok: [managedhost1.example.com]\r\n\r\nTASK [gather facts from app servers] *********************************************************************************************\r\nok: [managedhost1.example.com -&gt; managedhost2.example.com] =&gt; (item=managedhost2.example.com)\r\n\r\nTASK [debug] *********************************************************************************************************************\r\nok: [managedhost1.example.com] =&gt; {\r\n\"ansible_all_ipv4_addresses\": [\r\n\"172.30.9.52\"\r\n]\r\n}\r\n\r\nPLAY RECAP ***********************************************************************************************************************\r\nmanagedhost1.example.com : ok=3 changed=0 unreachable=0 failed=0<\/pre>\n<p>The <code>delegate_facts<\/code> directive can be set to True to assign the gathered facts from the task to the delegated host instead of the current host.<\/p>\n<pre class=\"lang:sh decode:true\">[miro@controlnode delegation]$ cat delegation41.yml\r\n---\r\n- hosts: servers1\r\n  tasks:\r\n    - name: gather facts from app servers\r\n      setup:\r\n      delegate_to: \"{{item}}\"\r\n      delegate_facts: True\r\n      with_items: \"{{groups['servers2']}}\"\r\n\r\n    - debug: var=\"ansible_all_ipv4_addresses\"\r\n<\/pre>\n<p>On running the above playbook, the output now shows the facts gathered from <em>managedhost1.example.com<\/em> instead of the facts from the current managed host.<\/p>\n<pre class=\"lang:sh decode:true\">[miro@controlnode delegation]$ ansible-playbook -i inventory delegation41.yml\r\n\r\nPLAY [servers1] ******************************************************************************************************************\r\n\r\nTASK [Gathering Facts] ***********************************************************************************************************\r\nok: [managedhost1.example.com]\r\n\r\nTASK [gather facts from app servers] *********************************************************************************************\r\nok: [managedhost1.example.com -&gt; managedhost2.example.com] =&gt; (item=managedhost2.example.com)\r\n\r\nTASK [debug] *********************************************************************************************************************\r\nok: [managedhost1.example.com] =&gt; {\r\n\"ansible_all_ipv4_addresses\": [\r\n\"172.30.9.51\"\r\n]\r\n}<\/pre>\n<p>&nbsp;<\/p>\n<p><span style=\"color: #3366ff;\">Example.<\/span><\/p>\n<p>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<br \/>\ntraffic coming to servera by delegating task to workstation.<\/p>\n<p>Create an inventory file named hosts under<code> ~\/ansible\/delegation<\/code>.<br \/>\nThe inventory file should have two groups defined: <em>webservers<\/em> and <em>proxyservers<\/em>. The <em>managedhost1.example.com<\/em> host should be part of the <em>webservers<\/em> group and <em>managedhost2.example.com<\/em> should be part of the <em>proxyservers<\/em> group.<\/p>\n<pre class=\"lang:sh decode:true \">[miro@controlnode delegation]$ cat hosts\r\n[webservers]\r\nmanagedhost1.example.com\r\n\r\n[proxyservers]\r\nmanagedhost2.example.com<\/pre>\n<p>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.<\/p>\n<p>Create the ~\/delegation\/managedhost2.example.comhttpd.conf.j2 template file for configuring reverse proxy in the ~\/delegation\/templates directory.<\/p>\n<p>Create a template file, named<code> index.html.j2<\/code>, for the website to be hosted on <em>managedhost1.example.com<\/em> under the <code>templates<\/code> directory. The file should contain the following content:<\/p>\n<pre class=\"lang:sh decode:true\">The webroot is {{ ansible_fqdn }}.<\/pre>\n<p>Create a <code>site.yml<\/code> playbook under the lab project directory, <code>~\/ansible\/delegation\/<\/code>. Define a play inside the playbook that will execute the tasks on all hosts. Use <em>miro<\/em> for remote connection and use privilege escalation to root using sudo. Define a task to install the <em>httpd<\/em> package and start and enable the <em>httpd<\/em> service on all hosts.\u00a0 Define a task to enable the firewall to allow web traffic on <em>managedhost1.example.com<\/em>. Define a task in the <code>site.yml<\/code> playbook to copy the<code> managedhost2.example.com-httpd.conf.j2<\/code> and <code>managedhost1.example.com-httpd.conf.j2<\/code> template files to the <code>\/etc\/httpd\/conf.d\/myconfig.conf<\/code> directory on their respective hosts. After copying the configuration file, use the <code>notify:<\/code> keyword to invoke the restart httpd handler defined in the next step. Define a handler to restart the <em>httpd<\/em> service when it is invoked.<\/p>\n<p>Define another play that will have tasks to deploy a website that needs to be run on <em>managedhost1.example.com<\/em> of the <em>webservers<\/em> inventory group.<br \/>\nIn the <code>site.yml<\/code> playbook, define a task to stop the incoming web traffic<br \/>\nto <code>managedhost1.example.com<\/code> by stopping the proxy server running on<br \/>\n<code>managedhost2.example.com<\/code>. Since the hosts keyword of the play points to <em>webservers<\/em> host group, use <code>delegate_to<\/code> keyword to delegate this task to <em>managedhost2.example.com<\/em> of the proxyservers host group. Define a task to copy the<code> index.html.j2<\/code> template present under templates directory to <code>\/var\/www\/html\/index.html<\/code> on<em> managedhost1.example.com<\/em>, part of the webservers group. Change the owner and group to apache and file permission to 0644. Add another task to the <code>site.yml<\/code> playbook that starts the proxy server by delegating the task to <em>managedhost2.example.com<\/em>.<\/p>\n<pre class=\"lang:sh decode:true  \">[miro@controlnode delegation]$ cat site.yml\r\n---\r\n- name: Install and configure httpd\r\n  hosts: all\r\n  remote_user: miro\r\n  become: yes\r\n  tasks:\r\n    - name: Install httpd\r\n      yum:\r\n        name: httpd\r\n        state: installed\r\n    - name: Start and enable httpd\r\n      service:\r\n        name: httpd\r\n        state: started\r\n        enabled: yes\r\n\r\n    - name: Install firewalld\r\n      yum:\r\n        name: firewalld\r\n        state: installed\r\n    - name: Start and enable firewalld\r\n      service:\r\n        name: firewalld\r\n        state: started\r\n        enabled: yes\r\n    - name: Enable firewall\r\n      firewalld:\r\n        zone: public\r\n        service: http\r\n        permanent: true\r\n        state: enabled\r\n        immediate: true\r\n      when: inventory_hostname in groups['webservers']\r\n    - name: template server configs\r\n      template:\r\n        src: \"templates\/{{ inventory_hostname }}-httpd.conf.j2\"\r\n        dest: \/etc\/httpd\/conf.d\/myconfig.conf\r\n        owner: root\r\n        group: root\r\n        mode: 0644\r\n      notify:\r\n        - restart httpd\r\n\r\n  handlers:\r\n    - name: restart httpd\r\n      service:\r\n        name: httpd \r\n        state: restarted\r\n\r\n- name: Deploy web service and disable proxy server\r\n  hosts: webservers\r\n  remote_user: miro\r\n  become: yes\r\n  tasks:\r\n    - name: Stop Apache proxy server\r\n      service:\r\n        name: httpd\r\n        state: stopped\r\n        delegate_to: \"{{ item }}\"\r\n        with_items: \"{{ groups['proxyservers'] }}\"\r\n    - name: Deploy webpages\r\n      template:\r\n        src: templates\/index.html.j2\r\n        dest: \/var\/www\/html\/index.html\r\n        owner: apache\r\n        group: apache\r\n        mode: 0644\r\n    - name: Start Apache proxy server\r\n      service:\r\n        name: httpd\r\n        state: started\r\n        delegate_to: \"{{ item }}\"\r\n        with_items: \"{{ groups['proxyservers'] }}\"<\/pre>\n<p>Check the syntax of the site.yml playbook. Resolve any syntax errors you find.<\/p>\n<pre class=\"lang:sh decode:true \">[miro@controlnode delegation]$ ansible-playbook -i hosts --syntax-check site.yml<\/pre>\n<p>&nbsp;<\/p>\n<p>&nbsp;<\/p>\n<p>&nbsp;<\/p>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>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 &hellip; <\/p>\n<p class=\"link-more\"><a href=\"http:\/\/miro.borodziuk.eu\/index.php\/2020\/03\/09\/ansible-delegation\/\" class=\"more-link\">Continue reading<span class=\"screen-reader-text\"> &#8220;Ansible Delegation&#8221;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":3608,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[86],"tags":[],"_links":{"self":[{"href":"http:\/\/miro.borodziuk.eu\/index.php\/wp-json\/wp\/v2\/posts\/3607"}],"collection":[{"href":"http:\/\/miro.borodziuk.eu\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/miro.borodziuk.eu\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/miro.borodziuk.eu\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/miro.borodziuk.eu\/index.php\/wp-json\/wp\/v2\/comments?post=3607"}],"version-history":[{"count":16,"href":"http:\/\/miro.borodziuk.eu\/index.php\/wp-json\/wp\/v2\/posts\/3607\/revisions"}],"predecessor-version":[{"id":3625,"href":"http:\/\/miro.borodziuk.eu\/index.php\/wp-json\/wp\/v2\/posts\/3607\/revisions\/3625"}],"wp:featuredmedia":[{"embeddable":true,"href":"http:\/\/miro.borodziuk.eu\/index.php\/wp-json\/wp\/v2\/media\/3608"}],"wp:attachment":[{"href":"http:\/\/miro.borodziuk.eu\/index.php\/wp-json\/wp\/v2\/media?parent=3607"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/miro.borodziuk.eu\/index.php\/wp-json\/wp\/v2\/categories?post=3607"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/miro.borodziuk.eu\/index.php\/wp-json\/wp\/v2\/tags?post=3607"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}