Advanced Execution¶
- In this lab we explore advanced Ansible execution strategies: rolling updates, task delegation, parallel execution control, and
run_once. - These techniques are essential for managing large fleets and zero-downtime deployments.
What will we learn?¶
serialfor rolling updates (batched execution)delegate_toto run tasks on a different hostrun_oncefor tasks that should run only on one hostthrottleandforksfor parallel execution controlmax_fail_percentagefor safe deployments
Prerequisites¶
- Complete Lab 004 in order to have working Ansible playbooks.
01. Rolling Updates with serial¶
---
- name: Rolling update
hosts: webservers # 10 servers
serial: 2 # Update 2 at a time
become: true
tasks:
- name: Take host out of load balancer
ansible.builtin.uri:
url: "http://lb.example.com/remove/{{ inventory_hostname }}"
method: POST
delegate_to: localhost
- name: Update application
ansible.builtin.apt:
name: myapp
state: latest
- name: Restart application
ansible.builtin.service:
name: myapp
state: restarted
- name: Wait for application to be ready
ansible.builtin.uri:
url: "http://{{ inventory_hostname }}/health"
status_code: 200
retries: 10
delay: 5
- name: Re-add host to load balancer
ansible.builtin.uri:
url: "http://lb.example.com/add/{{ inventory_hostname }}"
method: POST
delegate_to: localhost
serial Options¶
# Process 2 hosts at a time
serial: 2
# Process 30% of hosts at a time
serial: "30%"
# Escalating batch sizes: 1, then 5, then rest
serial:
- 1
- 5
- "100%"
# Max failures before stopping
max_fail_percentage: 10
02. delegate_to - Run on a Different Host¶
tasks:
# Run this task on localhost (the control node), not the target
- name: Send deployment notification
ansible.builtin.uri:
url: https://hooks.example.com/deploy
method: POST
body_format: json
body:
host: "{{ inventory_hostname }}"
status: deploying
delegate_to: localhost
# Run this task on a specific server in inventory
- name: Trigger backup on the NAS
ansible.builtin.command:
cmd: /opt/backup/backup.sh {{ inventory_hostname }}
delegate_to: backup-server
# Copy a file FROM one server TO another
- name: Sync config from primary to secondary
ansible.posix.synchronize:
src: /etc/nginx/nginx.conf
dest: /etc/nginx/nginx.conf
mode: push
delegate_to: primary-web
TIP: Use
delegate_facts: trueto store gathered facts under the delegated host’s name instead of the original host.
03. run_once - Execute Only Once in the Play¶
tasks:
# Run this task only ONCE, on the first host in the group
- name: Create database schema
ansible.builtin.command:
cmd: /opt/app/manage.py migrate
run_once: true
# Combine with delegate_to
- name: Send deployment notification to Slack
community.general.slack:
token: "{{ slack_token }}"
msg: "Deploying {{ app_version }} to {{ ansible_play_hosts | length }} servers"
run_once: true
delegate_to: localhost
04. throttle - Limit Parallel Task Execution¶
tasks:
# Run this task on max 3 hosts simultaneously (even with high forks)
- name: Restart memcached (rate limited)
ansible.builtin.service:
name: memcached
state: restarted
throttle: 3
# Useful for services that can't all restart simultaneously
- name: Rolling service restart
ansible.builtin.service:
name: elasticsearch
state: restarted
throttle: 1 # One at a time to maintain cluster quorum
05. forks - Global Parallelism¶
# Override at runtime
ansible-playbook site.yml --forks 30
# For sequential execution (1 host at a time)
ansible-playbook site.yml --forks 1
06. Wait for Conditions¶
tasks:
# Wait until a port is open
- name: Wait for service port to be ready
ansible.builtin.wait_for:
host: "{{ inventory_hostname }}"
port: 8080
delay: 5
timeout: 120
# Wait for a file to exist
- name: Wait for lock file to disappear
ansible.builtin.wait_for:
path: /var/run/deploy.lock
state: absent
timeout: 60
# Wait for HTTP endpoint
- name: Wait for health check
ansible.builtin.uri:
url: "http://{{ inventory_hostname }}:8080/health"
status_code: 200
register: health_check
until: health_check.status == 200
retries: 12
delay: 10
# Pause for a specified time
- name: Pause to let service stabilize
ansible.builtin.pause:
seconds: 30
# Pause with a message (prompts user to continue)
- name: Manual approval gate
ansible.builtin.pause:
prompt: "Check the deployment. Press Enter to continue or Ctrl+C to abort"
07. Retries on Failure¶
tasks:
# Retry a task until it succeeds
- name: Download package (with retries)
ansible.builtin.get_url:
url: https://example.com/package.tar.gz
dest: /tmp/package.tar.gz
register: download
until: download is succeeded
retries: 5
delay: 10
# Retry with custom success condition
- name: Wait for cluster to reach quorum
ansible.builtin.command:
cmd: /opt/db/check-quorum.sh
register: quorum_check
until: quorum_check.rc == 0
retries: 10
delay: 30
changed_when: false

