Платформа падала на вебинарах: внедрение Kubernetes для EdTech-стартапа

Задача клиента: EdTech-платформа с взрывным ростом

К нам обратилась онлайн-платформа образования «SkillUp» из Казани — стартап, запустивший курсы по программированию и дизайну. За последние 6 месяцев количество пользователей выросло с 500 до 5 000, и рост продолжался. Клиент столкнулся с критическими проблемами масштабирования.

Ситуация на момент обращения:

  • Платформа работала на 2 VPS-серверах (Docker Compose) — часто не справлялись с нагрузкой
  • Во время вебинаров (300-500 одновременных пользователей) сервис падал
  • Деплой нового кода требовал 15-минутного простоя
  • Нет автомасштабирования — ручное добавление ресурсов только после жалоб пользователей
  • 6 микросервисов: веб-фронтенд, API, видеостриминг, чат, платёжный шлюз, email-уведомления

Клиент хотел получить инфраструктуру, которая автоматически масштабируется при пиках нагрузки (вебинары, запуск курсов), позволяет обновлять код без простоя и самовосстанавливается при сбоях.

Особенно болезненными были запуски новых курсов — в день старта курса на платформу приходило в 5-8 раз больше пользователей, чем обычно. Серверы не справлялись, страницы загружались по 15-20 секунд, видео буферизировалось, чат отваливался. По подсчётам клиента, каждый такой инцидент стоил 50 000-100 000 рублей из-за возвратов и оттока пользователей.

Команда разработки состояла из 5 человек: 3 бэкенд-разработчика, 1 фронтендер и 1 DevOps-инженер на полставки. Они уже использовали Docker для локальной разработки и Docker Compose на серверах, но понимали, что переросли это решение. CI/CD был организован через GitHub Actions, но деплой требовал 15-минутного простоя — процесс включал остановку контейнеров, пулл новых образов и запуск.

Видеостриминговый сервис был наиболее ресурсоёмким компонентом — он обеспечивал трансляцию вебинаров и запись лекций. Именно он первым падал при пиках нагрузки. Платёжный шлюз и email-уведомления, напротив, потребляли минимум ресурсов, но были критичны для бизнеса — их недоступность означала потерю оплат.

Анализ: когда Kubernetes оправдан

Мы провели аудит и определили, что для данного клиента Kubernetes действительно оправдан. Kubernetes имеет смысл, если выполняются 3-4 условия:

  • Микросервисная архитектура — у клиента 6 независимых сервисов (да)
  • Высокие требования к доступности — платформа теряет деньги при каждом простое (да)
  • Переменная нагрузка — пики в 5-10 раз при вебинарах (да)
  • Частые релизы — команда деплоит 2-3 раза в день (да)
  • Команда DevOps — есть один DevOps-инженер (частично)

Однако мы честно объяснили клиенту альтернативы:

ЗадачаKubernetesАльтернативаРекомендация
1-3 сервисаИзбыточенDocker ComposeDocker Compose
5+ микросервисовПодходитDocker SwarmK3s или Swarm
Высокая доступностьПодходитManaged K8sManaged K8s
МонолитИзбыточенVM + AnsibleVM + Ansible

Для клиента с 6 микросервисами и переменной нагрузкой Kubernetes был правильным выбором.

Вот наше решение: K3s-кластер для EdTech

Мы выбрали K3s от Rancher — сертифицированный облегчённый Kubernetes, идеальный для стартапов с ограниченным бюджетом.

Перед началом работ мы детально проанализировали текущую архитектуру клиента и составили план миграции с Docker Compose на Kubernetes. Каждый из 6 микросервисов получил собственный Deployment с настроенными resource limits, health checks и стратегией обновления. Мы также настроили Horizontal Pod Autoscaler для видеостримингового сервиса — самого требовательного компонента.

Установка K3s-кластера

K3s — бинарный файл менее 100 МБ с минимальными требованиями (512 МБ RAM):

# Установка K3s (мастер-нода)
curl -sfL https://get.k3s.io | sh -

# Проверка статуса
sudo systemctl status k3s
sudo k3s kubectl get nodes

# Копируем kubeconfig
mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $(id -u):$(id -g) ~/.kube/config
export KUBECONFIG=~/.kube/config

