Skip to main content
RunBook Academy

AnsibleXLVIII · Maintenance Windows and RollbackMaintenance windows and rollback

A catalogue of rollback patterns, and what each is for

Advanced⏱ ~30 minansible-playbook

What you'll learn

  • Select the rollback pattern that matches the change being made
  • State for each pattern what it restores and what it leaves behind
  • Place a change on the reversibility spectrum before scheduling it
  • Recognise the changes for which no rollback pattern applies

Prerequisites

Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11

Not yet marked complete on this device.

“Roll back” is not one operation. It is five, and they differ in what they restore, how long they take, what they cost, and what they silently leave behind. Choosing between them is the work; the previous lesson said the choice must be made in advance, and this lesson is the menu.

The organising question for each pattern is not “how do I do this” but “what failure does this answer, and what does it not touch”.

The reversibility spectrum

Before the patterns, the axis they sit on.

ChangeReversibilityPatternTypical cost
Config file contentFullRestore previous fileSeconds
Service unit contentFullRevert unit, daemon_reloadSeconds
Application artefactFullRedeploy previous releaseSeconds to minutes
Package versionPartialVersion-pinned reinstallMinutes, may fail
KernelPartialBoot the previous entryA reboot
Whole machine stateFull, coarseSnapshot revertMinutes, loses newer data
Database schemaNone, usuallyDown-migration if written, else restoreTens of minutes, data loss
Deleted dataNoneRestore from backupHours, data loss
Provider-side secret rotationNoneRotate forward againVaries

Read the table top to bottom and the cost climbs faster than the difficulty of the change does. A three-character edit to a config file and a three-character edit to a migration script look identical in a diff and sit at opposite ends of this table.

Pattern 1 — restore the previous configuration file

Answers: a config change that produced bad behaviour. Restores: the file. Nothing else. Cost: seconds, plus a service reload.

The mechanism is backup: true on copy or template, which the documentation describes as “Create a backup file including the timestamp information so you can get the original file back if you somehow clobbered it incorrectly.” The path of that file comes back in the backup_file return value, documented as returned “changed and if backup=yes”.

Configuration changethe forward change, recording its own reverse
- name: Deploy the configuration
ansible.builtin.template:
  src: app.conf.j2
  dest: /etc/app/app.conf
  owner: root
  group: app
  mode: '0640'
  backup: true
register: config_write
notify: reload app

- name: Record the backup path for the reverse procedure
ansible.builtin.copy:
  content: "{{ config_write.backup_file }}"
  dest: /var/lib/app-change/last-config-backup
  mode: '0600'
when: config_write.backup_file is defined

When it is the right choice: the change was a config edit, the service is stateless or nearly so, and the effect is bounded by what the running process does now rather than what it did while misconfigured.

Pattern 2 — reinstall the previous package version

Answers: a package upgrade that broke something. Restores: the binaries and files the package owns. Cost: minutes, and it can fail outright.

Service impact possibleversion-pinned reinstall
# Debian family. apt accepts name=version.
- name: Pin the application package back to the previous release
ansible.builtin.apt:
  name: acme-app=2.4.1-1
  state: present
  allow_downgrade: true

# Red Hat family. dnf accepts an explicit NEVRA.
- name: Pin the application package back to the previous release
ansible.builtin.dnf:
  name: acme-app-2.4.1-1.el9
  state: present
  allow_downgrade: true

Three preconditions have to hold, and the pre-flight play should assert all three rather than discovering them during the window:

  1. The old version is still in a reachable repository. Many internal mirrors keep only the current release. Assert it with a repository query before the window, not after.
  2. The package supports downgrade. Post-install scripts that migrate on-disk state forward frequently have no reverse, so the older binary starts against data it does not understand.
  3. Dependency resolution does not take other packages with it. This is the explicit warning in both module documents: the task “could end up with a set of packages that does not match the complete list of specified packages to install”.

When it is the right choice: the package is self-contained, the old version is definitely available, and you have downgraded it successfully at least once somewhere else.

Pattern 3 — redeploy the previous artefact

Answers: a bad application release. Restores: the running code. Cost: seconds, and it is the most reliable pattern in this list.

This is why release-directory-plus-symlink layouts exist. The old release is still on disk; rolling back is changing which one current points at.

Service impact possibleartefact rollback by symlink
- name: Point current at the previous release
ansible.builtin.file:
  src: "/opt/app/releases/{{ rollback_to_release }}"
  dest: /opt/app/current
  state: link
  owner: app
  group: app
notify: restart app

- name: Confirm the application answers after the swap
ansible.builtin.uri:
  url: "http://{{ ansible_host }}:8080/healthz"
  status_code: 200
  timeout: 10
retries: 6
delay: 5
until: rollback_health is succeeded
register: rollback_health

It is reliable because nothing has to be reconstructed. The previous release is bytes on a disk that were serving traffic recently. Contrast pattern 2, where the previous version has to be fetched, resolved and installed by a package manager that may decide otherwise.

Its limits are the same as every code rollback: it does not reverse anything the new release wrote. Rows, files, queue messages, calls to other systems.

Pattern 4 — restore from a snapshot

Answers: a change whose blast radius is the whole machine. Restores: everything on the volume, as of the snapshot. Cost: minutes to revert, plus everything written since.

Available where the platform supports it: a hypervisor snapshot, a cloud volume snapshot, an LVM snapshot taken with lvcreate --snapshot, a ZFS snapshot. Not available on bare metal without volume management, which is the usual reason a plan cannot use it.

When it is the right choice: an irreversible in-place operation on a machine whose durable state lives somewhere else, where the alternative is rebuilding the host.

Pattern 5 — revert the Git commit and re-apply

Answers: a change to declared state, where the declaration is the whole story. Restores: everything the role declares. Cost: one playbook run.

This is the pattern everyone reaches for first, and it is genuinely the right answer for a real class of change: the role manages the complete content of what it manages, the change altered only that content, and nothing outside the role was touched.

Configuration changerevert and re-apply, scoped
# Confirm what the revert would change, without changing it.
ansible-playbook -i inventories/prod site.yml \
--limit 'appservers:&batch_a' --check --diff

# Apply.
ansible-playbook -i inventories/prod site.yml \
--limit 'appservers:&batch_a'

The previous lesson covered why it is insufficient on its own. The short version, restated because it is the failure this catalogue exists to prevent: the old revision makes no statement about state the new revision created, so users, packages, directories, cron entries and migrations added by the change survive the revert untouched, and the recap is green anyway.

When it is the right choice: the diff between the two revisions consists entirely of changed values in files the role already managed, with no additions. That is checkable — read the diff and look for anything that creates something.

Special case — the kernel

Worth its own note because the pattern is different from all five.

The previous kernel is still installed. Rolling back is a boot-entry selection, not a package operation. grub-reboot sets the default boot entry “for the next boot only”, which is exactly the right semantic for a rollback: if the previous kernel also fails to boot, the next reboot returns to the default rather than pinning you into a broken choice.

Service impact possiblekernel rollback is a boot selection
- name: Select the previous kernel for the next boot only
ansible.builtin.command:
  argv:
    - grub-reboot
    - "{{ previous_boot_entry }}"
changed_when: true

- name: Reboot into it
ansible.builtin.reboot:
  reboot_timeout: 600

- name: Confirm the running kernel is the one intended
ansible.builtin.assert:
  that:
    - ansible_facts['kernel'] is version(bad_kernel_version, '<')
  fail_msg: "Host booted {{ ansible_facts['kernel'] }}, not the previous kernel"

Note the assert. Without it the play reports success for a host that rebooted straight back into the kernel you were rolling back from, because the boot entry string did not match anything.

The changes with no pattern

An honest list. These are not solved by anything above.

  • Deleted data. Restore from backup, or it is gone.
  • A forward-only schema migration. A down-migration if someone wrote one and it has been tested; otherwise a database restore.
  • A rotated secret at the provider. The old value no longer exists. Rotating “back” means rotating forward to a third value, and every consumer has to be updated again.
  • An email, a webhook, a payment, a DNS propagation. Sent is sent.
  • A reformatted filesystem, a repartitioned disk, a destroyed volume.
  • A decommissioned host whose lease was released.

Knowledge check

Knowledge check · 4 questions

  1. Q1. A rollback restores /etc/systemd/system/app.service from its backup and then restarts the service with ansible.builtin.systemd_service. Both tasks report changed. The reverted behaviour is still present. Why?

  2. Q2. A configuration change caused a stateful service to write malformed records for eleven minutes. Restoring the previous config file from its backup and reloading the service accomplishes which of these? Select all that apply.

  3. Q3. Reverting to a pre-change snapshot is the safest rollback pattern available, because it restores the entire machine rather than only the part the change touched.

  4. Q4. When is revert the commit and re-run genuinely a sufficient rollback on its own?

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