08. Hands-on¶
- Create a rolling update playbook
lab029-rolling.ymlthat processes one host at a time and stops if more than 50% of hosts fail.
??? success “Solution”
docker exec ansible-controller sh -c "cat > /labs-scripts/lab029-rolling.yml << 'EOF'
---
- name: Rolling Update Simulation
hosts: all
serial: 1 # One host at a time
max_fail_percentage: 50 # Stop if more than 50% fail
tasks:
- name: Simulate taking host offline
ansible.builtin.debug:
msg: \"Taking {{ inventory_hostname }} out of rotation\"
delegate_to: localhost
- name: Simulate update
ansible.builtin.command:
cmd: \"sleep 1 && echo 'Updated!'\"
register: update_out
changed_when: true
- name: Verify update
ansible.builtin.debug:
msg: \"{{ update_out.stdout }}\"
- name: Simulate bringing host back online
ansible.builtin.debug:
msg: \"{{ inventory_hostname }} back online\"
delegate_to: localhost
- name: Post-update notification
hosts: all
gather_facts: false
tasks:
- name: Notify completion (once)
ansible.builtin.debug:
msg: \"All {{ ansible_play_hosts | length }} hosts updated successfully!\"
run_once: true
EOF"
- Run the rolling update playbook and observe that hosts are processed one at a time.
??? success “Solution”
docker exec ansible-controller sh -c "cd /labs-scripts && ansible-playbook lab029-rolling.yml"
### Output
# PLAY [Rolling Update Simulation] ****
# Each host is processed individually before moving to the next
- Create a delegation example playbook
lab029-delegate.ymlthat creates a file on remote hosts and then checks it once from the control node.
??? success “Solution”
docker exec ansible-controller sh -c "cat > /labs-scripts/lab029-delegate.yml << 'EOF'
---
- name: Delegation Example
hosts: all
gather_facts: false
tasks:
- name: Create a remote file
ansible.builtin.copy:
content: \"Created on {{ inventory_hostname }}\n\"
dest: /tmp/remote-file.txt
- name: Check file from control node
ansible.builtin.command:
cmd: \"echo 'Checking from: {{ inventory_hostname }}'\"
delegate_to: localhost
run_once: true
EOF"
docker exec ansible-controller sh -c "cd /labs-scripts && ansible-playbook lab029-delegate.yml"
- Run a playbook with
--forks 1to force strictly sequential execution and compare the runtime to the default forks setting.
??? success “Solution”
# Sequential (forks=1)
docker exec ansible-controller sh -c "cd /labs-scripts && ansible-playbook lab029-rolling.yml --forks 1"
# Default forks
docker exec ansible-controller sh -c "cd /labs-scripts && ansible-playbook lab029-rolling.yml"
09. Summary¶
serial: Nenables rolling updates - N hosts are updated at a timemax_fail_percentagestops the deployment if too many hosts faildelegate_to: localhostruns a task on the control node (load balancer calls, notifications)run_once: trueruns a task only once per play (DB migrations, Slack notifications)throttle: Nlimits concurrent task execution regardless offorksuntil+retries+delaycreates resilient polling loops