SSD Nodes Learn Hosting plans →
Руководства Matt ConnorАвтор: Matt Connor

Как работают Ansible --check и --diff для dry run

Узнайте, как использовать флаги --check и --diff для безопасного тестирования playbook. Разбираем, почему модули без поддержки check mode могут давать ложные результаты.

Что делает режим проверки Ansible

Режим проверки (check mode) в Ansible — это пробный запуск: ansible-playbook --check подключается к каждому хосту в сценарии, запрашивает у каждого модуля, соответствует ли текущее состояние целевому, и сообщает о возможных изменениях, не внося их. Добавьте --diff, чтобы увидеть содержимое файлов до и после внесения изменений. Вместе эти параметры отвечают на вопрос, который стоит задать перед любым реальным запуском: что именно изменится на этих серверах?

Режим проверки не является симуляцией вашего playbook. Модели сервера не существует. Каждый модуль просто получает запрос на чтение вместо записи. Модуль, способный работать в режиме только для чтения, сообщает changed и продолжает работу. Модуль, который не поддерживает такой запрос, ничего не делает и ничего не сообщает. В документации Ansible это сформулировано одной фразой: «Модули, не поддерживающие режим проверки, ничего не сообщают и ничего не делают». Этот пробел — причина, по которой пробный запуск может дать неверный ответ, поэтому большая часть данного руководства посвящена именно этому ограничению.

Запуск пробного прогона: --check и --diff

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

-C и -D — это сокращенные формы двух флагов. Использование --limit является намеренным. Разницу (diff) для одного хоста можно прочитать. Разницу для двадцати хостов приходится пролистывать.

Весь отчет строится на четырех ключевых словах.

  • ok: [web1] означает, что задача выполнена и изменений не обнаружено. Для модуля, управляющего состоянием (например, apt или template), это значит, что текущее состояние уже соответствует целевому. Для debug или set_fact это просто означает, что задача была выполнена.
  • changed: [web1] означает, что модуль прогнозирует внесение изменений. При использовании --diff строки выше показывают, какие именно. Большинство модулей делают точные прогнозы, но некоторые, имеющие частичную поддержку режима проверки, предупреждают об обратном в своей документации: get_url в режиме проверки отправляет только HEAD-запрос и сообщает, что статус изменений может быть неверным.
  • skipping: [web1] означает, что задача не выполнялась. Либо её условие when было вычислено как false, либо модуль не поддерживает работу в режиме проверки.
  • fatal: [web1] означает, что при проверке произошел сбой задачи. Прочитайте сообщение об ошибке, прежде чем делать вывод о неисправности playbook.

--diff выводит унифицированный diff для файловых модулей: удаленные строки помечаются -, а добавленные — +. Вывод сопровождается заголовком, строки которого начинаются с --- before и +++ after и содержат путь к целевому файлу. Модули, которые не записывают файлы, выводят свои собственные данные «до» и «после», поэтому ansible.builtin.user отображает атрибуты, которые будут изменены, а не содержимое файла.

Включите diff на постоянной основе в ansible.cfg, чтобы не забывать использовать этот флаг:

[diff]
always = true
context = 5

Перед использованием режима проверки стоит выполнить две более быстрые проверки. ansible-playbook site.yml --syntax-check выполняет синтаксический разбор YAML и структуры play без подключения к хостам. ansible-playbook site.yml --list-tasks выводит список задач, которые будут выполнены; это помогает понять, что роль, которую вы считали помеченной тегом, на самом деле таковой не является. Оба метода не требуют сетевых соединений и работают мгновенно.

Сам режим проверки устанавливает соединения. Он открывает SSH-сессию к каждому хосту в паттерне и собирает факты, поэтому недоступный хост приведет к сбою пробного прогона. Это само по себе является полезным сигналом, и именно поэтому определение поведения playbook при недоступности хостов имеет значение до того, как вы добавите пробный прогон в CI.

Почему режим проверки (check mode) завершается ошибкой на новом сервере

Этот плейбук корректен. Запустите его с флагом --check на сервере, где еще нет nginx, и большинство задач завершатся с ошибкой.

