Ansible Template Inheritance
Introduction
Template inheritance is a powerful feature in Ansible that allows you to create reusable, modular templates. This concept leverages Jinja2's inheritance capabilities to help you maintain consistent structure across multiple templates while reducing duplication.
In this guide, we'll explore how to use template inheritance in Ansible, enabling you to create base templates that can be extended and customized for specific use cases.
Understanding Template Inheritance
Template inheritance works similarly to object-oriented programming inheritance. You create a "base" or "parent" template that defines the overall structure, and then "child" templates can:
- Inherit this structure
- Override specific sections
- Add new content where needed
This approach promotes:
- DRY (Don't Repeat Yourself) principles
- Consistency across configuration files
- Easier maintenance by centralizing common elements
Basic Concepts of Jinja2 Template Inheritance
Before we dive into Ansible-specific examples, let's understand the core concepts of Jinja2 template inheritance:
Blocks and Block Inheritance
The inheritance system uses three primary directives:
{% block name %}
: Defines a section that child templates can override{% extends "template.j2" %}
: Indicates that this template inherits from another{{ super() }}
: References the content of the parent block (useful when extending rather than completely replacing)
Creating a Base Template
Let's start by creating a base template for a web server configuration:
{# base_nginx.conf.j2 #}
server {
listen 80;
server_name {{ server_name }};
{% block server_config %}
# Default server configuration
root /var/www/html;
index index.html;
{% endblock %}
{% block logging %}
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
{% endblock %}
{% block security %}
# Default security settings
{% endblock %}
}
This base template defines the structure with three blocks that can be customized:
server_config
: For basic server configurationlogging
: For log settingssecurity
: For security-related configurations
Creating Child Templates
Now, let's create a child template that extends our base:
{# php_nginx.conf.j2 #}
{% extends "base_nginx.conf.j2" %}
{% block server_config %}
# PHP application configuration
root /var/www/php_app;
index index.php index.html;
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
}
{% endblock %}
{% block security %}
# PHP-specific security settings
location ~ /\.ht {
deny all;
}
{% endblock %}
Note how this template:
- Extends the base template
- Overrides the
server_config
block with PHP-specific settings - Adds security settings to the
security
block - Inherits the
logging
block without changes
Using the super()
Function
Sometimes you want to extend a block rather than completely replace it. The super()
function allows you to include the parent block's content:
{# secure_nginx.conf.j2 #}
{% extends "base_nginx.conf.j2" %}
{% block security %}
{{ super() }}
# Additional security headers
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header X-XSS-Protection "1; mode=block";
{% endblock %}
In this example, we're keeping the default security settings and adding more security headers.
Practical Example: Implementing Template Inheritance in Ansible
Let's see how to implement template inheritance in an Ansible playbook:
Directory Structure
First, set up your directory structure:
ansible_project/
├── playbooks/
│ └── configure_webservers.yml
├── templates/
│ ├── base_nginx.conf.j2
│ ├── php_nginx.conf.j2
│ └── secure_nginx.conf.j2
├── inventory.ini
└── ansible.cfg
Ansible Playbook
Now, create a playbook that uses these templates:
# configure_webservers.yml
---
- name: Configure web servers
hosts: webservers
become: yes
vars:
server_name: example.com
tasks:
- name: Create Nginx configuration for PHP servers
template:
src: php_nginx.conf.j2
dest: /etc/nginx/sites-available/php_app.conf
when: inventory_hostname in groups['php_servers']
notify: Restart Nginx
- name: Create Nginx configuration for secure servers
template:
src: secure_nginx.conf.j2
dest: /etc/nginx/sites-available/secure_app.conf
when: inventory_hostname in groups['secure_servers']
notify: Restart Nginx
handlers:
- name: Restart Nginx
service:
name: nginx
state: restarted
Inventory File Example
# inventory.ini
[webservers]
web1.example.com
web2.example.com
web3.example.com
[php_servers]
web1.example.com
[secure_servers]
web2.example.com
web3.example.com
Advanced Template Inheritance Techniques
Nested Inheritance
You can create multiple levels of inheritance for more complex configurations:
Base Template → Framework-specific Template → Application-specific Template
For example:
{# framework_nginx.conf.j2 #}
{% extends "base_nginx.conf.j2" %}
{% block server_config %}
# Framework common settings
root /var/www/framework;
index index.php index.html;
{% endblock %}
Then:
{# laravel_nginx.conf.j2 #}
{% extends "framework_nginx.conf.j2" %}
{% block server_config %}
{{ super() }}
# Laravel specific configuration
location / {
try_files $uri $uri/ /index.php?$query_string;
}
{% endblock %}
Conditional Blocks
You can use Ansible variables within your templates to conditionally include content:
{% block caching %}
{% if enable_caching | default(false) %}
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
}
{% endif %}
{% endblock %}
Real-World Application: Multi-Environment Configuration
A common use case for template inheritance is managing configurations across different environments (development, staging, production):
Base Template
{# base_app_config.j2 #}
application:
name: "{{ app_name }}"
{% block database %}
database:
host: localhost
name: app_db
user: app_user
{% endblock %}
{% block features %}
features:
cache: true
logging: true
{% endblock %}
{% block security_settings %}
security:
encryption: aes-256
{% endblock %}
Production Environment Template
{# production_app_config.j2 #}
{% extends "base_app_config.j2" %}
{% block database %}
database:
host: "{{ prod_db_host }}"
name: "{{ prod_db_name }}"
user: "{{ prod_db_user }}"
connection_pool: 20
read_replica: true
{% endblock %}
{% block security_settings %}
security:
encryption: aes-256
ssl: true
headers:
strict_transport: true
content_security: "default-src 'self'"
{% endblock %}
Development Environment Template
{# development_app_config.j2 #}
{% extends "base_app_config.j2" %}
{% block features %}
{{ super() }}
debug: true
profiling: true
{% endblock %}
Implementation Flow
Let's visualize the template inheritance flow:
Best Practices for Template Inheritance
-
Keep the base template simple: Include only the structure and blocks that are truly common.
-
Use meaningful block names: Choose descriptive names that clearly indicate the purpose of each section.
-
Document your templates: Add comments explaining the purpose of blocks and expected variables.
-
Be consistent with indentation: Maintain consistent indentation to make templates readable.
-
Use default values: Provide sensible defaults for variables to make templates more robust.
-
Test your templates: Verify the generated output for each environment before deploying.
-
Version control: Keep your templates in version control alongside your Ansible code.
Common Pitfalls and Troubleshooting
Template Not Found Errors
If you encounter "template not found" errors, check:
- The path in the
extends
statement is relative to the location where Ansible runs - The template search path in your
ansible.cfg
You might need to use the full path:
{% extends "/path/to/templates/base_nginx.conf.j2" %}
Or configure the template path in ansible.cfg
:
[defaults]
template_path = /path/to/templates
Unexpected Output
If the generated configuration isn't what you expect:
- Use Ansible's
debug
module to view the processed template - Check for whitespace issues in your templates
- Verify block names match exactly between parent and child templates
Example debug task:
- name: Debug template output
debug:
msg: "{{ lookup('template', 'php_nginx.conf.j2') }}"
Summary
Template inheritance in Ansible offers a powerful way to create modular, maintainable configuration templates. By leveraging Jinja2's inheritance capabilities, you can:
- Create base templates with common structures
- Override specific sections where needed
- Extend existing blocks with additional content
- Keep your configurations DRY and consistent across environments
This approach is particularly valuable for managing complex configurations across multiple environments or for different variations of similar services.
Exercises
-
Create a base template for an Apache web server configuration with blocks for virtual hosts, logging, and security.
-
Extend your base template to create configurations for a static website and a dynamic application.
-
Implement a multi-tier template inheritance structure for a database configuration that handles development, staging, and production environments.
-
Create a template for a load balancer configuration that uses conditional blocks based on Ansible variables.
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)