11 ошибок при внедрении Kubernetes на проде: как мы спасли миграцию SaaS-платформы

Ситуация: миграция на Kubernetes провалилась

SaaS-платформа «КлаудСервис» — CRM для малого бизнеса, 12 000 клиентов, 45 микросервисов. Команда мигрировала с Docker Compose на Kubernetes (managed, Yandex Cloud) за 3 месяца. Через неделю после запуска в прод начался хаос: 3 инцидента за 5 дней, клиенты уходят, SLA нарушен.

Нас позвали в itfresh.ru как пожарную команду. За первый день аудита мы нашли 11 критических ошибок — каждая из них могла (и некоторые уже вызвали) привести к простою.

# Первичная диагностика кластера
kubectl get nodes
# NAME           STATUS   ROLES    AGE   VERSION
# node-prod-1    Ready    <none>   12d   v1.28.2
# node-prod-2    Ready    <none>   12d   v1.28.2
# node-prod-3    Ready    <none>   12d   v1.28.2

kubectl get pods --all-namespaces | grep -v Running | grep -v Completed
# NAMESPACE     NAME                           READY   STATUS             RESTARTS
# production    api-gateway-7d4f8b9c6-x2k4l    0/1     CrashLoopBackOff   47
# production    report-svc-5f6d7e8a9-m3n2p     0/1     OOMKilled          23
# production    search-svc-8a9b0c1d2-q4r5s     1/1     Running            12
# kube-system   coredns-6d4b75cb6d-7j8k9       0/1     CrashLoopBackOff   156

OOMKilled, CrashLoopBackOff, рестарты десятками — классические симптомы неправильно настроенного кластера. Разберём каждую ошибку.

Ошибки 1-3: ресурсы и health checks

Ошибка 1: Нет resource limits — OOM kills. Ни один deployment не имел requests/limits. Один pod мог съесть всю память ноды и убить соседей:

# ДО: deployment без ресурсных ограничений
spec:
  containers:
    - name: report-service
      image: cloudservice/report-svc:1.4.2
      # Ни requests, ни limits — pod может съесть всю ноду

# ПОСЛЕ: правильные resource limits
spec:
  containers:
    - name: report-service
      image: cloudservice/report-svc:1.4.2
      resources:
        requests:
          memory: "512Mi"
          cpu: "250m"
        limits:
          memory: "1Gi"
          cpu: "1000m"

Как определить правильные limits? Запустили pod без limits на staging с включённым metrics-server и наблюдали потребление 3 дня:

# Смотрим реальное потребление pod-ов
kubectl top pods -n production --sort-by=memory
# NAME                              CPU(cores)   MEMORY(bytes)
# report-svc-5f6d7e8a9-m3n2p       850m         1847Mi    ← OOM!
# api-gateway-7d4f8b9c6-x2k4l      120m         256Mi
# search-svc-8a9b0c1d2-q4r5s       340m         512Mi

# Правило: requests = среднее потребление, limits = пиковое × 1.5

Ошибка 2: Нет liveness/readiness probes. Kubernetes не знал, живы ли контейнеры. Pod мог зависнуть, но оставаться Running — и получать трафик:

# ПОСЛЕ: правильные probes
spec:
  containers:
    - name: api-gateway
      livenessProbe:
        httpGet:
          path: /healthz
          port: 8080
        initialDelaySeconds: 15
        periodSeconds: 10
        timeoutSeconds: 5
        failureThreshold: 3
      readinessProbe:
        httpGet:
          path: /ready
          port: 8080
        initialDelaySeconds: 5
        periodSeconds: 5
        timeoutSeconds: 3
        failureThreshold: 2
      startupProbe:   # для сервисов с долгой инициализацией
        httpGet:
          path: /healthz
          port: 8080
        initialDelaySeconds: 10
        periodSeconds: 5
        failureThreshold: 30  # 30 × 5 = 150 сек на старт

Ошибка 3: Single replica deployments. 8 из 45 сервисов работали с replicas=1. При рестарте ноды или деплое — даунтайм:

# Проверяем, какие deployment-ы имеют одну реплику
kubectl get deployments -n production -o custom-columns=\
  NAME:.metadata.name,REPLICAS:.spec.replicas | sort -k2 -n
# api-gateway     1    ← критичный сервис с 1 репликой!
# auth-service    1
# billing-svc     2
# ...

# Исправляем: минимум 2 реплики для всех сервисов
kubectl scale deployment api-gateway -n production --replicas=3
kubectl scale deployment auth-service -n production --replicas=2

Ошибки 4-6: отказоустойчивость и хранение

Ошибка 4: Нет PodDisruptionBudget. При обновлении ноды Kubernetes мог одновременно убить все pod-ы сервиса:

# PDB гарантирует минимальное количество доступных pod-ов
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api-gateway-pdb
  namespace: production
spec:
  minAvailable: 2       # минимум 2 pod-а всегда работают
  selector:
    matchLabels:
      app: api-gateway
---
# Или через maxUnavailable:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: search-svc-pdb
  namespace: production
spec:
  maxUnavailable: 1     # максимум 1 pod может быть недоступен
  selector:
    matchLabels:
      app: search-service

Ошибка 5: Нет pod anti-affinity. Все реплики одного сервиса оказывались на одной ноде. Падение ноды = полный даунтайм сервиса:

# Anti-affinity: реплики на разных нодах
spec:
  affinity:
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
        - weight: 100
          podAffinityTerm:
            labelSelector:
              matchExpressions:
                - key: app
                  operator: In
                  values:
                    - api-gateway
            topologyKey: kubernetes.io/hostname

Ошибка 6: Неправильный storage class. Использовали network-attached SSD для базы данных PostgreSQL в кластере. При переезде pod-а на другую ноду — 2-5 минут на re-attach диска:

# Проверяем storage classes
kubectl get sc
# NAME                PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE
# yc-network-ssd      disk-csi-driver         Delete          WaitForFirstConsumer
# yc-network-hdd      disk-csi-driver         Delete          WaitForFirstConsumer

# Для БД: local SSD + nodeSelector = минимальная латентность
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-ssd
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer

# PV на конкретной ноде с local SSD
apiVersion: v1
kind: PersistentVolume
metadata:
  name: postgres-pv
spec:
  capacity:
    storage: 100Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: local-ssd
  local:
    path: /mnt/ssd/postgres-data
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: kubernetes.io/hostname
              operator: In
              values:
                - node-prod-db-1

Ошибки 7-9: безопасность

Ошибка 7: Нет Network Policies. Любой pod мог обращаться к любому другому pod-у, включая базу данных. Компрометация одного сервиса = доступ ко всему:

# Default deny all ingress — запрещаем всё по умолчанию
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}   # применяется ко всем pod-ам
  policyTypes:
    - Ingress
    - Egress
---
# Разрешаем конкретные связи: api-gateway → auth-service
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-api-to-auth
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: auth-service
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: api-gateway
      ports:
        - protocol: TCP
          port: 8080
---
# PostgreSQL: только от конкретных сервисов
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-db-access
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: postgresql
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              db-access: "true"   # только pod-ы с этим лейблом
      ports:
        - protocol: TCP
          port: 5432

Ошибка 8: Нет RBAC. Все разработчики имели cluster-admin. Один junior случайно удалил namespace staging, потеряв все конфигурации:

# Role с минимальными правами для разработчика
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: developer-role
  namespace: production
rules:
  - apiGroups: [""]
    resources: ["pods", "pods/log", "services", "configmaps"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "list", "watch"]
  # Нет delete, create, update — только чтение
---
# RoleBinding: привязываем пользователя к роли
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: developer-binding
  namespace: production
subjects:
  - kind: User
    name: "developer@cloudservice.ru"
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: developer-role
  apiGroup: rbac.authorization.k8s.io

Ошибка 9: Непатченные ноды. Kubernetes 1.28.2 (текущий — 1.29.x), containerd не обновлялся 6 месяцев. Известные CVE открыты:

# Проверяем версии компонентов
kubectl get nodes -o wide
# CONTAINER-RUNTIME       OS-IMAGE            KERNEL-VERSION
# containerd://1.6.24     Ubuntu 22.04.3      5.15.0-86

# containerd 1.6.24 имеет CVE-2024-21626 (container escape!)
# Обновляем:
sudo apt update && sudo apt install -y containerd.io=1.7.13-1
sudo systemctl restart containerd

# Kubernetes node upgrade (managed кластер — через UI/API провайдера)
# Для self-managed:
sudo kubeadm upgrade apply v1.29.2
sudo systemctl restart kubelet

Ошибки 10-11: наблюдаемость и бэкапы

Ошибка 10: Нет централизованного логирования. Логи хранились только внутри контейнеров. При рестарте pod-а — логи теряются навсегда:

# Разворачиваем Loki + Promtail для сбора логов
# values.yaml для Helm chart loki-stack
loki:
  persistence:
    enabled: true
    size: 50Gi
  config:
    limits_config:
      retention_period: 30d
    schema_config:
      configs:
        - from: "2026-01-01"
          store: tsdb
          object_store: filesystem
          schema: v13
          index:
            prefix: index_
            period: 24h

promtail:
  config:
    clients:
      - url: http://loki:3100/loki/api/v1/push
    snippets:
      pipelineStages:
        - cri: {}
        - json:
            expressions:
              level: level
              trace_id: trace_id
        - labels:
            level:
            trace_id:

