Привет. Я Сергей Истомин, DevOps-инженер в KTS.

Это статья о том, как сделать так, чтобы заказчику было легко пользоваться Ансиблом. Сразу оговорюсь: речь пойдет о кейсе, когда на вашей ферме 100 голов, и всех вы знаете по именам (да, так тоже бывает).

Ниже будет спорный материал. Результат описанного рефакторинга — компромисс между практикой IAC и людьми. Последняя составляющая этого компромисса требует тонкой настройки и деликатного подхода, поскольку люди в любой момент могут неосознанно начать отдаляться от IAC. Причина всегда одна — когнитивная нагрузка. Как только она начнет доставлять дискомфорт, IAC потихоньку двигают в сторону.

Оглавление

Немного предыстории

Наш заказчик сделал осознанный выбор в пользу использования виртуальных машин и отказа от контейнеризации своего продакшена. Главные факторы — это очень ёмкие по ресурсам стейтфул-приложения, которые должны очень быстро производить вычисления. Latency — один из важнейших отслеживаемых показателей, и все усилия команды заказчика направлены на сокращение этого показателя. В общем тайминге ответа каждые 10 мс на счету. А всё, что может оказать сильное взаимное влияние приложений друг на друга (включая общие компоненты цепочки обработки трафика), проходит через скрупулезную оценку баланса пользы и вреда.

Но чтобы было что подвергать сомнению, сначала этот объем «сомнительных» технологий нужно было накопить. Да, были площадки в разных регионах мира, был Кубернетес ещё на заре становления его популярности, были динамические инвентори для деплоя различных компонентов продакшена, и решения, впоследствие потерявшие актуальность. И вот за несколько лет накопился огромный и неповоротливый репозиторий с ансибл-кодом, пользоваться которым становилось всё труднее. Сотрудники менялись, одни задачи вытесняли из памяти другие, документация велась местами на скорую руку, а местами — и так понятно. И пришло понимание, что с этим надо что-то делать.

Следы древних цивилизаций

Начали мы, как и полагается, с инвентаризации. И обнаружили:

  • три десятка плейбуков;

  • переменные в этих плейбуках;

  • переменные в host_vars;

  • переменные в group_vars;

  • переменные в разнообразных папках, вставляемые повсеместно;

  • переменные в ad-hoc-скриптах;

  • переменные в README, описанные для использования в командной строке (ведь вы же читаете README перед использованием, правда?);

  • переменные в ролях в дефолтах;

  • переменные в ролях в vars;

  • переменные в ролях в задачах (вроде и захардкожены, а вроде и переменные в loop — мой любимый вид хардкода);

  • переменные в инвентори;

  • инвентори в ini и в yaml, плоские и сгруппированные на любой вкус.

Если допустить, что вы и есть Ансибл, то для вас такая путаница не будет проблемой. Вы прекрасно знаете, где и в каком порядке прочитать свои переменные — у вас всего-то 22 места. И опять же, с вас какой спрос? Вам инженер команды даёт, вы выполняете.

Вот только инженер — не Ансибл, и для него простая задача что-то накатить может превратиться в увлекательный квест. 

В какой-то момент команда заказчика начала потихоньку уходить от этого веселья, нарастающего с каждым изменением. У них уже появился отдельный ансибл-репозиторий под раскатку продакшена, и в этом репозитории стали появляться инфровые роли. Основной ансибл-репозиторий рисковал превратиться в чулан Моники (привет, олды).

Баста!

И мы решили это хозяйство отрефакторить. Стояла цель сделать эту репу great again, несколько сузить широкие возможности Ансибла. Всех мух свести в одно место, все котлеты — в другое, чтобы сходу можно было понять, где что лежит и как оно влияет на результат.

Начали с нуля. Создали простую структуру:

├── inventories/
├── roles/
└── play_by_inventory.yml

Реализовали следующие принципы:

  • на логическую группу приходится один инвентори, в котором описаны:

    • сами хосты;

    • все переменные к группам и хостам;

    • применяемые к группам и хостам роли;

  • в ролях содержатся только дефолты и что-то базовое (позже поясню);

  • один универсальный плейбук play_by_inventory.yml, в который мы никогда не лезем что-то править, т.к. он выполняет чисто техническую функцию — женитьбу инвентори на ролях.

Неизбежно появился четвертый компонент — самые общие переменные группы all

Там живут штуки вроде 

ansible_python_interpreter: "/usr/bin/python3"

Итоговая базовая структура:

├── group_vars/
│   └── all.yml
├── inventories/
├── roles/
└── play_by_inventory.yml

Но group_vars/all.yml — это единственный случай переменных вне инвентори.

