← todos os artigos

Desligar um ambiente inteiro por horário com o cron scaler do KEDA

0

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/end embutido. 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 / end sã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), use minReplicaCount: 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:

  1. Tire replicas do 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.
  2. 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 StatefulSet com 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 por ScaledObject (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