- name: Install nginx
  ansible.builtin.apt:
    name: nginx
    state: present

- name: Write the site config
  ansible.builtin.template:
    src: site.conf.j2
    dest: /etc/nginx/conf.d/site.conf

- name: Start and enable nginx
  ansible.builtin.service:
    name: nginx
    state: started
    enabled: true

Задача apt сообщает о changed, и это верно: пакет отсутствует, поэтому реальный запуск привел бы к его установке. В режиме проверки установка не выполняется. Затем задача template завершается ошибкой, так как /etc/nginx/conf.d/ не существует на этом хосте, и никто его не создал. Задача service также завершается ошибкой, поскольку нет юнита nginx, состояние которого можно было бы запросить. Ни один из этих сбоев не является ошибкой в плейбуке. При «сухом» прогоне не было достигнуто необходимое состояние, что и подразумевает документация, предупреждая: режим проверки не может дать полезный результат для задачи, входные данные которой зависят от изменений, внесенных предыдущей задачей.

Честная формулировка правила такова: режим проверки наиболее точен для хоста, который уже был приведен к целевому состоянию, и выдает много шума на новом сервере. Запуск с --check, при котором каждая задача сообщает ok, — это реальный показатель того, что хост находится в целевом состоянии, так как это означает отсутствие изменений. При этом существуют два нюанса, актуальных для любого хоста: пропущенная задача command — это «пустое место», а не ответ, а модуль с частичной поддержкой режима проверки, например get_url, может неверно сообщать о своем статусе изменений. На совершенно новом хосте --check в основном лишь подтверждает, что хост новый. Когда вы пишете свой первый плейбук Ansible для VPS, ожидайте, что первый «сухой» прогон будет выглядеть как сплошная стена красного текста, и оценивайте плейбук по результатам второго запуска.

Почему задачи command и shell пропускаются в режиме проверки

ansible.builtin.command и ansible.builtin.shell не знают, что именно делает ваша команда. Не существует способа выполнить произвольный бинарный файл в режиме «только чтение», поэтому в режиме проверки модуль отказывается его запускать. Результат задачи содержит skipped: true, а сообщение — Command would have run if not in check mode, в выводе вы увидите skipping: [web1].

В документации модуля поддержка режима проверки названа «частичной», а предлагаемый обходной путь — это creates и removes. Укажите в задаче путь creates, и режим проверки сможет хотя бы выполнить проверку файла:

- name: Extract the release bundle
  ansible.builtin.command: /usr/bin/tar xf /tmp/app.tar.gz -C /opt/app
  args:
    creates: /opt/app/bin/app

Если /opt/app/bin/app уже существует, режим проверки сообщит Would not run command since '/opt/app/bin/app' exists, что является достоверным ответом. Если путь отсутствует, вы получите Command would have run if not in check mode, что также является достоверным ответом. Без creates эта задача будет «пустым местом» в вашем пробном запуске.

Последствия этого хуже, чем просто пустое место. Пропущенная задача command всё равно регистрирует результат, и этот результат содержит stdout, но это пустая строка, так как ничего не выполнялось. Условие следующей задачи вычисляется на основе этой пустой строки и незаметно выбирает неверную ветку: проверка версии не находит версию, и тест вида 'active' in svc.stdout возвращает false. Ваш playbook выдаёт один результат при реальном запуске и другой — при пробном, при этом нет никакой ошибки, на которую можно было бы указать. Это самый запутанный сбой во всей этой функциональности. Ошибка 'dict object' has no attribute 'stdout' относится к другому типу пропуска: задача, пропущенная из-за того, что её when был ложным, регистрирует результат вообще без ключа stdout.

check_mode: false и единственное место, где этот параметр уместен

check_mode: false в задаче означает «выполнить по-настоящему, даже в режиме --check». Это решение проблемы пропущенных команд, и оно безопасно только для задач, выполняющих чтение.

- name: Read the installed app version
  ansible.builtin.command: /usr/local/bin/app --version
  register: app_version
  check_mode: false
  changed_when: false

