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

18 KiB
Raw Permalink Blame History

Перенос 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/dataPersistentVolumeClaim на 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 средний Сделать дамп прямо перед переключением

План миграции

  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.