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 Variables • Ansible Handlers: Trigger Actions on Change • Ansible Variable Precedence: Complete GuideCategory: database-automation