Redirecting and Limiting Task Execution
Every task in this course so far has run on every host the play targets. This chapter covers two ways to deviate from that, in opposite directions: delegate_to changes where a task actually runs; run_once changes how many times it runs at all.
delegate_to: Changing Where A Task Actually Runs
Normally, a task runs on whichever host the play is currently processing — hosts: servers means every task connects to each server in that group individually. delegate_to redirects one specific task to execute on a completely different host, while still being considered “for” the originally-targeted host in the play’s own bookkeeping:
- name: Notify a monitoring system
ansible.builtin.debug:
msg: "Notifying about {{ inventory_hostname }}"
delegate_to: monitoring.example.cominventory_hostname still correctly refers to whichever server this iteration of the play is actually about — but the task itself executes on monitoring.example.com instead.
The Classic Pattern: delegate_to: localhost
By far the most common target. Recall implicit localhost from the basics section — tasks that should genuinely run on the control node, not any remote managed host:
- name: Log deployment locally
ansible.builtin.debug:
msg: "Deployed {{ inventory_hostname }} at {{ lookup('pipe', 'date') }}"
delegate_to: localhostThis task still runs once per host the play is processing — delegate_to changes where each of those runs happens, not how many times.
A Realistic Example: Updating A Load Balancer
The quintessential real-world use: pulling a web server out of a load balancer’s pool before maintenance, and putting it back afterward:
- name: Remove this host from the load balancer pool
ansible.builtin.debug:
msg: "Removing {{ inventory_hostname }} from LB pool"
delegate_to: loadbalancer.example.com
- name: Perform maintenance on this host
ansible.builtin.debug:
msg: "Doing maintenance on {{ inventory_hostname }}"
- name: Re-add this host to the load balancer pool
ansible.builtin.debug:
msg: "Re-adding {{ inventory_hostname }} to LB pool"
delegate_to: loadbalancer.example.comA Gotcha: Variables Still Resolve From The Original Host, Not The Delegated One
This is the real trap. Even though the task genuinely executes on the delegated host, variable and fact lookups inside it still resolve using the originally-targeted host’s context by default — not the delegated host’s own. {{ ansible_facts['hostname'] }} inside a delegated task gives the original host’s hostname fact, not the load balancer’s, even though the task is physically running on the load balancer.
- name: Show hostname during delegation
ansible.builtin.debug:
msg: "ansible_facts: {{ ansible_facts['hostname'] }}, hostvars: {{ hostvars['loadbalancer.example.com']['ansible_facts']['hostname'] }}"
delegate_to: loadbalancer.example.comThe first value is the original host’s hostname — confusing, given this task is actually executing on the load balancer. The second, via hostvars (the magic variable from the variables section), correctly gives the load balancer’s own hostname. Knowing which one you actually need matters.
run_once: Ensuring A Task Happens Exactly Once
Some tasks are genuinely play-wide, not per-host — one deployment-start notification, not one per host being deployed:
- name: Send one deployment-start notification
ansible.builtin.debug:
msg: "Deployment starting for {{ ansible_play_hosts | join(', ') }}"
run_once: trueWithout run_once, this would fire once per host in the play, redundantly. With it, it runs exactly once — on the first host in the play’s execution order — while its own content can still reference the whole group, via ansible_play_hosts, exactly as covered in the variables section’s magic variables chapter.
Combining delegate_to And run_once
A genuinely common real pattern — one notification, run from a sensible, dedicated place rather than an arbitrary member of the group:
- name: Send one deployment notification from a dedicated host
ansible.builtin.debug:
msg: "Deployment starting for {{ ansible_play_hosts | join(', ') }}"
run_once: true
delegate_to: localhostRuns exactly once, executing on the control node specifically, rather than on whichever host happened to be first.
Best Practices
- Use
delegate_to: localhostfor genuinely control-node-side actions taken on behalf of a remote host, not something that should run remotely at all. - Remember variable and fact context stays with the original host during a delegated task — reach for
hostvars[delegated_host]explicitly when you need the delegated host’s own data instead. - Use
run_oncefor genuinely play-wide actions that should happen exactly once, not per host. - Combine
run_oncewithdelegate_to: localhostfor the common “one notification, from the control node” pattern.