Skip to main content
RunBook Academy

← All runbooks in Ansible

high riskservice affecting~120 min

Runbook: Perform a rolling reboot

1 · Prerequisites

Confirm every item is in place before any state change.

2 · Pre-checks

Read-only diagnostic commands. If any of these don't match expected output, stop and investigate further.

  • · Out-of-band access exists for every host in scope and has been tested on at least one of them
  • · Every host boots the configuration it is currently running - no unapplied config that only exists in memory
  • · The service can tolerate the loss of one batch: capacity has been checked against the batch size, not assumed
  • · Every filesystem in /etc/fstab mounts cleanly, verified before the reboot rather than discovered after it
  • · Cluster or quorum members in scope are identified, and the batch size respects quorum
  • · The health check has been proven able to fail
  • · Batch size and the total host count come from --list-hosts against the exact command that will run

3 · Procedure

Execute each step in order. Verify the expected output of a step before moving to the next.

  1. 1Verify boot-time configuration will match runtime configuration on every host
  2. 2Capture the pre-reboot baseline: uptime, kernel, running services, mounted filesystems
  3. 3Drain the batch from the load balancer or cluster and confirm connections have finished
  4. 4Reboot the batch
  5. 5Wait for the connection to come back, and separately wait for the host to have actually rebooted
  6. 6Verify services, mounts and the health check on each host before returning it
  7. 7Return the batch to rotation and confirm it is receiving traffic
  8. 8Confirm capacity is restored before draining the next batch
  9. 9Repeat per batch; stop the play automatically if a batch fails
  10. 10Final check: no host is left drained, and every host rebooted

4 · Verification

Confirm the procedure actually fixed the problem.

  • Every host in scope has a boot time later than the start of the maintenance window - a host that did not reboot is a silent failure
  • Every filesystem in fstab is mounted on every host after the reboot
  • Every service that was running before is running after, compared against the captured baseline
  • The health check passes against a real request on each host before it returns to rotation
  • No host is left drained: the load balancer pool and the cluster both show full membership
  • Capacity metrics are back to baseline before the window is closed

5 · Rollback

If verification fails, undo the procedure in reverse order.

  • A reboot cannot be undone - the rollback for this procedure is stopping, not reversing
  • If a host does not come back: stop the rollout immediately, do not drain another batch, and recover that host out of band
  • If a host comes back degraded: keep it drained, leave it out of rotation, and treat it as an incident separate from the rollout
  • If the boot-time configuration turns out to differ from runtime, the remaining hosts have the same latent fault - stop and fix the configuration before continuing
  • Restore capacity first: return any healthy drained hosts to rotation before investigating the failed one
  • Record which hosts rebooted and which did not; a partially rebooted fleet is a state that must be closed out deliberately

6 · Escalation

When the runbook isn't enough, contact:

  • · Escalate immediately if a host does not return within the expected boot time - a hung boot needs console access, not another reboot attempt
  • · Escalate if two hosts in the same batch fail the same way; that is a configuration fault the whole fleet shares
  • · Escalate to the cluster owner before rebooting any host whose loss affects quorum
  • · Escalate to the service owner if capacity cannot be restored between batches - continuing means running the service below its designed redundancy

A rolling reboot is the change with no rollback. Once a host is going down it is going down, and the only available control is deciding whether the next one does.

That reframes the whole procedure. Every gate here exists to protect the hosts that have not been rebooted yet, and the single most important rule is the one that sounds like bookkeeping: do not drain the next batch until the last batch is back in rotation and carrying traffic.

When to use this runbook

  • A kernel, glibc or systemd update requires a reboot (see also the rolling kernel update runbook, which adds the fallback-kernel steps).
  • Clearing a condition that only a reboot resolves.
  • Proving that hosts still boot - a reboot drill.
  • Applying a hypervisor or firmware change that requires a guest restart.

Blast radius

One batch at a time, and the batch size is a capacity decision. If the service needs N hosts and you have N+2, the batch size is 2 and no argument about round numbers changes that.

State it explicitly before starting:

group          web
total hosts    40
required       32
batch size     4      (leaves 36 in service)
batches        10

Step 1: Will it boot into the state it is running?

This is the pre-check that catches the expensive failure, and it is the one most often skipped because it feels unrelated to rebooting.

Read-only / Safeboot-time versus runtime
# Every fstab entry mounts - if this fails now it will fail at boot
ansible web -b -m command -a 'findmnt --verify --verbose' -o

