feat: add versioned deployment bundle

This commit is contained in:
2026-09-20 18:48:55 +03:00
commit 6f85fcb0f6
7 changed files with 811 additions and 0 deletions
+255
View File
@@ -0,0 +1,255 @@
# WealthLedger deploy
Установка и обновление одного Linux-сервера с Docker Compose и SQLite:
```bash
./deploy.sh vX.Y.Z
```
`vX.Y.Z` — Git-тег **этого репозитория**. Его [release.env](release.env)
закрепляет полные адреса frontend и backend. Версии компонентов могут различаться.
Скрипт получает тег из `origin` и выполняет код и Compose выбранного commit;
изменения рабочего checkout не подменяют содержимое релиза.
Образы загружаются из `nexus.personal.ksv741.ru/docker-hosted/wealthledger/`.
Текущий кандидат пары — frontend/backend `0.2.3`; образы опубликованы для
`linux/amd64`. Не создавайте тег комплекта до проверки пары по разделу выпуска.
## Подготовка сервера
Выполняется один раз. Автоматическая установка Docker, настройка DNS, HTTPS,
firewall и registry в скрипт не входят.
1. Установите Git, Bash, Docker Engine с Compose plugin (2.24.4 или новее),
`flock` из util-linux и стандартные `tar`, `cp`, `mktemp`. Используется
локальный Docker daemon через Unix socket, не удалённый context.
2. Настройте внешний HTTPS reverse proxy: весь домен направляется на
`127.0.0.1:8080`. Отдельный upstream `/api` не нужен. Порт можно поменять
через `FRONTEND_PORT`; `FRONTEND_ORIGIN` должен совпадать с HTTPS origin.
3. Выберите одного оператора с доступом к Docker, Git origin, ключам и данным.
Скрипту нужны права читать всю БД и создавать копии. Процесс backend работает
с UID/GID `1000:1000`; при другом UID оператора настройте доступ заранее.
Запускайте команды из одной и той же учётной записи, чтобы Git и Docker
использовали подготовленные SSH-ключи и registry credentials.
4. Войдите в Nexus, вводя пароль интерактивно, без записи в shell history:
```bash
docker login nexus.personal.ksv741.ru --username wealthledger-ci
```
5. Разместите checkout отдельно от постоянных ресурсов. Пример:
```text
/opt/wealthledger/
├── deploy/ # этот Git checkout
│ ├── compose.yaml
│ ├── deploy.sh
│ ├── release.env # версии frontend и backend
│ ├── .env.example
│ └── README.md
├── production.env # секреты и настройки, chmod 600
├── certs/tbank-ca.pem # CA для подключения к T-Bank, не HTTPS сайта
├── data/wealthledger.db
├── backups/ # согласованные копии, без автоочистки
└── .deploy/ # lock, current и при сбое pending
```
```bash
git clone ssh://git@git.personal.ksv741.ru:2222/WealthLedger/deploy.git /opt/wealthledger/deploy
cd /opt/wealthledger/deploy
install -m 600 .env.example ../production.env
```
Последнюю команду выполняйте только при отсутствии `production.env`:
существующий файл с ключами не перезаписывайте.
6. Заполните [настройки](.env.example) защищённым редактором. `DATA_DIRECTORY`
и `TBANK_CA_CERT_FILE` — существующие абсолютные пути, не внутри checkout.
`TBANK_CA_CERT_PATH` — путь внутри контейнера; сертификат монтируется read-only.
Каталог данных должен быть доступен для записи UID 1000, сертификат — для
чтения. Не отключайте проверку TLS вместо установки корректного CA.
7. Проверьте доступ к образам, свободное место под полную копию data и существующую
схему HTTPS. Сохраните ключ шифрования отдельно от локальных backups.
Не храните рабочие секреты в Git, не выполняйте `source production.env` и не
печатайте полный `docker compose config` с реальными ключами.
## Подключение существующей установки
Сохраните её Compose и текущие image references до перехода. Заполните
`production.env` **прежними** `JWT_SECRET` и `TOKEN_ENCRYPTION_KEY` и укажите
действующий data directory и сертификат. Не копируйте один `.db` из работающего
приложения и не создавайте новые ключи: старые подключения должны расшифровываться.
Проект Compose должен называться `wealthledger`, сервисы — `backend`, `frontend`
и `migration`, база — `wealthledger.db`. Оператор должен исключить другие
процессы записи в этот каталог; старый и новый способы запуска параллельно
не используются. Скрипт откажется менять data mount или действующие ключи.
```bash
cd /opt/wealthledger/deploy
./deploy.sh vX.Y.Z
```
Первый переход всегда выполняет полный цикл с backup, поскольку ещё нет
подтверждённого baseline комплекта. Данные не переносятся и не переименовываются.
`--init` для действующей установки не применяется.
## Новая установка
Только для нового пустого data directory подготовьте UID/GID `1000:1000`,
permissions `0700` и новые ключи: JWT secret не короче 32 символов и отдельный
32-byte encryption key в base64. Например, `openssl rand -hex 32` и
`openssl rand -base64 32` в защищённом терминале; значения перенесите в env.
Предварительно установите сертификат T-Bank.
```bash
./deploy.sh vX.Y.Z --init
```
Отсутствие файла БД без `--init` считается ошибкой, а не разрешением создать
пустую базу. `--init` отклоняется при наличии прежних контейнеров, успешной
установки или непустого data directory. Для первой пустой базы backup не нужен.
Если env расположен иначе, используйте `--env-file /absolute/path/production.env`.
Backups и `.deploy` всегда находятся рядом с фактическим data directory,
поэтому другой env-файл не создаёт отдельную блокировку для той же базы.
## Обновление
После публикации нового тега комплекта достаточно той же команды:
```bash
./deploy.sh vX.Y.Z
```
1. Проверка тега, настроек, данных и CA; загрузка образов и проверка прав
runtime user. Ошибка до остановки сохраняет работающие сервисы.
2. Сравнение образов и конфигурации с реальными контейнерами. При исправной
неизменной установке — no-op. Если изменился только frontend, backend
продолжает работать, backup и migration не запускаются.
3. Если изменился backend или его конфигурация: stop → копия **всего** data
directory с WAL/SHM → migration → backend readiness. Неизменный frontend
не пересоздаётся; если меняется и он, запускается после готовности backend.
4. Успех записывается в `.deploy/current` только после проверки запуска.
Краткий простой допустим. После обновления проверьте через HTTPS deep link,
login/refresh, существующие карты и чтение зашифрованного подключения.
Проверка сертификата/прав запускает кратковременный контейнер с Node validation,
не второй API. Контейнерная готовность не заменяет browser smoke.
## Ошибка или прерывание
Скрипт возвращает ненулевой код, этап ошибки, предыдущие images и расположение
готовой копии. Он не печатает raw application logs или секреты. После начала
изменений `.deploy/pending` сохраняет безопасные сведения для восстановления;
следующая команда остановится, пока оператор не разберёт инцидент.
- Ошибка backup: миграции не запускались, backend остановлен. Скрытые каталоги
`backups/.partial-*` — незавершённые копии, использовать их для restore нельзя.
- Ошибка migration: новый backend не запускается. Не применяйте `migrate reset`
или автоматический `resolve`; сначала выясните состояние схемы.
- Ошибка готовности backend: неисправный backend останавливается. Старый image
автоматически не запускается — он может быть несовместим с новой схемой.
- Ошибка frontend: исправный backend остаётся работающим, БД не восстанавливается.
- После жёсткого прерывания migration может ещё работать: перед любым копированием
или восстановлением проверьте контейнеры и остановите писателей.
Посмотрите `docker ps -a` и `.deploy/pending`. Для диагностики соответствующего
контейнера используйте локально `docker logs <container>`; перед передачей логов
убедитесь, что в них нет чувствительных данных.
## Ручной откат и восстановление
`deploy.sh` не является автоматическим rollback: выбор старого тега сам по себе
не доказывает совместимость со схемой. Сначала проверьте план на отдельной копии
данных с прежним ключом, другим Compose project и отдельным портом.
Подготовьте снимок выбранного **совместимого** тега для ручных Compose-команд:
```bash
RECOVERY_BUNDLE=$(mktemp -d)
git archive vCOMPATIBLE.VERSION.HERE | tar -x -C "$RECOVERY_BUNDLE"
unset FRONTEND_IMAGE BACKEND_IMAGE COMPOSE_FILE COMPOSE_PROJECT_NAME COMPOSE_PROFILES
COMPOSE=(docker compose --project-name wealthledger \
--env-file /opt/wealthledger/production.env \
--env-file "$RECOVERY_BUNDLE/release.env" -f "$RECOVERY_BUNDLE/compose.yaml")
```
Замените демонстрационный тег настоящим. Используйте Bash и реальные абсолютные
пути. Для image rollback без изменения данных:
```bash
"${COMPOSE[@]}" stop backend migration
"${COMPOSE[@]}" pull backend
"${COMPOSE[@]}" up -d --no-deps --wait --wait-timeout 120 backend
```
Для frontend аналогично выполняются `pull frontend` и `up ... frontend`.
Migration при image rollback намеренно не запускается.
**Restore — отдельное решение с потерей всех записей после выбранного backup.**
После проверки целостности выбранной копии и наличия её ключа, при остановленных
писателях сохраните текущие данные под новым именем, не удаляя их:
```bash
"${COMPOSE[@]}" stop backend migration
mv /opt/wealthledger/data /opt/wealthledger/data-before-restore-UNIQUE_TIMESTAMP
cp -a /opt/wealthledger/backups/CHOSEN_BACKUP/data /opt/wealthledger/data
"${COMPOSE[@]}" up -d --no-deps --force-recreate --wait --wait-timeout 120 backend
```
Подставьте уникальное ещё не существующее имя и конкретный backup. Убедитесь,
что backend именно пересоздан: после замены bind-каталога простой stop/start
может сохранить mount прежнего каталога и вернуть более новые данные.
Проверьте owner `1000:1000`, `PRAGMA integrity_check`, доступность данных, auth, карты
и расшифровку подключения. Убедитесь, что восстановленный image совместим
со схемой; старые migration-файлы не являются down-migrations.
Только после проверки успешного recovery переместите `.deploy/pending` в файл
инцидента, например `.deploy/recovered-UNIQUE_TIMESTAMP`, и повторите smoke.
Если новый релиз ещё не установлен, текущая запись может оставаться прежней:
следующее обновление сверяется с реальными контейнерами. Файл `lock` не удаляйте:
блокировка освобождается процессом; удаление файла при живом процессе опасно.
## Резервные копии
Скрипт создаёт уникальный каталог и не перезаписывает прежние копии. Готовой
копия становится только после полного успешного `cp -a`; частичные копии
отмечены префиксом `.partial-`. Проверяйте восстановление на отдельной копии.
Очистка только ручная. Контролируйте свободное место; не удаляйте backup,
нужный незавершённому обновлению. Копия на том же диске не защищает от его потери.
Независимое хранение ключа обязательно; внешнее backup-хранилище сюда не входит.
## Выпуск комплекта и проверки
1. Дождитесь публикации компонентов в Nexus. В `release.env` закрепите ровно
`FRONTEND_IMAGE` и `BACKEND_IMAGE` с version tags. Не используйте `latest`.
2. Выполните локальный gate (Python 3 нужен только для tests):
```bash
bash -n deploy.sh tests/deploy.sh
bash tests/deploy.sh
git diff --check
```
Tests используют настоящий Git, backup-файлы и file locks, а Docker заменяют
управляемым double. Проверяются update/no-op, смена настроек, сохранение ключей,
ошибки pull/backup/migration/readiness, конкуренция и незавершённая операция.
Отдельная проверка реального Docker для restore: `bash tests/deploy.sh --docker-restore`.
Она создаёт временный project, заменяет bind-каталог и проверяет пересоздание
контейнера. На arm64 для проверки текущих amd64 images можно использовать
`DOCKER_DEFAULT_PLATFORM=linux/amd64` при наличии Docker emulation.
3. На disposable Linux-установке с настоящими images проверьте `--init`, adoption,
frontend-only/backend-only update, повтор, backup/restore и HTTPS smoke.
Сверьте OCI revision images с release commits компонентов. Для проверки
кандидата используйте временный Git origin/tag, не двигайте опубликованный тег.
4. После review и проверок примите commit в `main`, создайте аннотированный
`vX.Y.Z` именно на проверенном commit и отправьте этот тег. Версия комплекта
самостоятельная: одинаковые номера frontend/backend не требуются.
Выпуск компонента не выпускает комплект и не обновляет сервер автоматически.
Опубликованные Git и Nexus tags не переназначаются. Для исправления нужен
новый релиз комплекта. В репозитории не нужны npm, собственный release framework
или pipeline, автоматически выбирающий последние образы.