Files
magistr/docs/INFRASTRUCTURE.md
2026-07-16 22:56:43 +03:00

308 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 🏭 Инфраструктура
## Docker Compose (локальная разработка)
### Сервисы
```yaml
services:
backend: # Spring Boot (Java 17), порт 8080
frontend: # Apache httpd:alpine, порт 80
db: # PostgreSQL alpine3.23, порт 5432
```
### Сеть
Все сервисы работают в Docker-сети `proxy` (external). Перед первым запуском:
```bash
docker network create proxy
```
### Переменные окружения
Файл `.env` в корне проекта:
```env
POSTGRES_USER=myuser
POSTGRES_PASSWORD=replace-with-local-password
POSTGRES_DB=app_db
JWT_SECRET=replace-with-random-jwt-secret-minimum-32-bytes
JWT_ACCESS_TOKEN_TTL=15m
JWT_REFRESH_TOKEN_TTL=7d
```
`POSTGRES_PASSWORD` обязателен для `docker compose up`: пароль не хранится в `compose.yaml`. `JWT_SECRET` должен быть случайным секретом длиной минимум 32 байта. В продакшене секреты задаются через Kubernetes Secret, а не через коммитимые файлы.
Встроенного JWT fallback в приложении нет. Отсутствующее, короткое или шаблонное значение
останавливает запуск; профиль `production` также требует Secure refresh-cookie.
Локальный `backend/tenants.json` тоже не коммитится. Для ручного запуска backend вне Docker можно взять `backend/tenants.example.json`, создать рядом `tenants.json` и подставить локальный пароль.
### Dockerfile (Backend)
Backend собирается через multi-stage сборку Maven:
1. Этап сборки: `maven:3.9-eclipse-temurin-17``mvn package`
2. Этап запуска: `eclipse-temurin:17-jre-alpine``java -jar app.jar`
### Dockerfile (Frontend)
```dockerfile
FROM httpd:alpine
COPY . /usr/local/apache2/htdocs/
RUN chown -R www-data:www-data /usr/local/apache2/htdocs/
```
---
## Kubernetes (продакшн)
### Расположение конфигурации
Ожидаемое расположение production-манифестов: `../k8s/`. В текущей рабочей копии этот
внешний каталог недоступен, поэтому приведённые ниже изменения являются обязательным
runbook для оператора, а не утверждением о применённом или проверенном состоянии кластера.
### Ключевые ресурсы
| Ресурс | Тип | Описание |
|--------|-----|----------|
| `backend` | Deployment | Spring Boot приложение |
| `frontend` | Deployment | Apache httpd |
| `tenants-secret` | Внешний Secret | JSON-список tenant-подключений; значения отсутствуют в манифестах |
### JWT настройки
`app-config` задаёт TTL access/refresh-токенов и признак Secure-cookie:
- `JWT_ACCESS_TOKEN_TTL=15m`
- `JWT_REFRESH_TOKEN_TTL=7d`
- `JWT_REFRESH_COOKIE_SECURE=true`
`app-secret` задаёт `JWT_SECRET`. Этот Secret не создаётся файлами `../k8s/`: его заранее
предоставляет внешний secret manager или оператор. Deployment явно включает профиль
`production`, поэтому небезопасная cookie-конфигурация останавливает startup.
### Secrets для приложения и тенантов
Обязательные внешние объекты:
| Secret | Назначение |
|---|---|
| `app-secret` | JWT и fallback credentials backend |
| `tenants-secret` | Полный `tenants.json` с credentials tenant-БД |
| `otel-postgres-secret` | Credentials PostgreSQL receivers для OTel Collector |
| `gitea-registry` | Доступ Kubernetes к registry |
Backend ожидает ключ `tenants.json` из Secret `tenants-secret` по пути
`/config/tenants.json`. Чтобы kubelet доставлял последующие обновления Secret, необходимо
монтировать каталог `/config`, а не отдельный файл через `subPath`:
```yaml
volumeMounts:
- name: tenants-config
mountPath: /config
readOnly: true
volumes:
- name: tenants-config
secret:
secretName: tenants-secret
optional: false
items:
- key: tenants.json
path: tenants.json
```
В production для backend также требуется:
```yaml
env:
- name: TENANTS_CONFIG_REQUIRED
value: "true"
```
При обязательной конфигурации отсутствие, пустой список или ошибка разбора `tenants.json`
снимают readiness; резервная H2-БД не считается готовой tenant-БД.
При добавлении тенанта через API:
1. `TenantLifecycleService` создаёт отдельный candidate pool, проверяет соединение и Flyway
2. `KubernetesTenantSecretUpdater` выполняет `GET` актуального Secret и применяет к списку
только upsert запрошенного домена
3. полный объект Secret отправляется условным `PUT` с текущим `metadata.resourceVersion`;
при `409 Conflict` выполняются повторное чтение и повторное применение своей мутации
4. `TenantRoutingDataSource` атомарно публикует candidate только после успешной либо
подтверждённой повторным чтением персистенции
5. старый pool перестаёт принимать новые запросы и закрывается после drain/grace timeout
6. `TenantConfigWatcher` на остальных pod видит обновлённый файл и каждые 30 секунд
применяет добавление или удаление домена
Удаление использует тот же persistence-first порядок и применяет к актуальному Secret только
remove указанного домена. Число попыток записи ограничено тремя, задержка между повторами
возрастает, а идемпотентное состояние не записывается повторно.
Если после подтверждённой записи Secret локальная активация или удаление завершаются ошибкой,
backend восстанавливает прежний снимок только пока Secret сохраняет `resourceVersion` этой
записи. Более новое межподовое изменение останавливает компенсацию и не перезаписывается.
Неопределённый сетевой результат сначала сверяется повторным `GET`; совпавшее целевое
состояние принимается без квитанции, допускающей небезопасный автоматический откат.
Watcher пока не заменяет подключение существующего домена при изменении URL или credentials.
Такое изменение сбрасывает readiness, но полное применение на другом pod относится к
проблеме №13. До её исправления нельзя считать watcher механизмом полной синхронизации всех
полей `TenantConfig`.
Kubernetes-клиент доверяет только service-account CA и проверяет hostname API server.
Поскольку реализация использует HTTP `GET` и `PUT`, минимальная Role должна разрешать только
Kubernetes verbs `get` и `update` для одного существующего Secret:
```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: backend-tenant-secret
namespace: magistr
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["tenants-secret"]
verbs: ["get", "update"]
```
`patch`, `list`, `watch`, `create` и `delete` текущему Kubernetes-клиенту не нужны. RoleBinding
должен связывать эту Role с фактическим `spec.template.spec.serviceAccountName` backend в
namespace `magistr`. Secret должен существовать заранее и не иметь `immutable: true`.
Создание, ротация, отзыв скомпрометированных значений и очистка истории описаны в
[`SECURITY_RUNBOOK.md`](SECURITY_RUNBOOK.md). Эти действия требуют полномочий оператора и
не выполняются автоматически.
### Liveness и readiness probes backend
TCP probe подтверждает только открытый порт и не отражает готовность обязательных tenant-БД.
В production Deployment необходимо использовать раздельные HTTP probes:
```yaml
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
```
Liveness зависит только от состояния процесса. Readiness возвращает `200 UP` после успешных
миграций и свежей успешной проверки всех обязательных tenant-БД, иначе — `503 DOWN` и pod
исключается из приёма трафика без перезапуска. Actuator не публикует components, домены,
JDBC URL или credentials.
Параметры `initialDelaySeconds`, `periodSeconds`, `timeoutSeconds` и `failureThreshold` нужно
согласовать с текущим Deployment; замена механизма probe не подтверждает автоматически
корректность этих порогов.
### Внешние действия для production Kubernetes
Каталог `../k8s/` не изменялся и не проверялся в рамках текущей рабочей копии. Оператору после
получения доступа к нему необходимо:
1. обновить backend Deployment: directory mount без `subPath`,
`TENANTS_CONFIG_REQUIRED=true` и две HTTP probes;
2. добавить либо сузить Role до `get`/`update` одного `tenants-secret` и проверить RoleBinding
фактического ServiceAccount;
3. убедиться, что внешний `tenants-secret` существует, содержит непустой ключ `tenants.json`
и допускает обновление;
4. отрендерить манифесты и выполнить серверную проверку без применения:
```bash
kubectl kustomize ../k8s >/dev/null
kubectl apply --dry-run=server -k ../k8s
```
5. отдельно проверить полномочия фактического ServiceAccount:
```bash
BACKEND_SERVICE_ACCOUNT='укажите-фактический-service-account'
kubectl auth can-i get secret/tenants-secret \
--as="system:serviceaccount:magistr:${BACKEND_SERVICE_ACCOUNT}" -n magistr
kubectl auth can-i update secret/tenants-secret \
--as="system:serviceaccount:magistr:${BACKEND_SERVICE_ACCOUNT}" -n magistr
```
Применение манифестов, rollout и проверка реальных pod являются отдельными внешними
операциями и без разрешения автоматически не выполняются.
### Обновление backend
```bash
kubectl rollout restart deployment backend -n magistr
```
---
## Caddy (реверс-прокси)
**Расположение:** `../caddy-proxy/` для локальной разработки, в продакшене - отдельный сервис
В продакшене Caddy обрабатывает входящий трафик для `*.zuev.company`:
- Автоматическое получение TLS-сертификатов (Let's Encrypt)
- Маршрутизация `/api/*` → backend:8080
- Маршрутизация статики → frontend:80
---
## CI/CD (Gitea Actions)
### Пайплайн сборки Docker-образов
Расположение: `.gitea/workflows/docker-build.yaml`
Основные шаги:
1. Checkout кода
2. Login в Docker Registry
3. Build + Push образов (`backend`, `frontend`)
4. Генерация меток через `docker/metadata-action`
---
## Мониторинг (SigNoz + OpenTelemetry)
### Архитектура мониторинга
```mermaid
graph LR
Backend["Spring Boot"] -->|OTLP gRPC| Collector["OTel Collector"]
Frontend["JS (otel.js)"] -->|OTLP HTTP| Collector
Collector --> SigNoz["SigNoz"]
Collector -->|"Метрики PostgreSQL"| PgExporter["pg_exporter"]
```
### Интеграция Backend
Backend отправляет через OpenTelemetry:
- **Логи** — через Logback + OTLP exporter
- **Трейсы** — автоинструментация Spring Boot
- **Метрики** — JVM метрики, HTTP метрики
Tenant ID добавляется в:
- MDC (логи): `MDC.put("tenant.id", tenant)`
- Span атрибуты: `Span.current().setAttribute("tenant.id", tenant)`
### Интеграция Frontend
Файл `admin/js/otel.js` — клиентская телеметрия:
- Метрики производительности страниц
- Трейсы пользовательских действий
PostgreSQL receivers collector получают endpoint и credentials только из
`otel-postgres-secret`; ConfigMap collector содержит лишь `${env:...}` ссылки.
### Дашборды SigNoz
- JVM Dashboard (Heap, GC, Threads)
- PostgreSQL Dashboard (Connections, Queries)
- HTTP Dashboard (Requests, Latency, Errors)