74 KiB
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. Критические пробелы (блокеры)
apps/backend/Dockerfile— пустой (нужен multistage build).apps/adminPanel/Dockerfile— отсутствует.- Jenkins pipelines — scaffold с
registry.example.com, shared library не в репо. - PostgreSQL prod использует pg_cron, mysql_fdw, firebird_fdw — проверить нужность в test.
- Bitrix MySQL — cabinet/backend зависят; для test можно mock/отдельный pod MySQL.
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. Принципы
- Infrastructure as Code: Terraform поднимает сервер/OS/k3s; Helm + ArgoCD — всё внутри кластера.
- GitOps: единственный источник истины деплоя — репозиторий
sova-deploy(неkubectl applyруками). - Immutable artifacts: образы по тегам в Container Registry; rollback = смена тега в Git.
- Контуры изолированы: test / stage / prod — разные физические серверы, разные БД, разные secrets.
- k3s single-node на каждом сервере на старте; prod apps и prod PG HA могут быть на 2–3 серверах (§3.4, §7.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 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, манифесты деплоя:
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 должен:
- Отключить swap, настроить
br_netfilter, iptables. - Установить k3s:
curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION=... sh -. - Положить kubeconfig в
/root/.kube/config. - Установить
kubectl,helm. - Включить Traefik disable (
--disable traefik) — используем ingress-nginx. - Включить 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/nginxedge. - 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:
kubeseal→ зашифрованный SealedSecret в Git.- 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
При восстановлении кластера:
- Установить sealed-secrets controller.
- Применить backup Secret до деплоя приложений с SealedSecrets.
- Проверить:
kubectl get sealedsecret -A→ статусSynced. - Только потом включать 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-слоя:
- Namespace
sova-mocks— Helm releases:mis-mock(WireMock + JSON fixtures),mailpit,captcha-mock,bitrix-http-mock. - Env backend test:
MIS_URL,BITRIX_URL,SMARTCAPTCHA_URL,MAILER_DSN→ Cluster DNS (*.sova-mocks.svc.cluster.local). - Репозиторий
sova-mocks(рядом сsova-deploy) — fixtures для расписания и anonymous-reserve. - Техдолг до выката: 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:
- Backend пишет только в primary (один connection string).
- Primary перед
COMMITждёт подтверждения от sync replica, что WAL записан (synchronous_commit = on). - Данные гарантированно на обоих узлах после успешного commit.
- При падении primary Patroni / CNPG / Managed PG promote replica → новый primary за 30–120 сек.
- Backend переподключается через стабильный endpoint (PgBouncer, VIP, CNPG
-rwService) — не напрямую на 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 (время восстановления) | 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)
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
- Поднять prod-db-1, prod-db-2 (§3.4).
pg_dumpс текущей VM → restore на Patroni primary.- Подключить sync replica, дождаться
pg_stat_replication.sync_state = sync. - Короткое окно maintenance: stop writes на старой VM → final sync → switch
DATABASE_URLна VIP. - Поднять prod-app k3s, направить traffic на новый backend.
- Старую 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
- Helm chart
gitea/giteaв namespacegitea. - Ingress
git.sova.dev, TLS via cert-manager. - Включить Actions (
ENABLED_ACTIONS=true). - Зарегистрировать 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 приватных образов.
- Gitea → Settings → Applications → Generate token (read:package) для robot
sova-pull. - На 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>
- В
values-stage.yaml/values-prod.yaml:
imagePullSecrets:
- name: gitea-registry-secret
image:
repository: git.sova.dev/sova/backend
tag: backend-v1.0.0-stage
- 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:
- Test: автоматически по тегу
*-test. - Stage: автоматически по тегу
*-stageили manual approval job в CI. - 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
- ArgoCD UI: History → Rollback (откат Git commit в sova-deploy).
- Или: новый тег с предыдущей версией
backend-v1.4.1-test. - Или:
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 ≤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) - 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)
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. Безопасность
- SSH: только ключи, fail2ban, нестандартный порт опционально.
- k3s API: не expose 6443 публично; kubectl через SSH tunnel.
- Secrets: SealedSecrets, rotation раз в квартал.
- Registry: private, robot account для CI.
- ArgoCD: admin password → SSO Gitea; project RBAC.
- Pod security:
runAsNonRoot, drop capabilities, readOnlyRootFilesystem где возможно. - 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— Readyargocd.sova.devоткрывается, apps Syncedgit.sova.dev— репы созданы, CI runner onlinegrafana.sova.dev— dashboards показывают backend podsapi.test.sova.dev/news/list— 200 JSONadmin.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. Дальнейшее развитие
- Read replica для тяжёлых отчётов (ro endpoint
:5433) — не путать с sync standby. - Managed K8s (YC/Selectel) для prod-app — PG HA может остаться на Patroni db-VM.
- Cabinet + sovamed — новые Helm charts по образцу backend.
- Service Mesh — только при необходимости mTLS между сервисами.
- Preview environments — ArgoCD Application per MR (на test-сервере).
22. Связанные документы
- Terraform: prod-db HA — Patroni + etcd + VIP, SSH-only
DEPLOYMENT_PLAN.md— детали Dockerfile, Yandex Cloud variantdocs/infrastructure/docker.md— текущий Docker Compose (если есть)docs/apps/backend-scenarios/— бизнес-потоки backenddocs/flows.md— потоки данныхmonitoring/prometheus/prometheus.yml— текущие scrape targets
Документ версии 1.1. Контуры test/stage/prod — разные серверы. Prod PG: sync HA (§7.6). Плейсхолдер домена: sova.dev.