Эта задача корректно работает в обоих режимах. Она считывает версию и ничего не записывает; changed_when: false предотвращает ложные отчеты об изменениях, а check_mode: false позволяет app_version.stdout содержать реальную версию во время пробного запуска вместо пустой строки, благодаря чему условия, основанные на этом значении, вычисляются верно.

Понимайте этот параметр буквально, прежде чем вставлять его куда-либо еще. Задача с check_mode: false вносит изменения на серверы даже во время ansible-playbook --check. Если добавить его в задачу apt или template, чтобы пробный запуск выглядел аккуратнее, он перестанет быть пробным. Если задачу записи нельзя сделать безопасной, используйте вместо этого ограничение:

- name: Apply the database migration
  ansible.builtin.command: /usr/local/bin/app migrate --apply
  when: not ansible_check_mode

ansible_check_mode — это специальная переменная, которую Ansible устанавливает в true во время проверочного запуска. Существует и обратный параметр. check_mode: true принудительно переводит задачу в режим проверки всегда, даже во время реального запуска, превращая её в средство обнаружения отклонений: сохраните результат, и если changed сообщает об изменениях, значит, состояние хоста больше не соответствует заданным в задаче требованиям.

Почему задача сообщает об изменениях при каждом запуске

Запустите playbook дважды подряд без каких-либо изменений между запусками. При втором запуске каждая задача должна сообщать ok. Если какая-либо задача продолжает сообщать changed, это означает одно из двух: модуль не может определить состояние объекта, которым управляет, либо входные данные нестабильны. Обе проблемы решаемы, и ни одна из них не является «шумом», который нужно просто игнорировать.

  • command и shell без creates, removes или changed_when сообщают changed каждый раз, так как у модуля нет способа узнать, произошло ли что-то. Добавьте creates или настройте changed_when для проверки строки в выводе.
  • ansible.builtin.file с state: touch по своей природе сообщает changed при каждом запуске, так как обращение к файлу обновляет его временные метки. Используйте state: file, если вам нужно было только установить владельца или права доступа.
  • template, чей итоговый результат меняется, перезаписывает файл при каждом запуске. Временная метка из ansible_date_time, вызов now() или пароль, генерируемый заново, создают разные байты, поэтому модуль корректно сообщает об изменении. Уберите изменяющееся значение из шаблона.
  • ansible.builtin.user с password: "{{ pw | password_hash('sha512') }}" меняется при каждом запуске, так как password_hash выбирает случайную соль при каждом вызове, поэтому итоговый хеш никогда не совпадает с тем, что уже находится в /etc/shadow. Передайте явную соль, основанную на стабильных данных.
  • state: latest в модуле управления пакетами сообщает changed всякий раз, когда доступно обновление. Это честное поведение. Именно поэтому state: latest делает результат выполнения playbook непредсказуемым. Используйте state: present и выполняйте обновления целенаправленно.
  • ansible.builtin.unarchive, указывающий на URL без creates, загружает архив при каждом запуске, а для типов архивов, которые модуль не может сравнить с целевой директорией, он также выполняет распаковку заново. Укажите creates путь.

--diff — самый быстрый способ разобраться в причинах. Если задача сообщает changed, а diff показывает различающиеся байты, значит, ваши входные данные нестабильны. Если она сообщает changed, а diff ничего не показывает, значит, модуль не может выразить, что именно изменилось; обычно это указывает на задачу command или запись только метаданных, например, временной метки.

Не используйте changed_when: false, чтобы заглушить «шумную» задачу. Это подавляет отчет, поэтому notify никогда не срабатывает, и обработчик (handler), перезапускающий сервис, не выполняется. Вместо этого исправьте саму задачу.

Уменьшение радиуса поражения: --limit, --tags и --step

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

  • --limit: позволяет ограничить выполнение плейбука конкретными хостами или группами из инвентаря. Например, ansible-playbook site.yml --limit webservers применит изменения только к группе webservers.
  • --tags: запускает только те задачи, которые помечены определенными тегами. Это полезно для выборочного обновления конфигурации без перезапуска всего процесса.
  • --step: переводит Ansible в интерактивный режим. Система будет запрашивать подтверждение перед выполнением каждой задачи, что позволяет контролировать процесс пошагово.

