AnsibleXVIII · Files and Configuration ManagementFiles and configuration management
backup: and atomic replacement
What you'll learn
- Locate the backup file a module created and read its return value
- Distinguish a backup file existing from a tested rollback
- Explain what atomic replacement guarantees for a daemon reading the file
- State what unsafe_writes trades away and when it is genuinely required
- Design a rollback that does not depend on files scattered across the fleet
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
backup: true is available on every file-writing module in this part.
It is described identically in each:
Create a backup file including the timestamp information so you can get the original file back if you somehow clobbered it incorrectly.
Read that carefully. It says you can get the original file back. It does not say the automation can, that anything knows the backup exists, or that restoring it will bring the service back. Those are three separate problems and this option solves none of them.
What it actually produces
The backup lands beside the destination, on the managed node, with a timestamp and PID appended:
/etc/chrony/chrony.conf
/etc/chrony/chrony.conf.31456.2026-08-11@14:22:07~
The module returns the path in backup_file, which is the only reliable
way to know it. Register the result and you have the path; do not, and
the only record is a directory listing on one host.
- name: Render the chrony configuration
ansible.builtin.template:
src: chrony.conf.j2
dest: /etc/chrony/chrony.conf
owner: root
group: root
mode: '0644'
backup: true
validate: /usr/sbin/chronyd -f %s -p
register: chrony_config
notify: Restart chrony
- name: Record where the previous configuration went
ansible.builtin.debug:
msg: "Previous config saved as {{ chrony_config.backup_file }}"
when: chrony_config.backup_file is definedbackup_file is present only when the module actually changed the file.
An unchanged run returns nothing, which is correct and is why the
when guard is needed.
Why a backup file is not a rollback
Four gaps, each of which has ended a real incident badly.
One: nothing collects them. The backups are on the hosts. Restoring across forty hosts means a play that finds the right file per host, which means knowing the timestamp, which differs per host because runs are not simultaneous. If the reason you are rolling back is that the hosts are unreachable, the backups are unreachable too.
Two: they accumulate silently. Every changed run leaves another
copy. A task that flaps changed — the lineinfile growth case in this
part, a template with a timestamp in it — fills the directory. Nothing
prunes them. On a small root filesystem this becomes an outage of its
own, and the diagnosis (“disk full in /etc”) is genuinely confusing
the first time.
Three: they contain what the file contained. A backup of a
credentials file is a credentials file, with the mode the original had,
sitting next to it forever. Every secret rotation leaves the previous
secret on disk. no_log: true on the task does not change this.
Four: restoring the file is not restoring the service. The configuration is one part of the change. The package version, the running process, the state written since — none of those come back with the file. A daemon that has already reloaded the new config and rewritten its state directory does not return to the previous state because you put the old text back.
Atomic replacement, and why a daemon cares
Every module in this part carries the safe_file_operations attribute
with support: full:
$ ansible-doc ansible.builtin.template | grep -A3 'safe_file_operations:' safe_file_operations:
description: Uses Ansible's strict file operation functions to ensure proper permissions
and avoid data corruption
support: fullThe guarantee is that a process reading the destination path never sees a partially written file. It sees the old contents, or it sees the new contents, and there is no instant in between.
unsafe_writes, and what it gives up
The escape hatch exists because atomic replacement is not always possible:
By default this module uses atomic operations to prevent data corruption or inconsistent reads from the target filesystem objects, but sometimes systems are configured or just broken in ways that prevent this. One example is docker mounted filesystem objects, which cannot be updated atomically from inside the container and can only be written in an unsafe manner.
IMPORTANT! Unsafe writes are subject to race conditions and can lead to data corruption.
The concrete case is a single file bind-mounted into a container. The
mount is the file itself, so rename over it would replace the mount
point rather than the contents, and the kernel refuses. unsafe_writes: true makes the module truncate and rewrite in place instead.
That is a genuine trade, stated plainly: the reader can now see a half-written file.
# /etc/app/app.conf inside this container is a single-file bind mount.
# rename(2) over a mount point fails with EBUSY, so the atomic path is
# unavailable and the module must truncate in place. The application
# re-reads on SIGHUP only, so the window is not observed in practice.
- name: Update the in-container application config
ansible.builtin.template:
src: app.conf.j2
dest: /etc/app/app.conf
owner: root
group: root
mode: '0644'
unsafe_writes: trueDo not set it globally, and do not set it to make a permissions error go
away. If a module reports that it cannot write atomically, the usual
cause is that remote_tmp and the destination are on different
filesystems, or that the destination directory is not writable by the
identity doing the write. Both of those are fixable; suppressing the
symptom is not a fix.
Validation is the control that actually prevents the rollback
The strongest thing in this part is not backup, it is validate. It
runs a command against the temporary file, before the rename, and
refuses to install the file if the command fails.
# sudoers: a malformed file locks out every account that needs sudo.
- name: Deploy the automation sudo rule
ansible.builtin.template:
src: automation-sudoers.j2
dest: /etc/sudoers.d/50-automation
owner: root
group: root
mode: '0440'
validate: /usr/sbin/visudo -cf %s
# sshd_config: a malformed file locks out the connection Ansible uses.
- name: Deploy the SSH server configuration
ansible.builtin.template:
src: sshd_config.j2
dest: /etc/ssh/sshd_config
owner: root
group: root
mode: '0600'
validate: /usr/sbin/sshd -t -f %s
notify: Reload sshdA failed validate fails the task, on that host, before anything
changed. In a play with serial, that stops the rollout at the first
batch — which is the outcome you wanted, achieved before the damage
rather than after it. The backup file would have let you undo the
damage; validate means there is nothing to undo.
Knowledge check
Knowledge check · 4 questions
Q1. A change request says "rollback: backup: true is enabled on the template task". What is the strongest objection?
Q2. After an atomic replacement, a daemon that already has the file open keeps reading the old contents until it reopens the path.
Q3. Which control would have prevented an incident where a rendered sshd_config locked the automation account out of forty hosts?
Q4. Which are real costs of leaving backup: true on every file-writing task in a large estate? Select all that apply.
Passing score: 75%. Answers are checked in this browser.