Jinja2 продвинуто: фильтры, циклы, макросы и lookup

Рейтинг: 66.7% · 13 голосов
Подробный курс по Ansible с прицелом на экзамен RHCE EX294: установка и инвентарь, плейбуки, переменные и факты, Vault, циклы и условия, шаблоны Jinja2, роли и коллекции, Execution Environments и ansible-navigator, управление системами (диски, LVM, cron, SELinux, firewalld). Примеры на RHEL/Fedora. Актуально на 2026.
Ответить
Аватара пользователя
Maksim_DevOps
Сообщения: 47
Зарегистрирован: 11 май 2026, 05:31

Jinja2 продвинуто: фильтры, циклы, макросы и lookup

Сообщение Maksim_DevOps »

Оглавление курса (47)
  1. Что такое Ansible и зачем он нужен: автоматизация без агентов
  2. Сертификация RHCE и экзамен EX294: что внутри и как готовиться
  3. Архитектура Ansible: control node, узлы, модули и плагины
  4. Установка Ansible на control node: dnf, pip и версии ansible-core
  5. Подготовка управляемых узлов: SSH, пользователь и sudo
  6. Инвентарь Ansible: hosts, группы и переменные
  7. Настройка Ansible: файл ansible.cfg и приоритеты конфигурации
  8. Host patterns и инструменты командной строки Ansible
  9. Ad hoc команды Ansible: быстрые задачи без плейбука
  10. Ansible playbook: что это такое и как устроен
  11. Структура плейбука: play, task, модули и переменные
  12. Запуск плейбуков: проверка, теги, ограничения и отладка
  13. Параллелизм и порядок выполнения: forks, serial, strategy
  14. Факты Ansible: сбор информации об узлах
  15. Переменные Ansible: типы, объявление и приоритет
  16. Регистрация результатов и специальные переменные
  17. Ansible Vault: шифрование паролей и секретов
  18. Организация инвентаря: group_vars, host_vars и вложенные группы
  19. Динамический инвентарь и инвентарные плагины
  20. Циклы в Ansible: loop, списки и словари
  21. Повтор задачи до условия: until, retries и delay
  22. Условия в Ansible: директива when и логика
  23. Jinja2 в Ansible: выражения, фильтры и подстановки
  24. Обработчики Ansible: handlers, notify и flush_handlers
  25. Обработка ошибок: failed_when, changed_when и ignore_errors
  26. Блоки в Ansible: block, rescue и always
  27. Управление файлами: модули file, copy, fetch и stat
  28. Архивы и сборка файлов: archive, unarchive, assemble
  29. Точечное редактирование файлов: lineinfile и blockinfile
  30. Шаблоны Jinja2: модуль template и динамические конфиги
  31. Jinja2 продвинуто: фильтры, циклы, макросы и lookup (вы здесь)
  32. Модули Ansible: ansible-doc и поиск нужного модуля
  33. Разработка собственного модуля Ansible на Python
  34. Роли Ansible и Ansible Galaxy: переиспользование
  35. Разработка роли Ansible: создание с нуля
  36. Зависимости ролей и requirements.yml
  37. Коллекции Ansible: ansible.posix, community и своя коллекция
  38. Execution Environments: контейнеры для запуска Ansible
  39. Сборка Execution Environment с ansible-builder
  40. ansible-navigator: запуск плейбуков в Execution Environment
  41. Управление SSH-ключами через Ansible: authorized_key
  42. Управление дисками: filesystem, mount и parted
  43. LVM через Ansible: тома, группы и расширение
  44. Планировщик задач: cron и systemd timers через Ansible
  45. Безопасность: hardening SSH и репозитории dnf/yum
  46. SELinux через Ansible: булевы, контексты и порты
  47. Сквозной проект и подготовка к экзамену EX294
Ты уже умеешь подставлять переменную в шаблон через двойные фигурные скобки. Но как только конфиг перестаёт быть плоским - появляется список бэкендов, набор виртуальных хостов, словарь лимитов - простой подстановки не хватает. Начинается копипаста: десять почти одинаковых блоков, которые потом руками синхронизируешь и неизбежно где-то забываешь. Этот урок - про то, как Jinja2 убирает копипасту из конфигов. Циклы и условия прямо в шаблоне, повторно используемые макросы, умные фильтры для преобразования данных и плагины lookup, которые тащат данные снаружи прямо в момент рендера. Это ровно тот слой, который отличает человека, копирующего чужие роли, от инженера, который пишет свои.

