Legan Studio
Все статьи
~ 7 мин чтения

Kubernetes для бота MAX: когда нужен и как разворачивать

Когда боту в MAX нужен Kubernetes, а когда хватит Docker Compose. Helm-чарты, HPA, секреты, webhook-балансировка, миграции, observability и реальные расходы.

  • MAX
  • Kubernetes
  • DevOps

Kubernetes для бота — модный, но часто избыточный выбор. На этапе MVP и первой 1000 пользователей он стоит дороже, чем приносит пользы. Но при 100K+ активных пользователей, нескольких ботах в одной команде и жёстких требованиях SLA — становится единственным разумным вариантом. В этой статье разберём, когда боту в MAX реально нужен k8s, как организовать webhook за Ingress, балансировку, секреты, HPA, миграции, observability и сколько это стоит на месяц для типичных нагрузок.

Когда k8s избыточен

Боту в MAX не нужен Kubernetes, если:

  • < 10 000 активных пользователей в день;
  • один разработчик / небольшая команда;
  • нет требований SLA 99.9%+;
  • нет нескольких сервисов вокруг бота;
  • бюджет на инфраструктуру < 10 000 ₽/мес;
  • команда не умеет в k8s (а время дороже железа).

Достаточно VPS на 4 vCPU / 8 GB + Docker Compose + nginx + Postgres managed. Это покрывает большинство коммерческих ботов. Стоимость 2–5 тыс. ₽/мес.

Когда k8s оправдан

  • 100K+ активных пользователей, пиковая нагрузка > 200 RPS на webhook;
  • несколько ботов / микросервисов с общей инфраструктурой;
  • SLA 99.95%+ и multi-AZ deployment;
  • автоскейлинг — днём 20 подов, ночью 4;
  • DevOps-команда уже работает с k8s;
  • сложный CD-пайплайн с canary, blue/green;
  • мульти-регионный deployment;
  • частые релизы (5+ в день) с zero-downtime.

В реальности — это 5% проектов. Остальные приходят к k8s по принципу «модно», а потом 6 месяцев борются с YAML и платят $500/мес за то, что раньше работало на $30 VPS.

Архитектура bot в k8s

Минимальный набор:

[Ingress nginx] ──▶ [Service bot-webhook] ──▶ [Deployment: bot-app x N]
                                                    │
                                                    ▼
                            [PostgreSQL HA (Patroni / managed Yandex)]
                            [Redis cluster (managed)]
                            [S3 (Yandex Object Storage)]
                                                    │
                                                    ▼
                                  [CronJob: рассылки, отчёты]
                                  [Job: миграции БД]
                                  [Deployment: worker для тяжёлых задач]

Helm-chart для бота

Структура:

charts/botmax/
├── Chart.yaml
├── values.yaml
├── values-staging.yaml
├── values-prod.yaml
└── templates/
    ├── deployment.yaml
    ├── service.yaml
    ├── ingress.yaml
    ├── hpa.yaml
    ├── configmap.yaml
    ├── secret.yaml          # SealedSecret в реальности
    ├── migration-job.yaml
    └── cronjob-broadcast.yaml

values.yaml:

image:
  repository: registry.example.ru/botmax-backend
  tag: "1.4.2"
  pullPolicy: IfNotPresent

replicaCount: 4

resources:
  requests:
    cpu: 200m
    memory: 256Mi
  limits:
    cpu: 1000m
    memory: 512Mi

autoscaling:
  enabled: true
  minReplicas: 4
  maxReplicas: 30
  targetCPU: 70

ingress:
  host: api.max.example.ru
  tls:
    secretName: max-bot-tls

env:
  BACKEND_URL: "http://bot-svc:8080"
  POSTGRES_HOST: "postgres-cluster.botmax.svc"
  REDIS_URL: "redis://redis-master.botmax.svc:6379/0"

