Introduction
delegate_to is an Ansible keyword that runs a task on a different host while keeping variables bound to the original play target. The task executes on the delegated host (localhost, a load balancer, database, or control point), but all variables still reference the original managed host.
This is essential for rolling updates with load balancer management, monitoring checks, DNS registration, centralized database operations, and any task that needs to run outside the primary play target while maintaining context.
Basic Usage
# Run on localhost instead of remote host
- name: Add host to load balancer
ansible.builtin.command: /opt/lb/add-server {{ inventory_hostname }}
delegate_to: lb.internal
# Run locally
- name: Wait for SSH
ansible.builtin.wait_for:
host: "{{ ansible_host }}"
port: 22
timeout: 300
delegate_to: localhost
Common Patterns
Load Balancer Management
---
- name: Rolling update
hosts: webservers
serial: 1
tasks:
- name: Remove from load balancer
ansible.builtin.uri:
url: "https://{{ lb_host }}/api/servers/{{ inventory_hostname }}/disable"
method: POST
headers:
Authorization: "Bearer {{ lb_token }}"
delegate_to: localhost
- name: Deploy new version
ansible.builtin.shell: /opt/deploy.sh {{ app_version }}
- name: Wait for healthy
ansible.builtin.uri:
url: "http://localhost:8080/health"
status_code: 200
retries: 10
delay: 5
register: health
until: health.status == 200
- name: Add back to load balancer
ansible.builtin.uri:
url: "https://{{ lb_host }}/api/servers/{{ inventory_hostname }}/enable"
method: POST
headers:
Authorization: "Bearer {{ lb_token }}"
delegate_to: localhost
Monitoring / Alerting
- name: Silence monitoring during maintenance
ansible.builtin.uri:
url: "https://monitoring.internal/api/v1/silences"
method: POST
body_format: json
body:
matchers:
- name: instance
value: "{{ inventory_hostname }}"
startsAt: "{{ ansible_date_time.iso8601 }}"
endsAt: "{{ '%Y-%m-%dT%H:%M:%S' | strftime(ansible_date_time.epoch | int + 3600) }}"
comment: "Ansible maintenance"
delegate_to: localhost
# ... do maintenance ...
- name: Remove silence
ansible.builtin.uri:
url: "https://monitoring.internal/api/v1/silence/{{ silence_id }}"
method: DELETE
delegate_to: localhost
Database Operations from App Server
- name: Run database migration
ansible.builtin.command: /opt/myapp/migrate.sh
delegate_to: "{{ groups['dbservers'][0] }}"
run_once: true
# Variables still reference the original host
- name: Create database for this host
community.postgresql.postgresql_db:
name: "app_{{ inventory_hostname | replace('-', '_') }}"
delegate_to: db.internal
DNS Registration
- name: Register DNS entry
community.general.nsupdate:
server: ns1.internal
zone: internal.example.com
record: "{{ inventory_hostname }}"
type: A
value: "{{ ansible_default_ipv4.address }}"
delegate_to: localhost
delegate_to: localhost
# These are equivalent:
- name: Local action
ansible.builtin.command: echo "hello"
delegate_to: localhost
- name: Local action (shorthand)
local_action:
module: ansible.builtin.command
cmd: echo "hello"
# Common use: API calls, wait_for, local file operations
- name: Save host info locally
ansible.builtin.copy:
content: "{{ ansible_facts | to_nice_json }}"
dest: "/tmp/facts/{{ inventory_hostname }}.json"
delegate_to: localhost
delegate_to with delegate_facts
By default, facts from delegated tasks are assigned to the original host. Use delegate_facts: true to assign them to the delegated host:
# Facts saved to db.internal (not the play host)
- name: Gather DB server facts
ansible.builtin.setup:
delegate_to: db.internal
delegate_facts: true
# Now use db.internal's facts
- debug:
msg: "DB memory: {{ hostvars['db.internal']['ansible_memtotal_mb'] }}"
delegate_to with run_once
# Run migration once, on the DB server
- name: Database migration
ansible.builtin.command: python manage.py migrate
args:
chdir: /opt/myapp
delegate_to: "{{ groups['dbservers'][0] }}"
run_once: true
# Send one notification (not per host)
- name: Notify Slack
ansible.builtin.uri:
url: "{{ slack_webhook }}"
method: POST
body_format: json
body:
text: "Deployment complete on {{ ansible_play_hosts | join(', ') }}"
delegate_to: localhost
run_once: true
Variable Context
Tasks run on the delegated host, but variables reference the original play host:
# Play targets webservers, task runs on localhost
- name: Add host to config
ansible.builtin.lineinfile:
path: /etc/haproxy/servers.cfg
line: "server {{ inventory_hostname }} {{ ansible_host }}:{{ app_port }}"
delegate_to: lb.internal
# inventory_hostname = the web server
# Task runs on lb.internal
# Line references the web server's name and IP
Troubleshooting
"SSH connection failed" on Delegated Host
The delegated host must be reachable and in the inventory (or use add_host first):
# Add host dynamically
- ansible.builtin.add_host:
name: lb.internal
ansible_user: admin
ansible_ssh_private_key_file: ~/.ssh/lb_key
# Now delegate to it
- name: Update LB
ansible.builtin.command: /opt/lb/reload
delegate_to: lb.internal
Connection Type for localhost
# Avoid SSH to localhost — use local connection
# In inventory:
# localhost ansible_connection=local
# Or in task:
- name: Local task
ansible.builtin.command: echo "local"
delegate_to: localhost
vars:
ansible_connection: local
Related Articles
Conclusion
delegate_to runs tasks on a different host while keeping variable context from the original host. Key uses: load balancer management during rolling updates, centralized database operations, DNS registration, monitoring silences, and local API calls. Combine with run_once: true for tasks that should execute only once regardless of host count. Use delegate_facts: true when gathering facts about the delegated host. Remember that delegate_to: localhost is the most common pattern for API calls and local file operations.