Что такое Jinja2 и где он живёт
Jinja2 - это язык шаблонов на Python, и Ansible использует его буквально везде, где ты видишь фигурные скобки. Связка jinja2 ansible работает по простому правилу из двух конструкций:
- {{ ... }} - подстановка выражения. То, что внутри, вычисляется и подставляется как значение. Это самое частое.
- {% ... %} - управляющая логика: циклы for, условия if, присваивания. В плейбуках встречается реже, зато в шаблонах файлов (.j2) - постоянно.
Код: Выделить всё
- name: Подстановка и логика в разных местах
hosts: web
vars:
app_port: 8080
base_url: "http://{{ ansible_facts['fqdn'] }}:{{ app_port }}"
tasks:
- name: Показать собранный URL
ansible.builtin.debug:
msg: "Приложение будет доступно по {{ base_url }}"
- name: Условие на основе факта (when - это уже Jinja2-выражение)
ansible.builtin.debug:
msg: "Памяти меньше 2 ГБ, ставим лёгкую сборку"
when: ansible_facts['memtotal_mb'] < 2048

Фильтры: основной рабочий инструмент
Фильтры (ansible filters) - это функции, которые применяются к значению через вертикальную черту, как в Unix-конвейере. Слева значение, справа фильтр: {{ value | filter }}. Их можно сцеплять. Это центральная тема урока, потому что на экзамене RHCE (EX294) фильтры нужны и в плейбуках, и в шаблонах - без них не собрать ни одного нетривиального конфига.
Самый важный из всех - default. Ansible default filter подставляет запасное значение, если переменная не определена. Это спасает плейбуки от падений и делает роли переносимыми.
Код: Выделить всё
- name: Демонстрация ключевых фильтров
hosts: localhost
gather_facts: false
vars:
names: ["alice", "BOB", "carol"]
tasks:
- name: default - запасное значение, если переменная не задана
ansible.builtin.debug:
msg: "Воркеров: {{ workers | default(4) }}"
- name: length, upper, join
ansible.builtin.debug:
msg: >-
Всего имён: {{ names | length }},
первое в верхнем регистре: {{ names[0] | upper }},
списком: {{ names | join(', ') }}
- name: lower и конкатенация через ~
ansible.builtin.debug:
msg: "{{ 'HOST-' ~ inventory_hostname | lower }}"
- name: base64 - частый кейс для секретов и cloud-init
ansible.builtin.debug:
msg: "{{ 'super-secret' | b64encode }}"
- name: to_json / to_yaml - сериализация структур
ansible.builtin.debug:
msg: "{{ {'name': 'web', 'port': 8080} | to_json }}"
- default(значение) - запасной вариант. Особый случай: default(omit) - не передавать параметр модулю вовсе, как будто его не задавали. Незаменимо, когда у модуля есть свои дефолты и ты не хочешь их затирать.
- length - длина списка, словаря или строки. Часто идёт в паре с when.
- upper / lower - регистр. Полезно для нормализации имён хостов, тегов, ключей.
- join('разделитель') - склеить список в строку. Зеркальный к нему - split.
- b64encode / b64decode - base64. Нужно для cloud-init, для Secret в Kubernetes, для значений, которые не любят спецсимволы.
- to_json / to_yaml - превратить словарь или список в текст нужного формата. Обратные - from_json и from_yaml, когда читаешь чужой вывод.
- mandatory - наоборот, жёстко требует, чтобы переменная была задана: {{ db_password | mandatory }}. Если её нет - плейбук сразу падает с понятной ошибкой, а не позже и непонятно где.
Кроме фильтров есть тесты - проверки, которые пишутся через ключевое слово is и возвращают true или false. Самый ходовой - проверка на определённость:
Код: Выделить всё
- name: Запустить только если переменная задана
ansible.builtin.service:
name: "{{ app_service }}"
state: started
when: app_service is defined
- name: Проверка совпадения по шаблону (is match)
ansible.builtin.debug:
msg: "Это RHEL-семейство"
when: ansible_facts['distribution'] is match("RedHat|CentOS|Rocky|AlmaLinux")
Логику {% %} в плейбуках напрямую почти не пишут - там для этого есть loop и when. Зато в шаблонах файлов она раскрывается полностью. Это прямая связь с уроком про шаблоны файлов (модуль ansible.builtin.template): тот же самый Jinja2, только в .j2-файле, где {% for %} и {% if %} генерируют целые куски конфига.
Код: Выделить всё
# Фрагмент шаблона nginx.conf.j2
upstream backend {
{% for host in groups['app'] %}
server {{ hostvars[host]['ansible_default_ipv4']['address'] }}:{{ app_port | default(8080) }};
{% endfor %}
}
Иногда нужно, чтобы Ansible НЕ трогал двойные скобки - например, в шаблоне конфига, где сам сервис использует синтаксис {{ }} (Prometheus, Grafana, Helm). Для этого есть блок {% raw %} ... {% endraw %}:
Код: Выделить всё
# В .j2-шаблоне: пусть {{ $labels.instance }} достанется Prometheus как есть
{% raw %}
alert: HighLoad
expr: node_load1 > 4
annotations:
summary: "Нагрузка на {{ $labels.instance }} высокая"
{% endraw %}
Типичные грабли
- Скобки в when. when: "{{ x }}" - неправильно. Пиши when: x is defined. Ansible ругается warning-ом, но многие его игнорируют.
- Голый default не спасает от пустой строки. default("x") срабатывает только когда переменная undefined. Если она задана, но пустая, нужен второй аргумент: {{ var | default("x", true) }} - тогда дефолт подставится и для пустого, и для false-значения.
- Сцепление фильтров после default(omit). Нельзя писать | default(omit) | upper - omit это не строка. Если дальше идут фильтры, используй | default(None) или omit в конце.
- Число превращается в строку. После to_json или join результат - текст. Если дальше нужна арифметика, верни тип через int или float.
- Пробелы и ~. Для склейки строк надёжнее оператор ~, чем +: плюс попытается сложить как числа и может упасть на типах.
Мини-лаба
Прогони руками, лучше через ansible localhost -m debug или маленький плейбук:
- Создай переменную-список из 5 элементов и выведи: длину (length), элементы через запятую (join), первый элемент в верхнем регистре.
- Возьми незаданную переменную и выведи её с default("резерв"). Потом задай её пустой строкой и убедись, что нужен default("резерв", true).
- Сделай set_fact, который собирает строку подключения вида user@host:port через оператор ~ и фильтр lower.
- В .j2-шаблоне с ansible.builtin.template оберни кусок с чужими {{ }} в {% raw %} и проверь, что они не вычислились.
- Поставь на критичную переменную | mandatory и убедись, что без неё плейбук падает сразу.
- Чем отличается {{ }} от {% %} и почему в when не нужны фигурные скобки?
- Как заставить default подставить значение не только для undefined, но и для пустой строки?
- Что делает default(omit) и зачем он нужен в параметрах модулей?
- Когда применяют {% raw %} и какая ловушка ждёт, если про него забыть?
Jinja2 - это клей всего Ansible: подстановки {{ }}, логика {% %}, фильтры через | и тесты через is. Ты уже умеешь делать значения безопасными (default, mandatory), приводить типы и формат (upper, join, b64encode, to_json) и ветвить логику по фактам. Этого набора хватает для большинства задач и на работе, и на EX294. Глубже в фильтры - разбор сложных структур, map, selectattr, работа со словарями - мы нырнём в уроке 30.