G
GuideDevOps
Lesson 8 of 14

Templates (Jinja2)

Part of the Ansible tutorial series.

The Problem with the copy Module

In earlier tutorials we copied a static configuration file from the Control Node to the Managed Nodes using the copy module:

- name: Copy Nginx config
  copy:
    src: static_nginx.conf
    dest: /etc/nginx/nginx.conf

This assumes every single web server needs the exact same nginx.conf file.

But what if:

  • Server A has 2 CPU cores and Server B has 8? Nginx's worker_processes directive should match the CPU count for optimal performance.
  • You deploy to Staging and Production. The Staging Nginx should connect to the Staging Database, and Production should connect to the Production Database.

If you use copy, you must maintain multiple versions of the configuration file organically (e.g., nginx_staging_2core.conf, nginx_prod_8core.conf). This is unmanageable.


Enter Jinja2 Templates

Ansible solves this with the template module.

Instead of copying a raw, static file, the template module evaluates a file using the Jinja2 templating language. It replaces placeholder variables with dynamic values before copying the resulting file to the target node.

Template files traditionally use the .j2 extension.

Writing a Jinja2 Template

Create a file named nginx.conf.j2:

# Automatically generated by Ansible. DO NOT EDIT MANUALLY.
 
# Set workers equal to the number of CPU cores discovered via Facts
worker_processes {{ ansible_processor_vcpus }};
 
events {
    worker_connections 1024;
}
 
http {
    server {
        # Bind to the dynamically provided port variable
        listen {{ bind_port }};
        server_name {{ inventory_hostname }};
 
        location / {
            root /var/www/html;
            index index.html;
        }
        
        location /api {
            # Proxy traffic to environment-specific backend
            proxy_pass http://{{ backend_api_url }};
        }
    }
}

Deploying the Template

In your playbook, use the template module just as you would use the copy module:

---
- name: Deploy dynamic Nginx Configuration
  hosts: webservers
  become: yes
  vars:
    bind_port: 8080
    backend_api_url: "api.staging.example.com"
  
  tasks:
    - name: Generate and copy Nginx configuration
      template:
        src: files/nginx.conf.j2
        dest: /etc/nginx/nginx.conf
      notify: Restart Nginx

When Ansible executes this task on a server with 4 CPU cores (ansible_processor_vcpus = 4) named web1, the resulting file on the target server will look like this:

# Automatically generated by Ansible. DO NOT EDIT MANUALLY.
 
worker_processes 4;
 
events {
    worker_connections 1024;
}
 
http {
    server {
        listen 8080;
        server_name web1;
        # ...
        location /api {
            proxy_pass http://api.staging.example.com;
        }
    }
}

Advanced Jinja2 Syntax

Jinja2 isn't just for dropping variables into text. It supports logic, loops, and conditional statements inside the file generation!

Conditionals (if)

You can include text blocks based on variables:

server {
    listen 80;
    server_name myapp.com;
 
    # Only include SSL config if the enable_ssl variable is true
    {% if enable_ssl %}
    listen 443 ssl;
    ssl_certificate /etc/nginx/ssl/cert.pem;
    ssl_certificate_key /etc/nginx/ssl/key.pem;
    {% endif %}
}

Loops (for)

Imagine you need to configure a load balancer (HAProxy or Nginx) to proxy traffic to an array of backend servers.

# In your group_vars/all.yml
backend_servers:
  - 10.0.1.10
  - 10.0.1.11
  - 10.0.1.12

In your haproxy.cfg.j2 template, you loop over that array:

backend web_cluster
    balance roundrobin
    {% for server_ip in backend_servers %}
    server web-{{ loop.index }} {{ server_ip }}:80 check
    {% endfor %}

The generated result on the server:

backend web_cluster
    balance roundrobin
    server web-1 10.0.1.10:80 check
    server web-2 10.0.1.11:80 check
    server web-3 10.0.1.12:80 check

Template Best Practices

  1. Add a Warning Header: Systems administrators log onto servers. If they see a config file they don't like, they might manually vim and edit it. The next time Ansible runs, their manual changes will be overwritten and destroyed by the template evaluation! Always include a comment at the top of templates (e.g., # Managed by Ansible. Local changes will be overwritten!)
  2. Keep logic in the playbooks, not templates: While Jinja2 allows complex programming, templates should remain as clean as possible. Do heavy variable manipulation in the playbook logic, and just output the results in the template.
  3. Use the validate parameter: When generating critical files (sshd_config, sudoers, nginx.conf), use the template module's validate argument. This runs a command block on the target before saving the file. If validation fails, Ansible aborts, ensuring you don't push a corrupt file that brings the server down.
- name: Deploy sudoers
  template:
    src: sudoers.j2
    dest: /etc/sudoers
    validate: /usr/sbin/visudo -cf %s # Validates syntax before saving!