AnsiblePilot — Master Ansible Automation

AnsiblePilot is the leading resource for learning Ansible automation, DevOps, and infrastructure as code. Browse over 1,400 tutorials covering Ansible modules, playbooks, roles, collections, and real-world examples. Whether you are a beginner or an experienced engineer, our step-by-step guides help you automate Linux, Windows, cloud, containers, and network infrastructure.

Popular Topics

About Luca Berton

Luca Berton is an Ansible automation expert, author of 8 Ansible books published by Apress and Leanpub including "Ansible for VMware by Examples" and "Ansible for Kubernetes by Example", and creator of the Ansible Pilot YouTube channel. He shares practical automation knowledge through tutorials, books, and video courses to help IT professionals and DevOps engineers master infrastructure automation.

Ansible delegate_to: Run Tasks on Different Hosts (Complete Guide)

By Luca Berton · Published 2024-01-01 · Category: database-automation

Complete guide to Ansible delegate_to. Run tasks on localhost, delegate to load balancers, execute on different hosts, use with serial, and understand.

The delegate_to directive runs a task on a different host than the current play target, while keeping the context (variables, facts) of the original host. It's essential for load balancer management, local file operations, cross-host coordination, and API calls from the controller.

Basic Syntax

- name: Run task on localhost instead of remote host
  ansible.builtin.command: echo "Managing {{ inventory_hostname }}"
  delegate_to: localhost

The task runs on localhost but inventory_hostname and other variables still refer to the original target host.

See also: Ansible import_tasks vs include_tasks: Key Differences (2026 Guide)

Common Use Cases

Run on Localhost (Controller)

- name: Save host facts to local file
  ansible.builtin.copy:
    content: "{{ ansible_facts | to_nice_json }}"
    dest: "/tmp/facts_{{ inventory_hostname }}.json"
  delegate_to: localhost

- name: Send Slack notification ansible.builtin.uri: url: "{{ slack_webhook }}" method: POST body: '{"text": "Deployed to {{ inventory_hostname }}"}' body_format: json delegate_to: localhost run_once: true

Remove from Load Balancer Before Maintenance

- name: Remove host from HAProxy
  community.general.haproxy:
    state: disabled
    host: "{{ inventory_hostname }}"
    backend: webservers
  delegate_to: "{{ haproxy_host }}"

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

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

- name: Wait for health check ansible.builtin.uri: url: "http://{{ inventory_hostname }}:{{ app_port }}/health" status_code: 200 retries: 10 delay: 5 register: health until: health.status == 200

- name: Re-enable in HAProxy community.general.haproxy: state: enabled host: "{{ inventory_hostname }}" backend: webservers delegate_to: "{{ haproxy_host }}"

Delegate to Another Host in the Inventory

- name: Add DNS record on DNS server
  ansible.builtin.lineinfile:
    path: /etc/hosts
    line: "{{ ansible_default_ipv4.address }} {{ inventory_hostname }}"
  delegate_to: dns-server

- name: Register with monitoring ansible.builtin.uri: url: "http://monitoring-server:9090/api/targets" method: POST body: hostname: "{{ inventory_hostname }}" ip: "{{ ansible_default_ipv4.address }}" body_format: json delegate_to: monitoring-server

Cross-Host Database Operations

- hosts: app_servers
  tasks:
    - name: Create database user for this app server
      community.postgresql.postgresql_user:
        name: "app_{{ inventory_hostname | regex_replace('[.-]', '_') }}"
        password: "{{ db_password }}"
        db: myapp
      delegate_to: "{{ groups['db_servers'][0] }}"

local_action (Shorthand)

local_action is a shorthand for delegate_to: localhost:

# These are equivalent:
- name: Using delegate_to
  ansible.builtin.command: echo "hello"
  delegate_to: localhost

- name: Using local_action local_action: module: ansible.builtin.command cmd: echo "hello"

> Recommendation: Use delegate_to: localhost — it's more explicit and consistent.

See also: Troubleshooting: Configure User Quotas on Ansible Managed Systems

delegate_to with run_once

Combine for tasks that should run exactly once on a specific host:

- name: Run database migration once
  ansible.builtin.command: /opt/myapp/manage.py migrate
  delegate_to: "{{ groups['db_servers'][0] }}"
  run_once: true

- name: Send deploy notification once ansible.builtin.uri: url: "{{ slack_webhook }}" method: POST body: '{"text": "Deploy complete on {{ ansible_play_hosts | length }} hosts"}' body_format: json delegate_to: localhost run_once: true

delegate_facts

By default, facts gathered during a delegated task are assigned to the original host. Use delegate_facts: true to assign them to the delegated host:

- name: Gather facts from the database server
  ansible.builtin.setup:
  delegate_to: db-server
  delegate_facts: true
  # Facts are now stored under hostvars['db-server'], not the current host

