Skip to main content
RunBook Academy

AnsibleXVIII · Files and Configuration ManagementFiles and configuration management

Knowing what is there before you change it

Intermediate⏱ ~18 minansibleansible-playbook

What you'll learn

  • Compare a fleet of files by checksum without transferring any of them
  • Choose between stat, slurp and fetch for a given evidence-gathering question
  • Guard a change on the current contents of the file being changed
  • Avoid the memory and secret-exposure costs of reading files back
  • Collect a divergent config from one host without altering it

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.

Every change in this part has assumed the file on the host is what you believe it is. That assumption is where the surprises live: a host provisioned before the role existed, a config someone edited during an incident, a package upgrade that dropped a new default.

Three read-only modules answer three different questions about a remote file. They are the first move in any diagnosis and the correct precondition for any risky change.

ModuleQuestion it answersWhat crosses the wire
statDoes it exist, what is it, and is it the same as elsewhere?Metadata and a checksum
slurpWhat does it say, right now, inside this play?The contents, base64 encoded, into memory
fetchCan I take a copy back and keep it?The contents, to a file tree on the controller

stat: the cheapest useful question

stat returns metadata plus, by default, a SHA-1 checksum. That checksum is the load-bearing part: it lets you compare a file across a fleet without transferring any of them.

Read-only / Safeare these forty files the same file?
ansible -i inventories/prod webservers -m ansible.builtin.stat \
-a "path=/etc/nginx/nginx.conf" \
| grep -E 'SUCCESS|"checksum"'
Read-only / Safeone host out of five
$ ansible -i inventories/prod webservers -m ansible.builtin.stat -a 'path=/etc/nginx/nginx.conf'
web01.example.com | SUCCESS => "checksum": "8f14e45fceea167a5a36dedd4bea2543b7ac2e29"
web02.example.com | SUCCESS => "checksum": "8f14e45fceea167a5a36dedd4bea2543b7ac2e29"
web03.example.com | SUCCESS => "checksum": "c4ca4238a0b923820dcc509a6f75849bfa27b0e2"
web04.example.com | SUCCESS => "checksum": "8f14e45fceea167a5a36dedd4bea2543b7ac2e29"
web05.example.com | SUCCESS => "checksum": "8f14e45fceea167a5a36dedd4bea2543b7ac2e29"

Illustrative output

That is drift detection in one command, and it is the honest version of “is the fleet consistent?” — it compares the hosts to each other, which is a different question from comparing them to the repository.

The options worth knowing:

  • get_checksum (default true) — set false for large files when you only want metadata. Hashing a 4 GiB file on every host is a real cost.
  • checksum_algorithm (default sha1, aliases checksum, checksum_algo) — choices are md5, sha1, sha224, sha256, sha384, sha512. The documentation notes md5 can be unavailable on a FIPS-140 compliant host, which is a genuine cause of a task that works on most of the fleet and fails on the hardened subset.
  • follow (default false) — leave it off when you are asking what the path is, turn it on when you are asking about the target.
  • get_mime and get_attributes (both default true) — these shell out to file and lsattr on the managed node. On a minimal image where neither exists, the fields are simply absent.

slurp: reading the contents into the play

slurp returns the file’s contents base64-encoded, in the task result. It exists so a play can make a decision based on what a file currently says.

Read-only / Safereading a value out of a remote file
- name: Read the current cluster identifier
ansible.builtin.slurp:
  src: /etc/app/cluster-id
register: cluster_id_raw

- name: Fail if this host belongs to a different cluster
ansible.builtin.assert:
  that:
    - (cluster_id_raw.content | b64decode | trim) == expected_cluster_id
  fail_msg: >-
    {{ inventory_hostname }} reports cluster
    {{ cluster_id_raw.content | b64decode | trim }}, expected
    {{ expected_cluster_id }}

Two costs, both documented, both easy to trip over on a fleet.

Memory. The module note is specific:

This module returns an ‘in memory’ base64 encoded version of the file, take into account that this will require at least twice the RAM as the original file size.

Twice the file size on the managed node, plus the decoded copy on the controller, held for every host in the play simultaneously. Forty hosts times a 200 MiB log is not a slurp, it is a controller outage.

Secrets. The contents land in a registered variable. Any subsequent debug of that variable, any -v on a task that consumes it, and any callback plugin that logs task results puts the file’s contents in the run log. If the file holds credentials, no_log: true on both the slurp task and every task that uses the result is not optional.

fetch: taking a copy back

fetch is copy in reverse: it retrieves a file from managed nodes to a directory tree on the controller, organised by inventory hostname.

Read-only / Safecollecting the divergent file for comparison
ansible -i inventories/prod webservers -m ansible.builtin.fetch \
-a "src=/etc/nginx/nginx.conf dest=/tmp/audit-2026-08-11 flat=false"

With flat: false (the default), src=/etc/nginx/nginx.conf on web03.example.com lands at /tmp/audit-2026-08-11/web03.example.com/etc/nginx/nginx.conf. That layout is the point: diff -r across the tree compares the fleet directly, and the per-host directory means nothing overwrites anything.

flat: true strips the hostname and path. It is useful for a single host or for genuinely per-host filenames, and it is a footgun across a group — the documentation says so: “If using multiple hosts with the same filename, the file will be overwritten for each host.” Forty hosts, one surviving file, no error.

Other options that matter:

  • fail_on_missing (default true since 2.5) — a host without the file fails the task. When you are surveying and absence is a finding rather than an error, set it false and treat the missing file as data.
  • validate_checksum (default true) — verifies the copy matches the source after transfer.

Choosing between them

The decision is short:

  • Is it there, and does it match? stat. Cheapest, and the only one of the three you should run against a whole fleet by reflex.
  • Does this play need to read a value out of it? slurp, with no_log if the contents are sensitive, and never on a large file.
  • Do I need the file on the controller afterwards? fetch, with flat: false when more than one host is involved.

And one thing none of them do: none of them tell you what the program reading the file thinks it says. A syntactically valid file in the wrong section, a directive overridden later, an include you did not know about — stat and slurp see text. sshd -T, nginx -T, chronyc sources, sysctl -n see behaviour. When the question is “is the setting in effect”, ask the program.

Knowledge check

Knowledge check · 4 questions

  1. Q1. You want to know whether /etc/nginx/nginx.conf is identical across 200 hosts, as cheaply as possible. What do you run?

  2. Q2. Which are documented costs or hazards of the slurp module? Select all that apply.

  3. Q3. A stat task against a missing path fails, so a later condition on stat.mode is safe to write without an existence check.

  4. Q4. A play must collect /etc/app/app.conf from 12 hosts for comparison. Which combination is correct?

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