# Проверка кластера
kubectl get nodes
kubectl get pods -A

Добавили 2 воркер-ноды:

# На мастер-ноде получаем токен
sudo cat /var/lib/rancher/k3s/server/node-token

# На воркер-нодах
curl -sfL https://get.k3s.io | K3S_URL=https://MASTER_IP:6443 \
  K3S_TOKEN="TOKEN_VALUE" sh -

K3s включает из коробки: Traefik (ingress), CoreDNS, ServiceLB, Local Path Provisioner — полноценный кластер готов к работе.

Установка мастер-ноды заняла буквально 2 минуты — один curl-скрипт, и полноценный Kubernetes-кластер готов к работе.

Почему K3s, а не MicroK8s

Мы сравнили оба варианта и выбрали K3s:

ПараметрK3sMicroK8s
УстановкаСкрипт (curl)Snap
Размер~70 МБ~200 МБ
RAM (мин.)512 МБ540 МБ
IngressTraefikNginx
ХранилищеLocal Pathhostpath
HA кластерВстроенный etcdDqlite

K3s легче, быстрее запускается и лучше подходит для серверов стартапа с ограниченными ресурсами.

Основные концепции: Pod, Service, Ingress

Мы перенесли все 6 микросервисов клиента в Kubernetes-манифесты, используя ключевые абстракции платформы.

Мы подробно документировали каждый ресурс Kubernetes для команды клиента: Pod — минимальная единица (обычно один контейнер), ReplicaSet — обеспечивает запуск нужного количества реплик, Deployment — управляет ReplicaSet и обеспечивает rolling update. Это было важно, чтобы DevOps-инженер клиента мог самостоятельно управлять кластером после завершения проекта.

Deployment для веб-приложения

Создали Deployment с автомасштабированием и health checks:

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
  labels:
    app: web-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web-app
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: web-app
    spec:
      containers:
      - name: web-app
        image: myregistry/web-app:1.0.0
        ports:
        - containerPort: 8080
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "256Mi"
            cpu: "500m"
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 10
          periodSeconds: 5
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: database-url
        - name: APP_ENV
          value: "production"

Управление деплоем:

kubectl apply -f deployment.yaml
kubectl get deployments
kubectl get pods

# Масштабирование
kubectl scale deployment web-app --replicas=5

# Rolling update без простоя
kubectl set image deployment/web-app web-app=myregistry/web-app:1.1.0

# Откат
kubectl rollout undo deployment/web-app
kubectl rollout history deployment/web-app

Стратегия RollingUpdate с maxSurge: 1 и maxUnavailable: 0 означает, что при обновлении Kubernetes сначала создаёт новый Pod с новой версией, дожидается, пока он пройдёт readinessProbe, и только потом останавливает старый. Это гарантирует нулевой простой — пользователи платформы не замечают обновления. Для видеостримингового сервиса мы дополнительно настроили terminationGracePeriodSeconds: 60, чтобы текущие видеосессии корректно завершались перед остановкой Pod.

Resource requests и limits были подобраны эмпирически на основе нагрузочного тестирования: мы использовали k6 для имитации 500 одновременных пользователей и замерили реальное потребление CPU и RAM каждым сервисом. Это позволило установить точные значения без overprovisioning — каждый Pod получает ровно столько ресурсов, сколько ему необходимо, а scheduler Kubernetes может эффективно распределять Pod по нодам.

Service и Ingress для маршрутизации трафика

Настроили сетевой доступ к сервисам:

# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: web-app-service
spec:
  type: ClusterIP
  selector:
    app: web-app
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080

Ingress для маршрутизации по доменам:

# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web-app-ingress
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - app.example.com
    secretName: app-tls
  rules:
  - host: app.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: web-app-service
            port:
              number: 80
      - path: /api
        pathType: Prefix
        backend:
          service:
            name: api-service
            port:
              number: 80
kubectl apply -f service.yaml
kubectl apply -f ingress.yaml
kubectl get services
kubectl get ingress

Для SSL-терминации мы установили cert-manager, который автоматически получает и продлевает Let's Encrypt сертификаты. Клиенту больше не нужно вручную обновлять SSL — cert-manager делает это за 30 дней до истечения. Маршрутизация через Ingress позволила разделить трафик между фронтендом, API и видеостримингом по путям, сохранив один домен и один IP-адрес.

