Templates with Jinja2 in Ansible
In short: Jinja2 templates separate the shape of a configuration from the data that fills it. Ansible renders a template for each device using its variables, so one template can produce hundreds of consistent, reviewable configurations.
Why templates
Twenty access switches have almost identical configurations: only the hostname, management IP and a few VLANs differ. Copying and editing text by hand leads to typos and drift. A template holds the common structure once, with placeholders for what changes. Ansible uses the Jinja2 templating language, the same one used by many Python tools, to fill in those placeholders from inventory variables.
A switch template
{# templates/access_switch.j2 #}
hostname {{ inventory_hostname }}
!
{% for server in ntp_servers %}
ntp server {{ server }}
{% endfor %}
logging host {{ syslog_server }}
!
{% for vlan in vlans %}
vlan {{ vlan.id }}
name {{ vlan.name | upper }}
{% endfor %}
!
interface Vlan{{ mgmt_vlan | default(99) }}
ip address {{ mgmt_ip }} {{ mgmt_mask }}
{% if enable_ssh_v2 | default(true) %}
ip ssh version 2
{% endif %}
{{ ... }}prints a value;{% ... %}is logic;{# ... #}is a comment that never appears in the output.- Filters transform values:
upper,default,join,replace, and Ansible'sansible.utils.ipaddrfamily for IP maths. - Ansible sets
trim_blocksby default, so the line breaks after{% %}tags are removed. Use{%-or-%}to strip extra whitespace when needed.
Rendering and deploying
Render to files first, review them, then deploy. This separates "is the generated config right?" from "did the push work?".
- name: Build and deploy access switch configs
hosts: access
gather_facts: false
tasks:
- name: Render config locally for review
ansible.builtin.template:
src: templates/access_switch.j2
dest: "build/{{ inventory_hostname }}.cfg"
delegate_to: localhost
- name: Push rendered config
cisco.ios.ios_config:
src: "build/{{ inventory_hostname }}.cfg"
save_when: modified
tags: deploy
delegate_to: localhost runs the template task on the control node, since the switch cannot store files itself. Run the playbook once with --skip-tags deploy to build files only, check them with a diff or a colleague, then run again with --tags deploy. For Linux servers, the same template module writes the file directly to the server, for example a chrony.conf or an Nginx site.
Good template habits
| Habit | Why |
|---|---|
| Keep logic light | Complex calculations belong in variables or filters, not deep nested ifs |
Use default() carefully | Safe defaults are useful; hidden defaults surprise people |
Set DEFAULT_UNDEFINED_VAR_BEHAVIOR to fail | Ansible already errors on undefined variables; do not switch this off |
| One template per role or platform | Easier to read than one huge template full of vendor checks |
| Store templates in Git | Changes are reviewed and history is kept |
Tip: Jinja2 is not only for Ansible. The same skills apply when you generate configs from Python with the jinja2 library, as covered in Jinja2 for network configuration.
Practice
What is the difference between {{ }} and {% %} in Jinja2?
{{ }} outputs the value of an expression; {% %} runs logic such as loops and conditions without printing anything itself.Why use delegate_to: localhost with the template module for switches?
Why render configs to files before deploying?
Key takeaways
- Templates hold the shared structure; variables hold per-device data.
- {{ }} prints, {% %} controls logic, {# #} comments.
- Filters such as default, upper and ipaddr transform values.
- Render to files, review, then deploy with a config module.
- Keep templates simple and version controlled.
Original content written by Mohamed Fayas for FM TecHub. © 2026 FM TecHub. You're welcome to share a link to this page; please don't copy the text elsewhere without permission.