AnsiblePilot — Master Ansible Automation

AnsiblePilot is the leading resource for learning Ansible automation, DevOps, and infrastructure as code. Browse over 1,400 tutorials covering Ansible modules, playbooks, roles, collections, and real-world examples. Whether you are a beginner or an experienced engineer, our step-by-step guides help you automate Linux, Windows, cloud, containers, and network infrastructure.

Popular Topics

About Luca Berton

Luca Berton is an Ansible automation expert, author of 8 Ansible books published by Apress and Leanpub including "Ansible for VMware by Examples" and "Ansible for Kubernetes by Example", and creator of the Ansible Pilot YouTube channel. He shares practical automation knowledge through tutorials, books, and video courses to help IT professionals and DevOps engineers master infrastructure automation.

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 referenceusing when in Ansible playbooksAnsible inventory file structurethe Ansible loops referenceNginx vhost provisioning with Ansible

Category: database-automation

Browse all Ansible tutorials · AnsiblePilot Home