Решение - обработчики. Это та самая фишка, ради которой Ansible придумывал отдельный механизм: выполнять реакцию только тогда, когда реально что-то поменялось. Поменялся конфиг - перезапустили. Не поменялся - тишина. В этом уроке разберём, как работают ansible handlers, чем notify отличается от обычного вызова, зачем нужен listen и в каких случаях спасает flush_handlers.
Что такое обработчики ansible и как работает notify
Обработчик (handler) - это обычная задача, которая лежит в отдельном разделе handlers и по умолчанию не выполняется. Она ждёт уведомления. Уведомление шлёт другая задача через директиву notify, но - и это ключевой момент - только если сама задача вернула статус changed. Если задача отработала с результатом ok (ничего не изменилось), notify молчит, и обработчик не дёргается.
Второй важный момент: обработчики не запускаются сразу. Ansible собирает все уведомления в течение play и выполняет обработчики один раз в самом конце, после того как отработали все обычные задачи. Даже если десять задач уведомили один и тот же handler, он выполнится ровно один раз. Это и есть идемпотентность, которую мы хотим: лишний перезапуск сервиса не случится.
Классический пример - копируем конфиг и перезапускаем сервис только при изменении:
Код: Выделить всё
- name: Настройка nginx
hosts: web
become: true
tasks:
- name: Установить nginx
ansible.builtin.dnf:
name: nginx
state: present
- name: Выложить конфиг nginx
ansible.builtin.copy:
src: files/nginx.conf
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: "0644"
notify: Restart nginx
handlers:
- name: Restart nginx
ansible.builtin.systemd:
name: nginx
state: restarted
enabled: true
На RHEL-семействе (RHEL, Fedora, Rocky, AlmaLinux, RED OS, Astra) под капотом systemd, поэтому ansible.builtin.systemd - правильный выбор. Если работаешь с Ubuntu/Debian, всё то же самое, только имя пакета будет nginx и менеджер пакетов ansible.builtin.apt вместо dnf, а сам механизм обработчиков идентичен.

listen: один notify на пачку обработчиков
Часто после смены конфига надо не просто перезапустить один сервис, а сделать несколько действий: перезагрузить демон, прогреть кеш, пнуть мониторинг. Можно перечислить в notify список имён, но есть способ изящнее - директива listen. Она задаёт обработчику "тему" (topic), на которую тот подписывается. Уведомив тему, ты запустишь сразу все обработчики, которые её слушают.
Код: Выделить всё
tasks:
- name: Развернуть конфиг приложения
ansible.builtin.template:
src: app.conf.j2
dest: /etc/myapp/app.conf
mode: "0640"
notify: "config changed"
handlers:
- name: Перезапустить приложение
ansible.builtin.systemd:
name: myapp
state: restarted
listen: "config changed"
- name: Сбросить кеш
ansible.builtin.command: /usr/local/bin/myapp-cache-flush
listen: "config changed"
- name: Перечитать firewalld
ansible.posix.firewalld:
service: http
permanent: true
state: enabled
immediate: true
listen: "config changed"
flush_handlers: запустить обработчики прямо сейчас
По умолчанию обработчики ждут конца play. Но иногда так нельзя. Допустим, ты сменил конфиг сервиса, и следующая же задача должна обращаться к этому сервису по сети - проверить, что он поднялся с новыми настройками. Если обработчик с рестартом сработает только в конце, проверка пойдёт по старой конфигурации. Нужно перезапустить сервис посреди play, до остальных задач.
Для этого есть мета-задача ansible.builtin.meta: flush_handlers. Она принудительно выполняет все обработчики, накопившие уведомления к этому моменту, прямо там, где ты её поставил.
Код: Выделить всё
tasks:
- name: Поправить конфиг демона
ansible.builtin.template:
src: sshd_config.j2
dest: /etc/ssh/sshd_config.d/99-custom.conf
validate: "sshd -t -f %s"
mode: "0600"
notify: Restart sshd
- name: Принудительно прогнать обработчики сейчас
ansible.builtin.meta: flush_handlers
- name: Проверить, что sshd слушает порт
ansible.builtin.wait_for:
port: 22
timeout: 30
Типичные грабли
Имя не совпадает. Самая частая беда. notify: restart Nginx и handler с именем Restart nginx - это два разных имени. Ansible не ругается, просто молча не запускает обработчик. Внимательно следи за регистром и пробелами. Хорошая практика - копировать имя из handlers в notify, а не печатать заново.
Ошибка до flush. Если задача между notify и концом play (или до flush_handlers) упадёт, обработчики по умолчанию не выполнятся - они привязаны к финалу play, до которого исполнение просто не дойдёт. Хост уведомил обработчик о рестарте, но упал на следующей задаче - и остался со свежим конфигом, но старым работающим сервисом. На повторном прогоне copy вернёт ok (файл-то уже лежит), notify не сработает, и рассинхрон сохранится. Лечится либо ранним meta: flush_handlers сразу после критичной задачи, либо флагом
Код: Выделить всё
--force-handlersКод: Выделить всё
force_handlers: trueПорядок выполнения. Обработчики запускаются не в порядке уведомлений, а в порядке их объявления в разделе handlers (с учётом тем listen). Если тебе важно, чтобы "reload daemon" шёл строго перед "restart service", расставляй их в нужном порядке именно в блоке handlers, а не надейся на очередь notify.
Обработчик в роли. В роли обработчики кладут в roles/имя/handlers/main.yml, и notify из задач этой роли их видит автоматически. Но если уведомляешь обработчик другой роли - надёжнее завязываться на listen-тему, а не на имя.
Мини-лаба
Собери всё руками на тестовой RHEL/Fedora-машине:
- Напиши плейбук, который ставит nginx через ansible.builtin.dnf и кладёт конфиг через ansible.builtin.copy с notify на обработчик Restart nginx.
- Прогони дважды. Убедись, что во второй раз обработчик НЕ сработал (changed=0 на задаче copy, обработчик не в выводе).
- Поменяй конфиг, прогони снова - теперь обработчик должен выполниться. Это и есть ansible notify в действии.
- Добавь второй обработчик и переведи оба на общий listen-топик. Уведоми тему - проверь, что отработали оба.
- Вставь meta: flush_handlers после задачи с конфигом и задачу wait_for после него. Убедись, что рестарт идёт до проверки.
- Поломай имя в notify (смени регистр) и посмотри, как Ansible молча проглатывает уведомление - запомни этот симптом.
- При каком статусе задачи notify реально уведомляет обработчик, а при каком нет?
- Сколько раз выполнится один обработчик, если его уведомили пять разных задач за один play?
- Зачем нужен ansible.builtin.meta: flush_handlers и чем плох вариант ждать конца play?
- Что произойдёт с уведомлённым обработчиком, если задача между notify и концом play упадёт с ошибкой, и как это обойти?
Обработчики ansible - это механизм "реагировать только на изменения". notify шлёт уведомление при changed, обработчик копится и выполняется один раз в конце play, listen группирует несколько обработчиков под одной темой, а flush_handlers запускает их принудительно посреди play, когда дальнейшие задачи зависят от результата. Главные грабли - несовпадение имён и потерянные обработчики после ошибки. Запомни связку notify плюс handler restart сервиса при изменении конфига: на RHCE она встречается почти гарантированно, и решать её надо именно обработчиками, а не лобовым перезапуском в каждом прогоне.