← Ansible
14 мин · средне · Урок 5 из 12

Идемпотентность и почему bashible плохо

changed vs ok, почему command/shell не идемпотентны, creates/removes, changed_when, --check и --diff.

Что такое идемпотентность

Идемпотентность — свойство операции, при котором повторное выполнение с теми же параметрами не меняет результат.

Запусти playbook один раз или пять, состояние системы на выходе одинаковое. Это не магия. Это философия Ansible: каждый модуль проверяет текущее состояние и делает что-то, только если оно отличается от желаемого.

Статусы задач: ok, changed, failed

После каждой задачи Ansible выводит один из трёх статусов.

В конце каждого прогона выводится PLAY RECAP:

PLAY RECAP *****
web-1 : ok=5  changed=2  unreachable=0  failed=0

Хорошо написанный playbook на втором прогоне показывает changed=0. Всё ok: система уже в нужном состоянии, менять нечего.

Нормальные модули идемпотентны по умолчанию

Модуль apt проверяет: пакет уже установлен? Тогда ok. Нет? Ставит и возвращает changed.

- name: install nginx
  apt:
    name: nginx
    state: present

Первый прогон возвращает changed: nginx не стоял, поставил. На втором уже ok, пакет на месте и делать нечего.

То же самое с другими модулями:

Башибл: антипаттерн

«Башибл» — это когда вместо нормальных модулей весь playbook написан через command и shell.

# ПЛОХО - антипаттерн
- name: install nginx
  shell: apt-get install -y nginx

- name: create directory
  command: mkdir -p /var/www/html

- name: copy config
  shell: cp /tmp/nginx.conf /etc/nginx/nginx.conf

Модули command и shell не идемпотентны. Ansible не знает, что делает твоя команда и какой у неё эффект. Поэтому он:

  1. Выполняет её при каждом прогоне, без исключений (если не задан creates/removes).
  2. По умолчанию ставит статус changed, независимо от того, менялось ли что-то (это поведение можно переопределить через changed_when:).

Что теряешь:

Когда command/shell всё же нужны

Иногда специализированного модуля нет. Но тогда сделай command/shell идемпотентным вручную.

creates= — не выполнять, если файл/путь уже существует:

- name: init database
  command: /opt/app/bin/init-db
  args:
    creates: /var/lib/app/.db_initialized

Если /var/lib/app/.db_initialized уже есть, задача пропускается (ok). Сама команда должна создать этот файл при первом выполнении.

removes= — выполнять, только если путь существует:

- name: clean up temp archive
  command: rm /tmp/setup.tar.gz
  args:
    removes: /tmp/setup.tar.gz

changed_when: — явно сказать, когда считать задачу changed:

- name: check app version
  command: /opt/app/bin/app --version
  register: app_version
  changed_when: false          # только читаем - никогда не changed
- name: update cache
  shell: update-cache.sh
  register: cache_result
  changed_when: "'updated' in cache_result.stdout"

when: — вообще не выполнять, если условие не совпало:

- name: configure firewall
  command: ufw allow 80
  when: ansible_os_family == "Debian"

Главный совет: прежде чем писать shell, поищи модуль. С файлами работают copy, template, file. Пакеты ставят apt, yum и dnf, сервисами управляют service и systemd. Для git есть git, для docker — community.docker.*.

Dry-run: --check и --diff

--check запускает playbook без реальных изменений. Ansible показывает, что бы произошло, но ничего не трогает.

ansible-playbook -i inventory.ini site.yml --check

--diff добавляет построчный diff для файлов и шаблонов: видно, что именно изменилось бы в конфиге.

ansible-playbook -i inventory.ini site.yml --check --diff

Нормальные модули уважают --check. Модули command и shell в check-mode по умолчанию пропускаются (skipped). Ansible не может предсказать, что сделает произвольная команда. Исключение: если у задачи указан creates: или removes:, в check-режиме модуль проверит файл и вернёт корректный changed. Это ещё одна причина не писать башибл: в dry-run ты не видишь, что будет делать половина playbook'а.

Прогон 1 Прогон 2 apt: name=nginx state=present apt: name=nginx changed apt: name=nginx ok shell: apt-get install nginx shell: apt-get ... changed shell: apt-get ... changed (всегда!)

На втором прогоне apt понимает, что nginx уже стоит, и сообщает ok. shell выполняется снова и снова, потому что Ansible не умеет заглянуть внутрь команды.

Что важно запомнить

Идемпотентность — главный принцип Ansible. Хороший playbook на втором прогоне ничего не меняет: changed=0.

Нормальные модули проверяют состояние сами. command/shell — нет: всегда changed, выполняются каждый прогон. Если без shell не обойтись, добавляй creates=, removes= или changed_when:.

--check --diff — безопасный dry-run. Но command/shell в check-mode пропускаются, и это ещё причина использовать специализированные модули.

В следующем уроке соберём первый playbook.

Дрилл · вопросы как на собесе

Проверь себя без подсказок

Сформулируй ответ вслух или письменно, потом раскрой и честно отметь: знал или нет.

01

Идемпотентность Ansible — это правда или маркетинг? Назови три ситуации, когда модуль на самом деле не идемпотентен.

ANS-018
02

Как обеспечить идемпотентность собственных задач, основанных на `command`/`shell` (`creates`, `removes`, `changed_when`, `failed_when`)?

ANS-027
03

Что такое идемпотентность простыми словами, с примером модуля?

ANS-008

Прошли материал до конца? Отметьте урок пройденным.