Introduction
Rolling updates patch servers in small batches instead of all at once — ensuring some servers are always available to handle traffic. Ansible's serial keyword controls batch size, while health checks and conditional reboots keep updates safe and predictable.
Basic Rolling Update
Update a Single Package
---
- name: Rolling update nginx
hosts: web_servers
become: true
serial: 2 # Update 2 servers at a time
tasks:
- name: Update nginx to latest
ansible.builtin.yum:
name: nginx
state: latest
update_cache: true
notify: restart nginx
handlers:
- name: restart nginx
ansible.builtin.systemd:
name: nginx
state: restarted
Update All System Packages
---
- name: Full system update
hosts: all
become: true
serial: 2
tasks:
- name: Update all packages
ansible.builtin.yum:
name: "*"
state: latest
update_cache: true
Serial Strategies
Fixed Batch Size
serial: 3 # Always 3 at a time
Percentage-Based
serial: "25%" # 25% of hosts per batch
Escalating Batches
serial:
- 1 # First batch: 1 server (canary)
- 3 # Second batch: 3 servers
- "50%" # Remaining: half at a time
This is the safest strategy — test on one server first, then gradually increase.
Production Rolling Update with Health Checks
---
- name: Production rolling update
hosts: web_servers
become: true
serial:
- 1
- "30%"
max_fail_percentage: 10
pre_tasks:
- name: Disable server in load balancer
ansible.builtin.uri:
url: "http://lb.example.com/api/servers/{{ inventory_hostname }}/disable"
method: POST
delegate_to: localhost
- name: Wait for connections to drain
ansible.builtin.wait_for:
timeout: 30
tasks:
- name: Update all packages
ansible.builtin.yum:
name: "*"
state: latest
update_cache: true
register: update_result
- name: Check if reboot is needed
ansible.builtin.command: needs-restarting -r
register: reboot_check
failed_when: false
changed_when: reboot_check.rc == 1
- name: Reboot if needed
ansible.builtin.reboot:
reboot_timeout: 600
msg: "Rebooting for package updates"
when: reboot_check.rc == 1
- name: Wait for service to be ready
ansible.builtin.wait_for:
port: 80
delay: 10
timeout: 120
post_tasks:
- name: Verify application health
ansible.builtin.uri:
url: "http://{{ inventory_hostname }}/health"
status_code: 200
register: health_check
retries: 5
delay: 10
until: health_check.status == 200
- name: Re-enable in load balancer
ansible.builtin.uri:
url: "http://lb.example.com/api/servers/{{ inventory_hostname }}/enable"
method: POST
delegate_to: localhost
Security-Only Updates
- name: Security patches only
hosts: all
become: true
serial: 3
tasks:
- name: Install security updates
ansible.builtin.yum:
name: "*"
state: latest
security: true
register: security_update
- name: Show what was updated
ansible.builtin.debug:
msg: "Updated {{ security_update.changes.updated | default([]) | length }} packages"
Bugfix-Only Updates
- name: Bugfix updates only
ansible.builtin.yum:
name: "*"
state: latest
bugfix: true
Update with Rollback
---
- name: Update with rollback capability
hosts: web_servers
become: true
serial: 1
tasks:
- name: Create system snapshot
ansible.builtin.command: >
yum history info last
register: yum_history_before
changed_when: false
- name: Perform updates
block:
- name: Update packages
ansible.builtin.yum:
name: "*"
state: latest
register: update_result
- name: Verify service is running
ansible.builtin.uri:
url: "http://{{ inventory_hostname }}/health"
status_code: 200
register: health
retries: 3
delay: 10
until: health.status == 200
rescue:
- name: Rollback updates
ansible.builtin.command: yum history undo last -y
when: update_result is changed
- name: Restart services after rollback
ansible.builtin.systemd:
name: nginx
state: restarted
- name: Notify about rollback
ansible.builtin.debug:
msg: "⚠️ Update rolled back on {{ inventory_hostname }}"
Exclude Packages
- name: Update all except kernel
ansible.builtin.yum:
name: "*"
state: latest
exclude:
- kernel*
- docker*
Update Report
- name: Update and report
hosts: all
become: true
serial: 5
tasks:
- name: Update all packages
ansible.builtin.yum:
name: "*"
state: latest
register: update_result
- name: Save update report
ansible.builtin.copy:
content: |
Host: {{ inventory_hostname }}
Date: {{ ansible_date_time.iso8601 }}
Updated: {{ update_result.changes.updated | default([]) | length }} packages
Installed: {{ update_result.changes.installed | default([]) | length }} packages
dest: "/var/log/ansible-update-{{ ansible_date_time.date }}.log"
when: update_result is changed
Parameters Reference
| Parameter | Values | Description |
|---|---|---|
name | package name or "*" | Package(s) to update |
state | latest | Update to newest version |
update_cache | true | Refresh repo metadata first |
security | true | Only security updates |
bugfix | true | Only bugfix updates |
exclude | list | Packages to skip |
disablerepo | string | Disable specific repos |
enablerepo | string | Enable specific repos |
Related Articles
- Install Packages: yum Module
- Handle Yum Failures
- Ansible Best Practices Guide
- Ansible Roles Explained
Conclusion
Rolling updates with serial ensure your servers stay available during patching. Start with escalating batches (serial: [1, 3, "50%"]) to catch issues early, use needs-restarting -r for conditional reboots, and add load balancer drain/enable steps for production. For safety, use block/rescue with yum history undo for automatic rollback on failure.