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 overwriting
  • directory_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?