497 lines
18 KiB
Markdown
497 lines
18 KiB
Markdown
# Перенос 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`.
|