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
| Approach | Use Case |
|---|---|
run_once: true | Run on one host, share results with all |
delegate_to: host | Run on a specific host, counted against current host |
when: inventory_hostname == groups['all'][0] | Explicit first-host targeting |
run_once: true + delegate_to: localhost | Run on controller once |
Troubleshooting
| Issue | Solution |
|---|---|
Task runs multiple times with serial | run_once runs once per batch; use a flag file for true single execution |
| Wrong host runs the task | Add delegate_to: specific_host |
| Registered var not available | Should work — run_once shares vars; check variable name |
| Task skipped on all hosts | Check when condition isn't conflicting with run_once |
| Need result on specific host | Use delegate_to + delegate_facts: true |
Best Practices
- Use for database migrations — the #1 use case for
run_once - Combine with
delegate_to— control exactly which host runs it - Be aware of
serialbehavior —run_onceis per-batch, not per-play - Register and share — registered vars from
run_onceare available on all hosts - Add
changed_when— help Ansible know if the one-time task actually changed something - 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.