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

56 KiB
Raw Blame History

module-10. Runbook развёртывания HAN Chat

Статус: последовательная инструкция первого production-like деплоя и эксплуатации на одной Ubuntu VM.
Все значения в <УГЛОВЫХ_СКОБКАХ> — placeholders. Команды с cd <BACKEND_ROOT> требуют подстановки реального пути корня backend-репозитория на VM.
Источники: README.md, arch-00-glossary.md, arch-01-system-architecture.md, arch-02-api-contracts.md, arch-03-docker-compose-blueprint.md, arch-04-settings-and-content.md, arch-05-agent-development-process.md, module-01-api-backend.mdmodule-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:

<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>        разрешённый портал
<IDGTL_SENDER_NAME>    согласованное в Direct имя отправителя
<IDGTL_STATIC_EGRESS_IP> фактический статический egress IP `sms-worker`
<IDGTL_TEST_PHONE>     контролируемый номер для provider smoke

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 до SMS cutover и Safety stub письменно принят; real SMS не включается без gates module-11.

Ожидаемый результат: есть 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, i-Digital Direct
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.

Проверка с рабочей станции:

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-сеанса.

Прототипный скрипт можно адаптировать:

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 полезен для 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. Проверки

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, sms;
  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>:

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:

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;
  6. sms-service владеет versioned migrations/seed schema sms; runtime sms_user не имеет доступа к han_app/keycloak.

Только expand/migrate/contract. Destructive migration — отдельный backup, approval и release. Downgrade data migrations не обещается; rollback приложения требует backward-compatible schema.

Gate 3

  • Backups/PITR/TLS/deletion protection включены.
  • Шесть schemas/roles созданы, включая sms/sms_user.
  • 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:

