баг-фикс 30/34
This commit is contained in:
@@ -6,18 +6,20 @@
|
||||
|
||||
```yaml
|
||||
services:
|
||||
backend: # Spring Boot (Java 17), порт 8080
|
||||
frontend: # Apache httpd:alpine, порт 80
|
||||
db: # PostgreSQL alpine3.23, порт 5432
|
||||
backend: # Spring Boot (Java 17), внутренний порт 8080
|
||||
frontend: # Apache httpd, публикует HTTP_PORT (по умолчанию 80)
|
||||
db: # PostgreSQL 16.3, внутренний порт 5432
|
||||
```
|
||||
|
||||
### Сеть
|
||||
|
||||
Все сервисы работают в Docker-сети `proxy` (external). Перед первым запуском:
|
||||
Compose сам создаёт изолированную bridge-сеть `magistr`. Внешняя сеть или отдельно
|
||||
установленный reverse proxy для локального запуска не нужны. Apache раздаёт frontend и
|
||||
проксирует same-origin пути `/api` и `/actuator/health` в backend; backend и PostgreSQL не
|
||||
публикуют порты на хосте.
|
||||
|
||||
```bash
|
||||
docker network create proxy
|
||||
```
|
||||
Данные PostgreSQL сохраняются в именованном томе `postgres_data`. Обычный
|
||||
`docker compose down` не удаляет их; явный `docker compose down -v` выполняет полный сброс.
|
||||
|
||||
### Переменные окружения
|
||||
|
||||
@@ -30,9 +32,29 @@ POSTGRES_DB=app_db
|
||||
JWT_SECRET=replace-with-random-jwt-secret-minimum-32-bytes
|
||||
JWT_ACCESS_TOKEN_TTL=15m
|
||||
JWT_REFRESH_TOKEN_TTL=7d
|
||||
JWT_REFRESH_CLEANUP_RETENTION=30d
|
||||
LOGIN_RATE_MAX_FAILURES=5
|
||||
LOGIN_RATE_ATTEMPT_WINDOW=15m
|
||||
LOGIN_RATE_BASE_BLOCK_DURATION=1m
|
||||
LOGIN_RATE_MAX_BLOCK_DURATION=15m
|
||||
LOGIN_AUDIT_RETENTION=90d
|
||||
TRUSTED_PROXY_CIDRS=172.16.0.0/12
|
||||
OTEL_SDK_DISABLED=true
|
||||
HTTP_PORT=80
|
||||
```
|
||||
|
||||
`POSTGRES_PASSWORD` обязателен для `docker compose up`: пароль не хранится в `compose.yaml`. `JWT_SECRET` должен быть случайным секретом длиной минимум 32 байта. В продакшене секреты задаются через Kubernetes Secret, а не через коммитимые файлы.
|
||||
Начальный шаблон находится в `.env.example`: скопируйте его в игнорируемый Git файл `.env`
|
||||
и заполните пустые секреты. `POSTGRES_PASSWORD` и `JWT_SECRET` обязательны для
|
||||
`docker compose up` и не хранятся в `compose.yaml`; JWT должен быть случайным секретом длиной
|
||||
минимум 32 байта (`openssl rand -base64 48`). `POSTGRES_DB` одновременно задаёт создаваемую
|
||||
БД и входит в `SPRING_DATASOURCE_URL`, поэтому произвольное локальное имя остаётся
|
||||
согласованным. Все JWT TTL/cleanup-переменные и параметры защиты входа передаются
|
||||
backend-контейнеру явно. `TRUSTED_PROXY_CIDRS` должен содержать только сеть фактического
|
||||
reverse proxy: заголовок `X-Forwarded-For` от остальных источников backend игнорирует.
|
||||
Поскольку локальный Compose не запускает OpenTelemetry Collector, SDK по умолчанию отключён;
|
||||
при подключённом Collector задайте `OTEL_SDK_DISABLED=false` и его OTLP endpoint.
|
||||
|
||||
В продакшене секреты задаются через Kubernetes Secret, а не через коммитимые файлы.
|
||||
|
||||
Встроенного JWT fallback в приложении нет. Отсутствующее, короткое или шаблонное значение
|
||||
останавливает запуск; профиль `production` также требует Secure refresh-cookie.
|
||||
@@ -43,25 +65,41 @@ JWT_REFRESH_TOKEN_TTL=7d
|
||||
|
||||
Backend собирается через multi-stage сборку Maven:
|
||||
1. Этап сборки: `maven:3.9-eclipse-temurin-17` → `mvn package`
|
||||
2. Этап запуска: `eclipse-temurin:17-jre-alpine` → `java -jar app.jar`
|
||||
2. OpenTelemetry Java Agent `2.28.1` загружается как фиксированный Maven-артефакт и
|
||||
проверяется по закреплённому SHA-256
|
||||
3. Этап запуска: `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/
|
||||
FROM node:22-alpine3.23@sha256:8516dce... AS frontend-assets
|
||||
RUN npm ci
|
||||
RUN npm run build:vendor
|
||||
|
||||
FROM httpd:alpine3.23@sha256:4a15e9c...
|
||||
COPY --from=frontend-assets /build/dist/vendor/ /usr/local/apache2/htdocs/vendor/
|
||||
COPY security.conf /usr/local/apache2/conf/extra/magistr-security.conf
|
||||
COPY proxy.conf /usr/local/apache2/conf/extra/magistr-proxy.conf
|
||||
```
|
||||
|
||||
Оба базовых образа зафиксированы tag и manifest digest. Первый этап собирает зафиксированный
|
||||
OpenTelemetry bundle; второй раздаёт только runtime-
|
||||
файлы, без `node_modules`, тестов и build-исходников. Apache подключает `mod_headers`,
|
||||
`mod_proxy` и `mod_proxy_http`; proxy сохраняет исходный `Host`, чтобы `localhost` корректно
|
||||
маршрутизировался в tenant `default`. Сервер
|
||||
возвращает CSP с `script-src 'self'`, `script-src-attr 'none'`, `style-src 'self'` и точными
|
||||
SHA-256 для оставшихся статических style-атрибутов. `unsafe-inline` и `unsafe-eval` не
|
||||
используются. Дополнительно выставляются `nosniff`, `DENY`, строгий referrer policy и
|
||||
ограниченная Permissions Policy.
|
||||
|
||||
---
|
||||
|
||||
## Kubernetes (продакшн)
|
||||
|
||||
### Расположение конфигурации
|
||||
|
||||
Ожидаемое расположение production-манифестов: `../k8s/`. В текущей рабочей копии этот
|
||||
внешний каталог недоступен, поэтому приведённые ниже изменения являются обязательным
|
||||
runbook для оператора, а не утверждением о применённом или проверенном состоянии кластера.
|
||||
Production-манифесты находятся в `../k8s/`. Каталог расположен вне Git-корня `magistr`,
|
||||
поэтому его изменения проверяются и перечисляются отдельно от `git diff` репозитория.
|
||||
|
||||
### Ключевые ресурсы
|
||||
|
||||
@@ -78,6 +116,37 @@ runbook для оператора, а не утверждением о прим
|
||||
- `JWT_ACCESS_TOKEN_TTL=15m`
|
||||
- `JWT_REFRESH_TOKEN_TTL=7d`
|
||||
- `JWT_REFRESH_COOKIE_SECURE=true`
|
||||
- `JWT_REFRESH_CLEANUP_ENABLED=true`
|
||||
- `JWT_REFRESH_CLEANUP_RETENTION=30d`
|
||||
- `JWT_REFRESH_CLEANUP_BATCH_SIZE=500`
|
||||
- `JWT_REFRESH_CLEANUP_MAX_BATCHES=20`
|
||||
- `JWT_REFRESH_CLEANUP_INTERVAL_MS=3600000`
|
||||
- `JWT_REFRESH_CLEANUP_INITIAL_DELAY_MS=60000`
|
||||
|
||||
Cleanup проходит отдельно по каждой активной tenant-БД. `RETENTION` задаёт срок хранения
|
||||
свежих истёкших и отозванных audit-записей, `BATCH_SIZE` и `MAX_BATCHES` ограничивают объём
|
||||
одного прохода, а interval/initial delay управляют безопасным расписанием.
|
||||
|
||||
### Защита входа и доверенные proxy
|
||||
|
||||
`app-config` задаёт общий PostgreSQL rate limit для всех backend-pod:
|
||||
|
||||
- `LOGIN_RATE_MAX_FAILURES=5`
|
||||
- `LOGIN_RATE_ATTEMPT_WINDOW=15m`
|
||||
- `LOGIN_RATE_BASE_BLOCK_DURATION=1m`
|
||||
- `LOGIN_RATE_MAX_BLOCK_DURATION=15m`
|
||||
- `LOGIN_AUDIT_RETENTION=90d`
|
||||
- `TRUSTED_PROXY_CIDRS=10.42.0.0/16`
|
||||
|
||||
Последнее значение соответствует стандартной pod-сети K3s, из которой Traefik обращается
|
||||
к backend. Если кластер запущен с другим `cluster-cidr`, перед rollout нужно указать
|
||||
фактическую сеть ingress proxy. Не следует добавлять публичные сети клиентов: backend
|
||||
доверяет `X-Forwarded-For` только от непосредственного источника из этого списка.
|
||||
|
||||
Счётчики и audit-события находятся в каждой tenant-БД, поэтому два pod используют одно
|
||||
атомарное состояние. Старая история очищается раз в сутки ограниченными `SKIP LOCKED`
|
||||
пачками; параметры `LOGIN_AUDIT_CLEANUP_*` из `.env.example` позволяют изменить расписание
|
||||
и максимальный объём одного прохода.
|
||||
|
||||
`app-secret` задаёт `JWT_SECRET`. Этот Secret не создаётся файлами `../k8s/`: его заранее
|
||||
предоставляет внешний secret manager или оператор. Deployment явно включает профиль
|
||||
@@ -194,8 +263,7 @@ namespace `magistr`. Secret должен существовать заранее
|
||||
|
||||
### Liveness и readiness probes backend
|
||||
|
||||
TCP probe подтверждает только открытый порт и не отражает готовность обязательных tenant-БД.
|
||||
В production Deployment необходимо использовать раздельные HTTP probes:
|
||||
Production Deployment использует раздельные HTTP probes:
|
||||
|
||||
```yaml
|
||||
livenessProbe:
|
||||
@@ -214,29 +282,20 @@ Liveness зависит только от состояния процесса. R
|
||||
исключается из приёма трафика без перезапуска. Actuator не публикует components, домены,
|
||||
JDBC URL или credentials.
|
||||
|
||||
Параметры `initialDelaySeconds`, `periodSeconds`, `timeoutSeconds` и `failureThreshold` нужно
|
||||
согласовать с текущим Deployment; замена механизма probe не подтверждает автоматически
|
||||
корректность этих порогов.
|
||||
`../k8s/backend.yaml` монтирует каталог `/config` без `subPath`, а `app-config` задаёт
|
||||
`TENANTS_CONFIG_PATH=/config/tenants.json` и `TENANTS_CONFIG_REQUIRED=true`. Отсутствующий
|
||||
или невалидный обязательный Secret поэтому снимает readiness, не включая H2 fallback.
|
||||
|
||||
### Внешние действия для 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. отрендерить манифесты и выполнить серверную проверку без применения:
|
||||
Перед production rollout оператор должен убедиться, что внешний `tenants-secret` существует,
|
||||
содержит непустой ключ `tenants.json`, допускает обновление, и выполнить проверки без
|
||||
применения:
|
||||
|
||||
```bash
|
||||
kubectl kustomize ../k8s >/dev/null
|
||||
kubectl apply --dry-run=server -k ../k8s
|
||||
```
|
||||
|
||||
5. отдельно проверить полномочия фактического ServiceAccount:
|
||||
Отдельно проверьте полномочия фактического ServiceAccount:
|
||||
|
||||
```bash
|
||||
BACKEND_SERVICE_ACCOUNT='укажите-фактический-service-account'
|
||||
@@ -246,15 +305,21 @@ kubectl auth can-i update secret/tenants-secret \
|
||||
--as="system:serviceaccount:magistr:${BACKEND_SERVICE_ACCOUNT}" -n magistr
|
||||
```
|
||||
|
||||
Применение манифестов, rollout и проверка реальных pod являются отдельными внешними
|
||||
операциями и без разрешения автоматически не выполняются.
|
||||
Production rollout и проверка реальных pod являются внешними операциями и без отдельного
|
||||
разрешения из этой рабочей копии не выполнялись.
|
||||
|
||||
### Обновление backend
|
||||
### Ручное обновление backend и frontend
|
||||
|
||||
```bash
|
||||
kubectl rollout restart deployment backend -n magistr
|
||||
export BACKEND_IMAGE_REF='gitea.zuev.company/zuev/magistr-backend@sha256:<64 hex>'
|
||||
export FRONTEND_IMAGE_REF='gitea.zuev.company/zuev/magistr-frontend@sha256:<64 hex>'
|
||||
bash ../k8s/deploy.sh apply
|
||||
```
|
||||
|
||||
Манифесты содержат нулевой digest-sentinel. `deploy.sh` требует два реальных digest,
|
||||
локально подставляет их до `kubectl apply` и строго ждёт оба rollout; прямое применение
|
||||
Deployment-файлов запрещено.
|
||||
|
||||
---
|
||||
|
||||
## Caddy (реверс-прокси)
|
||||
@@ -275,10 +340,16 @@ kubectl rollout restart deployment backend -n magistr
|
||||
Расположение: `.gitea/workflows/docker-build.yaml`
|
||||
|
||||
Основные шаги:
|
||||
1. Checkout кода
|
||||
2. Login в Docker Registry
|
||||
3. Build + Push образов (`backend`, `frontend`)
|
||||
4. Генерация меток через `docker/metadata-action`
|
||||
1. Backend unit/component tests, frontend static/unit tests, Compose validation и shell-тест
|
||||
immutable rollout/rollback.
|
||||
2. Только после успешных gates — login, параллельная сборка и push backend/frontend с
|
||||
SHA-tag и release tag без `latest`.
|
||||
3. Build jobs публикуют digest обоих образов; deploy job устанавливает фиксированный
|
||||
`kubectl v1.33.12` после SHA-256 проверки.
|
||||
4. `scripts/deploy-images.sh` принимает только `image@sha256:...`, сохраняет предыдущие
|
||||
ссылки, применяет оба digest и ждёт rollout. При отказе автоматически возвращает оба
|
||||
предыдущих образа и повторно проверяет их готовность.
|
||||
5. Workflow-wide concurrency lock не допускает одновременные production deployment.
|
||||
|
||||
---
|
||||
|
||||
@@ -289,7 +360,7 @@ kubectl rollout restart deployment backend -n magistr
|
||||
```mermaid
|
||||
graph LR
|
||||
Backend["Spring Boot"] -->|OTLP gRPC| Collector["OTel Collector"]
|
||||
Frontend["JS (otel.js)"] -->|OTLP HTTP| Collector
|
||||
Frontend["JS (локальный OTel bundle)"] -->|OTLP HTTP| Collector
|
||||
Collector --> SigNoz["SigNoz"]
|
||||
|
||||
Collector -->|"Метрики PostgreSQL"| PgExporter["pg_exporter"]
|
||||
@@ -308,10 +379,13 @@ Tenant ID добавляется в:
|
||||
|
||||
### Интеграция Frontend
|
||||
|
||||
Файл `admin/js/otel.js` — клиентская телеметрия:
|
||||
Собранный `/vendor/otel.js` — клиентская телеметрия:
|
||||
- Метрики производительности страниц
|
||||
- Трейсы пользовательских действий
|
||||
|
||||
Исходные npm-пакеты имеют exact-версии и lockfile, собираются внутри frontend-образа и не
|
||||
загружаются браузером со стороннего CDN.
|
||||
|
||||
PostgreSQL receivers collector получают endpoint и credentials только из
|
||||
`otel-postgres-secret`; ConfigMap collector содержит лишь `${env:...}` ссылки.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user