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

209 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Общие 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` список ролей, которые он
синхронизирует:
```python
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` покажет, что приехало, и любое
изменение можно откатить до коммита.
### Рабочий цикл
Правка роли:
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. Новый сервер с самого начала строится на подписке.