[
  {
    "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:

/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 и достаточно диска.

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. Создание

cd <BACKEND_ROOT>
umask 077
cp .env.example .env
chmod 600 .env

Генерировать минимум 256-bit:

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;
  • SMS DB URL, парные Keycloak↔SMS tokens, Direct TOKEN_1, callback URL и отдельные callback credentials;
  • 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.

Пары должны совпасть:

BITRIX_LOCAL_APP_INTERNAL_TOKEN == BITRIX_INTERNAL_API_TOKEN
BITRIX_API_FORWARD_TOKEN == BITRIX_API_INBOX_TOKEN
KEYCLOAK_SMS_SERVICE_TOKEN == SMS_SERVICE_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.
  • placeholders change-me/<...> запрещены; real mode требует sender/template/API key/callback credentials и recorded static egress IP;
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-вариант

cd <BACKEND_ROOT>
docker login <REGISTRY>
docker compose pull
docker image ls --digests

Registry token read-only и короткоживущий.

Build-вариант

cd <BACKEND_ROOT>
DOCKER_BUILDKIT=1 docker compose build --pull

Build не получает production secrets. Записать image digests.

Frontend:

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

До запуска:

cd <BACKEND_ROOT>
docker compose config --services

В целевом real-SMS release ожидаются: nginx, api-backend, message-safety, keycloak, sms-service, sms-worker (либо документированный worker process), bitrix-sync, bitrix-local-app, redis, otel-collector и one-shot jobs/profile components.

Networks:

  • public: nginx и минимально Keycloak/frontend path;
  • backend: internal services/Redis;
  • egress: только утверждённые outbound workers; Keycloak в неё не входит, sms-worker входит;
  • observability: services + Collector.

Volumes:

  • redis-data;
  • ACME certs/webroot;
  • frontend static;
  • otel-queue;
  • никаких PG data volumes.

Проверить:

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-выпуск:

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 использует:

/etc/letsencrypt/live/<PUBLIC_HOST>/fullchain.pem
/etc/letsencrypt/live/<PUBLIC_HOST>/privkey.pem

Phase B: TLS activation

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:

[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:

[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

Установка и обязательная проверка расписания:

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, 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

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

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 или:

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 подтверждена.

13.4. Controlled rollout real SMS

До переключения Keycloak:

  1. применить App DB seed otp.phone.code_length, otp.phone.ttl_seconds, otp.phone.sms_order_timeout_ms;
  2. создать schema/role sms, применить migrations и idempotent seed sms_setting/active approved auth_otp;
  3. в test environment развернуть sms-service/worker с локальным mock Direct и выполнить contract/E2E;
  4. получить production Direct TOKEN_1, согласованные sender и template, отдельные callback credentials;
  5. определить egress IP фактическим запросом из sms-worker, подтвердить его статичность/NAT, записать в inventory и передать Direct для allowlist;
  6. развернуть production sms-service/worker и callback route, оставив KEYCLOAK_OTP_MOCK_ENABLED=true;
  7. применить Keycloak expand migration/SPI, мигрировать старые challenges по module-11;
  8. выполнить provider smoke отдельной ops-командой на <IDGTL_TEST_PHONE>; проверить journal, callback, redaction и отсутствие duplicate;
  9. только после подписанных evidence переключить KEYCLOAK_OTP_MOCK_ENABLED=false;
  10. проверить durable order до Direct response, resend/superseded, expiry snapshot, limits и verify при provider reject/timeout.

Production cutover запрещён при любом placeholder, несогласованном sender/template, отсутствующем API key/callback credentials, неподтверждённом callback IP или нестатическом egress IP. Direct API key — готовый TOKEN_1 для Basic, повторно Base64 не кодируется.

Rollback SMS: немедленно вернуть Keycloak в mock mode; не удалять schema/journal и не откатывать migrations без доказанной backward compatibility. Остановить новые real orders, дать worker завершить либо зафиксировать in-flight/uncertain; предпочтителен forward-fix.

14. Stage 11 — Keycloak bootstrap

14.1. Первый старт

Запустить PostgreSQL-ready Keycloak отдельно:

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. OTEL Collector;
  3. API backend/settings;
  4. SMS service/worker после migrations (при SMS release; Keycloak пока mock);
  5. Keycloak;
  6. Message Safety;
  7. Bitrix local app;
  8. Bitrix sync;
  9. nginx.

Команды:

cd <BACKEND_ROOT>
docker compose up -d redis
docker compose up -d otel-collector
docker compose up -d api-backend
docker compose up -d sms-service sms-worker
docker compose up -d keycloak
docker compose up -d message-safety
docker compose up -d bitrix-local-app bitrix-sync
docker compose up -d nginx
docker compose ps

После каждого шага ждать health, но проверять readiness отдельно из internal network:

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:

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

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.
  • real mode: durable order возвращает sms_message_id до Direct response; callback обновляет только SMS journal; resend делает старый challenge superseded.

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:

  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. при SMS incident вернуть KEYCLOAK_OTP_MOCK_ENABLED=true, прекратить новые real orders и сохранить journal/in-flight state;
  3. сохранить diagnostics и current state;
  4. остановить новые claims/send при возможности;
  5. переключить image tags на previous digests;
  6. не выполнять Alembic downgrade;
  7. docker compose up -d;
  8. health/smoke/idempotency;
  9. проверить outbox/inbox/SMS pending/uncertain/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.

Команды:

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:

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:

psql "<OPS_READONLY_DSN>" -c "select now(), count(*) from pg_stat_activity;"

Redis — только ops ACL:

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. Запрещены:

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 временно разрешён до controlled SMS cutover как 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, SLO, Keycloak version и Bitrix retry semantics не утверждены; OTP TTL задаётся app_settings, SMS journal по module-11 хранится бессрочно.
  8. init-managed-postgres.py по умолчанию не задаёт TLS parameters при bootstrap connection и печатает credential-bearing DSN; его production-hardening обязателен.
  9. Текущие Compose/env/config artifacts могут ещё не содержать sms-service; документация не разрешает real mode до реализации и прохождения rollout gates.

29. Ссылки на прототип