
Привет. Я Сергей Истомин, 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 — она позволяет тегам протранслироваться на уровень поиска тегов в ролях.
Роли
В идеале стоит привести каждую роль к двум простым принципам.
Роль имеет свой общий тег, совпадающий с ее названием. Это позволит запускать частичную накатку на инвентори.
Переменные роли начинаются с префикса из названия роли. Это позволит не заблудиться в переменных, когда вы их встретите в инвентори.
Как просто реализовать первый принцип? Создайте в роли в директори 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-ключей роли
basicansible-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 и простотой реализации. Репозиторий сейчас действительно актуален и позволяет в считанные минуты получить предсказуемый результат.
Кстати, это далеко не первая статья в нашем блоге, где мы делимся опытом работы с инфраструктурой. Если у вас руки чешутся оптимизировать все, до чего эти руки дотягиваются, предлагаю вашему вниманию:
Firezone, или как спрятать свою инфраструктуру от посторонних глаз
Как дать разработчикам свободу при деплое приложений и ускорить процессы в команде
JupyterHub на стероидах: реализация KubeFlow фич без масштабных интеграций
Поднимаем динамические окружения (фича-стенды) для stateless- и stateful-сервисов
Комментарии (6)

mureevms
08.09.2025 18:15Стоило ли так упрощать, сводить всё в одному плейбуку и к переменным только в инвентори?
Совершенно не кажется упрощением. И даже наоборот - это усложнение. Выглядит так, что мухи от котлет отделены таки были, но затем измельчены и перемешаны в однородную массу. В ходе рефакторинга были причесаны переменные и вынесены в инвентори. Кажется, было вполне достаточно только первого.
Переменные в инвентори - это антипаттерн, которого следует избегать. Инвентори должен содержать только настройки подключения и групп. По сути получается хордкод переменных и ролей в одном файле, что потенциально усложняет его поддержку и развитие. Относительно простая роль может содержать десятки переменных, которые должны быть заранее определены для каждого хоста/группы только в инвентори без возможности использования каких-то условий. Сами переменные не равнозначны между собой (для этого с самого начала и придумали их приоритет
group_vars/host_vars/roles_defaults/roles_vars) и поэтому переопределение - самый лучший путь.
MadridianFox
08.09.2025 18:15На всякий антипаттерн найдётся кейс где именно так получается лучше. У меня часто в инвентаре одна группа с небольшим количеством хостов и переменных. Я тоже сначала делил это на файл с хостами и отдельно group_vars, но со временем решил, что это избыточно.

sergey_ist Автор
08.09.2025 18:15"Переменные в инвентори - это антипаттерн".
Отчасти с Вами согласен, но это только и только в том случае, когда переменные там не ожидаются, так как уже применяются group_vars и часть логики в них. Добавьте сюда host_vars и переменные в плейбуках - вот уж где можно потеряться неподготовленному зрителю. Фактически так у нас до рефакторинга и было.
Мы же просто объединили group_vars и инвентори - теперь всё в одном месте и так стало прозрачнее и удобнее. В какое одно место мы могли бы еще всё свести? Решили в инвентори.
Мы не описываем новую всеобъемлющую методологию ведения проектов на Ансибл. Мы показали реальный кейс как сделать просто и в то же время оставить возможность роста и усложнения по мере необходимости.
PetyaUmniy
Достаточно оригинальный вызов ролей. Мне понравилось. :)
Но со многим не согласен. Особенно с формурировкой что заказчик хотел экономить время.
Вот такая структура:
говорит ни о чем ином, как о том что у вас нулевое переиспользование переменных. И все переменные плоским перечислением лежат в одном файле и все группы плоские.
Такая конфигурация супер удобная... до того момента когда вам не придется её редактировать.
Когда вам нужно будет изменить настройки предположим DNS на всех хостах (тривиальный пример, практический рефакторинг может быть гораздо сложнее), вы будете делать это в 100 файлах!
А что будет если инстансов будет 1000? Будете писать "плейбук" который редактирует 1000 инвентори? (Это грустная автоматизация, редактировать структуры в ямлах не затрагивая комментарии и форматирование используемое вокруг. Сам таким, бывало, занимался не от хорошей жизни - не рекомендую.)
Кажется что вариант с наследованием настроек через группы выглядел бы чище. Только действительно нужно строго следить за тем чтобы группы покрывали строго одну понятную из её названия сущность. Чтобы потом не искать: где-что.
Кстати у вас избыточность:
hostvars[inventory_hostname].group_rolesУ вас
group_rolesобъявлена в скоупе конкретного хоста и таску в play вы запускаете над этим же хостом, достаточноgroup_roles.MadridianFox
Мне показалось что тут как раз два уровня переменных: общие переменные а group_vars/all.yml и переменные в инвентаре.
sergey_ist Автор
Спасибо за комментарий.
Главная цель этого рефакторинга - сделать репозиторий понятным и удобным команде заказчика. У нас получилось. И да, у нас практически нулевое переиспользование переменных из инвентори, поскольку эти переменные либо относятся фактически к одной группе (а инвентори создан на эту группу), либо являются флагами для работы ролей.
Как бы мы изменили DNS на всех хоста? - в конкретно нашем случае dns мы меняем на dhcp-сервере, но в целом что-то глобальное меняется в базовой роли, а не в инвентори. Есть другой пример - раскатка файлбита. В инвентори мы указываем роль, что файлбит в принципе нужен и переменной определяем пресет, который уже заложен в роль. Добавить новый пресет - идем в роль. У роли в пресете, например, живет адрес эластика.
Я вас прекрасно понимаю, но сделать академически правильно и сделать просто и удобно - в данном случае оказалось разными вещами.