Skip to content

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.com

inventory_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: localhost

This 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.com

A 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.com

The 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: true

Without 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: localhost

Runs exactly once, executing on the control node specifically, rather than on whichever host happened to be first.

Best Practices

  • Use delegate_to: localhost for 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_once for genuinely play-wide actions that should happen exactly once, not per host.
  • Combine run_once with delegate_to: localhost for the common “one notification, from the control node” pattern.
Last updated on