В обычном коде ты бы завернул это в try/catch/finally. В Ansible ровно для этого есть блоки - block, rescue и always. Это не просто синтаксический сахар: грамотная обработка ошибок ansible через блоки превращает хрупкий линейный плейбук в управляемый сценарий, который умеет откатываться и прибираться за собой.
Что такое block и зачем группировать задачи
Самое базовое назначение ansible block - сгруппировать несколько задач и навесить на всю группу общие директивы. Вместо того чтобы копировать when, become или tags в каждую задачу, ты пишешь их один раз на блок.
Код: Выделить всё
- name: Настройка веб-сервера на RHEL
hosts: web
tasks:
- name: Установка и запуск nginx
block:
- name: Установить пакет nginx
ansible.builtin.dnf:
name: nginx
state: present
- name: Открыть http в firewalld
ansible.posix.firewalld:
service: http
permanent: true
immediate: true
state: enabled
- name: Запустить и включить nginx
ansible.builtin.service:
name: nginx
state: started
enabled: true
become: true
when: ansible_facts['os_family'] == "RedHat"
tags: webserver

block + rescue + always: try-catch-finally по-ansible'овски
Теперь главное, ради чего блоки и любят. Полная конструкция выглядит так и читается ровно как try/catch/finally:
- block - основные задачи (try). Выполняем то, что хотим.
- rescue - что делать, если в block что-то упало (catch). Сюда управление прыгает при первой же ошибке.
- always - выполняется в любом случае (finally), упал block или нет.
Соберём тот самый сценарий деплоя: пытаемся обновить, при сбое откатываемся, в конце всегда возвращаем сервис в строй.
Код: Выделить всё
- name: Безопасный деплой с откатом
hosts: app
become: true
vars:
release_dir: /opt/myapp/current
tasks:
- name: Обновление релиза с защитой от падения
block:
- name: Остановить сервис приложения
ansible.builtin.service:
name: myapp
state: stopped
- name: Развернуть новый релиз
ansible.builtin.unarchive:
src: /tmp/myapp-2.4.0.tar.gz
dest: "{{ release_dir }}"
remote_src: true
- name: Прогнать миграции (тут может упасть)
ansible.builtin.command:
cmd: /opt/myapp/bin/migrate --check
changed_when: false
rescue:
- name: Сообщить, что именно упало
ansible.builtin.debug:
msg: "Сбой на задаче '{{ ansible_failed_task.name }}'. Откатываемся."
- name: Откатить релиз из бэкапа
ansible.builtin.command:
cmd: /opt/myapp/bin/rollback
changed_when: true
always:
- name: Гарантированно запустить сервис
ansible.builtin.service:
name: myapp
state: started
Доступ к ansible_failed_task и ansible_failed_result
Внутри rescue Ansible подкладывает две магические переменные, и для отладки они бесценны:
- ansible_failed_task - сам объект упавшей задачи. Чаще всего берут ansible_failed_task.name, чтобы понять, на каком шаге рвануло.
- ansible_failed_result - полный результат упавшей задачи: msg, rc, stderr и прочее. Идеально, чтобы достать текст ошибки и, например, отправить в Slack или записать в журнал.
Код: Выделить всё
rescue:
- name: Показать причину сбоя
ansible.builtin.debug:
msg: >-
Задача: {{ ansible_failed_task.name }}
| rc={{ ansible_failed_result.rc | default('n/a') }}
| {{ ansible_failed_result.msg | default('нет msg') }}
Типичные грабли
- rescue прячет проблему. Если rescue отработал чисто, хост зелёный, и в CI ты можешь не заметить, что деплой на самом деле откатился. Внутри rescue добавляй явный вывод или, если откат недопустим, в конце rescue ставь ansible.builtin.fail, чтобы плейбук всё-таки честно упал.
- Хендлеры и блоки. notify внутри block работает, но если задача упала и сработал rescue, обычные хендлеры в конце плея не запустятся (Ansible их пропускает при провале плея на хосте). Не рассчитывай на хендлер для критичной уборки - кладируй её в always.
- Условие на блоке. when на block - это не "выполнить блок, если...", а условие, навешиваемое на каждую вложенную задачу. На register результат самого блока повесить нельзя - блок не возвращает единый результат.
- always всё равно выполнится. Даже если в rescue ты сделал fail, always отработает до падения. Это фича (прибраться надо всегда), но новички удивляются, почему "после fail ещё что-то выполнилось".
Собери руками на тестовой RHEL/Fedora-машине плейбук, который:
- в block создаёт каталог через ansible.builtin.file и пишет туда файл, а затем намеренно выполняет ansible.builtin.command с заведомо несуществующей командой;
- в rescue выводит через debug значения ansible_failed_task.name и ansible_failed_result, а затем удаляет недоделанный каталог (state: absent);
- в always пишет в /var/log сообщение о завершении прогона.
Контрольные вопросы
- В каком случае выполнится секция rescue, а в каком - always?
- Что произойдёт с состоянием хоста, если rescue отработал без ошибок?
- Как внутри rescue узнать имя упавшей задачи и текст её ошибки?
- Почему критичную уборку лучше класть в always, а не в хендлер?