Templates and Handlers
Scope: generating config files per machine with Jinja2 templates, and restarting things only when a change actually requires it — the pattern at the heart of most real playbooks.
Templates
A template is a config file with placeholders,
rendered per host at run time. This guide's
examples/templates/app.conf.j2:
# Managed by Ansible -- edits will be overwritten
[app]
port = {{ app_port }}
host = {{ inventory_hostname }}
environment = {{ env_name | default('production') }}
Three Jinja2 moves, which cover most real templates:
{{ variable }}— substitution, from any variable or fact.{{ var | default('...') }}— a filter; here, a fallback whenenv_nameisn't set anywhere.- (Not pictured, but next in line:
{% if %}blocks and{% for %}loops — how one template serves machines with genuinely different shapes.)
The template module renders and copies in one step,
comparing the result against what's on the target — so a
template task is idempotent exactly when its rendered
output stops changing.
Why templates rather than one file per machine: the file is mostly the same, and the sameness is the point. When the service's config format changes, you edit one template, and every machine's next run converges — instead of hunting down fourteen hand-maintained variants.
Handlers: the restart pattern
The eternal problem: a service must restart when its config changes — and only then. Restarting on every run breaks idempotency (and, in production, causes pointless blips); never restarting leaves new configs unloaded.
Ansible's answer is the handler: a task that runs only when notified by a task that changed:
- name: Template the app config
ansible.builtin.template:
src: templates/app.conf.j2
dest: "{{ app_dir }}/app.conf"
notify: Stamp config change
handlers:
- name: Stamp config change
ansible.builtin.copy:
content: "config changed at {{ ansible_facts['date_time']['iso8601'] }}\n"
dest: "{{ app_dir }}/.config-stamp"
(The demo handler stamps a file; the real-world version
is service: name=myapp state=restarted.)
Behavior, all verified in the lab:
- Run 1: template changes →
RUNNING HANDLER [Stamp config change]appears after all tasks, once per changed host. - Run 2: nothing changed → the handler section doesn't appear at all. Silence means "no restart needed."
- Ten tasks notify the same handler → it still runs once, at the end of the play. Handlers deduplicate notifications the way you'd want.
Ordering guarantees worth knowing: handlers run at the
end of the play, in the order they're defined (not
notified), and a failed host skips its handlers unless
you force them (--force-handlers) — a machine that
failed mid-play probably shouldn't get its restart.
The pair, working together
Templates + handlers are the load-bearing pattern of the whole tool: describe files, generate them per machine, bounce the service precisely when the description changed. Master these two chapters' worth of YAML and you can read — and write — most playbooks in the wild.