Files
infra/tasks/grimmory-deployment-plan.md
T
DmitryandClaude Opus 5 c676be81ec Capture current Ansible control plane state
Commit the accumulated infrastructure work that was living only in the
working tree: monitoring stack, emergency access/bot, gyro allocator,
grimmory, adguard, backup audit and the OpenCode agent definitions.

Also ignore Python bytecode, local archives and Nix/direnv artifacts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTocXkGUUazHdKKd3r9k71
2026-08-26 21:39:28 +03:00

552 lines
29 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.
# План развертывания Grimmory
## Целевая архитектура
- Отдельный unprivileged LXC `grimmory` на `cloud-pc`.
- Итоговые значения: CTID `149`, IP `192.168.1.34`. Первоначальный `.33` оказался занят после создания LXC.
- RootFS на storage `data`, первоначально `64 GB` с возможностью расширения.
- Ресурсы: `2 vCPU`, `4 GB RAM`, `1 GB swap`.
- Docker с `fuse-overlayfs` внутри LXC.
- Docker Compose: Grimmory и MariaDB.
- Публичный адрес: `https://books.ada-dev.ru`.
- Книги и данные находятся внутри диска LXC.
- Daily PBS snapshot всего CT.
- Offsite restic: дамп MariaDB, конфигурация и `/app/data`, но не книги.
- Образы фиксируются на версиях:
- `grimmory/grimmory:v3.2.4`;
- `lscr.io/linuxserver/mariadb:11.4.8`.
Размер диска нужно скорректировать до развертывания, если текущая библиотека уже приближается к 40-50 GB.
## 1. Предварительные проверки
1. Проверить, что CTID `149` свободен:
- в Proxmox API;
- в `pct list` на обоих узлах;
- в PBS среди исторических VMID.
2. Проверить IP `192.168.1.34`:
- отсутствие в inventory;
- отсутствие DHCP reservation;
- отсутствие ответа на ARP/ping;
- отсутствие DNS-записи с другим назначением.
3. Проверить емкость `cloud-pc:data`.
4. Проверить работоспособность PBS datastore и последнего backup job `cloud-pc`.
5. Проверить возможность создать DNS-запись `books.ada-dev.ru`.
6. Зафиксировать ожидаемый объем существующей коллекции и темп роста.
7. Получить digest образов после выбора версий. В первой итерации допустим pinned tag; digest предпочтительнее для полностью воспроизводимого состояния.
Проверка должна завершиться без создания LXC. При конфликте CTID/IP выбрать следующие свободные значения и использовать их согласованно во всех файлах.
## 2. Inventory
Изменить `ansible/inventory/hosts.yml`:
- добавить `grimmory` в `lxc_infra`;
- указать `ansible_host: 192.168.1.34`;
- указать `expected_lan_ip`;
- указать `ansible_user: root`;
- использовать существующий ProxyJump через `ru-vps`;
- добавить в `monitoring_exporters`, если Node Exporter разворачивается сразу.
После изменения проверить:
```bash
ansible-inventory --graph
ansible-inventory --host grimmory
```
Edge cases:
- не использовать IP за пределами маршрутизируемого service range `.5-.40`;
- убедиться, что имя `grimmory` разрешается Ansible именно в новый IP;
- не добавлять host в inventory до выбора окончательных CTID/IP во всех playbooks.
## 3. Provisioning LXC
Создать `ansible/playbooks/pve-grimmory.yml`, используя роль `pve_lxc`.
Параметры:
```yaml
pve_lxc_vmid: 149
pve_lxc_node: cloud-pc
pve_lxc_hostname: grimmory
pve_lxc_ip: 192.168.1.34/24
pve_lxc_gateway: 192.168.1.1
pve_lxc_storage: data
pve_lxc_disk: 64
pve_lxc_cores: 2
pve_lxc_memory: 4096
pve_lxc_swap: 1024
pve_lxc_unprivileged: true
pve_lxc_features:
- nesting=1
- keyctl=1
```
Дополнительно настроить Docker-in-LXC по существующему паттерну:
- разрешить `/dev/fuse`;
- добавить bind устройства `/dev/fuse`;
- перезапустить LXC только при изменении его PVE-конфигурации;
- дождаться SSH;
- проверить наличие пользовательского SSH-ключа.
Текущая роль `pve_lxc` не управляет `/dev/fuse`, поэтому эти задачи остаются в playbook, аналогично существующим Docker LXC.
Проверки:
```bash
ansible-playbook --syntax-check playbooks/pve-grimmory.yml
ansible-playbook playbooks/pve-grimmory.yml --check
```
`--check` для Proxmox API не гарантирует полноценной симуляции, поэтому его результат использовать только как дополнительную проверку.
После создания проверить:
- `pct status 149`;
- unprivileged mode включен;
- rootfs действительно находится на `data`;
- `onboot=1`;
- SSH работает через ProxyJump;
- `/dev/fuse` существует внутри LXC;
- повторный запуск playbook не создает изменений.
## 4. Docker Runtime
Внутри LXC установить:
- `docker.io`;
- Docker Compose v2, если доступен как `docker-compose-plugin`;
- `fuse-overlayfs`;
- `ca-certificates`;
- `curl`;
- `mariadb-client` для backup/restore-проверок;
- `prometheus-node-exporter`.
Настроить `/etc/docker/daemon.json`:
```json
{
"storage-driver": "fuse-overlayfs"
}
```
Проверить:
```bash
docker info
docker compose version
docker run --rm hello-world
```
Критерий успеха: Docker сообщает `fuse-overlayfs` как storage driver и переживает перезапуск LXC.
## 5. Persistent Data
Создать структуру:
```text
/opt/grimmory/
├── compose.yml
├── .env
├── data/
├── books/
├── bookdrop/
├── mariadb/
└── backup-staging/
```
Назначение:
- `data` -> `/app/data`;
- `books` -> `/books`;
- `bookdrop` -> `/bookdrop`;
- `mariadb` -> `/config`;
- `backup-staging` -> временные согласованные дампы.
Права каталогов должны соответствовать `APP_USER_ID`, `APP_GROUP_ID`, MariaDB `PUID/PGID`.
Edge cases:
- `bookdrop` не является библиотекой: после успешного импорта исходники удаляются оттуда;
- не размещать важные необработанные оригиналы только в `bookdrop`;
- не вкладывать bind mounts друг в друга;
- не использовать `DISK_TYPE=NETWORK`, поскольку файлы находятся на локальном rootfs;
- оставлять минимум 15-20% диска свободным: импорт и обработка могут временно удваивать размер книги;
- учитывать крупные PDF/CBR и возможные heap dump при OOM.
## 6. Compose
Управлять двумя контейнерами через Compose, поскольку Grimmory зависит от MariaDB. Для этого сервиса Compose логичнее отдельных systemd `docker run`.
Основные параметры:
- `DISK_TYPE=LOCAL`;
- `TZ` соответствует HomeLab;
- `ALLOWED_ORIGINS=https://books.ada-dev.ru`;
- MariaDB доступна только во внутренней Docker network;
- наружу публикуется только Grimmory `6060`;
- порт привязывается к LAN IP LXC, а не `0.0.0.0`, если Compose это позволяет;
- healthcheck использует `/api/v1/healthcheck`;
- `restart: unless-stopped`.
Создать systemd unit-обертку для:
```bash
docker compose -f /opt/grimmory/compose.yml up -d --remove-orphans
```
Не использовать `--pull always`: обновление должно быть отдельной контролируемой операцией.
Секреты:
- случайные отдельные пароли `DB_PASSWORD` и `MYSQL_ROOT_PASSWORD`;
- `.env` с mode `0600`;
- реальные значения не добавляются в Git;
- `.env.example` содержит только имена переменных и фиктивные значения;
- пароль первого администратора создается вручную через UI.
Проверки:
```bash
docker compose config
docker compose pull
systemctl start grimmory
docker compose ps
docker compose logs
curl -fsS http://127.0.0.1:6060/api/v1/healthcheck
```
Критерии:
- оба контейнера healthy;
- MariaDB не публикует порт на LAN;
- после рестарта LXC оба контейнера возвращаются в healthy;
- логи не содержат Flyway errors, authentication failures и циклических рестартов.
## 7. Первый запуск
1. Открыть сервис сначала по LAN.
2. Создать локального администратора.
3. Сохранить пароль в password manager.
4. Отключить неиспользуемую публичную регистрацию.
5. Создать отдельного обычного пользователя для чтения, если административная учетная запись не должна использоваться на BOOX.
6. Включить OPDS.
7. Создать отдельные OPDS credentials.
8. Выдать пользователю разрешение `Access OPDS`.
9. Создать тестовую библиотеку в `/books`.
10. Импортировать одну тестовую EPUB через BookDrop.
Важно: OPDS credentials отделены от обычного логина Grimmory. Пароль OPDS после создания нельзя получить повторно.
## 8. Reverse Proxy
Создать `ansible/playbooks/reverse-proxy-grimmory.yml` по паттерну Gitea/Vaultwarden.
Caddy site:
```caddy
books.ada-dev.ru {
reverse_proxy 192.168.1.34:6060
}
```
Caddy автоматически поддерживает WebSocket upgrade, но это нужно проверить для `/ws`.
Не добавлять дополнительный Caddy Basic Auth: он конфликтует с Basic Auth OPDS.
Проверки до переключения DNS:
- Caddy может достучаться до `192.168.1.34:6060` через OpenVPN;
- upstream healthcheck возвращает `2xx`;
- Caddyfile проходит `caddy validate`;
- конфигурация действительно смонтирована в контейнер Caddy.
После DNS:
```bash
curl -fsS https://books.ada-dev.ru/api/v1/healthcheck
```
Дополнительно проверить:
- валидную цепочку TLS;
- WebSocket `/ws`;
- правильные внешние `Host`, scheme и port;
- отсутствие redirect на внутренний `http://192.168.1.34:6060`;
- загрузку файла размером больше стандартного proxy limit;
- доступ с мобильной сети, а не только из домашнего LAN.
## 9. OPDS и BOOX Go 7
Каталог:
```text
https://books.ada-dev.ru/api/v1/opds
```
Настроить в NeoReader:
- URL корневого каталога;
- отдельный OPDS username/password;
- HTTPS;
- без второго слоя proxy-аутентификации.
Тестовый сценарий:
1. Каталог открывается.
2. Отображаются разделы и обложки.
3. Работает поиск.
4. EPUB скачивается.
5. Книга открывается в NeoReader.
6. Кириллица в названии, авторе и описании отображается корректно.
7. Повторная загрузка не создает поврежденный файл.
8. Проверить EPUB, PDF и FB2 отдельно.
9. Проверить работу из домашнего Wi-Fi и внешней сети.
10. Проверить поведение после смены OPDS-пароля.
Позиция чтения NeoReader обратно в Grimmory через OPDS не синхронизируется. Это ожидаемое поведение, а не дефект сервера.
## 10. Firewall
Минимальная политика:
- SSH `22` только из LAN и OpenVPN;
- `6060` только из LAN/OpenVPN и от нужного reverse-proxy маршрута;
- MariaDB не публикуется;
- остальные входящие соединения запрещены.
Edge case: Docker published ports могут обходить UFW. Поэтому проверить фактические nftables/iptables rules и доступ к `6060` с нежелательных сегментов, а не полагаться только на вывод `ufw status`.
Публично сервис должен быть доступен только через Caddy на `ru-vps`, не через router port-forward.
## 11. Monitoring
Добавить:
- Node Exporter target `192.168.1.34:9100`;
- LAN blackbox HTTP probe `/api/v1/healthcheck`;
- публичный HTTPS probe `https://books.ada-dev.ru/api/v1/healthcheck`;
- TLS expiry check;
- базовые алерты по availability, CPU, RAM, disk и backup freshness.
Не считать один health endpoint достаточным. Он может отвечать при частично неисправной функции OPDS.
Добавить синтетическую OPDS-проверку:
- Basic Auth тестового monitoring-пользователя;
- ответ `200`;
- ожидаемый OPDS content type;
- корректный XML/Atom;
- секреты только из runtime env, не из шаблона в Git.
Если хранить OPDS credential в monitoring нежелательно, ограничиться публичным healthcheck и ручным OPDS smoke test после обновлений.
## 12. PBS Backup
Добавить CTID в:
- `ansible/playbooks/pve-backup-jobs.yml`, job `homelab-pbs-daily-cloud`;
- `ansible/roles/backup_audit/defaults/main.yml`.
Сохранить исключение Docker merged layers:
```text
/var/lib/docker/fuse-overlayfs/*/merged
```
Проверки:
1. Применить backup job.
2. Запустить первый backup вручную.
3. Убедиться, что snapshot завершен без warnings.
4. Проверить наличие VMID `149` в PBS.
5. Проверить прохождение backup audit.
6. Проверить размер backup: слишком маленький может означать ошибочное исключение данных.
7. Выполнить тестовое file-level восстановление нескольких файлов.
8. Запланировать полный restore drill в отдельный временный CTID.
PBS snapshot MariaDB считается crash-consistent, но не заменяет логический dump.
## 13. Offsite Restic
Offsite должен содержать:
- согласованный `mariadb-dump`;
- `/opt/grimmory/data`;
- `compose.yml`;
- зашифрованную или отдельно защищенную конфигурацию;
- manifest с версиями Grimmory и MariaDB.
Не включать:
- `/opt/grimmory/books`;
- `/opt/grimmory/bookdrop`;
- `/opt/grimmory/mariadb` как raw-copy работающей БД;
- Docker layers;
- временные heap dump и cache.
Для дампа:
- использовать `mariadb-dump --single-transaction`;
- сначала писать во временный файл;
- проверять exit code;
- атомарно переносить завершенный dump в staging;
- только после этого запускать restic;
- удалять старый staging после успешного snapshot.
Расширить существующий restic-паттерн MariaDB-aware логикой, а не использовать SQLite backup helper.
Проверки:
- `mariadb` может прочитать dump;
- restic snapshot содержит dump и `/app/data`;
- backup audit отслеживает профиль Grimmory;
- выполнить пробное восстановление в отдельную временную MariaDB;
- убедиться, что восстановленная схема проходит Flyway startup на той же версии Grimmory.
## 14. Обновления
Обновлять только явно, например `v3.2.4 -> v3.2.5`.
Процесс:
1. Прочитать release notes и migration warnings.
2. Проверить свободное место.
3. Сделать MariaDB dump.
4. Сделать offsite restic snapshot.
5. Убедиться в наличии свежего PBS backup.
6. Изменить pinned image tag/digest.
7. Pull и restart.
8. Проверить Flyway logs.
9. Проверить health, UI, BookDrop и OPDS с BOOX.
10. Зафиксировать версию в документации.
Не выполнять code-only downgrade после применения Flyway migration. Для отката возвращать одновременно старый image и pre-upgrade backup базы.
## 15. Edge Cases
Обязательно проверить:
- заполнение rootfs во время массового импорта;
- дубликаты и одинаковые ISBN;
- книги без ISBN;
- кириллические имена и длинные пути;
- два файла одной книги в разных форматах;
- поврежденный EPUB/CBR;
- файлы больше 1 GB;
- cross-filesystem BookDrop move;
- перезапуск во время импорта;
- падение MariaDB во время обработки;
- недоступность Google Books/Open Library/Amazon;
- истечение TLS;
- смена внешнего домена;
- потеря OPDS-пароля;
- потеря admin-пароля;
- OOM и появление большого heap dump;
- несовместимая Flyway migration;
- неожиданное изменение floating Docker tag;
- частичный backup: БД без `/app/data` либо наоборот;
- восстановление БД с файлами библиотеки другого состояния.
## 16. Финальная приемка
Развертывание считается завершенным, когда:
- Ansible запускается повторно без изменений;
- LXC автоматически стартует;
- контейнеры healthy после reboot;
- MariaDB не доступна из LAN;
- HTTPS и WebSocket работают;
- NeoReader открывает OPDS и скачивает тестовые книги;
- импорт через BookDrop проходит полностью;
- Node Exporter и blackbox targets находятся в состоянии `UP`;
- создан и проверен PBS backup;
- создан и тестово восстановлен restic snapshot;
- секреты отсутствуют в Git diff;
- Obsidian-документация обновлена.
## 17. Ожидаемые файлы
Основные изменения:
- `ansible/inventory/hosts.yml`;
- `ansible/playbooks/pve-grimmory.yml`;
- `ansible/playbooks/reverse-proxy-grimmory.yml`;
- `ansible/playbooks/pve-backup-jobs.yml`;
- `ansible/roles/backup_audit/defaults/main.yml`;
- `ansible/playbooks/offsite-restic-yadisk.yml`;
- `ansible/playbooks/backup-audit.yml`;
- `ansible/roles/monitoring_server/templates/prometheus.yml.j2`;
- возможно `ansible/roles/monitoring_blackbox/templates/push-metrics.sh.j2`;
- `ansible/.env.example`, только если потребуются новые runtime secrets.
Документация:
- `Notes/Текущее состояние HomeLab после миграции на Proxmox.md`;
- `Log.md`;
- `Context.md`;
- при необходимости `HomeLab.md`.
## 18. Открытый параметр
Перед реализацией подтвердить `64 GB` как начальный размер диска или выбрать его по текущему объему коллекции.
Решение: текущая коллекция меньше `20 GB`, начальный rootfs оставлен `64 GB`.
## Журнал выполнения
### 2026-07-28 — preflight и проектные решения
- Прочитаны актуальные HomeLab notes и существующие Ansible-паттерны LXC, Caddy, monitoring, PBS и restic.
- CTID `149` отсутствует в `pct list` на `cloud-pc` и `mini-pc`, а также среди исторических snapshot в PBS.
- IP `192.168.1.33` отсутствует в inventory и AdGuard config, не отвечает на ping, ARP-состояние с `cloud-pc``INCOMPLETE`; пользователь подтвердил отсутствие DHCP reservation.
- Storage `cloud-pc:data` активен: доступно около `836 GB`, занято `3.67%`.
- PBS storage и datastore `pbs` активны; последние snapshots текущих cloud-pc CT созданы `2026-07-27`.
- `books.ada-dev.ru` уже разрешается в `157.22.231.198` (`ru-vps`); случайное имя в зоне не разрешается, поэтому это не wildcard. До добавления Caddy site TLS handshake не завершается.
- Подтверждены `grimmory/grimmory:v3.2.4` и `lscr.io/linuxserver/mariadb:11.4.8`; выбраны OCI index digests `sha256:dfa7afdfcf25d649fd664497a62385dd00cd9678c37546e182c172e41c8e80cb` и `sha256:91de7f701bc7fc3a424b81beafca7a7c6c4c5b7c8be6afd2ae148698695c0b0c`.
- Rootfs подтвержден как `data:64`, timezone — `Europe/Moscow`.
- Пароли MariaDB будут случайно созданы один раз внутри LXC и сохранены только в `/opt/grimmory/.env` с mode `0600`.
- В первой итерации автоматизируются health/TLS probes; OPDS smoke test остается ручным после создания credentials.
- Offsite audit первой итерации: атомарный MariaDB dump, проверка наличия dump в snapshot и `restic check`; автоматический restore в MariaDB отложен.
- Первый apply остановился до создания из-за quoting условия VMID guard; второй — из-за отсутствия прямого маршрута к Proxmox API. Для API использован временный SSH local-forward через `ru-vps`.
- После создания CT выяснился IP conflict: с `mini-pc` адрес `.33` резолвился как `YandexStationMini_1DD2.loc` и отклонял TCP/22, тогда как локальный bridge `cloud-pc` видел LXC. Выбран следующий адрес `192.168.1.34`; он не отвечает с обоих PVE-узлов, ARP — `INCOMPLETE`, DNS отсутствует.
### 2026-07-28 — реализация и автоматическая приёмка
- Создан unprivileged LXC `grimmory` CT `149` на `cloud-pc`: `192.168.1.34/24`, `data:64`, 2 vCPU, 4096 MB RAM, 1024 MB swap, `onboot=1`, `nesting=1`, `keyctl=1`, `/dev/fuse`.
- Из-за ограничений API token роль `pve_lxc` используется в create-only mode для Grimmory; VMID guard запрещает менять чужой CT, а network/keyctl/FUSE идемпотентно управляются через `pct` на PVE node.
- Установлены Docker Compose v2 `2.26.1`, Docker `26.1.5`, `fuse-overlayfs`, MariaDB client, Node Exporter и UFW. Docker подтвердил storage driver `fuse-overlayfs`.
- Случайные DB passwords созданы один раз внутри LXC; `/opt/grimmory/.env` имеет mode `0600`, значения не выводились и не добавлялись в Git.
- Compose запустил pinned Grimmory `v3.2.4` и MariaDB `11.4.8`; первый startup применил 142 Flyway migrations и занял около 6 минут. Health wait увеличен до 10 минут.
- После reboot LXC оба контейнера вернулись в `healthy`; MariaDB не слушает host/LAN port, Grimmory привязан к `192.168.1.34:6060`.
- UFW ограничивает SSH/`6060` LAN и OpenVPN, `9100` разрешён только от monitoring LXC. Отдельная `GRIMMORY-FILTER` chain в `DOCKER-USER` закрывает обход UFW через Docker published port.
- Caddy site применён. Проверены public health `200`, сертификат Let's Encrypt до `2026-10-26` и WebSocket/STOMP Upgrade `101` на `/ws`.
- Prometheus targets: Node Exporter `up=1`, LAN health `probe_success=1`, public Grimmory probe `probe_success=1`; TLS expiry покрыт существующим alert.
- CT `149` добавлен в `homelab-pbs-daily-cloud` и PBS audit. Первый PBS backup `ct/149/2026-07-28T14:47:32Z` успешен, размер `2583749785` bytes; audit success, file-level restore трёх объектов проверен.
- Ограничение PBS: storage `data` не поддерживает native LXC snapshots, поэтому job с mode `snapshot` автоматически перешёл в `suspend`; фактическая пауза гостя составила около 1 секунды.
- Создан restic snapshot `a3b012ab` в Yandex Disk. Включены atomic MariaDB dump, `/opt/grimmory/data`, Compose, manifest и зашифрованная restore-конфигурация; books/BookDrop/raw MariaDB исключены. `restic check` и restore непустого `grimmory.sql` прошли.
- Повторные runs `pve-grimmory.yml`, `reverse-proxy-grimmory.yml`, `pve-backup-jobs.yml`, Grimmory offsite/audit и limited monitoring завершились с `changed=0`.
- Обновлены Obsidian current state, `Log.md` и `Context.md`.
- Reverse DNS для `.34` содержит stale имя телефона, но ARP на обоих PVE nodes показывает MAC LXC `bc:24:11:43:75:31`. Нужно исключить `.34` из DHCP pool или закрепить reservation, чтобы предотвратить будущий конфликт.
### Требует участия пользователя
- Создать первого локального администратора и сохранить пароль в password manager.
- Отключить public registration, создать обычного reader-пользователя.
- Создать библиотеку `/books`, выполнить тестовый BookDrop import.
- Включить OPDS, создать отдельные OPDS credentials и выдать `Access OPDS`.
- Проверить EPUB, PDF и FB2 на BOOX Go 7 через Wi-Fi и внешнюю сеть.
- После создания OPDS пользователя решить, добавлять ли credential в synthetic monitoring; сейчас OPDS smoke test ручной.
- Создать DHCP reservation/exclusion для `192.168.1.34`.
- Полный restore drill CT и восстановление dump во временную MariaDB остаются отдельной плановой операцией; file-level restore и dump restore-from-restic уже проверены.
### 2026-07-28 — Caddy KOReader и OPDS compatibility
- `reverse-proxy-grimmory.yml` разделяет device endpoints `/api/koreader/**` и `/api/v1/opds/**` в отдельный Caddy handler.
- Для этих endpoints upstream compression отключён и установлен `Accept-Encoding: identity`; Caddy не добавляет proxy Basic Auth и не изменяет `Authorization`, `X-Auth-*`, `Range`, `WWW-Authenticate` или download headers.
- Запросы с `Accept-Encoding: gzip, br` подтверждённо возвращают `401` без `Content-Encoding` для KOReader и OPDS. Caddy validate, Grimmory health и idempotent rerun успешны.
- Дополнительно исправлен Grimmory OPDS `500` из-за `No acceptable representation`: Caddy нормализует `Accept` только для Atom/OpenSearch catalog routes, оставляя original `Accept` для book download/cover routes.