ansible.builtin.template Module: Deploy Jinja2 Templates (Complete Guide)
By Luca Berton · Published 2026-04-03 · Category: database-automation
Complete guide to ansible.builtin.template module. Deploy Jinja2 templates with variables, loops, conditionals to remote hosts.
The ansible.builtin.template module processes Jinja2 templates and deploys the rendered files to remote hosts. It's the primary way to generate configuration files dynamically.
Basic Usage
# playbook.yml
- name: Deploy nginx config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
notify: restart nginx
{# templates/nginx.conf.j2 #}
worker_processes {{ ansible_processor_vcpus }};
events {
worker_connections {{ nginx_max_connections | default(1024) }};
}
http {
server {
listen {{ http_port | default(80) }};
server_name {{ server_name }};
root {{ document_root }};
}
}
See also: Ansible Troubleshooting Installation Issues on macOS and Python
Variables in Templates
{# Access playbook variables #}
DB_HOST={{ db_host }}
DB_PORT={{ db_port }}
DB_NAME={{ db_name }}
{# Access facts #}
HOSTNAME={{ ansible_hostname }}
IP_ADDRESS={{ ansible_default_ipv4.address }}
TOTAL_RAM_MB={{ ansible_memtotal_mb }}
{# Default values #}
LOG_LEVEL={{ log_level | default('info') }}
WORKERS={{ worker_count | default(ansible_processor_vcpus) }}
Conditionals in Templates
{# if/elif/else #}
{% if env == 'production' %}
DEBUG=false
LOG_LEVEL=warning
{% elif env == 'staging' %}
DEBUG=true
LOG_LEVEL=info
{% else %}
DEBUG=true
LOG_LEVEL=debug
{% endif %}
{# Inline conditional #}
ssl_enabled={{ 'true' if use_ssl else 'false' }}
See also: Ansible Troubleshooting: Fix Jinja2 Syntax & Inventory Errors
Loops in Templates
{# Loop over a list #}
{% for server in backend_servers %}
upstream_server {{ server.host }}:{{ server.port }};
{% endfor %}
{# Loop with index #}
{% for user in users %}
# User {{ loop.index }}: {{ user.name }}
{{ user.name }}:x:{{ user.uid }}:{{ user.gid }}:{{ user.comment }}:{{ user.home }}:{{ user.shell }}
{% endfor %}
{# Loop with conditional #}
{% for host in groups['webservers'] %}
{% if hostvars[host].ansible_host is defined %}
{{ hostvars[host].ansible_host }} {{ host }}
{% endif %}
{% endfor %}
Filters
{# String filters #}
{{ name | upper }}
{{ name | lower }}
{{ name | capitalize }}
{{ name | replace('old', 'new') }}
{{ name | regex_replace('^prefix-', '') }}
{# List filters #}
{{ servers | join(', ') }}
{{ items | sort }}
{{ items | unique }}
{{ items | length }}
{{ items | first }}
{{ items | last }}
{# Number filters #}
{{ size_bytes | human_readable }}
{{ price | round(2) }}
{{ count | int }}
{# Default values #}
{{ optional_var | default('fallback') }}
{{ maybe_undefined | default(omit) }}
{# JSON/YAML output #}
{{ config_dict | to_nice_json }}
{{ config_dict | to_nice_yaml }}
See also: Ansible Dynamic Data Construction: Build Variables at Runtime (Guide)
Template Module Parameters
- name: Deploy with all options
ansible.builtin.template:
src: app.conf.j2 # Jinja2 template (relative to templates/ or role)
dest: /etc/myapp/app.conf # Destination on remote
owner: appuser # File owner
group: appgroup # File group
mode: '0640' # File permissions
backup: true # Create backup before overwriting
validate: '/opt/app/validate-config %s' # Validate before deploying
force: true # Overwrite even if dest exists
Validate before deploying
- name: Deploy nginx config (validate first)
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
validate: 'nginx -t -c %s'
notify: reload nginx
- name: Deploy sudoers
ansible.builtin.template:
src: sudoers.j2
dest: /etc/sudoers
validate: 'visudo -cf %s'
Template Best Practices
Add a managed-by header
# {{ ansible_managed }}
# DO NOT EDIT - This file is managed by Ansible
# Template: {{ template_path }}
# Host: {{ inventory_hostname }}
# Date: {{ ansible_date_time.iso8601 }}
[application]
name = {{ app_name }}
Use block/endblock for complex sections
upstream backend {
{% for server in backend_servers %}
server {{ server.host }}:{{ server.port }}{% if server.weight is defined %} weight={{ server.weight }}{% endif %};
{% endfor %}
}
Whitespace control
{# Trim whitespace with - #}
{% for item in items -%}
{{ item }}
{%- endfor %}
Practical Examples
Generate /etc/hosts
# {{ ansible_managed }}
127.0.0.1 localhost
{{ ansible_default_ipv4.address }} {{ ansible_fqdn }} {{ ansible_hostname }}
{% for host in groups['all'] %}
{{ hostvars[host].ansible_host | default(hostvars[host].ansible_default_ipv4.address) }} {{ host }}
{% endfor %}
Application .env file
# {{ ansible_managed }}
APP_NAME={{ app_name }}
APP_ENV={{ app_env }}
APP_DEBUG={{ 'true' if app_env != 'production' else 'false' }}
APP_URL=https://{{ domain_name }}
DB_CONNECTION=pgsql
DB_HOST={{ db_host }}
DB_PORT={{ db_port | default(5432) }}
DB_DATABASE={{ db_name }}
DB_USERNAME={{ db_user }}
DB_PASSWORD={{ vault_db_password }}
REDIS_HOST={{ redis_host | default('127.0.0.1') }}
REDIS_PORT={{ redis_port | default(6379) }}
MAIL_MAILER=smtp
MAIL_HOST={{ mail_host }}
MAIL_PORT={{ mail_port | default(587) }}
FAQ
Where should I put template files?
In a role: roles/myrole/templates/. In a playbook: same directory or templates/ subdirectory. Ansible searches both locations.
What's the difference between template and copy with content?
template processes Jinja2 syntax (variables, loops, conditions). copy with content writes literal text. Use template for dynamic content, copy for static.
Can I use template for binary files?
No. Template is for text files only. Use copy for binary files.
How do I debug template rendering?
Use ansible-playbook --check --diff to preview changes, or render locally: ansible all -m template -a "src=test.j2 dest=/tmp/test.txt" --check --diff
Basic Template
- name: Deploy config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
become: true
notify: restart nginx
Template File (Jinja2)
{# templates/nginx.conf.j2 #}
# Managed by Ansible - DO NOT EDIT
server {
listen {{ http_port | default(80) }};
server_name {{ server_name }};
root {{ document_root }};
index index.html;
{% if ssl_enabled %}
listen 443 ssl;
ssl_certificate {{ ssl_cert_path }};
ssl_certificate_key {{ ssl_key_path }};
{% endif %}
{% for location in locations | default([]) %}
location {{ location.path }} {
proxy_pass {{ location.backend }};
}
{% endfor %}
}
Variables in Templates
{# Access any Ansible variable #}
Hostname: {{ inventory_hostname }}
OS: {{ ansible_distribution }} {{ ansible_distribution_version }}
IP: {{ ansible_default_ipv4.address }}
Custom: {{ my_custom_var }}
Default: {{ optional_var | default('fallback') }}
Loops
{% for user in users %}
{{ user.name }}:{{ user.uid }}:{{ user.shell | default('/bin/bash') }}
{% endfor %}
{# With index #}
{% for item in items %}
{{ loop.index }}. {{ item }}{{ "," if not loop.last }}
{% endfor %}
Conditionals
{% if env == 'production' %}
log_level = warn
workers = {{ ansible_processor_vcpus * 2 }}
{% elif env == 'staging' %}
log_level = info
workers = {{ ansible_processor_vcpus }}
{% else %}
log_level = debug
workers = 2
{% endif %}
Filters in Templates
{# String manipulation #}
{{ hostname | upper }}
{{ path | basename }}
{{ url | urlsplit('hostname') }}
{# List operations #}
{{ servers | join(', ') }}
{{ packages | sort | join('\n') }}
{# Math #}
memory_limit = {{ ansible_memtotal_mb // 2 }}m
Multi-File Templates
- template:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
loop:
- { src: app.conf.j2, dest: /etc/myapp/app.conf }
- { src: db.conf.j2, dest: /etc/myapp/db.conf }
- { src: logging.conf.j2, dest: /etc/myapp/logging.conf }
notify: restart myapp
Validate Before Deploy
- template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
validate: "nginx -t -c %s"
become: true
- template:
src: sshd_config.j2
dest: /etc/ssh/sshd_config
validate: "/usr/sbin/sshd -t -f %s"
become: true
Generate from Inventory
{# haproxy.cfg.j2 - dynamic backend from inventory #}
backend webservers
balance roundrobin
{% for host in groups['webservers'] %}
server {{ host }} {{ hostvars[host].ansible_host }}:{{ http_port }} check
{% endfor %}
FAQ
template vs copy?
Use template when the file contains {{ variables }} or {% logic %}. Use copy for static files.
How do I include another template?
{% include 'partials/header.j2' %}
How do I escape Jinja2 syntax?
{% raw %}
This {{ won't }} be processed
{% endraw %}
Related Articles
• Ansible Vault CLI reference • using when in Ansible playbooks • Ansible inventory file structure • the Ansible loops reference • Nginx vhost provisioning with AnsibleCategory: database-automation