# 🏭 Инфраструктура ## 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)