diff --git a/docs/backlog/README.md b/docs/backlog/README.md new file mode 100644 index 0000000..ffde3ba --- /dev/null +++ b/docs/backlog/README.md @@ -0,0 +1,33 @@ +# Беклог + +Единый список будущих задач по серверу: то, что уже решили сделать, и идеи, +которые ещё надо обдумать. Это **источник истины по беклогу** — одна задача = +один файл в этом каталоге. Не план реализации: детальные проработки живут в +[`docs/drafts`](../drafts) (беклог даёт сводку и ссылку), а реализованное +переезжает в ADR/спеку и пункт беклога удаляется. + +Приоритет — грубая оценка «ценность / стоимость», не обязательство к порядку. +Пункты с пометкой _(ансибл-ревью)_ пришли из ревью плейбуков +[`docs/drafts/ansible-review.md`](../drafts/ansible-review.md) (2026-05-25). + +Драфты `timeweb.md` и `timeweb-migration-log.md` — исторические логи уже +завершённой миграции в Timeweb (cutover 2026-05-23), в беклог не входят. + +## Высокий + +- [Алерты на проблемные контейнеры](container-alerts.md) — wakapi крутился в restart-loop несколько дней незамеченным; healthcheck в compose + алерты Netdata на restart-loop/unhealthy +- [Рестарт контейнеров через handlers](ansible-handlers-restart.md) — `state: restarted` выполняется безусловно на каждом прогоне (лишний downtime), нет ни одного handler _(ансибл-ревью)_ + +## Средний + +- [Gitea runner on-demand в Yandex Cloud](gitea-runner-on-demand.md) — раннер активен только во время сборки; webhook→Cloud Function стартует ВМ, probe/decide гасят по idle; экономия ~95% +- [Синхронизация общих Ansible-ролей](shared-roles-sync.md) — `owner`/`eget`/`secrets` дублируются между репозиториями и дрейфуют; канон в ansible-shared + rsync-таски invoke +- [Composable-роль `backup`](ansible-backup-role.md) — бэкап — самый чистый шов для extraction (одинаков у всех, различается только список targets) _(ансибл-ревью)_ +- [Точечные фиксы идемпотентности и баг имени play](ansible-quick-fixes.md) — быстрые правки: имя play в wanderer, `changed_when` в netdata/eget, backup-targets через template _(ансибл-ревью)_ + +## Низкий + +- [`vars_files` → `group_vars/all/`](ansible-group-vars.md) — убрать повторяющийся boilerplate `vars_files` из всех плейбуков _(ансибл-ревью)_ +- [Причесать роль `owner` под конвенции](ansible-owner-role-cleanup.md) — `assert` вместо `fail`+`when`, `loop` вместо `with_*`, добавить `meta`/README _(ансибл-ревью)_ +- [Инвентарь: `host_vars`, группы, точечный `become`](ansible-inventory-hostvars.md) — хост-специфику в `host_vars/server.yml`, хост в именованную группу, глобальный root → точечный become _(ансибл-ревью)_ +- [Фоновая зачистка стиля и конфигурации](ansible-style-nits.md) — sudoers.d, профиль ansible-lint, `ansible.cfg`, кавычки, `cache_valid_time` _(ансибл-ревью)_ diff --git a/docs/backlog/ansible-backup-role.md b/docs/backlog/ansible-backup-role.md new file mode 100644 index 0000000..dd67fde --- /dev/null +++ b/docs/backlog/ansible-backup-role.md @@ -0,0 +1,17 @@ +# Composable-роль `backup` + +**Приоритет:** средний + +Бэкап — самый чистый шов для extraction: `gobackup.yml` + `backup.sh` + +`backup-targets` + интеграция с restic одинаковы у всех сервисов, различается +только список целей. Вынести в роль `backup` с параметром «список targets» — она +не трогает индивидуальность сервиса. Это правильный размер абстракции (как уже +сделанный `owner`), а **не** мега-роль `docker_app` (та отклонена осознанно: +приложения реально разные, catch-all обрастает `when:`). Обкатать на одном +сервисе, затем раскатать. + +Полный контекст: [docs/drafts/ansible-review.md](../drafts/ansible-review.md) §1. + +Связано: roles/owner (образец), files//gobackup.template.yml, +files//backup.template.sh, playbook-backups.yml, +feedback_per_app_playbook_duplication. diff --git a/docs/backlog/ansible-group-vars.md b/docs/backlog/ansible-group-vars.md new file mode 100644 index 0000000..61fd390 --- /dev/null +++ b/docs/backlog/ansible-group-vars.md @@ -0,0 +1,22 @@ +# `vars_files` → `group_vars/all/` + +**Приоритет:** низкий + +В каждом плейбуке повторяется boilerplate: + +```yaml +vars_files: + - vars/secrets.yml + - vars/vars.yml +``` + +Ansible автоматически подхватывает `group_vars/all.yml` и `group_vars/all/secrets.yml` +(vault) для группы `all`. Перенос `vars/vars.yml` → `group_vars/all/main.yml` и +`vars/secrets.yml` → `group_vars/all/vault.yml` убирает `vars_files` из всех +плейбуков. Низкий риск, адаптируется по одному плейбуку за раз. Учесть +`.crushignore`/проверку шифрования vault (маска `*secrets.yml`) и pre-commit-хук +проверки шифрования при переименовании. + +Контекст: [docs/drafts/ansible-review.md](../drafts/ansible-review.md) §2. + +Связано: все playbook-*.yml, vars/secrets.yml, vars/vars.yml, lefthook.yml. diff --git a/docs/backlog/ansible-handlers-restart.md b/docs/backlog/ansible-handlers-restart.md new file mode 100644 index 0000000..04240c7 --- /dev/null +++ b/docs/backlog/ansible-handlers-restart.md @@ -0,0 +1,20 @@ +# Рестарт контейнеров через handlers, а не безусловно + +**Приоритет:** высокий + +Ни в одном плейбуке нет `handlers:`. Вместо этого задача `state: restarted` +выполняется **всегда** — рестартит контейнер на каждом прогоне даже без +изменений (`playbook-caddyproxy.yml:106`, `playbook-netdata.yml:143`, +`playbook-authelia.yml:92`): не идемпотентно, лишний downtime. В +`playbook-gitea.yml` рестарта нет вовсе — несогласованность. Канонический +паттерн: шаблон конфига `notify`-ит handler, который делает +`docker_compose_v2: state: restarted` только при реальном изменении. Внедряется +инкрементально, по одному сервису. Заодно убрать мёртвый +`docker_compose_file_result` в `playbook-memos.yml:76` (регистрируется, нигде не +используется — задумывался под `when`/`notify`). + +Топ-приоритет ансибл-ревью. Полный контекст: +[docs/drafts/ansible-review.md](../drafts/ansible-review.md) §3. + +Связано: playbook-caddyproxy.yml, playbook-netdata.yml, playbook-authelia.yml, +playbook-gitea.yml, playbook-memos.yml. diff --git a/docs/backlog/ansible-inventory-hostvars.md b/docs/backlog/ansible-inventory-hostvars.md new file mode 100644 index 0000000..35afe8b --- /dev/null +++ b/docs/backlog/ansible-inventory-hostvars.md @@ -0,0 +1,18 @@ +# Инвентарь: `host_vars`, именованные группы, точечный `become` + +**Приоритет:** низкий + +- **`production.yml` и `timeweb.yml`** оба объявляют хост `server` под + `ungrouped:`, хост-специфичные данные (`application_dir`, + `mount_external_storage`, `ansible_host`, `ansible_user`) вписаны инлайн. + Конвенциональнее — `host_vars/server.yml` и хост в именованной группе. Два + инвентаря с одинаковым именем хоста + `hosts: all` — ошибка `-i` молча уедет не + туда. (`timeweb.yml` — артефакт завершённой миграции, заодно решить, нужен ли + он ещё.) +- `ansible_become: true` глобально — всё бежит под root. Для личного сервера + прагматично; точечный `become`/`become_user` ближе к наименьшим привилегиям, но + это низкий приоритет. + +Контекст: [docs/drafts/ansible-review.md](../drafts/ansible-review.md) §6. + +Связано: production.yml, timeweb.yml. diff --git a/docs/backlog/ansible-owner-role-cleanup.md b/docs/backlog/ansible-owner-role-cleanup.md new file mode 100644 index 0000000..cf1d4e7 --- /dev/null +++ b/docs/backlog/ansible-owner-role-cleanup.md @@ -0,0 +1,20 @@ +# Причесать роль `owner` под конвенции + +**Приоритет:** низкий + +Роль `owner` разошлась по стилю с `eget`/`secrets`: + +- **`roles/owner/tasks/main.yml:2-10`** — валидация аргументов через `fail` + + `when`, причём две задачи с **идентичным именем**. `eget` для того же делает + `assert` (`roles/eget/tasks/main.yml:15`). Привести к одному стилю — `assert` + либо декларативный `meta/argument_specs.yml`. +- **`roles/owner/tasks/main.yml:32,53`** — устаревшие `with_items`/`with_dict`; + конвенция — `loop` (`loop: "{{ owner_ssh_keys }}"`, + `loop: "{{ owner_env_dict | dict2items }}"`). +- У `owner` нет `meta/main.yml` и README, тогда как у `eget` и `secrets` есть. +- Имена задач с точкой на конце (`"Prepare env variables."`) — ansible-lint в + строгом профиле это ловит. + +Контекст: [docs/drafts/ansible-review.md](../drafts/ansible-review.md) §5. + +Связано: roles/owner, roles/eget (образец), roles/secrets. diff --git a/docs/backlog/ansible-quick-fixes.md b/docs/backlog/ansible-quick-fixes.md new file mode 100644 index 0000000..a597279 --- /dev/null +++ b/docs/backlog/ansible-quick-fixes.md @@ -0,0 +1,25 @@ +# Точечные фиксы идемпотентности и баг имени play + +**Приоритет:** средний + +Быстрые пойнтовые правки без структурных изменений: + +- **`playbook-wanderer.yml:2`** — play назван `"Configure gramps application"` + при `app_name: "wanderer"` (копипаст из gramps). Поправить имя. _(баг)_ +- **`playbook-netdata.yml:118-125`** — `changed_when: ...rc != 0` для read-only + запроса лишён смысла; должно быть `changed_when: false`. Лучше заменить + `shell: grep docker /etc/group` на модуль `ansible.builtin.getent` — уйдёт + `pipefail` и хрупкий парсинг. +- **`playbook-eget.yml:23-78`** — восемь `command` с `changed_when: false`, хотя + реально ставят/обновляют бинарники: прогон всегда «ok», теряется честность + `--diff`. Ставить через роль `eget` (она корректно проверяет версию) или через + проверку версии. +- **`playbook-memos.yml:57-67`** и аналоги — сборка `backup-targets` через + `lineinfile` в цикле не удаляет устаревшие строки при изменении списка; `mode: + "0750"` на файле-списке выглядит как copy-paste. Чище — `template`/`copy: + content` со всем списком. + +Контекст: [docs/drafts/ansible-review.md](../drafts/ansible-review.md) §4, §7. + +Связано: playbook-wanderer.yml, playbook-netdata.yml, playbook-eget.yml, +playbook-memos.yml, roles/eget. diff --git a/docs/backlog/ansible-style-nits.md b/docs/backlog/ansible-style-nits.md new file mode 100644 index 0000000..00ede93 --- /dev/null +++ b/docs/backlog/ansible-style-nits.md @@ -0,0 +1,25 @@ +# Фоновая зачистка стиля и конфигурации Ansible + +**Приоритет:** низкий + +Косметика и мелочи конфигурации, копятся в один проход: + +- **sudoers**: `playbook-backups.yml:52-59` правит `/etc/sudoers` через + `lineinfile`. Конвенция — отдельный файл в `/etc/sudoers.d/` (через + `copy`/`template` с `validate: visudo -cf %s`). +- **`.ansible-lint.yml`** содержит только `exclude_paths`, профиль не задан явно, + хотя AGENTS.md заявляет «профиль production». Прописать `profile: production` + либо поправить документацию. +- **`ansible.cfg`** минимален — добавить `stdout_callback = yaml`, + `interpreter_python = auto_silent`, `force_handlers = true` (последнее нужно + вместе с переходом на handlers). +- Несогласованные кавычки и пути (`'directory'` vs `"directory"`, + `src: "./files/..."` vs `src: "files/..."`, одинарные кавычки в + `playbook-all-setup.yml`). +- **`playbook-system.yml:24`** — `apt` без `cache_valid_time`, обновляет кэш + каждый прогон. + +Контекст: [docs/drafts/ansible-review.md](../drafts/ansible-review.md) §8. + +Связано: playbook-backups.yml, .ansible-lint.yml, ansible.cfg, +playbook-system.yml, playbook-all-setup.yml. diff --git a/docs/backlog/container-alerts.md b/docs/backlog/container-alerts.md new file mode 100644 index 0000000..a9d0638 --- /dev/null +++ b/docs/backlog/container-alerts.md @@ -0,0 +1,17 @@ +# Алерты на проблемные контейнеры + +**Приоритет:** высокий + +wakapi однажды упал на миграциях и несколько дней крутился в restart-loop — +никто не узнал. Docker считал контейнер «running», пока процесс жив. Нужны два +слоя: (1) healthcheck + `start_period` в compose-шаблонах, чтобы Docker видел +реальное состояние (окно `start_period` даёт миграциям отработать); (2) алерты +через Netdata на restart-loop (счётчик перезапусков растёт) и на +`container_health_status != healthy` дольше M минут, канал нотификаций — один +(Telegram/ntfy — выбрать). restart policy оставляем `unless-stopped` (алерт + +ручное решение вместо `on-failure`, который не встаёт после ребута). Опционально +позже — Uptime Kuma для внешнего HTTP-чека по публичным URL. + +Полный план: [docs/drafts/alerts.md](../drafts/alerts.md). + +Связано: playbook-netdata.yml, compose-шаблоны всех сервисов, project_server_specs. diff --git a/docs/backlog/gitea-runner-on-demand.md b/docs/backlog/gitea-runner-on-demand.md new file mode 100644 index 0000000..4e5dbf9 --- /dev/null +++ b/docs/backlog/gitea-runner-on-demand.md @@ -0,0 +1,20 @@ +# Gitea runner on-demand в Yandex Cloud + +**Приоритет:** средний + +Self-hosted раннер Gitea Actions, активный только во время сборки. Сборок ~10/нед +по ~5 мин — ВМ 24/7 даёт утилизацию ~1% (≈$23/мес), on-demand — ≈$1/мес (экономия +~95%). Архитектура: push → Gitea webhook → Cloud Function (HMAC-валидация + +стейт-машина старта) → Compute API стартует ВМ → `act_runner` в docker забирает +джобу; на ВМ probe (телеметрия раз в 30с) + decide (решение раз в 1 мин) гасят ВМ +после idle-окна через Compute REST из metadata-токена. Три слоя страховки от +зависшей ВМ (soft idle-stop, probe-staleness, внешний алерт Cloud Monitoring на +uptime > 24ч). Плейбук `playbook-gitea-runner.yml` + набор invoke-тасков +(`runner-bootstrap`, `runner-deploy-function`, `runner-pl`, ...). + +Дизайн проработан целиком (ресурсы YC, секреты, стоимость, принятые риски, план +внедрения из 11 шагов). Открытые вопросы: канал нотификаций, executor (docker), +webhook на PR. Полный дизайн: +[docs/drafts/gitea-runner-on-demand.md](../drafts/gitea-runner-on-demand.md). + +Связано: playbook-gitea.yml, roles/owner, vars/secrets.yml, tasks.py. diff --git a/docs/backlog/shared-roles-sync.md b/docs/backlog/shared-roles-sync.md new file mode 100644 index 0000000..aaf7071 --- /dev/null +++ b/docs/backlog/shared-roles-sync.md @@ -0,0 +1,21 @@ +# Синхронизация общих Ansible-ролей между репозиториями + +**Приоритет:** средний + +Серверы управляются изолированными репозиториями (pet-project-server, buckland, +далее — торрент-бокс). Кастомные роли (`owner`, `eget`, `secrets`) концептуально +общие, но физически продублированы и дрейфуют (`owner` в buckland стал мёртвым +кодом). Решение под pets-подход: канон ролей в `~/projects/private/ansible-shared/`, +каждый репозиторий держит закоммиченную копию (она же «пин версии») и +синхронизируется через rsync-таски invoke — `roles-pull` / `roles-push` / +`roles-status`. Подписка на роли явная и пер-серверная (`SYNCED_ROLES` в +`tasks.py`). Обновление роли на сервере — осознанный коммит с читаемым диффом, не +скрытый сайд-эффект. Предохранитель от дрейфа — предупреждение в lefthook. +Отвергнуты: общий `roles_path`, симлинки, submodule/subtree, galaxy-из-git, +монорепо (все ломают самодостаточность или вводят лишнюю петлю). + +Подход обсуждён, реализация не начата. Полный дизайн + план внедрения: +[docs/drafts/shared-roles-sync.md](../drafts/shared-roles-sync.md). + +Связано: roles/owner, roles/eget, roles/secrets, tasks.py, lefthook.yml, +project_isolated_repos_shared_roles, project_shared_invoke_command_surface.