Templates and Files
Learn to manage configuration files and generate dynamic content using Ansible templates and file modules.
Managing configuration files is a core part of infrastructure automation. Ansible provides powerful tools for both static file deployment and dynamic configuration generation. You'll learn to use templates that adapt to different environments, manage file permissions and ownership, and organize configuration files for maintainability.
Understanding File Management in Ansible
Ansible offers several modules for file operations, each suited to different scenarios:
- copy: Deploy static files unchanged
- template: Generate files from Jinja2 templates with variable substitution
- file: Manage file properties, directories, and links
- lineinfile: Modify individual lines in existing files
- blockinfile: Manage blocks of text in files
Working with Static Files
Basic File Deployment
The copy
module handles straightforward file deployment:
---
- name: Deploy static configuration files
hosts: webservers
become: yes
tasks:
- name: Copy SSL certificate
copy:
src: files/ssl/server.crt
dest: /etc/ssl/certs/server.crt
owner: root
group: root
mode: '0644'
backup: yes
- name: Copy SSL private key
copy:
src: files/ssl/server.key
dest: /etc/ssl/private/server.key
owner: root
group: ssl-cert
mode: '0640'
backup: yes
notify: restart nginx
- name: Deploy application scripts
copy:
src: files/scripts/
dest: /opt/myapp/scripts/
owner: myapp
group: myapp
mode: '0755'
directory_mode: '0755'
Key features of the copy module:
backup: yes
creates a backup before overwritingdirectory_mode
sets permissions for directories when copying entire directories- The
src
path ending with/
copies directory contents, not the directory itself
File Content Management
Create files with specific content directly in your playbook:
tasks:
- name: Create maintenance page
copy:
content: |
<!DOCTYPE html>
<html>
<head>
<title>Maintenance Mode</title>
</head>
<body>
<h1>Site Under Maintenance</h1>
<p>We'll be back shortly. Estimated downtime: {{ maintenance_duration | default('30 minutes') }}</p>
</body>
</html>
dest: /var/www/html/maintenance.html
owner: www-data
group: www-data
mode: '0644'
Creating Dynamic Templates
Templates use the Jinja2 templating engine to generate configuration files with variable substitution, conditionals, and loops.
Basic Template Structure
Create a template file templates/nginx.conf.j2
:
# nginx configuration generated by Ansible
# Last updated: {{ ansible_date_time.iso8601 }}
user {{ nginx_user | default('www-data') }};
worker_processes {{ nginx_worker_processes | default(ansible_processor_vcpus) }};
pid /run/nginx.pid;
events {
worker_connections {{ nginx_worker_connections | default(1024) }};
use epoll;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log {{ nginx_access_log | default('/var/log/nginx/access.log') }} main;
error_log {{ nginx_error_log | default('/var/log/nginx/error.log') }} warn;
# Performance settings
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout {{ nginx_keepalive_timeout | default(65) }};
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length {{ nginx_gzip_min_length | default(1024) }};
gzip_types
text/plain
text/css
text/xml
text/javascript
application/javascript
application/xml+rss
application/json;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
Deploy the template:
tasks:
- name: Generate nginx main configuration
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
backup: yes
notify: restart nginx
Templates with Conditionals
Use Jinja2 conditionals to adapt configuration based on variables:
# SSL configuration - templates/site.conf.j2
server {
{% if ssl_enabled | default(false) %}
listen 443 ssl http2;
listen [::]:443 ssl http2;
ssl_certificate {{ ssl_cert_path | default('/etc/ssl/certs/server.crt') }};
ssl_certificate_key {{ ssl_key_path | default('/etc/ssl/private/server.key') }};
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512;
ssl_prefer_server_ciphers off;
{% else %}
listen 80;
listen [::]:80;
{% endif %}
server_name {{ server_name | default(ansible_fqdn) }};
root {{ document_root | default('/var/www/html') }};
index index.html index.php;
{% if environment == 'development' %}
# Development-specific settings
error_log /var/log/nginx/{{ server_name }}-error.log debug;
access_log /var/log/nginx/{{ server_name }}-access.log combined;
{% else %}
# Production settings
error_log /var/log/nginx/{{ server_name }}-error.log warn;
access_log /var/log/nginx/{{ server_name }}-access.log main;
{% endif %}
location / {
try_files $uri $uri/ =404;
}
{% if php_enabled | default(false) %}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php{{ php_version | default('7.4') }}-fpm.sock;
}
{% endif %}
}
{% if ssl_enabled | default(false) %}
# HTTP to HTTPS redirect
server {
listen 80;
listen [::]:80;
server_name {{ server_name | default(ansible_fqdn) }};
return 301 https://$server_name$request_uri;
}
{% endif %}
Templates with Loops
Generate repetitive configuration using loops:
# Load balancer configuration - templates/upstream.conf.j2
{% for service in load_balanced_services %}
upstream {{ service.name }}_backend {
least_conn;
{% for server in service.servers %}
server {{ server.host }}:{{ server.port }} weight={{ server.weight | default(1) }}{% if server.backup | default(false) %} backup{% endif %};
{% endfor %}
# Health check
keepalive {{ service.keepalive | default(32) }};
}
server {
listen {{ service.port | default(80) }};
server_name {{ service.domain }};
location / {
proxy_pass http://{{ service.name }}_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Timeouts
proxy_connect_timeout {{ service.connect_timeout | default('5s') }};
proxy_send_timeout {{ service.send_timeout | default('60s') }};
proxy_read_timeout {{ service.read_timeout | default('60s') }};
}
}
{% endfor %}
Use it with corresponding variables:
vars:
load_balanced_services:
- name: api
domain: api.example.com
port: 80
servers:
- host: 192.168.1.10
port: 8080
weight: 2
- host: 192.168.1.11
port: 8080
weight: 1
- host: 192.168.1.12
port: 8080
backup: true
keepalive: 64
- name: admin
domain: admin.example.com
port: 80
servers:
- host: 192.168.1.15
port: 9000
Advanced Template Techniques
Using Filters in Templates
Jinja2 filters transform data within templates:
# Database configuration - templates/database.conf.j2
[database]
host = {{ db_host | default('localhost') }}
port = {{ db_port | default(5432) }}
name = {{ db_name | upper }}
user = {{ db_user | lower }}
# Convert memory setting to MB
shared_buffers = {{ (total_memory_mb * 0.25) | int }}MB
effective_cache_size = {{ (total_memory_mb * 0.75) | int }}MB
# Generate secure random password if not provided
{% if db_password is not defined %}
password = {{ ansible_date_time.epoch | hash('sha256') | truncate(16, true, '') }}
{% else %}
password = {{ db_password }}
{% endif %}
# Conditional features based on environment
{% set features = environment_features | default([]) %}
enable_logging = {{ 'logging' in features | lower }}
enable_replication = {{ 'replication' in features | lower }}
enable_ssl = {{ 'ssl' in features | lower }}
# Generate comma-separated list of allowed hosts
allowed_hosts = {{ allowed_host_list | join(', ') }}
# Format timestamp
last_updated = {{ ansible_date_time.iso8601 | strftime('%Y-%m-%d %H:%M:%S') }}
Template Includes
Break large templates into manageable pieces:
# Main template - templates/nginx-site.conf.j2
server {
{% include 'nginx/listen.j2' %}
{% include 'nginx/server-name.j2' %}
{% include 'nginx/ssl.j2' %}
{% include 'nginx/locations.j2' %}
}
Create partial templates:
# templates/nginx/listen.j2
{% if ssl_enabled %}
listen 443 ssl http2;
listen [::]:443 ssl http2;
{% else %}
listen 80;
listen [::]:80;
{% endif %}
# templates/nginx/ssl.j2
{% if ssl_enabled %}
ssl_certificate {{ ssl_certificate }};
ssl_certificate_key {{ ssl_certificate_key }};
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512;
{% endif %}
Template Debugging
Add debugging information to templates during development:
{% if debug_mode | default(false) %}
<!-- Debug Information -->
<!-- Generated on: {{ ansible_date_time.iso8601 }} -->
<!-- Target host: {{ inventory_hostname }} -->
<!-- Environment: {{ environment | default('unknown') }} -->
<!-- Variables:
- ssl_enabled: {{ ssl_enabled | default('not set') }}
- server_name: {{ server_name | default('not set') }}
- document_root: {{ document_root | default('not set') }}
-->
{% endif %}
File Modification Techniques
Line-by-Line Modifications
Use lineinfile
for precise configuration changes:
tasks:
- name: Configure SSH settings
lineinfile:
path: /etc/ssh/sshd_config
regexp: '{{ item.regexp }}'
line: '{{ item.line }}'
backup: yes
loop:
- { regexp: '^#?PermitRootLogin', line: 'PermitRootLogin no' }
- { regexp: '^#?PasswordAuthentication', line: 'PasswordAuthentication no' }
- { regexp: '^#?Port', line: 'Port {{ ssh_port | default(22) }}' }
notify: restart sshd
- name: Add custom SSH configuration
lineinfile:
path: /etc/ssh/sshd_config
line: '{{ item }}'
insertafter: EOF
loop:
- '# Custom configuration'
- 'ClientAliveInterval 300'
- 'ClientAliveCountMax 2'
notify: restart sshd
Block Modifications
Use blockinfile
to manage sections of configuration:
tasks:
- name: Configure application settings block
blockinfile:
path: /etc/myapp/config.ini
block: |
# Application settings managed by Ansible
[performance]
max_workers = {{ max_workers | default(ansible_processor_vcpus) }}
memory_limit = {{ memory_limit | default('512M') }}
cache_enabled = {{ cache_enabled | default(true) | lower }}
[database]
host = {{ db_host }}
port = {{ db_port | default(5432) }}
pool_size = {{ db_pool_size | default(10) }}
[logging]
level = {{ log_level | default('INFO') }}
file = {{ log_file | default('/var/log/myapp/app.log') }}
marker: '# {mark} ANSIBLE MANAGED BLOCK - APPLICATION CONFIG'
backup: yes
notify: restart application
Organizing Templates and Files
Directory Structure
Organize your templates and files logically:
ansible-project/
├── templates/
│ ├── nginx/
│ │ ├── nginx.conf.j2
│ │ ├── site.conf.j2
│ │ └── upstream.conf.j2
│ ├── database/
│ │ ├── postgresql.conf.j2
│ │ └── pg_hba.conf.j2
│ └── application/
│ ├── app.conf.j2
│ └── systemd.service.j2
├── files/
│ ├── ssl/
│ │ ├── server.crt
│ │ └── server.key
│ ├── scripts/
│ │ ├── backup.sh
│ │ └── maintenance.sh
│ └── static/
│ └── maintenance.html
Template Variables Organization
Create variable files that correspond to your templates:
# group_vars/webservers.yml
nginx_config:
user: www-data
worker_processes: '{{ ansible_processor_vcpus }}'
worker_connections: 1024
keepalive_timeout: 65
gzip_enabled: true
ssl_enabled: true
site_config:
server_name: www.example.com
document_root: /var/www/html
php_enabled: true
php_version: '8.1'
Complete Example: Multi-Service Configuration
Here's a comprehensive example that demonstrates templates, files, and handlers working together:
---
- name: Configure web application stack
hosts: webservers
become: yes
vars:
app_name: ecommerce
app_version: '2.1.0'
ssl_enabled: true
php_enabled: true
cache_enabled: true
tasks:
- name: Create application directories
file:
path: '{{ item }}'
state: directory
owner: www-data
group: www-data
mode: '0755'
loop:
- /var/www/{{ app_name }}
- /var/www/{{ app_name }}/logs
- /var/www/{{ app_name }}/cache
- name: Deploy SSL certificates
copy:
src: 'files/ssl/{{ item }}'
dest: '/etc/ssl/{{ item }}'
owner: root
group: ssl-cert
mode: "{{ '0640' if 'key' in item else '0644' }}"
loop:
- certs/{{ app_name }}.crt
- private/{{ app_name }}.key
notify: restart nginx
- name: Generate nginx main configuration
template:
src: nginx/nginx.conf.j2
dest: /etc/nginx/nginx.conf
backup: yes
notify: restart nginx
- name: Generate site configuration
template:
src: nginx/site.conf.j2
dest: '/etc/nginx/sites-available/{{ app_name }}'
backup: yes
notify: restart nginx
- name: Enable site
file:
src: '/etc/nginx/sites-available/{{ app_name }}'
dest: '/etc/nginx/sites-enabled/{{ app_name }}'
state: link
notify: restart nginx
- name: Configure PHP-FPM pool
template:
src: php/pool.conf.j2
dest: '/etc/php/{{ php_version }}/fpm/pool.d/{{ app_name }}.conf'
backup: yes
notify: restart php-fpm
when: php_enabled
- name: Generate application configuration
template:
src: application/config.php.j2
dest: '/var/www/{{ app_name }}/config.php'
owner: www-data
group: www-data
mode: '0640'
- name: Deploy maintenance scripts
copy:
src: files/scripts/
dest: /opt/{{ app_name }}/scripts/
owner: root
group: root
mode: '0755'
directory_mode: '0755'
handlers:
- name: restart nginx
service:
name: nginx
state: restarted
- name: restart php-fpm
service:
name: 'php{{ php_version }}-fpm'
state: restarted
Next Steps
Templates and file management form the backbone of configuration automation. You've learned to generate dynamic configuration files, deploy static resources, and organize your automation for maintainability.
In the next section, we'll explore roles - Ansible's way of organizing automation into reusable, shareable components that can be combined to create sophisticated infrastructure automation.
The template and file management techniques you've learned here provide the foundation for creating flexible, environment-aware configuration management that scales with your infrastructure needs.
Found an issue?