Управляющие конструкции в шаблоне: for, if, set

Двойные фигурные скобки {{ }} выводят значение. Фигурная скобка со знаком процента {% %} - это управляющая конструкция, она ничего не печатает сама, а управляет потоком. Базовый пример - сгенерировать секцию upstream для nginx из списка серверов. Переменная в груповых vars:

Код: Выделить всё

backends:
  - { host: 10.0.0.11, port: 8080, weight: 5 }
  - { host: 10.0.0.12, port: 8080, weight: 3 }
  - { host: 10.0.0.13, port: 8080, backup: true }
Шаблон upstream.conf.j2:

Код: Выделить всё

upstream app {
{% for b in backends %}
    server {{ b.host }}:{{ b.port | default(8080) }} weight={{ b.weight | default(1) }}{% if b.backup | default(false) %} backup{% endif %};
{% endfor %}
}
Здесь сразу три вещи: цикл {% for %}, условие {% if %} внутри строки и фильтр default для необязательных полей. Добавили четвёртый сервер в переменную - конфиг перегенерировался сам, синхронизировать руками нечего.

Внутри цикла Jinja2 даёт служебную переменную loop: loop.index (нумерация с 1), loop.index0 (с 0), loop.first, loop.last. Удобно, когда между элементами нужен разделитель, а после последнего - нет:

Код: Выделить всё

