# Перенос HD Portal (device-diagnostic) в Kubernetes **Репозиторий:** `it-dept/device-diagnostic` на `gitlab.gigacoms.info` **Публичное название:** HD Portal **Текущая платформа:** Docker Swarm (нода `sandbox`) **Целевая платформа:** Kubernetes (кластер `k8s_infrastructure`) --- ## Что делает приложение HD Portal — внутренний веб-портал для инженеров техподдержки. Позволяет: 1. **Найти абонента** по номеру договора через REST API BGBilling (`bill.gigacoms.ru`) 2. **Подключиться к сетевому оборудованию** абонента по SSH или Telnet и выполнить диагностические команды 3. **Сохранить результаты** диагностики в PostgreSQL 4. **Создать заявку** в BGERP/BGCRM (`hd.gigacoms.ru`) с описанием проблемы 5. Веб-интерфейс (Thymeleaf + Bootstrap) + REST API с API-Key аутентификацией (Swagger UI) ### Поддерживаемое оборудование | Тип | Коды из биллинга | Протокол | |---|---|---| | BDCom | 14, 16, 119, 122 | SSH/Telnet | | DLink | 9, 49 | SSH/Telnet | | SNR | 17, 25 | SSH/Telnet | | TPLink | 65, 70 | SSH/Telnet | | ZTE EPON | 32 | SSH/Telnet | | ZTE GPON | 107 | SSH/Telnet | | СКАТ (BRAS) | 0 | SSH/Telnet | --- ## Технический стек | Компонент | Версия | |---|---| | Java | 17 | | Spring Boot | 3.2.4 | | Spring Security | form login + API key filter | | Thymeleaf | шаблонизатор UI | | Flyway | миграции схемы БД (9 миграций) | | PostgreSQL | 15 (своя БД приложения) | | MariaDB 10.2 | внешняя БД BGERP (read-only: пользователи, группы) | | sshj 0.35.0 | SSH-подключения к оборудованию | | commons-net 3.9.0 | Telnet-подключения | | springdoc-openapi 2.5.0 | Swagger UI (`/swagger-ui.html`) | | образ | `eclipse-temurin:17-jre-alpine` | --- ## Текущая архитектура (Docker Swarm) ``` Swarm (нода: sandbox, 10.203.0.211) │ ├── Service: diagnostic-app (replicas: 1) │ ├── Image: reg.gitlab.gigacoms.info/it-dept/device-diagnostic:latest │ ├── Port: 8181 → 8080 (ingress mode) │ ├── Network: diagnostic-network + nginx-proxy_nginx-network │ ├── JVM: -Xmx512m -Xms256m │ └── Env: APP_DATASOURCE_*, BGERP_DATASOURCE_*, BGERP_API_* │ └── Service: diagnostic-db (replicas: 1) ├── Image: postgres:15-alpine ├── Volume: /mnt/swarm_quorum/diagnostic/postgresql/data → /var/lib/postgresql/data └── Network: diagnostic-network ``` **Внешние зависимости приложения:** | Система | Адрес | Тип | Назначение | |---|---|---|---| | BGBilling REST API | `https://bill.gigacoms.ru/bgbilling/...` | HTTPS | Данные по договору абонента | | BGERP REST API | `https://hd.gigacoms.ru/` | HTTPS | Создание заявок, сообщений | | BGERP MariaDB | `10.203.0.221:3306/bgcrm` | TCP | Аутентификация пользователей | | Сетевые устройства | LAN `10.x.x.x` | SSH/Telnet | Диагностические команды | --- ## Ключевые наблюдения для миграции ### 1. Приложение stateless — подходит для Deployment Само приложение не хранит состояния между запросами. HTTP-сессии in-memory (стандарт Spring). Единственное состояние — PostgreSQL. Подходит для `Deployment` с 1 репликой (масштабирование возможно при наличии shared session store, но не нужно сейчас). ### 2. PostgreSQL — StatefulSet с Longhorn PVC Текущий bind-mount `/mnt/swarm_quorum/diagnostic/postgresql/data` → `PersistentVolumeClaim` на Longhorn. Данные переносятся стандартным `pg_dump` / `pg_restore`. ### 3. Credentials hardcoded в коде — блокер В `ContractInfoService.java` жёстко прописаны: ```java private String apiUrl = "https://bill.gigacoms.ru/bgbilling/aiDmUnxl6KC8R1ELG4QJDPDm3IPn6V"; private String apiUser = "sbersalute"; private String apiPassword = "ULW2KmPd6bqc"; ``` Перед миграцией **обязательно** перенести эти значения в env-переменные (`BGBILLING_API_URL`, `BGBILLING_API_USER`, `BGBILLING_API_PASSWORD`) через `@Value` и `application.properties`. Без этого K8s Secret не поможет. ### 4. Пароли пользователей хранятся открытым текстом `SecurityConfig` использует `NoOpPasswordEncoder`. Пользователи логинятся с паролями из BGERP MariaDB. Это работает, но является техническим долгом. Не блокирует миграцию. ### 5. Сетевой доступ к оборудованию LAN Приложение SSH/Telnet-ается к коммутаторам и BRAS во внутренней сети (`10.x.x.x`). K8s-воркер (`10.203.0.96`) находится в той же сети — дополнительных маршрутов не нужно. **Важно:** flannel-интерфейсы (`flannel.1`, `cni0`) должны быть в `trusted` зоне firewalld на мастере и воркере (это уже обеспечено существующей инфраструктурой). ### 6. Nginx → Traefik Сейчас приложение за Nginx reverse proxy (Swarm network `nginx-proxy_nginx-network`). В K8s — Traefik IngressRoute. UI и API требуют аутентификации, поэтому BasicAuth на уровне Traefik не нужен (приложение само управляет доступом). ### 7. CI: этап `mkdir` через SSH становится не нужным Шаг `mkdir` в CI создаёт директорию `/mnt/swarm_quorum/diagnostic` на Swarm-хосте по SSH. В K8s PVC создаётся декларативно. Этап `mkdir` из `.gitlab-ci.yml` нужно убрать или заменить на `kubectl apply`. --- ## Целевая архитектура в Kubernetes ``` Namespace: hd-portal │ ├── Secret: hd-portal-env │ └── все переменные окружения приложения │ ├── Secret: hd-portal-registry-pull (docker-registry) │ ├── PersistentVolumeClaim: hd-portal-postgres-data │ └── Longhorn, RWO, 5Gi │ ├── StatefulSet: hd-portal-postgres │ ├── Image: postgres:15-alpine │ └── Volume: hd-portal-postgres-data → /var/lib/postgresql/data │ ├── Service: hd-portal-postgres (ClusterIP, 5432) │ ├── Deployment: hd-portal │ ├── Image: reg.gitlab.gigacoms.info/it-dept/device-diagnostic:latest │ ├── replicas: 1 │ ├── resources: requests 256Mi/200m, limits 768Mi/1 │ └── Env from Secret: hd-portal-env │ ├── Service: hd-portal (ClusterIP, 8080) │ └── Traefik IngressRoute: порт 10003 → hd-portal:8080 (без BasicAuth — приложение имеет собственную аутентификацию) ``` --- ## Манифесты ### Namespace ```yaml apiVersion: v1 kind: Namespace metadata: name: hd-portal ``` ### Secret — переменные приложения Создаётся вручную (не хранится в Git): ```bash kubectl create secret generic hd-portal-env \ --from-literal=APP_DATASOURCE_URL="jdbc:postgresql://hd-portal-postgres:5432/diagnostic" \ --from-literal=APP_DATASOURCE_USERNAME="postgres" \ --from-literal=APP_DATASOURCE_PASSWORD="<пароль>" \ --from-literal=BGERP_DATASOURCE_URL="jdbc:mariadb://10.203.0.221/bgcrm?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Europe/Moscow" \ --from-literal=BGERP_DATASOURCE_USERNAME="user.portal" \ --from-literal=BGERP_DATASOURCE_PASSWORD="<пароль>" \ --from-literal=BGERP_API_URL="https://hd.gigacoms.ru/" \ --from-literal=BGERP_API_USERNAME="autobot" \ --from-literal=BGERP_API_PASSWORD="<пароль>" \ --from-literal=BGBILLING_API_URL="https://bill.gigacoms.ru/bgbilling/<токен>" \ --from-literal=BGBILLING_API_USER="sbersalute" \ --from-literal=BGBILLING_API_PASSWORD="<пароль>" \ --from-literal=SPRING_PROFILES_ACTIVE="prod" \ --from-literal=JAVA_OPTS="-Xmx512m -Xms256m -Duser.timezone=Europe/Moscow" \ --from-literal=TZ="Europe/Moscow" \ -n hd-portal ``` > Строка `BGBILLING_API_URL` и `BGBILLING_API_USER/PASSWORD` — только после исправления `ContractInfoService.java`. ### Secret для pull из GitLab Registry ```bash kubectl create secret docker-registry hd-portal-registry-pull \ --docker-server=reg.gitlab.gigacoms.info \ --docker-username= \ --docker-password= \ -n hd-portal ``` ### PVC для PostgreSQL ```yaml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: hd-portal-postgres-data namespace: hd-portal spec: storageClassName: longhorn accessModes: - ReadWriteOnce resources: requests: storage: 5Gi ``` ### StatefulSet — PostgreSQL ```yaml apiVersion: apps/v1 kind: StatefulSet metadata: name: hd-portal-postgres namespace: hd-portal spec: serviceName: hd-portal-postgres replicas: 1 selector: matchLabels: app: hd-portal-postgres template: metadata: labels: app: hd-portal-postgres spec: containers: - name: postgres image: postgres:15-alpine env: - name: POSTGRES_DB value: diagnostic - name: POSTGRES_USER valueFrom: secretKeyRef: name: hd-portal-env key: APP_DATASOURCE_USERNAME - name: POSTGRES_PASSWORD valueFrom: secretKeyRef: name: hd-portal-env key: APP_DATASOURCE_PASSWORD - name: TZ value: Europe/Moscow ports: - containerPort: 5432 volumeMounts: - name: data mountPath: /var/lib/postgresql/data readinessProbe: exec: command: ["pg_isready", "-U", "postgres"] initialDelaySeconds: 5 periodSeconds: 5 resources: requests: memory: 128Mi cpu: 100m limits: memory: 512Mi cpu: 500m volumes: - name: data persistentVolumeClaim: claimName: hd-portal-postgres-data ``` ### Service — PostgreSQL ```yaml apiVersion: v1 kind: Service metadata: name: hd-portal-postgres namespace: hd-portal spec: selector: app: hd-portal-postgres ports: - port: 5432 targetPort: 5432 clusterIP: None # headless для StatefulSet ``` ### Deployment — приложение ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: hd-portal namespace: hd-portal spec: replicas: 1 selector: matchLabels: app: hd-portal template: metadata: labels: app: hd-portal spec: imagePullSecrets: - name: hd-portal-registry-pull containers: - name: app image: reg.gitlab.gigacoms.info/it-dept/device-diagnostic:latest ports: - containerPort: 8080 envFrom: - secretRef: name: hd-portal-env resources: requests: memory: 256Mi cpu: 200m limits: memory: 768Mi cpu: "1" readinessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 30 periodSeconds: 10 livenessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 60 periodSeconds: 30 ``` ### Service — приложение ```yaml apiVersion: v1 kind: Service metadata: name: hd-portal namespace: hd-portal spec: selector: app: hd-portal ports: - port: 8080 targetPort: 8080 ``` ### Traefik IngressRoute Добавить в `inventory/prod/group_vars/traefik.yml`: ```yaml traefik_port_map: # ... существующие записи ... - name: hd-portal # ≤15 символов port: 10003 namespace: hd-portal service: hd-portal servicePort: 8080 basicauth: enabled: false # приложение имеет собственную форму входа ``` --- ## Необходимые изменения в коде (до миграции) ### Обязательно: вынести BGBilling credentials из кода Файл: `src/main/java/ru/gigacoms/net/diagnostic/bgbilling/ContractInfoService.java` **Было:** ```java private String apiUrl = "https://bill.gigacoms.ru/bgbilling/aiDmUnxl6KC8R1ELG4QJDPDm3IPn6V"; private String apiUser = "sbersalute"; private String apiPassword = "ULW2KmPd6bqc"; ``` **Стало:** ```java @Value("${bgbilling.api.url}") private String apiUrl; @Value("${bgbilling.api.user}") private String apiUser; @Value("${bgbilling.api.password}") private String apiPassword; ``` И в `application.properties`: ```properties bgbilling.api.url=${BGBILLING_API_URL} bgbilling.api.user=${BGBILLING_API_USER} bgbilling.api.password=${BGBILLING_API_PASSWORD} ``` ### Желательно: вынести API-ключи из application.properties в Secret Сейчас `app.api.keys` указаны прямо в `application.properties`. При K8s-деплое лучше передавать через env-переменную: ```properties app.api.keys=${APP_API_KEYS} ``` --- ## Перенос данных PostgreSQL ```bash # 1. Дамп с Swarm-ноды (sandbox) ssh root@10.203.0.211 "docker exec \ pg_dump -U postgres diagnostic" > diagnostic_dump.sql # 2. Создать PVC и запустить StatefulSet (применить манифесты) # 3. Восстановить дамп в K8s PostgreSQL kubectl exec -n hd-portal hd-portal-postgres-0 -- \ psql -U postgres -d diagnostic < diagnostic_dump.sql ``` --- ## Изменения в CI/CD Убрать из `.gitlab-ci.yml` этап `mkdir` — он создавал директории на Swarm-хосте по SSH. В K8s PVC управляется декларативно. Опционально: добавить этап `deploy` для обновления образа в K8s после push: ```bash kubectl set image deployment/hd-portal app=reg.gitlab.gigacoms.info/it-dept/device-diagnostic:$CI_COMMIT_SHA -n hd-portal ``` Или через Flux — после push в Registry Flux Image Automation обновит манифест автоматически (если настроен ImageRepository + ImagePolicy). --- ## Интеграция с Flux Манифесты размещаются в `k8s/k8s-fleet`: ``` clusters/production/hd-portal/ ├── namespace.yaml ├── pvc.yaml ├── statefulset-postgres.yaml ├── service-postgres.yaml ├── deployment.yaml └── service.yaml ``` Secrets (`hd-portal-env`, `hd-portal-registry-pull`) создаются вручную и **не хранятся в Git**. Traefik-маршрут добавляется через Ansible (`setup_traefik.yml`) при изменении `traefik_port_map`. --- ## Риски и ограничения | Риск | Оценка | Митигация | |---|---|---| | BGBilling credentials в коде | **высокий** | Обязательно исправить до миграции (см. секцию выше) | | `NoOpPasswordEncoder` (plaintext пароли) | средний | Технический долг, не блокирует миграцию | | SSH/Telnet к сетевым устройствам из pod | низкий | Воркер в той же LAN, маршрутизация работает | | Доступность BGERP MariaDB (`10.203.0.221`) | низкий | Прямой TCP, не зависит от K8s | | Длительные SSH-сессии к оборудованию | низкий | Диагностика занимает секунды, не минуты | | Flyway при старте — PostgreSQL может быть не готов | средний | `readinessProbe` на DB + `initialDelaySeconds: 30` на app | | Потеря данных при переносе PostgreSQL | средний | Сделать дамп прямо перед переключением | --- ## План миграции 1. **Исправить `ContractInfoService.java`** — вынести credentials в env-переменные, собрать новый образ. 2. **Создать deploy token** для `it-dept/device-diagnostic` (scope: `read_registry`). 3. **Сделать дамп PostgreSQL** с ноды `sandbox`. 4. **Применить манифесты** через Flux или `kubectl apply`: Namespace → PVC → StatefulSet postgres → Service postgres → Deployment → Service 5. **Создать секреты вручную**: - `hd-portal-env` - `hd-portal-registry-pull` 6. **Восстановить дамп** в K8s PostgreSQL. 7. **Проверить запуск** — убедиться в `/actuator/health` и доступности UI. 8. **Добавить Traefik-маршрут** (`traefik_port_map`, порт 10003), запустить `setup_traefik.yml`. 9. **Остановить Swarm-сервис** `diagnostic-app` и `diagnostic-db` после успешной проверки. 10. **Удалить этап `mkdir`** из `.gitlab-ci.yml`.