Использование этих флагов снижает риск непреднамеренных сбоев при работе с продуктивными средами.

Режим проверки (check mode) показывает, какие изменения будут внесены. Эти флаги определяют, сколько машин получат обновления одновременно.

--limit ограничивает выполнение подмножеством инвентаря. Параметр принимает те же шаблоны, что и hosts:, поэтому работают как --limit web1, так и --limit 'webservers:!web3'. Шаблон необходимо брать в кавычки. Неэкранированный ! в интерактивной сессии bash вызывает расширение истории по восклицательному знаку, и оболочка переписывает команду до того, как её получит Ansible.

Проверяйте шаблон перед тем, как доверять ему. ansible-playbook site.yml --limit 'webservers:!web3' --list-hosts выводит список совпавших хостов и завершает работу, не подключаясь ни к одному из них. Шаблон, который не находит совпадений, безопасен, так как Ansible не переходит к выполнению на всём инвентаре. В этом случае выводится предупреждение о том, что шаблон не совпал ни с одним хостом, после чего программа завершается с ошибкой, сообщающей, что инвентарь, шаблон хостов и --limit не определили целевые хосты. Понимание того, как файл инвентаря определяет эти группы, делает поведение шаблона предсказуемым.

--tags deploy запускает только помеченные задачи, а --skip-tags packages выполняет всё остальное. --list-tags выводит список доступных задач. Теги становятся полезны, когда playbook разрастается настолько, что вы перестаете запускать его целиком; это также одна из причин разделения длинного playbook на роли.

--start-at-task "Write the site config" возобновляет выполнение с указанной задачи. Используйте эту возможность для восстановления, но учитывайте последствия: все задачи до указанной будут пропущены, включая те, что устанавливают факты или регистрируют переменные, необходимые для последующих шагов.

--step запрашивает подтверждение перед каждой задачей и ожидает ответа yes, no или continue. Это работает медленно, но является подходящим инструментом при первом запуске деструктивных операций, так как позволяет прервать выполнение между двумя задачами, а не после двадцати.

Развертывание изменений с использованием serial

По умолчанию Ansible выполняет одну задачу для всех хостов в play, прежде чем перейти к следующей. Это работает быстро, но означает, что неудачная задача затронет весь парк серверов одновременно. К тому моменту, как вы заметите ошибку и нажмете Ctrl-C, изменения уже будут применены везде.

serial разбивает play на пакеты. Весь play выполняется сначала для первой группы хостов, затем для следующей.

- name: Roll out the web tier
  hosts: webservers
  serial: [1, 5, "30%"]
  max_fail_percentage: 0
  tasks:
    - name: Deploy the release
      ansible.builtin.include_role:
        name: webapp

Первый пакет состоит из одного хоста. Если он успешно проходит проверку, второй пакет включает пять хостов, а все последующие — 30 процентов от общего числа хостов в play. max_fail_percentage: 0 прерывает выполнение play, как только любой хост в пакете выдает ошибку, так как любой сбой превышает допустимый порог, равный нулю. Гарантия безопасности для одной машины обеспечивается первым пакетом, а не этой настройкой: релиз, который ломает всё, завершится с ошибкой на первом же хосте и остановится. Сбой, который проявляется только в пакете из пяти хостов, уже затронет остальные машины в этой группе, так как Ansible выполняет каждую задачу для всего пакета целиком, прежде чем перейти к следующей. any_errors_fatal: true — это более жесткий вариант, который останавливает play для всех при первой же ошибке на любом хосте.

Запуск сначала на одном хосте — это не паранойя, и на то есть конкретная причина. Состав групп в инвентаре со временем меняется. Сервер, добавленный через полгода после остальных, может иметь другой дистрибутив, содержать сервис, установленный вручную, или отличаться разметкой дисков. Playbook может быть корректным для группы, но ошибочным для конкретного хоста, и ни один прогон в режиме dry run на уже настроенном сервере этого не покажет. Управление парком серверов Linux — это во многом практика поиска нестандартных хостов до того, как это сделают изменения.

