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 run_once vs delegate_to vs serial: Control Task Execution Scope

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

Complete comparison of Ansible run_once, delegate_to, and serial. Learn when to run tasks on a single host, delegate to a specific target, or control batch.

run_once, delegate_to, and serial all control where and how often tasks execute — but they solve completely different problems. Confusing them causes duplicate operations, missed hosts, or broken rolling updates.

Quick Comparison

| Feature | run_once | delegate_to | serial | |---------|----------|-------------|--------| | What it does | Run task once for entire play | Run task on a different host | Process hosts in batches | | Scope | Play level | Task level | Play level | | Other hosts | Skip the task | Still run (on delegate target) | Wait for batch to finish | | Common use | DB migrations, downloads | Load balancer, monitoring | Rolling deployments | | Variables from | First host in batch | Original host (not delegate) | Current batch hosts |

See also: Ansible run_once: Execute Tasks Once Across All Hosts (Complete Guide)

run_once: Execute Once for All Hosts

run_once: true runs a task on the first host only, skipping it for all other hosts.

- name: Deploy to web cluster
  hosts: webservers  # web01, web02, web03, web04
  tasks:
    - name: Run database migration (once only)
      ansible.builtin.command: /opt/app/bin/migrate up
      run_once: true
      # Runs on web01 only, skips web02/03/04

- name: Deploy application code ansible.builtin.copy: src: app.tar.gz dest: /opt/app/ # Runs on ALL hosts

run_once with serial

When using serial, run_once runs once per batch, not once for the entire play:

- hosts: webservers  # web01, web02, web03, web04
  serial: 2
  tasks:
    - name: Download artifact
      ansible.builtin.get_url:
        url: "https://releases.example.com/app-{{ version }}.tar.gz"
        dest: /tmp/app.tar.gz
      run_once: true
      # Runs on web01 (batch 1) AND web03 (batch 2)
      # ⚠️ Not truly "once" — once per batch!

Common run_once Patterns

# Database migration
- ansible.builtin.command: /opt/app/bin/migrate
  run_once: true

# Download shared artifact - ansible.builtin.get_url: url: "{{ artifact_url }}" dest: /tmp/artifact.tar.gz run_once: true delegate_to: localhost # Download to Ansible controller

# Send deployment notification - ansible.builtin.uri: url: "https://slack.webhook.url" method: POST body: '{"text": "Deploying {{ version }}"}' run_once: true

delegate_to: Run on a Different Host

delegate_to executes a task on a different host than the current play target, but uses variables from the original host.

- name: Rolling update with load balancer
  hosts: webservers
  serial: 1
  tasks:
    - name: Remove from load balancer
      ansible.builtin.uri:
        url: "http://lb.example.com/api/pool/web/members/{{ inventory_hostname }}"
        method: DELETE
      delegate_to: lb.example.com
      # Runs on lb.example.com, but {{ inventory_hostname }} is the current web server

- name: Deploy application ansible.builtin.copy: src: app.tar.gz dest: /opt/app/

- name: Add back to load balancer ansible.builtin.uri: url: "http://lb.example.com/api/pool/web/members" method: POST body: '{"host": "{{ inventory_hostname }}", "port": 8080}' delegate_to: lb.example.com

delegate_to: localhost

# Run a command on the Ansible controller
- name: Add DNS record
  community.general.cloudflare_dns:
    zone: example.com
    record: "{{ inventory_hostname }}"
    type: A
    value: "{{ ansible_default_ipv4.address }}"
  delegate_to: localhost

# Wait for a port from the controller's perspective - name: Wait for service to be reachable ansible.builtin.wait_for: host: "{{ inventory_hostname }}" port: 8080 timeout: 60 delegate_to: localhost

Variable Context with delegate_to

- hosts: webservers  # web01 (10.0.1.1), web02 (10.0.1.2)
  tasks:
    - name: Register on monitoring server
      ansible.builtin.lineinfile:
        path: /etc/monitoring/targets.conf
        line: "{{ inventory_hostname }} {{ ansible_default_ipv4.address }}"
      delegate_to: monitoring.example.com
      # Runs on monitoring.example.com
      # But inventory_hostname = web01/web02 (original host)
      # And ansible_default_ipv4 = 10.0.1.1/10.0.1.2 (original host facts)

