Короткий ответ
Модули shell/command по умолчанию не умеют определять, изменилось ли что-то на самом деле — Ansible просто помечает их как changed, если команда завершилась с кодом 0, независимо от того, создала ли она что-то новое; правильное решение — заменить shell-команду на специализированные модули ansible.builtin.file (для создания директории и смены владельца) и, если команда необходима, использовать параметры creates/removes или явный changed_when на основе анализа вывода.
Как это работает подробнее
специализированные модули (file, copy, template, package и т.п.) перед действием проверяют текущее состояние системы (есть ли файл, какой у него владелец, какое содержимое) и сообщают changed только если реальное состояние отличается от желаемого; у shell/command такой проверки нет, потому что Ansible не умеет заранее знать семантику произвольной команды, поэтому по умолчанию он консервативно считает любой успешный запуск изменением.
Пример кода
# было: неидемпотентно
- name: Create release dir
ansible.builtin.shell: >
mkdir -p /opt/app/releases/{{ release_id }} &&
chown app:app /opt/app/releases/{{ release_id }}
# стало: идемпотентно
- name: Create release dir
ansible.builtin.file:
path: "/opt/app/releases/{{ release_id }}"
state: directory
owner: app
group: app
mode: "0755"Что использовать на практике
- Практика. использовать
shell/commandстоит только когда нет подходящего модуля, и в этом случае обязательно задаватьcreates: /opt/app/releases/{{ release_id }}(задача не выполнится повторно, если путь уже существует) илиchanged_when: false/кастомное условие поstdout, если команда действительно безопасно перезапускаемая, но сама по себе не меняет состояние на повторных запусках; для создания директорий и прав всегда предпочтительнееansible.builtin.file, который нативно идемпотентен.
Подводные камни
- Ошибка. частая ошибка — оставлять задачи
shell/commandпомеченными какchangedна каждом прогоне и игнорировать это, из-за чего handlers, завязанные наnotifyот такой задачи, срабатывают при каждом запуске плейбука (например, лишний перезапуск сервиса); другая ошибка — добавлятьchanged_when: falseтам, где команда действительно может что-то менять, что маскирует реальные изменения и усложняет аудит.