Task Delegation

What is Task Delegation? Task delegation allows you to execute a task on one host while referencing data from other hosts. This enables complex orchestration scenarios like updating load balancers, performing centralized operations, or coordinating between dependent systems.

Understanding Delegation

Why Delegate Tasks?

Delegation solves orchestration challenges:

  • Load Balancer Management: Remove servers from pool during updates
  • Centralized Operations: Run API calls from control node
  • Cross-Host Dependencies: Update monitoring from monitored hosts
  • Local Operations: Perform file operations on Ansible controller
  • Database Management: Execute admin commands from primary server

Common Use Cases

  • Updating load balancer configurations
  • Calling external APIs or webhooks
  • Writing logs to centralized server
  • Performing DNS updates
  • Coordinating database replication

delegate_to Keyword

Basic Delegation

Delegate tasks to specific hosts using delegate_to:

---
- name: Web server rolling update
  hosts: webservers
  serial: 1
  tasks:
    - name: Remove server from load balancer
      ansible.builtin.command: >
        /usr/bin/remove_from_pool {{ inventory_hostname }}
      delegate_to: loadbalancer.example.com

    - name: Update application
      ansible.builtin.yum:
        name: myapp
        state: latest

    - name: Restart application
      ansible.builtin.service:
        name: myapp
        state: restarted

    - name: Wait for application to be ready
      ansible.builtin.wait_for:
        port: 8080
        state: started
        timeout: 60

    - name: Add server back to load balancer
      ansible.builtin.command: >
        /usr/bin/add_to_pool {{ inventory_hostname }}
      delegate_to: loadbalancer.example.com

Delegate to Localhost

Run tasks on the Ansible control node:

---
- name: Manage cloud infrastructure
  hosts: webservers
  tasks:
    - name: Create DNS record for each server
      community.general.cloudflare_dns:
        zone: example.com
        record: "{{ inventory_hostname_short }}"
        type: A
        value: "{{ ansible_host }}"
        account_email: admin@example.com
        account_api_key: "{{ cloudflare_api_key }}"
      delegate_to: localhost

    - name: Log deployment
      ansible.builtin.lineinfile:
        path: /var/log/ansible/deployments.log
        line: "{{ ansible_date_time.iso8601 }} - Deployed to {{ inventory_hostname }}"
        create: yes
      delegate_to: localhost

Delegate to Another Host in Inventory

---
- name: Database replication setup
  hosts: db_replicas
  tasks:
    - name: Get primary server data directory
      ansible.builtin.command: cat /etc/postgresql/data_dir
      delegate_to: "{{ groups['db_primary'][0] }}"
      register: primary_data_dir

    - name: Configure replication from primary
      ansible.builtin.template:
        src: recovery.conf.j2
        dest: /var/lib/postgresql/recovery.conf
      vars:
        primary_host: "{{ groups['db_primary'][0] }}"
        primary_port: 5432

local_action Shorthand

Using local_action

Simplified syntax for delegating to localhost:

---
- name: Call external API
  hosts: webservers
  tasks:
    # Using delegate_to (verbose)
    - name: Send notification (delegate_to)
      ansible.builtin.uri:
        url: https://api.slack.com/webhooks/YOUR_WEBHOOK
        method: POST
        body_format: json
        body:
          text: "Deploying to {{ inventory_hostname }}"
      delegate_to: localhost

    # Using local_action (concise)
    - name: Send notification (local_action)
      local_action:
        module: ansible.builtin.uri
        url: https://api.slack.com/webhooks/YOUR_WEBHOOK
        method: POST
        body_format: json
        body:
          text: "Deploying to {{ inventory_hostname }}"

Local File Operations

---
- name: Collect logs from servers
  hosts: all
  tasks:
    - name: Fetch application logs
      ansible.builtin.fetch:
        src: /var/log/app/app.log
        dest: /tmp/collected-logs/{{ inventory_hostname }}/app.log
        flat: yes

    - name: Create summary report locally
      local_action:
        module: ansible.builtin.copy
        content: |
          Server: {{ inventory_hostname }}
          IP: {{ ansible_host }}
          Status: Logs collected
        dest: /tmp/collected-logs/{{ inventory_hostname }}/summary.txt

