AnsibleXVIII · Files and Configuration ManagementFiles and configuration management
Knowing what is there before you change it
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
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.
| Module | Question it answers | What crosses the wire |
|---|---|---|
stat | Does it exist, what is it, and is it the same as elsewhere? | Metadata and a checksum |
slurp | What does it say, right now, inside this play? | The contents, base64 encoded, into memory |
fetch | Can 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.
ansible -i inventories/prod webservers -m ansible.builtin.stat \
-a "path=/etc/nginx/nginx.conf" \
| grep -E 'SUCCESS|"checksum"'$ 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(defaulttrue) — setfalsefor large files when you only want metadata. Hashing a 4 GiB file on every host is a real cost.checksum_algorithm(defaultsha1, aliaseschecksum,checksum_algo) — choices aremd5,sha1,sha224,sha256,sha384,sha512. The documentation notesmd5can 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(defaultfalse) — leave it off when you are asking what the path is, turn it on when you are asking about the target.get_mimeandget_attributes(both defaulttrue) — these shell out tofileandlsattron 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.
- 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.
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(defaulttruesince 2.5) — a host without the file fails the task. When you are surveying and absence is a finding rather than an error, set itfalseand treat the missing file as data.validate_checksum(defaulttrue) — 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, withno_logif the contents are sensitive, and never on a large file. - Do I need the file on the controller afterwards?
fetch, withflat: falsewhen 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
Q1. You want to know whether /etc/nginx/nginx.conf is identical across 200 hosts, as cheaply as possible. What do you run?
Q2. Which are documented costs or hazards of the slurp module? Select all that apply.
Q3. A stat task against a missing path fails, so a later condition on stat.mode is safe to write without an existence check.
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.