Анотация
Эта статья показывает, как Ansible помогает выпускать обновления быстро и без остановок сервиса. Мы разберём простыми словами 6 приёмов, которые снижают риски, делают релизы предсказуемыми и экономят время команды: быстрые разовые операции, мягкая обработка ошибок, ожидание готовности сервисов, фоновые «долгие» задачи, поэтапные раскатки и однократные шаги «для всей компании».
Зачем это бизнесу
Когда продукт растёт, важно обновлять его без простоя и без «неожиданностей». Ansible — это инструмент, который превращает «человеческие инструкции» в точные и повторяемые шаги. Ниже — базовые приёмы, которые дают предсказуемость и контроль.
1) Разовые команды и плейбуки: когда что использовать
Идея.
- Разовая команда (ad-hoc) — быстро проверить гипотезу или сделать маленькую правку на всех серверах.
- Плейбук — записанный сценарий. Его можно запускать много раз и быть уверенным в результате.
Когда разовая команда уместна: «жив ли сервер?», «поставить один пакет», «перезапустить сервис».
Когда нужен плейбук: всё, что повторяется; всё, что важно не забыть; всё, что нужно показать коллеге или прогнать в CI.
Примеры разовых команд:
# Проверить связь со всеми серверами
ansible all -m ping
# Перезапустить nginx на группе app
ansible app -m service -a 'name=nginx state=restarted' --become
# Установить пакет htop на базах
ansible db -m package -a 'name=htop state=present' --become
Итог. Разовые команды — «быстрый термометр». Плейбуки — «правила лечения».
2) Мягкая обработка ошибок: block / rescue / always
Идея. Делать изменения смело, но с «планом Б»: если шаг не удался, вернуть всё как было и зафиксировать статус.
Как это выглядит:
Зачем бизнесу. Ошибка не превращается в простой: есть автоматический откат и понятный отчёт.
Что за блоки и когда они срабатывают — простым текстом
- block — это основная часть. Здесь перечисляем шаги, которые хотим выполнить. Они идут по порядку и выполняются, пока не случится ошибка.
- rescue — это план отката. Он запускается только если любой шаг внутри block завершился с ошибкой и эта ошибка не была проигнорирована (то есть без ignore_errors: true). В rescue мы возвращаем систему в рабочее состояние и фиксируем, что пошло не так.
- always — это действия, которые нужно сделать в любом случае: уборка временных файлов, сбор статусов, логирование. always выполняется всегда после block и/или rescue (если хост доступен).
Когда и зачем это использовать
- Рисковые изменения: правка конфигов, переключение версий/фич-флагов — нужен предсказуемый откат при сбое.
- «Транзакции» из нескольких шагов: сделать → проверить → включить. Если проверка не прошла — вернуть всё как было.
- Чистка «хвостов»: временные файлы, локи, режимы обслуживания — убрать независимо от исхода.
- Понятный отчёт о результате: в always собрать статусы/логи и показать, чем всё кончилось.
- Снижение простоя: ошибки не валят весь плей — быстро откатываемся и продолжаем.
- Стандартизация релизов: единый шаблон «план А / план Б / уборка» во всех плейбуках.
Типичные подводные камни
- ignore_errors внутри block: ошибка «проглочена» → rescue не запустится. Если нужен контролируемый фейл — используйте failed_when.
- Неверное место для notify: перезапуск до проверки включает плохой конфиг. Ставьте notify только после успешной валидации/переключения.
- Ожидание «на глаз»: sleep вместо проверки состояния. Используйте retries + until в самом блоке.
- Нет материала для отката: не сделали бэкап/кандидатный файл — rescue нечем откатывать. Готовьте «подушку» заранее.
- always ломается: тяжёлые или хрупкие задачи в always могут сами упасть и скрыть первопричину. Держите always коротким и устойчивым.
- Недоступный хост: rescue не сработает при UNREACHABLE. Сначала восстанавливаем доступ, затем — откат.
- Неедиомпотентные шаги: повторный запуск усугубляет состояние. Делайте копии/темп-файлы и проверяйте перед заменой.
- Потерянные переменные: в rescue обращаются к данным, которых нет из-за раннего сбоя. Используйте | default(...) и готовьте альтернативный путь.
- Рассыпанные условия/права: забыли общий when/become на уровне block — часть задач не совпадёт по контексту. Ставьте их на сам block.
3) Ждать готовность сервисов без «слепого» сна: retries + until
Идея. Не зашивать фиксированные паузы. Вместо этого — проверять реальное состояние: порт открылся? страница отвечает 200?
Плюс. Релизы идут быстрее и стабильнее: не ждём лишнего, но и не бежим раньше времени.
Что за блоки и когда они срабатывают — простым текстом
- until — это условие «готовности». Задача будет повторяться, пока выражение не станет истинным (или пока не закончатся попытки).
- retries — сколько раз пробовать; delay — пауза между попытками в секундах.
- Обычно мы сохраняем результат задачи через register и проверяем его поля в until (например, код ответа HTTP или факт существования файла).
- Если указать только retries/delay без until, повторов не будет — задача выполнится один раз.
- Если после всех попыток условие так и не стало истинным, задача помечается как failed.
Частые кейсы для until:
- HTTP-проверка: result.status == 200 (модуль uri).
- Открытие порта: result.state == "started" или result.elapsed < ... (модуль wait_for).
- Команда завершилась успешно: result.rc == 0 (модули command/shell).
- Файл появился: result.stat.exists (модуль stat).
Типичные ошибки:
- «result is undefined» — забыли register.
- Сравнение разных типов (строка vs число). Приводите типы: result.status | int == 200.
- Повторная задача имеет побочные эффекты (каждый ретрай что-то меняет). Держите проверку читающей, а не «изменяющей».
- Слишком большие ожидания или бесконечные «зависания». Планируйте верхнюю границу: общее_время ≈ retries × delay.
4) Долгие задачи — в фоне: async / poll и «fire-and-forget»
Идея. Некоторые шаги занимают минуты (миграция БД, копирование больших файлов). Их можно запустить в фоне, а сам релиз продолжать.
Плюс. Команда не «застревает» на одном шаге. Менеджеру видно: релиз идёт, статус контролируем.
Что за блоки и когда они срабатывают — простым текстом
- async — говорит Ansible, сколько максимум секунд можно выполнять задачу на удалённой машине. Задача стартует в фоне и может жить без открытого SSH-сеанса до указанного лимита.
- poll — как часто опрашивать статус фоновой задачи.
Если poll: 0 — режим «fire-and-forget»: запустили и сразу пошли дальше, не ждём результат.
Если poll не указан — Ansible будет ждать завершения, опрашивая примерно раз в 10 секунд (блокирующее ожидание).
Если poll: N — будет ждать и опрашивать каждые N секунд. - Результат фоновой задачи сохраняется в переменной, где есть ansible_job_id. Чтобы позже проверить статус, используйте модуль async_status и передайте туда этот jid.
- Если задача выполняется дольше, чем указано в async, Ansible прервёт её на удалённом хосте. Ставьте разумный запас (например, 3600 секунд для часовых миграций).
- Асинхронность задаётся на каждый хост отдельно. Параллелизм между хостами контролируется обычными механизмами (serial, forks, при желании — throttle на задаче).
- Для очень долгих операций (часы/после перезагрузки) надёжнее запускать их под присмотром systemd или скрипта-демона и контролировать готовность отдельной проверкой (порт/HTTP/файл), а не держать гигантский async.
Типовые сценарии:
- Долгая миграция БД, большая сборка/копирование — запускаем в фоне, продолжаем плей, позже собираем статусы.
- Массовые операции на десятках хостов — не блокируемся на «медленных», плей движется дальше.
Частые ошибки:
- Запустили poll: 0, но потеряли ansible_job_id — потом нечем проверять статус. Всегда делайте register и сохраняйте jid.
- Поставили слишком маленький async — задача убивается на полпути. Дайте запас.
- Ожидают «готовность» без явной проверки. Асинхронность — это про запуск, а готовность проверяйте отдельно (uri, wait_for, command + until).
- Надеются на rescue для фоновой задачи. Если вы ушли дальше по плейбуку, rescue уже не «поймает» падение процесса в фоне. Делайте явную проверку статуса и ветвление.
5) Обновлять по очереди, без общей остановки: serial, strategy=free, max_fail_percentage
Идея. Обновлять не все сервера сразу, а по одному или небольшими группами. Если что-то пошло не так — остановиться вовремя.
Плюс. Пользователи не замечают релиз: часть узлов всегда остаётся рабочей.
Что за блоки и когда они срабатывают — простым текстом
- serial — задаёт, сколько хостов обновляем одновременно.
Можно число (serial: 1 — по одному) или процент (serial: 20% — по 20% группы за раз).
Работает пачками: Ansible берёт первую пачку, выполняет все задачи, затем переходит к следующей. Это и есть «роллинг». - strategy=free — говорит Ansible не ждать медленные хосты внутри текущей пачки.
Быстрые хосты идут вперёд, медленные догоняют. Это ускоряет раскатку, но порядок между хостами не гарантируется.
Важно: strategy=free не отменяет serial — размер пачки сохраняется, просто внутри пачки хосты не синхронизируются шаг-в-шаг. - max_fail_percentage — «красная кнопка безопасности».
Если процент упавших хостов превысил порог (например, 20), Ansible останавливает плей.
Помните про малые группы: при 2 хостах 50% — это уже 1 хост.
Когда и зачем это использовать
- Нужен роллинг без простоя → ставим маленький serial (часто 1), чтобы всегда оставалась рабочая часть кластера.
- Хотим ускорить раскатку в пределах пачки → добавляем strategy=free.
- Хотим остановить релиз при серии фейлов → задаём max_fail_percentage (обычно 10–30% в зависимости от риска).
Типичные подводные камни
- Слишком большой serial может просадить доступность (слишком много узлов одновременно выводятся из строя).
- С strategy=free шаги на разных хостах могут идти в разнобой — не рассчитывайте на строгую синхронизацию между хостами (например, «сначала у всех остановить, потом у всех запустить» — это не так).
- max_fail_percentage в маленьких группах ведёт себя «ступеньками»: считайте в штуках, а не только в процентах.
- Обработчики (handlers) запускаются после пачки. Планируйте это: перезапуск сервиса может происходить несколько раз — по завершении каждой пачки.
6) Общие действия — один раз: delegate_to, run_once
Идея. Если нужно создать общий артефакт (например, ключ или токен), делаем это один раз на «контрол-хосте», а потом распространяем.
Плюс. Нет дублирования и «расхождений» между серверами.
Что за блоки и когда они срабатывают — простым текстом
- delegate_to — попросить Ansible выполнить конкретную задачу на другом хосте, а не на том, к которому вы сейчас подключены.
Частые случаи: сделать действие на localhost (машина, где запущен Ansible), на бастионе, на балансировщике (lb), на DNS-сервере и т. п. Пример: «для каждого app-сервера вывести его из балансировщика» — сам шаг выполняется на хосте lb. - run_once — выполнить задачу только один раз для всего текущего запуска (или «пачки» при serial).
Удобно для «общих артефактов»: сгенерировать ключ, получить токен, скачать архив билда. Вместо сотни одинаковых действий — одно. - Вместе: run_once + delegate_to: localhost — «сделай один раз и сделай это на локальной машине». Классика: сгенерировать ключ/токен/архив, а потом разослать.
- Важно понимать про роллинг (serial): run_once выполняется один раз на всю задачу, но привязывается к первой группе хостов в текущей пачке. Если хотите гарантированно один раз на весь плей, безопаснее делать run_once: true + delegate_to: localhost.
- delegate_to не копирует переменные автоматически между хостами. Он просто говорит, где выполнить шаг. Если нужно «поделиться результатом» со всеми, либо сохраните артефакт (файл) и разошлите, либо используйте hostvars[...] для доступа к результату.
Когда и зачем это использовать
- Один общий артефакт для всех: сгенерировать ключ/сертификат/токен один раз на localhost, затем разослать → run_once: true + delegate_to: localhost.
- Действия на стороннем узле: вывести/ввести хост из балансировщика, обновить записи DNS, дернуть API CI/CD — выполняем задачу на lb, dns, bastion через delegate_to.
- Сбор метаданных/билда: скачать архив релиза, подготовить шаблоны, упаковать конфиг один раз — потом распространяем.
- Экономия времени и идемпотентность: вместо N одинаковых команд на каждом хосте — один подготовительный шаг и простая раздача файлов.
- Безопасность: секрет (ключ/токен) создаём локально и контролируем его права; на сервера доставляем только необходимое.
Типичные подводные камни
- Забыли delegate_to: «однократная» команда запускается на первом хосте группы, а не на локалке. Решение: всегда явно указывать delegate_to: localhost, если ожидаете локальное выполнение.
- Только run_once без делегации: задача выполнится один раз, но на первом хосте текущей пачки (особенно заметно при serial). Добавляйте делегацию туда, где действительно нужно.
- Повтор при роллинге: с serial «один раз» может случиться на первую пачку, а не на весь плей. Критичные «единственные» шаги ставьте до роллинга или делегируйте на localhost.
- Потерянный результат: сделали run_once, но не сохранили вывод. Используйте register, а затем раздавайте файл или обращайтесь к переменной через hostvars[groups['all'][0]]….
- Смешение контекстов: команда делегирована на lb, а переменные берутся с app-хоста. Проверяйте, откуда берёте пути/файлы (inventory_hostname vs делегируемый узел).
- Права и секреты: ключи/токены лежат в /tmp без ограничений. Выдавайте строгие режимы (0600), чистите временные файлы.
- Параллельность: несколько «однократных» задач без явного порядка могут конфликтовать. Кладите их подряд и избегайте лишнего параллелизма для подготовки артефактов.
Почему это работает (в двух словах)
Ansible выполняет шаги точно по сценарию. Мы управляем тем, когда и как они идут:
- «План Б» при ошибках → меньше простоев.
- Проверка готовности вместо «ожидания на глаз» → быстрее и надёжнее.
- Фоновые операции → релиз не стоит на месте.
- Роллинг-подход → сервис остаётся доступным.
- Общие шаги один раз → меньше ручной работы и меньше ошибок.
CTA
Хотите такой же понятный план для ваших релизов? Напишите вопросы в комментариях — в следующих материалах разберём условия выполнения, теги и ускорение плейбуков на реальных кейсах.