k8s/research/apps/hd-portal.md
2026-07-15 11:14:21 +03:00

497 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Перенос 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=<deploy_token_user> \
--docker-password=<deploy_token_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 <postgres_container_id> \
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`.