Skip to main content

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:

jinja
{% 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):

yaml
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):

jinja
# 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):

yaml
---
- 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
# 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 iteration
  • loop.last: True if this is the last iteration
  • loop.length: Total number of items in the sequence

Example Template (nginx_upstreams.conf.j2):

jinja
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:

yaml
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):

jinja
# 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:

yaml
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):

jinja
# 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:

yaml
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):

jinja
# 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:

yaml
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):

jinja
# 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:

jinja
{% for item in users | map(attribute='name') %}
User: {{ item }}
{% endfor %}

Select Filter

The select filter allows you to filter items:

jinja
{% for user in users | selectattr('is_admin', 'eq', true) %}
Admin: {{ user.name }}
{% endfor %}

Sort Filter

The sort filter sorts the items:

jinja
{% 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:

yaml
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):

jinja
# 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

  1. Keep templates readable:

    • Use proper indentation
    • Add comments where necessary
    • Break complex templates into smaller ones
  2. 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
  3. Use meaningful variable names:

    • Name your loop variables clearly
    • Use plural names for collections and singular for the loop variable
  4. Error handling:

    • Use the default filter to provide fallback values
    • Check if variables exist before using them in loops
jinja
{% for server in servers | default([]) %}
{{ server.name }}
{% else %}
# No servers configured
{% endfor %}
  1. 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:

jinja
{% for user in users -%}
{{ user.name }}
{%- endfor %}

Undefined Variables

If a variable might not exist, use the default filter:

jinja
{% for item in maybe_undefined_var | default([]) %}
{{ item }}
{% endfor %}

Complex Expressions

For complex expressions, it's better to compute them in your playbook:

yaml
- 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:

jinja
{% 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

  1. Create a template that generates an NGINX configuration with load balancing for multiple microservices.
  2. Generate a comprehensive firewall rules file using loops to iterate through a list of allowed services and ports.
  3. Build a template that creates a monitoring config file that includes different checking frequencies based on service importance.
  4. 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! :)