run_once Directive

Execute Task Once for All Hosts

Run a task only once, even when multiple hosts are targeted:

---
- name: Database migration
  hosts: app_servers
  tasks:
    - name: Run database migration (only once)
      ansible.builtin.command: /opt/app/migrate.sh
      run_once: true
      delegate_to: "{{ groups['app_servers'][0] }}"

    - name: Send deployment notification (only once)
      ansible.builtin.uri:
        url: https://hooks.slack.com/services/YOUR/WEBHOOK
        method: POST
        body_format: json
        body:
          text: "Deployment started for {{ groups['app_servers'] | length }} servers"
      run_once: true
      delegate_to: localhost

    - name: Deploy application (runs on all hosts)
      ansible.builtin.copy:
        src: /tmp/app.jar
        dest: /opt/app/app.jar

run_once with Loops

Loop over all hosts in a single task execution:

---
- name: Update monitoring for all servers
  hosts: webservers
  tasks:
    - name: Register all servers with monitoring
      ansible.builtin.uri:
        url: https://monitoring.example.com/api/servers
        method: POST
        body_format: json
        body:
          hostname: "{{ item }}"
          ip: "{{ hostvars[item]['ansible_host'] }}"
          environment: production
      loop: "{{ groups['webservers'] }}"
      run_once: true
      delegate_to: localhost

Delegation Patterns

Rolling Update with Load Balancer

---
- name: Zero-downtime deployment
  hosts: webservers
  serial: 2  # Update 2 servers at a time
  max_fail_percentage: 25
  tasks:
    - name: Disable server in load balancer
      ansible.builtin.command: >
        haproxy-mgmt disable {{ inventory_hostname }}
      delegate_to: "{{ item }}"
      loop: "{{ groups['loadbalancers'] }}"

    - name: Wait for connections to drain
      ansible.builtin.wait_for:
        timeout: 30
      delegate_to: localhost

    - name: Stop application
      ansible.builtin.service:
        name: myapp
        state: stopped

    - name: Deploy new version
      ansible.builtin.copy:
        src: /releases/myapp-v2.0.jar
        dest: /opt/myapp/myapp.jar

    - name: Start application
      ansible.builtin.service:
        name: myapp
        state: started

    - name: Health check
      ansible.builtin.uri:
        url: "http://{{ ansible_host }}:8080/health"
        status_code: 200
      register: health_check
      until: health_check.status == 200
      retries: 5
      delay: 10
      delegate_to: localhost

    - name: Enable server in load balancer
      ansible.builtin.command: >
        haproxy-mgmt enable {{ inventory_hostname }}
      delegate_to: "{{ item }}"
      loop: "{{ groups['loadbalancers'] }}"

Centralized Configuration Management

---
- name: Update configuration server
  hosts: application_servers
  tasks:
    - name: Register service with config server
      ansible.builtin.uri:
        url: https://config.example.com/api/services
        method: POST
        body_format: json
        body:
          service_name: "{{ service_name }}"
          host: "{{ inventory_hostname }}"
          port: "{{ service_port }}"
          metadata:
            version: "{{ app_version }}"
            environment: "{{ environment }}"
      delegate_to: localhost
      run_once: false  # Runs for each host

    - name: Update DNS records
      community.general.route53:
        state: present
        zone: example.com
        record: "{{ inventory_hostname }}.example.com"
        type: A
        value: "{{ ansible_host }}"
        overwrite: yes
      delegate_to: localhost

Database Coordination

---
- name: Configure database replication
  hosts: db_replicas
  tasks:
    - name: Backup primary database before adding replica
      community.postgresql.postgresql_db:
        name: production_db
        state: dump
        target: /tmp/pre-replica-backup.sql
      delegate_to: "{{ groups['db_primary'][0] }}"
      run_once: true

    - name: Get replication credentials from primary
      ansible.builtin.command: >
        psql -t -c "SELECT * FROM pg_create_physical_replication_slot('replica_{{ inventory_hostname_short }}');"
      delegate_to: "{{ groups['db_primary'][0] }}"
      register: replication_slot

    - name: Configure replica to follow primary
      ansible.builtin.template:
        src: postgresql.conf.j2
        dest: /etc/postgresql/postgresql.conf
      notify: restart postgresql