# Установка:
helm install loki grafana/loki-stack -n monitoring \
  -f values.yaml --create-namespace

Ошибка 11: Нет бэкапа etcd. Самая опасная ошибка. etcd — мозг кластера: все объекты, секреты, конфигурации. Потеря etcd = потеря всего кластера:

# Бэкап etcd (для self-managed кластеров)
# Скрипт /usr/local/bin/etcd-backup.sh
#!/bin/bash
BACKUP_DIR=/var/backups/etcd
TIMESTAMP=$(date +%Y%m%d_%H%M%S)

ETCDCTL_API=3 etcdctl snapshot save \
  "${BACKUP_DIR}/etcd-snapshot-${TIMESTAMP}.db" \
  --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key

# Проверяем целостность
ETCDCTL_API=3 etcdctl snapshot status \
  "${BACKUP_DIR}/etcd-snapshot-${TIMESTAMP}.db" --write-out=table

# Удаляем бэкапы старше 30 дней
find ${BACKUP_DIR} -name "etcd-snapshot-*.db" -mtime +30 -delete

# Копируем в S3
aws s3 cp "${BACKUP_DIR}/etcd-snapshot-${TIMESTAMP}.db" \
  s3://cloudservice-backups/etcd/

# Cron: каждые 6 часов
# 0 */6 * * * /usr/local/bin/etcd-backup.sh >> /var/log/etcd-backup.log 2>&1

Для managed-кластеров (Yandex Cloud, GKE, EKS) бэкап etcd делает провайдер, но обязательно проверьте: включена ли эта функция, какой retention period, можно ли восстановить конкретный объект.

Чеклист и результаты

После исправления всех 11 ошибок мы составили чеклист для production-ready Kubernetes:

  • ✓ Resource requests/limits на всех pod-ах
  • ✓ Liveness, readiness и startup probes
  • ✓ Минимум 2 реплики для каждого сервиса
  • ✓ PodDisruptionBudget для критичных сервисов
  • ✓ Pod anti-affinity для распределения по нодам
  • ✓ Правильный storage class для stateful workloads
  • ✓ Network Policies (default deny + явные разрешения)
  • ✓ RBAC с принципом минимальных привилегий
  • ✓ Регулярное обновление нод и runtime
  • ✓ Централизованное логирование (Loki/ELK)
  • ✓ Бэкап etcd каждые 6 часов

Результаты за месяц после исправлений:

МетрикаДоПосле
Инцидентов в неделю3-50
OOMKilled pod-ов в день12-200
Среднее время деплоя25 мин (с даунтаймом)4 мин (zero downtime)
Uptime96.2%99.95%
Время восстановления после сбоя ноды15-30 мин (ручное)2 мин (автоматическое)

Kubernetes — мощный инструмент, но он не прощает ошибок в конфигурации. Если вы планируете миграцию на Kubernetes или уже мигрировали, но сталкиваетесь с нестабильностью — обращайтесь к нам в itfresh.ru для аудита кластера.

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

Минимум 3 ноды для worker-ов, чтобы обеспечить anti-affinity и пережить потерю одной ноды. Для control plane — 3 master-а (или managed кластер, где провайдер это гарантирует). Для staging достаточно 2 нод, для dev — 1.
Запустите приложение без limits на staging, включите metrics-server и наблюдайте потребление через kubectl top pods в течение 3-5 дней. Requests = среднее потребление (это влияет на scheduling). Limits = пиковое потребление × 1.5 (это жёсткий потолок, при превышении по памяти — OOMKill).
Обязательно. Managed Kubernetes управляет control plane, но не вашими workloads. Без Network Policies любой скомпрометированный pod получит доступ к базе данных, секретам и другим сервисам. Это нарушает принцип defense in depth и создаёт риски при проверках безопасности.
Kubernetes поддерживает 3 последние minor-версии. Рекомендуем обновляться в течение 2-3 месяцев после выхода новой stable-версии. Критически важно: никогда не пропускайте больше одной minor-версии при обновлении (1.27 → 1.28 → 1.29, но НЕ 1.27 → 1.29). Для containerd и kubelet следите за CVE.
Можно, но с оговорками. Используйте операторы (CloudNativePG, Zalando Postgres Operator) вместо голых StatefulSet. Обязательны: local SSD для хранения, PDB, бэкапы WAL в S3, мониторинг replication lag. Для малых команд проще использовать managed базу (RDS, Yandex Managed PostgreSQL) — меньше операционной нагрузки.

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

Специалисты АйТи Фреш помогут с архитектурой, DevOps, безопасностью и разработкой — 15+ лет опыта

📞 Связаться с нами
#Kubernetes#resource limits#liveness probes#PDB#pod anti-affinity#network policies#RBAC#etcd backup
Комментарии 0

Оставить комментарий

загрузка...