Skip to main content
RunBook Academy

AnsibleXVIII · Files and Configuration ManagementFiles and configuration management

backup: and atomic replacement

Intermediate⏱ ~18 minansible-playbookansible-doc

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

Not yet marked complete on this device.

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.

Configuration changecapturing the backup path
- 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 defined

backup_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:

Read-only / Safethe attribute, from the module documentation
$ 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: full

The 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.

Configuration changeunsafe_writes where it is actually required
# /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: true

Do 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.

Configuration changevalidate on the files that gate your access
# 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 sshd

A 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

  1. Q1. A change request says "rollback: backup: true is enabled on the template task". What is the strongest objection?

  2. Q2. After an atomic replacement, a daemon that already has the file open keeps reading the old contents until it reopens the path.

  3. Q3. Which control would have prevented an incident where a rendered sshd_config locked the automation account out of forty hosts?

  4. 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.