Работа с шаблонами (Jinja2)

TL;DR: template модуль + файл .j2 = динамическая генерация конфигов. {{ переменная }} — подстановка, {% if %} — условия, {% for %} — циклы. Шаблон обрабатывается на Control Node, результат копируется на сервер.

Модуль template берёт файл-шаблон (.j2), обрабатывает его Jinja2, и копирует результат на сервер.

Создание шаблона

Файл templates/nginx.conf.j2:

# Managed by Ansible — DO NOT EDIT MANUALLY
server {
    listen {{ nginx_port | default(80) }};
    server_name {{ ansible_fqdn }};
 
    root {{ web_root | default('/var/www/html') }};
    index index.html;
 
    # Условный блок
    {% if enable_ssl | default(false) %}
    listen 443 ssl;
    ssl_certificate     /etc/ssl/certs/fullchain.pem;
    ssl_certificate_key /etc/ssl/private/privkey.pem;
    {% endif %}
 
    # Цикл по upstream-серверам
    {% for backend in app_backends | default([]) %}
    location {{ backend.path }} {
        proxy_pass {{ backend.url }};
        proxy_set_header Host $host;
    }
    {% endfor %}
 
    # Комментарий Jinja2 (не попадёт в файл)
    {# Этот блок для будущего расширения #}
}

Использование в Playbook

- name: Настроить Nginx
  ansible.builtin.template:
    src: nginx.conf.j2
    dest: /etc/nginx/sites-available/default
    owner: root
    group: root
    mode: '0644'
    validate: "nginx -t -c %s"       # валидация перед применением
    backup: yes                       # сохранить предыдущую версию
  notify: Reload Nginx

Переменные для шаблона

# group_vars/webservers.yml
nginx_port: 8080
enable_ssl: true
app_backends:
  - path: /api
    url: http://localhost:3000
  - path: /admin
    url: http://localhost:8000

Синтаксис Jinja2

Подстановки

{{ variable }}                         {# простая подстановка #}
{{ variable | default('fallback') }}   {# значение по умолчанию #}
{{ variable | upper }}                 {# фильтр: в верхний регистр #}
{{ list_var | join(', ') }}            {# склеить список #}
{{ password | hash('sha512') }}        {# хеш пароля #}

Условия

{% if env == 'production' %}
worker_processes auto;
{% elif env == 'staging' %}
worker_processes 2;
{% else %}
worker_processes 1;
{% endif %}

Циклы

{% for user in users %}
{{ user.name }}:{{ user.uid }}:{{ user.shell | default('/bin/bash') }}
{% endfor %}
 
{# С индексом #}
{% for item in items %}
server_{{ loop.index }} = {{ item }}
{% endfor %}
 
{# loop.index  — счётчик с 1 #}
{# loop.index0 — счётчик с 0 #}
{# loop.first  — первая итерация? #}
{# loop.last   — последняя итерация? #}

Управление пробелами

{# По умолчанию Jinja2 оставляет пустые строки вместо блоков #}
{# Используй - для обрезки пробелов #}
 
{% for server in servers -%}
server {{ server }};
{%- endfor %}

Практические примеры

systemd unit

# templates/app.service.j2
[Unit]
Description={{ app_name }}
After=network.target
 
[Service]
User={{ app_user }}
WorkingDirectory={{ app_dir }}
ExecStart={{ app_dir }}/venv/bin/gunicorn \
  --workers {{ ansible_processor_vcpus * 2 + 1 }} \
  --bind 0.0.0.0:{{ app_port }} \
  {{ app_module }}:app
Restart=always
Environment=NODE_ENV={{ env }}
 
[Install]
WantedBy=multi-user.target

SSH config

# templates/sshd_config.j2
Port {{ ssh_port | default(22) }}
PermitRootLogin {{ 'yes' if allow_root_login | default(false) else 'no' }}
PasswordAuthentication {{ 'yes' if allow_password_auth | default(false) else 'no' }}
MaxAuthTries 3
{% for key in ssh_allowed_keys | default([]) %}
AuthorizedKeysFile {{ key }}
{% endfor %}

Типичные ошибки

ОшибкаСимптомРешение
template без notifyКонфиг обновлён, сервис не перезапущенВсегда добавлять notify для сервисов
Нет validateНевалидный конфиг ломает сервисvalidate: "nginx -t -c %s" перед применением
Пробелы и пустые строкиЛишние пустые строки в конфигеИспользовать -%} для обрезки whitespace
Переменная не определенаAnsibleUndefinedVariableИспользовать | default('value') или определить в defaults