Деплой полного стека: приложение + БД + Redis

Мы развернули полный стек платформы клиента — веб-приложение, PostgreSQL с персистентным хранилищем и Redis для кэширования.

Развёртывание полного стека образовательной платформы было наиболее ответственным этапом. Мы перенесли все 6 микросервисов поочерёдно, начиная со stateless-компонентов (фронтенд, API) и заканчивая stateful-сервисами (PostgreSQL, Redis). Каждый сервис тестировался в staging-окружении перед переключением продакшен-трафика. Особое внимание уделили видеостриминговому сервису — он требовал увеличенных resource limits и отдельного PersistentVolume для временных файлов транскодирования.

Секреты и конфигурация

Безопасное хранение credentials:

# Создание секретов
kubectl create secret generic app-secrets \
  --from-literal=database-url='postgresql://appuser:SecretPass123@postgres-service:5432/appdb' \
  --from-literal=redis-url='redis://redis-service:6379/0' \
  --from-literal=secret-key='your-app-secret-key-here'
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  APP_ENV: "production"
  APP_LOG_LEVEL: "warning"
  APP_WORKERS: "4"
  NGINX_WORKER_PROCESSES: "auto"

Мы разделили конфигурацию на секреты (credentials, API-ключи) и ConfigMap (настройки среды, параметры приложения). Секреты шифруются Kubernetes и доступны только тем Pod, которым они явно назначены. ConfigMap позволяет менять параметры приложения без пересборки Docker-образа — например, переключить уровень логирования с warning на debug для диагностики проблемы, а затем вернуть обратно.

Для production-среды мы также настроили NetworkPolicy — правила сетевой изоляции между сервисами. Например, PostgreSQL доступен только из Pod API-сервиса, Redis — из API и видеостримингового сервиса. Это снижает поверхность атаки: даже если злоумышленник получит доступ к одному контейнеру, он не сможет подключиться к базе данных напрямую.

Persistent Storage для PostgreSQL

Данные БД сохраняются при перезапуске Pod:

# postgres-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 20Gi
  storageClassName: local-path

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
      - name: postgres
        image: postgres:15-alpine
        ports:
        - containerPort: 5432
        env:
        - name: POSTGRES_DB
          value: appdb
        - name: POSTGRES_USER
          value: appuser
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: password
        - name: PGDATA
          value: /var/lib/postgresql/data/pgdata
        volumeMounts:
        - name: postgres-storage
          mountPath: /var/lib/postgresql/data
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
      volumes:
      - name: postgres-storage
        persistentVolumeClaim:
          claimName: postgres-pvc

---
apiVersion: v1
kind: Service
metadata:
  name: postgres-service
spec:
  type: ClusterIP
  selector:
    app: postgres
  ports:
  - port: 5432
    targetPort: 5432

Redis для кэша:

# redis-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis
spec:
  replicas: 1
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
      - name: redis
        image: redis:7-alpine
        ports:
        - containerPort: 6379
        resources:
          requests:
            memory: "64Mi"
            cpu: "50m"
          limits:
            memory: "128Mi"
            cpu: "100m"

---
apiVersion: v1
kind: Service
metadata:
  name: redis-service
spec:
  selector:
    app: redis
  ports:
  - port: 6379
    targetPort: 6379

Деплой и проверка

Собрали всё через kustomize:

# kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - secrets.yaml
  - postgres-pvc.yaml
  - postgres-deployment.yaml
  - redis-deployment.yaml
  - deployment.yaml
  - service.yaml
  - ingress.yaml

kubectl apply -k .

Проверка:

kubectl get all
kubectl logs -f deployment/web-app
kubectl exec -it deployment/web-app -- /bin/sh
kubectl port-forward service/web-app-service 8080:80

После успешного деплоя мы провели smoke-тестирование: проверили доступность всех API-эндпоинтов, работоспособность видеостриминга, корректность соединения с базой данных и Redis. Также убедились, что Ingress корректно маршрутизирует трафик по доменам и путям, а SSL-сертификат автоматически выпущен через cert-manager. Всё работало штатно с первого раза благодаря предварительному тестированию в staging.

