Files
han-app/modules/module-10-deployment-runbook.md
T

1200 lines
52 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.
# module-10. Runbook развёртывания HAN Chat
> Статус: последовательная инструкция первого production-like деплоя и эксплуатации на одной Ubuntu VM.
> Все значения в `<УГЛОВЫХ_СКОБКАХ>` — placeholders. Команды с `cd <BACKEND_ROOT>` требуют подстановки реального пути корня backend-репозитория на VM.
> Источники: [`README.md`](README.md), [`arch-00-glossary.md`](arch-00-glossary.md), [`arch-01-system-architecture.md`](arch-01-system-architecture.md), [`arch-02-api-contracts.md`](arch-02-api-contracts.md), [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md), [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), [`arch-05-agent-development-process.md`](arch-05-agent-development-process.md), [`module-01-api-backend.md`](module-01-api-backend.md)[`module-09-observability.md`](module-09-observability.md).
## 1. Неподвижные правила
1. Один root `docker compose` запускается из `<BACKEND_ROOT>`.
2. Ровно один edge nginx публикует `80/443`.
3. API, Keycloak, Redis, OTEL, Safety и Bitrix-сервисы не имеют host `ports`.
4. `/internal/*` не маршрутизируется публично.
5. Managed PostgreSQL находится вне Compose, в той же VPC, без public IP.
6. S3 — внешний Selectel-compatible storage; клиент получает только presigned URL.
7. Секреты не коммитятся, не вставляются в команды shell history и не выводятся в отчёты.
8. Миграции выполняются отдельными one-shot steps до новой версии приложения.
9. Message Safety запускается как documented stub до замены; это не production antivirus/moderation.
10. `bitrix-sync` запускается как DB-connectivity stub; полноценной CRM sync нет.
## 2. Роли и обозначения
- **Cloud admin**: VPC, VM, PG, S3, DNS/security groups.
- **Deploy operator**: VM, Compose, migrations, release/rollback.
- **Bitrix admin**: local app, connector, Open Line 8, callbacks.
- **Security owner**: secrets, Keycloak admin MFA, firewall, retention.
Placeholders:
```text
<PUBLIC_HOST> например chat.example.ru
<VM_PUBLIC_IP> публичный IPv4 VM
<VM_PRIVATE_IP> приватный IPv4 VM
<VPC_CIDR> например 10.20.0.0/24
<PG_PRIVATE_HOST> private FQDN/IP managed PG
<PG_PORT> 5432 или 6432
<PG_DATABASE> han_chat
<BACKEND_REPO_URL> URL репозитория
<BACKEND_ROOT> /opt/han-chat/backend
<RELEASE> immutable tag/git SHA
<ACME_EMAIL> адрес ops, не placeholder в реальном запуске
<BITRIX_PORTAL> разрешённый портал
```
## 3. Stage 0 — решения до provisioning
### 3.1. Зафиксировать параметры
- region/availability zone и VPC;
- hostnames и TTL DNS;
- VM image Ubuntu 24.04 LTS;
- sizing;
- PG plan/storage/backups/PITR;
- S3 region/endpoint/bucket names;
- container registry и immutable image tags/digests;
- remote observability backend;
- RPO/RTO и maintenance window;
- ответственных за alerts/Bitrix/Keycloak.
Начальный sizing без local Grafana stack:
- VM: 4 vCPU, 8 ГБ RAM, 80 ГБ SSD, 4 ГБ swap;
- managed PG: минимум 2 vCPU, 4 ГБ RAM, 50 ГБ, HA по возможности;
- Redis limit: 512 МиБ;
- OTEL Collector: 512 МиБ + 510 ГБ queue;
- свободный диск VM после pull/build: не менее 30%.
Это baseline, не гарантия. До real traffic обязателен load test с long Safety poll и WS.
### Gate 0
- [ ] Владельцы и maintenance window назначены.
- [ ] RPO/RTO приняты хотя бы временно: ориентир RPO PG ≤15 минут/PITR, RTO ≤4 часа.
- [ ] Решено: images pull из registry или build на VM.
- [ ] Remote telemetry backend выбран либо явно принят ограниченный debug-only режим.
- [ ] Риск mock OTP и Safety stub письменно принят.
**Ожидаемый результат:** есть release checklist с конкретными values; не создано ни одной публичной БД/Redis.
## 4. Stage 1 — VPC, VM, DNS и security groups
### 4.1. Сеть
Создать одну private subnet для VM и managed PG. PG получает только private address. VM имеет public IP только для nginx/SSH.
Security groups:
| Source | Destination | Port | Rule |
|---|---|---:|---|
| trusted ops CIDR/VPN | VM | SSH `<SSH_PORT>` | allow |
| internet | VM | TCP 80 | allow для redirect/ACME |
| internet | VM | TCP 443 | allow |
| VM private IP/SG | managed PG | `<PG_PORT>` | allow |
| VM | internet | 443 | allow egress: registry, Bitrix, S3, OTLP, ACME |
| internet | managed PG | any | deny |
| internet | VM | 6379, 4317, 4318, 8000, 8080, 9000 | deny |
Если cloud SG не поддерживает egress allow-list, оставить egress open и контролировать destinations приложением/TLS; не ломать S3/Bitrix/OIDC.
### 4.2. DNS
Создать `A <PUBLIC_HOST> → <VM_PUBLIC_IP>`. Не добавлять `www`, если он не нужен и не включён в certificate. Для отдельного API host действуют правила arch-03; MVP предпочтительно использует один host с paths.
Проверка с рабочей станции:
```bash
dig +short <PUBLIC_HOST>
```
Ответ должен совпасть с `<VM_PUBLIC_IP>`.
### Gate 1
- [ ] PG не имеет public endpoint.
- [ ] SSH доступен только trusted source.
- [ ] Снаружи открыты только 80/443/ограниченный SSH.
- [ ] DNS стабильно разрешается с нескольких resolver.
- [ ] VM достигает private PG и внешних HTTPS endpoints.
**Ожидаемый результат:** `nc -vz <PG_PRIVATE_HOST> <PG_PORT>` с VM успешен; с внешней машины PG недоступен.
## 5. Stage 2 — hardening Ubuntu и deploy user
### 5.1. Первичный вход
Войти cloud user, добавить отдельный deploy key. Не отключать пароль/root до проверки второго SSH-сеанса.
Прототипный скрипт можно адаптировать:
```bash
sudo DEPLOY_USER=deploy \
DEPLOY_DIR=/opt/han-chat \
SSH_PORT=<SSH_PORT> \
SWAP_SIZE_GB=4 \
PUBLIC_DOCKER_PORTS=80,443 \
./deploy/setup-vm-han-chat.sh
```
Скрипт из [`../../HAN_chat/deploy/setup-vm-han-chat.sh`](../../HAN_chat/deploy/setup-vm-han-chat.sh) полезен для UFW, fail2ban, Docker и `DOCKER-USER`, но перед production:
- проверить его версию/review;
- не передавать реальные IP/ключи в git;
- проверить auto reboot unattended upgrades относительно maintenance;
- решить, действительно ли deploy user нужен в группе `docker` (это root-equivalent);
- не применять `AllowTcpForwarding no`, если утверждённый break-glass DB tunnel необходим; предпочтителен VPN/bastion.
### 5.2. Проверки
```bash
sudo sshd -t
sudo ufw status verbose
sudo fail2ban-client status sshd
docker version
docker compose version
sudo iptables -L HAN-CHAT-DOCKER -n -v
timedatectl status
df -h
free -h
```
Открыть второй SSH session как `deploy`, затем отключить root/password login. `.env` позже имеет mode `0600`.
### Gate 2
- [ ] SSH key login `deploy` проверен во втором сеансе.
- [ ] Root/password auth выключены.
- [ ] UFW и DOCKER-USER активны после restart Docker.
- [ ] Docker Engine/Compose plugin закреплены поддерживаемой версией.
- [ ] NTP active; disk/swap соответствуют sizing.
- [ ] Break-glass процедура сохранена вне VM.
**Ожидаемый результат:** reboot VM не теряет SSH, firewall и Docker service.
## 6. Stage 3 — managed PostgreSQL
### 6.1. Защита и восстановление managed PostgreSQL
До создания схем включить и проверить настройки managed PostgreSQL:
- **Daily backup** — автоматический полный снимок БД не реже одного раза в сутки. Нужно задать срок хранения, например 7–14 дней, и убедиться, что резервные копии размещаются отдельно от вычислительного узла БД. Наличие backup без периодической проверки восстановления не считается достаточным.
- **PITR (Point-in-Time Recovery)** — восстановление состояния БД на выбранный момент времени между полными backup за счёт архивации WAL. Это позволяет откатиться, например, к состоянию непосредственно перед ошибочной миграцией или удалением данных. Период доступного восстановления должен соответствовать принятому RPO.
- **Encryption at rest** — шифрование дисков, backup и WAL на стороне провайдера managed PostgreSQL. Ключ управляется провайдером либо KMS организации; пароль пользователя БД не заменяет это шифрование.
- **TLS для соединения с PostgreSQL** — шифрование трафика между контейнерами на VM и managed PostgreSQL с обязательной проверкой имени сервера. CA-сертификат обычно выдаёт провайдер PostgreSQL. Это **не** публичный сертификат сайта и не сертификат Let's Encrypt.
- **Alerts** — уведомления как минимум о нехватке диска, исчерпании подключений, высокой загрузке CPU/IO, replication lag (если есть реплики), неуспешном backup и истечении/замене CA.
- **Deletion protection** — запрет удаления кластера обычной командой/API. Отключение защиты должно быть отдельным подтверждаемым действием администратора. Эта настройка не защищает от `DROP TABLE`, поэтому least privilege и backup всё равно обязательны.
CA managed PostgreSQL скачать из панели или документации провайдера в защищённый путь VM, например `/opt/han-chat/secrets/pg/ca.pem`, с владельцем `root:deploy` и mode `0440` (либо `0400`, если файл читает один пользователь). Все DSN используют `sslmode=verify-full` и `sslrootcert=/run/secrets/pg-ca.pem` либо эквивалент драйвера. Режимы `disable`, `allow`, `prefer` и `require` без проверки CA для production запрещены.
Публичный HTTPS-сертификат `<PUBLIC_HOST>` выпускается отдельно через **Let's Encrypt** на Stage 9. Он устанавливается в корневой nginx, а не в PostgreSQL.
### 6.2. Роли
Целевая модель разделяет:
- admin/bootstrap role;
- migration role каждого schema с DDL;
- runtime role без DDL.
Прототип `init-managed-postgres.py` выдаёт runtime roles `CREATE` на schema и печатает DSN. Это допустимо только для bootstrap/dev, но **слишком широко для production runtime**. Перед production адаптировать:
1. создать пять schemas: `han_app`, `bitrix_local`, `bitrix_sync`, `keycloak`, `message_safety`;
2. создать runtime roles;
3. создать migration roles либо controlled admin job;
4. schema owner = migration role;
5. runtime: `USAGE`, DML и sequence grants только на свои objects;
6. `ALTER DEFAULT PRIVILEGES` от migration owner;
7. запретить чужие schemas и public schema create;
8. `bitrix_sync_user` не получает `han_app` grants, пока module-07 остаётся stub.
Команда bootstrap требует `<PROJECT_PATH>`:
```bash
cd <BACKEND_ROOT>
cp deploy/pg-init.env.example deploy/pg-init.env
chmod 600 deploy/pg-init.env
# заполнить private host/database/admin и generated passwords
set -a; source deploy/pg-init.env; set +a
python3 deploy/init-managed-postgres.py
unset HAN_PG_ADMIN_PASSWORD
```
Не сохранять stdout с DSN в shared logs. Исторический `init-managed-postgres.sql` содержит placeholder passwords и database `postgres`; для целевой БД применять только после review и замены database name.
### 6.3. Проверка least privilege
Для каждого runtime user:
```bash
psql "host=<PG_PRIVATE_HOST> port=<PG_PORT> dbname=<PG_DATABASE> user=<RUNTIME_USER> sslmode=verify-full sslrootcert=<PG_CA_PATH>" \
-c "select current_user, current_setting('search_path');"
```
Негативно проверить `CREATE TABLE` и доступ к чужой schema — они должны завершиться permission denied.
### 6.4. Migration policy
Порядок ownership:
1. `api-backend` Alembic владеет `han_app`, triggers, seed;
2. `bitrix-local-app` Alembic владеет `bitrix_local`;
3. `message-safety` stub не создаёт PG tables до production implementation;
4. `bitrix-sync` stub — optional empty baseline;
5. Keycloak мигрирует standard tables сам; custom provider имеет собственные versioned migrations.
Только expand/migrate/contract. Destructive migration — отдельный backup, approval и release. Downgrade data migrations не обещается; rollback приложения требует backward-compatible schema.
### Gate 3
- [ ] Backups/PITR/TLS/deletion protection включены.
- [ ] Пять schemas/roles созданы.
- [ ] Runtime roles не имеют DDL/чужого доступа.
- [ ] Migration credentials отделены от runtime.
- [ ] Empty/previous-version migration test успешен.
- [ ] PITR restore point создан перед первым release.
**Ожидаемый результат:** runtime `SELECT 1` успешен, unauthorized schema read/create запрещены.
## 7. Stage 4 — S3 buckets, IAM, CORS и lifecycle
Создать три приватных bucket:
- `<PREFIX>-quarantine`;
- `<PREFIX>-attachments`;
- `<PREFIX>-documents`.
Public ACL/listing выключены. Versioning включить для data buckets по policy; server-side encryption включить.
IAM:
- API role/key: exact prefixes, presign PUT quarantine, Head/copy/delete quarantine, write/read data;
- Safety role/key: **read-only quarantine**;
- backup/ops role: отдельно;
- frontend: никаких permanent credentials.
CORS quarantine:
```json
[
{
"AllowedOrigins": ["https://<PUBLIC_HOST>"],
"AllowedMethods": ["PUT"],
"AllowedHeaders": ["Content-Type", "x-amz-*"],
"ExposeHeaders": ["ETag", "x-amz-checksum-sha256"],
"MaxAgeSeconds": 600
}
]
```
Уточнить фактические required signed headers. Не разрешать `*` origin с credentials.
Lifecycle:
- quarantine: expire orphan objects только после периода, превышающего Safety poll + recovery; initial 2 дня, согласовать;
- incomplete multipart upload: abort через 1 день;
- attachments/documents: без auto-delete до legal retention;
- noncurrent versions: policy после legal review.
### Gate 4
- [ ] Все buckets private.
- [ ] API key не может list/write вне exact scope.
- [ ] Safety key не может write/delete.
- [ ] Browser test origin выполняет presigned PUT.
- [ ] Quarantine lifecycle не удалит active `safety_tasks`.
- [ ] Data lifecycle соответствует retention.
**Ожидаемый результат:** anonymous GET/PUT получает deny; API capability test проходит.
## 8. Stage 5 — repository и release layout
На VM:
```text
/opt/han-chat/
backend/ # checkout текущего release
releases/<RELEASE>/ # optional immutable release dirs
secrets/ # не в git
backups/ # только metadata/short-lived encrypted artifacts
```
Рекомендуемый rollout — immutable images из registry. Build на VM допустим для MVP, но требует reproducible Dockerfiles и достаточно диска.
```bash
sudo install -d -m 0755 -o deploy -g deploy /opt/han-chat
git clone <BACKEND_REPO_URL> <BACKEND_ROOT>
cd <BACKEND_ROOT>
git fetch --tags
git checkout --detach <RELEASE>
git status --short
```
Ожидается clean tree. Запретить deploy из mutable branch без recorded SHA.
Проверить структуру: root `docker-compose.yml`, service directories, `nginx`, `keycloak`, `redis`, `observability`, frontend artifact.
### Gate 5
- [ ] Checkout exact SHA/tag.
- [ ] Working tree clean.
- [ ] Images/Dockerfiles pinned, `latest` отсутствует.
- [ ] SBOM/vulnerability scan без unresolved critical/high.
- [ ] Root Compose — единственный production entrypoint.
## 9. Stage 6 — `.env` и secrets
### 9.1. Создание
```bash
cd <BACKEND_ROOT>
umask 077
cp .env.example .env
chmod 600 .env
```
Генерировать минимум 256-bit:
```bash
openssl rand -hex 32
```
Не выполнять `export SECRET=...` в shared shell history. Использовать editor с restricted permissions или secret manager/secret files.
### 9.2. Обязательные группы
- `APP_ENV`, release/version, log level;
- private PG host/port/database, TLS CA и runtime DSN;
- Redis ACL credentials/URLs DB0/1/2;
- public web/API/auth URLs;
- Keycloak realm/audience/hostname/bootstrap/provider technical secrets;
- paired service tokens из arch-02;
- Bitrix client/application/webhook/encryption secrets;
- S3 endpoint/buckets/API and read-only Safety credentials;
- OTEL endpoint/remote exporter secrets;
- nginx/TLS/rate limits;
- frontend public build values.
Пары должны совпасть:
```text
BITRIX_LOCAL_APP_INTERNAL_TOKEN == BITRIX_INTERNAL_API_TOKEN
BITRIX_API_FORWARD_TOKEN == BITRIX_API_INBOX_TOKEN
```
Service token и webhook token — разные secrets.
### 9.3. Validation
Добавить/запустить `scripts/validate-env`:
- mandatory not empty;
- нет `change-me`, example IP/domain, default OTP;
- URLs have correct schemes;
- public URLs HTTPS, internal URLs service DNS;
- PG TLS enabled;
- paired tokens equal;
- CORS/origins exact;
- no duplicate keys;
- `FRONTEND_DEV_PROXY_ENABLED=false`;
- Safety timeout согласован с nginx;
- secrets minimum length;
- mock OTP risk flag explicitly accepted.
```bash
cd <BACKEND_ROOT>
./scripts/validate-env .env
docker compose config --quiet
```
`docker compose config` может раскрыть resolved secrets; не сохранять/публиковать его stdout.
### Gate 6
- [ ] `.env` mode 0600, отсутствует в git.
- [ ] Все placeholder/default secrets отклонены.
- [ ] Paired tokens совпадают.
- [ ] DSN private/TLS; URLs/issuer согласованы.
- [ ] Validation и Compose interpolation успешны.
- [ ] Secret recovery/rotation owner назначен.
## 10. Stage 7 — images и frontend static
### Pull-вариант
```bash
cd <BACKEND_ROOT>
docker login <REGISTRY>
docker compose pull
docker image ls --digests
```
Registry token read-only и короткоживущий.
### Build-вариант
```bash
cd <BACKEND_ROOT>
DOCKER_BUILDKIT=1 docker compose build --pull
```
Build не получает production secrets. Записать image digests.
Frontend:
```bash
cd <FRONTEND_PROJECT_PATH>
npm ci
npm run test
npx expo export --platform web
```
Скопировать artifact в versioned `frontend-static` volume/image. `index.html` revalidate, hashed assets immutable. Build env содержит только public URL/realm/client id. Проверить отсутствие service tokens/mock OTP/S3 keys командой secret scanner.
### Gate 7
- [ ] Все images доступны по digest.
- [ ] Frontend build/tests успешны.
- [ ] Static artifact не содержит secrets/source maps по policy.
- [ ] nginx image/config содержит request-id module и TLS features.
- [ ] Disk после pull/build >30% free.
## 11. Stage 8 — root Compose, networks и volumes
До запуска:
```bash
cd <BACKEND_ROOT>
docker compose config --services
```
Ожидаются: `nginx`, `api-backend`, `message-safety`, `keycloak`, `bitrix-sync`, `bitrix-local-app`, `redis`, `otel-collector` и one-shot jobs/profile components.
Networks:
- `public`: nginx и минимально Keycloak/frontend path;
- `backend`: internal services/Redis;
- `observability`: services + Collector.
Volumes:
- `redis-data`;
- ACME certs/webroot;
- frontend static;
- `otel-queue`;
- никаких PG data volumes.
Проверить:
```bash
docker compose config | rg 'ports:|expose:|networks:|volumes:'
```
Если `rg` на VM нет, использовать reviewed script, не ручной визуальный просмотр. Единственные published mappings — nginx 80/443.
Redis: ACL, AOF everysec, RDB, maxmemory, volume, no host port. OTEL: config read-only, queue bounded, no public OTLP.
### Gate 8
- [ ] Только nginx публикует ports.
- [ ] Internal services не подключены к public без причины.
- [ ] Named volumes созданы и permissions проверены.
- [ ] Container resource limits/healthchecks заданы.
- [ ] `docker compose config --quiet` success.
## 12. Stage 9 — TLS bootstrap, фаза 1
Прототип `ssl-issue.sh` останавливает весь Compose и использует standalone Certbot. Для full stack предпочтителен **webroot two-phase**, чтобы не делать `compose down`.
### Phase A: HTTP bootstrap
1. DNS уже указывает на VM.
2. Запустить nginx с bootstrap config: только `/.well-known/acme-challenge/` и redirect; TLS block не требует отсутствующий cert.
3. Запустить Certbot profile:
Сначала проверить процесс через Let's Encrypt staging CA, добавив `--staging`. Staging-сертификат не является доверенным браузерами и нужен только для проверки DNS, firewall, ACME webroot и конфигурации. После успешной проверки удалить staging lineage либо выпустить production-сертификат с отдельным `--cert-name <PUBLIC_HOST>`.
Production-выпуск:
```bash
cd <BACKEND_ROOT>
docker compose --profile tls-bootstrap up -d nginx
docker compose --profile certbot run --rm certbot certonly \
--webroot -w /var/www/certbot \
-d <PUBLIC_HOST> \
--cert-name <PUBLIC_HOST> \
--email <ACME_EMAIL> \
--agree-tos --no-eff-email --non-interactive
```
Каталоги `/etc/letsencrypt` и `/var/www/certbot` должны быть общими named volumes для контейнеров `certbot` и `nginx`. Сертификат и закрытый ключ не копируются в репозиторий или image. TLS-конфигурация nginx использует:
```text
/etc/letsencrypt/live/<PUBLIC_HOST>/fullchain.pem
/etc/letsencrypt/live/<PUBLIC_HOST>/privkey.pem
```
### Phase B: TLS activation
```bash
cd <BACKEND_ROOT>
docker compose exec -T nginx nginx -t
# активировать rendered TLS config атомарно
docker compose exec -T nginx nginx -s reload
```
HSTS пока не включать. Проверить chain/hostname/redirect, затем включить HSTS без preload.
### Автоматическое обновление Let's Encrypt
Сертификаты Let's Encrypt действуют 90 дней. Проверку обновления выполнять **дважды в сутки**; Certbot сам обновляет сертификат только при приближении срока истечения. Для VM предпочтителен systemd timer, который вызывает репозиторный скрипт root Compose.
Скрипт `<BACKEND_ROOT>/deploy/ssl-renew.sh` должен:
1. взять `flock`, чтобы исключить параллельные запуски;
2. выполнить `docker compose --profile certbot run --rm certbot renew --webroot -w /var/www/certbot --quiet`;
3. при успешном обновлении проверить `docker compose exec -T nginx nginx -t`;
4. только после успешной проверки выполнить `docker compose exec -T nginx nginx -s reload`;
5. записать структурированный результат и метрику времени до истечения;
6. вернуть ненулевой exit code при ошибке, чтобы сработал alert;
7. не удалять действующий сертификат при неуспешном renew.
Пример unit `/etc/systemd/system/han-chat-cert-renew.service`:
```ini
[Unit]
Description=Renew HAN Chat Let's Encrypt certificate
Requires=docker.service
After=docker.service network-online.target
[Service]
Type=oneshot
User=deploy
WorkingDirectory=<BACKEND_ROOT>
ExecStart=<BACKEND_ROOT>/deploy/ssl-renew.sh
```
Пример timer `/etc/systemd/system/han-chat-cert-renew.timer`:
```ini
[Unit]
Description=Check HAN Chat certificate renewal twice daily
[Timer]
OnCalendar=*-*-* 03,15:20:00
RandomizedDelaySec=30m
Persistent=true
Unit=han-chat-cert-renew.service
[Install]
WantedBy=timers.target
```
Установка и обязательная проверка расписания:
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now han-chat-cert-renew.timer
sudo systemctl list-timers han-chat-cert-renew.timer
cd <BACKEND_ROOT>
docker compose --profile certbot run --rm certbot renew --dry-run
sudo systemctl start han-chat-cert-renew.service
sudo systemctl status han-chat-cert-renew.service --no-pager
```
Дополнительно настроить alert при остатке менее 21 дня и критический alert менее 7 дней. Проверять срок можно synthetic probe снаружи и метрикой exporter/скрипта на VM. Ошибка одного запуска renew не должна останавливать nginx.
Прототипные [`ssl-common.sh`](../../HAN_chat/bitrix-local-app/deploy/ssl-common.sh), [`ssl-renew.sh`](../../HAN_chat/bitrix-local-app/deploy/ssl-renew.sh) полезны концептуально, но должны работать с **root Compose**, не service compose.
### Gate 9
- [ ] Staging issuance rehearsal успешен.
- [ ] Production cert chain/hostname valid.
- [ ] HTTP только ACME + 308.
- [ ] TLS 1.0/1.1 rejected; 1.2/1.3 accepted.
- [ ] Renewal dry-run и safe reload успешны.
- [ ] `han-chat-cert-renew.timer` включён, имеет следующий запуск и переживает reboot.
- [ ] Alert expiry настроен.
## 13. Stage 10 — миграции и seed
Остановить public traffic либо использовать maintenance page до gate.
### 13.1. Preflight
```bash
cd <BACKEND_ROOT>
docker compose run --rm api-backend alembic current
docker compose run --rm bitrix-local-app alembic current
```
Создать PITR marker. Выполнить dry-run/SQL review в clone/staging.
### 13.2. Upgrade
```bash
cd <BACKEND_ROOT>
docker compose run --rm api-backend alembic upgrade head
docker compose run --rm bitrix-local-app alembic upgrade head
```
Для message-safety stub PG migration отсутствует. Для bitrix-sync stub — baseline только если реализован. Keycloak стандартную schema мигрирует выбранная pinned версия при controlled startup; provider migration выполняется отдельным approved job.
### 13.3. Seed `app_settings`
Seed обязан быть idempotent/versioned и содержать все ключи arch-04. Выполнить migration или:
```bash
cd <BACKEND_ROOT>
docker compose run --rm api-backend python -m app.cli.seed_settings --file <PROJECT_PATH>/deploy/app-settings.production-like.yaml
docker compose run --rm api-backend python -m app.cli.validate_settings
```
Команды являются целевым интерфейсом; если CLI ещё не реализован, gate не проходить ручными ad-hoc INSERT без reviewed SQL.
Проверить public keys/DTO, consent versions/URLs, CORS host, file MIME/size, UX idle timeout. Не копировать phone/URLs из prototype без product approval.
### Gate 10
- [ ] PITR marker до migrations.
- [ ] Expected Alembic revisions active.
- [ ] Runtime users не выполняли DDL.
- [ ] Seed idempotency проверена повторным запуском.
- [ ] Mandatory settings valid; secrets отсутствуют в `app_settings`.
- [ ] Backward compatibility с текущими images подтверждена.
## 14. Stage 11 — Keycloak bootstrap
### 14.1. Первый старт
Запустить PostgreSQL-ready Keycloak отдельно:
```bash
cd <BACKEND_ROOT>
docker compose up -d keycloak
docker compose ps keycloak
docker compose logs --since=10m keycloak
```
Bootstrap admin secret существует только на первый запуск. После создания named admin с MFA удалить/ротировать bootstrap credential из runtime env.
### 14.2. Realm
Clean environment может импортировать secret-free `han-chat` realm template. Живой production realm нельзя перетирать `--import-realm` без diff.
Проверить:
- client `han-chat-frontend`, public, PKCE S256;
- direct/implicit/password/social disabled;
- audience `han-chat-api`;
- exact redirect/web origins;
- issuer `https://<PUBLIC_HOST>/auth/realms/han-chat`;
- claims `sub`, `phone_number`, verified, audience;
- custom phone OTP provider;
- settings bridge token/path;
- mock code non-default и не виден UI/log;
- brute-force, token/session TTL, refresh rotation;
- admin console limited by VPN/allow-list.
### 14.3. Provider migration
Custom OTP tables мигрируются versioned mechanism до включения flow. Не редактировать standard Keycloak tables вручную.
### Gate 11
- [ ] Discovery/JWKS public через HTTPS.
- [ ] Issuer exact, no internal hostname.
- [ ] Realm drift check clean.
- [ ] Только Authorization Code + PKCE S256.
- [ ] OTP wrong/replay/limit tests fail safely.
- [ ] Settings bridge cache/fail-closed tested.
- [ ] Bootstrap admin removed; named admin MFA enabled.
## 15. Stage 12 — ordered startup и health gates
Архитектурный порядок:
1. Redis;
2. Keycloak;
3. OTEL Collector;
4. Message Safety;
5. API backend;
6. Bitrix local app;
7. Bitrix sync;
8. nginx.
Команды:
```bash
cd <BACKEND_ROOT>
docker compose up -d redis
docker compose up -d keycloak otel-collector
docker compose up -d message-safety
docker compose up -d api-backend
docker compose up -d bitrix-local-app bitrix-sync
docker compose up -d nginx
docker compose ps
```
После каждого шага ждать health, но проверять readiness отдельно из internal network:
```bash
docker compose exec -T api-backend python -c "<INTERNAL_HEALTH_PROBE_COMMAND>"
```
Не использовать host ports для curl. Допустим dedicated toolbox container в `backend` network.
Expected:
- Redis `PONG`;
- Keycloak DB/realm/provider ready;
- Collector health + exporter queue;
- Safety ready и Redis DB2;
- API DB/Redis/JWKS/settings/S3/Safety ready;
- local app до Bitrix install может быть `portal_not_installed`;
- bitrix-sync возвращает `mode=db_connectivity_stub`, не CRM-ready;
- nginx config test success.
### Gate 12
- [ ] Все containers live, нет restart loop/OOM.
- [ ] Critical readiness green.
- [ ] Expected degraded statuses только Bitrix not-installed/sync stub.
- [ ] `docker compose ps` не публикует internal ports.
- [ ] Internal `/internal/*` снаружи 404.
- [ ] OTEL принимает telemetry.
## 16. Stage 13 — Bitrix24 local app и Open Lines
Bitrix admin создаёт local application:
- install URL `https://<PUBLIC_HOST>/bitrix/install`;
- handler URL `https://<PUBLIC_HOST>/bitrix/handler`;
- placement URL `https://<PUBLIC_HOST>/bitrix/placement`;
- required scopes по module-06;
- client id/secret загружены в secret store до install;
- portal/domain allow-list exact.
Выполнить install в Bitrix24. Local app должен сохранить encrypted OAuth, затем:
1. `imconnector.register` connector `han_mobile_app`;
2. `imconnector.activate` line `8`;
3. `event.bind`;
4. status/reconciliation.
Не использовать prototype `/bitrix-internal/internal/v1/*`: canonical path только internal Docker `/internal/openlines/v1/*`, наружу он отсутствует.
Проверить status из toolbox/internal network с Bearer token, не печатая token:
```bash
cd <BACKEND_ROOT>
docker compose run --rm --no-deps <TOOLBOX_SERVICE> \
<SAFE_STATUS_PROBE_COMMAND>
```
Создать test dialog/message через public API, не прямым legacy payload с телефоном. Проверить mapping и ответ оператора.
### Gate 13
- [ ] OAuth stored encrypted; token не в logs.
- [ ] Connector configured/active on line 8.
- [ ] Events bound exactly once.
- [ ] Outbound text reaches Open Lines once.
- [ ] Operator reply reaches API, затем delivery ack.
- [ ] Duplicate callback не создаёт duplicate message.
- [ ] Internal status с internet недоступен.
## 17. Stage 14 — public smoke и E2E
### 17.1. Edge
```bash
curl -I http://<PUBLIC_HOST>/
curl -fsS https://<PUBLIC_HOST>/api/v1/public/app-config
curl -fsS https://<PUBLIC_HOST>/api/v1/public/content
curl -fsS https://<PUBLIC_HOST>/auth/realms/han-chat/.well-known/openid-configuration
curl -i https://<PUBLIC_HOST>/internal/safety/v1/messages/check
openssl s_client -connect <PUBLIC_HOST>:443 -servername <PUBLIC_HOST>
```
Expected: 308; public 200 strict DTO; discovery 200; internal 404; valid cert.
### 17.2. Auth/frontend
- guest открывает content без write;
- protected write без JWT → 401;
- consent → mock OTP → PKCE tokens;
- bootstrap не передаёт phone body;
- session-start создаёт `ux_session_id`;
- silent refresh работает без OTP;
- logout очищает tokens;
- wrong/replayed OTP не выдаёт tokens.
### 17.3. Message Safety правила stub
Обязательные E2E:
- text, начинающийся после normalization с `ф/Ф` → public `422 message_blocked`, Bitrix не вызван;
- text с цифры → Safety `203`, API poll до `200` или test terminal `400`; клиент никогда не получает `203`;
- прочий text → allow;
- terminal stub `400` преобразуется в `422`, не в generic validation;
- timeout → `503/504`, checkpoint/recovery, без duplicate;
- один slow poll не блокирует другие requests.
Статус `message-safety` должен быть явно `stub`; для real production он не заменяет antivirus/file scan.
### 17.4. Files
- allow image/PDF ≤ configured size;
- wrong extension+MIME/oversize/checksum reject;
- direct presigned PUT quarantine;
- allow promote attachments;
- deny остаётся вне data и quarantine cleanup;
- Safety read-only credential не может write;
- download URL owner-only + audit;
- presigned URL отсутствует в logs.
### 17.5. Realtime и ownership
- WS connects/subscribes;
- operator reply arrives;
- reconnect + REST gap reconciliation;
- polling fallback;
- чужие dialog/message/attachment/document id → 404;
- idempotency same body replay, changed body 409;
- rate limits 429 + `Retry-After`.
### Gate 14
- [ ] Полный first-send flow успешен.
- [ ] Safety allow/deny/pending/timeout проверены.
- [ ] Text/file/WS/polling работают.
- [ ] Ownership и no-public-internal tests зелёные.
- [ ] Нет secret/PII/message body/presigned URL в logs.
- [ ] Audit events созданы.
- [ ] Bitrix получает только allowed message.
## 18. Stage 15 — observability validation
Следовать [`module-09-observability.md`](module-09-observability.md):
1. послать request с известным `X-Request-ID`;
2. найти nginx log, API trace и downstream spans;
3. проверить `service.name`, environment, trace/request/UX ids;
4. при выбранном telemetry backend проверить dashboards всех services; до выбора — проверить bounded stdout/debug acceptance и явно зафиксировать ограничение;
5. trigger safe synthetic 4xx/5xx и проверить alert route, если backend с alerting уже выбран;
6. при настроенном remote OTLP временно блокировать его, проверить bounded queue и business continuity;
7. проверить Collector health/drop/refused;
8. выполнить PII/secret canary test.
### Gate 15
- [ ] Три сигнала доступны.
- [ ] Trace cross-service связан.
- [ ] Для выбранного backend alerts доставляются on-call и SLO queries возвращают данные; иначе limitation/TBD явно принят и traffic не называется production-ready.
- [ ] Collector outage не ломает business path.
- [ ] Redaction test пройден.
## 19. Stage 16 — opening traffic
До открытия:
- удалить maintenance response;
- включить HSTS после финального TLS test;
- сохранить release SHA/image digests/schema revisions/realm desired version;
- создать restore point;
- подтвердить on-call;
- не удалять previous images;
- observation window 60 минут.
В первые 60 минут: 5xx, auth, message delivery, DB/Redis, memory, restart, Collector queue, Bitrix OAuth/DLQ.
### Gate 16
- [ ] Все Gate 0–15 подписаны.
- [ ] Rollback release доступен.
- [ ] Backup/restore evidence свежий.
- [ ] Нет active page alert.
- [ ] Product owner принял stub limitations.
## 20. Backup и restore
### PostgreSQL
- provider daily + PITR;
- перед migrations/Keycloak upgrade — manual restore point;
- ежеквартальный restore clone;
- проверить все schemas, Alembic/Keycloak revisions, keys, grants.
### S3
- versioning/lifecycle data buckets;
- inventory/checksum при поддержке;
- restore не делает objects public;
- quarantine не является backup.
### Redis
AOF/RDB ускоряют restart, но не business backup. При corruption поднять clean Redis; API восстанавливает durable state из PG. Никогда не считать Redis dump достаточным для messages/audit.
### Keycloak
DB backup включает realm/users/signing keys/provider data. Secret-free realm export — config backup, не полный data backup. После restore проверить issuer/JWKS/PKCE/OTP/refresh.
### Restore rehearsal
1. isolated VPC/hostnames;
2. restore PG clone и S3 copies;
3. deploy same image digests;
4. не направлять production DNS/Bitrix callbacks;
5. run migrations only if required release;
6. smoke auth/chat without real operator impact;
7. record measured RPO/RTO;
8. destroy isolated secrets/resources controlled.
## 21. Rollback
### Application-only
1. объявить incident/maintenance;
2. сохранить diagnostics и current state;
3. остановить новые claims/send при возможности;
4. переключить image tags на previous digests;
5. не выполнять Alembic downgrade;
6. `docker compose up -d`;
7. health/smoke/idempotency;
8. проверить outbox/inbox/recovery.
### После backward-incompatible migration
Обычный rollback запрещён. Выбор:
- forward-fix;
- restore PG PITR + coordinated S3/Bitrix reconciliation;
- maintenance до решения.
Нельзя rollback DB отдельно от Keycloak signing/session state без анализа.
### TLS/nginx rollback
`nginx -t`; оставить старый config/workers при failure. Не удалять working cert. При ошибке renewal current cert остаётся, но alert.
### Gate rollback
- [ ] Previous images available.
- [ ] Schema совместима.
- [ ] No duplicate outbound after restart.
- [ ] Data reconciliation completed.
- [ ] Incident timeline содержит release/request ids без PII.
## 22. Routine operations
Ежедневно автоматически:
- health/synthetics/SLO/alerts;
- PG backup/PITR status;
- TLS expiry/renew;
- disk/inodes/OTEL queue;
- Redis AOF/memory/evictions;
- DLQ/backlog/quarantine age;
- Keycloak signing/OAuth/settings cache.
Еженедельно:
- vulnerability/image updates review;
- failed login/rate-limit trend;
- Bitrix connector desired/observed;
- S3 lifecycle/inventory;
- restore/rollback artifacts availability.
Ежемесячно:
- patch OS/images in maintenance;
- secret/access review;
- capacity/cardinality/cost;
- stale users/admins;
- runbook sample drill.
Команды:
```bash
cd <BACKEND_ROOT>
docker compose ps
docker compose logs --since=15m <SERVICE>
docker stats --no-stream
docker system df
docker compose exec -T nginx nginx -t
```
Не использовать unbounded `logs`, `docker system prune -a`, Redis `KEYS/FLUSH*` или ad-hoc DB DELETE.
## 23. Incident commands
Безопасный triage:
```bash
cd <BACKEND_ROOT>
date -Is
docker compose ps
docker stats --no-stream
docker compose logs --since=10m --tail=500 <SERVICE>
df -h
free -h
sudo ss -lntp
sudo iptables -L HAN-CHAT-DOCKER -n -v
```
PG:
```bash
psql "<OPS_READONLY_DSN>" -c "select now(), count(*) from pg_stat_activity;"
```
Redis — только ops ACL:
```bash
docker compose exec -T redis redis-cli --user <OPS_USER> --pass '<FROM_SECURE_INPUT>' PING
```
Никогда не вставлять secret literal в ticket/chat. Предпочесть stdin/secret file. Не выполнять ручной replay message/DLQ до проверки idempotency и ambiguous Bitrix outcome.
Типовые сценарии:
- API 503: DB/Redis/JWKS/settings/Safety readiness и circuits;
- send timeout: safety checkpoint/outbox, не повторять с новым key;
- Bitrix down: OAuth/circuit/backlog/DLQ, reads оставить;
- Redis loss: clean restart, durable fallback, polling;
- PG outage: не restart storm; provider incident;
- disk full: остановить ingest growth, очистить только known cache/old image после inventory;
- cert near expiry: webroot/DNS/rate limit, staging rehearsal;
- secret leak: revoke/rotate, telemetry deletion, redeploy.
## 24. Upgrades
Общий порядок:
1. release notes/security advisories;
2. compatibility matrix;
3. backup/PITR;
4. staging clone;
5. image/build/test/scan;
6. expand migration;
7. one service at a time по dependency order;
8. health/E2E/observation;
9. contract migration later;
10. record digests/revisions.
Keycloak: не пропускать unsupported majors; проверить SPI/provider migration и JWKS. Redis: AOF compatibility/rewrite. Collector: config validate against exact version. nginx: `nginx -t` и TLS scan. PostgreSQL major upgrade сначала rehearsal clone.
## 25. Disaster recovery
### Потеря VM
1. provision new Ubuntu VM в VPC;
2. применить reviewed hardening;
3. attach public IP/update DNS с low TTL;
4. restore secrets из vault, не со старого disk без проверки;
5. pull exact images;
6. mount/create volumes; Redis можно clean;
7. connect existing/restored PG/S3;
8. TLS issue/restore safely;
9. ordered startup/gates;
10. Bitrix callback/connectors verify;
11. public smoke, then traffic.
### Потеря PG
Restore PITR в new managed instance, private SG/TLS, update DSN, validate schemas/grants/revisions. Остановить writes до chosen restore point/reconciliation. S3 objects после restore point могут стать orphan; выполнить audit-backed reconcile.
### Потеря S3
Без data backup/versioning полное восстановление невозможно. Временно отключить file operations, оставить text flow, restore objects/inventory, reconcile DB metadata, не генерировать URLs отсутствующих objects.
### Compromise
Isolate VM, preserve forensic snapshot, rotate all service/DB/S3/Bitrix/Keycloak secrets, revoke sessions/signing keys по масштабу, deploy clean VM/images, restore trusted data, notify по incident/legal process.
## 26. Teardown cautions
`docker compose down` не удаляет managed PG/S3, но может остановить callbacks. `down -v` удалит Redis/ACME/OTEL queue volumes и запрещён без approval. Запрещены:
```text
docker compose down -v
docker system prune -a --volumes
DROP DATABASE / DROP SCHEMA
S3 recursive delete
cloud project/VPC delete
certbot delete active cert
```
Перед teardown:
- export inventory/digests/config without secrets;
- revoke Bitrix app/callbacks;
- revoke/rotate credentials;
- backup/retention/legal hold;
- DNS drain;
- deletion protection removal — отдельное approval;
- проверить shared VPC/PG/S3;
- зафиксировать evidence уничтожения.
## 27. Definition of Done
- VM/VPC/DNS/SG/hardening соответствуют Gate 1–2;
- managed PG private/TLS/backups/least privilege/migrations работают;
- S3 private/IAM/CORS/lifecycle проверены;
- exact release/images/frontend deployed;
- `.env` validated, secrets protected;
- один root Compose, один nginx, только 80/443;
- Redis/Collector volumes/resources/security работают;
- Keycloak realm/provider/PKCE/OTP готов;
- ordered startup/readiness пройден;
- Bitrix connector line 8 и callback flow проверены;
- full auth/text/file/realtime/safety E2E зелёный;
- observability/redaction проверены; SLO/alerts проверены для выбранного backend либо явно остаются принятым production-blocking TBD;
- backup restore и rollback rehearsed;
- ops/incident/upgrade/DR owners назначены;
- все assumptions/TBD приняты до открытия traffic.
## 28. Допущения, TBD и архитектурные конфликты
### Допущения
- D-A1: одна VM и один public host на MVP.
- D-A2: обязательный минимум — `otel-collector`; доступность remote backend не предполагается до закрытия D-TBD11, local Grafana stack не обязателен.
- D-A3: managed provider даёт private network, TLS, backups/PITR.
- D-A4: Bitrix portal/connector/line остаются разрешёнными значениями architecture.
- D-A5: mock OTP временно разрешён как documented risk.
### TBD до production
- D-TBD1: реальные domains, Expo native redirect URI и Bitrix placement frame ancestors.
- D-TBD2: final VM/PG sizing, RPS/WS, SLO/RPO/RTO.
- D-TBD3: legal retention/erasure для PG/S3/audit/telemetry.
- D-TBD4: secret manager и rotation windows.
- D-TBD5: production Safety вместо stub и antivirus inbound operator files.
- D-TBD6: полноценный bitrix-sync/GRANT или явное исключение CRM sync из release.
- D-TBD7: pinned Keycloak/nginx/Collector versions и SPI compatibility.
- D-TBD8: exact migration/seed/toolbox CLI commands после реализации repo.
- D-TBD9: Keycloak admin VPN/MFA topology.
- D-TBD10: final CSP/CORS/S3 headers и cloud-specific IAM.
- D-TBD11: выбрать observability backend/provider, endpoint/auth/retention и alert route либо явно ограничить среду acceptance-режимом без production-ready SLO.
### Обнаруженные конфликты
1. `arch-01/02/03` описывают полноценный `bitrix-sync`, но module-07 реализует только `SELECT 1`. Первый release не поддерживает обещанную CRM profile sync.
2. `arch-01/02` ожидают production-like Safety и S3 scan, но module-05 — Redis-only random stub, file-only default allow и test-only terminal `400`. Это блокер настоящего production, даже если допустимо для production-like acceptance.
3. Прототипный PG init даёт runtime role `CREATE` schema и не разделяет migration/runtime roles; runbook требует ужесточения.
4. Прототипные TLS scripts используют отдельный service Compose/standalone downtime, тогда как целевая архитектура требует root Compose и two-phase webroot.
5. Prototype публиковал `/bitrix-internal/*` и использовал `/internal/v1/*`; целевой контур это запрещает и использует `/internal/openlines/v1/*`.
6. `arch-04` не содержит ряд proposed env из module-0409; production `.env.example` должен быть синхронизирован до реализации.
7. Точные RPO/RTO, retention, SLO, Keycloak version/TTL и Bitrix retry semantics не утверждены; начальные значения runbook не закрывают product/security decision.
8. `init-managed-postgres.py` по умолчанию не задаёт TLS parameters при bootstrap connection и печатает credential-bearing DSN; его production-hardening обязателен.
## 29. Ссылки на прототип
- Исторические шаги: [`../../HAN_chat/Deploy_steps.md`](../../HAN_chat/Deploy_steps.md).
- Исторические команды эксплуатации: [`../../HAN_chat/backend-managing.md`](../../HAN_chat/backend-managing.md).
- VM baseline: [`../../HAN_chat/deploy/setup-vm-han-chat.sh`](../../HAN_chat/deploy/setup-vm-han-chat.sh).
- PG bootstrap: [`../../HAN_chat/deploy/init-managed-postgres.py`](../../HAN_chat/deploy/init-managed-postgres.py), [`../../HAN_chat/deploy/init-managed-postgres.sql`](../../HAN_chat/deploy/init-managed-postgres.sql), [`../../HAN_chat/deploy/pg-init.env.example`](../../HAN_chat/deploy/pg-init.env.example).
- Legacy nginx: [`../../HAN_chat/bitrix-local-app/deploy/nginx-tohin.ru.site.conf`](../../HAN_chat/bitrix-local-app/deploy/nginx-tohin.ru.site.conf), [`../../HAN_chat/bitrix-local-app/deploy/nginx/nginx.conf`](../../HAN_chat/bitrix-local-app/deploy/nginx/nginx.conf).
- Legacy TLS: [`../../HAN_chat/bitrix-local-app/deploy/ssl-issue.sh`](../../HAN_chat/bitrix-local-app/deploy/ssl-issue.sh), [`../../HAN_chat/bitrix-local-app/deploy/ssl-renew.sh`](../../HAN_chat/bitrix-local-app/deploy/ssl-renew.sh), [`../../HAN_chat/bitrix-local-app/deploy/ssl-install-cron.sh`](../../HAN_chat/bitrix-local-app/deploy/ssl-install-cron.sh).