ADG Оставить заявку
Блог Автоматизация 5 мин чтения

Ansible 2.4: include_tasks vs import_tasks - как мы поломали динамические роли при миграции

Ansible 2.4 депрекирует include в пользу include_tasks/import_tasks. Разница в timing выполнения сломала несколько динамических ролей - разбираем типичные ловушки при переходе.

Контекст момента

Ansible 2.4 - include_tasks/import_tasks вместо include, расширенные network facts, checkpoint для долгих playbook

Ansible 2.4 вышел на прошлой неделе и принёс несколько приятных вещей: расширенные network_facts для сетевых модулей, механизм checkpoint для долгих playbook, и поддержку inventory-плагинов. Но главное событие для нашей кодовой базы - официальная депрекация include в пользу двух отдельных директив: include_tasks и import_tasks. Предупреждения о депрекации появились ещё в 2.3, но в 2.4 они стали громче и настойчивее. Мы решили не ждать и прогнали миграцию по всем нашим плейбукам. Сломали несколько динамических ролей. Разбираем что именно и почему.

В чём разница между import и include

На первый взгляд кажется что это просто переименование. Это не так.

import_tasks - статический импорт. Ansible обрабатывает его на этапе парсинга плейбука, до запуска. Задачи из импортированного файла становятся частью play «в момент компиляции». Это означает что теги, условия when и прочее работают на уровне каждой отдельной задачи из импортированного файла.

include_tasks - динамическое включение. Файл читается и подключается в момент выполнения, когда Ansible доходит до этой строки. Теги и условие when, навешенные на саму include_tasks, применяются ко всему блоку целиком - не к каждой задаче внутри.

Старый include пытался делать что-то среднее в зависимости от контекста, и именно это стало источником неопределённости. Разработчики решили разделить чётко.

Что у нас сломалось

У нас накопился десяток ролей, где include использовался с переменными в именах файлов - классический паттерн динамической диспетчеризации:

- include: "tasks/{{ ansible_distribution }}.yml"

Такой include был динамическим по природе - имя файла вычислялось в runtime. При замене на import_tasks Ansible начал падать с ошибкой ещё на этапе парсинга: в момент компиляции переменная ansible_distribution ещё не определена, facts не собраны, значение неизвестно.

Правильный путь здесь - include_tasks. Не import_tasks.

Звучит очевидно постфактум. Но у нас было несколько мест где мы по инерции написали import_tasks везде, а потом чесали голову глядя на ошибку парсинга.

Вторая ловушка: теги и include_tasks

Допустим у нас роль с задачами разбита на файлы по логическим группам, и мы хотим запускать отдельные группы через теги:

- include_tasks: tasks/packages.yml
  tags: packages

- include_tasks: tasks/config.yml
  tags: config

С import_tasks это работало бы ожидаемо: тег packages применялся бы к каждой задаче внутри packages.yml, и ansible-playbook --tags packages запускал бы именно их.

С include_tasks тег packages применяется к самой задаче включения. А задачи внутри packages.yml тегов не имеют - если только они не прописаны там явно. В итоге --tags packages не запускает ничего, потому что сама include_tasks не является задачей с результатом - это директива, которая должна выполниться чтобы задачи появились. Поведение контринтуитивное.

Решения два: либо прописывать теги в каждой задаче внутри подключаемого файла, либо переходить на import_tasks там где нужны теги - но тогда нельзя использовать переменные в именах файлов. Выбирать нужно осознанно.

Третья ловушка: условие when

Похожая история с when. У import_tasks условие when копируется в каждую задачу из импортируемого файла. У include_tasks - оно применяется к самой директиве включения. Для большинства случаев результат одинаковый, но если внутри включаемого файла есть задачи, которые меняют переменные, на которые опирается when - поведение расходится.

В одной из наших ролей была конструкция вроде:

- include: tasks/detect_version.yml

- include: tasks/apply_patch.yml
  when: patch_needed | bool

Переменная patch_needed выставлялась в detect_version.yml. При переходе на import_tasks Ansible пытался вычислить patch_needed на этапе парсинга - и не находил её, потому что она появляется только после выполнения первого файла. Здесь правильный выбор - include_tasks для обоих блоков.

Что с network facts в 2.4

Помимо всей этой истории с include, в 2.4 расширили network_facts для сетевых модулей. В 2.3 мы уже переводили парк Cisco под Ansible и упирались в то, что ios_facts собирает не всё что нужно. В 2.4 добавились факты о маршрутах, OSPF-соседях и более детальная информация по интерфейсам для ряда платформ. Для наших сетевых плейбуков это полезно - сможем убрать несколько костылей с ios_command и ручным парсингом вывода.

Checkpoint-механизм для долгих playbook пока смотрим теоретически - у нас нет прогонов которые бы реально прерывались и требовали восстановления. Но идея сохранять состояние выполнения и возобновлять с нужного места правильная.

Как делали миграцию

Прогнали grep -r "^\s*- include:" . по всем ролям. Получили список примерно из ста строк. Для каждого вхождения смотрели на два вопроса: имя файла статическое или динамическое, и используются ли теги.

  • Динамическое имя (переменная в пути) - include_tasks, без вариантов.
  • Статическое имя + теги нужны - import_tasks.
  • Статическое имя + теги не важны - в принципе оба варианта, выбирали import_tasks как умолчание для предсказуемости.

После замены прогнали --syntax-check по всем плейбукам. Нашли ещё пару мест где у нас было when с переменными из facts, которые вычислялись раньше - там import_tasks тоже был неправильным выбором.

Итог

Миграция заняла день с хвостом. Большинство ролей прошли без сюрпризов. Сюрпризы были там где include использовался нетривиально - динамические пути и теги. Предупреждения об устаревании в 2.3 мы читали и считали что понимаем разницу. Оказалось, понимали примерно.

В managed-проектах где у нас сотни задач по автоматизации - лучше разобраться с этим сейчас, пока include просто выдаёт warnings, а не падает с ошибкой. Когда его выпилят совсем - разбираться будет неприятнее.

Контакт

Нужна такая же инженерная работа?

Опишите задачу и контекст. Ответим в течение рабочего дня, при необходимости подпишем NDA.