--- title: План платформы Kubernetes + Terraform + ArgoCD + Gitea CI/CD --- # План развёртывания тестового и мульти-контурного окружения Sova > **Контекст:** сейчас проект живёт в Docker Compose (`environments/`, `infrastructure/`, `local/`, `monitoring/`, заготовки `jenkins/`). Цель — **три изолированных сервера** (test / stage / prod), на каждом — свой k3s-кластер с **Terraform**, **ArgoCD**, **мониторингом**, **CI/CD через Gitea** (центральный Git/Registry на test-сервере или отдельно) и **системой тегов** для promotion между контурами. > > **Prod PostgreSQL:** master + sync replica, автоматический failover — только prod, не test (§7.6). > > **As-is prod (без K8s):** одна VM — **12 GB RAM, 6 vCPU, 216 GB disk** (nginx + PHP + PostgreSQL + Redis на одной машине). Это **нижняя граница**, не целевой размер; при миграции в k3s + HA PG ресурсы увеличиваются (§3). > > Дополняет корневой [`DEPLOYMENT_PLAN.md`](../../DEPLOYMENT_PLAN.md) (там акцент на Managed K8s в облаке). **Этот документ** — практический план под сценарий «self-hosted k3s + Gitea + **отдельный сервер на контур**». --- ## 0. TL;DR — что получится в итоге ```mermaid flowchart TB subgraph devs [Разработчики] Dev[Push / Tag в Gitea] end subgraph srv_test [Сервер TEST] subgraph k3s_test [k3s test] GIT[Gitea + Registry + CI] ARGOt[ArgoCD test] BT[backend test] AT[adminPanel test] PGt[(PG single)] MOCK[sova-mocks] end end subgraph srv_stage [Сервер STAGE] subgraph k3s_stage [k3s stage] BS[backend stage] AS[adminPanel stage] PGs[(PG single)] end end subgraph srv_prod [Серверы PROD] subgraph k3s_prod [k3s prod apps] BP[backend prod] AP[adminPanel prod] end subgraph pg_ha [PostgreSQL HA 2 nodes] PG1[(Primary)] PG2[(Replica sync)] PG1 <-->|WAL sync| PG2 RW[PgBouncer rw] end end Dev --> GIT GIT --> ARGOt GIT -->|promote| k3s_stage GIT -->|promote| k3s_prod BT --> PGt BT --> MOCK BS --> PGs BP --> RW RW --> PG1 RW -.->|failover| PG2 ``` **Браузерные точки входа** (плейсхолдеры до выдачи реального домена): | Сервис | URL (плейсхолдер) | Сервер / контур | |--------|-------------------|-----------------| | Gitea | `https://git.sova.dev` | **test** (центральный Git) | | ArgoCD test | `https://argocd.test.sova.dev` | test | | ArgoCD stage | `https://argocd.stage.sova.dev` | stage | | ArgoCD prod | `https://argocd.sova.dev` | prod | | Grafana | per-env `grafana.{test,stage,sova}.dev` | каждый сервер | | backend **test** | `https://api.test.sova.dev` | test | | adminPanel **test** | `https://admin.test.sova.dev` | test | | backend **stage** | `https://api.stage.sova.dev` | stage | | adminPanel **stage** | `https://admin.stage.sova.dev` | stage | | backend **prod** | `https://api.sova.dev` | prod | | adminPanel **prod** | `https://admin.sova.dev` | prod | > Когда выдадут домен — замените `sova.dev` одной командой по репозиторию `sova-deploy` и Terraform DNS-модулю. --- ## 1. Анализ текущего состояния (as-is) ### 1.1. Что уже есть в репозитории | Каталог | Содержимое | Готовность к K8s | |---------|------------|------------------| | `infrastructure/` | Dockerfile nginx (VTS), PHP 8.4/8.2, nextjs, pgsql 14, redis | **База для образов**, но prod-Dockerfile приложений не готовы | | `environments/` | Layered Docker Compose: apps, dbs, dev, monitoring, jenkins | **Эталон** портов, доменов, зависимостей | | `local/` | Изолированный `sova-local`: PG16, MySQL Bitrix, nginx :8081/:8082, admin :3211 | **Эталон** env-переменных и smoke-тестов | | `monitoring/` | Prometheus scrape config, Grafana provisioning, community dashboards | **Перенос** в kube-prometheus-stack values | | `jenkins/` | 2 одинаковых Jenkinsfile (backend/cabinet), пустой frontend | **Не переносить as-is** — заменить Gitea Actions | | `scripts/` | cron hourly/daily, certbot, local-smoke | cron → **CronJob**; certbot → **cert-manager** | ### 1.2. Текущая архитектура Docker Compose ```mermaid flowchart LR subgraph public [public-network] Nginx[nginx :80/:443] Next[nextjs :3001] Grafana[grafana :3000] end subgraph internal [internal-network] PHP84[php84 backend] PHP82[php82 cabinet] PG[(pgsql :5432)] Redis[(redis)] Prom[prometheus] end Internet --> Nginx Nginx -->|api.sovamed.ru| PHP84 Nginx -->|cabinet.*| PHP82 Nginx -->|adm.* static| AdminDist Nginx -->|dev.sovamed.ru| Next PHP84 --> PG PHP84 --> Redis Prom --> Nginx ``` ### 1.3. Приложения и приоритет миграции | Приложение | Стек | Prod-домен сегодня | Фаза K8s | |------------|------|--------------------|----------| | **backend** | Symfony 7.3, PHP 8.4 | `api.sovamed.ru` | **Фаза 1 (test)** | | **adminPanel** | React 19, Vite | `adm.sovamed.ru` | **Фаза 1 (test)** | | cabinet | Symfony 5.4, PHP 8.2 | `cabinet.sovamed.ru` | Фаза 2 | | sovamed | Next.js | `dev.sovamed.ru` | Фаза 2 | | kiosk, widgetDoctors | Static SPA | `cdn.*`, `dev.wmtmed.ru` | Фаза 3 | ### 1.4. Критические пробелы (блокеры) 1. `apps/backend/Dockerfile` — **пустой** (нужен multistage build). 2. `apps/adminPanel/Dockerfile` — **отсутствует**. 3. Jenkins pipelines — **scaffold** с `registry.example.com`, shared library не в репо. 4. PostgreSQL prod использует **pg_cron, mysql_fdw, firebird_fdw** — проверить нужность в test. 5. Bitrix MySQL — cabinet/backend зависят; для test можно mock/отдельный pod MySQL. 6. `apps/` может быть **gitignored** в infra-репо — CI должен собирать из **отдельных Gitea-реп**. ### 1.5. Prod as-is (без Kubernetes) Текущая prod-VM (Docker Compose / bare services на одной машине): | Ресурс | Значение сегодня | Комментарий | |--------|------------------|-------------| | RAM | **12 GB** | nginx + PHP-FPM + PostgreSQL + Redis + мониторинг | | CPU | **6 vCPU** | | | Disk | **216 GB** | PG data + логи + образы Docker | | K8s | **нет** | миграция = новый sizing + HA PG | Это **baseline**, не целевая конфигурация. При переходе на k3s добавляется overhead платформы (~1.5–2 GB RAM), а для HA PostgreSQL нужен **второй сервер** (§7.6). --- ## 2. Целевая архитектура (to-be) ### 2.1. Принципы 1. **Infrastructure as Code:** Terraform поднимает сервер/OS/k3s; Helm + ArgoCD — всё внутри кластера. 2. **GitOps:** единственный источник истины деплоя — репозиторий `sova-deploy` (не `kubectl apply` руками). 3. **Immutable artifacts:** образы по тегам в Container Registry; rollback = смена тега в Git. 4. **Контуры изолированы:** test / stage / prod — **разные физические серверы**, разные БД, разные secrets. 5. **k3s single-node на каждом сервере** на старте; prod apps и prod PG HA могут быть на **2–3 серверах** (§3.4, §7.6). 6. **Prod PostgreSQL:** primary + sync replica + auto failover; test/stage — один инстанс PG достаточно. ### 2.2. Слои платформы ```mermaid flowchart TB subgraph L0 [L0 — Сервер] OS[Ubuntu 22.04/24.04 LTS] TF[Terraform: provisioning] end subgraph L1 [L1 — Kubernetes] K3s[k3s single-node] CNI[Flannel / built-in] Storage[local-path-provisioner] end subgraph L2 [L2 — Platform] ING[ingress-nginx] CM[cert-manager] ARGO[ArgoCD] MON[kube-prometheus-stack] LOKI[Loki + Promtail] SS[sealed-secrets] end subgraph L3 [L3 — DevOps] GIT[Gitea] RUN[Gitea Actions Runner] REG[Registry: gitea built-in или Harbor] end subgraph L4 [L4 — Apps per env] APP[backend + adminPanel Helm] DB[PostgreSQL + MySQL Bitrix + Redis per env] MOCK[Mock/stub layer test-only] CRON[CronJobs Symfony console] end L0 --> L1 --> L2 --> L3 --> L4 ``` ### 2.3. Namespace map | Namespace | Назначение | |-----------|------------| | `kube-system` | k3s системные компоненты | | `ingress` | ingress-nginx | | `cert-manager` | cert-manager | | `argocd` | ArgoCD | | `monitoring` | Prometheus, Grafana, Alertmanager, Loki | | `sealed-secrets` | controller | | `gitea` | Gitea + runner (опционально вынести на VM) | | `sova-test` | приложения test | | `sova-stage` | приложения stage | | `sova-prod` | приложения prod (когда будет готов) | | `sova-data-test` | PostgreSQL (main + cabinet), MySQL Bitrix, Redis test | | `sova-mocks` | WireMock MIS, Mailpit, captcha/calltouch/bitrix-http stubs (только test) | > Полная матрица внешних зависимостей backend и стратегия test/stage/prod: **[Backend: внешние сервисы](./backend-external-services.md)**. --- ## 3. Серверы и ресурсы (test / stage / prod — разные машины) ### 3.1. Топология | Сервер | Роль | k3s | PostgreSQL | |--------|------|-----|------------| | **test** | QA, mocks, центральный Gitea/Registry/CI | single-node | **1 инстанс** | | **stage** | pre-prod, sandbox-интеграции | single-node | **1 инстанс** | | **prod-app** | backend, adminPanel, Redis, MySQL Bitrix | single-node | клиент → HA | | **prod-db-1** | PG **primary** (sync) | нет | primary | | **prod-db-2** | PG **replica** (sync standby) | нет | replica | | **prod-db-witness** | **etcd quorum only** | нет | — | **Минимум prod HA:** prod-app + **2 db-VM** + **1 witness-VM** (1 vCPU / 1 GB). Gitea/Registry на **test**; stage/prod **pull** образов (§9.3). > **Witness нельзя** ставить на prod-app — см. §7.6.3 и §19. ### 3.2. Sizing: test-сервер | Ресурс | Минимум | Рекомендуется | |--------|---------|---------------| | CPU | 4 vCPU | **8 vCPU** | | RAM | 16 GB | **32 GB** | | Disk | 150 GB SSD | **200+ GB NVMe** | | OS | Ubuntu 22.04/24.04 LTS | | | Сеть | Публичный IP, 80/443/22 | | | DNS | A-запись `*.test.sova.dev` → IP test-сервера | | **Бюджет диска (200 GB):** registry ~50 GB (retention!), Loki ~15 GB, PG test ~30 GB, остальное — apps/k8s. | Компонент | RAM | |-----------|-----| | k3s + Gitea + Registry + runner | ~2.5 GB | | monitoring lite + Loki | ~2.5 GB | | PG + MySQL + Redis test | ~2 GB | | backend + adminPanel | ~1.5 GB | | **Итого** | **~9 GB** + 30% → **16 GB min** | ### 3.3. Sizing: stage-сервер | Ресурс | Минимум | Рекомендуется | |--------|---------|---------------| | CPU | 4 vCPU | **6 vCPU** | | RAM | 12 GB | **16 GB** | | Disk | 100 GB | **150 GB** | Без Gitea/Registry. Один PostgreSQL. ### 3.4. Sizing: prod (baseline as-is: 12 GB / 6 CPU / 216 GB) Текущая prod-VM — всё на одной машине **без k8s** (§1.5). Целевая prod с HA **не умещается** в 12 GB на одном диске с k8s. #### Вариант A — рекомендуемый (prod-app + 2× prod-db) | Сервер | CPU | RAM | Disk | Нагрузка | |--------|-----|-----|------|----------| | **prod-app** | **8 vCPU** | **16 GB** | **150 GB** | k3s, apps, Redis, MySQL Bitrix, ingress, ArgoCD | | **prod-db-1** | **4 vCPU** | **12 GB** | **150 GB NVMe** | PG primary + Patroni | | **prod-db-2** | **4 vCPU** | **12 GB** | **150 GB NVMe** | PG sync replica | **Итого prod:** ~16 vCPU, ~40 GB RAM (vs 6 / 12 GB сегодня). #### Вариант B — промежуточный (2 сервера, replica позже) | Сервер | CPU | RAM | Disk | |--------|-----|-----|------| | **prod-app** | 8 vCPU | 16 GB | 150 GB | | **prod-db-1** | 6 vCPU | 16 GB | 216 GB (как сейчас) | ⚠️ Без **prod-db-2** HA **нет** — только подготовка; replica обязательна до cutover prod в k8s. #### Диск prod-db PG data 80–120 GB, WAL 20 GB, pg_dump 30 GB, запас 40+ GB. MySQL Bitrix / Redis — на **prod-app**. ### 3.5. Terraform per environment Модули: `test-server`, `stage-server`, `prod-app`, `prod-db-1`, `prod-db-2`. Remote state вне серверов (§5.1). Firewall: prod-app → prod-db:5432; replication prod-db-1 ↔ prod-db-2. --- ## 4. Структура репозиториев в Gitea Разделить **код приложений**, **инфраструктуру Terraform**, **манифесты деплоя**: ```text gitea.sova.dev/ ├── sova-backend # apps/backend (Symfony) ├── sova-adminpanel # apps/adminPanel (React) ├── sova-cabinet # позже ├── sova-infra # этот монорепо: infrastructure/, monitoring/, terraform/ ├── sova-deploy # Helm charts + ArgoCD Applications + values per env └── sova-platform # опционально: только Terraform + bootstrap scripts ``` ### 4.1. `sova-deploy` — GitOps репозиторий (главный для ArgoCD) ```text sova-deploy/ ├── README.md ├── apps/ │ ├── backend/ │ │ ├── Chart.yaml │ │ ├── values.yaml # defaults │ │ ├── values-test.yaml │ │ ├── values-stage.yaml │ │ ├── values-prod.yaml │ │ └── templates/ │ │ ├── deployment.yaml # pod: nginx + php-fpm sidecar │ │ ├── service.yaml │ │ ├── ingress.yaml │ │ ├── hpa.yaml │ │ ├── migrate-job.yaml # PreSync hook │ │ ├── cronjobs.yaml # upload:doctors, bitrix-update-reviews │ │ └── externalsecret.yaml │ └── adminpanel/ │ ├── Chart.yaml │ ├── values-test.yaml │ └── templates/... ├── data/ │ ├── postgresql/ # test/stage only: Helm values (Bitnami/CNPG 1 instance) │ └── redis/ # test/stage: Helm values │ # prod PostgreSQL — вне K8s; конфиги в sova-platform (§4.2), не здесь ├── argocd/ │ ├── projects/sova-project.yaml │ ├── app-of-apps.yaml │ └── apps/ │ ├── platform-ingress.yaml │ ├── platform-cert-manager.yaml │ ├── platform-monitoring.yaml │ ├── platform-argocd.yaml │ ├── backend-test.yaml │ ├── adminpanel-test.yaml │ ├── backend-stage.yaml │ └── ... └── environments/ ├── test/ │ └── kustomization.yaml # optional Kustomize overlay ├── stage/ └── prod/ ``` ### 4.2. `sova-infra` / `sova-platform` — Terraform и bare-metal > **ArgoCD (`sova-deploy`)** применяет только Kubernetes-манифесты и Helm. **Patroni, HAProxy, Keepalived, etcd** на prod-db-VM — **не** в `sova-deploy`, а здесь. ```text sova-platform/ ├── terraform/ │ ├── modules/ │ │ ├── prod-db/ # Patroni + etcd + HAProxy + VIP (SSH, provider-agnostic) │ │ ├── k3s/ # установка k3s на test/stage/prod-app │ │ ├── server/ # опционально: VM через API провайдера │ │ ├── dns/ │ │ └── backup/ │ ├── envs/ │ │ ├── test-server/ │ │ ├── stage-server/ │ │ ├── prod-app/ │ │ └── prod-db/ # 2× PG + witness (см. terraform/envs/prod-db в монорепо) │ └── global/ │ └── versions.tf ├── ansible/ │ ├── inventory/prod-db/ │ └── playbooks/ │ ├── patroni-cluster.yml # альтернатива remote-exec в Terraform │ ├── haproxy-keepalived.yml │ └── templates/ # patroni.yml, haproxy.cfg, keepalived.conf ├── bootstrap/ │ ├── 01-hardening.sh │ ├── 02-install-k3s.sh │ └── 03-install-helm-charts.sh ``` В монорепо уже есть стартовый модуль: [`terraform/modules/prod-db`](../../terraform/modules/prod-db/README.md). --- ## 5. Terraform: пошаговая реализация ### 5.1. Providers и remote state (критично) **Проблема «курицы и яйца»:** если Terraform state лежит на том же сервере, что и k3s/Gitea/MinIO, при падении сервера вы не сможете восстановить инфраструктуру — state недоступен вместе с кластером. **Правило:** state для **базовой инфраструктуры** (VPS/k3s bootstrap) хранится **только снаружи** кластера и сервера: | Вариант | Когда использовать | |---------|-------------------| | **Yandex Object Storage** (S3-compatible) | Основной вариант для RU | | AWS S3 / Backblaze B2 | Если уже есть аккаунт | | **Terraform Cloud / HCP Terraform** | Managed backend, locking из коробки | | Приватный Git **без секретов** | Только как временный fallback; locking вручную | **Не использовать** как единственный backend: Gitea на том же сервере, MinIO внутри k3s, локальный файл на VPS без репликации. ```hcl # terraform/envs/test-server/backend.tf terraform { required_version = ">= 1.6" required_providers { null = { source = "hashicorp/null", version = "~> 3.2" } helm = { source = "hashicorp/helm", version = "~> 2.12" } kubectl = { source = "alekc/kubectl", version = "~> 2.0" } } backend "s3" { bucket = "sova-terraform-state" key = "envs/test-server/terraform.tfstate" region = "ru-central1" endpoints = { s3 = "https://storage.yandexcloud.net" } skip_credentials_validation = true skip_region_validation = true skip_requesting_account_id = true } } ``` Credentials для backend (`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` для Yandex S3) — в CI/CD secrets или локально в `~/.aws/credentials`, **не** в state file. > State для **Helm releases внутри уже работающего k3s** можно держать в том же remote backend (отдельный `key`), но recovery-сценарий всегда начинается с внешнего state bootstrap-модуля. ### 5.2. Модуль `server` (существующий VPS) ```hcl # terraform/modules/k3s-single-node/main.tf variable "server_ip" {} variable "ssh_user" { default = "root" } variable "k3s_version" { default = "v1.30.4+k3s1" } variable "cluster_domain" { default = "sova.dev" } resource "null_resource" "k3s_install" { connection { type = "ssh" host = var.server_ip user = var.ssh_user private_key = file(var.ssh_private_key_path) } provisioner "remote-exec" { script = "${path.module}/scripts/install-k3s.sh" environment = { K3S_VERSION = var.k3s_version CLUSTER_DOMAIN = var.cluster_domain } } } ``` Скрипт `install-k3s.sh` должен: 1. Отключить swap, настроить `br_netfilter`, iptables. 2. Установить k3s: `curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION=... sh -`. 3. Положить kubeconfig в `/root/.kube/config`. 4. Установить `kubectl`, `helm`. 5. Включить **Traefik disable** (`--disable traefik`) — используем ingress-nginx. 6. Включить **local-path-provisioner** (default в k3s). ### 5.3. Platform через Terraform Helm releases **Ingress на bare metal (k3s):** не нужны NodePort + iptables + внешний nginx на хосте. k3s включает **ServiceLB (Klipper LoadBalancer)** — при `type: LoadBalancer` порты **80 и 443** автоматически биндятся на IP сервера. ```hcl resource "helm_release" "ingress_nginx" { name = "ingress-nginx" repository = "https://kubernetes.github.io/ingress-nginx" chart = "ingress-nginx" namespace = "ingress" create_namespace = true set { name = "controller.service.type" value = "LoadBalancer" # k3s ServiceLB → 80/443 на IP ноды } set { name = "controller.service.externalTrafficPolicy" value = "Local" # сохранить client IP для rate-limit / logs } } ``` **Альтернатива** (если ServiceLB недоступен): `DaemonSet` + `hostNetwork: true` — ingress слушает 80/443 напрямую на каждой ноде. Traefik в k3s **отключён** при установке (`--disable traefik`) — единственный edge ingress. > MetalLB и NodePort 30080/30443 — **не нужны** на single-node k3s с ServiceLB. ### 5.4. Terraform outputs ```hcl output "kubeconfig_path" { value = "/root/.kube/config" } output "argocd_url" { value = "https://argocd.sova.dev" } output "gitea_url" { value = "https://git.sova.dev" } ``` ### 5.5. Порядок apply ```mermaid flowchart TD A[terraform apply: k3s install] --> B[helm: sealed-secrets] B --> C[helm: cert-manager + ClusterIssuer] C --> D[helm: ingress-nginx] D --> E[helm: argocd] E --> F[helm: kube-prometheus-stack] F --> G[helm: gitea] G --> H[push sova-deploy → argocd app-of-apps] H --> I[ArgoCD sync platform + apps] ``` --- ## 6. Platform layer (детально) ### 6.1. ingress-nginx - Аналог текущего `infrastructure/nginx` edge. - **k3s ServiceLB:** chart с `controller.service.type: LoadBalancer` — порты 80/443 на IP сервера без NodePort/iptables (см. §5.3). - Один IngressClass `nginx` для всех env. - Анnotations для rate-limit, body-size (upload файлов backend). - Метрики: ServiceMonitor → Prometheus (замена scrape `nginx:88` из Docker). **Маппинг доменов test:** | Ingress host | Service | Порт | |--------------|---------|------| | `api.test.sova.dev` | `backend-test` | 80 | | `admin.test.sova.dev` | `adminpanel-test` | 80 | | `argocd.sova.dev` | `argocd-server` | 80 | | `grafana.sova.dev` | `kube-prometheus-grafana` | 80 | | `git.sova.dev` | `gitea-http` | 3000 | ### 6.2. cert-manager ```yaml # ClusterIssuer Let's Encrypt staging (для отладки) apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt-staging spec: acme: server: https://acme-staging-v02.api.letsencrypt.org/directory email: devops@sova.dev privateKeySecretRef: name: letsencrypt-staging solvers: - http01: ingress: class: nginx ``` После проверки — production issuer. **До выдачи домена** используйте: - self-signed ClusterIssuer + импорт CA в браузер; - или **nip.io** / **sslip.io**: `api.test.192-168-1-100.sslip.io`. ### 6.3. ArgoCD Установка через Helm chart `argo/argo-cd`: - Ingress: `argocd.sova.dev` - SSO: позже через Gitea OAuth - Репозитории: `sova-deploy` (SSH deploy key read-only) - **App of Apps** паттерн (см. раздел 9) - Image Updater (опционально): автобамп тегов — или только CI меняет values ```yaml # argocd/app-of-apps.yaml apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: sova-root namespace: argocd spec: project: default source: repoURL: git@gitea.sova.dev:sova/sova-deploy.git targetRevision: main path: argocd/apps destination: server: https://kubernetes.default.svc namespace: argocd syncPolicy: automated: prune: true selfHeal: true ``` ### 6.4. Sealed Secrets / External Secrets Секреты **не коммитить** открытым текстом. | Секрет | Ключи (из backend `.env`) | |--------|---------------------------| | `backend-env-test` | `DATABASE_URL`, `REDIS_URL`, `APP_SECRET`, `JWT_*`, `MIS_URL`, ... | | `backend-env-stage` | те же, другие значения | | `registry-credentials` | pull from Gitea Container Registry | Workflow: 1. `kubeseal` → зашифрованный SealedSecret в Git. 2. ArgoCD применяет → controller расшифровывает в обычный Secret. **Бэкап master key Sealed Secrets (критично для DR):** При пересоздании кластера controller **генерирует новый** приватный ключ. Все SealedSecret из Git перестанут расшифровываться, если ключ не восстановить **до** sync ArgoCD. ```bash # Сразу после установки controller (Фаза 1/2): kubectl get secret -n kube-system \ -l sealedsecrets.bitnami.com/sealed-secrets-key \ -o yaml > sealed-secrets-master-key-BACKUP.yaml # Сохранить в менеджер паролей / offline vault — НЕ в публичный Git ``` **При восстановлении кластера:** 1. Установить sealed-secrets controller. 2. Применить backup Secret **до** деплоя приложений с SealedSecrets. 3. Проверить: `kubectl get sealedsecret -A` → статус `Synced`. 4. Только потом включать ArgoCD sync apps. Ротация ключа: [официальный re-encryption flow](https://github.com/bitnami-labs/sealed-secrets#sealing-key-renewal) — планировать раз в год, не при каждом rebuild. --- ## 7. Data layer (PostgreSQL, Redis, Bitrix MySQL) ### 7.1. PostgreSQL по контурам | Контур | Топология | Решение | |--------|-----------|---------| | **test** | 1 инстанс | Bitnami chart или CNPG `instances: 1` | | **stage** | 1 инстанс | То же, отдельный сервер | | **prod** | **primary + sync replica + auto failover** | §7.6 | | Вариант (test/stage) | Плюсы | Минусы | |---------|-------|--------| | **A. Helm Bitnami PostgreSQL** | Просто, как в Compose | RAM, бэкапы сами | | **B. CloudNativePG** `instances: 1` | Единый оператор с prod | Сложнее на test | | **C. Managed PG в облаке** | Надёжно | Доп. cost | **Рекомендация test/stage:** Bitnami PostgreSQL chart (`pg-test`, `pg-stage`). ### 7.2. Схема БД test ```sql -- Аналог local/postgres/init CREATE DATABASE sova_backend_test; CREATE DATABASE sova_cabinet_test; -- когда подключим cabinet CREATE USER sova_test WITH PASSWORD '...'; GRANT ALL ON DATABASE sova_backend_test TO sova_test; ``` Миграции: Symfony `doctrine:migrations:migrate` через ArgoCD **PreSync Job** (см. §7.2.1). ### 7.2.1. Миграции БД и откат (подводные камни) **PreSync Job** — правильный паттерн, но с ограничениями: | Риск | Что делать | |------|------------| | Тяжёлая миграция (индекс на миллионах строк, >5 мин) | ArgoCD sync timeout — увеличить `timeout.reconciliation` / `syncOptions` для Job; тестировать миграции на копии prod | | PreSync Job упал | Деплой **останавливается** — это ожидаемо и правильно | | **Rollback ArgoCD** | Откатывает только **stateless** манифесты (Deployment, ConfigMap). **Схема БД не откатывается автоматически** | | Откат после failed migration | `doctrine:migrations:execute --down VERSION` вручную или restore PG из snapshot | ```yaml # argocd Application — фрагмент syncPolicy syncPolicy: syncOptions: - CreateNamespace=true hooks: - type: PreSync # Job: php bin/console doctrine:migrations:migrate --no-interaction # activeDeadlineSeconds: 600 # для тяжёлых миграций ``` **Правило:** rollback в ArgoCD = откат **кода**, не **данных**. Для test допустим `pg_dump` перед каждым major release; для prod — обязателен backup + documented down-migration. ### 7.3. Redis Bitnami Redis chart, auth enabled (`REDIS_PASSWORD` → Secret). ### 7.4. Bitrix MySQL (обязательно для полного backend test) Backend читает Bitrix **напрямую через MySQL** (`BitrixService`, sync отзывов) и через **SQL views** в PostgreSQL (`view_news`, `view_promo`, … → таблицы `b_iblock_*`). **Test-контур — новый инстанс**, не prod Bitrix: - Helm `bitnami/mysql` в `sova-data-test`; - init SQL из `local/mysql-bitrix/init/` (seed); - `DATABASE_BITRIX_URL` в SealedSecret; - опционально **mysql_fdw** в test-PG (как на prod) — см. [§1.3 в backend-external-services.md](./backend-external-services.md). ### 7.5. Внешние HTTP-сервисы и заглушки (test) Backend вызывает MIS (Инфоклиника), SmartCaptcha, почту, Calltouch, Bitrix HTTP (картинки). **SMS и почта в test — заглушки**; **БД — свои инстансы**; остальное — по матрице ниже. | Сервис | Test | Stage | Prod | |--------|------|-------|------| | MIS / widget API | Mock в `sova-mocks` | Sandbox или mock | Prod MIS | | SMS (sms.ru, sms4b) | Noop (код пока не вызывается) | Noop | Live | | Почта | Mailpit / `null://null` | Sandbox SMTP | Live SMTP | | SmartCaptcha | Mock (always OK) | Mock | Yandex API | | Calltouch | Noop (в коде вызов закомментирован) | Noop | Live | | Bitrix HTTP (images) | Static mock nginx | Stage site | Prod | **Реализация mock-слоя:** 1. Namespace `sova-mocks` — Helm releases: `mis-mock` (WireMock + JSON fixtures), `mailpit`, `captcha-mock`, `bitrix-http-mock`. 2. Env backend test: `MIS_URL`, `BITRIX_URL`, `SMARTCAPTCHA_URL`, `MAILER_DSN` → Cluster DNS (`*.sova-mocks.svc.cluster.local`). 3. Репозиторий `sova-mocks` (рядом с `sova-deploy`) — fixtures для расписания и anonymous-reserve. 4. **Техдолг до выката:** hardcoded `https://widget.sovamed.ru` в `UploadFilialsCommand` / `UploadPrice*` — mock должен отвечать на те же пути или команды рефакторятся на env. Подробности, env-фрагмент SealedSecret и чек-лист: **[backend-external-services.md](./backend-external-services.md)**. ### 7.6. PostgreSQL HA — prod only (master + replica, sync, failover) > **Только prod.** Test и stage — один PG-инстанс (§7.1). MySQL Bitrix, Redis — **не** входят в эту схему. #### 7.6.1. Что значит «сквозная запись в две БД» **Не делать:** двойной `INSERT` из Symfony в два `DATABASE_URL` — race conditions, split-brain, невозможность транзакций. **Правильно:** **синхронная streaming-репликация PostgreSQL**: 1. Backend пишет **только в primary** (один connection string). 2. Primary перед `COMMIT` **ждёт подтверждения от sync replica**, что WAL записан (`synchronous_commit = on`). 3. Данные **гарантированно** на обоих узлах после успешного commit. 4. При падении primary **Patroni / CNPG / Managed PG** promote replica → новый primary за **30–120 сек**. 5. Backend переподключается через **стабильный endpoint** (PgBouncer, VIP, CNPG `-rw` Service) — не напрямую на IP одной VM. ```mermaid sequenceDiagram participant BE as backend pod participant RW as PgBouncer rw participant P as PG Primary participant R as PG Replica sync BE->>RW: INSERT / COMMIT RW->>P: SQL P->>R: WAL stream sync R-->>P: flush ack P-->>RW: COMMIT ok RW-->>BE: ok Note over P: primary down R->>R: Patroni promote BE->>RW: reconnect RW->>R: new primary ``` #### 7.6.2. Варианты реализации (prod) | Вариант | Где крутится | Плюсы | Минусы | |---------|--------------|-------|--------| | **A. Patroni + etcd** на **2 db-VM** | prod-db-1, prod-db-2 (вне k3s) | Изолированные IOPS, привычный ops, extensions (pg_cron, mysql_fdw) | etcd 3-й узел или witness | | **B. CloudNativePG** | operator на prod-app k3s, PG pods anti-affinity на 2 db-VM | GitOps-native, auto failover | PG в k8s + storage class; extensions проверить | | **C. Yandex Managed PG HA** | managed cloud | Меньше ops | cost, сеть до prod-app, extensions | **Рекомендация для Sova:** **Вариант A (Patroni на 2 db-VM)** — prod уже на VM с PG; extensions (`pg_cron`, `mysql_fdw`, `firebird_fdw`) критичны; apps на k3s **prod-app** подключаются к Patroni VIP. **Terraform-модуль (provider-agnostic, SSH):** [`terraform/modules/prod-db`](../../terraform/modules/prod-db/README.md) + окружение [`terraform/envs/prod-db`](../../terraform/envs/prod-db/). Серверы заказываются у любого хостера (Selectel и т.д.), модуль **не** вызывает API провайдера. #### 7.6.3. Patroni topology (рекомендуемый) ```text prod-db-1 (192.168.x.10) prod-db-2 (192.168.x.11) ┌─────────────────────┐ ┌─────────────────────┐ │ PostgreSQL 16 │ sync │ PostgreSQL 16 │ │ Patroni │◄────────►│ Patroni │ │ role: primary │ WAL │ role: replica │ └──────────┬──────────┘ └──────────┬──────────┘ │ │ └──────────┬──────────────────────┘ │ ┌───────▼────────┐ │ HAProxy / VIP │ :5432 rw → primary only │ (или PgBouncer)│ :5433 ro → replica (опц.) └───────▲────────┘ │ prod-app k3s (backend pods) ``` **etcd / DCS (критично):** quorum **3 узла** — `prod-db-1`, `prod-db-2` + **отдельная witness-VM** (1 vCPU / 1 GB RAM, без k3s, без PostgreSQL). | Размещение witness | Вердикт | |--------------------|---------| | **Отдельная крошечная VM** | ✅ Рекомендуется | | **prod-app** (k3s) | ❌ **Запрещено** — ребут/обновление k3s → потеря etcd quorum → Patroni **demote primary** → БД read-only | | Только 2 db-узла без 3-го | ❌ Нет quorum при падении одного db | **Если witness на prod-app неизбежен** (не рекомендуется): перед любым обслуживанием prod-app — `patronictl pause` на кластере; после — `patronictl resume`. Зафиксировано в §19. Альтернатива bare VM: Patroni с **Kubernetes DCS** только если PG внутри k8s (CloudNativePG, §7.6.4). **PostgreSQL config (фрагмент):** ```ini # postgresql.conf — primary synchronous_commit = on synchronous_standby_names = 'FIRST 1 (prod-db-2)' wal_level = replica max_wal_senders = 5 ``` ```ini # postgresql.conf — replica hot_standby = on ``` #### 7.6.4. CloudNativePG (альтернатива, если PG в GitOps) ```yaml apiVersion: postgresql.cnpg.io/v1 kind: Cluster metadata: name: sova-pg-prod namespace: sova-data-prod spec: instances: 2 primaryUpdateStrategy: unsupervised postgresql: parameters: synchronous_commit: "on" minSyncReplicas: 1 maxSyncReplicas: 1 storage: size: 120Gi affinity: enablePodAntiAffinity: true topologyKey: kubernetes.io/hostname ``` Backend `DATABASE_URL`: ```text postgresql://sova:***@sova-pg-prod-rw.sova-data-prod.svc:5432/sova_backend ``` `-rw` Service переключается на новый primary после failover. **CNPG pods должны быть на разных физических нодах** — для этого 2 db-VM как worker nodes или dedicated node pool. #### 7.6.5. Backend connection и PgBouncer (prod) **Два endpoint — два режима pooler** (типичная боль Symfony + PgBouncer): | Порт | Режим PgBouncer | Кто подключается | Зачем | |------|-----------------|------------------|-------| | **6432** | `pool_mode = transaction` | `DATABASE_URL` backend (php-fpm, API) | Короткие HTTP-запросы, высокая плотность соединений | | **5432** | `session` **или** напрямую PostgreSQL/VIP | PreSync migrate Job, Symfony Messenger worker, `doctrine:migrations:*` | Advisory locks, prepared statements, long transactions | ```yaml # SealedSecret backend-env-prod (фрагмент) DATABASE_URL: postgresql://sova_prod:***@pg-prod.sova.internal:6432/sova_backend?sslmode=prefer # API / php-fpm → PgBouncer transaction mode DATABASE_MIGRATE_URL: postgresql://sova_prod:***@pg-prod.sova.internal:5432/sova_backend?sslmode=prefer # migrate-job.yaml и messenger worker → session mode или VIP минуя transaction pooler ``` ```yaml # apps/backend/templates/migrate-job.yaml — фрагмент env: - name: DATABASE_URL valueFrom: secretKeyRef: name: backend-env key: DATABASE_MIGRATE_URL # не 6432 transaction pool ``` `MESSENGER_TRANSPORT_DSN=doctrine://default` при Doctrine transport — worker Deployment тоже на **5432 / session**, не на 6432. **VIP:** `pg-prod.sova.internal` → Keepalived VIP на db-узлах (HAProxy). PgBouncer можно co-locate на db-VM или prod-app — оба порта слушают на одном DNS. Symfony + Doctrine: **не один DSN на всё**. Rollback / failover — см. §7.6.6; `pdo_pgsql` reconnect после switchover. **PHP-FPM:** короткий `statement_timeout`; при `connection lost` — 503 на 1–2 запроса во время switchover; readiness probe проверяет PG rw. #### 7.6.6. Failover и RPO/RTO | Метрика | Sync replication | Async (не для prod Sova) | |---------|------------------|--------------------------| | **RPO** (потеря данных) | **0** при 1 sync standby | секунды–минуты WAL | | **RTO** (время восстановления) | **1–3 мин** (Patroni + k8s rollout) | то же | | Commit latency | +1–5 ms (RTT до replica) | ниже | При падении **обоих** db-узлов — restore из **WAL-G / pg_dump** в Object Storage (§7.6.7). #### 7.6.7. Бэкапы prod PG (дополнение к репликации) Реплика **не заменяет** backup (удаление таблицы реплицируется на standby). | Метод | Частота | Хранение | |-------|---------|----------| | `pg_dump -Fc` CronJob / systemd timer | daily | S3 / Yandex Object Storage 30d | | WAL archiving (pgBackRest / WAL-G) | continuous | S3, PITR | | Тест restore | monthly | на stage PG | #### 7.6.8. Миграция с текущей prod-VM (12 GB / single PG) ```mermaid flowchart LR A[Prod VM today single PG] --> B[Поднять prod-db-1 + prod-db-2 Patroni] B --> C[pg_basebackup / logical dump restore] C --> D[Sync replica catch-up] D --> E[Cutover DATABASE_URL на VIP] E --> F[prod-app k3s backend] A --> F ``` 1. Поднять **prod-db-1**, **prod-db-2** (§3.4). 2. `pg_dump` с текущей VM → restore на Patroni primary. 3. Подключить sync replica, дождаться `pg_stat_replication.sync_state = sync`. 4. Короткое окно maintenance: stop writes на старой VM → final sync → switch `DATABASE_URL` на VIP. 5. Поднять **prod-app** k3s, направить traffic на новый backend. 6. Старую VM держать read-only **7 дней** как fallback. #### 7.6.9. Мониторинг и алерты (prod PG) | Alert | Expr / check | |-------|----------------| | Replication lag | `pg_replication_lag_seconds > 10` | | Sync standby lost | `sync_standby_names` пуст, только async | | Patroni no leader | `patroni_master == 0` | | Disk > 80% | node filesystem on db-VM | Grafana dashboard: postgres_exporter на **обоих** db-узлах. #### 7.6.10. Чек-лист prod PG HA - [ ] 2 db-VM в разных зонах / стойках (если возможно) - [ ] `synchronous_commit = on`, `minSyncReplicas: 1` или Patroni sync - [ ] Backend **не** использует IP одной ноды — только VIP / `-rw` - [ ] etcd quorum: **3 узла**, witness на **отдельной** VM (не prod-app) - [ ] Failover drill на stage **не подходит** (1 PG) — отдельный drill на prod-db или ephemeral cluster - [ ] pg_dump + WAL в S3, restore проверен - [ ] Extensions `pg_cron`, `mysql_fdw` работают на primary после migrate --- ## 8. Приложения: образы и Helm ### 8.1. backend — Dockerfile (создать в `sova-backend`) Опираться на `infrastructure/php-8.4/Dockerfile` + план из `DEPLOYMENT_PLAN.md` §4.1. **Pod layout (sidecar):** ```mermaid flowchart LR subgraph pod [Pod backend-test-xxx] NG[nginx container :8080] PHP[php-fpm container :9000] NG -->|fastcgi 127.0.0.1:9000| PHP end ING[Ingress] --> NG PHP --> PG[(PostgreSQL)] PHP --> MY[(MySQL Bitrix)] PHP --> RD[(Redis)] PHP --> MIS[Mock MIS test-only] ``` > **Prod:** PostgreSQL **вне** pod — подключение к Patroni VIP (§7.6). На test/stage — PG в кластере или на том же сервере. Конфиг nginx для sidecar — упрощённый из `infrastructure/nginx/prod/conf.d` для `api.sovamed.ru` (только `/public` + fastcgi). ### 8.2. adminPanel — Dockerfile Multistage: `node:24-alpine` build → `nginx:alpine` runtime. **Runtime env без пересборки:** ```javascript // public/env.js — подменяется через ConfigMap + initContainer window.__ENV__ = { API_BASE_URL: "https://api.test.sova.dev" }; ``` ### 8.3. CronJobs (миграция из `scripts/`) | Docker/cron | K8s CronJob | Расписание | |-------------|-------------|------------| | `scripts/cron.hourly.sh` → upload:doctors, upload:deps | `backend-sync-doctors` | `0 1 * * *` | | `scripts/cron.oncyday.sh` → bitrix-update-reviews | `backend-sync-reviews` | `0 3 * * *` | | `ClearScheduleCacheCommand` | `backend-clear-schedule-cache` | `0 */6 * * *` | **CronJobs и внешние зависимости:** sync-команды (`upload:doctors`, `bitrix-update-reviews`, `upload:news`, …) требуют **test MySQL Bitrix** и **mock MIS**. На первом выкате test включить только `ClearScheduleCacheCommand`; остальные — после готовности mock-слоя (см. [backend-external-services.md §6](./backend-external-services.md)). ```yaml apiVersion: batch/v1 kind: CronJob metadata: name: backend-upload-doctors namespace: sova-test spec: schedule: "0 1 * * *" timeZone: "Europe/Moscow" concurrencyPolicy: Forbid # не запускать вторую копию, пока первая не завершилась successfulJobsHistoryLimit: 3 failedJobsHistoryLimit: 3 jobTemplate: spec: activeDeadlineSeconds: 3600 # kill зависшую job через 1 ч template: spec: restartPolicy: OnFailure containers: - name: console image: registry.sova.dev/sova/backend:{{ .Values.image.tag }} command: ["php", "bin/console", "upload:doctors"] envFrom: - secretRef: { name: backend-env } ``` > `concurrencyPolicy: Forbid` обязателен для sync-кронов (`bitrix-update-reviews`, `upload:*`), иначе при зависшей БД параллельные job'ы начнут мешать друг другу. --- ## 9. CI/CD через Gitea Actions ### 9.1. Почему Gitea Actions, а не Jenkins | Jenkins (as-is) | Gitea Actions | |-----------------|---------------| | Scaffold, shared libs отсутствуют | YAML в репозитории приложения | | Отдельный контейнер + docker.sock | Runner in K8s с **Kaniko/Buildah** (k3s = containerd, не docker.sock) | | Не в `make dev` | Native интеграция с Gitea | ### 9.2. Gitea setup 1. Helm chart `gitea/gitea` в namespace `gitea`. 2. Ingress `git.sova.dev`, TLS via cert-manager. 3. Включить **Actions** (`ENABLED_ACTIONS=true`). 4. Зарегистрировать **act_runner** (Kubernetes executor). **act_runner: ресурсы и сборка образов** k3s использует **containerd**, монтировать `docker.sock` в runner **нельзя**. Для build/push образов: | Инструмент | Когда | |------------|-------| | **Kaniko** | Сборка в pod без privileged; push в Gitea CR | | **Buildah** | Альнатива Kaniko, rootless | | Runner на **отдельной VM** | Если CI нагрузка критична для prod pods | ```yaml # act_runner в K8s — обязательные limits (иначе npm/composer install «положит» кластер) resources: requests: cpu: "500m" memory: "1Gi" limits: cpu: "2" memory: "4Gi" ``` Рекомендация: **отдельный node pool / taint** `ci=true:NoSchedule` для runner pods, когда появится второй сервер. На single-node — limits + `priorityClassName` ниже, чем у backend. ```yaml # gitea-values.yaml (фрагмент) gitea: config: actions: ENABLED: true repository: DEFAULT_BRANCH: main service: http: port: 3000 ingress: enabled: true hosts: - host: git.sova.dev ``` ### 9.3. Container Registry **Вариант A:** встроенный Gitea Container Registry (`git.sova.dev/sova/backend`). **Вариант B:** Harbor (тяжелее, но удобнее для retention policies). **Retention policy (обязательно с первого дня):** CI пушит образ на каждый тег (`backend-v*-test`). PHP + Node образы быстро съедают **200 GB** диска. | Мера | Настройка | |------|-----------| | Gitea CR cleanup | Admin → Packages → retention: хранить последние N тегов per repo | | CronJob в кластере | Удалять untagged manifests старше 7 дней | | CI | Не пушить `:latest` на каждый commit — только semver-теги | ```yaml # Пример CronJob registry-gc (Harbor / или скрипт к Gitea API) # DELETE untagged + keep last 10 tags per image ``` **Pull образов на stage/prod (центральный Registry на test-сервере):** Gitea Container Registry живёт на **test** (`git.sova.dev`). k3s на stage/prod **по умолчанию не может** pull приватных образов. 1. Gitea → Settings → Applications → **Generate token** (read:package) для robot `sova-pull`. 2. На **stage** и **prod** создать Secret (или SealedSecret в `sova-deploy`): ```yaml apiVersion: v1 kind: Secret metadata: name: gitea-registry-secret namespace: sova-stage # или sova-prod type: kubernetes.io/dockerconfigjson data: .dockerconfigjson: ``` 3. В `values-stage.yaml` / `values-prod.yaml`: ```yaml imagePullSecrets: - name: gitea-registry-secret image: repository: git.sova.dev/sova/backend tag: backend-v1.0.0-stage ``` 4. Firewall: stage/prod-app → test-server **443** (Registry API). См. чек-лист §20 (stage/prod). ### 9.4. Pipeline backend (`.gitea/workflows/build.yml`) ```yaml name: backend-ci-cd on: push: tags: - 'backend-v*' pull_request: branches: [main] env: REGISTRY: git.sova.dev IMAGE: git.sova.dev/sova/backend jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: '8.4' extensions: pdo_pgsql, redis, intl, zip, gd - run: composer install --prefer-dist - run: composer phpunit - run: composer audit build-and-push: needs: test if: startsWith(github.ref, 'refs/tags/backend-v') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Parse tag id: meta run: | TAG="${GITHUB_REF#refs/tags/}" echo "full_tag=$TAG" >> $GITHUB_OUTPUT # backend-v1.2.3-test → env=test, version=1.2.3 echo "env=$(echo $TAG | sed -E 's/backend-v([0-9.]+)-([a-z]+)/\2/')" >> $GITHUB_OUTPUT echo "version=$(echo $TAG | sed -E 's/backend-v([0-9.]+).*/\1/')" >> $GITHUB_OUTPUT - name: Docker login run: echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login $REGISTRY -u sova-ci --password-stdin - name: Build run: | docker build -f Dockerfile \ -t $IMAGE:${{ steps.meta.outputs.full_tag }} \ -t $IMAGE:${{ steps.meta.outputs.version }} \ . - name: Push run: | docker push $IMAGE:${{ steps.meta.outputs.full_tag }} docker push $IMAGE:${{ steps.meta.outputs.version }} deploy-gitops: needs: build-and-push runs-on: ubuntu-latest steps: - name: Bump image tag in sova-deploy (with retry) env: DEPLOY_KEY: ${{ secrets.SOVA_DEPLOY_KEY }} run: | git clone git@gitea.sova.dev:sova/sova-deploy.git cd sova-deploy ENV="${{ steps.meta.outputs.env }}" TAG="${{ steps.meta.outputs.full_tag }}" git config user.email "ci-bot@sova.dev" git config user.name "sova-ci" MAX_RETRIES=5 for attempt in $(seq 1 $MAX_RETRIES); do git pull --rebase origin main yq -i ".image.tag = \"${TAG}\"" "apps/backend/values-${ENV}.yaml" git add "apps/backend/values-${ENV}.yaml" git diff --cached --quiet && { echo "No changes"; exit 0; } git commit -m "chore(backend): bump ${ENV} to ${TAG}" if git push origin main; then echo "Push OK on attempt ${attempt}" exit 0 fi echo "Push failed (race?), retry ${attempt}/${MAX_RETRIES}..." git reset --hard HEAD~1 sleep $((attempt * 2)) done echo "Failed to push after ${MAX_RETRIES} attempts" exit 1 ``` > **Гонка состояний:** если два пайплайна пушат в `sova-deploy` одновременно, второй получит `non-fast-forward`. Цикл `pull --rebase` + retry обязателен (см. выше). Альтернатива — [gitops-promoter](https://github.com/argoproj-labs/gitops-promoter) или ArgoCD Image Updater без git commit из CI. > ArgoCD подхватит изменение в `sova-deploy` за ~3 мин (poll) или мгновенно через webhook. ### 9.5. Pipeline adminPanel Аналогично, tag pattern: `adminpanel-v1.0.0-test`. Build-arg `VITE_API_BASE_URL` **не использовать** если перешли на runtime `env.js`. --- ## 10. Система тегирования и promotion ### 10.1. Формат тегов (SemVer + контур) ```text {app}-v{MAJOR.MINOR.PATCH}-{env} Примеры: backend-v1.4.2-test → деплой в sova-test backend-v1.4.2-stage → деплой в sova-stage backend-v1.4.2-prod → деплой в sova-prod adminpanel-v2.0.0-test ``` **Правила:** | Действие | Кто | Тег | |----------|-----|-----| | Feature готова к QA | разработчик | `backend-v1.5.0-test` | | QA passed → stage | тимлид / CI manual | `backend-v1.5.0-stage` | | Stage passed → prod | release manager | `backend-v1.5.0-prod` | | Hotfix prod | release manager | `backend-v1.4.3-prod` | ### 10.2. Promotion flow (без пересборки образа) ```mermaid sequenceDiagram participant Dev as Developer participant Gitea as Gitea CI participant CR as Container Registry participant Deploy as sova-deploy repo participant Argo as ArgoCD Dev->>Gitea: tag backend-v1.5.0-test Gitea->>Gitea: test + build Gitea->>CR: push backend:backend-v1.5.0-test Gitea->>Deploy: values-test.yaml tag=bump Argo->>Deploy: poll / webhook Argo->>Argo: sync sova-test Note over Dev,Argo: После QA — promotion без rebuild Dev->>Gitea: tag backend-v1.5.0-stage Gitea->>CR: retag same digest OR promote manifest Gitea->>Deploy: values-stage.yaml tag=bump Argo->>Argo: sync sova-stage ``` **Promotion policy:** 1. **Test:** автоматически по тегу `*-test`. 2. **Stage:** автоматически по тегу `*-stage` **или** manual approval job в CI. 3. **Prod:** только manual approval + тег `*-prod` + ArgoCD project restriction. ### 10.3. ArgoCD Projects — RBAC по контурам ```yaml # argocd/projects/sova-project.yaml apiVersion: argoproj.io/v1alpha1 kind: AppProject metadata: name: sova namespace: argocd spec: destinations: - namespace: sova-test server: https://kubernetes.default.svc - namespace: sova-stage server: https://kubernetes.default.svc - namespace: sova-prod server: https://kubernetes.default.svc sourceRepos: - git@gitea.sova.dev:sova/sova-deploy.git roles: - name: test-deployer policies: - p, proj:sova:test-deployer, applications, sync, sova/backend-test, allow - name: prod-deployer policies: - p, proj:sova:prod-deployer, applications, sync, sova/backend-prod, allow ``` ### 10.4. Rollback 1. **ArgoCD UI:** History → Rollback (откат Git commit в sova-deploy). 2. **Или:** новый тег с предыдущей версией `backend-v1.4.1-test`. 3. **Или:** `kubectl rollout undo` — аварийно, но GitOps desync. **Важно:** rollback откатывает **Deployment/ConfigMap**, но **не миграции БД**. Если новая версия применила `doctrine:migrations:migrate`, откат кода оставит схему «впереди» кода — возможны runtime-ошибки. Варианты: - `php bin/console doctrine:migrations:execute --down 'VersionXXX'` вручную; - restore PostgreSQL из snapshot (test); - forward-fix: новый релиз с совместимой миграцией. См. §7.2.1. --- ## 11. Мониторинг по контурам ### 11.1. Замена текущего `monitoring/` | Docker Compose | Kubernetes | |----------------|------------| | `prom/prometheus` | Prometheus Operator (kube-prometheus-stack) | | `grafana/grafana-enterprise` | Grafana subchart | | `node-exporter` | DaemonSet (встроен) | | `php-fpm-exporter` | ServiceMonitor на backend pods | | nginx `:88/metrics` | ServiceMonitor на ingress-nginx | | rules `*.rules` (отсутствуют) | **PrometheusRule** CR — создать | ### 11.2. Labels для multi-env Все ServiceMonitor / Pod labels: ```yaml labels: app: backend env: test # test | stage | prod team: sova ``` Grafana dashboards — переменная `$env` для фильтрации. ### 11.3. Минимальный набор алертов ```yaml # PrometheusRule — backend-down groups: - name: sova-apps rules: - alert: BackendDown expr: kube_deployment_status_replicas_available{deployment="backend", namespace=~"sova-.*"} == 0 for: 2m labels: severity: critical annotations: summary: "Backend недоступен в {{ $labels.namespace }}" - alert: HighErrorRate expr: rate(nginx_ingress_controller_requests{status=~"5..", ingress=~"backend.*"}[5m]) > 0.1 for: 5m labels: severity: warning ``` ### 11.4. Логи (Loki) - Promtail DaemonSet собирает логи всех pods в `sova-*` namespaces. - Grafana datasource Loki + Explore. - Замена `docker logs` для отладки. **Retention и диск (обязательно):** без лимитов Loki съест оставшееся место на NVMe. ```yaml # kube-prometheus-stack / loki values (фрагмент) loki: limits_config: retention_period: 168h # 7 дней compactor: retention_enabled: true storage: # local-path PVC с явным sizeLimit size: 10Gi ``` На single-node 200 GB: Loki **≤10–20 GB**, Prometheus **≤15 GB**, registry **≤50 GB** — остальное под PG и образы. ### 11.5. External monitoring (Bitrix server) Сохранить scrape `192.168.2.11` из `monitoring/prometheus/prometheus.yml` как **additionalScrapeConfigs** в Helm values — если Bitrix-сервер доступен из k8s network. --- ## 12. Пошаговый план внедрения (фазы) ### Фаза 0 — Подготовка (1–2 дня) - [ ] Заказ/получение сервера, SSH доступ, публичный IP - [ ] **Remote Terraform state:** bucket Yandex Object Storage / S3 / Terraform Cloud (§5.1) - [ ] Создать организацию/репы в Gitea (или поднять Gitea временно через docker на сервере) - [ ] Зафиксировать плейсхолдер домена `sova.dev` в `/etc/hosts` для локальной проверки - [ ] Написать production Dockerfile для backend и adminPanel - [ ] Вынести `apps/` в отдельные Gitea-репозитории (если ещё не сделано) ### Фаза 1 — Bootstrap сервера + k3s (1 день) - [ ] Terraform: `terraform apply` модуль k3s (state во **внешнем** backend) - [ ] Проверка: `kubectl get nodes` → Ready - [ ] Sealed-secrets controller - [ ] **Бэкап Sealed Secrets master key** → offline vault (§6.4) ### Фаза 2 — Platform (2–3 дня) - [ ] cert-manager + ClusterIssuer (staging) - [ ] ingress-nginx с `type: LoadBalancer` (k3s ServiceLB, §5.3) - [ ] ArgoCD + Ingress `argocd.sova.dev` - [ ] kube-prometheus-stack + Grafana `grafana.sova.dev` - [ ] Loki stack с **retention 7d** и лимитом PVC (§11.4) ### Фаза 3 — Gitea + Registry + CI (2–3 дня) - [ ] Gitea Helm + `git.sova.dev` - [ ] act_runner registered (Kaniko/Buildah, **requests/limits**, §9.2) - [ ] **Registry retention policy** — cleanup старых тегов (§9.3) - [ ] Создать repos: sova-backend, sova-adminpanel, sova-deploy, sova-platform - [ ] CI secrets: REGISTRY_PASSWORD, SOVA_DEPLOY_KEY - [ ] Первый успешный pipeline: build hello-world image - [ ] deploy-gitops с **retry** на git push (§9.4) ### Фаза 4 — Test contour (3–5 дней) - [ ] PostgreSQL (main + cabinet) + MySQL Bitrix + Redis в `sova-data-test` - [ ] Mock-слой в `sova-mocks`: MIS (WireMock), Mailpit, captcha-mock - [ ] SealedSecrets для `backend-env-test` (все URL → test DB + mocks, см. [backend-external-services.md §5.2](./backend-external-services.md)) - [ ] Helm chart backend + adminpanel - [ ] ArgoCD Applications: backend-test, adminpanel-test, mocks-test - [ ] Tag `backend-v0.1.0-test` → end-to-end deploy - [ ] Smoke: login JWT, `/news/list`, anonymous-reserve через mock MIS + mock captcha - [ ] CronJobs: сначала `ClearScheduleCacheCommand`; sync — после mock/Bitrix seed ### Фаза 5 — Stage contour (2–3 дня, **отдельный сервер**) - [ ] Terraform: `stage-server` — k3s single-node (§3.3) - [ ] PostgreSQL **single instance** на stage-сервере - [ ] Pull образов с Gitea Registry (test-сервера) - [ ] values-stage.yaml, ArgoCD apps на stage - [ ] Tag `backend-v0.1.0-stage` ### Фаза 6 — Prod contour (**prod-app + 2× prod-db**, §3.4, §7.6) - [ ] Terraform: `prod-db-1`, `prod-db-2` — Patroni + sync replication + HAProxy VIP - [ ] Terraform: `prod-app` — k3s, apps **без** PG pod - [ ] Миграция данных с текущей prod-VM (§7.6.8) - [ ] `DATABASE_URL` → VIP / PgBouncer, **не** IP одной db-VM - [ ] Failover drill: stop primary → backend восстанавливает запись < 3 min - [ ] pg_dump + WAL → Object Storage; restore drill - [ ] Production ClusterIssuer, HPA, PDB на prod-app - [ ] On-call alerts (PG replication, Patroni leader, disk) ### Фаза 7 — Расширение приложений - [ ] cabinet (php82 chart) - [ ] sovamed (nextjs deployment) - [ ] kiosk static site --- ## 13. Маппинг Docker Compose → Kubernetes | Compose service | K8s resource | Примечание | |-----------------|--------------|------------| | nginx + php84 | Deployment backend (sidecar) | один pod | | adminPanel dist | Deployment adminpanel | nginx static | | pgsql | StatefulSet / Helm postgresql | per env (main + cabinet DB) | | mysql-bitrix | Helm mysql | per env, seed `local/mysql-bitrix/init/` | | redis | Helm redis | per env | | mock HTTP (local dead URLs) | Deployments в `sova-mocks` | test only: MIS, captcha, mailpit | | nextjs | Deployment sovamed | port 3001 | | prometheus + grafana | kube-prometheus-stack | | | jenkins | **удалить** | Gitea Actions | | cron scripts | CronJob | | | certbot | cert-manager | | | internal-network | NetworkPolicy | deny cross-env | --- ## 14. NetworkPolicy (изоляция контуров) Политики **разные per contour**. Пример ниже — **test** (БД внутри k8s). **Prod** — PostgreSQL **вне** кластера на prod-db-VM (§7.6). ### 14.1. Test (`sova-test`) ```yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: backend-ingress-only namespace: sova-test spec: podSelector: matchLabels: app: backend policyTypes: [Ingress, Egress] ingress: - from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: ingress egress: - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: sova-data-test ports: - port: 5432 - port: 3306 - port: 6379 - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: sova-mocks ports: - port: 8080 - port: 1025 - port: 8025 - to: # DNS - namespaceSelector: {} ports: - port: 53 protocol: UDP # Stage: egress :443 к whitelist (MIS sandbox, SMTP). Prod: см. §14.2. ``` ### 14.2. Prod (`sova-prod`) — PostgreSQL вне k8s На prod **нельзя** копировать test-политику с `namespaceSelector: sova-data-test` — backend не достучится до Patroni VIP. ```yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: backend-egress-prod namespace: sova-prod spec: podSelector: matchLabels: app: backend policyTypes: [Egress] egress: # PostgreSQL HA — VIP и/или прямые IP db-VM (private VLAN) - to: - ipBlock: cidr: 10.0.1.20/32 # Keepalived VIP (pg-prod.sova.internal) - ipBlock: cidr: 10.0.1.10/32 # prod-db-1 - ipBlock: cidr: 10.0.1.11/32 # prod-db-2 ports: - port: 5432 # session / migrate / messenger - port: 6432 # PgBouncer transaction (если на db-VM) # MySQL Bitrix, Redis — на prod-app (in-cluster Service) или ipBlock - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: sova-data-prod ports: - port: 3306 - port: 6379 # Registry на test-сервере - to: - ipBlock: cidr: 203.0.113.5/32 # IP test-сервера (git.sova.dev) ports: - port: 443 # MIS, SMTP, captcha — prod whitelist - to: - ipBlock: cidr: 0.0.0.0/0 except: - 10.0.0.0/8 - 172.16.0.0/12 - 192.168.0.0/16 ports: - port: 443 - to: {} ports: - port: 53 protocol: UDP ``` > IP подставить из Terraform tfvars / private VLAN Selectel. DNS egress обязателен. ### 14.3. Stage Как test (БД in-cluster) **плюс** egress `:443` к test Registry и sandbox-интеграциям. --- ## 15. Безопасность 1. **SSH:** только ключи, fail2ban, нестандартный порт опционально. 2. **k3s API:** не expose 6443 публично; kubectl через SSH tunnel. 3. **Secrets:** SealedSecrets, rotation раз в квартал. 4. **Registry:** private, robot account для CI. 5. **ArgoCD:** admin password → SSO Gitea; project RBAC. 6. **Pod security:** `runAsNonRoot`, drop capabilities, readOnlyRootFilesystem где возможно. 7. **Backup:** pg_dump nightly → S3/MinIO; etcd snapshot k3s. --- ## 16. Runbook: первый деплой backend test ```bash # 1. На dev машине — создать тег git tag backend-v0.1.0-test git push origin backend-v0.1.0-test # 2. CI (Gitea Actions) автоматически: # - тесты # - docker build + push git.sova.dev/sova/backend:backend-v0.1.0-test # - commit в sova-deploy apps/backend/values-test.yaml # 3. ArgoCD (или дождаться auto-sync) argocd app sync backend-test # 4. Проверка curl -s https://api.test.sova.dev/ | jq . curl -s https://api.test.sova.dev/news/list?page=1&perPage=2 # 5. Миграции (если PreSync hook не настроен) kubectl -n sova-test exec -it deploy/backend -- php bin/console doctrine:migrations:status ``` --- ## 17. Runbook: promotion test → stage ```bash # Вариант A — новый тег (рекомендуется) git tag backend-v0.1.0-stage git push origin backend-v0.1.0-stage # Вариант B — manual bump без rebuild (same image digest) cd sova-deploy yq -i '.image.tag = "backend-v0.1.0-test"' apps/backend/values-stage.yaml git commit -am "promote backend 0.1.0 test→stage" git push argocd app sync backend-stage ``` --- ## 18. Связь с локальной разработкой | Локально (`make local-up`) | Test K8s | |----------------------------|----------| | http://localhost:8081 | https://api.test.sova.dev | | http://localhost:3211 | https://admin.test.sova.dev | | `local/.env.local` | SealedSecret `backend-env-test` | | `mock-*.local` (dead URLs) | Services в `sova-mocks` с реальными ответами | | `make local-smoke` | CI job `smoke-test-test` после deploy | | postgres :15432 | pg-test.sova-data-test:5432 | | mysql-bitrix :13306 | mysql-bitrix-test.sova-data-test:3306 | Разработчик **не деплоит руками** — только теги и MR в Gitea. --- ## 19. Риски и mitigations | Риск | Mitigation | |------|------------| | Terraform state на упавшем сервере | Remote backend **вне** k3s (§5.1) | | Sealed Secrets key потерян при DR | Бэкап master key в offline vault (§6.4) | | Registry заполнил диск | Retention policy + CronJob GC (§9.3) | | CI runner «положил» кластер | requests/limits, Kaniko, taint node для CI (§9.2) | | Loki заполнил диск | retention 7d + PVC limit (§11.4) | | Гонка CI в sova-deploy | pull --rebase + retry loop (§9.4) | | Rollback не откатывает миграции | §7.2.1, manual down-migration или PG restore | | PG single point of failure на prod | **2 db-VM**, sync replication + Patroni (§7.6) | | Commit latency из-за sync replica | RTT db-1↔db-2 < 2 ms (один DC); мониторить lag | | Failover 503 на API | PgBouncer + pod restart policy; health checks | | **Ребут prod-app demote PG** | Witness **не** на prod-app; иначе `patronictl pause` перед обслуживанием (§7.6.3) | | etcd witness на prod-app | Split-brain / read-only БД при restart k3s → **отдельная witness-VM** | | stage/prod не pull образов | `imagePullSecrets` + firewall к test Registry (§9.3) | | PgBouncer ломает migrations | `DATABASE_MIGRATE_URL` на :5432 session, API на :6432 transaction (§7.6.5) | | Prod 12 GB RAM недостаточно для k3s+HA | §3.4: prod-app 16 GB, db nodes отдельно | | Один сервер — SPOF | Бэкапы; test/stage/prod **разные** серверы | | RAM не хватит на prod-app | §3.4: min 16 GB для k3s + apps; PG на отдельных db-VM | | Bitrix MySQL недоступен из k8s | **Test:** свой MySQL в `sova-data-test`; prod — VPN/peering | | Вызов prod MIS/SMS из test | NetworkPolicy + env только на `sova-mocks`; без egress :443 в test | | Hardcoded widget.sovamed.ru в cron | Рефакторинг на env или fixtures на mock | | Пустой backend Dockerfile | **Блокер фазы 0** | | Gitea + мониторинг + apps на одном CPU | Мониторинг «lite» values, 30s scrape | | Domain не готов | sslip.io / /etc/hosts / mkcert | --- ## 20. Чек-лист «готовность test-контура» - [ ] `kubectl get nodes` — Ready - [ ] `argocd.sova.dev` открывается, apps Synced - [ ] `git.sova.dev` — репы созданы, CI runner online - [ ] `grafana.sova.dev` — dashboards показывают backend pods - [ ] `api.test.sova.dev/news/list` — 200 JSON - [ ] `admin.test.sova.dev/login` — SPA грузится, API calls работают - [ ] Tag `backend-v*-test` → auto deploy < 10 min - [ ] Rollback через ArgoCD проверен - [ ] Mock MIS: `/api/reservation/intervals` и `anonymous-reserve` — 200 - [ ] Mailpit: письмо из `/service/sendmail` видно в UI (Basic Auth или port-forward) - [ ] Backend **не** резолвит prod `widget.sovamed.ru` / sms.ru (проверка egress) - [ ] CronJob `ClearScheduleCacheCommand` отработал; sync CronJobs — по готовности mock/Bitrix - [ ] Sealed Secrets master key **забэкаплен** offline (не только в кластере) - [ ] Terraform state в **внешнем** S3/TFC - [ ] Registry retention настроен, диск < 70% занят - [ ] Loki retention 7d, PVC в пределах лимита - [ ] Secrets не лежат в Git plaintext (только SealedSecrets) ### Чек-лист stage/prod (дополнительно) - [ ] `imagePullSecrets` (`gitea-registry-secret`) в `sova-stage` / `sova-prod` — pull с test Registry (§9.3) - [ ] `values-*.yaml`: `imagePullSecrets` + `image.repository: git.sova.dev/sova/...` - [ ] NetworkPolicy prod: egress `ipBlock` к Patroni VIP / prod-db IP (§14.2), не только namespace ### Чек-лист prod (дополнительно) - [ ] Patroni: 2 узла, `sync_state = sync` - [ ] Failover drill: primary stop → writes OK через VIP - [ ] `DATABASE_URL` (6432 transaction) и `DATABASE_MIGRATE_URL` (5432 session) в SealedSecret - [ ] Witness etcd на **отдельной** VM, не на prod-app - [ ] pg_dump restore проверен --- ## 21. Дальнейшее развитие 1. **Read replica** для тяжёлых отчётов (ro endpoint `:5433`) — не путать с sync standby. 2. **Managed K8s** (YC/Selectel) для prod-app — PG HA может остаться на Patroni db-VM. 3. **Cabinet + sovamed** — новые Helm charts по образцу backend. 4. **Service Mesh** — только при необходимости mTLS между сервисами. 5. **Preview environments** — ArgoCD Application per MR (на test-сервере). --- ## 22. Связанные документы - [**Terraform: prod-db HA**](../../terraform/modules/prod-db/README.md) — Patroni + etcd + VIP, SSH-only - [`DEPLOYMENT_PLAN.md`](../../DEPLOYMENT_PLAN.md) — детали Dockerfile, Yandex Cloud variant - [`docs/infrastructure/docker.md`](./docker.md) — текущий Docker Compose (если есть) - [`docs/apps/backend-scenarios/`](../apps/backend-scenarios/index.md) — бизнес-потоки backend - [`docs/flows.md`](../flows.md) — потоки данных - [`monitoring/prometheus/prometheus.yml`](../../monitoring/prometheus/prometheus.yml) — текущие scrape targets --- *Документ версии 1.1. Контуры test/stage/prod — **разные серверы**. Prod PG: sync HA (§7.6). Плейсхолдер домена: `sova.dev`.*