Прямой ответ
Ansible перестаёт быть игрушкой, когда у него появляются три вещи: структурированный inventory (группы по ролям — webservers, dbservers, vpn — с переменными групп в group_vars/, а не хардкодом в задачах), идемпотентные задачи (прогон дважды = тот же результат: модули с состоянием, а не shell-команды «на всякий случай» — основы Ansible), и rolling-стратегия для парка (serial: 1 + пост-проверки: обновление/деплой идёт по одному серверу, ошибка останавливает прогон до поломки всех — та же логика, что rolling-деплой, применённая к конфигурации). Ключевая дисциплина — проверка после каждого хоста: задача-assert или uri-чек эндпоинта между батчами (serial без проверки обновляет весь парк «вслепую», просто последовательно). И типовой антипаттерн, убивающий доверие к автоматизации: shell:/command: там, где есть модуль (copy, template, systemd, apt) — shell-задачи не идемпотентны по построению, второй прогон «что-то да сломает», и changed статус врёт.
Inventory и переменные: структура, которая масштабируется
Практичная раскладка: inventory/production.yml (YAML-inventory читабельнее ini): группы по ролям и окружениям (prod: children: webservers, dbservers), хосты с минимальными индивидуальными переменными (ansible_host, порт); переменные — по уровням: group_vars/all.yml (общее: NTP, пользователи), group_vars/webservers.yml (nginx-настройки роли), host_vars — исключение, не правило (различающиеся хосты одной роли — запах: роль должна параметризоваться, а не форкаться). Полезные паттерны: ansible_ssh_common_args для jump-хоста (доступ через бастион — VPN/WireGuard как альтернатива), динамические inventory-плагины для облаков (парк из облачного API — серверы появляются/исчезают без правок файла), и ansible-inventory --graph — мгновенный взгляд на структуру (отладка «почему переменная не применилась» начинается с него: ansible-inventory --host web1 показывает фактические переменные хоста со всеми уровнями). Правило секретов: пароли/ключи — не в group_vars открытым текстом (ansible-vault или внешний секрет-стор — Vault/SOPS); inventory-репозиторий публичен внутри команды — секрет в нём утечёт.
Идемпотентность: как писать задачи, которые не страшно перезапускать
Принцип: задача описывает целевое состояние, а не действие. apt: name=nginx state=present (поставлен — пропустит), template: src=nginx.conf.j2 dest=/etc/nginx/nginx.conf notify: reload nginx (файл совпадает — не тронет; изменился — перечитает конфиг через handler), systemd: name=app state=started enabled=yes — модули сами проверяют состояние. Антипаттерны и замены: shell: echo 'x' >> /etc/file (на каждом прогоне дубль строки) → lineinfile/blockinfile; shell: systemctl restart app безусловно → systemd + notify (рестарт только при изменении — «плейбук перезапускает сервис на каждом прогоне» =owntime без причины); command: create-db.sh → postgresql_db с состоянием или creates=/var/lib/db/initialized (аргумент-страховка). Проверка идемпотентности — в привычку: прогнать плейбук дважды подряд — второй прогон должен показать changed=0 (исключения — задачи с check_mode: false по необходимости, помеченные явно); CI-линтеры (ansible-lint) ловят часть антипаттернов автоматически. Отдельный класс — «почти идемпотентные» задачи с внешними эффектами (API-вызовы, миграции БД): флаги-страховки creates:/removes: или отдельный плейбук «одноразовых операций» вне регулярных прогонов.
Rolling: serial, проверки, откат
Деплой новой версии приложения на четыре веб-сервера: hosts: webservers, serial: 1 (по одному; serial: [1, 2, "50%"] — канареечный старт с ускорением — первый сервер, потом два, потом половина), max_fail_percentage: 0 (любая ошибка — стоп прогона). Структура play: pre-задачи — вывод сервера из балансировки (флаг healthcheck-файла или API LB — healthcheck-механика); роль деплоя (код, зависимости, миграции, рестарт сервиса); блок проверки — uri: url= inventory_hostname }}:8080/health status_code=200 + assert на версию (curl /version = ожидаемая); post — возврат в балансировку. Ошибка на втором сервере: прогон остановлен, первый уже на новой версии и проверен, третий-четвёртый не тронуты — разбор без аварии; откат — повторный прогон с предыдущей версией переменной (версия — параметр: --extra-vars app_version=v1.2.3, не «git checkout на сервере руками»). Для ОС-обновлений паттерн тот же (rolling-обновления). Дополнение — pre_flight-проверки в начале play (место на диске, достижимость БД): дешёвые assert'ы ловят половину «падений на середине».
Практический сценарий: деплой с миграцией БД без простоя
Задача: релиз v2 с изменением схемы БД на стек web×3 + db×1. Порядок в плейбуке деплоя: (1) pre-flight — бэкап-чек (pgbackrest check прошёл не позже суток), место на дисках; (2) мигрирующая задача на db-группе с expand-совместимостью (новая колонка nullable — старый код работает со старой схемой — alembic-практика: expand/migrate/contract, не «одним прыжком»); (3) rolling веб-серверов: serial 1, вывод из LB, деплой v2, рестарт, healthcheck + смоук (новый эндпоинт отвечает), возврат в LB; (4) наблюдение — 15 минут метрик между батчами (проверка не только «процесс жив», но и латентность/ошибки — дашборд); (5) contract-миграция — отдельным прогоном после полного раската (удаление старой колонки, когда ни один сервер не на v1). Сбой в сценарии: третий сервер не проходит healthcheck — прогон встал, 2/3 парка на v2 и работают, откат третьего — локальный повтор роли с v1; разбор — порт нового воркера конфликтовал (конфиг-шаблон не учёл переменную окружения) — фикс в group_vars, повтор. Весь инцидент — внутри автоматизации: ни одного ручного действия на серверах, состояние известно из inventory и логов прогона.
FAQ
Ansible или Terraform? Дополняют: Terraform — инфраструктура (серверы, сети, DNS — декларативное состояние ресурсов), Ansible — конфигурация внутри (пакеты, файлы, сервисы); типичная связка: Terraform поднял VM → Ansible настроил. Пересечения (cloud-init, образы) решаются выбором границы, а не спором инструментов.
Нужен ли AWX/Semaphore (веб-обёртки)? Для команды с самостоятельными запусками — да: контроль доступа, история, секреты вне локальных ключей; для 1–3 админов — cron/CI-запуски ansible-playbook из git достаточны, обёртка — ещё один сервис обслуживать.
Почему плейбук «завис» на одном хосте?
SSH-ожидание (хост недоступен — таймауты ConnectTimeout в ansible.cfg), или задача ждёт ввода (shell-команда с промптом — антипаттерн), или долгая операция без async; диагностика — -vvv (видно, на каком соединении/задаче), ansible host -m ping до прогона.
Как тестировать роли до прода?
Молекула (molecule) — тестовые контейнеры/VM с прогоном роли и assert'ами (infra-код с тестами — как приложение); минимум без молекулы — --check --diff прогон на стейдже (что изменится — глазами) и двойной прогон на идемпотентность.
Источники: документация Ansible (inventory, delegation, error handling) — ссылки в шапке.