Как обработать отказ операции в Ansible playbook

Ответ

В Ansible я управляю ошибками через комбинацию директив ignore_errors, failed_when, changed_when и блоков rescue. Это позволяет писать идемпотентные и отказоустойчивые плейбуки.

1. Игнорирование ожидаемых ошибок (ignore_errors): Использую, когда неудача задачи не должна останавливать выполнение всего плейбука.

- name: Остановить сервис, если он запущен
  ansible.builtin.systemd:
    name: my-old-service
    state: stopped
  ignore_errors: yes  # Не падать, если сервис уже остановлен или не существует
  register: stop_result

2. Кастомные условия неудачи (failed_when): Позволяет определить, что считать ошибкой, на основе вывода задачи. Это мощнее простого ignore_errors.

- name: Проверить, что порт 8080 свободен
  ansible.builtin.shell: netstat -tln | grep ':8080'
  register: port_check
  failed_when: port_check.rc == 0  # Считаем ошибкой, если порт занят (команда grep нашла совпадение)
  changed_when: false

3. Блоки обработки ошибок (block / rescue / always): Аналогично try-catch-finally в языках программирования. Основной инструмент для сложных сценариев.

- name: Развертывание с откатом при ошибке
  block:
    - name: Копировать новую конфигурацию
      ansible.builtin.copy:
        src: /tmp/new_nginx.conf
        dest: /etc/nginx/nginx.conf
    - name: Проверить синтаксис nginx
      ansible.builtin.command: nginx -t
      register: nginx_test
    - name: Перезагрузить nginx
      ansible.builtin.systemd:
        name: nginx
        state: reloaded
  rescue:
    - name: Восстановить старую конфигурацию при ошибке
      ansible.builtin.copy:
        src: /tmp/backup_nginx.conf
        dest: /etc/nginx/nginx.conf
      notify: restart nginx
    - name: Записать ошибку в лог
      ansible.builtin.debug:
        msg: "Развертывание провалилось на шаге '{{ ansible_failed_task.name }}'. Конфигурация откачена."
  always:
    - name: Всегда удалять временные файлы
      ansible.builtin.file:
        path: /tmp/new_nginx.conf
        state: absent

Практический пример из CI/CD: При развертывании в Kubernetes может потребоваться проверить, что новый под запустился, прежде чем удалять старый.

- name: Развернуть новую версию приложения
  kubernetes.core.k8s:
    definition: "{{ lookup('file', 'deployment.yaml') }}"
    wait: yes
    wait_timeout: 60
  register: deployment_result
  failed_when: not deployment_result.resources[0].status.availableReplicas

Ответ 18+ 🔞

А, ну так вот, слушай, про управление ошибками в Ansible. Это ж, блядь, целая наука, если честно. Ты представь: пишешь плейбук, запускаешь его на десятке серверов, а он на первом же падает, как подкошенный, из-за какой-нибудь ерунды. И вся автоматизация коту под хвост. Так что тут надо хитрить, как лиса, и предвидеть всякое.

1. Игнорирование ожидаемых ошибок (ignore_errors): Это как раз для тех случаев, когда ты знаешь, что может пойти по пизде, но тебе похуй. Ну, не совсем похуй, но ты готов к этому. Например, пытаешься остановить сервис, а его уже нету. Без этой директивы плейбук просто накроется медным тазом.

- name: Остановить сервис, если он запущен
  ansible.builtin.systemd:
    name: my-old-service
    state: stopped
  ignore_errors: yes  # Не падать, если сервис уже остановлен или не существует
  register: stop_result

Смотри, тут главное — register. Потому что если ты ошибку проигнорировал, то тебе же надо знать, что там произошло, а? А то остановил ты его или нет, нихуя не понятно. Так что всегда регистрируй результат, чувак, доверия ебать ноль ко всему.

2. Кастомные условия неудачи (failed_when): Вот это уже поинтереснее. ignore_errors — это тупой молоток, а failed_when — хирургический скальпель. Ты сам решаешь, что считать пиздецом, а что — нормой.

- name: Проверить, что порт 8080 свободен
  ansible.builtin.shell: netstat -tln | grep ':8080'
  register: port_check
  failed_when: port_check.rc == 0  # Считаем ошибкой, если порт занят (команда grep нашла совпадение)
  changed_when: false

Понимаешь прикол? Команда grep возвращает код 0, если нашла совпадение. То есть если порт занят. А нам это как раз не надо! Вот мы и говорим: «Ансибл, ёпта, если команда выполнилась успешно (rc == 0), то это и есть наша ошибка! Падай!» А changed_when: false — это чтобы он не считал, что что-то изменил, просто проверка.

3. Блоки обработки ошибок (block / rescue / always): Это, блядь, высший пилотаж. Прямо как в нормальных языках: try — catch — finally. Охуенно удобная штука для сложных операций, где надо иметь план «Б».

- name: Развертывание с откатом при ошибке
  block:
    - name: Копировать новую конфигурацию
      ansible.builtin.copy:
        src: /tmp/new_nginx.conf
        dest: /etc/nginx/nginx.conf
    - name: Проверить синтаксис nginx
      ansible.builtin.command: nginx -t
      register: nginx_test
    - name: Перезагрузить nginx
      ansible.builtin.systemd:
        name: nginx
        state: reloaded
  rescue:
    - name: Восстановить старую конфигурацию при ошибке
      ansible.builtin.copy:
        src: /tmp/backup_nginx.conf
        dest: /etc/nginx/nginx.conf
      notify: restart nginx
    - name: Записать ошибку в лог
      ansible.builtin.debug:
        msg: "Развертывание провалилось на шаге '{{ ansible_failed_task.name }}'. Конфигурация откачена."
  always:
    - name: Всегда удалять временные файлы
      ansible.builtin.file:
        path: /tmp/new_nginx.conf
        state: absent

Смотри как красиво. В block ты делаешь рискованную операцию — замена конфига nginx. Если на любом этапе (копирование, проверка синтаксиса, перезагрузка) случается пиздец, выполнение прыгает сразу в rescue. Там ты откатываешь всё нахуй к старой конфигурации и пишешь в лог, что всё плохо. А секция always выполнится в любом случае — хоть всё прошло удачно, хоть нет. Идеально для уборки за собой, чтобы не оставлять временные файлы, как распиздяй какой-нибудь.

Практический пример из CI/CD: Ну и куда же без этого. Вот типичная задача: развернуть в кубере и убедиться, что под поднялся, прежде чем старый удалять.

- name: Развернуть новую версию приложения
  kubernetes.core.k8s:
    definition: "{{ lookup('file', 'deployment.yaml') }}"
    wait: yes
    wait_timeout: 60
  register: deployment_result
  failed_when: not deployment_result.resources[0].status.availableReplicas

Тут failed_when опять рулит. Мы ждём 60 секунд (wait_timeout), и если за это время количество доступных реплик не станет больше нуля, то считаем, что развертывание не удалось. И плейбук упадёт, не дав тебе удалить старую версию и оставить сервис в полной жопе. Умно, да? Э бошка думай, прежде чем что-то ломать.

Короче, суть в чём: не надейся, что всё всегда будет гладко. Предполагай худшее, обрабатывай ошибки явно, и тогда твои плейбуки будут не просто скриптами, а надёжными, ебать, инструментами, которые не подведут в самый ответственный момент. Иначе будет вам хиросима и нигерсраки на продакшене, а тебя потом спросят: «Ну и какого хуя?»