14 KiB
🏭 Инфраструктура
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:
- Этап сборки:
maven:3.9-eclipse-temurin-17→mvn package - Этап запуска:
eclipse-temurin:17-jre-alpine→java -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=15mJWT_REFRESH_TOKEN_TTL=7dJWT_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:
TenantLifecycleServiceсоздаёт отдельный candidate pool, проверяет соединение и FlywayKubernetesTenantSecretUpdaterвыполняетGETактуального Secret и применяет к списку только upsert запрошенного домена- полный объект Secret отправляется условным
PUTс текущимmetadata.resourceVersion; при409 Conflictвыполняются повторное чтение и повторное применение своей мутации TenantRoutingDataSourceатомарно публикует candidate только после успешной либо подтверждённой повторным чтением персистенции- старый pool перестаёт принимать новые запросы и закрывается после drain/grace timeout
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/ не изменялся и не проверялся в рамках текущей рабочей копии. Оператору после
получения доступа к нему необходимо:
- обновить backend Deployment: directory mount без
subPath,TENANTS_CONFIG_REQUIRED=trueи две HTTP probes; - добавить либо сузить Role до
get/updateодногоtenants-secretи проверить RoleBinding фактического ServiceAccount; - убедиться, что внешний
tenants-secretсуществует, содержит непустой ключtenants.jsonи допускает обновление; - отрендерить манифесты и выполнить серверную проверку без применения:
kubectl kustomize ../k8s >/dev/null
kubectl apply --dry-run=server -k ../k8s
- отдельно проверить полномочия фактического 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
Основные шаги:
- Checkout кода
- Login в Docker Registry
- Build + Push образов (
backend,frontend) - Генерация меток через
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)