Перейти к содержимому

Docker Compose и Kubernetes: когда пора переходить

Константин Потапов
18 мин

Три сервера, SSH, молитва. 5000 RPS. Дальше Compose превращается в ад поддержки. Helm, kind и когда лучше остаться.

API держал около 5000 RPS в пик. Compose крутил восемь копий на трёх серверах. Деплой: SSH на каждый, git pull, docker-compose up -d --build, молитва. Обычно падало.

Rolling update руками, 30-60 секунд простоя на сервер. Ночью умерла машина, балансировщик продолжал слать туда трафик, узнали утром от клиентов. Автоскейлинга нет: поднял сервер, час спустя он простаивает, платишь за воздух. Health check смотрел, жив ли контейнер, не живо ли приложение.

Дальше масштабировать Compose значило кормить ад поддержки. Нужен был оркестратор. Не обязательно Kubernetes.

Compose хорош локально и на одном-трёх серверах до примерно 500 RPS. Дальше простой начинает стоить денег.

Что Compose не закрывает

С Compose даже docker-compose up -d --no-deps --build service оставляет щель в несколько секунд. Пользователь её видит.

В Kubernetes новые поды стартуют, проходят readiness, начинают принимать трафик, старые уходят. Откат: kubectl rollout undo.

# Деплой новой версии
kubectl set image deployment/myapp myapp=myapp:v2
 
# Откат, если что-то пошло не так
kubectl rollout undo deployment/myapp

Контейнер умер: restart: unless-stopped его поднимет. Умер сервер: SSH на новый и поднимай стек. В k8s под падает, scheduler поднимает другой. Нода падает, поды уезжают на живые.

В Compose состояние смотришь по SSH. В k8s пишешь replicas: 5 и kubectl apply. Git источник правды. ArgoCD или Flux применяют сами. Откат: git revert.

# Commit изменений в манифестах
git commit -m "Scale app to 10 replicas"
git push
 
# ArgoCD или Flux автоматически применят изменения
# Rollback? Просто git revert и push

healthcheck в Compose: контейнер ответил на /health. База лежит, контейнер жив, пользователи получают 500. В k8s два зонда. Liveness: убить и поднять заново. Readiness: пока не готов, трафик не слать.

livenessProbe:
  httpGet:
    path: /health/live
    port: 8080
  initialDelaySeconds: 30
  periodSeconds: 10
 
readinessProbe:
  httpGet:
    path: /health/ready
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5

Автоскейлинг в Compose: руками или cron. Horizontal Pod Autoscaler смотрит CPU, память или свою метрику из Prometheus.

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: myapp-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: myapp
  minReplicas: 3
  maxReplicas: 20
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
Docker Compose
Kubernetes
Деплой
SSH + молитва
kubectl apply (zero-downtime)
Downtime
30-60 сек на сервер
0 секунд
100%
Автоскейлинг
Руками или костыли
HPA автоматически
Восстановление
Вручную в 3 ночи
Self-healing встроен

Compose оставляю для локальной разработки, монолита на одном-двух серверах при 100-200 RPS, MVP и команды без DevOps. Там проще Render или Fly.io, чем кластер.

Kubernetes решает сложные задачи. Если задачи простые, он создаёт новые.

Минимум понятий

Говорите кластеру: пять копий, 2 CPU, 4 GB. Он решает куда поставить, следит и перезапускает.

Pod: один или несколько контейнеров на одной ноде.

apiVersion: v1
kind: Pod
metadata:
  name: myapp-pod
spec:
  containers:
    - name: myapp
      image: myapp:latest
      ports:
        - containerPort: 8080

Deployment: сколько реплик, какой образ, как обновлять.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: myapp
          image: myapp:v1.0.0
          ports:
            - containerPort: 8080

Service: стабильный IP и DNS. ClusterIP внутри, NodePort снаружи, LoadBalancer в облаке.

apiVersion: v1
kind: Service
metadata:
  name: myapp-service
spec:
  selector:
    app: myapp
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080
  type: LoadBalancer

Ingress: HTTP снаружи внутрь.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: myapp-ingress
spec:
  rules:
    - host: myapp.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: myapp-service
                port:
                  number: 80

ConfigMap и Secret вместо переменных в compose.

