Посчитай на досуге простую арифметику своего облачного биллинга. Стандартная рабочая неделя инженеров, тестировщиков и продакт-менеджеров длится около 45–50 часов: с девяти утра до семи вечера с понедельника по пятницу. В сутках 24 часа, в неделе — 168 часов. Это означает, что тестовые и предпроизводственные среды (development, QA, staging) используются активными людьми меньше чем треть времени.
Остальные 118 часов в неделю — а это почти 70% всего времени жизни кластера — дорогостоящие виртуальные машины в AWS EKS, Google GKE или арендованные ноды в дата-центре просто молотят воздух. Поды крутятся, базы данных простаивают, а счетчик денег в облачной панели монотонно тикает в пользу провайдера.
Казалось бы, решение лежит на поверхности: настроить расписание и гасить поды на ночь. Но в стандартном арсенале Kubernetes тебя ждет неприятный сюрприз.
Архитектурный тупик Horizontal Pod Autoscaler
Первая мысль любого системного инженера — воспользоваться нативным механизмом Horizontal Pod Autoscaler (HPA). Но здесь мы упираемся в жесткое архитектурное ограничение ядра Kubernetes: поле minReplicas в спецификации HPA принципиально не может быть меньше единицы.
Если ты попытаешься применить манифест с minReplicas: 0, API-сервер отвергнет его на этапе валидации схемы. Разработчики контроллера исходили из консервативной логики: HPA оценивает загрузку CPU и памяти существующих подов. Если подов в кластере ноль, то и метрики с них собирать невозможно, а значит, система никогда не узнает, когда нужно снова проснуться.
В итоге команды либо пишут громоздкие самописные bash-скрипты, которые по ночам через cron дергают kubectl scale deployment --replicas=0, либо мирятся с гигантскими переплатами.
К счастью, в экосистеме Cloud Native есть зрелый проект уровня CNCF Graduated — KEDA (Kubernetes Event-Driven Autoscaling). Инженер Мейндерт Стёй (Mijndert Stuij) в своем руководстве показал, как элегантно усыпить dev-окружение с помощью триггера KEDA Cron Scaler.
Как KEDA обходит ограничение нулевой реплики
Секрет KEDA кроется в четком разделении обязанностей между двумя разными уровнями управления:
- Режим бодрствования (от 1 до Max реплик): когда рабочее время активно, KEDA динамически создает и контролирует обычный HPA-объект, позволяя сервису свободно масштабироваться под нагрузкой.
- Переход в ноль (Scale to Zero): когда наступает время сна, контроллер KEDA берет управление на себя и напрямую перезаписывает поле
.spec.replicasв целевом Deployment, выставляя его в ноль.
В манифесте ScaledObject мы объявляем временное окно доступности сервиса:
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: backend-service-autoscaler
namespace: development
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: backend-service
minReplicaCount: 0
maxReplicaCount: 5
cooldownPeriod: 300
triggers:
- type: cron
metadata:
timezone: Europe/Moscow
start: 0 8 * * 1-5 # Старт в 08:00 по будням
end: 0 20 * * 1-5 # Усыпление в 20:00 по будням
desiredReplicas: "1"
Обрати внимание на поле timezone: указание явного часового пояса спасает от классической путаницы со временем UTC, в котором по умолчанию работают ноды кластера. Вне интервала с 08:00 до 20:00 KEDA автоматически переводит поды в состояние 0 current / 0 desired.
Подводный камень GitOps: война ArgoCD и KEDA
Если в твоей компании настроен честный GitOps через ArgoCD или Flux, внедрение KEDA в лоб немедленно приведет к «войне контроллеров».
Картина выглядит так: в 20:00 KEDA выставляет в деплойменте 0 реплик. ArgoCD видит, что живое состояние кластера разошлось с описанием в Git-репозитории (где было указано replicas: 1), объявляет статус OutOfSync и услужливо возвращает одну реплику обратно.
Чтобы подружить KEDA с ArgoCD, необходимо настроить два важных правила:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: development-backend
namespace: argocd
spec:
# 1. Запрещаем ArgoCD перезаписывать количество реплик, управляемых KEDA
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers:
- /spec/replicas
syncPolicy:
automated:
selfHeal: true
prune: true
syncOptions:
# 2. Обязательно включаем ServerSideApply для установки тяжелых CRD KEDA
- ServerSideApply=true
Директива ignoreDifferences сообщает GitOps-движку: «Следи за версиями образов, переменными окружения и портами, но оставь число подов на усмотрение внешнего автомасштабировщика».
Экономика и реальность холодного старта
Какую выгоду дает эта схема на практике? Если твой dev-кластер состоит из десятка микросервисов, усыпление на ночь и выходные экономит от 60% до 67% затрат на вычислительные узлы. Если в кластере настроен Karpenter или Cluster Autoscaler, при усыплении подов пустые виртуальные машины будут автоматически выводиться из пула (node consolidation), физически снижая чек за инфраструктуру.
Но чудес не бывает, и за экономию приходится платить понятным инженерным компромиссом — холодным стартом (cold start). Когда первый разработчик или тестировщик открывает сервис утром, кластеру требуется от 15 до 60 секунд, чтобы поднять контейнеры, прогреть рантайм и пройти Readiness-пробы.
Для сервисов на Go, Rust или Node.js задержка практически незаметна: они стартуют за пару секунд. А вот для тяжелых монолитов на Spring Boot или сервисов машинного обучения прогрев может занять пару минут. Самый простой и элегантный способ обойти эту задержку — настроить расписание start на 15–30 минут раньше фактического начала рабочего дня команды. В 08:30 кластер плавно просыпается, и к девяти утра все сервисы уже ждут инженеров в полной боевой готовности.
