From copy to template
Scenario: There are four nearly identical nginx config files in the repository, one per environment. A security header needs adding, and it lands in three of them.
New words, in plain English
- Jinja2 - the templating language. Same
{{ }}you already use, plus{% %}for logic. .j2- the conventional extension for a Jinja2 template file.templatemodule - renders a.j2file with the current host's variables and writes the result to the target.- Render - to turn a template plus a set of values into a finished file.
copy moves bytes. template renders a file through Jinja2 first, substituting variables for the host being configured, and only then writes it. That one difference collapses four environment-specific files into one template plus four sets of variables.
The substitution is per host, which is the important part: the same template produces a different file on web01 and web02 because their variables differ. Anything available in the play - variables, facts, magic variables like inventory_hostname - is available inside the template.
Inside a role, src: resolves relative to roles/<role>/templates/, so src: nginx.conf.j2 needs no path at all. Outside a role, it resolves relative to a templates/ directory next to the playbook, then to the playbook directory itself.
A convention worth adopting: put a generated-file banner at the top of every template. When someone SSHes in at 3 AM and edits the file by hand, that line tells them their change will be erased on the next run - and tells them where the real source lives.
Analogy:copyis a photocopier: same page, every time.templateis a mail merge: one letter, personalised for each recipient from a list of details.
A worked example
# templates/app.conf.j2
# ANSIBLE MANAGED - edits here are overwritten. Source: roles/webapp/templates/
[server]
name = {{ inventory_hostname }}
port = {{ webapp_port }}
workers = {{ webapp_workers }}
env = {{ app_env }}
# The task
- name: Render the application config
ansible.builtin.template:
src: app.conf.j2
dest: /etc/webapp/app.conf
mode: "0644"
backup: true # keep a timestamped copy of the previous version
notify: reload webapp
template is fully idempotent: it renders into a temporary file, compares the result with what is already on the target, and only replaces it if they differ. That means it works correctly with --check and --diff - --diff shows you the exact line-by-line change before you make it, which is the single most useful thing you can put in a change ticket.
backup: true keeps a timestamped copy of the previous version on the target. It costs almost nothing and has saved a great many people during a bad rollout.
Tip: Always start a template with a managed-file banner naming the source path. It converts a mysterious overwritten change into an obvious one.
Goal: Put this to work in the ansible-template-defaults lab. Open/labs/ansible, pickansible-template-defaults, and fix the real broken project - Ansible really does SSH into four managed hosts and converge them.