13 KiB
Общие Ansible-роли между серверными репозиториями
Статус
Черновик. Подход обсуждён, реализация не начата.
Контекст
Серверы управляются изолированными Ansible-репозиториями: по одному на
сервер (pet-project-server, buckland, далее — новый торрент-бокс).
Кастомные роли (owner, eget, secrets) концептуально общие, но
физически продублированы, и копии дрейфуют: eget разошёлся
косметически, owner в buckland превратился в мёртвый код, пока
актуальная версия развивалась в pet-project-server.
Нужен способ держать роли общими между репозиториями, при этом:
- правка роли применяется мгновенно — без петли commit → push → install;
- каждый репозиторий остаётся самодостаточным: клонируется и деплоится без внешних зависимостей, CI (yamllint + ansible-lint) работает как сейчас;
- обновление роли на конкретном сервере — явное решение, а не незаметный сайд-эффект.
Осознанный выбор для pets-серверов
Это решение спроектировано под подход «pets, not cattle» и не претендует на масштаб. Серверы — питомцы: у каждого своё железо, свой набор приложений и свой темп жизни. Из этого следуют принципы, противоположные командной инженерии:
- дублирование допустимо, принудительная консистентность — нет;
- каждый сервер принимает обновление общей роли явно, в свой момент, через видимый дифф в собственной истории git;
- никакой инфраструктуры публикации (registry, версии, релизы) — потребитель ролей один человек, который сам же их и пишет.
Если серверов станет много или появятся другие потребители — решение нужно пересмотреть (см. «Путь миграции»).
Решение
Вендоринг копий плюс локальная синхронизация через файловую систему.
Каноническая версия ролей живёт в отдельной директории
~/projects/private/ansible-shared/. Каждый серверный репозиторий
держит свою закоммиченную копию нужных ролей в roles/ и
синхронизируется с каноном командами invoke (rsync, git не участвует).
~/projects/private/
ansible-shared/
roles/
owner/
eget/
secrets/
pet-project-server/
roles/owner/ # закоммиченная копия
roles/eget/
roles/secrets/
buckland/
roles/eget/ # подписан только на eget
<новый сервер>/
roles/...
Ключевое свойство: «пином версии» служит сама закоммиченная копия.
Репозиторий всегда деплоит ровно то, что лежит в его git; обновление
роли — это roles-pull + осознанный коммит с читаемым диффом.
Подписка
Каждый репозиторий объявляет в tasks.py список ролей, которые он
синхронизирует:
SYNCED_ROLES = ["owner", "eget", "secrets"]
Подписка явная и пер-серверная: buckland может не подписываться на
owner, пока живёт в старой архитектуре (всё под primary_user).
Роли вне списка синхронизация не трогает — репозиторий может держать
и сугубо локальные роли.
Команды
Три задачи invoke, одинаковый блок (~30 строк) в tasks.py каждого
репозитория:
inv roles-pull [-- <role>]— канон → репозиторий. Для каждой роли из подписки (или одной указанной):rsync -a --delete <shared>/roles/<role>/ roles/<role>/. Флаг--deleteобязателен: удалённые в каноне файлы должны исчезать из копии, иначе дрейф через «хвосты».inv roles-push [-- <role>]— репозиторий → канон, тот же rsync в обратную сторону.inv roles-status—diff -r -qкаждой подписанной роли против канона; выводитok/differs/missing in sharedпо ролям, ненулевой код выхода при расхождении.
Защита от ошибок:
- обе команды печатают список затронутых файлов (
rsync -i) — операция видна, а не молчалива; roles-pushпри отличающемся каноне показывает дифф и просит подтверждение (защита от затирания более свежего канона устаревшей копией; типовой случай — правили роль в двух репозиториях параллельно);roles-pullподтверждения не требует: затирается рабочая копия, её состояние защищено git —git diffпокажет, что приехало, и любое изменение можно откатить до коммита.
Рабочий цикл
Правка роли:
- Роль правится прямо в том репозитории, где идёт работа, — тестировать её всё равно можно только запуском реального плейбука этого сервера. Петля правка → запуск нулевая.
- Когда правка устоялась:
inv roles-pushи коммит в серверном репозитории. - В остальных репозиториях —
inv roles-pull, когда их сервер готов принять обновление. Дифф виден вgit diff, коммитится в историю этого репозитория.
Новый сервер:
- Создать репозиторий, объявить
SYNCED_ROLES. inv roles-pull— роли приезжают из канона, коммитятся.
Новая общая роль: написать в любом репозитории, добавить в его
SYNCED_ROLES, inv roles-push; остальные подписываются по мере
надобности.
Предохранитель в lefthook
Дрейф неизбежен (синхронизация — дисциплина, не механизм), поэтому он
должен быть видимым. В pre-commit каждого репозитория — проверка: если
в staged-файлах есть roles/<подписанная роль>/ и роль отличается от
канона, печатается предупреждение «роль X отличается от ansible-shared,
не забудь roles-push / roles-pull». Именно предупреждение, не
блокировка: коммит локальной правки до push в канон — нормальный
рабочий момент. Если директории ansible-shared нет на машине,
проверка молча пропускается (CI, чужая машина).
Статус ansible-shared
ansible-shared — рабочая директория, а не репозиторий-сервис. Можно
сделать её git-репозиторием для бэкапа и истории канона, но этот git
не стоит в рабочей петле: коммиты туда делаются когда угодно и ничего
не блокируют. Серверные репозитории не знают о её git-статусе — они
видят только файлы.
Отвергнутые альтернативы
- Общий
roles_pathв ansible.cfg (roles_path = ./roles:../ansible-shared/roles). Нулевая синхронизация, но репозитории теряют самодостаточность (CI и клон на другой машине ломаются), а главное — правка роли молча применяется ко всем серверам при следующем запуске. Для pets-подхода это анти-свойство. - Симлинки на общую директорию, закоммиченные в git. Та же семантика плюс ломкость: валидны только при конкретной раскладке директорий на диске.
- Git submodule / subtree. Решают задачу для команд ценой петли commit → push → update и известной UX-боли; принудительная консистентность здесь не нужна, а петля — главное, от чего уходим.
- Galaxy-роли из git-репозитория (
requirements.ymlс git-URL, версии тегами). Правильный инструмент при росте масштаба, но сейчас церемония tag → push → install — это и есть слишком большая петля обратной связи для одного человека. - Монорепозиторий на все серверы. Отвергнут на уровне организации проектов: серверы — pets, изолированные репозитории с локальной историей ценнее общей точки правды.
Цена и риски
- Синхронизация держится на дисциплине. Забытый
roles-push— канон отстал; правка одной роли в двух репозиториях без push — конфликт, который разрешается руками через дифф. Митигация — предупреждение в lefthook иroles-status; при текущем темпе изменений (единицы в год) этого достаточно. - Канон существует в одном экземпляре на одной машине. Потеря машины —
потеря только канона, не ролей: они восстановимы из любой свежей
копии (
roles-push). Опциональный git вansible-sharedзакрывает и это. - Блок задач в
tasks.pyдублируется по репозиториям и сам может дрейфовать. Принимаем: это ~30 строк, меняются реже ролей.
Путь миграции
Если ролей станет десяток, серверов — больше трёх, или появится второй
пользователь, ansible-shared уже имеет структуру стандартного
galaxy-источника: повесить теги, перевести репозитории на
requirements.yml с git-URL, удалить rsync-задачи. Вендоринг копий
делает переход безопасным — в любой момент репозитории самодостаточны.
План внедрения
- Создать
~/projects/private/ansible-shared/roles/из актуальных ролей pet-project-server (owner,eget,secrets). - Добавить задачи
roles-pull/roles-push/roles-statusвtasks.pypet-project-server; объявить подписку на все три роли. - То же в buckland; подписка только на
eget(его копия сначала выравнивается черезroles-pull— дифф косметический, стиль кавычек). Мёртвуюroles/ownerудалить. - Предупреждение о дрейфе в lefthook обоих репозиториев.
- Новый сервер с самого начала строится на подписке.