209 lines
13 KiB
Markdown
209 lines
13 KiB
Markdown
# Общие 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. Новый сервер с самого начала строится на подписке.
|