Files
pet-project-server/docs/drafts/shared-roles-sync.md
T
av 5cd66d01bc
Linting / YAML Lint (push) Has been cancelled
Linting / Ansible Lint (push) Has been cancelled
Tududi: add task tracke application
2026-07-04 16:56:01 +03:00

13 KiB
Raw Blame History

Общие 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-statusdiff -r -q каждой подписанной роли против канона; выводит ok / differs / missing in shared по ролям, ненулевой код выхода при расхождении.

Защита от ошибок:

  • обе команды печатают список затронутых файлов (rsync -i) — операция видна, а не молчалива;
  • roles-push при отличающемся каноне показывает дифф и просит подтверждение (защита от затирания более свежего канона устаревшей копией; типовой случай — правили роль в двух репозиториях параллельно);
  • roles-pull подтверждения не требует: затирается рабочая копия, её состояние защищено git — git diff покажет, что приехало, и любое изменение можно откатить до коммита.

Рабочий цикл

Правка роли:

  1. Роль правится прямо в том репозитории, где идёт работа, — тестировать её всё равно можно только запуском реального плейбука этого сервера. Петля правка → запуск нулевая.
  2. Когда правка устоялась: inv roles-push и коммит в серверном репозитории.
  3. В остальных репозиториях — inv roles-pull, когда их сервер готов принять обновление. Дифф виден в git diff, коммитится в историю этого репозитория.

Новый сервер:

  1. Создать репозиторий, объявить SYNCED_ROLES.
  2. 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-задачи. Вендоринг копий делает переход безопасным — в любой момент репозитории самодостаточны.

План внедрения

  1. Создать ~/projects/private/ansible-shared/roles/ из актуальных ролей pet-project-server (owner, eget, secrets).
  2. Добавить задачи roles-pull / roles-push / roles-status в tasks.py pet-project-server; объявить подписку на все три роли.
  3. То же в buckland; подписка только на eget (его копия сначала выравнивается через roles-pull — дифф косметический, стиль кавычек). Мёртвую roles/owner удалить.
  4. Предупреждение о дрейфе в lefthook обоих репозиториев.
  5. Новый сервер с самого начала строится на подписке.