apiVersion: v1
kind: ConfigMap
metadata:
  name: myapp-config
data:
  DATABASE_URL: "postgresql://user@db:5432/mydb"
  LOG_LEVEL: "info"
---
apiVersion: v1
kind: Secret
metadata:
  name: myapp-secrets
type: Opaque
data:
  DB_PASSWORD: cGFzc3dvcmQxMjM= # base64 encoded

k8s ставит поды, масштабирует, лечит, даёт DNS, rolling update, секреты. Не ставит мониторинг, CI, бэкапы и безопасность. Это Prometheus, ArgoCD, Velero, RBAC отдельно.

Helm

Одно приложение: Deployment, Service, Ingress, ConfigMap, Secret. Двести строк. Три окружения с разными репликами и доменами. Без Helm: копипаста и Find & Replace.

myapp/
  Chart.yaml
  values.yaml
  templates/
    deployment.yaml
    service.yaml
    ingress.yaml
    configmap.yaml

Было:

version: "3.8"
services:
  app:
    image: myapp:latest
    ports:
      - "8080:8080"
    environment:
      DATABASE_URL: postgresql://user:pass@db:5432/mydb
      REDIS_URL: redis://redis:6379
    depends_on:
      - db
      - redis
 
  db:
    image: postgres:15
    environment:
      POSTGRES_PASSWORD: secretpass
    volumes:
      - db_data:/var/lib/postgresql/data
 
  redis:
    image: redis:7-alpine
 
volumes:
  db_data:

Стало, values.yaml:

replicaCount: 3
 
image:
  repository: myapp
  tag: "v1.0.0"
  pullPolicy: IfNotPresent
 
service:
  type: LoadBalancer
  port: 80
  targetPort: 8080
 
ingress:
  enabled: true
  host: myapp.example.com
 
env:
  DATABASE_URL: postgresql://user@postgres:5432/mydb
  REDIS_URL: redis://redis:6379
 
resources:
  limits:
    cpu: 500m
    memory: 512Mi
  requests:
    cpu: 250m
    memory: 256Mi
 
autoscaling:
  enabled: true
  minReplicas: 3
  maxReplicas: 10
  targetCPUUtilizationPercentage: 70
apiVersion: apps/v1
kind: Deployment
metadata:
  name: { { include "myapp.fullname" . } }
spec:
  replicas: { { .Values.replicaCount } }
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: myapp
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          ports:
            - containerPort: { { .Values.service.targetPort } }
          env:
            - name: DATABASE_URL
              value: { { .Values.env.DATABASE_URL } }
            - name: REDIS_URL
              value: { { .Values.env.REDIS_URL } }
          resources: { { - toYaml .Values.resources | nindent 10 } }
          livenessProbe:
            httpGet:
              path: /health
              port: { { .Values.service.targetPort } }
            initialDelaySeconds: 30
          readinessProbe:
            httpGet:
              path: /health/ready
              port: { { .Values.service.targetPort } }
            initialDelaySeconds: 5
# Установка chart
helm install myapp ./myapp
 
# Обновление с новыми параметрами
helm upgrade myapp ./myapp --set image.tag=v1.1.0
 
# Откат к предыдущей версии
helm rollback myapp
 
# Удаление
helm uninstall myapp
# Dev (1 реплика, маленькие ресурсы)
helm install myapp-dev ./myapp -f values-dev.yaml
 
# Staging (3 реплики)
helm install myapp-staging ./myapp -f values-staging.yaml
 
# Production (10 реплик, HPA)
helm install myapp-prod ./myapp -f values-prod.yaml
KubernetesHelmDockerkubectl

Готовые чарты на Artifact Hub.

# PostgreSQL
helm repo add bitnami https://charts.bitnami.com/bitnami
helm install postgres bitnami/postgresql
 
# Redis
helm install redis bitnami/redis
 
# Nginx Ingress Controller
helm install nginx-ingress ingress-nginx/ingress-nginx
 
# Prometheus + Grafana (мониторинг)
helm install monitoring prometheus-community/kube-prometheus-stack

Локально

Полный кластер ноутбуку тяжёл. kind, minikube, k3d.

