В этом уроке разберём арсенал, которым ты рулишь успехом и провалом задач вручную: ignore_errors, failed_when, changed_when, плюс принудительный fail и assert, и групповые рубильники any_errors_fatal и max_fail_percentage. Это не просто удобство - на RHCE (EX294) умение правильно поставить changed_when и failed_when на command/shell это частая тонкость, на которой народ срезается. Дистрибутив по умолчанию RHEL/Fedora (dnf, systemd, firewalld), но вся эта механика контроля статусов от дистрибутива не зависит вообще - она про логику Ansible, а не про пакеты.
Как Ansible решает, что задача упала: rc, failed и changed
Каждый модуль возвращает результат - словарь с ключами. Для нас важны три вещи: rc (код возврата команды), failed (булев флаг "провал") и changed (булев флаг "что-то поменялось"). По умолчанию для command и shell правило простое: если rc не ноль - задача failed; и changed всегда true, потому что модуль не умеет проверять, был ли реальный эффект.
Все три флага ты можешь переопределить своими условиями. ignore_errors говорит "если задача упала - не останавливай весь плей, иди дальше". failed_when задаёт СВОЁ определение провала. changed_when задаёт СВОЁ определение изменения. Важно понимать порядок: сначала модуль отрабатывает, потом Ansible применяет твои changed_when и failed_when к тому, что вернулось. То есть это пост-фактум переоценка результата, а не предсказание.
Чтобы посмотреть, что вообще возвращает модуль, сохрани результат в register и распечатай:
Код: Выделить всё
- name: Посмотреть, что отдаёт команда
hosts: web
tasks:
- name: Запустить проверку
ansible.builtin.command: id apache
register: out
- name: Показать структуру результата
ansible.builtin.debug:
var: out

ignore_errors: продолжить, когда сбой не критичен
Самый грубый инструмент. По запросу "ansible ignore_errors" обычно ищут именно это: как не дать одной упавшей задаче угробить весь прогон. Ставишь ignore_errors: true - и даже если задача завалилась, плей идёт дальше. Задача всё равно помечается красным как failed в отчёте, но фатальной остановки нет.
Код: Выделить всё
- name: Попытаться остановить сервис, которого может не быть
ansible.builtin.systemd:
name: legacy-app
state: stopped
ignore_errors: true
failed_when: своё определение провала
Вот где начинается взрослая работа с ошибками. failed_when принимает условие (или список условий) - и если оно истинно, задача считается провалившейся, даже если модуль сам по себе отработал успешно. И наоборот: если модуль вернул rc!=0, но твоё failed_when ложно - задача НЕ падает.
Допустим, утилита делает поиск и возвращает rc=1, когда ничего не нашла - для нас это нормально, а вот rc=2 это уже реальная беда:
Код: Выделить всё
- name: Грепнуть конфиг, rc=1 (не найдено) это ок
ansible.builtin.command: grep "max_connections" /etc/my.cnf
register: grep_out
failed_when: grep_out.rc == 2
Код: Выделить всё
- name: Проверить вывод на скрытую ошибку
ansible.builtin.command: /usr/local/bin/deploy.sh
register: dep
failed_when: >
dep.rc != 0 or
"ERROR" in dep.stdout or
"connection refused" in dep.stderr
Код: Выделить всё
- name: Провал только если И код плохой, И вывод пустой
ansible.builtin.command: /opt/check.sh
register: chk
failed_when:
- chk.rc != 0
- chk.stdout == ""
changed_when и спасение от вечного "changed" у command/shell
Теперь главный герой урока для экзамена. Модули command, shell, raw, script не идемпотентны by design - они не знают, что делает твоя команда, поэтому штампуют changed=yes на каждый запуск. Если такая команда чисто читающая (проверка, grep, статус) - руками говорим Ansible "это изменением не считается":
Код: Выделить всё
- name: Чисто читающая проверка, изменением не считается
ansible.builtin.command: systemctl is-active nginx
register: nginx_state
changed_when: false
А если команда реально что-то делает, но только иногда - завязывай changed на вывод. Например, скрипт печатает "UPDATED", когда применил изменение:
Код: Выделить всё
- name: Применить миграцию, changed только при реальном апдейте
ansible.builtin.command: /opt/app/migrate.sh
register: migr
changed_when: "'UPDATED' in migr.stdout"
notify: restart app
Принудительный fail, assert и групповые рубильники
Иногда нужно упасть осознанно. Модуль ansible.builtin.fail роняет задачу с твоим текстом - по запросу "ansible fail" это первое, что нужно. Обычно в паре с when:
Код: Выделить всё
- name: Проверить переменную окружения
ansible.builtin.fail:
msg: "Переменная deploy_env не задана или неверна: {{ deploy_env | default('пусто') }}"
when: deploy_env not in ['dev', 'stage', 'prod']
Код: Выделить всё
- name: Превентивная проверка параметров перед раскаткой
ansible.builtin.assert:
that:
- app_port | int > 1024
- app_port | int < 65536
- target_dir is defined
fail_msg: "Неверные параметры: порт {{ app_port }} или не задан target_dir"
success_msg: "Параметры в порядке, продолжаем"
Теперь масштаб. По умолчанию падение задачи на одном хосте убирает только этот хост из дальнейшей работы - остальные продолжают. Иногда так нельзя. any_errors_fatal: true означает "упал хоть один хост на этой задаче - валим весь плей на всех хостах разом". Полезно, когда хосты в кластере зависят друг от друга:
Код: Выделить всё
- name: Критичная фаза, падение одного валит всех
hosts: dbcluster
any_errors_fatal: true
tasks:
- name: Применить схему
ansible.builtin.command: /opt/apply-schema.sh
changed_when: true
Код: Выделить всё
- name: Роллинг-апдейт с порогом отказов
hosts: webfarm
serial: 10
max_fail_percentage: 20
tasks:
- name: Обновить пакет
ansible.builtin.dnf:
name: myapp
state: latest
Код: Выделить всё
- name: Гарантировать перезапуск даже при сбое дальше
hosts: web
force_handlers: true
tasks: ...
Повтори руками на тестовой RHEL/Fedora-машине:
- Запусти ansible.builtin.command с заведомо читающей командой (например, id root), сохрани в register, выведи debug - найди rc, changed, failed.
- Добавь changed_when: false и убедись, что в отчёте задача стала green ok вместо yellow changed.
- Сделай задачу с grep по файлу: настрой failed_when так, чтобы rc=1 (не найдено) НЕ был провалом, а rc=2 (ошибка доступа) был.
- Напиши assert на проверку переменной (например, app_port в диапазоне), прогони с заведомо плохим значением и посмотри на fail_msg.
- Поставь на задаче ignore_errors: true и сравни поведение плея с тем же без него.
- Почему command и shell всегда показывают changed=yes и какой строчкой это чинят для читающих команд?
- Чем ignore_errors принципиально отличается от failed_when - в каком случае что выбрать?
- Как в failed_when сделать логику OR между несколькими условиями, если YAML-список это AND?
- Что произойдёт с остальными хостами при падении одного, если задан any_errors_fatal: true, и чем тут поможет max_fail_percentage?
ignore_errors глушит ошибку грубо, failed_when и changed_when дают точный контроль над тем, что считать провалом и изменением, fail и assert роняют плей осознанно с понятным сообщением, а any_errors_fatal, max_fail_percentage и force_handlers рулят поведением на всём парке хостов. Главное, что унесёшь на экзамен: для command/shell почти всегда нужен либо changed_when: false (читающие команды), либо changed_when по выводу, плюс осмысленный failed_when - без них плейбук врёт об идемпотентности и падает не там, где должен.