2. Поддерживаемые playbooks: идемпотентность, условия и обработка ошибок
Понимать идемпотентность, уверенно использовать register/when/failed_when/changed_when, циклы, block/rescue/always, отличать include_* от import_*, работать с фактами (facts, module setup). В разделе — небольшие примеры кода с комментариями и production best practices.
Idempotency (почему это критично)
Idempotency означает: повторный запуск playbook приводит систему в то же желаемое состояние без лишних изменений.
Почему это важно в production:
- безопасные повторные прогоны (re-run) после частичного падения
- корректный rolling update (одни и те же шаги на разных нодах)
- минимизация «шума» (перезапуски сервисов/перекаты конфигов без причин)
Пример: «плохой» и «хороший» подход
# Плохо: shell не гарантирует идемпотентность и часто всегда будет marked changed
- name: Add line to file (bad)
ansible.builtin.shell: "echo 'foo=bar' >> /etc/myapp.env"
# Хорошо: модуль lineinfile гарантирует состояние
- name: Ensure foo=bar in env file (good)
ansible.builtin.lineinfile:
path: /etc/myapp.env
regexp: '^foo='
line: 'foo=bar'
create: true
notify: Restart myapp
Production best practices:
- использовать модули вместо
shell/command - всегда стараться, чтобы задача была idempotent (и чтобы это отражалось в
changed) - рестартовать сервисы через handlers (только когда реально изменилось)
register / when / failed_when / changed_when
register + when
- name: Check if nginx is installed (dpkg)
ansible.builtin.command: "dpkg -s nginx"
register: nginx_check
changed_when: false
failed_when: false
- name: Install nginx if missing
ansible.builtin.apt:
name: nginx
state: present
update_cache: true
when: nginx_check.rc != 0
changed_when: false— проверка не должна «портить» отчёт измененийfailed_when: false— проверка может вернуть non-zero, но это не ошибка playbook
failed_when / changed_when по содержимому stdout
- name: Run a healthcheck script
ansible.builtin.command: "/usr/local/bin/healthcheck"
register: hc
changed_when: false
failed_when: "'CRITICAL' in hc.stdout"
Production best practices:
- не превращать всё в shell; если нужно — аккуратно контролировать
changed_when/failed_when - стараться, чтобы логика ошибок была явной и читаемой
Обработка ошибок: retries/until, ignore_errors и «остановить всё»
В production важно различать:
- временные сбои (сеть, репозиторий, зависимость ещё поднимается) — лечатся
retries/until - ожидаемые неуспехи (проверка «установлено ли» или «есть ли файл») — лечатся
failed_when: false - настоящие ошибки — должны останавливать прогон или хотя бы плей на хосте
retries/until вместо sleep
- name: Wait for service to become healthy
ansible.builtin.uri:
url: "http://127.0.0.1:8080/health"
method: GET
status_code: 200
register: health
retries: 10
delay: 3
until: health.status == 200
ignore_errors (использовать осторожно)
ignore_errors: true позволяет продолжить выполнение даже при ошибке. Это удобно для «best effort» действий, но может скрыть реальную проблему.
- name: Try to stop old service (best effort)
ansible.builtin.service:
name: legacy-service
state: stopped
ignore_errors: true
Production best practices:
- не ставить
ignore_errors«чтобы пайплайн зелёный» — лучше настроить корректныйfailed_when - если ошибка допустима, логика должна быть явной (например,
when:+ проверка наличия сервиса)
assert (явно валидировать предпосылки)
- name: Validate required vars
ansible.builtin.assert:
that:
- myapp_env in ['dev', 'staging', 'prod']
- myapp_port | int > 0
fail_msg: "Invalid configuration for myapp"
any_errors_fatal и max_fail_percentage
Когда деплой должен остановиться при проблеме на одном хосте (чтобы не «размазать» поломанную версию по флоту):
- name: Deploy critical change
hosts: web
serial: "20%"
any_errors_fatal: true
tasks:
- name: Deploy config
ansible.builtin.template:
src: app.conf.j2
dest: /etc/myapp/app.conf
А если допустим частичный отказ (например, несколько хостов могут быть временно недоступны), можно ограничить процент:
- name: Deploy with tolerated failures
hosts: web
serial: 10
max_fail_percentage: 10
tasks:
- name: Ensure package present
ansible.builtin.apt:
name: curl
state: present
Loop'ы (loop, with_items, with_dict)
loop — современный базовый способ.
- name: Install common packages
ansible.builtin.apt:
name: "{{ item }}"
state: present
loop:
- curl
- jq
- ca-certificates
Цикл по словарю:
- name: Create app directories
ansible.builtin.file:
path: "{{ item.value.path }}"
state: directory
owner: "{{ item.value.owner }}"
mode: "{{ item.value.mode }}"
loop: "{{ (app_dirs | dict2items) }}"
vars:
app_dirs:
logs: { path: /var/log/myapp, owner: app, mode: "0755" }
data: { path: /var/lib/myapp, owner: app, mode: "0750" }
Production best practices:
- избегать вложенных циклов и сложных Jinja‑выражений в одной задаче — лучше вынести в vars
- использовать
labelв loop (если вывод становится нечитаемым)
Blocks (block/rescue/always)
block/rescue/always — структурированная обработка ошибок и «rollback» шагов.
Практика: fallback при установке пакета
- name: Install nginx with fallback repo
become: true
block:
- name: Install nginx from default repos
ansible.builtin.apt:
name: nginx
state: present
update_cache: true
rescue:
- name: Add fallback repository (example)
ansible.builtin.apt_repository:
repo: "ppa:nginx/stable"
state: present
- name: Install nginx from fallback repo
ansible.builtin.apt:
name: nginx
state: present
update_cache: true
always:
- name: Ensure nginx service enabled
ansible.builtin.service:
name: nginx
enabled: true
Практика: «rollback» через rescue
- name: Deploy config with rollback
become: true
block:
- name: Backup current config
ansible.builtin.copy:
src: /etc/myapp/config.yaml
dest: /etc/myapp/config.yaml.bak
remote_src: true
changed_when: false
failed_when: false
- name: Deploy new config
ansible.builtin.template:
src: config.yaml.j2
dest: /etc/myapp/config.yaml
mode: "0644"
notify: Restart myapp
rescue:
- name: Restore previous config
ansible.builtin.copy:
src: /etc/myapp/config.yaml.bak
dest: /etc/myapp/config.yaml
remote_src: true
notify: Restart myapp
Production best practices:
blockпомогает держать «сценарий» в одном месте и не размазыватьwhenпо всему файлу- «rollback» в Ansible часто ограничен (особенно для миграций), поэтому для критичных изменений нужно иметь отдельный plan B (например, фича‑флаги, быстрое переключение)
Includes vs Imports
Ключевая разница:
import_tasks/import_role— статически: Ansible «раскрывает» задачи на этапе парсингаinclude_tasks/include_role— динамически: подключение происходит во время выполнения (можно условно включать)
Пример: условная логика по OS через include_tasks
- name: OS-specific tasks
ansible.builtin.include_tasks: "{{ ansible_os_family | lower }}.yml"
Файлы:
tasks/debian.yml
tasks/redhat.yml
Production best practices:
- для простых случаев предпочитать
import_tasks(предсказуемее) - для OS‑ветвлений и условных веток —
include_tasks
Факты (facts, setup module)
Факты (ansible_facts) позволяют писать кросс‑платформенные playbooks.
- name: Gather facts (явно)
ansible.builtin.setup:
- name: Print OS info
ansible.builtin.debug:
msg: "OS={{ ansible_facts['distribution'] }} {{ ansible_facts['distribution_version'] }}"
Production best practices:
- если playbook очень большой и факты не нужны — можно отключать gather_facts (ускорение), но тогда не использовать
ansible_*факты - не «хардкодить» пути и имена сервисов — использовать ветвления по
ansible_os_family,ansible_service_mgrи т.д.
Практика (итоговые задания)
- Обработать ошибки: реализовать fallback при установке пакета через
block/rescue/always - Условная логика для разных OS:
include_tasksна основеansible_os_family - Rollback: резервная копия файла + восстановление в
rescue
Production best practices (сводка)
- Читаемость важнее «умного Jinja»: лучше чуть больше файлов, но проще сопровождение
- Idempotency и handlers: никаких лишних рестартов
- Минимум shell: модули → корректный diff/changed и меньший риск
- Теги для безопасных частичных прогонов (
--tags config,--tags users) - Диагностика:
changed_when/failed_whenделают отчёты предсказуемыми (и CI надёжнее)