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

14 KiB
Raw Blame History

🏭 Инфраструктура

Docker Compose (локальная разработка)

Сервисы

services:
  backend:     # Spring Boot (Java 17), порт 8080
  frontend:    # Apache httpd:alpine, порт 80
  db:          # PostgreSQL alpine3.23, порт 5432

Сеть

Все сервисы работают в Docker-сети proxy (external). Перед первым запуском:

docker network create proxy

Переменные окружения

Файл .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-17mvn package
  2. Этап запуска: eclipse-temurin:17-jre-alpinejava -jar app.jar

Dockerfile (Frontend)

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:

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 также требуется:

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:

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. Эти действия требуют полномочий оператора и не выполняются автоматически.

Liveness и readiness probes backend

TCP probe подтверждает только открытый порт и не отражает готовность обязательных tenant-БД. В production Deployment необходимо использовать раздельные HTTP probes:

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. отрендерить манифесты и выполнить серверную проверку без применения:
kubectl kustomize ../k8s >/dev/null
kubectl apply --dry-run=server -k ../k8s
  1. отдельно проверить полномочия фактического ServiceAccount:
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

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)

Архитектура мониторинга

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)