Ansible Template Loops
Introduction
Ansible templates are powerful tools that allow you to generate dynamic configuration files by combining static content with variables from your inventory, playbooks, or facts gathered from your systems. One of the most powerful features of Ansible templates is the ability to use loops, which enable you to iterate over collections of data and generate repetitive content efficiently.
Templates in Ansible are built on the Jinja2 templating engine, which provides robust looping constructs that can help you create complex configuration files with minimal repetition in your code. In this guide, we'll explore how to use loops in Ansible templates to create dynamic, flexible, and maintainable configuration files.
Understanding Template Loops
Ansible templates use Jinja2's looping syntax, which allows you to iterate over various data structures:
- Lists/arrays
- Dictionaries/maps
- Nested data structures
- Custom sequences
The basic syntax for a loop in a Jinja2 template looks like this:
{% for item in items %}
{{ item }}
{% endfor %}
Let's explore how these loops can be used in practice with Ansible templates.
Basic Loop Examples
Looping Through a List
Let's start with a simple example where we loop through a list of web servers to generate an Apache virtual host configuration:
Inventory File (inventory.yml
):
all:
vars:
websites:
- name: example.com
port: 80
document_root: /var/www/example.com
- name: blog.example.com
port: 80
document_root: /var/www/blog
- name: shop.example.com
port: 443
document_root: /var/www/shop
Template File (vhosts.conf.j2
):
# Apache Virtual Hosts
# Generated by Ansible on {{ ansible_date_time.date }}
{% for site in websites %}
<VirtualHost *:{{ site.port }}>
ServerName {{ site.name }}
DocumentRoot {{ site.document_root }}
ErrorLog ${APACHE_LOG_DIR}/{{ site.name }}_error.log
CustomLog ${APACHE_LOG_DIR}/{{ site.name }}_access.log combined
</VirtualHost>
{% endfor %}
Playbook (configure_apache.yml
):
---
- name: Configure Apache Virtual Hosts
hosts: webservers
tasks:
- name: Generate Apache virtual hosts configuration
template:
src: vhosts.conf.j2
dest: /etc/apache2/sites-available/vhosts.conf
notify: Restart Apache
handlers:
- name: Restart Apache
service:
name: apache2
state: restarted
Output (/etc/apache2/sites-available/vhosts.conf
):
# Apache Virtual Hosts
# Generated by Ansible on 2023-04-15
<VirtualHost *:80>
ServerName example.com
DocumentRoot /var/www/example.com
ErrorLog ${APACHE_LOG_DIR}/example.com_error.log
CustomLog ${APACHE_LOG_DIR}/example.com_access.log combined
</VirtualHost>
<VirtualHost *:80>
ServerName blog.example.com
DocumentRoot /var/www/blog
ErrorLog ${APACHE_LOG_DIR}/blog.example.com_error.log
CustomLog ${APACHE_LOG_DIR}/blog.example.com_access.log combined
</VirtualHost>
<VirtualHost *:443>
ServerName shop.example.com
DocumentRoot /var/www/shop
ErrorLog ${APACHE_LOG_DIR}/shop.example.com_error.log
CustomLog ${APACHE_LOG_DIR}/shop.example.com_access.log combined
</VirtualHost>
In this example, we iterate through each website defined in the websites
variable and generate a VirtualHost configuration block for each one.
Advanced Loop Techniques
Accessing Loop Variables
Jinja2 provides special variables within loops that can be useful:
loop.index
: The current iteration (1-indexed)loop.index0
: The current iteration (0-indexed)loop.first
: True if this is the first iterationloop.last
: True if this is the last iterationloop.length
: Total number of items in the sequence
Example Template (nginx_upstreams.conf.j2
):
upstream backend_servers {
{% for server in backend_servers %}
server {{ server.ip }}:{{ server.port }}{% if not loop.last %};{% endif %}
{% endfor %}
}
Conditional Logic Within Loops
You can combine loops with conditionals to create more complex templates:
Inventory Data:
users:
- name: john
groups: ['developers', 'admin']
shell: /bin/bash
- name: sarah
groups: ['developers']
shell: /bin/zsh
- name: guest
groups: ['visitors']
shell: /bin/sh
Template (sudoers.j2
):
# Sudoers configuration
# Generated by Ansible
{% for user in users %}
{% if 'admin' in user.groups %}
{{ user.name }} ALL=(ALL) ALL
{% elif 'developers' in user.groups %}
{{ user.name }} ALL=(ALL) NOPASSWD: /usr/bin/apt-get
{% endif %}
{% endfor %}
Output:
# Sudoers configuration
# Generated by Ansible
john ALL=(ALL) ALL
sarah ALL=(ALL) NOPASSWD: /usr/bin/apt-get
Nested Loops
You can use nested loops to iterate through multi-dimensional data structures:
Inventory Data:
departments:
development:
manager: John Smith
projects:
- name: website
servers: ['web01', 'web02', 'web03']
- name: mobile_app
servers: ['app01', 'app02']
operations:
manager: Sarah Jones
projects:
- name: infrastructure
servers: ['infra01', 'infra02']
Template (project_report.j2
):
# Department Projects Report
# Generated: {{ ansible_date_time.date }}
{% for dept_name, dept_data in departments.items() %}
Department: {{ dept_name }}
Manager: {{ dept_data.manager }}
Projects:
{% for project in dept_data.projects %}
- {{ project.name }}
Servers:
{% for server in project.servers %}
* {{ server }}
{% endfor %}
{% endfor %}
{% endfor %}
Output:
# Department Projects Report
# Generated: 2023-04-15
Department: development
Manager: John Smith
Projects:
- website
Servers:
* web01
* web02
* web03
- mobile_app
Servers:
* app01
* app02
Department: operations
Manager: Sarah Jones
Projects:
- infrastructure
Servers:
* infra01
* infra02
Practical Real-World Examples
Example 1: Generating an HAProxy Configuration
Let's create a template for an HAProxy load balancer configuration:
Inventory Data:
haproxy:
frontend:
name: main
bind_port: 80
default_backend: web_servers
backends:
- name: web_servers
balance: roundrobin
servers:
- name: web1
address: 192.168.1.101
port: 8080
options: check
- name: web2
address: 192.168.1.102
port: 8080
options: check backup
- name: api_servers
balance: leastconn
servers:
- name: api1
address: 192.168.1.201
port: 9000
options: check
- name: api2
address: 192.168.1.202
port: 9000
options: check
Template (haproxy.cfg.j2
):
# HAProxy Configuration
# Generated by Ansible on {{ ansible_date_time.date }}
global
log /dev/log local0
log /dev/log local1 notice
user haproxy
group haproxy
daemon
defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5000
timeout client 50000
timeout server 50000
frontend {{ haproxy.frontend.name }}
bind *:{{ haproxy.frontend.bind_port }}
default_backend {{ haproxy.frontend.default_backend }}
{% for backend in haproxy.backends %}
backend {{ backend.name }}
balance {{ backend.balance }}
{% for server in backend.servers %}
server {{ server.name }} {{ server.address }}:{{ server.port }} {{ server.options }}
{% endfor %}
{% endfor %}
Example 2: Generating an NGINX Server Configuration with Multiple Virtual Hosts
Inventory Data:
nginx:
worker_processes: 4
worker_connections: 1024
sites:
- domain: example.com
root: /var/www/example.com
ssl: true
ssl_cert: /etc/ssl/certs/example.com.crt
ssl_key: /etc/ssl/private/example.com.key
locations:
- path: /
content: |
try_files $uri $uri/ /index.html;
- path: /api
content: |
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
- domain: blog.example.com
root: /var/www/blog
ssl: false
locations:
- path: /
content: |
try_files $uri $uri/ /index.php?$args;
- path: ~ \.php$
content: |
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
Template (nginx.conf.j2
):
# NGINX Configuration
# Generated by Ansible on {{ ansible_date_time.date }}
user www-data;
worker_processes {{ nginx.worker_processes }};
pid /run/nginx.pid;
events {
worker_connections {{ nginx.worker_connections }};
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
gzip on;
gzip_disable "msie6";
{% for site in nginx.sites %}
server {
listen 80;
{% if site.ssl %}
listen 443 ssl;
ssl_certificate {{ site.ssl_cert }};
ssl_certificate_key {{ site.ssl_key }};
{% endif %}
server_name {{ site.domain }};
root {{ site.root }};
{% for location in site.locations %}
location {{ location.path }} {
{{ location.content | indent(12) }}
}
{% endfor %}
}
{% endfor %}
}
Loop Filters and Transformations
Jinja2 provides filters that can be used with loops to transform data:
Map Filter
The map
filter transforms each item in a sequence:
{% for item in users | map(attribute='name') %}
User: {{ item }}
{% endfor %}
Select Filter
The select
filter allows you to filter items:
{% for user in users | selectattr('is_admin', 'eq', true) %}
Admin: {{ user.name }}
{% endfor %}
Sort Filter
The sort
filter sorts the items:
{% for server in servers | sort(attribute='priority', reverse=true) %}
{{ server.name }}: {{ server.priority }}
{% endfor %}
Example: Combined Filters
Let's see a more complex example that combines multiple filters:
Inventory Data:
applications:
- name: backend
importance: high
resources:
memory: 4096
cpu: 2
- name: frontend
importance: medium
resources:
memory: 2048
cpu: 1
- name: database
importance: critical
resources:
memory: 8192
cpu: 4
Template (resource_allocation.j2
):
# Resource Allocation by Importance
# Generated by Ansible
{% for app in applications | sort(attribute='resources.memory', reverse=true) | selectattr('importance', 'in', ['high', 'critical']) %}
{{ app.name | upper }} ({{ app.importance }}):
- Memory: {{ app.resources.memory }} MB
- CPU: {{ app.resources.cpu }} cores
{% endfor %}
Output:
# Resource Allocation by Importance
# Generated by Ansible
DATABASE (critical):
- Memory: 8192 MB
- CPU: 4 cores
BACKEND (high):
- Memory: 4096 MB
- CPU: 2 cores
Best Practices for Template Loops
-
Keep templates readable:
- Use proper indentation
- Add comments where necessary
- Break complex templates into smaller ones
-
Avoid too much logic in templates:
- Preprocess complex data in your playbooks or roles
- Use filters in your tasks to prepare data before it gets to templates
-
Use meaningful variable names:
- Name your loop variables clearly
- Use plural names for collections and singular for the loop variable
-
Error handling:
- Use the
default
filter to provide fallback values - Check if variables exist before using them in loops
- Use the
{% for server in servers | default([]) %}
{{ server.name }}
{% else %}
# No servers configured
{% endfor %}
- Performance considerations:
- Avoid deeply nested loops for large data sets
- Consider preprocessing data in your playbooks
Common Pitfalls and Solutions
Whitespace Control
By default, Jinja2 preserves whitespace. To control this, use the minus sign (-
) in your template tags:
{% for user in users -%}
{{ user.name }}
{%- endfor %}
Undefined Variables
If a variable might not exist, use the default
filter:
{% for item in maybe_undefined_var | default([]) %}
{{ item }}
{% endfor %}
Complex Expressions
For complex expressions, it's better to compute them in your playbook:
- name: Prepare data for template
set_fact:
filtered_servers: "{{ servers | selectattr('status', 'eq', 'active') | list }}"
- name: Generate configuration
template:
src: servers.conf.j2
dest: /etc/app/servers.conf
Then in your template:
{% for server in filtered_servers %}
{{ server.name }}
{% endfor %}
Summary
Ansible template loops are a powerful feature that allows you to generate dynamic, clean, and DRY configuration files. By leveraging Jinja2's looping constructs, you can iterate over various data structures, apply conditions, and transform data to produce exactly the output you need.
In this guide, we've covered:
- Basic looping syntax in Ansible templates
- Working with loop variables and nested loops
- Applying filters and transformations
- Real-world examples of using loops in configuration files
- Best practices and common pitfalls
With these tools, you can create flexible, maintainable templates that adapt to changes in your infrastructure without requiring manual edits to configuration files.
Additional Exercises
- Create a template that generates an NGINX configuration with load balancing for multiple microservices.
- Generate a comprehensive firewall rules file using loops to iterate through a list of allowed services and ports.
- Build a template that creates a monitoring config file that includes different checking frequencies based on service importance.
- Create a template that generates a hosts file with different sections for different types of servers.
Further Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)