Files
docs/infrastructure/k8s-cicd-platform-plan.md
T

74 KiB
Raw Blame History

title
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 (там акцент на Managed K8s в облаке). Этот документ — практический план под сценарий «self-hosted k3s + Gitea + отдельный сервер на контур».


0. TL;DR — что получится в итоге

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

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.52 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 могут быть на 23 серверах (§3.4, §7.6).
  6. Prod PostgreSQL: primary + sync replica + auto failover; test/stage — один инстанс PG достаточно.

2.2. Слои платформы

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: внешние сервисы.


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 80120 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, манифесты деплоя:

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)

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, а здесь.

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.


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 без репликации.

# 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)

# 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 сервера.

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

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

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

# 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
# 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.

# Сразу после установки 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 — планировать раз в год, не при каждом 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

-- Аналог 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
# 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.

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.

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 за 30120 сек.
  5. Backend переподключается через стабильный endpoint (PgBouncer, VIP, CNPG -rw Service) — не напрямую на IP одной VM.
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/envs/prod-db. Серверы заказываются у любого хостера (Selectel и т.д.), модуль не вызывает API провайдера.

7.6.3. Patroni topology (рекомендуемый)

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 (фрагмент):

# postgresql.conf — primary
synchronous_commit = on
synchronous_standby_names = 'FIRST 1 (prod-db-2)'
wal_level = replica
max_wal_senders = 5
# postgresql.conf — replica
hot_standby = on

7.6.4. CloudNativePG (альтернатива, если PG в GitOps)

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:

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
# 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
# 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 (время восстановления) 13 мин (Patroni + k8s rollout) то же
Commit latency +15 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)

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

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 без пересборки:

// 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).

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
# 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.

# 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-теги
# Пример 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):
apiVersion: v1
kind: Secret
metadata:
  name: gitea-registry-secret
  namespace: sova-stage   # или sova-prod
type: kubernetes.io/dockerconfigjson
data:
  .dockerconfigjson: <base64 docker config for git.sova.dev>
  1. В values-stage.yaml / values-prod.yaml:
imagePullSecrets:
  - name: gitea-registry-secret

image:
  repository: git.sova.dev/sova/backend
  tag: backend-v1.0.0-stage
  1. Firewall: stage/prod-app → test-server 443 (Registry API).

См. чек-лист §20 (stage/prod).

9.4. Pipeline backend (.gitea/workflows/build.yml)

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 или 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 + контур)

{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 (без пересборки образа)

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 по контурам

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

labels:
  app: backend
  env: test          # test | stage | prod
  team: sova

Grafana dashboards — переменная $env для фильтрации.

11.3. Минимальный набор алертов

# 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.

# 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 ≤1020 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 (23 дня)

  • 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 (23 дня)

  • 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 (35 дней)

  • 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)
  • 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 (23 дня, отдельный сервер)

  • 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)

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.

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

# 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

# Вариант 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. Связанные документы


Документ версии 1.1. Контуры test/stage/prod — разные серверы. Prod PG: sync HA (§7.6). Плейсхолдер домена: sova.dev.