Обработчики, условия и циклы

Урок 10 - добавляем логику: перезапуск по событию, ветвления и циклы.

«Перезапускай nginx не на каждом прогоне, а только когда реально поменялся его конфиг».

Базовых задач хватает не всегда. Часто нужно: перезапустить сервис, но только если изменился его конфиг; выполнить задачу при условии; повторить задачу для списка элементов. Для этого есть handlers, when и loop.

Handlers и notify

Handler - особая задача, которая выполняется только если её «вызвали» через notify, и только если вызвавшая задача завершилась как changed. Классика - перезапуск сервиса при изменении конфига.

tasks:
  - name: Развернуть конфиг nginx
    ansible.builtin.template:
      src: nginx.conf.j2
      dest: /etc/nginx/nginx.conf
    notify: Перезапустить nginx

handlers:
  - name: Перезапустить nginx
    ansible.builtin.service:
      name: nginx
      state: restarted

Если шаблон не изменился - задача ok, handler не запустится. Если изменился - changed, и в конце плея nginx перезапустится. Причём даже если уведомление пришло от пяти задач, handler выполнится один раз.

Условие when

- name: Поставить firewalld только на RedHat-системах
  ansible.builtin.dnf:
    name: firewalld
    state: present
  when: ansible_facts['os_family'] == "RedHat"

Выражение в when - это Jinja2 без двойных скобок. Если оно ложно - задача пропускается (skipping).

Циклы loop

- name: Создать несколько пользователей
  ansible.builtin.user:
    name: "{{ item }}"
    state: present
  loop:
    - alice
    - bob
    - carol

Переменная item по очереди принимает каждое значение списка. Если совместить loop и when, условие проверяется отдельно для каждого элемента.

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

Notify не запускает handler сразу - он ставит его в очередь. Очередь handler'ов выполняется в конце плея, причём каждый handler не более одного раза, в порядке их объявления. Это сделано осознанно: незачем перезапускать сервис посреди настройки - лучше один раз в конце, когда все конфиги на месте.

   task A (changed) --notify--> [очередь: restart nginx]
   task B (changed) --notify--> [очередь: restart nginx]  (дубль убран)
   task C (ok)      ----------- ничего
                                       |
                       конец плея -> выполнить очередь -> restart nginx (1 раз)
# Модель notify: собираем уникальные handler'ы, выполняем в конце плея
notified = []        # порядок объявления важен, дубли убираем
def notify(handler, changed):
    if changed and handler not in notified:
        notified.append(handler)

notify("restart nginx", changed=True)   # task A
notify("restart nginx", changed=True)   # task B -> дубль, не добавится
notify("reload sysctl", changed=False)  # не changed -> пропуск

print("В конце плея выполнить:", notified)  # ['restart nginx']

Попробуй сам ▶ Несмотря на два уведомления о рестарте nginx, в очереди он один. А reload sysctl не попал, потому что вызвавшая задача была ok, а не changed.

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

  • Имя в notify не совпадает с name handler'а. Они связываются по точному совпадению строки - опечатка и handler молча не сработает.
  • Двойные скобки в when. В when Jinja-выражение пишется без {{ }}.
  • Ждать, что handler сработает посреди плея. Он выполняется в конце (или раньше, если явно вызвать meta: flush_handlers).

Best practices

  • Перезапуск сервисов - всегда через handler с notify, а не как обычную задачу: так избегаешь лишних рестартов.
  • В when опирайся на факты (ansible_facts) - о них следующий раздел.
  • Для повторяющихся однотипных задач используй loop вместо копипасты.

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

Связка template + notify + handler - это типовой паттерн настройки почти любого сервиса: развернул конфиг из шаблона, и если он реально изменился - перезапустил сервис; не изменился - не трогаешь работающий процесс. Так избегают лишних рестартов, которые на проде означают кратковременные обрывы соединений. Циклы же часто комбинируют со словарями, а не только списками строк: loop по списку словарей позволяет одной задачей создать несколько пользователей, у каждого со своими группами и shell. А когда логика становится сложной, задачи группируют в блоки (block) с общим условием и обработкой ошибок через rescue - это уже структурное программирование внутри playbook.

Итоги

Handlers через notify дают перезапуск по событию (только при changed, один раз в конце плея), when добавляет условия, loop - повторение по списку. Это превращает простые playbook'и в гибкую логику. Дальше - переменные, факты и шаблоны.

Проверьте себя
1. Когда выполнится handler, вызванный через notify?
AСразу же после notify
BВ конце плея, и только если вызвавшая задача была changed; один раз даже при многих notify
CПеред началом плея
DНа каждом хосте по числу notify
2. Как правильно записать условие в when?
Awhen: {{ os_family == 'RedHat' }}
Bwhen: ansible_facts['os_family'] == "RedHat" (Jinja без двойных скобок)
Cwhen: [os_family: RedHat]
Dwhen: $os_family == RedHat