diff --git a/adr/016-versioned-deployment-bundle.md b/adr/016-versioned-deployment-bundle.md new file mode 100644 index 0000000..595ce63 --- /dev/null +++ b/adr/016-versioned-deployment-bundle.md @@ -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 комплекта. diff --git a/adr/README.md b/adr/README.md index 8413903..420e41b 100644 --- a/adr/README.md +++ b/adr/README.md @@ -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) diff --git a/architecture/deployment.md b/architecture/deployment.md index bc6955b..7dadddc 100644 --- a/architecture/deployment.md +++ b/architecture/deployment.md @@ -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 можно снять маркер незавершённой операции. diff --git a/glossary.md b/glossary.md index b901ee1..67d24bb 100644 --- a/glossary.md +++ b/glossary.md @@ -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` и