secrets:
  MAX_BOT_TOKEN: vault:secret/botmax/token#value
  WEBHOOK_SECRET: vault:secret/botmax/webhook#secret

Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: botmax-app
spec:
  replicas: {{ .Values.replicaCount }}
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  selector:
    matchLabels:
      app: botmax
  template:
    metadata:
      labels: { app: botmax }
    spec:
      containers:
        - name: app
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          ports:
            - containerPort: 8080
          envFrom:
            - configMapRef: { name: botmax-config }
            - secretRef:    { name: botmax-secrets }
          readinessProbe:
            httpGet: { path: /health, port: 8080 }
            initialDelaySeconds: 5
            periodSeconds: 5
          livenessProbe:
            httpGet: { path: /health, port: 8080 }
            initialDelaySeconds: 30
            periodSeconds: 30
          resources:
            {{- toYaml .Values.resources | nindent 12 }}

maxUnavailable: 0 — критично для webhook'а: ни одного потерянного запроса во время деплоя.

Webhook за Ingress

Webhook бота — обычный HTTP-эндпоинт. Ingress nginx с annotation для proxy-buffering и таймаутов:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: botmax-webhook
  annotations:
    nginx.ingress.kubernetes.io/proxy-body-size: "8m"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "60"
    nginx.ingress.kubernetes.io/proxy-buffering: "off"
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  tls:
    - hosts: [api.max.example.ru]
      secretName: max-bot-tls
  rules:
    - host: api.max.example.ru
      http:
        paths:
          - path: /max/webhook
            pathType: Prefix
            backend:
              service:
                name: bot-svc
                port: { number: 8080 }

cert-manager автоматически выпускает Let's Encrypt сертификат при создании Ingress.

HPA: автомасштабирование

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: botmax-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: botmax-app
  minReplicas: 4
  maxReplicas: 30
  metrics:
    - type: Resource
      resource: { name: cpu, target: { type: Utilization, averageUtilization: 70 } }
    - type: Pods
      pods:
        metric: { name: http_requests_per_second }
        target: { type: AverageValue, averageValue: "100" }
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 30
      policies:
        - type: Pods
          value: 4
          periodSeconds: 30
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
        - type: Percent
          value: 25
          periodSeconds: 60

scaleDown намеренно медленнее scaleUp — лучше платить лишнее за 5 минут переездом, чем оказаться без подов на пике.

Секреты: SealedSecrets / External Secrets

Хранить чувствительные значения в Secret манифестах в git нельзя. Варианты:

  1. SealedSecrets (Bitnami): шифруете секрет публичным ключом контроллера, в git кладёте зашифрованный объект.
  2. External Secrets Operator (рекомендую) — синхронизирует из Vault/Yandex KMS/AWS Secret Manager:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: botmax-secrets
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: vault-backend
    kind: ClusterSecretStore
  target:
    name: botmax-secrets
  data:
    - secretKey: MAX_BOT_TOKEN
      remoteRef: { key: secret/botmax, property: token }
    - secretKey: WEBHOOK_SECRET
      remoteRef: { key: secret/botmax, property: webhook_secret }

ESO раз в час подтягивает свежие значения из Vault, обновляет Secret, пересоздаёт поды (или вы используете Reloader для ручного триггера).

Миграции БД как Job

apiVersion: batch/v1
kind: Job
metadata:
  name: botmax-migrate-{{ .Release.Revision }}
  annotations:
    "helm.sh/hook": pre-upgrade,pre-install
    "helm.sh/hook-weight": "-5"
    "helm.sh/hook-delete-policy": hook-succeeded
spec:
  backoffLimit: 0
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: migrate
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          command: ["alembic", "upgrade", "head"]
          envFrom:
            - secretRef: { name: botmax-secrets }
            - configMapRef: { name: botmax-config }

pre-upgrade гарантирует: миграция запускается до обновления Deployment. Если миграция падает — Deployment не апдейтится, прод остаётся на старой версии. Подробнее в статье «Миграции БД для бота MAX».

Рассылки и фоновые задачи: CronJob и worker

Массовая рассылка (статья «Рассылки в MAX без блокировок») — отдельный Job, не основной Deployment:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: botmax-broadcast-daily
spec:
  schedule: "0 10 * * *"      # каждый день в 10:00
  concurrencyPolicy: Forbid
  jobTemplate:
    spec:
      backoffLimit: 0
      template:
        spec:
          restartPolicy: Never
          containers:
            - name: broadcast
              image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
              command: ["python", "-m", "tasks.broadcast", "--segment", "daily"]
              envFrom:
                - secretRef: { name: botmax-secrets }

