Шаблоны Jinja2: конфиги из переменных

Урок 13 - генерируем конфиги под каждый сервер с помощью Jinja2-шаблонов.

«Один шаблон nginx.conf.j2 - и на каждом сервере свой правильный конфиг с его портом, его доменом, его числом воркеров».

Копировать готовый файл модулем copy хорошо, когда он одинаков везде. Но конфиги почти всегда отличаются от хоста к хосту. Jinja2-шаблоны решают это: ты пишешь шаблон с подстановками, а модуль template рендерит его под каждый хост, подставляя переменные и факты.

Синтаксис Jinja2

Три ключевые конструкции: {{ переменная }} - подстановка значения; {% if %}...{% endif %} - условие; {% for %}...{% endfor %} - цикл. Пример шаблона nginx.conf.j2:

worker_processes {{ nginx_worker_processes }};

server {
    listen {{ nginx_port }};
    server_name {{ server_domain }};

    {% if ssl_enabled %}
    ssl_certificate     /etc/ssl/{{ server_domain }}.crt;
    {% endif %}

    {% for path in static_paths %}
    location {{ path }} { root /var/www; }
    {% endfor %}
}

А вот задача, которая его применяет. Обрати внимание на validate и notify:

- name: Развернуть конфиг nginx из шаблона
  ansible.builtin.template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    validate: nginx -t -c %s
  notify: Перезапустить nginx

Как работает под капотом

Модуль template читает .j2-файл, прогоняет его через движок Jinja2 со всеми переменными и фактами хоста, получает итоговый текст. Затем сравнивает его с тем, что уже лежит на хосте: если совпадает - ok и handler не сработает; если отличается - записывает новый файл, статус changed, и срабатывает notify. Параметр validate запускает проверку (nginx -t) до подмены, защищая от записи битого конфига.

Смоделируем рендеринг шаблона с подстановкой, условием и циклом средствами Python.

# Имитация рендеринга Jinja2-шаблона через format и циклы
def render(vars_):
    lines = [f"worker_processes {vars_['workers']};", "server {",
             f"    listen {vars_['port']};",
             f"    server_name {vars_['domain']};"]
    if vars_.get("ssl_enabled"):                      # {% if %}
        lines.append(f"    ssl_certificate /etc/ssl/{vars_['domain']}.crt;")
    for path in vars_.get("static_paths", []):        # {% for %}
        lines.append(f"    location {path} {{ root /var/www; }}")
    lines.append("}")
    return "\n".join(lines)

config = render({"workers": 4, "port": 443, "domain": "shop.example.com",
                 "ssl_enabled": True, "static_paths": ["/img", "/css"]})
print(config)

Попробуй сам ▶ Из набора переменных получился готовый текст конфига - ровно то, что делает template, только настоящий движок мощнее (фильтры, наследование шаблонов). Поменяй ssl_enabled на False - блок сертификата исчезнет.

Фильтры Jinja2

Фильтры преобразуют значения через |: {{ name | upper }} - в верхний регистр, {{ items | length }} - длина списка, {{ port | default(80) }} - значение по умолчанию, если переменная не задана. default - один из самых полезных: страхует от undefined.

Частые ошибки

  • Undefined-переменная в шаблоне. Если переменная не задана - рендер упадёт. Страхуйся фильтром default.
  • Путать {{ }} и {% %}. Двойные фигурные - вывод значения; фигурная с процентом - управляющая конструкция (if/for).
  • Не использовать validate. Без проверки можно записать синтаксически битый конфиг и уронить сервис при рестарте.

Best practices

  • Для любых конфигов с переменной частью - template, а не copy.
  • Связывай template с handler через notify: перезапуск только при реальном изменении.
  • Используй validate для критичных конфигов (nginx, sshd, sudoers) и default для необязательных переменных.

В реальной работе

Jinja2-шаблоны - это место, где переменные, факты и логика сходятся в готовый артефакт. На практике в шаблонах активно используют фильтры: to_nice_json и to_nice_yaml для генерации структурированных конфигов, join для сборки списков серверов в строку, regex_replace для преобразований. Частый приём - генерация конфига балансировщика, который перечисляет все бэкенды: цикл for по группе web через groups['web'] и hostvars вытаскивает адреса всех веб-серверов и вставляет их в upstream. Так конфиг автоматически обновляется при добавлении сервера в группу - тебе не нужно править его руками, достаточно изменить инвентарь и прогнать playbook.

Итоги

Jinja2-шаблоны через модуль template генерируют конфиги под каждый хост, подставляя переменные и факты, с условиями и циклами. validate защищает от битых конфигов, notify перезапускает сервис только при изменении. Это вершина связки «переменные + факты + шаблоны». Дальше упакуем всё в роли.

Проверьте себя
1. Что делает модуль template по сравнению с copy?
AНичего, это синонимы
BРендерит Jinja2-шаблон с переменными и фактами хоста, генерируя файл под каждый сервер
CТолько удаляет файлы
DКопирует каталоги целиком
2. Зачем нужен параметр validate в модуле template?
AУскоряет рендеринг
BПроверяет сгенерированный конфиг (например, nginx -t) до подмены, защищая от записи битого файла
CШифрует файл
DВключает цикл for в шаблоне