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.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.
- Β·Ansible playbook basics (tasks, modules, vars)
- Β·Comfortable with the copy module
- Β·Know what facts and group/host vars are
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
templates/nginx.conf.j2jinja2 source# {{ ansible_managed }}server_name {{ inventory_hostname }};listen {{ http_port | default(80) }};worker_processes {{ ansible_facts['processor_vcpus'] }};moduleβrenders
per host
serverarendered# Ansible managedserver_name servera;listen 80;worker_processes 2;serverbrendered# Ansible managedserver_name serverb;listen 8080;worker_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`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.
$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']When should you use the `template` module instead of `copy`?
What's the difference between `{{ }}` and `{% %}` in a Jinja2 template?
A variable might be undefined and you want a sensible fallback in the rendered file. Which filter?
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.
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.
Read this lesson as text5 chapters Β· quiz answers
01The template module
ansible.builtin.templaterenders a Jinja2.j2file on the control node and copies the RESULT to dest β unlikecopy, which ships the bytes verbatim. You still set owner, group, and mode just like copy, and you can addvalidate:to run a syntax check on the rendered file before it replaces the live one. By convention the source.j2files live in atemplates/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
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
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
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/| lowerchange case,| join(',')flattens a list,| to_nice_yamlpretty-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
05Facts make each host different
Because a template is rendered with THIS host's own facts and vars, one
.j2file becomes a tailored config on every machine. A load-balancer template can list every web server by walkinggroups['webservers']and reading each one's IP fromhostvars. 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
templatemodule instead ofcopy? - βWhen the file contains Jinja2 variables or logic that must be rendered per host β
copyships the file unchanged, byte for byte.templaterenders the .j2 through Jinja2 with the target host's vars and facts, then copies the RESULT.copyships 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 whenevermyvaris 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.
keep going β these pair well with what you just learned.
Ansible control node & inventory
Ansible is agentless β one control node pushes work to a fleet over SSH. Install ansible-core, write an inventory with groups, ping the fleet, and fire ad-hoc commands. RHCE-ready.
Ansible playbooks & YAML
Plays, tasks, and idempotency. Watch a playbook run task-by-task β ok, changed, skipped β then run it again and watch everything go green with 0 changed. RHCE-ready.
Ansible core modules
file, user, copy, dnf, service β the modules that do the real work. Declare the desired state and let Ansible converge to it, idempotently. RHCE-ready.