Chapter 6: Jinja2 Templating
Topic 34

The template Module

TemplatingModule

The template module is how Ansible turns a .j2 file full of variables into a real config file on the managed node. You give it src — the template on the control node — and dest, where the rendered file goes, and it renders, copies, and reports changed only when the result differs from what is already there. For Larkspur, this is the task that writes /etc/nginx/sites-available/larkspur.conf with the right server_name per environment and /etc/larkspur/app.env with the database host filled in.

The rule for choosing it is simple: reach for template over copy the instant a file contains anything that varies between hosts or environments. A file with even one {{ }} is a template, not a static asset, and shipping it with copy forces you to maintain a near-duplicate per host — exactly the snowflake problem templating exists to kill.

What a template task does, control node to managed node
.j2 + vars/facts
render on control node
validate (nginx -t)
write file to the node

src and dest

src: larkspur.conf.j2 resolves automatically from the role's templates/ directory, so you name the file and Ansible finds it. dest: /etc/nginx/sites-available/larkspur.conf is the absolute path on the target where the rendered output lands. In the same task you set owner, group, and mode on the result, so the file arrives with the right permissions instead of needing a follow-up file task to fix them.

Rendering the Larkspur nginx vhost from a template
- name: Render the Larkspur nginx vhost
  ansible.builtin.template:
    src: larkspur.conf.j2          # resolved from the role's templates/
    dest: /etc/nginx/sites-available/larkspur.conf
    owner: root
    group: root
    mode: "0644"             # quoted string, not a bare number
    validate: "nginx -t -c %s"  # check the rendered file before it goes live
    backup: true
  notify: reload nginx

Why template Beats copy

copy ships a static file byte-for-byte; template renders variables first. That difference is the whole point. One larkspur.conf.j2 serves web1 and web2 with their own server_name, and staging and prod with their own worker_processes, from a single source. The alternative — a separate file per host kept in sync by hand — rots the moment someone edits one copy and forgets the others.

The tell that a file should be a template is a single {{ }}. The moment a config needs one host-, group-, or environment-specific value, it stops being a static asset and becomes a template. In practice that covers most config files, which is why template is the workhorse and copy is reserved for the genuinely fixed things — a binary, a license file, a config with zero variables.

validate= Before Replacing

validate: 'nginx -t -c %s' runs the config checker against the rendered file before Ansible moves it into place. The %s is the magic part: it expands to the temp path of the new candidate file, not the live one, so nginx tests the file that is about to go in. A broken render fails the task instead of overwriting a working config with one that will not start the service.

The footgun here is hard-coding the path. Writing validate: 'nginx -t -c /etc/nginx/nginx.conf' checks the old file, which is already valid, so validation passes while the broken new file goes in anyway. The %s is what points the checker at the candidate. Every config with a checker has one — visudo -cf %s, sshd -t -f %s — and using it turns a service-breaking typo into a failed task.

backup=yes

backup: true keeps a timestamped copy of the previous file on the target before overwriting it. If a render slips past validation and turns out wrong, you have a known-good file sitting right next to the live one, ready to restore by hand. On a file that gates whether nginx or gunicorn even starts, that is cheap insurance — a few kilobytes against an outage.

It is not a substitute for validate; it is the layer underneath it. Validation stops the bad files it can detect, and the backup catches the ones it cannot — a render that is syntactically valid but semantically wrong, like a correct config pointing at the wrong upstream. Turn both on for any config whose failure takes the service down.

Triggering Handlers

A template task that reports changed is what fires notify: reload nginx. This is where honest changed reporting earns its keep: the rendered config landing identical to what was already there correctly fires nothing, so the service does not churn on every run. Only a real change to the file triggers the reload. Pair every config-file template with a notify: to the matching reload or restart handler, and the service takes effect when the file changes and stays quiet when it does not.

Common Mistakes
  • Using copy for a config that has even one varying value, then maintaining a near-duplicate file per host — the copies drift, which is the snowflake problem templating exists to kill.
  • Templating nginx.conf without validate: 'nginx -t -c %s', so a typo in the .j2 renders a broken config, replaces the live file, and the next reload handler takes the site down.
  • Hard-coding the validate path (nginx -t -c /etc/nginx/nginx.conf) instead of %s — it checks the old file, so validation passes while the broken candidate goes in anyway.
  • Setting mode as a bare number (mode: 644) instead of a quoted string (mode: "0644"), getting the wrong octal and either a world-readable secrets file or one gunicorn cannot read.
  • Rendering a secrets file like app.env with mode: "0644" so the database password is world-readable on the box — secret-bearing templates need 0600 or 0640 and a non-default owner.
Best Practices
  • Add validate= with the config's own checker (nginx -t -c %s, visudo -cf %s, sshd -t -f %s) to any template whose failure would break the service, so a bad render fails the task instead of the daemon.
  • Set owner, group, and a quoted mode in the same template task rather than a follow-up file task, keeping permissions atomic with the content.
  • Turn on backup: true for config files that gate service startup, so a regrettable render leaves a restorable copy on the target.
  • Pair every config-file template with a notify: to the matching reload or restart handler, so a changed file actually takes effect and an unchanged one does not churn the service.
  • Use 0600 or 0640 and a non-default owner on any template that renders secrets, so a password never lands world-readable on the target.
Comparable tools Terraform templatefile() and the local_file resource render then write the same way Chef template and Puppet file with ERB, the agent-world equivalents envsubst the unstructured shell version

Knowledge Check

When should you choose template over copy for a config file?

  • The moment the file contains even one host-, group-, or environment-specific value expressed as a {{ }}
  • Only once the file grows larger than a few kilobytes of content and copying it byte-for-byte becomes too slow over the connection
  • Whenever the file needs specific owner, group, and mode permission settings applied to it on the target
  • Only for files that must trigger a notify: handler to reload or restart a service whenever the file changes

What does the %s in validate: 'nginx -t -c %s' refer to?

  • The temp path of the newly rendered candidate file, so the checker tests the file about to go live rather than the old one
  • The live config path already in place on the managed node, so the checker confirms the currently running file is still valid
  • A placeholder for the service name that nginx should reload once the validation step has finished successfully
  • The control node's untouched copy of the source .j2 template file, checked before it is shipped out

Why must mode be written as a quoted string like "0644" rather than a bare 644?

  • YAML reads a bare number as decimal, so the octal permissions come out wrong — potentially world-readable or unreadable
  • Bare numbers are silently ignored by the module and the file just keeps whatever existing default mode it already had on disk
  • The quotes are purely cosmetic styling and either form, quoted or bare, applies the exact same permissions identically
  • Unquoted modes only parse correctly with the copy module, not with template

A template task renders a file identical to what is already on the target. What happens to its notify: reload nginx?

  • Nothing fires — the task reports no change, so the handler is not triggered and the service does not churn
  • The handler runs anyway, simply because the template task executed at all on this pass regardless of the result
  • The handler is queued up but deferred to fire on the next full playbook run against the host instead of this one
  • The task errors out because there is no change for it to notify about

You got correct