# 🏭 Π˜Π½Ρ„Ρ€Π°ΡΡ‚Ρ€ΡƒΠΊΡ‚ΡƒΡ€Π° ## 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. `TenantConfigWatcher` Π΄ΠΎ ΠΌΡƒΡ‚Π°Ρ†ΠΈΠΈ Ρ€Π°Π·Π±ΠΈΡ€Π°Π΅Ρ‚ ΠΈ ΠΏΡ€ΠΈ нСобходимости примСняСт Ρ‚Π΅ΠΊΡƒΡ‰ΡƒΡŽ mounted-ΠΏΡ€ΠΎΠ΅ΠΊΡ†ΠΈΡŽ ΠΊΠ°ΠΊ бСзопасный baseline; ошибка ΠΏΠΎΠ΄Π³ΠΎΡ‚ΠΎΠ²ΠΊΠΈ ΠΏΡ€Π΅Ρ€Ρ‹Π²Π°Π΅Ρ‚ ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΡŽ 2. `TenantLifecycleService` создаёт ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½Ρ‹ΠΉ candidate pool, провСряСт соСдинСниС ΠΈ Flyway 3. `KubernetesTenantSecretUpdater` выполняСт `GET` Π°ΠΊΡ‚ΡƒΠ°Π»ΡŒΠ½ΠΎΠ³ΠΎ Secret ΠΈ примСняСт ΠΊ списку Ρ‚ΠΎΠ»ΡŒΠΊΠΎ upsert Π·Π°ΠΏΡ€ΠΎΡˆΠ΅Π½Π½ΠΎΠ³ΠΎ Π΄ΠΎΠΌΠ΅Π½Π° 4. ΠΏΠΎΠ»Π½Ρ‹ΠΉ ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ Secret отправляСтся условным `PUT` с Ρ‚Π΅ΠΊΡƒΡ‰ΠΈΠΌ `metadata.resourceVersion`; ΠΏΡ€ΠΈ `409 Conflict` Π²Ρ‹ΠΏΠΎΠ»Π½ΡΡŽΡ‚ΡΡ ΠΏΠΎΠ²Ρ‚ΠΎΡ€Π½ΠΎΠ΅ Ρ‡Ρ‚Π΅Π½ΠΈΠ΅ ΠΈ ΠΏΠΎΠ²Ρ‚ΠΎΡ€Π½ΠΎΠ΅ ΠΏΡ€ΠΈΠΌΠ΅Π½Π΅Π½ΠΈΠ΅ своСй ΠΌΡƒΡ‚Π°Ρ†ΠΈΠΈ 5. `TenantRoutingDataSource` Π°Ρ‚ΠΎΠΌΠ°Ρ€Π½ΠΎ ΠΏΡƒΠ±Π»ΠΈΠΊΡƒΠ΅Ρ‚ candidate Ρ‚ΠΎΠ»ΡŒΠΊΠΎ послС ΡƒΡΠΏΠ΅ΡˆΠ½ΠΎΠΉ Π»ΠΈΠ±ΠΎ ΠΏΠΎΠ΄Ρ‚Π²Π΅Ρ€ΠΆΠ΄Ρ‘Π½Π½ΠΎΠΉ ΠΏΠΎΠ²Ρ‚ΠΎΡ€Π½Ρ‹ΠΌ Ρ‡Ρ‚Π΅Π½ΠΈΠ΅ΠΌ пСрсистСнции 6. старый pool пСрСстаёт ΠΏΡ€ΠΈΠ½ΠΈΠΌΠ°Ρ‚ΡŒ Π½ΠΎΠ²Ρ‹Π΅ запросы ΠΈ пСрСдаётся Π½Π° Π·Π°ΠΊΡ€Ρ‹Ρ‚ΠΈΠ΅ послС drain/grace timeout 7. lifecycle Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ `TenantLifecycleMutationResult` с `TenantSecretUpdateReceipt`; semantic fence рСгистрируСтся Ρ‚ΠΎΠ»ΡŒΠΊΠΎ для фактичСски записанного измСнСния (`persisted=true`, `changed=true`) 8. `TenantConfigWatcher` Π½Π° ΠΎΡΡ‚Π°Π»ΡŒΠ½Ρ‹Ρ… pod Π²ΠΈΠ΄ΠΈΡ‚ ΠΎΠ±Π½ΠΎΠ²Π»Ρ‘Π½Π½Ρ‹ΠΉ Ρ„Π°ΠΉΠ» ΠΈ ΠΊΠ°ΠΆΠ΄Ρ‹Π΅ 30 сСкунд синхронизируСт ΠΏΠΎΠ»Π½Ρ‹ΠΉ Π½ΠΎΡ€ΠΌΠ°Π»ΠΈΠ·ΠΎΠ²Π°Π½Π½Ρ‹ΠΉ `TenantConfig`, Π²ΠΊΠ»ΡŽΡ‡Π°Ρ измСнСния `name`, `domain`, `url`, `username` ΠΈ `password`; Π½ΠΎΠ²Ρ‹ΠΉ ΠΈΠ»ΠΈ ΠΈΠ·ΠΌΠ΅Π½Ρ‘Π½Π½Ρ‹ΠΉ pool ΠΏΡ€ΠΎΡ…ΠΎΠ΄ΠΈΡ‚ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΡƒ соСдинСния, Flyway ΠΈ Π°Ρ‚ΠΎΠΌΠ°Ρ€Π½Ρ‹ΠΉ swap Π±Π΅Π· ΠΏΠΎΠ²Ρ‚ΠΎΡ€Π½ΠΎΠΉ записи Secret Π£Π΄Π°Π»Π΅Π½ΠΈΠ΅ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ Ρ‚ΠΎΡ‚ ΠΆΠ΅ persistence-first порядок ΠΈ примСняСт ΠΊ Π°ΠΊΡ‚ΡƒΠ°Π»ΡŒΠ½ΠΎΠΌΡƒ Secret Ρ‚ΠΎΠ»ΡŒΠΊΠΎ remove ΡƒΠΊΠ°Π·Π°Π½Π½ΠΎΠ³ΠΎ Π΄ΠΎΠΌΠ΅Π½Π°. Число ΠΏΠΎΠΏΡ‹Ρ‚ΠΎΠΊ записи ΠΎΠ³Ρ€Π°Π½ΠΈΡ‡Π΅Π½ΠΎ трСмя, Π·Π°Π΄Π΅Ρ€ΠΆΠΊΠ° ΠΌΠ΅ΠΆΠ΄Ρƒ ΠΏΠΎΠ²Ρ‚ΠΎΡ€Π°ΠΌΠΈ возрастаСт, Π° ΠΈΠ΄Π΅ΠΌΠΏΠΎΡ‚Π΅Π½Ρ‚Π½ΠΎΠ΅ состояниС Π½Π΅ записываСтся ΠΏΠΎΠ²Ρ‚ΠΎΡ€Π½ΠΎ. Если послС ΠΏΠΎΠ΄Ρ‚Π²Π΅Ρ€ΠΆΠ΄Ρ‘Π½Π½ΠΎΠΉ записи Secret локальная активация ΠΈΠ»ΠΈ ΡƒΠ΄Π°Π»Π΅Π½ΠΈΠ΅ Π·Π°Π²Π΅Ρ€ΡˆΠ°ΡŽΡ‚ΡΡ ошибкой, backend восстанавливаСт ΠΏΡ€Π΅ΠΆΠ½ΠΈΠΉ снимок Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΏΠΎΠΊΠ° Secret сохраняСт `resourceVersion` этой записи. Π‘ΠΎΠ»Π΅Π΅ Π½ΠΎΠ²ΠΎΠ΅ ΠΌΠ΅ΠΆΠΏΠΎΠ΄ΠΎΠ²ΠΎΠ΅ ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠ΅ останавливаСт ΠΊΠΎΠΌΠΏΠ΅Π½ΡΠ°Ρ†ΠΈΡŽ ΠΈ Π½Π΅ пСрСзаписываСтся. НСопрСдСлённый сСтСвой Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚ сначала свСряСтся ΠΏΠΎΠ²Ρ‚ΠΎΡ€Π½Ρ‹ΠΌ `GET`; совпавшСС Ρ†Π΅Π»Π΅Π²ΠΎΠ΅ состояниС принимаСтся Π±Π΅Π· ΠΊΠ²ΠΈΡ‚Π°Π½Ρ†ΠΈΠΈ, Π΄ΠΎΠΏΡƒΡΠΊΠ°ΡŽΡ‰Π΅ΠΉ нСбСзопасный автоматичСский ΠΎΡ‚ΠΊΠ°Ρ‚. Watcher ΠΏΠΎΠ΄Ρ‚Π²Π΅Ρ€ΠΆΠ΄Π°Π΅Ρ‚ SHA-256 Ρ„Π°ΠΉΠ»ΠΎΠ²ΠΎΠΉ Ρ€Π΅Π²ΠΈΠ·ΠΈΠΈ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ послС ΠΏΠΎΠ»Π½ΠΎΠ³ΠΎ ΡƒΡΠΏΠ΅ΡˆΠ½ΠΎΠ³ΠΎ примСнСния снимка. ΠŸΡ€ΠΈ ошибкС ΠΏΡ€Π΅ΠΆΠ½ΠΈΠΉ ΠΏΡ€ΠΈΠΌΠ΅Π½Ρ‘Π½Π½Ρ‹ΠΉ Ρ…Π΅Ρˆ сохраняСтся, readiness снимаСтся, Π° синхронизация повторяСтся с ΡΠΊΡΠΏΠΎΠ½Π΅Π½Ρ†ΠΈΠ°Π»ΡŒΠ½ΠΎΠΉ Π·Π°Π΄Π΅Ρ€ΠΆΠΊΠΎΠΉ ΠΎΡ‚ 30 Π΄ΠΎ 300 сСкунд. Π—Π°Ρ‰ΠΈΡ‚Π° ΠΎΡ‚ Π·Π°Π΄Π΅Ρ€ΠΆΠΊΠΈ kubelet основана Π½Π΅ Π½Π° raw hash, Π° Π½Π° ΠΏΠΎΠ»Π½ΠΎΠΌ Π½ΠΎΡ€ΠΌΠ°Π»ΠΈΠ·ΠΎΠ²Π°Π½Π½ΠΎΠΌ снимкС. ΠšΠ²ΠΈΡ‚Π°Π½Ρ†ΠΈΡ фактичСской persisted-ΠΌΡƒΡ‚Π°Ρ†ΠΈΠΈ Π·Π°Π΄Π°Ρ‘Ρ‚ `previousTenants` ΠΈ `committedTenants`: previous ΠΈ ΠΏΡ€ΠΎΠΌΠ΅ΠΆΡƒΡ‚ΠΎΡ‡Π½Ρ‹Π΅ committed-снимки Π²Ρ€Π΅ΠΌΠ΅Π½Π½ΠΎ deferred, послСдний committed оТидаСтся. ΠŸΠΎΡΡ‚ΠΎΠΌΡƒ ΠΏΠΎΡΠ»Π΅Π΄ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΡŒ H0 β†’ H1 β†’ H2 Π½Π΅ откатываСтся ΠΏΡ€ΠΈ доставкС H0/H1, Π° H2 примСняСтся. НСизвСстный merged snapshot, Π½Π΅ ΡΠΎΠ²ΠΏΠ°Π΄Π°ΡŽΡ‰ΠΈΠΉ с deferred-состояниями, примСняСтся сразу. Persisted no-op ΠΈ локальная опСрация fence Π½Π΅ ΡΠΎΠ·Π΄Π°ΡŽΡ‚. Если sync оборвался послС swap Π½Π° ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ readiness, retry провСряСт ΡƒΠΆΠ΅ Π°ΠΊΡ‚ΠΈΠ²Π½ΠΎΠ΅ соСдинСниС ΠΈ Π½Π΅ выполняСт Π²Ρ‚ΠΎΡ€ΠΎΠΉ swap. 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)