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