Мониторинг кластера: Prometheus + Grafana

Без мониторинга невозможно управлять кластером. Мы развернули полный стек наблюдаемости.

Мониторинг для образовательной платформы — не роскошь, а необходимость. Без него невозможно понять, почему во время вебинара на 300 человек начинает тормозить видео, или почему после деплоя новой версии API увеличилось время отклика. Мы настроили кастомные метрики для каждого микросервиса клиента и создали набор алертов, которые отправляются в Slack команды разработки.

Установка стека мониторинга через Helm

Установили Prometheus + Grafana + Alertmanager:

# Установка Helm
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

helm install monitoring prometheus-community/kube-prometheus-stack \
  --namespace monitoring \
  --create-namespace \
  --set grafana.adminPassword='SecureGrafanaPass' \
  --set prometheus.prometheusSpec.retention=15d \
  --set prometheus.prometheusSpec.resources.requests.memory=512Mi

Дашборды Grafana для K8s:

  • Node Exporter Full (ID: 1860) — метрики нод
  • Kubernetes Cluster Monitoring (ID: 6417) — обзор кластера
  • Kubernetes Pod Monitoring (ID: 6336) — метрики Pod
  • Nginx Ingress Controller (ID: 9614) — входящий трафик

Мы настроили кастомные алерты для каждого микросервиса: если API-сервис возвращает более 1% ошибок 5xx в течение 5 минут, если среднее время ответа превышает 2 секунды, если потребление памяти Pod приближается к limit. Все алерты отправляются в Slack-канал команды разработки с детальной информацией для быстрой диагностики.

Диагностика и типичные проблемы

Мы настроили алерты и передали клиенту набор диагностических команд:

# Общее состояние
kubectl cluster-info
kubectl top nodes
kubectl top pods --all-namespaces

# Диагностика Pod
kubectl describe pod POD_NAME
kubectl logs POD_NAME --previous
kubectl events --sort-by=.lastTimestamp

# Ресурсы
kubectl get pods -o custom-columns='NAME:.metadata.name,CPU_REQ:.spec.containers[*].resources.requests.cpu,MEM_REQ:.spec.containers[*].resources.requests.memory'

# Persistent Volumes
kubectl get pv,pvc --all-namespaces

Частые проблемы: Pending — не хватает ресурсов; CrashLoopBackOff — приложение падает; ImagePullBackOff — проблема с образом.

Анализ затрат и практические рекомендации

Мы помогли клиенту спланировать бюджет и дали рекомендации по дальнейшему развитию.

Мы помогли клиенту оценить общую стоимость владения (TCO) для каждого варианта и выбрать оптимальный баланс между стоимостью и возможностями. Для стартапа с активным ростом важно, чтобы инфраструктура масштабировалась вместе с бизнесом, а не становилась узким местом. K3s на собственных VPS оказался оптимальным выбором: стоимость 3 VPS-серверов составила около 7 000 рублей в месяц, что значительно меньше managed Kubernetes, при этом клиент получил полный контроль над кластером.

Сравнение затрат

Сравнение для типичного приложения клиента:

РешениеИнфраструктура/мес.Время DevOpsИтого
VPS + Docker Compose1 500-3 000 руб.2-4 ч/мес.~5 000 руб.
K3s на VPS (3 ноды)4 500-9 000 руб.8-16 ч/мес.~20 000 руб.
Managed K8s10 000-25 000 руб.4-8 ч/мес.~25 000 руб.

Пошаговый план, который мы реализовали

Проект шёл по нашей проверенной методологии:

Этап 1: Контейнеризация (1 неделя) — создали Dockerfile для каждого сервиса, стабилизировали в Docker Compose.

Этап 2: Кластер разработки (1 неделя) — перенесли Docker Compose в K8s-манифесты, настроили CI/CD.

Этап 3: Продакшен (2 недели) — кластер из 3 нод, Ingress, TLS, мониторинг, бэкапы.

Этап 4: Оптимизация (постоянно) — Horizontal Pod Autoscaler, оптимизация resource limits, GitOps.

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

