Ansible run_once — Execute Tasks on a Single Host

Introduction

When a play targets multiple hosts, every task runs on every host. But some tasks should only run once — database migrations, schema changes, license activation, or cluster initialization. run_once: true executes the task on the first host in the batch and skips it on all other hosts, while still making the registered results available everywhere.

Basic Usage

---
- name: Deploy application
  hosts: webservers  # 10 hosts
  tasks:
    - name: Run database migration (once only)
      ansible.builtin.command:
        cmd: /opt/app/bin/migrate
      run_once: true
      # Runs on web01 only; web02-web10 skip this task

    - name: Restart application (all hosts)
      ansible.builtin.systemd:
        name: myapp
        state: restarted
      # Runs on all 10 hosts

Which Host Runs It?

By default, run_once executes on the first host in the play's host list:

- hosts: webservers
  # Inventory order: web01, web02, web03
  tasks:
    - name: This runs on web01
      ansible.builtin.debug:
        msg: "I am {{ inventory_hostname }}"
      run_once: true
      # Output: "I am web01"

Control Which Host

    # Use delegate_to to choose the host
    - name: Run migration on db01 specifically
      ansible.builtin.command:
        cmd: /opt/app/bin/migrate
      run_once: true
      delegate_to: db01

    # Run on localhost (controller)
    - name: Send deployment notification
      ansible.builtin.uri:
        url: "https://hooks.slack.com/services/xxx"
        method: POST
        body_format: json
        body:
          text: "Deployment started"
      run_once: true
      delegate_to: localhost

Registered Variables

Variables registered by run_once tasks are available on all hosts:

    - name: Get current version (runs once)
      ansible.builtin.command:
        cmd: curl -s https://api.example.com/version
      register: current_version
      run_once: true
      delegate_to: localhost
      changed_when: false

    - name: Deploy matching version (all hosts)
      ansible.builtin.copy:
        src: "/releases/app-{{ current_version.stdout }}.tar.gz"
        dest: /opt/app/
      # current_version is available on ALL hosts

Common Use Cases

Database Migrations

- name: Application deployment
  hosts: app_servers
  serial: "50%"
  tasks:
    - name: Deploy code
      ansible.builtin.unarchive:
        src: app-v2.tar.gz
        dest: /opt/app/

    - name: Run database migration
      ansible.builtin.command:
        cmd: /opt/app/bin/migrate --no-input
      run_once: true
      register: migration
      changed_when: "'Migrated' in migration.stdout"

    - name: Show migration result
      ansible.builtin.debug:
        msg: "{{ migration.stdout_lines | last }}"
      run_once: true
      when: migration.changed

Cluster Initialization

- name: Initialize Redis cluster
  hosts: redis_nodes
  tasks:
    - name: Start Redis on all nodes
      ansible.builtin.systemd:
        name: redis
        state: started

    - name: Create cluster (once)
      ansible.builtin.command:
        cmd: >
          redis-cli --cluster create
          {% for host in groups['redis_nodes'] %}
          {{ hostvars[host].ansible_host }}:6379
          {% endfor %}
          --cluster-replicas 1 --cluster-yes
      run_once: true
      register: cluster_init
      changed_when: "'OK' in cluster_init.stdout"

Cache Clearing

    - name: Flush CDN cache (once per deployment)
      ansible.builtin.uri:
        url: "https://cdn.example.com/api/purge"
        method: POST
        headers:
          Authorization: "Bearer {{ cdn_token }}"
      run_once: true
      delegate_to: localhost
      no_log: true

Leader Election

    - name: Determine cluster leader
      ansible.builtin.command:
        cmd: etcdctl endpoint status --write-out=json
      register: etcd_status
      run_once: true
      changed_when: false

    - name: Run maintenance on leader only
      ansible.builtin.command:
        cmd: etcdctl defrag
      when: inventory_hostname == (etcd_status.stdout | from_json)[0].endpoint

run_once with serial

- hosts: webservers      # 20 hosts
  serial: 5              # 4 batches of 5
  tasks:
    - name: This runs ONCE PER BATCH
      ansible.builtin.debug:
        msg: "Batch leader: {{ inventory_hostname }}"
      run_once: true
      # Runs 4 times total (once per batch), not once for the entire play

    # To run truly once across all batches, use a flag:
    - name: Check if already done
      ansible.builtin.stat:
        path: /tmp/.migration_done
      register: migration_flag

    - name: Run migration only if not done
      ansible.builtin.command:
        cmd: /opt/app/migrate.sh
      run_once: true
      when: not migration_flag.stat.exists

    - name: Set migration flag
      ansible.builtin.file:
        path: /tmp/.migration_done
        state: touch
      run_once: true
      delegate_to: localhost

run_once vs delegate_to vs when

ApproachUse Case
run_once: trueRun on one host, share results with all
delegate_to: hostRun on a specific host, counted against current host
when: inventory_hostname == groups['all'][0]Explicit first-host targeting
run_once: true + delegate_to: localhostRun on controller once

Troubleshooting

IssueSolution
Task runs multiple times with serialrun_once runs once per batch; use a flag file for true single execution
Wrong host runs the taskAdd delegate_to: specific_host
Registered var not availableShould work — run_once shares vars; check variable name
Task skipped on all hostsCheck when condition isn't conflicting with run_once
Need result on specific hostUse delegate_to + delegate_facts: true

Best Practices

  1. Use for database migrations — the #1 use case for run_once
  2. Combine with delegate_to — control exactly which host runs it
  3. Be aware of serial behavior — run_once is per-batch, not per-play
  4. Register and share — registered vars from run_once are available on all hosts
  5. Add changed_when — help Ansible know if the one-time task actually changed something
  6. Use for notifications — Slack/email alerts should fire once, not per host

Conclusion

run_once ensures tasks like database migrations, cache purges, and cluster initialization happen exactly once — not once per host. Combined with delegate_to, you control which host runs it. Just remember: with serial, run_once means once per batch, not once total. For truly single execution across batches, add a flag file or conditional check.