Повтор задачи в Ansible: until, retries и delay
Ключевая идея простая. Обычная задача выполняется один раз. Если добавить к ней until, Ansible будет повторять её, пока условие не станет истинным или пока не кончатся попытки. Это и есть повтор задачи в Ansible, и реализован он тремя директивами уровня задачи:
- until - условие выхода. Пишется как Jinja2-выражение, но БЕЗ фигурных скобок (оно шаблонизируется неявно). Пока выражение ложно - крутим дальше.
- retries - сколько раз повторить, если условие не выполнилось. По умолчанию 3.
- delay - пауза в секундах между попытками. По умолчанию 5.
Вот канонический пример - ждём, пока веб-приложение начнёт отвечать кодом 200:
Код: Выделить всё
- name: Дождаться готовности приложения по HTTP
ansible.builtin.uri:
url: http://localhost:8080/health
status_code: 200
register: app_health
until: app_health.status == 200
retries: 30
delay: 5
Часто проверяют не HTTP, а вывод команды. Тогда условие вешают на rc или на содержимое stdout:
Код: Выделить всё
- name: Ждать, пока сервис в systemd станет active
ansible.builtin.command: systemctl is-active nginx
register: svc_state
until: svc_state.stdout == "active"
retries: 12
delay: 5
changed_when: false

Пауза в плейбуке: ansible pause, wait_for и wait_for_connection
until хорош, когда есть что проверять командой. Но иногда нужна именно пауза в плейбуке или ожидание конкретного ресурса - порта, файла, доступности хоста. Для этого есть три отдельных инструмента, и путать их не стоит.
ansible.builtin.pause - тупая пауза или остановка с вопросом к человеку. Используй ansible pause осторожно: жёсткий sleep на N секунд - это антипаттерн, потому что либо ждёшь слишком мало, либо слишком долго. А вот пауза с подтверждением оператора перед опасным шагом - вполне законно.
Код: Выделить всё
- name: Подождать оператора перед выкаткой на прод
ansible.builtin.pause:
prompt: "Проверь дашборд и нажми Enter для продолжения"
- name: Дать БД 10 секунд на прогрев (если очень надо)
ansible.builtin.pause:
seconds: 10
Код: Выделить всё
- name: Дождаться, пока приложение начнёт слушать порт 8080
ansible.builtin.wait_for:
port: 8080
host: 127.0.0.1
state: started
delay: 3
timeout: 60
- name: Дождаться появления PID-файла
ansible.builtin.wait_for:
path: /run/myapp/app.pid
state: present
timeout: 30
ansible.builtin.wait_for_connection - особый случай. Он ждёт не порт, а саму возможность Ansible достучаться до хоста по SSH. Незаменим после перезагрузки. Связка классическая: перезагрузили - дождались, пока хост снова отвечает.
Код: Выделить всё
- name: Перезагрузить сервер
ansible.builtin.reboot:
reboot_timeout: 600
# reboot уже сам ждёт возврата, но если ребутишь руками:
- name: Послать reboot и не ждать
ansible.builtin.shell: sleep 2 && systemctl reboot
async: 1
poll: 0
- name: Дождаться, пока система снова станет доступна
ansible.builtin.wait_for_connection:
delay: 15
timeout: 300
sleep: 5
Циклы, индексы и спецпеременные, плюс типичные грабли
until можно комбинировать с loop: тогда каждый элемент цикла прогоняется со своими retries. Результат по элементам собирается в registered-переменную в поле results - это список. Доступ к индексу текущей итерации даёт спецпеременная loop. У неё внутри цикла есть полезные поля: ansible_loop.index (нумерация с 1), ansible_loop.index0 (с 0), а через loop_control можно включить расширенную информацию.
Код: Выделить всё
- name: Ждать готовности нескольких портов с индексацией
ansible.builtin.wait_for:
port: "{{ item }}"
timeout: 30
loop:
- 8080
- 9090
- 5432
loop_control:
extended: true
register: ports_check
Теперь грабли, на которых спотыкаются почти все:
- Фигурные скобки в until. Условие until шаблонизируется неявно. Писать until: "{{ x.rc == 0 }}" не нужно и иногда вредно - достаточно until: x.rc == 0.
- Условие, которое никогда не станет истинным. Если опечатался в имени поля (stout вместо stdout), задача честно отработает все retries и упадёт. Всегда сначала проверь реальную структуру результата через debug с var.
- changed/failed внутри until. Промежуточные провалы попыток НЕ считаются ошибкой, пока есть retries. Но если retries кончились, а условие так и ложно - задача падает. Это ожидаемо.
- sleep вместо ожидания. pause с фиксированными секундами вместо wait_for или until - хрупко. На медленной машине не хватит, на быстрой - потеряешь время.
- wait_for порта на 0.0.0.0 vs 127.0.0.1. Если приложение слушает только localhost, указывай host: 127.0.0.1, иначе ожидание зависнет до таймаута.
Мини-лаба: дождаться готовности приложения после рестарта
Повтори руками на любой RHEL/Fedora-машине (на Ubuntu/Debian замени dnf на apt, остальное идентично; на RED OS и Astra тоже работает - там тот же systemd):
- Поставь и запусти nginx: модулем ansible.builtin.dnf, затем ansible.builtin.service со state: restarted.
- Сразу после рестарта добавь задачу wait_for на port: 80, state: started, timeout: 30 - убедись, что плейбук не идёт дальше, пока порт не открылся.
- Сделай health-check через ansible.builtin.uri с until по status == 200, retries: 10, delay: 3.
- Намеренно сломай: укажи в until несуществующее поле и посмотри, как задача отрабатывает все попытки и падает. Почини.
- Добавь loop_control: extended: true к какому-нибудь циклу и выведи через debug значение ansible_loop.index.
- Чем until отличается от loop и какие две директивы всегда идут с ним в паре?
- Почему в условии until обычно не нужны фигурные скобки {{ }}?
- В каком случае берёшь wait_for, а в каком - wait_for_connection?
- Что вернёт register, если until стоит на задаче с loop, и как достать результат конкретной итерации?