Тяжёлые задачи (генерация отчётов, RAG-индексация) — отдельный Deployment с собственными ресурсами.

Observability: Prometheus + Grafana + Loki

Стандартный стек:

  • Prometheus скрапит /metrics каждого пода;
  • Loki + Promtail агрегирует логи;
  • Grafana отображает метрики и логи;
  • Alertmanager шлёт алерты в Telegram/MAX-чат поддержки.

Минимум метрик в боте:

from prometheus_client import Counter, Histogram

webhook_requests = Counter("bot_webhook_requests_total", "Webhook requests", ["status"])
message_handler_duration = Histogram("bot_handler_duration_seconds", "Handler duration", ["handler"])
llm_tokens = Counter("bot_llm_tokens_total", "LLM tokens", ["model", "type"])

Подробнее — в статье «Prometheus и Grafana для бота MAX».

NetworkPolicy и безопасность

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: botmax-app-policy
spec:
  podSelector:
    matchLabels: { app: botmax }
  policyTypes: [Ingress, Egress]
  ingress:
    - from:
        - namespaceSelector:
            matchLabels: { name: ingress-nginx }
      ports: [{ protocol: TCP, port: 8080 }]
  egress:
    - to:
        - podSelector:
            matchLabels: { app: postgres }
      ports: [{ protocol: TCP, port: 5432 }]
    - to:
        - podSelector:
            matchLabels: { app: redis }
      ports: [{ protocol: TCP, port: 6379 }]
    - to:                # внешние API: MAX, Yandex Cloud, GigaChat
        - ipBlock: { cidr: 0.0.0.0/0 }
      ports:
        - { protocol: TCP, port: 443 }

Default deny + explicit allow — стандарт production.

Стоимость

Реалистичные расчёты для бота MAX в managed k8s (Yandex / Selectel):

РазмерУзлыRAM/CPUЦена/мес
Тестовый1 × s2.small4 GB / 2 vCPU~3 000 ₽
Малый прод (10K MAU)2 × s2.medium16 GB / 4 vCPU~12 000 ₽
Средний (100K MAU)3 × s3.large48 GB / 12 vCPU~35 000 ₽
Крупный (1M MAU)5 × c3.xlarge160 GB / 40 vCPU~120 000 ₽

Сюда добавьте managed PostgreSQL HA (от 5 000 ₽), Object Storage (1–5 000 ₽), Load Balancer (~1 500 ₽), мониторинг (бесплатно, если open source). Для сравнения — VPS + Docker Compose для 10K MAU = 2 000 ₽/мес.

Common pitfalls

  1. Поднял k8s «потому что модно» — на 1000 пользователях оверкил, тратите больше на DevOps, чем на разработку.
  2. maxUnavailable: 25% — теряете webhook'и во время деплоя.
  3. Секреты в git как plain Secret — sealed secrets обязательны.
  4. PostgreSQL в кластере на ephemeral volumes — потеряете данные при restart pod'а.
  5. Один Ingress на 50 ботов — общая точка отказа, при reload роняет всех.
  6. Нет PodDisruptionBudget — drain ноды убивает все поды одновременно.
  7. Миграции в init container — параллельные поды конкурируют, dead lock.
  8. HPA только по CPU — для I/O-bound бота этого мало, добавьте custom metric requests/sec.

Итого

