$ cat "Кейс: Миграция монолита в Kubernetes и настройка Zero-Downtime CI/CD.md"

Миграция монолита в Kubernetes: опыт команды Noxet

В этом кейсе инженеры Noxet делятся практическим опытом перевода крупного монолитного сервиса с физических серверов в облачную оркестрацию Kubernetes. Проект решал классические проблемы зрелых продуктов: медленный ручной релизный цикл, деградация производительности в часы пиковых нагрузок и риск простоев при выкатке обновлений.


Исходное состояние и архитектурные вызовы

До начала модернизации приложение представляло собой тяжелый монолитный бэкенд, развернутый на двух выделенных серверах под управлением systemd и Nginx:

  • Stateful-зависимости: Сессии пользователей хранились в оперативной памяти конкретного процесса, из-за чего на балансировщике требовалось использовать Sticky Sessions (привязку клиентов по IP/cookie).
  • Файловые блокировки: Загружаемые пользователями файлы сохранялись на локальный диск сервера, требуя ненадежной синхронизации через NFS.
  • Ручной деплой: Выкатка релизов занимала до 3 часов, требовала ночных окон обслуживания и периодически сопровождалась недоступностью сервиса из-за перезапуска служб.
  • Отсутствие автоскейлинга: В моменты маркетинговых акций серверы упирались в полку по CPU и RAM, приводя к отказам 502/504 Gateway Timeout.

Этап 1. Подготовка приложения (12-Factor App)

Перенос монолита в контейнеры «как есть» без адаптации архитектуры приводит лишь к переносу старых проблем на новый уровень абстракции. Первым шагом команда выполнила рефакторинг:

  1. Экстернализация состояния (Stateless): Хранение пользовательских сессий перенесено в распределенный кластер Redis Sentinel. Это позволило запускать любое количество параллельных инстансов без потери авторизации пользователей.
  2. Объектное хранилище: Работа с локальными файлами заменена на взаимодействие по S3 API (кластер MinIO в On-Premise контуре).
  3. Graceful Shutdown: Реализована корректная обработка сигналов SIGTERM. При получении сигнала приложение перестает принимать новые HTTP-соединения, завершает обработку текущих запросов в течение 15 секунд и только затем освобождает ресурсы.
  4. Структурированное логирование: Вывод логов переведен в stdout/stderr в формате JSON, что обеспечило прозрачный сбор логов демоном Vector в централизованное хранилище VictoriaLogs / OpenSearch.

Этап 2. Оптимизация сборки контейнеров

Безопасность и размер образов критически влияют на скорость старта подов в Kubernetes. Мы разработали multi-stage Dockerfile:

# --- Этап сборки (Builder) ---
FROM golang:1.22-alpine AS builder
WORKDIR /app
RUN apk add --no-cache git ca-certificates
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /bin/server ./cmd/server

# --- Финальный минимальный образ (Runtime) ---
FROM gcr.io/distroless/static-debian12:nonroot
WORKDIR /app
COPY --from=builder /bin/server /app/server
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app/server"]

Результат оптимизации образа:

  • Размер итогового контейнера снизился с 1.4 ГБ до 28 МБ.
  • Полное отсутствие пакетных менеджеров и утилит (sh, bash, curl) в финальном образе исключает вектор выполнения произвольного кода при уязвимостях.
  • Запуск от непривилегированного пользователя (nonroot:nonroot).

Этап 3. Архитектура развертывания в Kubernetes

Для обеспечения бесшовного обновления (Zero-Downtime Deployment) и автоматического масштабирования манифест приложения параметризован через Helm:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: core-api
  labels:
    app: core-api
spec:
  replicas: 4
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 0
  selector:
    matchLabels:
      app: core-api
  template:
    metadata:
      labels:
        app: core-api
    spec:
      terminationGracePeriodSeconds: 30
      containers:
        - name: app
          image: registry.noxet.ru/project/core-api:v2.4.1
          resources:
            requests:
              cpu: "300m"
              memory: "512Mi"
            limits:
              cpu: "1500m"
              memory: "1536Mi"
          readinessProbe:
            httpGet:
              path: /healthz/ready
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 5
            failureThreshold: 2
          livenessProbe:
            httpGet:
              path: /healthz/live
              port: 8080
            initialDelaySeconds: 15
            periodSeconds: 10

Ключевые механизмы устойчивости:

  • maxUnavailable: 0: Старые поды не выводятся из эксплуатации до тех пор, пока новый под не пройдет проверку readinessProbe и не начнет принимать пользовательский трафик.
  • HorizontalPodAutoscaler (HPA): Автоматическое масштабирование количества реплик от 4 до 24 подов при утилизации CPU выше 70% или превышении порога в 250 rps на инстанс.
  • PodDisruptionBudget (PDB): Гарантирует наличие минимум 75% доступных подов во время планового обновления и дренажа нод кластера.

Этап 4. Автоматизированный CI/CD пайплайн в GitLab

Сборка и доставка автоматизированы через GitLab CI с использованием Kaniko (безопасная сборка без использования privileged docker-in-docker сокета):

stages:
  - test
  - build
  - deploy

run_tests:
  stage: test
  image: golang:1.22-alpine
  script:
    - go test -v -race ./...

build_image:
  stage: build
  image:
    name: gcr.io/kaniko-project/executor:v1.20.0-debug
    entrypoint: [""]
  script:
    - /kaniko/executor
        --context "${CI_PROJECT_DIR}"
        --dockerfile "${CI_PROJECT_DIR}/Dockerfile"
        --destination "${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}"
        --destination "${CI_REGISTRY_IMAGE}:latest"
        --cache=true
  only:
    - main

deploy_production:
  stage: deploy
  image: alpine/helm:3.14.0
  script:
    - helm upgrade --install core-api ./helm \
        --namespace production \
        --set image.tag=${CI_COMMIT_SHORT_SHA} \
        --wait --timeout 5m
  environment:
    name: production
  only:
    - main

Измеримые результаты внедрения

  1. Время деплоя: Сократилось с 2–3 часов ручной работы до 6 минут полного цикла (тестирование, сборка Kaniko, раскатка Helm).
  2. Доступность 100% при релизах: Благодаря RollingUpdate, readinessProbe и preStop hook ни один входящий пользовательский запрос не сбрасывается во время обновления.
  3. Автоматическая реакция на всплески нагрузки: При проведении промо-акций кластер за 90 секунд автоматически масштабирует пул подов, удерживая задержку (p99 latency) в пределах 45 мс.
  4. Безопасность среды: Изоляция сети через Kubernetes NetworkPolicies и запуск контейнеров в режиме Read-Only корневой файловой системы.

Если вашей инфраструктуре требуется миграция в Kubernetes, построение надежного CI/CD или аудит отказоустойчивости — обратитесь к инженерам Noxet.