# Services that are running but not enabled will not come back.
# systemctl compares the two lists for you:
ansible web -b -m command \
-a 'systemctl list-unit-files --type=service --state=disabled --no-pager --plain' -o
ansible web -b -m command \
-a 'systemctl list-units --type=service --state=running --no-pager --plain' -o

# Anything already broken
ansible web -b -m command -a 'systemctl --failed --no-pager --plain' -o

Step 2: Capture the baseline

Read-only / Safebaseline
ansible web -b -m command -a 'uptime -s' -o | tee baseline-boottime.txt
ansible web -b -m command -a 'uname -r' -o | tee baseline-kernel.txt
ansible web -b -m command \
-a 'systemctl list-units --type=service --state=running --no-pager --plain' \
> baseline-services.txt
ansible web -b -m command -a 'findmnt -rn -o TARGET' > baseline-mounts.txt

uptime -s prints the boot timestamp. That is the value Step 6 compares against to prove the host actually rebooted, and it is far more reliable than “it responded to ping again”.

Step 3: The play

Service impact possiblerolling-reboot.yml
- name: Rolling reboot
hosts: web
become: true
serial: 4
max_fail_percentage: 0
tasks:
  - name: Record the boot time before we touch anything
    ansible.builtin.command: uptime -s
    register: boot_before
    changed_when: false

  - name: Drain from the load balancer
    ansible.builtin.uri:
      url: "https://lb.example.com/api/pool/web/{{ inventory_hostname }}/drain"
      method: POST
      status_code: [200, 204]
    delegate_to: localhost

  - name: Allow in-flight connections to finish
    ansible.builtin.wait_for:
      timeout: 30
    delegate_to: localhost

  - name: Reboot and wait for the host to come back
    ansible.builtin.reboot:
      msg: "Rolling reboot - scheduled maintenance"
      pre_reboot_delay: 5
      post_reboot_delay: 30
      reboot_timeout: 900
      test_command: systemctl is-system-running --wait

  - name: Confirm the host actually rebooted
    ansible.builtin.command: uptime -s
    register: boot_after
    changed_when: false
    failed_when: boot_after.stdout == boot_before.stdout

  - name: Every fstab entry is mounted
    ansible.builtin.command: findmnt --verify
    changed_when: false

  - name: Service answers a real request
    ansible.builtin.uri:
      url: "http://{{ ansible_host }}:8080/healthz"
      status_code: 200
      return_content: true
    register: health
    retries: 12
    delay: 10
    until: health.status == 200 and 'ok' in health.content

  - name: Return to the load balancer pool
    ansible.builtin.uri:
      url: "https://lb.example.com/api/pool/web/{{ inventory_hostname }}/enable"
      method: POST
      status_code: [200, 204]
    delegate_to: localhost

  - name: Confirm it is receiving traffic before the next batch drains
    ansible.builtin.uri:
      url: "https://lb.example.com/api/pool/web/{{ inventory_hostname }}"
      return_content: true
    register: pool
    retries: 6
    delay: 10
    until: "'active' in pool.content"
    delegate_to: localhost

Read that task list as a contract. Every task between the reboot and the return to the pool is a gate the host must pass while it is out of rotation and harmless. max_fail_percentage: 0 means any host failing any of them ends the play, and the next batch never drains.

The last task is the one people leave out and the one that makes serial safe. Without it, serial proceeds as soon as the play tasks finish, which can be before the load balancer has actually started sending traffic to the returned hosts. Batch after batch, the pool shrinks slightly faster than it recovers, and at batch seven you are below capacity with no single failure to point at.

Step 4: The reboot module’s options, and what they actually do

Read-only / Safeansible-doc
ansible-doc ansible.builtin.reboot | head -60

Verified parameter names on 2.21.3: boot_time_command, connect_timeout, msg, post_reboot_delay, pre_reboot_delay, reboot_command, reboot_timeout, search_paths, test_command.

The two that matter most:

  • reboot_timeout is how long the module waits for the host to come back before failing. The default is generous for a VM and short for a physical host with a long POST and a RAID controller that takes four minutes to initialise. Set it from the observed boot time of this hardware, not from the default.
  • test_command is what the module runs to decide the host is usable. The default proves the connection works. systemctl is-system-running --wait waits for systemd to finish starting units, which is a meaningfully stronger claim - it is the difference between “sshd is up” and “the machine has finished booting”.