{% for srv in dns_servers %}nameserver {{ srv }}
{% endfor %}
{# либо в одну строку через join, см. ниже #}
Переменную можно завести прямо в шаблоне через {% set %} - это удобно, чтобы не тащить промежуточный расчёт в плейбук:

Код: Выделить всё

{% set workers = (ansible_processor_vcpus | default(2)) * 2 %}
worker_processes {{ workers }};
Комментарий в Jinja2 - это {# ... #}, в отрендеренный файл он не попадает. И сразу про типичную боль с пустыми строками: управляющие теги оставляют после себя переводы строк. Лечится минусом - {%- ... -%} обрезает пробельные символы слева/справа, либо настройкой trim_blocks. Для модуля ansible.builtin.template trim_blocks включён по умолчанию, lstrip_blocks - нет.

Изображение

Ansible jinja2 фильтры, которые реально пригодятся

Фильтр - это преобразование значения через вертикальную черту: {{ value | filter }}. В Ansible filters делятся на стандартные Jinja2 и добавленные самим Ansible. Разберём те, что встречаются в боевых ролях постоянно.

default - подставить значение, если переменная не определена. Хитрость: пустая строка или false по умолчанию НЕ считаются "не определено". Чтобы подменять и их (например, пустую строку), добавь второй аргумент true: {{ myvar | default("fallback", true) }}.

ternary - тернарный оператор без if: {{ (state == "present") | ternary("enabled", "disabled") }}. Читается как "условие ? а : б".

map - применить фильтр или вытащить атрибут у каждого элемента списка. Собрать все host из списка словарей:

Код: Выделить всё

{{ backends | map(attribute='host') | list }}
{# -> ['10.0.0.11', '10.0.0.12', '10.0.0.13'] #}
{{ dns_servers | map('regex_replace', '$', ' # managed') | list }}
select / reject (и selectattr / rejectattr) - фильтрация списка по условию. Оставить только бэкенды-бэкапы или, наоборот, только основные:

Код: Выделить всё

{{ backends | rejectattr('backup', 'defined') | map(attribute='host') | list }}
{{ users | selectattr('shell', 'equalto', '/bin/bash') | list }}
ansible regex_replace - замена по регулярному выражению, с поддержкой обратных ссылок \\1, \\2. Классика - выдрать домен из FQDN или нормализовать имя:

Код: Выделить всё

{{ "web01.dc.example.com" | regex_replace('^([^.]+)\\..*$', '\\1') }}
{# -> web01 #}
{{ iface | regex_replace('[^a-zA-Z0-9]', '_') }}
Тонкость про ansible regex_replace: внутри YAML обратный слеш надо экранировать, поэтому в плейбуке пиши \\1, а не \1. И помни - это поиск по всей строке, без якорей ^ и $ заменяться будет каждое вхождение.

combine - слить словари (правый перекрывает левый). Базовые настройки плюс оверрайды для конкретного хоста:

Код: Выделить всё

{{ defaults_cfg | combine(host_overrides, recursive=true) }}
dict2items / items2dict - превратить словарь в список пар key/value, чтобы по нему пройтись циклом. Незаменимо, когда данные приходят как map, а итерировать надо парами:

Код: Выделить всё

{% for item in limits | dict2items %}
{{ item.key }} hard nofile {{ item.value }}
{% endfor %}
ipaddr - работа с IP и сетями. Важно: это НЕ builtin, фильтр живёт в коллекции ansible.utils, и без неё в свежем ansible-core он не найдётся. Ставится через ansible-galaxy collection install ansible.utils, в шаблоне можно звать полным именем ansible.utils.ipaddr. Примеры: {{ "10.0.0.5/24" | ansible.utils.ipaddr('network') }} вернёт 10.0.0.0, а ('netmask') - 255.255.255.0.

Фильтры можно и нужно сцеплять в конвейер: map -> select -> sort -> unique -> join. Длинная цепочка читается как pipeline в shell и заменяет десяток строк ручной логики.

Макросы, include и блоки: убираем повторы

Если внутри шаблона повторяется один и тот же кусок разметки - его выносят в макрос. Это как функция: объявил один раз, вызываешь с аргументами. Jinja2 макросы особенно хороши, когда генерируешь однотипные секции конфига.

Код: Выделить всё

{% macro vhost(name, root, port=80) %}
server {
    listen {{ port }};
    server_name {{ name }};
    root {{ root }};
}
{% endmacro %}

{% for site in sites %}
{{ vhost(site.name, site.root, site.port | default(80)) }}
{% endfor %}
Макрос можно вынести в отдельный файл (например, macros.j2) и подключить через {% import %}, тогда он переиспользуется в нескольких шаблонах:

Код: Выделить всё

{% import 'macros.j2' as m %}
{{ m.vhost('api.local', '/srv/api') }}
Рядом стоят ещё две конструкции. {% include 'fragment.j2' %} тупо вставляет содержимое другого шаблона в текущий (например, общую "шапку" с предупреждением "файл управляется Ansible, руками не трогать"). А {% block name %}...{% endblock %} - это точки расширения для наследования шаблонов через {% extends %}; в типовых ролях нужно реже, чем макросы, но знать про него стоит.

Плагины lookup: данные снаружи в момент рендера

Фильтр преобразует то, что у тебя уже есть. А ansible lookup - это плагин, который ИДЁТ за данными наружу: читает файл, переменную окружения, выполняет команду, генерирует пароль. Запускается lookup на управляющей машине (там, где крутится ansible), а не на целевом хосте - это частый источник путаницы. Синтаксис: {{ lookup('plugin', 'аргумент') }}.

Самые ходовые плагины ansible lookup:
  • file - вернуть содержимое файла: {{ lookup('ansible.builtin.file', '/etc/hostname') }}.
  • env - значение переменной окружения управляющей машины: {{ lookup('ansible.builtin.env', 'HOME') }}.
  • pipe - выполнить команду локально и взять её stdout: {{ lookup('ansible.builtin.pipe', 'git rev-parse --short HEAD') }}.
  • password - сгенерировать пароль и закешировать его в файл, чтобы при повторном прогоне он не менялся (идемпотентность): {{ lookup('ansible.builtin.password', '/tmp/secrets/db chars=ascii_letters,digits length=20') }}.
  • template - отрендерить вложенный j2-шаблон в строку и вернуть результат (удобно для генерации фрагмента "на лету").
Пример в задаче плейбука - сгенерировать ключ и положить его в файл за один проход:

Код: Выделить всё

- name: Сгенерировать пароль приложения
  ansible.builtin.copy:
    content: "{{ lookup('ansible.builtin.password', '/srv/creds/app length=24') }}"
    dest: /etc/app/secret
    owner: app
    mode: '0400'
  # после файла можно дёрнуть restorecon/SELinux-контекст при необходимости
Когда lookup, а когда переменная? Простое правило. Если данные статичны и известны заранее - это обычная переменная в vars/group_vars, так нагляднее и она версионируется. lookup бери, когда значение нужно ВЫЧИСЛИТЬ или ВЗЯТЬ извне в момент запуска: прочитать файл, спросить окружение, сходить за секретом. И помни про разницу lookup и query: query() (или with_ в старом стиле) всегда возвращает список и используется для циклов loop, а lookup по умолчанию склеивает результат в строку.

Маленькая кросс-дистрибутивная пометка. Шаблоны и lookup от дистрибутива не зависят - это логика на управляющей машине. А вот что ты в них подставляешь, отличается: путь к пакетному менеджеру (dnf на RHEL/Fedora/RED OS против apt на Ubuntu/Debian/Astra), имена сервисов systemd, дефолтные shell у пользователей. Эту разницу обычно прячут в переменные через факт ansible_os_family и фильтр default, а сам шаблон остаётся один.

Типичные грабли
  • Лишние пустые строки в отрендеренном файле из-за переводов строк после {% ... %}. Лечи минусом {%- -%} или включи lstrip_blocks/trim_blocks в шапке шаблона.
  • default не срабатывает на пустой строке и false - они "определены". Нужен default("x", true).
  • Обратные слеши в regex_replace внутри YAML съедаются. Пиши \\1 для обратной ссылки, бери regex в одинарные кавычки.
  • ipaddr "не найден" - забыли установить коллекцию ansible.utils. Это не часть ansible-core.
  • lookup ушёл за файлом, а файла нет на УПРАВЛЯЮЩЕЙ машине, и ты ищешь его на целевом хосте. lookup всегда читает локально.
  • map(attribute=...) на списке, где у части элементов нет этого ключа, падает. Сначала отфильтруй selectattr('key', 'defined').
Мини-лаба

Руками, без подсматривания:
  • Опиши в group_vars список из 3 бэкендов (host, port, опциональный weight) и сгенерируй upstream-секцию шаблоном с циклом for и фильтром default.
  • Добавь четвёртый бэкенд с флагом backup: true и выведи его с суффиксом backup через {% if %}.
  • Напиши макрос vhost(name, root, port=80) и сгенерируй им два виртуальных хоста из списка sites.
  • Через lookup('ansible.builtin.pipe', ...) подставь в файл текущий git-хеш каталога, а через lookup password сгенерируй пароль с кешированием в файл - прогони плейбук дважды и убедись, что пароль не поменялся (идемпотентность).
  • Возьми список FQDN и фильтром regex_replace оставь только короткое имя хоста.
Контрольные вопросы
  • Чем {{ }} отличается от {% %} в Jinja2 и зачем нужен минус в {%- -%}?
  • Почему {{ x | default("y") }} не подставит "y", когда x - пустая строка, и как это обойти?
  • В чём принципиальная разница между фильтром и lookup, и на какой машине выполняется lookup?
  • Какой фильтр соберёт из списка словарей значения одного поля, а какой превратит словарь в список пар для цикла?
Итог

Jinja2 в Ansible - это не просто подстановка значений, а полноценный язык генерации конфигов: циклы и условия убирают копипасту, set прячет вычисления внутри шаблона, макросы и include дают переиспользование, а богатый набор Ansible filters (map, select/reject, combine, dict2items, regex_replace, ipaddr) превращает сырые данные в готовый конфиг одной цепочкой. lookup добавляет к этому подтягивание данных снаружи. На EX294 ровно это и проверяют: умеешь ли ты не вписать значение руками, а описать данные и сгенерировать из них конфиг. Кто уверенно гоняет for/if и фильтры в шаблонах - тот пишет роли, которые не разваливаются при добавлении пятого сервера.
👍7 ❤️3 🔥 😄 🤔2
Аватара пользователя
dhasty
Сообщения: 1
Зарегистрирован: 12 май 2026, 15:47

Re: Jinja2 продвинуто: фильтры, циклы, макросы и lookup

Сообщение dhasty »

Вот про lookup и control node прям спасибо. Сидел и не мог понять почему pipe с git-хешем берёт мой ноут а не сервер, думал баг. Оказалось так и задумано.
👍1 ❤️2 🔥 😄 🤔
Аватара пользователя
dfggfd
Сообщения: 1
Зарегистрирован: 15 май 2026, 00:00

Re: Jinja2 продвинуто: фильтры, циклы, макросы и lookup

Сообщение dfggfd »

Грабли с regex_replace и двойным слешем словил буквально вчера на проде, конфиг не рендерился. Жаль урок не вышел на день раньше ) А по selectattr defined вопрос - его обязательно ставить перед каждым map с attribute или только когда ключ может отсутствовать?
👍1 ❤️3 🔥 😄 🤔
Ответить
← Предыдущая глава
Шаблоны Jinja2: модуль template и динамические конфиги
Следующая глава →
Модули Ansible: ansible-doc и поиск нужного модуля

Все главы курса «Ansible: автоматизация и подготовка к RHCE (EX294)»

Поделиться темой: ✈ Telegram VK

Вернуться в «Ansible: автоматизация и подготовка к RHCE (EX294)»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость