// learn Β· linux Β· rhce Β· 10 min

Ansible templates.

One Jinja2 `.j2` file, a tailored config on every host. The template module renders the file with each machine's own variables and facts before it lands β€” so a single `nginx.conf.j2` becomes a correct, host-specific config everywhere.

Ansible templates animated tutorial. The ansible.builtin.template module renders a Jinja2 .j2 file with each host's variables and facts and copies the result, unlike copy which ships bytes verbatim. Covers expression tags with double braces, control structures for and if with statement tags, whitespace trimming, filters including default, upper, lower, join, and password_hash, the ansible_managed header, and reading hostvars and groups so one template produces a per-host config. RHCE EX294 ready.
// ansible Β· templates (jinja2)

One `.j2` file, a tailored config on every host. Watch the same template render into different concrete files for servera and serverb as we spotlight the template module, expressions, control structures, filters, and per-host facts.

// before you start
you should know
  • Β·Ansible playbook basics (tasks, modules, vars)
  • Β·Comfortable with the copy module
  • Β·Know what facts and group/host vars are
by the end you'll

Render Jinja2 templates per host with the template module: insert values with {{ }}, branch and loop with {% %}, transform with filters, and use facts so one .j2 produces a correct file on every machine.

pace: 10 minutes

ansible β€” step 1 / 5 Β· template
🧬templates/nginx.conf.j2jinja2 source
1# {{ ansible_managed }}
2server_name {{ inventory_hostname }};
3listen {{ http_port | default(80) }};
4worker_processes {{ ansible_facts['processor_vcpus'] }};
template module→renders per host
πŸ–₯️serverarendered
1# Ansible managed
2server_name servera;
3listen 80;
4worker_processes 2;
πŸ–₯️serverbrendered
1# Ansible managed
2server_name serverb;
3listen 8080;
4worker_processes 8;

The template module

`ansible.builtin.template` renders a Jinja2 `.j2` file on the control node and copies the RESULT to dest β€” unlike `copy`, which ships the bytes verbatim. You still set owner, group, and mode just like copy, and you can add `validate:` to run a syntax check on the rendered file before it replaces the live one. By convention the source `.j2` files live in a `templates/` directory beside your playbook, so Ansible finds them with a bare filename.

$ansible.builtin.template:\n src: httpd.conf.j2\n dest: /etc/httpd/conf/httpd.conf\n mode: '0644'
$validate: "httpd -t -f %s"
$templates/httpd.conf.j2
// key insight

`copy` ships bytes; `template` ships a Jinja2 file that is RENDERED with each host's variables and facts first. That one difference means a single `nginx.conf.j2` becomes a correct, host-specific config on every machine β€” no per-host files to maintain.

// exam-ready Β· template module & jinja2
$ansible.builtin.template src=X.j2 dest=Y
$mode / owner / group
$validate: "cmd %s"
$templates/ dir
${{ var }}
${% if %}…{% endif %}
${% for x in list %}…{% endfor %}
${%- … -%}
$| default(X)
$| upper / | lower
$| join(',')
$| password_hash('sha512')
${{ ansible_managed }}
$hostvars[h] / groups['grp']
// check yourself
4 quick questions
Q1

When should you use the `template` module instead of `copy`?

Q2

What's the difference between `{{ }}` and `{% %}` in a Jinja2 template?

Q3

A variable might be undefined and you want a sensible fallback in the rendered file. Which filter?

Q4

How can one template file produce a different nginx config on every web server?

These aren't graded β€” they're just for active recall, which is what actually makes the lesson stick.

πŸ””
// next: react to change

Ansible handlers & notifications

A template only matters if the service picks up the change. Use `notify` and handlers to restart a service only when its config actually changed β€” never on an unchanged run.

open β†’

Read this lesson as text5 chapters Β· quiz answers

  1. 01The template module

    ansible.builtin.template renders a Jinja2 .j2 file on the control node and copies the RESULT to dest β€” unlike copy, which ships the bytes verbatim. You still set owner, group, and mode just like copy, and you can add validate: to run a syntax check on the rendered file before it replaces the live one. By convention the source .j2 files live in a templates/ directory beside your playbook, so Ansible finds them with a bare filename.

    • $ ansible.builtin.template:\n src: httpd.conf.j2\n dest: /etc/httpd/conf/httpd.conf\n mode: '0644' β€” render β†’ copy result to dest
    • $ validate: "httpd -t -f %s" β€” check before replacing live file
    • $ templates/httpd.conf.j2 β€” convention: .j2 in templates/ dir
  2. 02Jinja2 expressions β€” {{ }}

    A {{ }} tag inserts a value into the output: a variable, a fact, arithmetic, or a dict/list lookup. This is the exact same templating engine you already use inside playbooks β€” nothing new to learn. Whatever the expression evaluates to for THIS host gets dropped into the rendered file at that spot.

    • $ ServerName {{ inventory_hostname }} β€” a magic var
    • $ Listen {{ http_port }} β€” a play/group var
    • $ worker_processes {{ ansible_facts['processor_vcpus'] }}; β€” a gathered fact
  3. 03Control structures β€” {% %}

    Statement tags use {% %} and produce no direct output β€” they drive logic. {% for x in list %}…{% endfor %} repeats a block once per item, and {% if cond %}…{% endif %} (with optional {% else %}) emits a block conditionally. Remember the split: {{ }} OUTPUTS a value, {% %} is CONTROL FLOW. Add a dash to trim surrounding whitespace β€” {%- … -%} β€” so loops don't leave blank lines in the rendered file.

    • $ {% for h in groups['webservers'] %}\nserver {{ h }};\n{% endfor %} β€” loop emits one line per host
    • $ {% if tls_enabled %}…{% else %}…{% endif %} β€” conditional block
    • $ {%- for x in xs -%}…{%- endfor -%} β€” trim whitespace around tags
  4. 04Filters β€” transform values

    Pipe a value through a filter to transform it before it lands in the output: | default(X) supplies a fallback when undefined, | upper/| lower change case, | join(',') flattens a list, | to_nice_yaml pretty-prints, and | password_hash('sha512') produces a crypt hash. Filters chain left-to-right, so {{ name | lower | trim }} applies each in turn. They're the everyday glue that turns raw vars into config-ready strings.

    • $ {{ name | default('localhost') }} β€” fallback for undefined
    • $ {{ admins | join(', ') }} β€” list β†’ comma string
    • $ {{ pw | password_hash('sha512') }} β€” crypt-style hash
  5. 05Facts make each host different

    Because a template is rendered with THIS host's own facts and vars, one .j2 file becomes a tailored config on every machine. A load-balancer template can list every web server by walking groups['webservers'] and reading each one's IP from hostvars. Always drop {{ ansible_managed }} at the top as a header so humans know the file is generated and shouldn't be hand-edited.

    • $ {% for h in groups['webservers'] %}\nserver {{ hostvars[h]['ansible_facts']['default_ipv4']['address'] }};\n{% endfor %} β€” walk the group, read each IP
    • $ # {{ ansible_managed }} β€” do-not-edit header
    • $ {{ ansible_facts['fqdn'] }} β€” this host's own fact

Quiz β€” answers

When should you use the template module instead of copy?
β†’When the file contains Jinja2 variables or logic that must be rendered per host β€” copy ships the file unchanged, byte for byte.template renders the .j2 through Jinja2 with the target host's vars and facts, then copies the RESULT. copy ships the bytes verbatim with no substitution. Both can set owner/group/mode.
What's the difference between {{ }} and {% %} in a Jinja2 template?
β†’{{ }} outputs the value of an expression; {% %} is a statement / control structure (for, if) that produces no direct output.Expression tags {{ }} emit a value into the file. Statement tags {% %} drive control flow (loops, conditionals) and render nothing themselves.
A variable might be undefined and you want a sensible fallback in the rendered file. Which filter?
β†’| default(value){{ myvar | default('localhost') }} substitutes the given value whenever myvar is undefined, so the template still renders cleanly.
How can one template file produce a different nginx config on every web server?
β†’It's rendered with each host's own facts and vars (and can read hostvars/groups), so the output differs per host.Templating runs once per target with that host's facts/vars in scope, and it can reach across the fleet via hostvars and groups β€” so a single .j2 yields a host-specific file everywhere.
// more in systems

keep going β€” these pair well with what you just learned.

see all systems β†’

See also

back to RHCSA / RHCE trackall lessons