В самой директории inventories было решено не хранить директорий вроде group_vars, чтобы осталось пространство под простую структуру логических группировок инвентори-файлов.

Инвентори

Инвентори пишем в yaml. Помним, что при проигрывании можно в качестве инвентори указать директорию, и Ансибл возьмет в работу все находящиеся в этой директории файлы, объединив хосты и переменные в указанные вами группы. Если группировки не использовать, то всё окажется в группе all.

Пример простого инвентори

all:
 hosts:
   host_1:
     host_roles:
       - role_a
     basic_docker_enabled: true

Здесь host_1 получит роль role_a, и будет заявлен параметр basic_docker_enabled: true.

Пример сложного инвентори

big_group:
 vars:
   group_roles:
     - role_a
     - role_b
 children:
   small_group_1:
     hosts:
       host_1:
         host_roles:
           - role_c
       host_2:
       host_3:
         group_roles:
           - role_b

Здесь произойдут такие назначения:

  • host_1 получит role_a, role_b, role_c (объединены списки group_roles и host_roles);

  • host_2 получит role_a, role_b (задействован только список group_roles);

  • host_3 получит role_b (переопределен список group_roles).

Плейбук

Состав плейбука

Внутри play_by_inventory.yml такой код:

- hosts: all
  become: true
  tasks:
    - name: Discovering roles in inventory files
      include_role:
        name: "{{ discovered_role }}"
      loop: "{{ ( hostvars[inventory_hostname].group_roles | default([]) + hostvars[inventory_hostname].host_roles | default([]) ) | unique }}"
      loop_control:
        loop_var: discovered_role
      tags: always

Как это работает

Запускаются только роли, если они поименованы в списке в переменных group_roles и/или host_roles в инвентори. При этом:

  • списки group_roles  и host_roles объединяются для каждого хоста;

  • переменные можно переопределить в дочерних группах и на уровне хостов.

Теги дополнительно ограничивают запускаемые задачи из объема поименованых в инвентори ролей. Если же предварительно подготовить роль и повесить на нее тег, то через теги можно указать на запуск только определенных ролей, что удобно, например, при обновлении. Для этого механизма важно наличие в плейбуке в таске магической директивы tags: always — она позволяет тегам протранслироваться на уровень поиска тегов в ролях.

Роли

В идеале стоит привести каждую роль к двум простым принципам.

  1. Роль имеет свой общий тег, совпадающий с ее названием. Это позволит запускать частичную накатку на инвентори.

  2. Переменные роли начинаются с префикса из названия роли. Это позволит не заблудиться в переменных, когда вы их встретите в инвентори.

Как просто реализовать первый принцип? Создайте в роли в директори tasks отдельный yaml-файл, например, myrole.yml , куда перенесите содержимое из tasks\main.yml, а тело самого tasks\main.yml превратите в: 

- import_tasks: "myrole.yml"
  tags:
    - myrole

Второй принцип важен больше для удобства чтения и ориентирования.

Про «что-то базовое»

Выше я писал, что в ролях мы держим «только дефолты и что-то базовое». Дело в том, что в этом репозитории есть 2 вида ролей — универсальные и кастомизированные. Пример кастомизированной роли — базовая роль (basic) для раскатки на новую машину. В этой роли в директории files лежат публичные ssh-ключи, а в vars/main.yml лежит список пользователей и их параметры. И чтобы на машине (или на группе машин) при прокатке роли появились эти пользователи, в инвентори этой машины (или группы) в group_roles или в host_roles нужно прописать только:

     - basic

Как запускать

Вот простые примеры:

  • Прокатить 1 инвентори-файл
    ansible-playbook play_by_inventory.yml -i inventories/yandex/monitoring.yml

  • Прокатить все инвентори-файлы из директории
    ansible-playbook play_by_inventory.yml -i inventories/yandex/

  • Прокатить все инвентори-файлы из директории, ограничиться ролью basic (роль подготовлена и имеет общий тег)
    ansible-playbook play_by_inventory.yml -i inventories/yandex/ --tags=basic

  • Прокатить все инвентори-файлы из директории, ограничиться задачей обновления ssh-ключей роли basic
    ansible-playbook play_by_inventory.yml -i inventories/yandex/ --tags=basic_users

С этим, разумеется, можно миксовать прочие полезные ключи, такие как:

--limit — сужает перечень хостов/групп до перечисленных;

--user — переопределяет пользователя, от лица которого пойдет проигрывание плейбука;

--private-key — переопределяет приватный ключ; пользователя, от лица которого пойдет проигрывание плейбука;

–-diff — вывод в протокол изменений в файлах на целевых хостах;