Критерийkindminikubek3d
ОсноваDocker контейнерыVM (VirtualBox/Docker)k3s в Docker
Скорость запускаОчень быстро (10-20 сек)Медленно (1-2 мин)Быстро (20-30 сек)
RAMМало (~2GB)Много (~4-8GB)Мало (~1-2GB)
Multi-node кластерДаДа (сложнее)Да
LoadBalancer supportЧерез MetalLBИз коробкиИз коробки
Близость к prodОчень близкоБлизкоk3s не полный k8s
Лучше дляCI/CD, тестированиеОбучение, devБыстрый dev

kind для CI и близости к прод-k8s. minikube для учёбы, если RAM не жалко. k3d на каждый день.

brew install kind kubectl
 
# Или через binary
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.20.0/kind-darwin-arm64
chmod +x ./kind
mv ./kind /usr/local/bin/kind
# Базовый single-node кластер
kind create cluster --name dev
 
# Multi-node кластер (1 control-plane + 2 workers)
cat <<EOF | kind create cluster --config=-
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: worker
EOF
kubectl cluster-info
kubectl get nodes
# Загрузка локального образа в kind
kind load docker-image myapp:latest --name dev
 
# Деплой
kubectl apply -f deployment.yaml
kubectl get pods
kubectl logs -f <pod-name>
 
# Проброс порта для доступа
kubectl port-forward deployment/myapp 8080:8080
# Теперь доступно на http://localhost:8080
kind delete cluster --name dev
brew install k3d
 
# Или curl
curl -s https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash
# Кластер с 3 worker нодами и LoadBalancer на порту 8080
k3d cluster create dev \
  --agents 3 \
  --port "8080:80@loadbalancer"
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
 
# Если Service type: LoadBalancer, доступен на localhost:8080
curl http://localhost:8080
k3d cluster stop dev
k3d cluster start dev

Минимум 8 GB RAM и 4 ядра, лучше 16 и 6. Docker Desktop режу до 6 GB и 4 CPU. В манифестах лимиты. Не поднимаю фронт, если тестирую API. После тестов kubectl delete: остановленный под память всё равно ест.

Если ноутбук задыхается, дешевле облачный dev-кластер за $10-20 в месяц, чем день на перезагрузки.

Как смотреть, что сломалось

# Получить список подов
kubectl get pods
 
# Подробная информация о поде
kubectl describe pod <pod-name>
 
# Логи пода
kubectl logs <pod-name>
kubectl logs <pod-name> -f  # follow (real-time)
kubectl logs <pod-name> --previous  # логи предыдущего упавшего контейнера
 
# Логи всех подов deployment
kubectl logs -l app=myapp --all-containers=true
 
# Exec в под (как docker exec)
kubectl exec -it <pod-name> -- /bin/bash
 
# Проброс порта
kubectl port-forward pod/<pod-name> 8080:8080
 
# События кластера
kubectl get events --sort-by='.lastTimestamp'
 
# Top (CPU/Memory usage)
kubectl top nodes
kubectl top pods

Pending: describe, обычно не хватает CPU/памяти или нод. CrashLoopBackOff: logs --previous. Running и молчит: readiness. Service пустой: kubectl get endpoints, поды не готовы.

# Добавляем репозиторий
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
 
# Устанавливаем kube-prometheus-stack (Prometheus + Grafana + Alertmanager)
helm install monitoring prometheus-community/kube-prometheus-stack
 
# Получаем пароль Grafana
kubectl get secret monitoring-grafana -o jsonpath="{.data.admin-password}" | base64 --decode
 
# Проброс порта для доступа
kubectl port-forward svc/monitoring-grafana 3000:80
# Grafana доступна на http://localhost:3000 (admin / <password>)
helm repo add grafana https://grafana.github.io/helm-charts
helm install loki grafana/loki-stack \
  --set grafana.enabled=false \
  --set promtail.enabled=true
 
# Добавьте Loki как data source в Grafana
# URL: http://loki:3100

В Explore: {app="myapp"} и {app="myapp"} |= "ERROR".

Если не Kubernetes

Nomad: один бинарь, 50 MB RAM, Docker и не только Docker, Consul и Vault рядом. Экосистема меньше, Ingress нет, managed от облаков нет. Имеет смысл команде до десяти человек на стеке HashiCorp.

