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 нельзя. Варианты:
- SealedSecrets (Bitnami): шифруете секрет публичным ключом контроллера, в git кладёте зашифрованный объект.
- 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.small | 4 GB / 2 vCPU | ~3 000 ₽ |
| Малый прод (10K MAU) | 2 × s2.medium | 16 GB / 4 vCPU | ~12 000 ₽ |
| Средний (100K MAU) | 3 × s3.large | 48 GB / 12 vCPU | ~35 000 ₽ |
| Крупный (1M MAU) | 5 × c3.xlarge | 160 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
- Поднял k8s «потому что модно» — на 1000 пользователях оверкил, тратите больше на DevOps, чем на разработку.
maxUnavailable: 25%— теряете webhook'и во время деплоя.- Секреты в git как plain Secret — sealed secrets обязательны.
- PostgreSQL в кластере на ephemeral volumes — потеряете данные при restart pod'а.
- Один Ingress на 50 ботов — общая точка отказа, при reload роняет всех.
- Нет PodDisruptionBudget — drain ноды убивает все поды одновременно.
- Миграции в init container — параллельные поды конкурируют, dead lock.
- 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 для той же нагрузки в более простой конфигурации.