AnsibleXVII · Templates and Jinja2Templates and Jinja2
Template a drop-in, not the whole file
What you'll learn
- Decide whether Ansible should own a whole file or a fragment of one
- Template a drop-in and know which service layouts support one
- Explain why partial ownership of a single file is worse than either alternative
- Answer what did Ansible change on this host from the filesystem alone
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
The default assumption in this part has been that Ansible owns the file:
the template is the source of truth, anything on the host is replaced,
and drift is corrected on the next run. That is the right model and it
is what makes changed=0 meaningful.
Sometimes you cannot have it. The file is shipped by a package that rewrites it on upgrade. Another team edits it. It contains a section generated by a different tool. Ownership is not available, and the question becomes what to do instead.
Three ways to own less than a file, ranked
1. Template a drop-in. Ansible owns a whole file in a directory the service includes. Full ownership, smaller scope. This is the right answer whenever the service supports it.
2. Manage a marked block with blockinfile. Ansible owns a
delimited region of somebody else’s file. Workable, and it is the second
choice for a reason.
3. Manage individual lines with lineinfile. Ansible owns a regex
match. This is the option that produces the surprises, and the files
part covers when it is genuinely appropriate.
The gap between the first and the rest is larger than the gap between
the second and third. A drop-in gives you back everything: full-file
replacement, atomic writes, validate:, a clean diff, and the ability
to answer “what does Ansible manage here” by listing a directory.
Which services support drop-ins
More than people expect. The convention is close to universal on modern Linux:
| Service | Drop-in location | Notes |
|---|---|---|
| systemd units | /etc/systemd/system/<unit>.service.d/*.conf | Overrides the packaged unit; needs daemon_reload |
| nginx | /etc/nginx/conf.d/*.conf | Inside the http context |
| sudo | /etc/sudoers.d/* | visudo -cf validates a fragment properly |
| sshd | /etc/ssh/sshd_config.d/*.conf | Where the distribution’s sshd_config has the Include |
| systemd-networkd, logind, journald | <name>.conf.d/*.conf | Same convention throughout |
| APT | /etc/apt/apt.conf.d/* | |
| sysctl | /etc/sysctl.d/*.conf | |
| logrotate | /etc/logrotate.d/* | |
| PAM, cron, udev, modprobe | various .d directories |
The example
- name: Deploy the reverse proxy site definition
ansible.builtin.template:
src: site.conf.j2
dest: /etc/nginx/conf.d/10-{{ app_name }}.conf
owner: root
group: root
mode: '0644'
validate: nginx -t -c %s
notify: Reload nginxNote the numeric prefix. Include globs are read in sorted order, and the
order matters for anything where later directives override earlier ones.
10-, 20-, 50- makes the ordering explicit and leaves room to
insert; a bare {{ app_name }}.conf makes it alphabetical by
application name, which is an ordering nobody chose.
The validate on that task hits the fragment problem from the previous
lesson — nginx -t -c will reject a bare server block. For this
specific case the workaround is a wrapper: a minimal staging
configuration that wraps the fragment in an http block. Whether that
is worth building is a judgement call, and if it is not, the honest
answer is to leave validate off this task and rely on the canary
instead of pretending the check is there.
What a drop-in actually buys
A smaller diff. The change under review is the fragment, not a 400-line file where three lines differ. Reviewers read small diffs properly and skim large ones.
A smaller blast radius. A mistake in a drop-in breaks the feature
that drop-in configures. A mistake in a whole templated nginx.conf
breaks every site on the host, including the ones that had nothing to do
with the change.
A clean answer to provenance. ls /etc/nginx/conf.d/ tells an
operator on the box exactly which files are managed. With
blockinfile and lineinfile, that question requires reading files and
recognising marker comments.
Package upgrades stop being a conflict. The package owns its file
and can rewrite it freely; your drop-in is untouched. With a
lineinfile edit, an upgrade can silently revert your change — and
Ansible will silently re-apply it on the next run, so the setting
oscillates and nobody notices until the window between the two matters.
Removal is state: absent on a file. Undoing a lineinfile means
knowing what the line was before, which nothing records.
When a whole file is still right
Drop-ins are not automatically better. Take the whole file when:
- The service has no include mechanism. Many application configs do not, and forcing one is not your job.
- Ansible genuinely is the source of truth. If nothing else writes the file and no package rewrites it, owning it outright gives the strongest guarantee available and the simplest mental model.
- The configuration only makes sense as a whole. A file whose directives interact heavily is easier to reason about in one template than split across fragments whose combined effect nobody can see.
validate:needs the complete file. Owning the whole thing makes the parameter work, and giving that up for a smaller diff is usually a bad trade.
$ ls -la /etc/nginx/conf.d/ && head -3 /etc/nginx/conf.d/10-billing.conftotal 20
drwxr-xr-x 2 root root 4096 Aug 11 09:14 .
drwxr-xr-x 6 root root 4096 Aug 11 09:14 ..
-rw-r--r-- 1 root root 1204 Aug 11 09:14 10-billing.conf
-rw-r--r-- 1 root root 318 Jul 02 16:41 default.conf
# Ansible managed - roles/webserver/templates/site.conf.j2
# Do not edit on the host; changes are overwritten on the next run.
# Source of truth: git.example.com/infra/ansibleThe header is the subject of the next lesson. It is what turns a directory listing into an answer.
Knowledge check
Knowledge check · 4 questions
Q1. A package ships /etc/app/app.conf and rewrites it on every upgrade. The service supports /etc/app/conf.d/*.conf. What is the best model?
Q2. A role deploys /etc/ssh/sshd_config.d/50-hardening.conf and the run is green, but sshd -T shows the old values. What is the most likely cause?
Q3. What does templating a drop-in give you that lineinfile on a shared file does not? Select all that apply.
Q4. A systemd drop-in that sets ExecStart replaces the ExecStart from the packaged unit.
Passing score: 75%. Answers are checked in this browser.