Никогда не было и вот снова

В очередной раз возвращаюсь к своему маленькому бложику. На этот раз после 4-х летнего перерыва. Будем считать на это повлияли смена работы и самоизоляция. Продолжить я решил с небольшой заметки про нежно любимый мной Ansible. А точнее про его волшебные особенности работы с переменными и ролями. Мне всегда хотелось видеть в ролях Ansible жесткую изоляцию. Но это конечно совсем не так. Разберу на примере, то что я имею в виду, основываясь на поведении Ansible 2.9.7.

Ansible я очень активно использовал на работе в Qligent для автоматизации ручных инструкций по установке продукта. В результате за 3-4 года сформировалась достаточно большая база кастомных ролей и плейбуков. Ну а сейчас я использую Ansible исключительно для настройки домашнего HP MicroServer, на котором крутится парочка локальных сервисов в docker контейнерах. Каждый сервис фактически представляет собой docker-compose.yml, специфичные конфиги и директории, с которыми он работает.

Итак, во время написания ролей для локальных сервисов я решил вынести повторяющиеся действия необходимые для инициализации в отдельную роль. Назовем ее puzan_service. Туда попали:

  • создание отдельного пользователя
  • assert
  • создание директории с именем сервиса
  • определение необходимых фактов

Ну и роль сама получила как минимум один параметр, который определяет ее поведение - puzan_service_name. В упрощенном варианте это имя директории, в которую в последующем копируется compose файл. Теперь для инициализации сервисов достаточно в meta добавлять нечто вроде:

dependencies:
  - role: puzan_service
    vars:
      puzan_service_name: usefull_service

Примерно тут мои ожидания несколько расходятся с реальностью. Указанный подход отлично работает. Более того на текущий момент похоже это официальный подход для подключения ролей с параметрами в качестве зависимостей. Но есть нюанс — после выполнения роли в контексте останется переменная puzan_service_name. В целом может показаться, что это не страшно. Но меня это не устраивает: puzan_service предназначена для множественного использования, а это значит каждый ее запуск не должен быть связан с предыдущим. В текущем примере puzan_service_name обязательный параметр и его каждый раз необходимо определять. Но что, если у роли будет еще необязательный параметр или кто-то допустит опечатку в имени переменной? Это приведет к неожиданному поведению и вероятно сложному поиску причины ошибки, так как роль может выполнится со старыми значениями переменных.

Я начал работать с Ansible где-то около версии 1.9. И как ни странно там был немного другой метод определения зависимостей с переменными:

dependencies:
  - role: puzan_service
    puzan_service_name: usefull_service

Данный метод также до сих пор работает. Но более удивительно то, что он работает, как я хочу: puzan_service_name не попадает в глобальный контекст. Я подготовил небольшой репозиторий с тестами данного поведения. Кроме использования meta dependencies там также продемонстрирована работа использования ролей в playbook'ах, а также разница поведения import_role и include_role.

Следует отменить, что из всех конструкций только include_role на текущий момент имеет явный механизм контроля доступа к переменным снаружи. Это реализовано через параметр public.

Уже давненько имеются issues на github'е на эту тему:

Исправляться они не спешат. Один из моих PR'ов в ansible, был замержен всего лишь за 3 года. Поэтому я не думаю, что тут что-то быстро изменится.

В целом мои рекомендации свозятся к следующему:

  • Используйте только вариант передачи параметров в роль без vars
  • Не используйте import_role и include_role с public: yes
  • Есть конечно исключения: В любой непонятной ситуации — думай!

В итоге. Будьте внимательны и пишите больше тестов. Даже на роли в Anisble. Переменные и факты в Ansible здоровенная перемешенная куча. Ansible к сожалению сам не дает никаких явных инструментов по ограничению доступов к ним. Поэтому тут надежда только на Вас самих и на практики, которые вы используете. Ниже небольшая сводка по описанному поведению для версии Ansible 2.9.7

  • Использование в playbook'ах и meta dependencies

    Поведение одинаковое при подключении ролей в playbook'ах и через dependencies.

    • Без vars

      Playbook пример:

      - name: Playbook
        hosts: localhost
        roles:
          - role: some_role
            some_role_var: some
      

      Meta role пример:

      dependencies:
        - role: some_role
          some_role_var: some
      

      some_role_var не доступна во внешнем контексте до и после запуска роли

    • C vars

      Playbook пример:

      - name: Playbook
        hosts: localhost
        roles:
          - role: some_role
            vars:
              some_role_var: some
      

      Meta role пример:

      dependencies:
        - role: some_role
          vars:
            some_role_var: some
      

      some_role_var доступна во внешнем контексте до и после запуска роли

  • Использование import_role

    - import_role:
      name: some_role
      vars:
        some_role_var: some
    

    some_role_var доступна во внешнем контексте до и после запуска роли

  • Использование include_role с public: no

    - include_role:
        name: some_role
        public: no
      vars:
        some_role_var: some
    

    some_role_var не доступна во внешнем контексте до и после запуска роли

  • Использование include_role с public: yes

    - include_role:
        name: some_role
        public: yes
      vars:
        some_role_var: some
    

    some_role_var не доступна во внешнем контексте до запуска роли, но доступна после.

И в виде таблички:

Кейс Переменные до Переменные после
roles без vars Не доступны Не доступны
roles с vars Доступны Доступны
import_role Доступны Доступны
include_role, public: no Не доступны Не доступны
include_role, public: yes Не доступны Доступны


Comments

comments powered by Disqus