What a Template Is
A Jinja2 template is a text file with placeholders. When Ansible's template
module runs, it substitutes every placeholder with the resolved variable value, then
copies the result to the target host. It's how you generate config files that adapt
to each host without writing one config file per host.
Three Kinds of Markup
| Markup | Purpose | Example |
|---|---|---|
{{ ... }} | Print a value | port = {{ mysql_port }} |
{% ... %} | Statement (if, for, set) | {% if enable_ssl %}...{% endif %} |
{# ... #} | Comment (stripped from output) | {# this never appears #} |
A Real Template — my.cnf.j2
JINJA2 — templates/my.cnf.j2
# Managed by Ansible — do not edit by hand
[mysqld]
server-id = {{{{ server_id }}}}
port = {{{{ mysql_port | default(3306) }}}}
bind-address = {{{{ ansible_default_ipv4.address }}}}
datadir = {{{{ mysql_datadir }}}}
socket = {{{{ mysql_datadir }}}}/mysql.sock
log-error = /var/log/mysqld.log
pid-file = {{{{ mysql_datadir }}}}/mysqld.pid
# Tuning — sized to host RAM
innodb_buffer_pool_size = {{{{ (ansible_memtotal_mb * 0.7) | int }}}}M
max_connections = {{{{ mysql_max_connections | default(200) }}}}
{{% if mysql_role == "primary" %}}
# Primary-only settings
log-bin = mysql-bin
binlog_format = ROW
gtid_mode = ON
enforce_gtid_consistency = ON
{{% endif %}}
{{% if mysql_replicas is defined and mysql_replicas | length > 0 %}}
# Replicas registered in inventory:
{{% for replica in mysql_replicas %}}
# - {{{{ replica }}}}
{{% endfor %}}
{{% endif %}}
YAML — calling the template
- name: Render /etc/my.cnf from template
ansible.builtin.template:
src: my.cnf.j2
dest: /etc/my.cnf
owner: root
group: root
mode: "0644"
backup: true
notify: restart mysql
The 12 Filters You'll Use Most
| Filter | What it does | Example |
|---|---|---|
default(x) | Use x if value is undefined | {{ port | default(3306) }} |
upper / lower | Case conversion | {{ name | upper }} |
length | List or string length | {{ users | length }} |
join(sep) | List → string | {{ ips | join(',') }} |
split(sep) | String → list | {{ csv | split(',') }} |
int / float | Type conversion | {{ '42' | int }} |
map('attr', 'k') | Pluck k from each item | {{ users | map(attribute='name') | list }} |
selectattr | Filter list of dicts | {{ users | selectattr('admin') | list }} |
combine | Merge two dicts | {{ defaults | combine(overrides) }} |
to_json | Render as JSON | {{ data | to_json }} |
regex_replace | Substitute by regex | {{ s | regex_replace('-', '_') }} |
b64encode | Base64 encode | {{ pwd | b64encode }} |
Conditionals in Templates
JINJA2 — if / elif / else
{{% if mysql_role == "primary" %}}
# Primary settings
log-bin = mysql-bin
{{% elif mysql_role == "replica" %}}
# Replica settings
read_only = ON
{{% else %}}
# Default — standalone
{{% endif %}}
Loops in Templates
JINJA2 — for loops with loop variables
# Inventory of all DB hosts
{{% for host in groups['databases'] %}}
{{{{ hostvars[host].ansible_host }}}} {{{{ host }}}}
{{% endfor %}}
# With loop.index for numbered output
{{% for user in admin_users %}}
user{{{{ loop.index }}}} = {{{{ user }}}}
{{% endfor %}}
# With if-else inside the loop
{{% for port in firewall_ports %}}
allow {{{{ port }}}}{{% if not loop.last %},{{% endif %}}
{{% endfor %}}
Whitespace Control — The Final 10%
By default, Jinja2 leaves the newlines around {% %} tags in the output, which
produces messy files. Use - on the tag to strip whitespace:
JINJA2 — whitespace stripping
{{# without trim — leaves blank lines #}}
{{% for u in users %}}
{{{{ u }}}}
{{% endfor %}}
{{# with trim — clean output #}}
{{% for u in users -%}}
{{{{ u }}}}
{{% endfor %}}
{{# strip both ends #}}
{{%- if condition -%}}
content
{{%- endif -%}}
💡 Tip: In
ansible.cfg set jinja2_extensions = jinja2.ext.do to enable the do tag for in-place list/dict mutation. Set trim_blocks = True in your template if you'd rather have whitespace stripping be the default.