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.confThis 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_processesdirective 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 NginxWhen 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.12In 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 checkTemplate Best Practices
- Add a Warning Header: Systems administrators log onto servers. If they see a config file they don't like, they might manually
vimand 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!) - 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.
- Use the
validateparameter: When generating critical files (sshd_config,sudoers,nginx.conf), use the template module'svalidateargument. 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!