--check — холостой прогон без реальных изменений.

Результаты рефакторинга

До рефакторинга заказчику приходилось проводить анализ годами накопленных сложных конфигураций, выполненных в разных местах и стилях — и все это только для того, чтобы что-то поправить в конфигах или шаблонах при вводе новой машины в эксплуатацию. Тратить на это силы и время хотелось всё меньше, команда заказчика переставала использовать накопленный опыт. 

До

Для наглядности посмотрим, как могло выглядеть наполнение машины с Нексусом до рефакторинга.

├── ad-hoc/ — находим директории с названиями датацентров
│   └── yandex/ — наш ДЦ. Идем глубже
│       └── nexus.corp/ — наша машина. Идем глубже
│           └── ... — несколько shell-скриптов на ansible-playbook на разные инвентори, разные роли. Ок, разбираемся.
├── global_vars/ — тут смотрим, нет ли чего для нашей задачи (а то вдруг пропустим?)
│   ├── logging.yml — находим пресеты для filebeat (но будут ли они задействованы?)
│   ├── sysctl.yml — находим директивы sysctl (но все ли они нам нужны?)
│   ├── time.yml — находим переменные под разные датацентры (теперь где-то нужно найти логику выбора)
│   └── users.yml — находим список пользователей (берем и надеемся, что он актуальный)
├── group_vars/ — тут смотрим обязательно, чтобы избежать сюрпризов
│   ├── ... — пропускаем файлы с названиями групп вне нашего контекста
│   └── all — находим список consul_hosts (хмурим брови, удивляемся — у нас в инфре кто-то видел Consul?..)
├── host_vars/ — видим только десяток машин, пытаемся понять, чем они особенны на фоне остальных
│   ├── ... — пропускаем файлы с названиями других машин
│   └── nexus.corp.yml — находим малоинформативные переменные, но что-то про SSL-сертификаты (берем?)
├── inventories/ — видим каталоги по датацентрам, раскрываем
│   └── yandex/ — наш ДЦ, идем глубже
│       └── nexus.yml — похоже, что это наш инвентори. Есть переменные. О! Одна из переменных упоминает filebeat.
├── .../ — видим еще каталоги с названиями других сервисов
├── nginx_vars/ — стоп, а здесь нам что-то понадобится? Ведь у нас nginx проксирует запросы на машине…
│   └── ... — раскрываем и видим несколько файлов с названиями по категориям, топонимам, ролям. Вроде всё мимо нас.
├── corp_vars/ — так, ну а здесь-то что может быть? Внутри что-то про elasticsearch, пропускам.
├── ... — видим три десятка плейбуков вперемешку с дополнительными инвентори-файлами.

Это простой случай с относительно свежей машиной. Добавим на вход квеста переменные и логические условия в плейбуках, и получим кофе-брейк, отрицание и гнев.

После

А теперь взглянем, как это выглядит сейчас (как и выше, исключаем роли и плейбук).

├── group_vars/
│   └── all.yml — файл номер 1
├── inventories/
│   └── yandex/
│       └── nexus.yml — файл номер 2

На этом всё. И поскольку «файл номер 1» минималистичен и прост, глаза пробегают по нему за две секунды. И фактически всё, что вы хотите знать о машине и о возможностях ее конфигурирования, находится в одном файле — в инвентори. Справедливости ради стоит отметить, что для большего комфорта инженера мы напоминаем команду запуска инвентори-файла в комментариях в начале этого инвентори-файла.

Итог

После рефакторинга репозиторий обрел понятную структуру. Даже диагонального чтения README достаточно, чтобы понять принципы и разобраться, что и где находится. Репозиторием начали пользоваться.

Стоило ли так упрощать, сводить всё в одному плейбуку и к переменным только в инвентори? Ведь Ансибл описывает свои возможности гораздо шире. Стоит ли продавливать канонический подход с теми же host_vars и group_vars

На мой взгляд, результат важнее. Нужно убирать решения, конфликтующие с планами заказчика на развитие и на что заказчик хотел бы тратить время. Нам удалось добиться отличного баланса между концепцией IAC и простотой реализации. Репозиторий сейчас действительно актуален и позволяет в считанные минуты получить предсказуемый результат.

Кстати, это далеко не первая статья в нашем блоге, где мы делимся опытом работы с инфраструктурой. Если у вас руки чешутся оптимизировать все, до чего эти руки дотягиваются, предлагаю вашему вниманию:

Комментарии (6)


  1. PetyaUmniy
    08.09.2025 18:15

    Достаточно оригинальный вызов ролей. Мне понравилось. :)

    Но со многим не согласен. Особенно с формурировкой что заказчик хотел экономить время.
    Вот такая структура:

    ├── group_vars/
    │   └── all.yml — файл номер 1
    ├── inventories/
    │   └── yandex/
    │       └── nexus.yml — файл номер 2
    
    

    говорит ни о чем ином, как о том что у вас нулевое переиспользование переменных. И все переменные плоским перечислением лежат в одном файле и все группы плоские.
    Такая конфигурация супер удобная... до того момента когда вам не придется её редактировать.
    Когда вам нужно будет изменить настройки предположим DNS на всех хостах (тривиальный пример, практический рефакторинг может быть гораздо сложнее), вы будете делать это в 100 файлах!
    А что будет если инстансов будет 1000? Будете писать "плейбук" который редактирует 1000 инвентори? (Это грустная автоматизация, редактировать структуры в ямлах не затрагивая комментарии и форматирование используемое вокруг. Сам таким, бывало, занимался не от хорошей жизни - не рекомендую.)
    Кажется что вариант с наследованием настроек через группы выглядел бы чище. Только действительно нужно строго следить за тем чтобы группы покрывали строго одну понятную из её названия сущность. Чтобы потом не искать: где-что.

    Кстати у вас избыточность: hostvars[inventory_hostname].group_roles
    У вас group_roles объявлена в скоупе конкретного хоста и таску в play вы запускаете над этим же хостом, достаточно group_roles.


    1. MadridianFox
      08.09.2025 18:15

      Мне показалось что тут как раз два уровня переменных: общие переменные а group_vars/all.yml и переменные в инвентаре.


    1. sergey_ist Автор
      08.09.2025 18:15

      Спасибо за комментарий.

      Главная цель этого рефакторинга - сделать репозиторий понятным и удобным команде заказчика. У нас получилось. И да, у нас практически нулевое переиспользование переменных из инвентори, поскольку эти переменные либо относятся фактически к одной группе (а инвентори создан на эту группу), либо являются флагами для работы ролей.

      Как бы мы изменили DNS на всех хоста? - в конкретно нашем случае dns мы меняем на dhcp-сервере, но в целом что-то глобальное меняется в базовой роли, а не в инвентори. Есть другой пример - раскатка файлбита. В инвентори мы указываем роль, что файлбит в принципе нужен и переменной определяем пресет, который уже заложен в роль. Добавить новый пресет - идем в роль. У роли в пресете, например, живет адрес эластика.

      Я вас прекрасно понимаю, но сделать академически правильно и сделать просто и удобно - в данном случае оказалось разными вещами.


  1. mureevms
    08.09.2025 18:15

    Стоило ли так упрощать, сводить всё в одному плейбуку и к переменным только в инвентори?

    Совершенно не кажется упрощением. И даже наоборот - это усложнение. Выглядит так, что мухи от котлет отделены таки были, но затем измельчены и перемешаны в однородную массу. В ходе рефакторинга были причесаны переменные и вынесены в инвентори. Кажется, было вполне достаточно только первого.

    Переменные в инвентори - это антипаттерн, которого следует избегать. Инвентори должен содержать только настройки подключения и групп. По сути получается хордкод переменных и ролей в одном файле, что потенциально усложняет его поддержку и развитие. Относительно простая роль может содержать десятки переменных, которые должны быть заранее определены для каждого хоста/группы только в инвентори без возможности использования каких-то условий. Сами переменные не равнозначны между собой (для этого с самого начала и придумали их приоритет group_vars/host_vars/roles_defaults/roles_vars) и поэтому переопределение - самый лучший путь.


    1. MadridianFox
      08.09.2025 18:15

      На всякий антипаттерн найдётся кейс где именно так получается лучше. У меня часто в инвентаре одна группа с небольшим количеством хостов и переменных. Я тоже сначала делил это на файл с хостами и отдельно group_vars, но со временем решил, что это избыточно.


    1. sergey_ist Автор
      08.09.2025 18:15

      "Переменные в инвентори - это антипаттерн".

      Отчасти с Вами согласен, но это только и только в том случае, когда переменные там не ожидаются, так как уже применяются group_vars и часть логики в них. Добавьте сюда host_vars и переменные в плейбуках - вот уж где можно потеряться неподготовленному зрителю. Фактически так у нас до рефакторинга и было.

      Мы же просто объединили group_vars и инвентори - теперь всё в одном месте и так стало прозрачнее и удобнее. В какое одно место мы могли бы еще всё свести? Решили в инвентори.

      Мы не описываем новую всеобъемлющую методологию ведения проектов на Ансибл. Мы показали реальный кейс как сделать просто и в то же время оставить возможность роста и усложнения по мере необходимости.