docs: document versioned deployment bundle #20

Merged
ksv741 merged 1 commits from codex/deploy-design into develop 2026-09-20 19:19:48 +03:00
4 changed files with 135 additions and 142 deletions
+72
View File
@@ -0,0 +1,72 @@
# 016. Версионируемый комплект развёртывания
**Status:** Accepted
Frontend и backend выпускаются независимо. Выбор их версий непосредственно
на сервере перекладывает подбор совместимой пары на оператора при каждом
обновлении. Принято решение выпускать комплект в репозитории `deploy`:
его релиз закрепляет проверенную пару конкретных версий обоих образов.
Независимые релизы компонентов сохраняются; выпуск компонента сам по себе
не означает готовность нового релиза развёртывания.
Оператор подключается к серверу по SSH и одной командой запускает обновление
до выбранного релиза развёртывания. При обновлении backend команда должна
скачать образы до его остановки, сохранить резервную копию остановленной базы,
выполнить миграции и проверить запуск. Ограничения
[ADR 015](015-single-server-sqlite-deployment.md)
сохраняются: один backend, краткий простой, неуспешная миграция блокирует запуск;
откат образа не восстанавливает базу автоматически.
## Доставка и подготовка сервера
На сервере хранится Git checkout `deploy`. Команда обновления получает
выбранный тег комплекта и загружает закреплённые образы из Nexus;
сборка приложения на сервере не требуется.
Docker с Compose, внешний HTTPS и доступ к registry считаются подготовленными.
README комплекта должен описывать последовательность этой разовой подготовки
и подключения существующей установки. Автоматическое provisioning сервера
в комплект не входит.
Рабочие данные, сертификат доверия T-Bank и секреты остаются вне обновляемого
checkout. При подключении существующей установки сохраняются её БД,
JWT secret и ключ шифрования. Сертификат `certs/tbank-ca.pem` монтируется
в контейнер только для чтения; путь внутри контейнера задаётся через
`TBANK_CA_CERT_PATH`. Этот сертификат не обслуживает входящий HTTPS.
## Резервные копии
Перед обновлением backend создаётся согласованная копия всего каталога данных
при остановленном backend. Копии хранятся в локальном `backups/` вне checkout,
без автоматического удаления; очистку выполняет оператор вручную.
Такие копии позволяют восстановиться после неудачного обновления, но сами
по себе не защищают от потери диска сервера.
## Обновление и сбои
Если релиз меняет только frontend, обновляется только его контейнер:
backend не останавливается, миграции и резервное копирование БД для этого
обновления не выполняются. Этот путь применим только при неизменных образе
и конфигурации backend.
Если миграции прошли, но новый backend не прошёл проверку запуска, обновление
завершается с ошибкой, неисправный backend останавливается. Оператор получает
диагностику, сведения о предыдущей версии и путь к резервной копии.
Автоматического отката нет: прежний образ может быть несовместим с новой
схемой. Откат образа и восстановление базы остаются отдельными явными действиями
по правилам ADR 015.
## Выпуск комплекта
После публикации образов компонентов отдельно готовится релиз `deploy`:
фиксируется совместимая пара версий, выполняется её проверка и выпускается
тег комплекта. Публикация компонента не создаёт релиз комплекта автоматически
и не запускает обновление сервера. Оператор выбирает выпущенный тег при
запуске команды развёртывания.
## Состояние реализации
Комплект и команда реализованы в отдельном репозитории `deploy`.
Публикация первого тега и обновление рабочего сервера — отдельные действия.
[Контейнерная поставка](../architecture/deployment.md) описывает границы,
пошаговая инструкция находится в README комплекта.
+1
View File
@@ -20,3 +20,4 @@
- [013 — НКР как первый источник кредитных рейтингов облигаций](013-nkr-bond-credit-ratings.md)
- [014 — Актуальная OpenAPI-схема как вход frontend](014-trusted-openapi-input.md)
- [015 — SQLite в ограниченном single-server deployment](015-single-server-sqlite-deployment.md)
- [016 — Версионируемый комплект развёртывания](016-versioned-deployment-bundle.md)
+54 -142
View File
@@ -1,19 +1,21 @@
# Контейнерная поставка
Runbook для одного Docker host, одного backend и краткого простоя.
Поставка для одного Docker host, одного backend и краткого простоя.
[Топология](containers.md#контейнерная-поставка) и
[ADR 015](../adr/015-single-server-sqlite-deployment.md) объясняют границы.
Команды выполняются из checkout `WealthLedger/main`, содержащего `compose.yaml`.
Development дополнительно требует checkout `frontend/` и `backend/`.
Production-комплект находится в отдельном [репозитории deploy](https://git.personal.ksv741.ru/WealthLedger/deploy).
Его README — единственная пошаговая инструкция установки, обновления и recovery;
[ADR 016](../adr/016-versioned-deployment-bundle.md) объясняет выбор релиза комплекта.
Development-команды ниже выполняются из workspace с checkout `frontend/` и `backend/`.
## Предпосылки
- Docker Engine с Compose plugin **не ниже 2.24.4** (`!override` в development),
доступ к registry и достаточно свободного диска для non-root UID 1000.
Локально проверены Engine 29.8.0, Compose 5.5.1, Linux arm64.
Production images `0.2.3` опубликованы для Linux amd64.
Другую архитектуру проверяют отдельно; multi-platform images не обещаны.
- Nexus Docker hosted repository `docker-hosted`, доступный через
`https://nexus.ksv741.keenetic.pro/v2/`. Docker Bearer Token Realm включён;
`nexus.personal.ksv741.ru/docker-hosted/`. Docker Bearer Token Realm включён;
reverse proxy передаёт исходные host и HTTPS scheme. Адрес token realm
в `WWW-Authenticate` также должен начинаться с `https://`.
Deployment policy запрещает redeploy существующих release tags.
@@ -29,8 +31,8 @@ Development дополнительно требует checkout `frontend/` и `b
`127.0.0.1:8080` (или выбранный frontend port). Отдельного `/api` upstream нет.
DNS, TLS, firewall и provisioning Caddy выполняются вне этого runbook.
Реальная публикация и server pull-only login требуют отдельной проверки
на вашей инфраструктуре. Локальные images не доказывают registry permissions.
Серверный login и доступ к images проверяются на целевом сервере.
Локальный smoke не доказывает серверные registry permissions.
## Development
@@ -69,7 +71,7 @@ Project `wealthledger-dev` хранит SQLite в `development-data`. Обычн
version, затем выполняет `npm ci`, `npm run verify`; backend дополнительно
выполняет `npm run test:e2e`. У каждого image OCI version/revision labels.
Images публикуются как `nexus.ksv741.keenetic.pro/wealthledger/frontend:X.Y.Z`
Images публикуются как `nexus.personal.ksv741.ru/docker-hosted/wealthledger/frontend:X.Y.Z`
и `.../backend:X.Y.Z`. `latest` не создаётся.
Workflow отказывается перезаписывать существующие release tags;
защита release tags и запрет параллельных publishers обязательны. Если одна
@@ -78,8 +80,8 @@ Workflow отказывается перезаписывать существу
Выберите опубликованные version tags, выполните pull и сравните OCI revision
с release commit. Выполните smoke с disposable data/config до первого server
rollout. В production env хранится пара полных image references, например
`nexus.ksv741.keenetic.pro/wealthledger/frontend:0.2.0`. Сохраните versions/revisions
rollout. В `release.env` комплекта хранится пара полных image references, например
`nexus.personal.ksv741.ru/docker-hosted/wealthledger/frontend:0.2.3`. Сохраните versions/revisions
в deployment record. Release tags не должны переназначаться другим images.
## Перенос существующих images из Gitea
@@ -94,11 +96,11 @@ rollout. В production env хранится пара полных image referenc
```bash
skopeo login git.personal.ksv741.ru
skopeo login nexus.ksv741.keenetic.pro
skopeo login nexus.personal.ksv741.ru
for component in frontend backend; do
skopeo copy --all --preserve-digests \
"docker://git.personal.ksv741.ru/wealthledger/$component:0.2.0" \
"docker://nexus.ksv741.keenetic.pro/wealthledger/$component:0.2.0" || exit 1
"docker://nexus.personal.ksv741.ru/docker-hosted/wealthledger/$component:0.2.0" || exit 1
done
```
@@ -120,150 +122,60 @@ SQLite и не меняет encryption keys. Не переносите Git relea
## Первая установка
Создайте restricted environment file и data directory:
На подготовленном сервере оператор клонирует `deploy`, настраивает внешний
HTTPS и registry login, размещает server env, сертификат T-Bank и data directory
вне checkout. Для новой пустой базы команда вызывается с `--init`.
Для действующей установки сохраняются прежние данные и ключи, а первый переход
выполняется как полное обновление с backup. Отсутствующая БД без `--init` — ошибка.
Точные команды и разрешения каталогов приведены в README комплекта.
```bash
sudo install -d -m 0700 /srv/wealthledger
sudo install -d -m 0700 -o 1000 -g 1000 /srv/wealthledger/data
sudo install -m 0600 .env.compose.example /srv/wealthledger/production.env
```
Дальнейшие команды выполняет оператор, имеющий доступ к environment file,
backup directory и Docker. Заполните env локальным защищённым редактором:
полные ссылки `FRONTEND_IMAGE` и `BACKEND_IMAGE` с опубликованными version tags,
публичный HTTPS origin и абсолютный data path. Версии в примере — образец
формата; перед запуском выберите реально опубликованные releases.
Пустые required значения блокируют config; некорректные secrets блокируют backend.
Не выводите полный rendered config с настоящими secrets в журналы.
Для новой базы создайте JWT secret длиной хотя бы 32 символа и отдельный
32-byte encryption key в base64 (`openssl rand -hex 32` и
`openssl rand -base64 32` в защищённом терминале). Сохраните ключ отдельно
от data backup. Для существующей установки перенесите прежние JWT secret
и encryption key, а согласованную копию **всего** data directory — при
остановленном прежнем backend. Файл должен называться `wealthledger.db`;
если переименовываете, согласуйте также WAL/SHM companion names до старта.
Не запускайте старый и новый backend на одной базе одновременно.
Войдите в registry пользователем `wealthledger-ci` без помещения пароля в командную строку:
```bash
docker login nexus.ksv741.keenetic.pro --username wealthledger-ci
export COMPOSE_FILE=compose.yaml
export COMPOSE_PROJECT_NAME=wealthledger
export COMPOSE_ENV_FILES=/srv/wealthledger/production.env
docker compose config --quiet
docker compose pull frontend backend migration
docker compose up -d --wait --wait-timeout 120
docker compose ps -a
```
Migration и backend используют один image и один data mount.
Migration должна завершиться с кодом 0; иначе backend не запускается.
Readiness принимает 401 от существующего `/api/v1/auth/me` без credentials.
Frontend становится healthy после backend. Backend host port отсутствует.
Runtime non-root; root filesystem read-only, nginx temp files находятся в tmpfs.
Проверьте SPA deep link, регистрацию/вход, refresh, существующие карты,
чтение старого зашифрованного подключения и один финансовый запрос.
Production refresh-cookie имеет Secure flag: проверяйте через HTTPS Caddy;
loopback HTTP curl без browser cookie jar не доказывает browser session.
При локальном smoke используйте disposable credentials и явную передачу cookie.
Направляйте Caddy на новый deployment только после успешной проверки.
Прежний runtime удаляйте только после проверки новой установки.
Git-тег `deploy` закрепляет пару images через `release.env`. Версия комплекта
не обязана совпадать с версиями компонентов. Публикация image не выпускает
комплект и не обновляет сервер автоматически. Команда получает выбранный тег,
загружает images до простоя и использует весь snapshot этого commit.
## Backup при остановленном backend
До backend update проверьте совместимость новой версии с текущим frontend
и схемой, а обратной версии — с новой схемой. При неизвестной совместимости
не обещайте image rollback. В выбранном rollout отсутствуют параллельные API.
Ниже data path соответствует примеру env; при другом path замените его.
Сохраните текущую пару image references/revisions и отдельно — ключи.
```bash
docker compose stop backend
docker compose ps -a backend
install -d -m 0700 /srv/wealthledger/backups
BACKUP_DIRECTORY="/srv/wealthledger/backups/$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -m 0700 "$BACKUP_DIRECTORY"
cp -a /srv/wealthledger/data "$BACKUP_DIRECTORY/data"
```
Перед копированием убедитесь, что backend остановлен, migration не выполняется
и никто другой не пишет в каталог. Успех `cp` обязателен перед продолжением.
Не копируйте отдельный `.db` из работающего приложения. Проверяйте backup
на отдельной копии: migration status, `PRAGMA integrity_check`, auth, карты
и расшифровка тестового подключения исходным ключом. Backup без проверяемого
ключа недостаточен для восстановления подключения.
Сначала останавливаются писатели, затем копируется весь каталог данных,
включая возможные WAL/SHM. Только успешная копия допускает миграции. Локальные
backups находятся рядом с data directory, вне checkout, и автоматически
не удаляются. Незавершённая копия помечается `.partial-` и не подходит для restore.
Ключ шифрования хранится отдельно. Локальная копия не защищает от потери диска.
## Независимое обновление frontend
После проверки API compatibility замените только `FRONTEND_IMAGE` в env:
```bash
docker compose pull frontend
docker compose up -d --no-deps --wait --wait-timeout 120 frontend
```
Backend container и SQLite не пересоздаются. Входная точка кратко недоступна.
Проверьте deep link, login/refresh и новую OCI revision. Старым browser tabs
может понадобиться reload: старые hashed chunks не сохраняются между images.
Если изменились только image или конфигурация frontend, backend не
пересоздаётся, backup и migration не выполняются. Возможен короткий перерыв
на внешней точке входа; старым browser tabs может потребоваться reload.
При неизменных исправных контейнерах повтор команды не пересоздаёт сервисы.
## Независимое обновление backend
Выполните stopped-backend backup выше. Замените только `BACKEND_IMAGE` в env.
Последовательность не продолжается после failed migration:
Изменение image или конфигурации backend запускает stop → backup → migration →
readiness. Неизменный frontend не пересоздаётся. Во время простоя возможен 502.
Ошибка migration блокирует старт, ошибка готовности останавливает новый backend;
ошибка frontend не останавливает исправный backend. Успех фиксируется только
после проверок запуска затронутых компонентов.
```bash
docker compose pull backend migration
docker compose up --no-deps --force-recreate --abort-on-container-exit \
--exit-code-from migration migration &&
docker compose up -d --no-deps --wait --wait-timeout 120 backend
```
Frontend не пересоздаётся; nginx повторно разрешает backend DNS. Во время
перерыва возможен 502. При failure оставьте backend остановленным, сохраните
диагностику migration и выберите совместимый image либо отдельный restore.
Не используйте `migrate reset` или автоматический `resolve` как recovery.
Проверьте migration status, health, login/refresh, карты и расшифровку подключения.
После успешного smoke запишите фактически установленную пару image references.
Одна file lock защищает установку от параллельных команд. Маркер незавершённого
обновления блокирует повтор до разбора оператором. Он содержит этап, версии
и путь к backup, но не секреты и не копию данных. После обновления нужен smoke
через HTTPS: healthcheck сам по себе не доказывает работу Secure refresh-cookie.
## Откат image
Для frontend верните предыдущий совместимый version tag и повторите frontend update.
Для backend сначала остановите процесс и подтвердите совместимость старого
image с **текущей** схемой. Верните предыдущий image reference и запустите только backend:
```bash
docker compose pull backend
docker compose up -d --no-deps --wait --wait-timeout 120 backend
```
Данные и encryption key не заменяются. Migration service здесь намеренно
не запускается: старые migrations не являются down-migrations. Прочитайте
новые записи, сделанные после backup, и проверьте auth/cards/connection.
При несовместимости оставьте приложение остановленным до решения о recovery.
Откат версии и восстановление базы — разные операции. Прежний backend можно
запускать только после проверки совместимости с текущей схемой. Автоматического
rollback нет; migration service при ручном image rollback не запускается.
Порядок ручных Compose-команд и проверки recovery находится в README комплекта.
## Explicit restore
Это отдельное решение оператора, **не часть image rollback**. Оператор выбирает
backup timestamp и явно принимает потерю всех изменений после него, включая
сессии, карты и подключения. Сначала воспроизведите restore на isolated project
с отдельным data path/port и совместимым image; не используйте production data
как fixture. Убедитесь в наличии соответствующего encryption key.
После отдельного решения остановите backend и убедитесь, что migration не
работает. Сохраните нынешний каталог под новым именем, ничего не удаляя:
```bash
docker compose stop backend
mv /srv/wealthledger/data /srv/wealthledger/data-before-restore-TIMESTAMP
cp -a /srv/wealthledger/backups/CHOSEN_TIMESTAMP/data /srv/wealthledger/data
```
Проверьте owner 1000:1000, permissions, выбранный compatible image и исходный
ключ. Запустите backend командой rollback с `--no-deps`, затем выполните smoke.
Сверьте наличие записей до backup и отсутствие известных записей потерянного
интервала. Сохраните прежний каталог для расследования. При необходимости
новой migration сначала выполните обычную backup/update последовательность.
Оператор выбирает проверенную копию и явно принимает потерю всех более новых
записей. Сначала восстановление проверяется на isolated данных с прежним ключом
и совместимым image. На целевой установке останавливаются писатели, текущий
каталог сохраняется под новым именем, затем восстанавливается выбранная копия.
Backend пересоздаётся, чтобы заново подключить заменённый bind-каталог:
простой stop/start может сохранить mount прежних данных.
Проверяются целостность БД, auth/cards и расшифровка подключения. Только после
подтверждённого recovery можно снять маркер незавершённой операции.
+8
View File
@@ -4,6 +4,14 @@
Связи между ними объясняет [обзор продукта](overview.md), подробные правила —
[предметные области](README.md#предметные-области).
## Релиз развёртывания WealthLedger
Версия комплекта установки WealthLedger, закрепляющая проверенную совместимую
пару релизов frontend и backend. Понятие принято в
[ADR 016](adr/016-versioned-deployment-bundle.md).
_Не использовать_: релиз frontend или backend как синоним релиза всей установки.
## User
Локальный владелец данных WealthLedger, идентифицируемый `id` и