Проект занял 5 недель. Вот измеримые результаты:

  • Автомасштабирование — при вебинарах на 500 пользователей платформа автоматически поднимает дополнительные Pod-реплики за 30 секунд
  • Нулевой простой при деплое — Rolling Update вместо 15-минутного даунтайма
  • Самовосстановление — упавший сервис автоматически перезапускается за 10-15 секунд
  • Время деплоя — с 15 минут (с простоем) до 2 минут (без простоя)
  • Доступность — SLA поднялся с 97% до 99.9% за первый месяц
  • Мониторинг — полная видимость кластера через Grafana, алерты при аномалиях
  • Экономия — автомасштабирование позволяет использовать минимум ресурсов в спокойное время и увеличивать при пиках вместо постоянного overprovisioning

Первый настоящий тест новой инфраструктуры состоялся через неделю после запуска — клиент проводил вебинар на 450 участников одновременно. Автоскейлер автоматически поднял видеостриминговый сервис с 2 до 6 реплик за 30 секунд, API — с 3 до 5 реплик. Платформа работала без единого сбоя. Для сравнения: предыдущий вебинар на 300 участников на Docker Compose привёл к 40-минутному простою.

Команда разработки отметила кардинальное улучшение процесса деплоя: раньше каждый релиз сопровождался нервозностью и 15-минутным простоем, теперь команда деплоит 3-5 раз в день через CI/CD без какого-либо влияния на пользователей. Rolling Update заменяет Pod по одному, и пользователи не замечают обновления.

DevOps-инженер клиента получил полную документацию по кластеру, runbook для типичных ситуаций и прошёл 2-дневное обучение с нашим специалистом. Через месяц он уверенно управлял кластером самостоятельно.

Финансовый эффект оказался значительным. До миграции клиент арендовал 2 мощных VPS за 5 000 руб./мес. каждый, которые были постоянно загружены на 80-90%, даже когда нагрузка была минимальной. С Kubernetes на 3 VPS за 7 000 руб./мес. суммарно клиент получил больше ресурсов, но благодаря автоскейлингу может оптимально их использовать. При этом при пиковых нагрузках система автоматически использует все доступные ресурсы, тогда как раньше серверы просто падали.

Мы также настроили Horizontal Pod Autoscaler для видеостримингового сервиса по метрике CPU utilization. При нагрузке свыше 70% автоскейлер добавляет реплики, при снижении — убирает. Это обеспечивает оптимальное использование ресурсов: в ночное время работает 1 реплика, во время вебинара — до 8 реплик. Клиент платит только за реально используемые ресурсы.

CI/CD-пайплайн был интегрирован с GitHub Actions: при мерже в main автоматически собирается Docker-образ, пушится в container registry и применяется новый Deployment в кластере. Весь процесс от коммита до продакшена занимает 3-4 минуты без единой секунды простоя.

Часто задаваемые вопросы

Да, K3s и MicroK8s работают на одном сервере. K3s требует всего 512 МБ RAM. Но на одном сервере нет отказоустойчивости — для продакшена минимум 3 ноды. Для разработки одного сервера достаточно.

K3s — сертифицированный CNCF дистрибутив, полностью совместимый с API. Отличия: SQLite вместо etcd, один бинарный файл, containerd вместо Docker. Любые Helm-чарты и манифесты работают на K3s.

Для self-hosted Kubernetes — да, минимум на частичную занятость. Managed Kubernetes снижает нагрузку, но всё равно требует специалиста. Для Docker Compose DevOps не нужен.

Используйте kompose convert -f docker-compose.yml. Результат потребует доработки: resource limits, health checks, Ingress. Рекомендуется поэтапная миграция — сначала stateless-сервисы.

Если бюджет позволяет — однозначно да. Managed K8s снимает обслуживание control plane, автоматические обновления, интеграцию с облачными сервисами. Стоимость выше на 30-50%, но экономия времени DevOps окупает разницу.

Нужна помощь с настройкой?

Специалисты АйТи Фреш помогут с внедрением и настройкой — 15+ лет опыта, обслуживание от 15 000 ₽/мес

📞 Связаться с нами
#kubernetes малый бизнес#k8s внедрение#k3s установка#microk8s#kubernetes стоит ли#kubernetes для начинающих#kubernetes pod service#kubernetes деплой