Files
han-app/architectory/arch-10-deployment.md

337 lines
27 KiB
Markdown
Raw Permalink 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.
# arch-10. Контракт развёртывания
> Канонический контракт rollout для всех application VM. OS-роли, SSH/sudo, secrets delivery и hardening — [`arch-06-service-hosting-security.md`](arch-06-service-hosting-security.md); этот документ их не ослабляет.
> Процедуры конкретной машины — [`module-10-deployment-vm1.md`](../VM1_app/documentation/module-10-deployment-vm1.md) и [`module-10-deployment-vm2.md`](../VM2_services/documentation/module-10-deployment-vm2.md).
> Nginx TLS — [`arch-08-nginx.md`](arch-08-nginx.md). Redis — [`arch-09-redis.md`](arch-09-redis.md). Telemetry — [`arch-07-observability.md`](arch-07-observability.md). Compose — [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md).
## Назначение
Документ фиксирует то, что **должно совпасть между ВМ1 и ВМ2** и то, что связывает их как систему: независимый deploy, VPC/SG, managed PG, S3, роли `deploy`/`admin`, секреты, TLS/ACME процедура, порядок cutover Safety, backup/rollback/DR.
Команды сервисов конкретной машины и её Compose **не** дублируются здесь. Агент репозитория VM читает этот контракт плюс свой runbook.
Прямые `docker compose` команды выполняются `admin` только при bootstrap/recovery либо инкапсулируются в утверждённые root-owned systemd-units. Они не являются основанием выдавать `deploy` доступ к Docker daemon. Значения в `<УГЛОВЫХ_СКОБКАХ>` — placeholders.
## 1. Неподвижные правила
1. Один root Compose project описывается в `<BACKEND_ROOT>` своей VM; в steady state его запускает root-owned systemd-unit/helper, а не пользователь из группы `docker`.
2. На каждой VM ровно один nginx; ВМ1 и ВМ2 имеют независимые public DNS/TLS ingress и deployment lifecycle.
3. Application containers не имеют public host ports. Nginx ВМ1 публикует свой `80/443`; nginx ВМ2 — отдельный `80/443` только для exact CRM webhook и private `8443` для Message Safety/internal access.
4. `/internal/*` не маршрутизируется публично.
5. Managed PostgreSQL находится вне Compose, в той же VPC, без public IP.
6. S3 — внешний Selectel-compatible storage; клиент получает только presigned URL.
7. Секреты не коммитятся, не вставляются в команды shell history и не выводятся в отчёты.
8. Миграции выполняются отдельными one-shot steps до новой версии приложения.
9. Production Message Safety работает только на ВМ2; local stub/fallback на ВМ1 запрещён. Caller ВМ1 использует private HTTPS, service token и проверку internal CA, при недоступности — fail closed.
10. `bitrix-sync` работает только на ВМ2 и вводится после preflight/cutover gates module-07; до подтверждённого cutover public webhook остаётся закрыт на edge.
11. На ВМ1 и ВМ2 отдельные root Compose projects/systemd units; deploy/rollback выполняются независимо.
12. ВМ2 — самостоятельная service VM с минимальным public webhook ingress, allow-listed egress, отдельным IAM principal и service-specific secret files.
13. OS-роли, SSH/sudo, secrets delivery, container hardening и private-VM lockdown подчиняются arch-06.
## 2. Роли и обозначения
- **Cloud admin**: VPC, VM, PG, S3, DNS/security groups.
- **Deploy operator / OS user `deploy`**: запуск утверждённых release/rollback/migration systemd-units; без группы `docker`, записи в production compose/unit/scripts/config и общего sudo.
- **Break-glass OS user `admin`**: bootstrap и аварийное восстановление; не используется для штатного деплоя.
- **OS user `tunnel`**: только allow-listed local TCP forwarding к private endpoints; без sudo/shell operations.
- **Bitrix admin**: local app, connector, Open Line 8, callbacks.
- **Security owner**: secrets, Keycloak admin MFA, firewall, retention.
- **Safety Service Owner**: API/data contract, capacity result и v2 cutover/rollback sign-off.
- **Rule Pack Owner**: rules bundle, corpus, monitor report и version release.
- **Product Owner**: mnemonic `safety.chat.blocked` и business acceptance chat flow.
- **Operations Owner**: VM2 alerts, host KESL/broker и database updates, incident/reprovision/restore rehearsal.
```text
<PUBLIC_HOST> например chat.example.ru
<VM_PUBLIC_IP> публичный IPv4 ВМ1
<PROCESSING_PUBLIC_HOST> отдельный public host ВМ2, например processing.example.ru
<VM2_PUBLIC_IP> публичный IPv4/LB address ВМ2
<VM_PRIVATE_IP> приватный IPv4 VM
<VPC_CIDR> например 10.20.0.0/24
<PG_PRIVATE_HOST> private FQDN/IP managed PG
<PG_PORT> 5433 для Selectel PgBouncer; иной порт — только фактическое значение другого provider/endpoint
<PG_DATABASE> han_chat
<BACKEND_REPO_URL> URL репозитория этой VM
<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
```
`<BACKEND_ROOT>` и `<BACKEND_REPO_URL>` **разные** у ВМ1 и ВМ2.
## 3. Stage 0 — решения до provisioning
Зафиксировать: region/AZ и VPC; hostnames и TTL DNS; VM image Ubuntu 24.04 LTS; sizing каждой VM; 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; Safety Service/Rule Pack/Product/Security/Operations owners и approvals cutover.
Sizing и load gates конкретной машины — профильный runbook. Monthly availability SLO для MVP не задаётся; это не отменяет latency/load gates и alerts.
### 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, DNS и security groups
Создать private subnet для ВМ1, ВМ2, SigNoz и managed PG. ВМ1 и ВМ2 имеют отдельные public IP/LB только для своих nginx; service-to-service и PostgreSQL traffic остаётся private. SSH к обеим VM — только ops VPN/bastion.
| Source | Destination | Port | Rule |
|---|---|---:|---|
| trusted ops CIDR/VPN | ВМ1, ВМ2 | SSH `<SSH_PORT>` | allow |
| internet | ВМ1 | TCP 80/443 | allow edge redirect/ACME/application |
| internet | nginx ВМ2 | TCP 80/443 | allow ACME/redirect + exact CRM webhook |
| ВМ1 SG | ВМ2 | TCP 8443 | private TLS only |
| ВМ1/ВМ2 service SG | managed PG | `<PG_PORT>` | allow по нужным DB roles |
| ВМ2 collector | private SigNoz | TCP 4317 | allow |
| ВМ2 workers | S3 endpoints | TCP 443 | allow |
| ВМ2 `bitrix-sync` | approved Bitrix portal | TCP 443 | allow |
| host KESL ВМ2 | approved update sources по operator KESL runbook | vendor-required destinations/ports | allow |
| ВМ2 | trusted DNS/NTP | UDP/TCP 53, UDP 123 | allow |
| internet | managed PG | any | deny |
| internet | ВМ2 | any кроме nginx 80/443 | deny ingress |
| internet | обе VM | 6379, 4317, 4318, 8000, 8080, 8443, 9000 | deny public |
ВМ2 использует default-deny egress. Registry/OS repositories открываются только в bootstrap/controlled window и затем снова закрываются. Если provider SG не умеет destination allow-list, применяется host firewall/proxy/NAT policy; постоянный open egress для ВМ2 не является допустимым production состоянием.
DNS: `A <PUBLIC_HOST> → <VM1_PUBLIC_IP>`, `A <PROCESSING_PUBLIC_HOST> → <VM2_PUBLIC_IP>`, private DNS `processing.internal → <VM2_PRIVATE_IP>`. Public host ВМ2 используется только CRM webhook; private name не публикуется во внешнем DNS.
### 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
Процедура первичная для **каждой** application VM и выполняется только по её профильному production runbook. Перед production: review версии; не передавать IP/ключи в git; проверить unattended upgrades; `deploy` не в группе `docker`; `AllowTcpForwarding no` по умолчанию; отдельный `tunnel` при необходимости; root-owned systemd-units и `/etc/sudoers.d/deploy` без wildcard.
`PUBLIC_DOCKER_PORTS` задаёт профильный runbook (ВМ1: `80,443`; ВМ2 — свои public 80/443, private 8443 не internet).
Проверки: `sshd -t`, UFW, fail2ban, Docker/Compose versions, `iptables -L HAN-CHAT-DOCKER`, timedatectl, disk/swap. Второй SSH session как `deploy`, затем отключить root/password login.
### Gate 2
- [ ] SSH key login `deploy` проверен во втором сеансе.
- [ ] Root/password auth выключены.
- [ ] `deploy` не состоит в группе `docker`; `sudo -l` содержит только утверждённые конкретные systemd-команды.
- [ ] Production compose, units, deploy scripts и secret mappings принадлежат root и недоступны `deploy` на запись.
- [ ] UFW и DOCKER-USER активны после restart Docker.
- [ ] Для published Docker ports allow rules сопоставляют original host destination через `conntrack --ctorigdstport`; positive/negative probes увеличивают counters нужных allow/deny rules после restart Docker и reboot.
- [ ] После обновления firewall helper active `oneshot RemainAfterExit` unit явно перезапущен; `enable --now` не считается применением новой версии.
- [ ] Docker Engine/Compose plugin закреплены поддерживаемой версией.
- [ ] NTP active; disk/swap соответствуют sizing.
- [ ] Break-glass процедура сохранена вне VM.
**Ожидаемый результат:** reboot VM не теряет SSH, firewall и Docker service.
### 5.1. Lockdown private/no-egress VM
Для SigNoz и другой VM, которая после раскатки не должна иметь internet ingress/egress, bootstrap выполняется по lifecycle arch-06. Checklist закрытия public IP/SSH/egress и запрет постоянного open egress — arch-06; повторное открытие только break-glass с повтором lockdown.
## 6. Stage 3 — managed PostgreSQL
До создания схем включить daily backup, PITR, encryption at rest, TLS `sslmode=verify-full` + provider CA, alerts (disk/connections/CPU/IO/replication/backup/CA), deletion protection.
CA: `/opt/han-chat/secrets/pg/ca.pem`, `root:deploy` `0440` (либо `0400`). Режимы `disable`/`allow`/`prefer`/`require` без проверки CA для production запрещены.
Публичные HTTPS-сертификаты `<PUBLIC_HOST>` и `<PROCESSING_PUBLIC_HOST>` выпускаются отдельно через Let's Encrypt на Stage 9 nginx соответствующей VM, не в PostgreSQL.
Целевая модель ролей: admin/bootstrap; migration role каждого schema с DDL; runtime без DDL. Прототип `init-managed-postgres.py` слишком широк для production runtime.
Шесть schemas: `han_app`, `bitrix_local`, `bitrix_sync`, `keycloak`, `message_safety`, `sms`. Schema owner = migration role; runtime: `USAGE`, DML и sequence grants только на свои objects; `bitrix_sync_user` — только grants module-07 §13.
Ownership миграций:
1. `api-backend` Alembic — `han_app`;
2. `bitrix-local-app``bitrix_local`;
3. `message-safety` stub не создаёт PG tables до production implementation;
4. `bitrix-sync` — schema `bitrix_sync`; api-backend отдельно мигрирует shared `han_app.sync_queue`;
5. Keycloak — standard tables; custom provider — свои migrations;
6. `sms-service` — schema `sms`; runtime `sms_user` без доступа к `han_app`/`keycloak`.
Только expand/migrate/contract. Destructive migration — отдельный backup, approval и release. Downgrade data migrations не обещается.
### 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.
## 7. Stage 4 — S3
Три приватных bucket: `<PREFIX>-quarantine`, `<PREFIX>-attachments`, `<PREFIX>-documents`. Public ACL/listing выключены. Versioning для data buckets по policy; SSE включить.
IAM: API role — exact prefixes, presign PUT quarantine, Head/copy/delete quarantine, write/read data; Safety role — **read-only quarantine**; backup/ops отдельно; frontend — никаких permanent credentials. Отдельный IAM principal ВМ2.
CORS quarantine: `AllowedOrigins` только `https://<PUBLIC_HOST>`; PUT; headers `Content-Type`, `If-None-Match`, checksum; не `*` origin с credentials.
Lifecycle: quarantine failed/orphan expire 48 ч только при отсутствии active `safety_tasks`; incomplete multipart abort 1 день; attachments/documents без auto-delete до legal retention.
### Gate 4
- [ ] Все buckets private.
- [ ] API key не может list/write вне exact scope.
- [ ] Safety key не может write/delete.
- [ ] Browser test origin выполняет presigned PUT с checksum и `If-None-Match: *`; повтор того же key получает `412`.
- [ ] Complete фиксирует authoritative `version_id`, ETag и checksum; Safety читает только эту version.
- [ ] Wrong version/ETag и изменённый source дают deny/error и не promote-ятся.
- [ ] Conditional promote mismatch не создаёт delivery outbox/Bitrix call.
- [ ] Quarantine lifecycle не удалит active `safety_tasks`.
- [ ] Data lifecycle соответствует retention.
## 8. Общий 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. Deploy из mutable branch без recorded SHA запрещён. `latest` отсутствует.
`.env` — только несекретный config и `SECRETS_SOURCE=file|selectel`. Секреты выдаёт `deployment/secrets/han-secrets`. `docker compose config` может раскрыть secrets; stdout не публиковать.
Validation: нет `change-me`, paired tokens equal, PG TLS, public HTTPS, `FRONTEND_DEV_PROXY_ENABLED=false` на production-like.
Состав `.env`/secret groups — профильный runbook.
## 9. TLS/ACME процедура
Канонические bootstrap, renewal, validation и safe reload задаёт
[`arch-08-nginx.md`](arch-08-nginx.md). Rollout каждой VM применяет эту модель
независимо, без `compose down` и без named volume сертификатов:
- root-only ACME state: `/etc/letsencrypt`;
- read-only для nginx host staging: `/var/lib/han-chat/public-tls`;
- host ACME webroot: `/var/lib/han-chat/acme`;
- root-owned systemd timer/hook дважды в сутки с `flock`; пользователь `deploy`
может запускать только утверждённый unit и не получает доступ к ACME state;
- staging CA rehearsal предшествует production issuance; после renewal root
hook проверяет certificate/key, атомарно обновляет staging, выполняет
container `nginx -t` и только затем HUP;
- success path возвращает `0` с пустым stderr; ошибка сохраняет действующий
certificate/config и поднимает alert (<21 дней, page <7 дней).
Host ВМ1 — `<PUBLIC_HOST>`, ВМ2 — `<PROCESSING_PUBLIC_HOST>`; private `8443`
использует internal CA, не Let's Encrypt. Ни ACME state, ни staging между VM не
разделяются.
## 10. Сквозной порядок startup и cutover
1. На ВМ2 оператор выполняет KESL runbook: KESL 12.4 standalone, database update/canary.
2. Затем enable/start root-owned broker socket/service и проверяет status/permissions `/run/han-kesl/scan.sock`.
3. Unit ВМ2 поднимает Redis Safety и local Collector, затем Safety API/worker и `bitrix-sync`.
4. Последним на ВМ2 — nginx public `80/443` и private `8443`.
5. На ВМ1 — Redis/Collector, API, SMS, Keycloak, local app, edge nginx.
6. Только private `MESSAGE_SAFETY_URL` ВМ1 переключается на ВМ2 после Safety gates. Public CRM webhook DNS/routes ВМ2 не требуют изменения ВМ1.
Broker — custom integration: до cutover target VM2 обязана подтвердить точный
формат/exit semantics `kesl-control --scan-file --action Inform`, socket
permissions/cleanup и throughput. Только `clean` допускает allow; `infected`
даёт deny; scanner error или stale database остаются retryable и завершаются
`503`, не allow. Runtime фиксирует `scanner_engine=kesl` и
`signatures_version=hash(KESL version + database date)`; KESL update выполняется
ежечасно.
Legacy single-VM `docker compose up` не является evidence готовности target ВМ2. Один `message_id` нельзя одновременно отправлять в v1 и v2. После cutover ВМ1 не содержит local Safety/Redis DB2.
При потере ВМ2 fail-open запрещён. Reprovision ВМ2 из immutable image/config; Redis пустой; gates повторяются. RTO ≤4 ч; restore rehearsal минимум дважды в год.
## 11. Observability, opening traffic, backup, rollback
Gate 5–14 не дублируются в этом общем контракте: это профильные release gates конкретной VM, описанные в `module-10-deployment-vm1.md` / `module-10-deployment-vm2.md` и executable runbook соответствующего репозитория. Нумерация внутри VM2 runbook локальна его технической приёмке; системное открытие трафика всегда требует подписанных результатов обоих контуров.
**Gate 15 — observability:** arch-07 и профильный module-09 VM spec; сквозной `request_id` через nginx ВМ1 → API → Safety ВМ2 обязателен до объявления production-ready.
**Gate 16 — opening traffic:** снять maintenance, включить HSTS после TLS test, сохранить digests/revisions, restore point и on-call, затем наблюдать 60 минут. До открытия должны быть подписаны Gate 0–4 этого документа, применимые Gate 5–14 профильных runbook обеих VM и Gate 15.
Backup: PG daily+PITR — business restore; S3 versioning; Redis не business backup; Keycloak в PG. Restore rehearsal в isolated VPC без production DNS/Bitrix callbacks.
Rollback приложения: previous immutable digests, без Alembic downgrade, `SCHEMA_BACKWARD_COMPATIBLE_CONFIRMED=true`. Backward-incompatible migration — только forward-fix или PITR. TLS: оставить старый config/cert при failure.
Запрещены: `docker compose down -v`, `docker system prune -a --volumes`, `DROP DATABASE` / `DROP SCHEMA`, recursive S3 delete, `certbot delete` active cert, unbounded logs, Redis `KEYS/FLUSH*`, ad-hoc DB DELETE.
Routine (автомат ежедневно): health/synthetics, PG backup/PITR, TLS expiry, disk/OTEL queue, Redis AOF/memory, DLQ/quarantine age. Еженедельно: image CVE, login/rate-limit trend, S3 inventory. Ежемесячно: OS patch, secret review, capacity, runbook 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
```
Incident 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: readonly `select now(), count(*) from pg_stat_activity`. Redis — только ops ACL `PING`. Secret literal не вставлять в ticket. Не replay message/DLQ до idempotency и ambiguous Bitrix outcome.
Типовые сценарии: API 503 — DB/Redis/JWKS/Safety circuits; send timeout — checkpoint, не новый key; Bitrix down — OAuth/circuit/DLQ; Redis loss — clean restart + polling; PG outage — не restart storm; disk full — known cache/old images после inventory; cert expiry — webroot/staging; secret leak — rotate, telemetry deletion.
Upgrades: notes → compatibility → PITR → staging → expand migration → one service at a time → E2E → contract later. Keycloak не пропускать unsupported majors.
DR потеря VM: новая Ubuntu в VPC, hardening, DNS, secrets из vault не со старого disk, exact images, Redis можно clean, existing PG/S3, TLS, ordered startup. Потеря PG: PITR в new instance, остановить writes, reconcile S3 orphans. Потеря S3: без versioning полное восстановление невозможно; отключить file ops. Compromise: isolate, forensic snapshot, rotate all, clean deploy, notify.
Перед teardown: inventory без secrets, revoke Bitrix/credentials, legal hold, DNS drain, deletion protection — отдельное approval, shared VPC/PG/S3 проверить.
## 12. Общий Definition of Done
- VM/VPC/DNS/SG/hardening соответствуют Gate 1–2;
- managed PG private/TLS/backups/least privilege/migrations работают;
- S3 private/IAM/CORS/lifecycle проверены;
- `deploy` не имеет Docker/root-equivalent доступа;
- независимые public 80/443; ВМ2 дополнительно private 8443;
- после Safety cutover ВМ1 вызывает ВМ2 только по private HTTPS с проверенным CA bind;
- container hardening — arch-06;
- для каждой private/no-egress VM завершён lockdown;
- backup restore и rollback rehearsed;
- ops/incident/upgrade/DR owners назначены;
- все assumptions/TBD приняты до открытия traffic.
Профильный DoD VM дополняет свои сервисы, Compose и cutover steps.
## 13. Допущения, TBD и конфликты
**Допущения:** D-A1 отдельные public hosts; D-A2 обязательный минимум — local Collector; D-A3 managed PG private/TLS/PITR; D-A4 Bitrix portal/connector/line — значения architecture; D-A5 mock OTP до SMS cutover как documented risk.
**TBD:** D-TBD1 domains/Expo redirect/Bitrix frame ancestors; D-TBD2 final VM1/PG sizing, public SLO, RPO/RTO; D-TBD3 legal retention; D-TBD4 secret manager; D-TBD5 Safety v2 cutover; D-TBD6 bitrix-sync cutover; D-TBD7 pinned versions; D-TBD8 exact CLI после реализации; D-TBD9 Keycloak admin VPN/MFA; D-TBD10 CSP/CORS/IAM; D-TBD11 SigNoz auth/retention/alert route.
**Конфликты:** module-07 documentation ≠ cutover; v1 stub ≠ production Safety; prototype PG init слишком широк; prototype TLS downtime; prototype `/bitrix-internal/*` запрещён; Compose/env могут ещё не содержать VM2/SMS artifacts.
## 14. Ссылки
- ВМ1: [`module-10-deployment-vm1.md`](../VM1_app/documentation/module-10-deployment-vm1.md).
- ВМ2: [`module-10-deployment-vm2.md`](../VM2_services/documentation/module-10-deployment-vm2.md).
- Исполняемые процедуры: [`RUNBOOK.production.ru.md`](../VM1_app/codebase/backend/deployment/RUNBOOK.production.ru.md) для ВМ1 и [`RUNBOOK.ru.md`](../VM2_services/codebase/services/deployment/RUNBOOK.ru.md) для ВМ2.