Step 5: Watch a batch by hand for the first one

Read-only / Safeobserve the first batch
ansible-playbook -i inventories/production rolling-reboot.yml \
--limit web --list-hosts

ansible-playbook -i inventories/production rolling-reboot.yml \
--limit 'web01.example.com:web02.example.com:web03.example.com:web04.example.com' \
--diff | tee "reboot-batch1.log"

Run the first batch as its own invocation. It costs one extra command and it means the first time you learn that reboot_timeout is too short for this hardware, it happens to four hosts with you watching rather than to forty unattended.

Step 6: Verify, per batch and at the end

Read-only / Safeprove they rebooted
ansible web -b -m command -a 'uptime -s' -o | tee after-boottime.txt
diff baseline-boottime.txt after-boottime.txt | head -50

Every host must show a different boot time. A host with an unchanged boot time did not reboot, whatever the run log said, and it is still running the old kernel or the old configuration while the change record says otherwise.

Read-only / Safeservices and mounts
ansible web -b -m command \
-a 'systemctl list-units --type=service --state=running --no-pager --plain' \
> after-services.txt
diff baseline-services.txt after-services.txt

ansible web -b -m command -a 'findmnt -rn -o TARGET' > after-mounts.txt
diff baseline-mounts.txt after-mounts.txt

ansible web -b -m command -a 'systemctl --failed --no-pager --plain' -o

Missing services and missing mounts are the two findings that the health check will not catch, because a web tier can serve /healthz perfectly while the NFS share holding user uploads never came back.

Step 7: Close out

Read-only / Safenothing left drained
curl -sS https://lb.example.com/api/pool/web | python3 -m json.tool | grep -c '"active"'
ansible web --list-hosts | grep -c 'example.com'

Those counts must match. A host left drained is online, patched, idle and invisible - it reports healthy, it is in the inventory, and it is carrying nothing. Find it now; the alternative is discovering it during the next capacity incident.

Rollback

There is none. A reboot is not reversible, and this section is about what to do instead.

SituationAction
Host does not come back within reboot_timeoutStop the rollout. Do not drain another batch. Recover that host via console or BMC.
Host comes back but a service is missingLeave it drained. Treat it as a separate incident. Do not return it to rotation to “see if it works”.
Two hosts fail the same wayStop. The fault is in the configuration and every remaining host has it.
Capacity has dropped below the required levelReturn every healthy drained host to rotation first, then investigate.
Boot-time config differs from runtimeStop, fix the declaration on the remaining hosts, re-verify Step 1, then resume.
Read-only / Safestop and account for what happened
# Which hosts rebooted?
ansible web -b -m command -a 'uptime -s' -o | tee state-after-abort.txt

# Which are drained?
curl -sS https://lb.example.com/api/pool/web | python3 -m json.tool | grep -B2 drain

A stopped rolling reboot leaves the fleet in three states: rebooted and in service, rebooted and drained, not rebooted. Write that list down and close it out deliberately. It is the state that gets forgotten, and a host that stayed drained after an aborted window is capacity you are paying for and not receiving.

Common patterns

SymptomLikely causeResolution
Host never comes backBoot fault, filesystem check, or a bad fstab entryConsole access; findmnt --verify would have caught the fstab case
Host comes back without a serviceThe service was running but not enabledEnable it; check the whole group for the same gap
A mount is missing after rebootIt was mounted by hand and never added to fstabDeclare it; re-verify the group
reboot task times out on physical hostsDefault reboot_timeout too short for the POSTRaise it based on observed boot time
Play proceeds too fast, capacity dipsNo task confirming the host is back in the poolAdd the pool-membership gate as the last task
A host “rebooted” but is on the old kernelThe reboot never happened; the module or a wrapper failed quietlyCompare uptime -s before and after; that is the only proof
Cluster loses quorum mid-rolloutBatch size did not respect quorumBatch size for cluster members is a quorum calculation, not a percentage

Escalation

Escalate when:

  • A host does not return in the expected time. That needs console access, not a second reboot.
  • Two hosts fail identically. The fleet shares the fault.
  • A host in scope affects quorum.
  • Capacity cannot be restored between batches.

References

  1. ansible.builtin.reboot module
  2. ansible.builtin.wait_for_connection module
  3. Controlling playbook execution: strategies and more