Kubernetes для бота MAX оправдан при 100K+ MAU, нескольких сервисах, требованиях SLA 99.95%+ или сильной DevOps-экспертизе в команде. Минимальный stack: Helm chart с Deployment + Service + Ingress (nginx + cert-manager Let's Encrypt) + HPA + ConfigMap + ExternalSecret. Миграции БД — pre-upgrade Job. Рассылки и фоновые задачи — CronJob и отдельный Deployment. Секреты через ESO + Vault. NetworkPolicy в режиме default-deny. Стоимость для среднего бота 100K MAU — около 35 тыс. ₽/мес инфры + DevOps-инженер. Если у вас 1000 пользователей и одна нода Docker Compose тянет — оставайтесь на ней, k8s подождёт.

Частые вопросы

Когда боту в MAX реально нужен Kubernetes?

K8s оправдан при 100K+ активных пользователей в день, пиковом RPS 200+ на webhook, нескольких связанных сервисах вокруг бота, требованиях SLA 99.95%+ с multi-AZ и опыте DevOps в команде. До этой планки достаточно VPS с Docker Compose, nginx и managed Postgres — это покрывает 90% коммерческих ботов и стоит 2–5 тыс. ₽/мес против 35–50 тыс. ₽/мес для k8s. Внедрять k8s «на вырост» обычно дороже, чем мигрировать позже, когда нагрузка реально вырастет.

Как обеспечить zero-downtime деплой webhook бота в k8s?

Используйте RollingUpdate с maxSurge: 1 и maxUnavailable: 0 — гарантирует, что во время обновления количество доступных подов не падает. Обязательно настройте readinessProbe на /health — pod исключается из Service до того, как пройдёт первый health check. preStop hook со sleep 10 секунд позволяет завершить inflight-запросы перед остановкой. PodDisruptionBudget с minAvailable: 80% защищает от одновременного убийства всех подов при drain'е ноды.

Где хранить секреты бота в k8s?

Не в обычных Secret манифестах в git — они только base64-encoded, не зашифрованы. Два рабочих варианта: SealedSecrets (Bitnami) — шифруете публичным ключом кластера, в git кладёте sealed-объект; External Secrets Operator + Vault/Yandex KMS — секреты живут в централизованном vault, ESO синхронизирует их в Kubernetes Secret раз в час. ESO предпочтительнее в командах с несколькими сервисами и общим vault, SealedSecrets проще для старта.

Как настроить HPA для бота, который I/O-bound, а не CPU-bound?

HPA только по CPU не сработает — бот может тратить 10% CPU и при этом тонуть в I/O ожидании на БД и LLM. Добавьте custom metric через prometheus-adapter: например, http_requests_per_second на под, целевое значение 100 RPS. Дополнительно — метрика длины очереди задач, если используете worker. behavior.scaleUp быстрый (30 сек stabilization, +4 пода за раз), scaleDown медленный (5 минут stabilization, -25%) — это балансирует между расходами и надёжностью на пиках.

Как запускать миграции БД в k8s правильно?

Используйте Helm hook pre-upgrade с Job, у которого backoffLimit: 0 (не ретраить миграцию автоматически). Helm запустит Job до обновления Deployment, дождётся успеха, и только потом выкатит новый образ. Если миграция упала — Deployment остаётся на старой версии, прод не падает. Не делайте миграцию в initContainer самого Deployment — параллельные поды одновременно запустят миграции, что приведёт к дедлокам или гонкам в Alembic version table.

Что выбрать: managed k8s или self-hosted?

Для 95% команд — managed (Yandex Managed Kubernetes, Selectel MKS). Стоимость control-plane обычно бесплатна или 1–2 тыс. ₽/мес, а вы экономите DevOps-инженера на поддержке etcd, апгрейдах, бэкапах кластера. Self-hosted (kubeadm, k3s, RKE) оправдан только для крупных компаний с собственной инфраструктурой и DevOps-командой 3+ человек, или при требованиях информационной безопасности типа «данные не покидают наши стойки».

Сколько стоит k8s-кластер для бота со 100K активных пользователей?

Реалистично — 35–50 тыс. ₽/мес: 3 ноды s3.large (48 GB / 12 vCPU суммарно) для приложения, managed PostgreSQL HA с replica (~10 тыс. ₽), Redis cluster (~3 тыс. ₽), Object Storage (1–3 тыс. ₽), Load Balancer (~1.5 тыс. ₽), резерв на трафик. Сюда не входит DevOps-инженер — обычно 0.3–0.5 ставки на поддержку, что добавляет ещё 50–100 тыс. ₽/мес. Сравните с 5–8 тыс. ₽/мес за VPS + Docker Compose для той же нагрузки в более простой конфигурации.