AnsibleXVIII · Files and Configuration ManagementFiles and configuration management
Which file module, and why
What you'll learn
- Choose between copy, template, file, lineinfile, blockinfile and assemble from the ownership of the file
- State the two documented constraints on copy that push work towards template
- Explain why a file managed by many line edits has no declared state
- Identify the cases where a single line edit is the correct choice
- Predict which module a reviewer can audit without logging into a host
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
Six modules in ansible.builtin put content into a file on a managed
node: copy, template, file, lineinfile, blockinfile and
assemble. They are not six styles of doing the same thing. They differ
in one property that matters more than any of their options:
how much of the file’s contents the automation declares.
template declares all of it. lineinfile declares one line and says
nothing about the other four hundred. That difference decides whether a
reviewer reading your repository can answer “what is in /etc/chrony.conf
on web03?” without logging into web03.
The decision, stated first
| Module | Declares | Choose it when |
|---|---|---|
template | The entire file | You own the file and any part of it varies by host, group or environment |
copy | The entire file | You own the file and it is byte-identical everywhere |
blockinfile | A marked region | You own part of a file that something else also writes |
lineinfile | One line | You need one setting changed in a file owned by a vendor or a package |
assemble | The whole file, from fragments | The program has no conf.d directory and several roles must each contribute a stanza |
file | Metadata only | The thing you are managing is a directory, a symlink, permissions, or an absence |
Read that table as a preference order from the top. Each row down gives away a little more of your ability to say what the file contains.
Why “declared state” is the axis
Consider a /etc/ssh/sshd_config managed by eleven lineinfile tasks.
Every one of them is idempotent. Every one has a regexp, a line, and
a passing test. The play runs green.
Now answer this: is X11Forwarding disabled on your fleet?
You cannot tell from the repository. Eleven tasks say what eleven settings should be; the file has roughly ninety directives and a distribution default for each one you did not name. Nothing in your automation records the other seventy-nine. If a package upgrade changes a default, the run stays green and the behaviour changes.
Now the same file as a template. The answer is: open
templates/sshd_config.j2 and read it. Every directive that will exist
on the host is in that file, in review, in Git history, with a blame
line naming who changed it and when.
copy versus template
copy transfers a file unchanged. template renders it through Jinja2
on the controller first, then transfers the result. Two documented
constraints push more work towards template than people expect.
The first is about variables. The copy module’s content option
sets a file’s contents from a string, and it is tempting to build that
string from a variable. Upstream is explicit:
If you need variable interpolation in copied files, use the
ansible.builtin.templatemodule. Using a variable with thecontentparameter produces unpredictable results.
The reason is that content is typed str and receives whatever the
templating layer produced — a multi-line string, a rendered data
structure, or a type that stringifies differently than you expect. There
is no rendering step you control. template gives you one.
The second is about scale. copy will recurse into a directory, and
the module’s own notes cap that:
The
ansible.builtin.copymodule recursively copy facility does not scale to lots (>hundreds) of files.
The mechanism is in the action plugin: a recursive copy enumerates the
tree on the controller and issues per-file operations. For a handful of
files it is fine. For a release artefact of ten thousand files it is a
slow, chatty run whose failure mode is a task that appears to hang.
Ship an archive and use ansible.builtin.unarchive, or use
ansible.posix.synchronize, and say in a comment why.
# Wrong: content built from a variable. Documented as unpredictable.
- name: Write the chrony config
ansible.builtin.copy:
dest: /etc/chrony/chrony.conf
content: "{{ chrony_config_body }}"
mode: '0644'
# Right, but only if the file is byte-identical on every host.
- name: Install the shared chrony config
ansible.builtin.copy:
src: chrony.conf
dest: /etc/chrony/chrony.conf
owner: root
group: root
mode: '0644'
# Right when anything varies - the servers, the region, the drift file.
- name: Render the chrony config
ansible.builtin.template:
src: chrony.conf.j2
dest: /etc/chrony/chrony.conf
owner: root
group: root
mode: '0644'
validate: /usr/sbin/chronyd -f %s -pWhen a single line is the right answer
The course’s preference for whole-file management is not a prohibition.
There is a shape of problem where lineinfile is the correct and
maintainable choice, and it has a clear test:
You do not own the file.
The package owns it. The vendor ships it. An upgrade will replace it, and you want that upgrade to bring its new defaults. You need exactly one setting different from the shipped value.
Examples that pass the test:
/etc/default/grubon a Debian-family host — package-managed, replaced on upgrade, and you want one kernel parameter.- A vendor agent’s
/opt/vendor/agent.confwhere the vendor’s support policy is void if you replace the file. /etc/sysctl.confon a system where asysctl.ddrop-in is not honoured by the boot sequence you are stuck with.
Examples that fail it, and are the ones you meet in practice:
sshd_config,nginx.conf,chrony.conf,rsyslog.conf,sudoers— you own the policy in every one of those, and every one is routinely mismanaged by a pile of line edits.
assemble, and the case for it
assemble concatenates a directory of fragments into one destination
file. Upstream describes the problem it solves precisely:
Often a particular program will take a single configuration file and does not support a
conf.dstyle structure where it is easy to build up the configuration from multiple sources.
Files are joined in string sorting order, which is why fragments are
conventionally named 00-header, 10-auth, 90-footer. Its useful
options are regexp to filter which fragments participate,
ignore_hidden (default false, so editor backups do get included
unless you set it), delimiter, and validate.
- name: Build sudoers from role fragments
ansible.builtin.assemble:
src: /etc/sudoers.d.fragments
dest: /etc/sudoers.assembled
remote_src: true
ignore_hidden: true
regexp: '^[0-9]{2}-'
owner: root
group: root
mode: '0440'
validate: /usr/sbin/visudo -cf %sIts cost is that the destination file’s contents are declared by the set of fragments present on the host, which is one indirection further from the repository than a template. Use it when several independent roles genuinely must contribute, and the program refuses a drop-in directory. If one role owns the file, template it.
Reading the choice back out of a repository
The practical test for a reviewer is a grep, not an opinion.
grep -rhA5 'ansible.builtin.lineinfile\|ansible.builtin.blockinfile' roles/ \
| grep -oP '^\s+(path|dest):\s*\K\S+' \
| sort | uniq -c | sort -rn$ grep -rhA5 'lineinfile' roles/ | grep -oP '(path|dest):\\s*\\K\\S+' | sort | uniq -c | sort -rn 11 /etc/ssh/sshd_config
6 /etc/security/limits.conf
4 /etc/sysctl.conf
1 /etc/default/grubIllustrative output
The bottom line is fine: one setting, one vendor file. The top line is
the finding. Eleven tasks against sshd_config is not eleven small
problems, it is one file whose contents nobody has written down.
Knowledge check
Knowledge check · 4 questions
Q1. A role manages /etc/nginx/nginx.conf with nine lineinfile tasks. Every task is idempotent and the play reports ok on a second run. What is the defect?
Q2. Which situations make lineinfile the right choice rather than a compromise? Select all that apply.
Q3. Building a file from a variable with the copy module content parameter is equivalent to templating it, and is the lighter-weight option.
Q4. A release drops 8,000 files that must land in /opt/app on 40 hosts. What does the copy module documentation say about this?
Passing score: 75%. Answers are checked in this browser.