Desligar um ambiente inteiro por horário com o cron scaler do KEDA
O cenário é comum: você tem um cluster de homologação (ou dev, ou staging) que só é usado em horário comercial, de segunda a sexta. Fora disso — noite, madrugada, fim de semana — ele fica ligado consumindo nó, memória e, na nuvem, dinheiro de verdade. Ninguém desliga porque desligar “na mão” é chato e ligar de volta esquecido é pior.
O jeito mais frágil de resolver isso é um CronJob rodando kubectl scale. Ele não tem estado (se falhar, o ambiente fica no limbo), some no primeiro kubectl apply do Argo CD, e você precisa manter duas listas de deployments em sincronia. Dá pra fazer melhor com o cron scaler do KEDA.
E o HPA nativo, não dá pra usar?
Desde o Kubernetes v1.37 — Beta, feature gate HPAScaleToZero ligado por padrão —, o HorizontalPodAutoscaler nativo já aceita minReplicas: 0. Duas ressalvas tiram ele da jogada pra esse caso:
- Só funciona com métrica object ou external (fila, lag de consumer, request rate via um adapter tipo Prometheus Adapter). CPU e memória continuam sem suporte a zero — sem pod rodando não tem o que medir.
- Não existe conceito de horário. O HPA é puramente reativo a métrica; não tem
start/endembutido. Pra desligar “das 20h às 7h” você precisaria expor uma métrica externa que varie sozinha com o relógio — na prática, reimplementar um cron como métrica.
O KEDA não compete com o HPA, aliás: o ScaledObject cria um HorizontalPodAutoscaler por baixo dos panos (é por isso que ter os dois no mesmo alvo dá conflito — mais adiante). O que ele soma são os triggers — mais de 60, incluindo o cron — que o HPA sozinho não tem. Pra “desligar fora do expediente”, o caminho direto continua sendo o cron scaler.
O modelo: a janela é quando está ligado
O ponto que confunde: o cron scaler não “dispara” nada num horário. Ele define uma janela [start, end) e, enquanto o relógio está dentro dela, mantém o deployment num número fixo de réplicas. Fora da janela, ele solta a restrição.
Para “desligar fora do expediente”, a janela é o expediente:
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: api-pedidos
namespace: homolog
spec:
scaleTargetRef:
name: api-pedidos
minReplicaCount: 0 # fora da janela -> zero
cooldownPeriod: 300
triggers:
- type: cron
metadata:
timezone: America/Sao_Paulo
start: 0 7 * * 1-5 # 07:00, seg a sex
end: 0 20 * * 1-5 # 20:00, seg a sex
desiredReplicas: "2" # capacidade dentro do expediente
Lê como: “das 7h às 20h, de segunda a sexta, no horário de São Paulo, quero 2 réplicas; fora disso, zero”. O dia-da-semana (1-5) já resolve o fim de semana — ele nunca entra na janela, então fica desligado sábado e domingo.
Alguns detalhes dos campos:
timezoneé obrigatório e usa a IANA Time Zone Database. Como você declara o fuso, o horário de verão é tratado sozinho.start/endsão cron Linux de 5 campos (minuto hora dia-do-mês mês dia-da-semana), sem segundos. Os dois não podem resolver para o mesmo instante.desiredReplicasé um inteiro como string ("2", com aspas).minReplicaCount: 0é o que permite a ida a zero. Se você quiser deixar 1 réplica de pé fora do horário (pra health check, uma demo eventual), useminReplicaCount: 1.
O detalhe que ninguém conta: o Argo CD vai reclamar
Se o replicas dos seus deployments está versionado no Git e você usa Argo CD (ou Flux), o KEDA vai alterar spec.replicas o tempo todo — e isso vira drift permanente: o Argo CD vê “Git diz 2, cluster diz 0”, marca como OutOfSync e, no pior caso, sincroniza de volta pra 2 no meio da madrugada.
Duas formas de resolver, escolha uma:
- Tire
replicasdo manifesto. Se o campo não existe no Git, o KEDA passa a ser o dono e não há o que divergir. É a opção mais limpa quando o KEDA cuida do scaling daquele workload de ponta a ponta. - Ignore o campo no Argo CD. Na
Application:
spec:
ignoreDifferences:
- group: apps
kind: Deployment
jqPathExpressions:
- .spec.replicas
Sem isso, você não tem uma rotina de desligamento — tem uma guerra de sincronização.
E os N serviços?
Um ScaledObject aponta pra um scaleTargetRef. Não existe “escale tudo que tem o label X” nativo. Então você tem um ScaledObject por deployment que participa do desligamento. Na prática isso é um for no seu Kustomize ou um range no seu Helm chart:
{{- range .Values.desligarForaDoExpediente }}
---
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: {{ . }}
namespace: {{ $.Release.Namespace }}
spec:
scaleTargetRef:
name: {{ . }}
minReplicaCount: 0
cooldownPeriod: 300
triggers:
- type: cron
metadata:
timezone: America/Sao_Paulo
start: {{ $.Values.expediente.start | quote }}
end: {{ $.Values.expediente.end | quote }}
desiredReplicas: {{ $.Values.expediente.replicas | quote }}
{{- end }}
Uma lista só (desligarForaDoExpediente: [api-pedidos, api-clientes, frontend, ...]) e a janela definida em um lugar. Serviço novo entra na rotina adicionando uma linha.
O que não desligar
Escopo isso para deployments stateless de aplicação. Fique longe de:
- Bancos de dados e qualquer
StatefulSetcom dado. O KEDA até escala StatefulSet, mas levar um Postgres a zero toda noite é pedir corrupção e dor de cabeça de recovery. - Ingress controller / gateway. Deixe de pé. Requisições fora do horário vão receber erro ou pagar a latência de scale-from-zero — o que costuma ser aceitável em não-produção, mas é uma decisão consciente.
- Jobs e
CronJobs. Não são afetados porScaledObject(isso éScaledJob, outro recurso). Se você tem um batch noturno em homolog, ele continua rodando.
Ligar de volta sem susto
A janela deve abrir antes do primeiro uso. Se as pessoas chegam às 8h, comece às 7h ou 7h30: cold start de pod, pool de conexão com o banco, cache frio e warmup de JVM não são instantâneos. start: 0 7 * * 1-5 te dá essa folga.
Na descida, o cooldownPeriod (padrão 300s) é quanto o KEDA espera depois do end antes de escalar pra baixo — então o ambiente cai uns 5 minutos depois das 20h, não no minuto exato. Ajuste se quiser corte mais seco.
Precisa manter o ambiente de pé numa noite específica (deploy de emergência, demo pro cliente lá fora)? Anote no ScaledObject:
metadata:
annotations:
autoscaling.keda.sh/paused-replicas: "2"
Isso congela em 2 réplicas e ignora o cron até você remover a anotação. É a válvula de escape sem editar a janela.
Um último cuidado: se o deployment já tem um HPA nativo, remova. O KEDA cria o próprio HPA e dois HPAs no mesmo alvo brigam entre si.
Quando isso não vale
Produção com SLA fica de fora — o risco de alguém precisar do serviço às 3h e ele estar frio não compensa a economia. E se o seu workload tem cold start de vários minutos ou depende de uma cadeia grande de serviços subindo em ordem, o custo de religar todo dia pode passar do que você economiza. Nesses casos, ou você aceita 1 réplica mínima, ou o desligamento fica só pro fim de semana.
Para homolog, dev e staging, porém, essa é uma das economias mais fáceis de cloud native: um ScaledObject por serviço, uma janela, e o ambiente se desliga sozinho todo dia às 20h.
Fontes
- Kubernetes v1.37 — Scale Workloads to Zero with HorizontalPodAutoscaler (Beta)
- Kubernetes — Horizontal Pod Autoscaling (scaling to and from zero)
- KEDA — Cron scaler (2.20)
- KEDA — ScaledObject spec (
minReplicaCount,cooldownPeriod,pollingInterval) - KEDA — Pausing autoscaling (
paused-replicas) - Argo CD — Diffing /
ignoreDifferences