Delegation Variables and Facts

Understanding Variable Context

When delegating, variable context can be confusing:

---
- name: Variable delegation example
  hosts: webservers
  tasks:
    - name: Example showing variable context
      ansible.builtin.debug:
        msg: |
          Current host (inventory_hostname): {{ inventory_hostname }}
          Ansible host IP (ansible_host): {{ ansible_host }}
          Delegated to: localhost

          To access facts from current host after delegation:
          Original host: {{ hostvars[inventory_hostname]['ansible_host'] }}
      delegate_to: localhost

delegate_facts

Control where gathered facts are assigned:

---
- name: Gather facts with delegation
  hosts: webservers
  tasks:
    - name: Gather facts from load balancer
      ansible.builtin.setup:
      delegate_to: loadbalancer.example.com
      delegate_facts: true
      # Facts will be assigned to loadbalancer.example.com

    - name: Use load balancer facts
      ansible.builtin.debug:
        msg: "Load balancer OS: {{ hostvars['loadbalancer.example.com']['ansible_distribution'] }}"

Delegation with Parallelism

Serial Execution with Delegation

---
- name: Controlled parallel deployment
  hosts: webservers
  serial: 5  # Process 5 hosts at a time
  tasks:
    - name: Update load balancer config
      ansible.builtin.template:
        src: haproxy.cfg.j2
        dest: /etc/haproxy/haproxy.cfg
      delegate_to: loadbalancer.example.com
      # This could cause issues if multiple hosts update simultaneously

    - name: Better approach - use run_once
      ansible.builtin.template:
        src: haproxy.cfg.j2
        dest: /etc/haproxy/haproxy.cfg
      delegate_to: loadbalancer.example.com
      run_once: true
      # Runs only once per serial batch

Throttle Delegation

---
- name: Rate-limited API calls
  hosts: servers
  tasks:
    - name: Register with API (rate-limited)
      ansible.builtin.uri:
        url: https://api.example.com/register
        method: POST
        body_format: json
        body:
          hostname: "{{ inventory_hostname }}"
      delegate_to: localhost
      throttle: 1  # Only 1 host at a time makes API call

Delegation Limitations

Tasks That Cannot Be Delegated

Some tasks don't support delegation:

  • include and import statements
  • add_host and group_by
  • debug module
  • Tasks with connection: local already set
---
- name: Delegation limitations
  hosts: all
  tasks:
    # This works
    - name: Run command with delegation
      ansible.builtin.command: echo "hello"
      delegate_to: localhost

    # This doesn't work (ignored)
    - name: Debug with delegation (ignored)
      ansible.builtin.debug:
        msg: "This runs on current host, not delegated"
      delegate_to: localhost

    # This doesn't work
    - name: Add host with delegation (error)
      ansible.builtin.add_host:
        name: newhost
      delegate_to: localhost  # Will fail

Best Practices

Delegation Best Practices:
  • Use run_once: Combine with delegation for single-execution tasks
  • Check Idempotency: Ensure delegated tasks are idempotent
  • Mind Variables: Use hostvars to access original host data
  • Serial + Delegation: Be careful with concurrent delegated tasks
  • Error Handling: Add retries for delegated network operations
  • Document Intent: Comment why delegation is necessary
  • Test Thoroughly: Delegation can create race conditions
  • Use throttle: Rate-limit API calls when delegating to localhost

Common Issues

Connection Failures

If delegation fails to connect:

  • Verify the delegated host is in inventory
  • Check SSH connectivity to delegated host
  • Ensure proper credentials for delegated host
  • Use delegate_to: localhost for local operations

Variable Confusion

If variables show wrong values:

  • Remember variables refer to current host, not delegated host
  • Use hostvars[inventory_hostname] to access original host vars
  • Use delegate_facts: true when gathering facts

Race Conditions

If concurrent updates cause issues:

  • Use run_once: true for single-execution tasks
  • Implement serial: 1 for sequential processing
  • Add throttle to limit concurrency
  • Use file locking for shared resource updates

Next Steps