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

ParameterValuesDescription
namepackage name or "*"Package(s) to update
statelatestUpdate to newest version
update_cachetrueRefresh repo metadata first
securitytrueOnly security updates
bugfixtrueOnly bugfix updates
excludelistPackages to skip
disablerepostringDisable specific repos
enablerepostringEnable specific repos

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.