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

26 KiB
Raw Blame History

arch-10. Контракт развёртывания

Канонический контракт rollout для всех application VM. OS-роли, SSH/sudo, secrets delivery и hardening — arch-06-service-hosting-security.md; этот документ их не ослабляет.
Процедуры конкретной машины — module-10-deployment-vm1.md и module-10-deployment-vm2.md.
Nginx TLS — arch-08-nginx.md. Redis — arch-09-redis.md. Telemetry — arch-07-observability.md. Compose — 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, ClamAV signatures, incident/reprovision/restore rehearsal.
<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
ВМ2 freshclam approved signature CDN TCP 443/80 по vendor manifest 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-appbitrix_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:

/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. 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 unit поднимает Redis Safety и local Collector.
  2. Затем clamd/freshclam, Safety API/worker и bitrix-sync.
  3. Последним на ВМ2 — nginx public 80/443 и private 8443.
  4. На ВМ1 — Redis/Collector, API, SMS, Keycloak, local app, edge nginx.
  5. Только private MESSAGE_SAFETY_URL ВМ1 переключается на ВМ2 после Safety gates. Public CRM webhook DNS/routes ВМ2 не требуют изменения ВМ1.

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.

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:

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. Ссылки