Порядок выполнения операций

  1. ansible-playbook site.yml --syntax-check выявляет ошибки в YAML и структуре без обращения к сети.
  2. ansible-playbook site.yml --limit web1 --list-hosts подтверждает, что ваш шаблон соответствует ожидаемым данным.
  3. ansible-playbook site.yml --limit web1 --check --diff — это пробный запуск. Изучите полученный diff.
  4. ansible-playbook site.yml --limit web1 --diff применяет изменения к конкретному хосту.
  5. Повторите шаг 4. Все задачи должны сообщить о статусе ok. Любой элемент со статусом changed — это задача, которую необходимо исправить, прежде чем применять изменения к остальному парку серверов.
  6. ansible-playbook site.yml --check --diff для всего инвентаря теперь возвращает осмысленный результат, так как сконвергированные хосты не выдают изменений, и вы видите только реальную разницу.

Предупреждение относительно шага 3. --diff выводит содержимое файлов в терминал и в лог CI-задания, поэтому шаблон, содержащий пароль от базы данных, отобразит этот пароль в логах. Установите diff: false для этой задачи, чтобы скрыть вывод, или no_log: true, чтобы полностью скрыть результат, а само значение храните в зашифрованном файле Ansible Vault, а не в репозитории.

FAQ

Вносит ли ansible-playbook --check какие-либо изменения на сервере?

Нет, за одним исключением, которое вы контролируете. В режиме проверки (check mode) каждый модуль должен отчитываться о состоянии вместо внесения изменений; модули, которые не поддерживают такой функционал, ничего не делают и не выводят. Исключением является ключевое слово задачи check_mode: false, которое принудительно выполняет конкретную задачу даже во время запуска --check. Проверьте свои плейбуки и роли на наличие check_mode: false, прежде чем доверять результатам пробного запуска, и убедитесь, что каждое такое вхождение относится только к задачам чтения состояния.

В чем разница между --check и --diff?

--check определяет, будут ли выполнены реальные действия. --diff определяет уровень детализации вывода. --check сам по себе лишь сообщает, что файл будет изменен. --diff сам по себе применяет изменения и показывает измененные строки. Используйте их вместе для получения читаемого отчета о пробном запуске, а также оставьте --diff включенным для реальных запусков, установив always = true в секции [diff] файла ansible.cfg.

Почему моя задача Ansible сообщает о состоянии changed при каждом запуске?

Это происходит, если модуль не может определить текущее состояние объекта или если передаваемое значение каждый раз отличается. Модули command и shell всегда сообщают changed, если вы не добавите creates или changed_when. Задача file с параметром state: touch меняет состояние по своей логике. Шаблон, который генерирует временную метку или новый пароль, создает разные байты при каждом запуске, поэтому файл действительно перезаписывается. Запустите плейбук дважды подряд: всё, что по-прежнему имеет статус changed на втором проходе, требует исправления.

Почему мои задачи command и shell пропускаются во время пробного запуска?

Потому что для произвольной команды не существует способа выполнения в режиме «только чтение». В режиме проверки модуль command устанавливает skipped: true с сообщением Command would have run if not in check mode. Добавьте creates или removes, чтобы режим проверки мог оценить результат проверки файла. Для задачи, которая только считывает состояние, установите check_mode: false вместе с changed_when: false: тогда зарегистрированный результат будет содержать реальные данные во время пробного запуска, а условия, построенные на этом результате, продолжат работать.

Почему режим проверки завершается ошибкой на новом сервере, но проходит на уже настроенном?

Потому что режим проверки не создает состояние, от которого зависят последующие задачи. Пробный запуск на хосте без установленного nginx пометит установку как changed, а затем завершится ошибкой на задаче записи в /etc/nginx/conf.d/, так как этот каталог не был создан. Это ожидаемое поведение. Режим проверки предназначен для обнаружения отклонений (drift) на хостах, которые плейбук уже привел к целевому состоянию. Он не может проверить первый запуск. На новом хосте примените плейбук к одной машине и анализируйте результаты второго запуска.

#ansible#check-mode#idempotency#automation#safety