Skip to main content
RunBook Academy

LinuxXXXIII · Fleet Patch ManagementStrategy

Fleet patch strategy - waves, canaries, and rollback

Intermediate⏱ ~10 minAnsiblemonitoring

What you'll learn

  • Design a fleet patch strategy
  • Apply patches in waves
  • Validate health at each wave
  • Roll back on regression

Prerequisites

Verified against Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL 9.x · Rocky Linux 9.x · AlmaLinux 9.x · Linux kernel 6.1 LTS / 6.6 LTS · systemd 255+ · OpenSSH 8.7p1 (RHEL 9) / 9.6p1 (Ubuntu 24.04) · nftables 1.0.x · chrony 4.x · Pacemaker 2.1.x · Corosync 3.1.x · 2026-08-09

Not yet marked complete on this device.

Patching a fleet is not “apt upgrade on every host”. It is a disciplined wave-based rollout with health validation and rollback capability.

The wave strategy

Wave 1: dev / staging (immediate)
Wave 2: canary (5-10% of production)
Wave 3: production (50%)
Wave 4: production (remaining)

Each wave has a hold period (15-60 minutes) to validate health before proceeding to the next wave. That hold has to be implemented by something - a pause step, a scheduler, a human gate. No tool gives it to you for free.

Apply via configuration management

Use Ansible, Puppet, or similar to apply patches:

# Ansible playbook
- name: Apply security patches
  hosts: all
  serial: "5%"
  max_fail_percentage: 0        # any host failure aborts the whole rollout
  tasks:
    - name: Security-only upgrade
      ansible.builtin.apt:
        upgrade: safe           # never removes packages; dist can
        update_cache: yes
        cache_valid_time: 3600
      environment:
        DEBIAN_FRONTEND: noninteractive

    - name: Detect pending reboot (Debian/Ubuntu)
      ansible.builtin.stat:
        path: /var/run/reboot-required
      register: reboot_required

    - name: Reboot if the kernel or libc changed
      ansible.builtin.reboot:
        reboot_timeout: 600
      when: reboot_required.stat.exists

    - name: Verify service health from outside the host
      ansible.builtin.uri:
        url: "https://{{ inventory_hostname }}/health"
        status_code: 200
      register: health
      until: health.status == 200
      retries: 10
      delay: 15
      delegate_to: localhost

    - name: Soak before the next batch
      ansible.builtin.pause:
        minutes: 15

On RHEL, replace the stat check with a needs-restarting -r command task: exit code 1 means a reboot is required.

serial: "5%" rolls out to 5% of hosts at a time. It provides batching only - the delay between batches is the explicit pause task, and the abort gate is max_fail_percentage.

Health validation

After each wave, check:

  • Service is responding (HTTP health endpoint), probed from outside the host.
  • The running kernel matches the newest installed kernel: uname -r against /boot/vmlinuz-*.
  • No pending reboot: /var/run/reboot-required absent, or needs-restarting -r exits 0.
  • No process still mapping a replaced library: needrestart -b (Debian/Ubuntu) or needs-restarting -s (RHEL) returns an empty service list.
  • No failed units: systemctl list-units --state=failed.
  • No new errors in journald / auditd.
  • CPU, memory, network within normal range.
  • Synthetic monitoring passes.

The first four are what separate installed from remediated. A vulnerability scanner reads package versions and will call a patched-but-not-rebooted host compliant. It is not. linux-checklist-post-patching-validation carries the full list with commands.

Automate the validation:

# After a patch wave, check the error rate.
# --data-urlencode does the escaping: a bare
# ...?query=rate(errors[5m]) is a shell syntax error, because ( ) and [ ]
# are shell metacharacters, and the URL would be malformed even if it ran.
error_rate=$(curl -sG http://prometheus:9090/api/v1/query \
    --data-urlencode 'query=sum(rate(http_requests_total{code=~"5.."}[5m]))' \
    | jq -r '.data.result[0].value[1] // "0"')

# bc, not [[ ]] - bash cannot compare floating-point numbers
if (( $(echo "$error_rate > 0.01" | bc -l) )); then
    echo "Error rate too high: $error_rate - halting the rollout" >&2
    exit 1
fi

Rollback

Have a rollback plan for every patch:

  • Packages: sudo dnf history undo <id> or sudo apt-get install <package>=<old-version>. Use undo, not rollback: dnf history undo <id> reverses that one transaction, while dnf history rollback <id> reverses every transaction performed after <id>, sweeping up unrelated security updates applied since. linux-rollback-strategies covers the distinction.
  • Containers: redeploy the previous image.
  • Configuration: revert the configuration management state.

Test rollback in staging: deploy the patch, deploy the rollback, verify both work.

Maintenance windows

For non-emergency patches, schedule maintenance windows:

  • Off-peak hours (typically nights or weekends).
  • Pre-announced to stakeholders.
  • Limited duration (2-4 hours).
  • Rollback ready.

The window is a contract: the change starts at T+0, rolls forward at T+30 minutes, completes by T+window-end. If something goes wrong, rollback by T+window-end.

Knowledge check

Knowledge check · 6 questions

  1. Q1. What is the first wave of a fleet patch?

  2. Q2. Patches should always be applied to all hosts at once.

  3. Q3. Which of the following are valid wave strategies? Select all that apply.

  4. Q4. Your playbook uses serial: "5%" and a health-check task, and the canary batch fails its health check. With default Ansible settings, what happens next?

  5. Q5. A wave completes. Every host reports the fixed openssl package installed, and the scanner shows the CVE remediated. What still has to be verified before signing the wave off?

  6. Q6. serial: "5%" inserts a pause between batches, so no explicit soak step is needed.

Passing score: 75%. Answers are checked in this browser.