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

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: copy is a photocopier: same page, every time. template is 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, pick ansible-template-defaults, and fix the real broken project - Ansible really does SSH into four managed hosts and converge them.