delegate_facts

To store gathered facts on the delegate target instead of the original host:

- name: Gather facts from monitoring server
  ansible.builtin.setup:
  delegate_to: monitoring.example.com
  delegate_facts: true
  # Facts stored under hostvars['monitoring.example.com'], not current host

See also: How to Do Rolling Updates with Zero Downtime in Ansible

serial: Batch Execution

serial processes hosts in batches, completing all tasks on one batch before starting the next.

# Process 2 hosts at a time
- hosts: webservers  # 8 hosts
  serial: 2
  tasks:
    - name: Stop service
      ansible.builtin.systemd:
        name: myapp
        state: stopped

- name: Deploy code ansible.builtin.copy: src: app.tar.gz dest: /opt/app/

- name: Start service ansible.builtin.systemd: name: myapp state: started # Batch 1: web01, web02 (stop → deploy → start) # Batch 2: web03, web04 (stop → deploy → start) # Batch 3: web05, web06 ... # Batch 4: web07, web08 ...

Percentage-Based serial

# Process 25% of hosts at a time
- hosts: webservers
  serial: "25%"

Ramping serial (Canary Pattern)

# Start with 1 host, then ramp up
- hosts: webservers
  serial:
    - 1      # First: 1 host (canary)
    - 5      # Then: 5 hosts
    - "100%" # Finally: all remaining
  tasks:
    - name: Deploy and verify
      ansible.builtin.include_tasks: deploy.yml

max_fail_percentage

# Abort if more than 30% of hosts in a batch fail
- hosts: webservers
  serial: 5
  max_fail_percentage: 30
  tasks:
    - name: Deploy
      ansible.builtin.include_tasks: deploy.yml

Combining All Three

- name: Zero-downtime rolling deployment
  hosts: webservers
  serial: 2                    # 2 hosts at a time
  max_fail_percentage: 25
  tasks:
    - name: Announce deployment start
      ansible.builtin.uri:
        url: "{{ slack_webhook }}"
        method: POST
        body: '{"text": "Starting batch deploy"}'
      run_once: true            # Once per batch

- name: Drain connections on load balancer ansible.builtin.uri: url: "http://lb/api/drain/{{ inventory_hostname }}" method: POST delegate_to: lb.example.com # Run on LB, using webserver vars

- name: Wait for connections to drain ansible.builtin.wait_for: timeout: 30

- name: Deploy application ansible.builtin.copy: src: "releases/{{ version }}.tar.gz" dest: /opt/app/

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

- name: Verify health ansible.builtin.uri: url: "http://{{ inventory_hostname }}:8080/health" register: health until: health.status == 200 retries: 10 delay: 5 delegate_to: localhost # Check from controller

- name: Re-enable on load balancer ansible.builtin.uri: url: "http://lb/api/enable/{{ inventory_hostname }}" method: POST delegate_to: lb.example.com

See also: Ansible run_once: Execute Task on Single Host (Complete Guide)

FAQ

Does run_once guarantee execution on the first host?

It runs on the first host in the current batch (as determined by inventory order). With serial, it runs once per batch, not once for the entire play. If you need truly once, use run_once: true with delegate_to: localhost.

Can I delegate_to a host not in my inventory?

Yes, but Ansible won't have facts for it unless you gather them explicitly. The host must be reachable via SSH (or the appropriate connection plugin). Add it to inventory for best results.

What happens if a host fails in a serial batch?

The remaining tasks for that host are skipped. If max_fail_percentage is exceeded, the entire play aborts. Otherwise, the next batch proceeds.

Can I use serial with free strategy?

No. serial requires the default linear strategy (or host_pinned). The free strategy lets hosts proceed independently, which conflicts with batch processing.

Conclusion

run_once for tasks that should execute once (migrations, notifications). delegate_to for tasks that target a different host (load balancers, monitoring, DNS). serial for batch processing (rolling updates, canary deploys). Combine all three for zero-downtime rolling deployments.

Related Articles

Ansible serial Rolling Updates GuideAnsible delegate_to Guidehow Ansible handlers fire on changesAnsible Playbook Structure Guide

Category: database-automation

Browse all Ansible tutorials · AnsiblePilot Home