- name: Use delegated facts ansible.builtin.debug: msg: "DB server has {{ hostvars['db-server']['ansible_memtotal_mb'] }}MB RAM"

See also: Troubleshooting: Configure User Quotas on XFS File Systems Using Ansible

Connection Behavior

When you delegate a task, Ansible connects to the delegated host using that host's connection settings:

# Inventory
[webservers]
web1 ansible_host=10.0.0.1

[network_devices] switch1 ansible_host=10.0.0.254 ansible_connection=network_cli ansible_network_os=cisco.ios.ios

# Playbook — task connects to web1 with SSH, delegated task connects to switch1 with network_cli - hosts: webservers tasks: - name: Configure switch port for this server cisco.ios.ios_config: lines: - description Connected to {{ inventory_hostname }} parents: interface GigabitEthernet0/1 delegate_to: switch1

Override Connection for Delegation

- name: Run API call via localhost connection
  ansible.builtin.uri:
    url: "https://api.example.com/hosts/{{ inventory_hostname }}"
  delegate_to: localhost
  connection: local  # Explicit local connection

Delegation in Handlers

tasks:
  - name: Update config
    ansible.builtin.template:
      src: app.conf.j2
      dest: /etc/app/app.conf
    notify: update load balancer

handlers: - name: update load balancer ansible.builtin.uri: url: "http://{{ lb_host }}/api/reload" method: POST delegate_to: localhost

Real-World Patterns

Rolling Update with LB Drain

- hosts: webservers
  serial: 1
  tasks:
    - name: Drain from LB
      ansible.builtin.command: "lb-ctl drain {{ inventory_hostname }}"
      delegate_to: "{{ lb_host }}"

- name: Wait for connections to drain ansible.builtin.pause: seconds: 30

- name: Deploy update ansible.builtin.include_role: name: deploy

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

- name: Re-enable in LB ansible.builtin.command: "lb-ctl enable {{ inventory_hostname }}" delegate_to: "{{ lb_host }}"

Collect Info from All Hosts Locally

- name: Generate inventory report
  ansible.builtin.lineinfile:
    path: /tmp/inventory_report.csv
    line: "{{ inventory_hostname }},{{ ansible_default_ipv4.address }},{{ ansible_distribution }}"
    create: true
  delegate_to: localhost

Common Mistakes

Forgetting Variable Context

# Variables still reference the ORIGINAL host, not the delegate
- name: This runs on localhost but vars are from web1
  ansible.builtin.debug:
    msg: "inventory_hostname = {{ inventory_hostname }}"
    # Prints "web1", NOT "localhost"
  delegate_to: localhost

Missing run_once with delegate_to

# Bug: runs once PER HOST in the play, all delegated to same target
- name: Create shared resource
  ansible.builtin.command: create-shared-thing
  delegate_to: "{{ groups['db_servers'][0] }}"
# Runs N times (once per webserver) on the same DB server!

# Fix: add run_once - name: Create shared resource ansible.builtin.command: create-shared-thing delegate_to: "{{ groups['db_servers'][0] }}" run_once: true

FAQ

What does delegate_to do in Ansible?

delegate_to runs a task on a different host than the play target while keeping the original host's variables and facts. For example, you can manage a load balancer from a web server play, or save files locally while iterating over remote hosts.

What is the difference between delegate_to localhost and local_action?

They're functionally equivalent. delegate_to: localhost is more explicit and is the recommended syntax. local_action is an older shorthand that's less commonly used in modern playbooks.

Do variables change when using delegate_to?

No, variables like inventory_hostname, ansible_host, and gathered facts still reference the original play target, not the delegated host. Use hostvars['delegated_host'] to access the delegated host's variables.

Can I delegate to a host not in my inventory?

Yes, but you need to ensure Ansible can connect to it. You may need to add connection parameters inline or define the host in your inventory with appropriate connection settings.

What is delegate_facts?

When delegate_facts: true is set, facts gathered during a delegated task are stored under the delegated host (not the original). Without it, facts from a delegated setup task would be assigned to the wrong host.

Conclusion

delegate_to is essential for cross-host coordination: • delegate_to: localhost — API calls, local file ops, notifications • delegate_to: lb_host — Load balancer management during deploys • delegate_to + run_once — One-time actions on a specific host • delegate_facts: true — Assign gathered facts to the right host • Variables stay with original host — Use hostvars for delegate host data

Related Articles

Execute Command on Ansible Host (localhost)Ansible register: Save Task Output to VariablesAnsible Handlers: Trigger Actions on ChangeAnsible Variable Precedence: Complete Guide

Category: database-automation

Browse all Ansible tutorials · AnsiblePilot Home