job "myapp" {
  datacenters = ["dc1"]
 
  group "app" {
    count = 3
 
    task "web" {
      driver = "docker"
 
      config {
        image = "myapp:v1.0.0"
        ports = ["http"]
      }
 
      resources {
        cpu    = 500
        memory = 512
      }
 
      service {
        name = "myapp"
        port = "http"
 
        check {
          type     = "http"
          path     = "/health"
          interval = "10s"
          timeout  = "2s"
        }
      }
    }
  }
}

ECS: если уже весь AWS. Fargate без серверов. Lock-in и цена, особенно Fargate.

Fly.io: fly deploy, как Heroku с Docker, точки по миру, три бесплатные VM. Контроля меньше, счёт растёт вместе с нагрузкой.

# Установка CLI
brew install flyctl
 
# Вход
fly auth login
 
# Инициализация приложения
fly launch
 
# Деплой
fly deploy
 
# Масштабирование
fly scale count 5
fly scale vm shared-cpu-2x

Docker Swarm: если знаете Compose, освоите за день. docker stack deploy -c docker-compose.yml. Docker Inc его почти не развивает.

# Инициализация Swarm
docker swarm init
 
# Деплой compose файла
docker stack deploy -c docker-compose.yml myapp
 
# Масштабирование
docker service scale myapp_web=5
КритерийKubernetesNomadAWS ECSFly.ioDocker Swarm
СложностьВысокаяСредняяНизкаяОчень низкаяНизкая
ЭкосистемаОгромнаяСредняяAWS-onlyМаленькаяМёртвая
ГибкостьМаксимумВысокаяСредняяНизкаяСредняя
Vendor lock-inНетНетAWSFly.ioНет
СтоимостьDIY дёшево, managed дорогоDIY дёшевоДорогоРастётДёшево
Для когоСредние/крупные командыМалые команды, HashiCorp стекAWS-native проектыСтартапыМалые проекты

Compose хватает: остаюсь. Перерос: сначала Fly или Nomad. Kubernetes когда нужна его гибкость, не строчка в резюме.

Цена перехода

Чтобы деплоить: 20-40 часов. Чтобы держать прод: 100-200. Архитектор: 500+.

Managed EKS/GKE/AKS: control plane $70-150, ноды $50-200, addons ваши. Self-hosted: софт бесплатный, команда нет.

Пять микросервисов, 20-30 подов, около 2000 RPS. GKE $250, 10-15 часов DevOps в месяц, две недели на онбординг. Окупается, если экономите на ручном масштабировании и простоях.

Не иду в k8s, если разработчиков меньше трёх, монолит на одном сервере, нет DevOps, это MVP или бюджет меньше $500. Managed дорог, свой кластер без экспертизы опасен.

Как переезжал

Неделя-две: туториал kubernetes.io, локальный kind, аудит сервисов и секретов, выбор managed или свой, registry в CI.

Дальше самый простой stateless.

api:
  image: myapi:latest
  ports:
    - "8080:8080"
  environment:
    DATABASE_URL: postgresql://db:5432/mydb
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
        - name: api
          image: myapi:v1.0.0
          ports:
            - containerPort: 8080
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: api-secrets
                  key: database-url
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 500m
              memory: 512Mi
          livenessProbe:
            httpGet:
              path: /health
              port: 8080
          readinessProbe:
            httpGet:
              path: /health/ready
              port: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: api
spec:
  selector:
    app: api
  ports:
    - port: 80
      targetPort: 8080
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: main-ingress
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - api.example.com
      secretName: api-tls
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: api
                port:
                  number: 80

Базы в k8s не тороплю. RDS и Cloud SQL проще. Если всё же тащить: StatefulSet, PVC, бэкап, учебный restore. Redis можно чартом Bitnami. Файлы: S3 или PVC ReadWriteMany.

Потом kube-prometheus-stack и Loki. Трафик 10%, неделя метрик, 50%, 100%. Старый Compose живёт ещё две недели.

Я перешёл в 2021-м. Первые два месяца боль и грабли. Потом инфраструктура, которая масштабируется без меня, поднимается сама и даёт десять деплоев в день без окна. Это стоит времени, если задача достаточно сложная. Если нет, Compose и сон спокойнее.

См. также: