AnsibleXLVIII · Maintenance Windows and RollbackMaintenance windows and rollback
A catalogue of rollback patterns, and what each is for
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
“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.
| Change | Reversibility | Pattern | Typical cost |
|---|---|---|---|
| Config file content | Full | Restore previous file | Seconds |
| Service unit content | Full | Revert unit, daemon_reload | Seconds |
| Application artefact | Full | Redeploy previous release | Seconds to minutes |
| Package version | Partial | Version-pinned reinstall | Minutes, may fail |
| Kernel | Partial | Boot the previous entry | A reboot |
| Whole machine state | Full, coarse | Snapshot revert | Minutes, loses newer data |
| Database schema | None, usually | Down-migration if written, else restore | Tens of minutes, data loss |
| Deleted data | None | Restore from backup | Hours, data loss |
| Provider-side secret rotation | None | Rotate forward again | Varies |
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”.
- 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 definedWhen 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.
# 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: trueThree preconditions have to hold, and the pre-flight play should assert all three rather than discovering them during the window:
- 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.
- 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.
- 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.
- 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_healthIt 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.
# 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.
- 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
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?
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.
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.
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.