18 KiB
Перенос HD Portal (device-diagnostic) в Kubernetes
Репозиторий: it-dept/device-diagnostic на gitlab.gigacoms.info
Публичное название: HD Portal
Текущая платформа: Docker Swarm (нода sandbox)
Целевая платформа: Kubernetes (кластер k8s_infrastructure)
Что делает приложение
HD Portal — внутренний веб-портал для инженеров техподдержки. Позволяет:
- Найти абонента по номеру договора через REST API BGBilling (
bill.gigacoms.ru) - Подключиться к сетевому оборудованию абонента по SSH или Telnet и выполнить диагностические команды
- Сохранить результаты диагностики в PostgreSQL
- Создать заявку в BGERP/BGCRM (
hd.gigacoms.ru) с описанием проблемы - Веб-интерфейс (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 жёстко прописаны:
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
apiVersion: v1
kind: Namespace
metadata:
name: hd-portal
Secret — переменные приложения
Создаётся вручную (не хранится в Git):
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
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
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: hd-portal-postgres-data
namespace: hd-portal
spec:
storageClassName: longhorn
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
StatefulSet — PostgreSQL
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
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 — приложение
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 — приложение
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:
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
Было:
private String apiUrl = "https://bill.gigacoms.ru/bgbilling/aiDmUnxl6KC8R1ELG4QJDPDm3IPn6V";
private String apiUser = "sbersalute";
private String apiPassword = "ULW2KmPd6bqc";
Стало:
@Value("${bgbilling.api.url}")
private String apiUrl;
@Value("${bgbilling.api.user}")
private String apiUser;
@Value("${bgbilling.api.password}")
private String apiPassword;
И в application.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-переменную:
app.api.keys=${APP_API_KEYS}
Перенос данных PostgreSQL
# 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:
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 | средний | Сделать дамп прямо перед переключением |
План миграции
- Исправить
ContractInfoService.java— вынести credentials в env-переменные, собрать новый образ. - Создать deploy token для
it-dept/device-diagnostic(scope:read_registry). - Сделать дамп PostgreSQL с ноды
sandbox. - Применить манифесты через Flux или
kubectl apply:
Namespace → PVC → StatefulSet postgres → Service postgres → Deployment → Service - Создать секреты вручную:
hd-portal-envhd-portal-registry-pull
- Восстановить дамп в K8s PostgreSQL.
- Проверить запуск — убедиться в
/actuator/healthи доступности UI. - Добавить Traefik-маршрут (
traefik_port_map, порт 10003), запуститьsetup_traefik.yml. - Остановить Swarm-сервис
diagnostic-appиdiagnostic-dbпосле успешной проверки. - Удалить этап
mkdirиз.gitlab-ci.yml.