KEDA: Autoescalado por Eventos en Kubernetes

KEDA (Kubernetes Event-Driven Autoscaling) es un operador que permite escalar workloads de Kubernetes basándose en eventos externos: longitud de una cola de mensajes, métricas de Prometheus, cron schedules o cualquier fuente de eventos personalizada. A diferencia del HPA estándar que solo escala por CPU y memoria, KEDA puede escalar a cero réplicas cuando no hay trabajo pendiente y reactivar los pods automáticamente al llegar nuevos eventos.

Requisitos Previos

  • Kubernetes 1.24+
  • kubectl configurado
  • helm v3 instalado
  • Acceso a las fuentes de eventos (Kafka, RabbitMQ, Prometheus, etc.)

Instalación de KEDA

# Instalar KEDA con Helm (método recomendado)
helm repo add kedacore https://kedacore.github.io/charts
helm repo update

# Instalación en el namespace keda
helm install keda kedacore/keda \
  --namespace keda \
  --create-namespace \
  --set prometheus.metricServer.enabled=true \
  --set prometheus.operator.enabled=true

# Verificar la instalación
kubectl get pods -n keda
kubectl get crd | grep keda

# Verificar que el Metrics Server de KEDA está registrado
kubectl get apiservice | grep external.metrics

ScaledObject y ScaledJob

KEDA usa dos recursos principales: ScaledObject para Deployments/StatefulSets y ScaledJob para Jobs de Kubernetes.

# Estructura básica de un ScaledObject
cat > scaled-object-ejemplo.yaml << 'EOF'
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: mi-worker-scaler
  namespace: produccion
spec:
  scaleTargetRef:
    # El Deployment o StatefulSet que KEDA va a escalar
    name: mi-worker
    kind: Deployment
    apiVersion: apps/v1
  
  # Número mínimo de réplicas (0 = escalar a cero)
  minReplicaCount: 0
  
  # Número máximo de réplicas
  maxReplicaCount: 50
  
  # Tiempo en segundos antes de escalar hacia abajo
  cooldownPeriod: 60
  
  # Intervalo de polling de las métricas (segundos)
  pollingInterval: 15
  
  # Umbral para escalar a cero (segundos de inactividad)
  idleReplicaCount: 0
  
  triggers:
    # Aquí van los triggers (ejemplos en las secciones siguientes)
    - type: kafka
      metadata:
        topic: mi-topico
        bootstrapServers: kafka:9092
        consumerGroup: mi-consumer-group
        lagThreshold: "100"
EOF
# ScaledJob: para procesamiento de tareas de una sola vez
cat > scaled-job-ejemplo.yaml << 'EOF'
apiVersion: keda.sh/v1alpha1
kind: ScaledJob
metadata:
  name: procesador-imagenes
  namespace: produccion
spec:
  jobTargetRef:
    parallelism: 1
    completions: 1
    template:
      spec:
        containers:
          - name: procesador
            image: procesador-imagenes:v1.0
            env:
              - name: QUEUE_URL
                value: "amqp://rabbitmq:5672"
        restartPolicy: OnFailure
  
  # Número máximo de Jobs en paralelo
  maxReplicaCount: 20
  
  # Política de escalado
  scalingStrategy:
    strategy: "accurate"  # accurate, default o custom
  
  # Cuántos Jobs completados mantener
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 3
  
  triggers:
    - type: rabbitmq
      metadata:
        host: amqp://user:pass@rabbitmq:5672/
        queueName: imagenes-a-procesar
        queueLength: "1"
EOF

kubectl apply -f scaled-job-ejemplo.yaml

Scaler de Kafka

# Escalar un consumer de Kafka basado en el lag del consumer group
cat > keda-kafka.yaml << 'EOF'
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: kafka-consumer-scaler
  namespace: produccion
spec:
  scaleTargetRef:
    name: kafka-consumer-worker
  minReplicaCount: 1
  maxReplicaCount: 30
  cooldownPeriod: 120
  pollingInterval: 10
  triggers:
    - type: kafka
      metadata:
        # Servidores de Kafka
        bootstrapServers: kafka-broker-1:9092,kafka-broker-2:9092,kafka-broker-3:9092
        
        # Tópico a monitorear
        topic: pedidos-nuevos
        
        # Consumer group que se quiere escalar
        consumerGroup: procesador-pedidos
        
        # Número de mensajes por réplica antes de escalar
        lagThreshold: "50"
        
        # Desactivar SSL
        saslType: plaintext
        tls: disable
EOF

kubectl apply -f keda-kafka.yaml

# Con autenticación SASL/TLS
cat > keda-kafka-auth.yaml << 'EOF'
apiVersion: v1
kind: Secret
metadata:
  name: kafka-credentials
  namespace: produccion
type: Opaque
stringData:
  sasl: "plaintext"
  username: "kafka-user"
  password: "kafka-password"
---
apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
  name: kafka-trigger-auth
  namespace: produccion
spec:
  secretTargetRef:
    - parameter: sasl
      name: kafka-credentials
      key: sasl
    - parameter: username
      name: kafka-credentials
      key: username
    - parameter: password
      name: kafka-credentials
      key: password
---
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: kafka-consumer-seguro
  namespace: produccion
spec:
  scaleTargetRef:
    name: kafka-consumer-worker
  minReplicaCount: 1
  maxReplicaCount: 20
  triggers:
    - type: kafka
      metadata:
        bootstrapServers: kafka:9093
        topic: pedidos-nuevos
        consumerGroup: procesador-pedidos
        lagThreshold: "100"
        saslType: plaintext
        tls: enable
      authenticationRef:
        name: kafka-trigger-auth
EOF

kubectl apply -f keda-kafka-auth.yaml

Scaler de RabbitMQ

# Escalar basado en el número de mensajes en una cola de RabbitMQ
cat > keda-rabbitmq.yaml << 'EOF'
apiVersion: v1
kind: Secret
metadata:
  name: rabbitmq-secret
  namespace: produccion
type: Opaque
stringData:
  # URL de conexión a la Management API de RabbitMQ
  host: "http://user:password@rabbitmq:15672/vhost"
---
apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
  name: rabbitmq-trigger-auth
  namespace: produccion
spec:
  secretTargetRef:
    - parameter: host
      name: rabbitmq-secret
      key: host
---
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: rabbitmq-consumer-scaler
  namespace: produccion
spec:
  scaleTargetRef:
    name: worker-procesador
  minReplicaCount: 0
  maxReplicaCount: 25
  cooldownPeriod: 30
  pollingInterval: 5
  triggers:
    - type: rabbitmq
      metadata:
        # Nombre de la cola a monitorear
        queueName: tareas-pendientes
        
        # Escalar 1 réplica por cada N mensajes
        queueLength: "10"
        
        # Protocolo: amqp o http
        protocol: http
      authenticationRef:
        name: rabbitmq-trigger-auth
EOF

kubectl apply -f keda-rabbitmq.yaml

Scaler de Prometheus

# Escalar basado en métricas personalizadas de Prometheus
cat > keda-prometheus.yaml << 'EOF'
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: prometheus-scaler
  namespace: produccion
spec:
  scaleTargetRef:
    name: api-backend
  minReplicaCount: 2
  maxReplicaCount: 20
  cooldownPeriod: 60
  pollingInterval: 30
  triggers:
    - type: prometheus
      metadata:
        # URL del servidor de Prometheus
        serverAddress: http://prometheus.monitoring.svc.cluster.local:9090
        
        # Query de Prometheus para obtener la métrica de escala
        # Ejemplo: peticiones HTTP por segundo promedio de los últimos 2 minutos
        query: sum(rate(http_requests_total{service="api-backend"}[2m]))
        
        # Umbral: escalar para mantener esta cantidad de peticiones por réplica
        threshold: "100"
        
        # Valor mínimo de la métrica para activar el escaler
        activationThreshold: "10"
        
        # Namespace de la métrica (opcional)
        namespace: produccion
EOF

kubectl apply -f keda-prometheus.yaml

# Scaler de Prometheus con autenticación bearer token
cat > keda-prometheus-auth.yaml << 'EOF'
apiVersion: v1
kind: Secret
metadata:
  name: prometheus-bearer-token
  namespace: produccion
type: Opaque
stringData:
  bearerToken: "mi-token-secreto"
---
apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
  name: prometheus-auth
  namespace: produccion
spec:
  secretTargetRef:
    - parameter: bearerToken
      name: prometheus-bearer-token
      key: bearerToken
---
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: api-scaler-con-auth
  namespace: produccion
spec:
  scaleTargetRef:
    name: api-backend
  triggers:
    - type: prometheus
      metadata:
        serverAddress: https://prometheus.midominio.com
        query: avg(rate(http_requests_total[1m]))
        threshold: "50"
      authenticationRef:
        name: prometheus-auth
EOF

Scaler Cron para Escalado Programado

# Escalar según un horario predefinido (útil para picos de tráfico conocidos)
cat > keda-cron.yaml << 'EOF'
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: cron-scaler
  namespace: produccion
spec:
  scaleTargetRef:
    name: api-backend
  minReplicaCount: 2
  maxReplicaCount: 50
  triggers:
    - type: cron
      metadata:
        # Zona horaria (usar nombres de zona IANA)
        timezone: "Europe/Madrid"
        
        # Inicio del período de alta carga: lunes a viernes a las 8:00 AM
        start: "0 8 * * 1-5"
        
        # Fin del período: lunes a viernes a las 10:00 PM
        end: "0 22 * * 1-5"
        
        # Número de réplicas durante el período de alta carga
        desiredReplicas: "15"
    
    # Segundo trigger cron: fin de semana
    - type: cron
      metadata:
        timezone: "Europe/Madrid"
        start: "0 10 * * 0,6"   # Sábado y domingo a las 10:00 AM
        end: "0 20 * * 0,6"     # Sábado y domingo a las 8:00 PM
        desiredReplicas: "5"
EOF

kubectl apply -f keda-cron.yaml

Escalado a Cero

# Configurar un Deployment para que escale a cero fuera del horario de trabajo
cat > escalado-a-cero.yaml << 'EOF'
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: ambiente-dev-scaler
  namespace: desarrollo
spec:
  scaleTargetRef:
    name: app-desarrollo
  
  # minReplicaCount: 0 permite escalar a cero réplicas
  minReplicaCount: 0
  maxReplicaCount: 5
  
  # Tiempo en segundos sin eventos antes de escalar a cero
  cooldownPeriod: 300
  
  triggers:
    # Solo escalar durante horario laboral
    - type: cron
      metadata:
        timezone: "Europe/Madrid"
        start: "0 9 * * 1-5"
        end: "0 18 * * 1-5"
        desiredReplicas: "3"
EOF

kubectl apply -f escalado-a-cero.yaml

# Verificar el estado del ScaledObject
kubectl get scaledobjects -n desarrollo
kubectl describe scaledobject ambiente-dev-scaler -n desarrollo

# Ver las métricas actuales de KEDA
kubectl get hpa -n desarrollo  # KEDA crea un HPA internamente

Solución de Problemas

El ScaledObject no escala:

# Ver los logs del operador de KEDA
kubectl logs -n keda -l app=keda-operator -f

# Verificar el estado del ScaledObject
kubectl describe scaledobject <nombre> -n <namespace>

# Ver las métricas que KEDA está obteniendo
kubectl get --raw "/apis/external.metrics.k8s.io/v1beta1" | jq .

# Verificar el HPA creado por KEDA
kubectl get hpa -n <namespace>
kubectl describe hpa keda-hpa-<nombre-scaledobject> -n <namespace>

Error de conexión al scaler externo:

# Para Kafka: verificar conectividad desde el pod del operador KEDA
kubectl exec -n keda -it <keda-operator-pod> -- \
  nc -zv kafka-broker:9092

# Para RabbitMQ: verificar credenciales y URL
kubectl exec -n keda -it <keda-operator-pod> -- \
  curl -s http://user:pass@rabbitmq:15672/api/overview | jq .rabbitmq_version

Escalado agresivo que afecta la estabilidad:

# Ajustar los parámetros de estabilización en el HPA
# Editar el ScaledObject para agregar configuración de comportamiento
kubectl edit scaledobject <nombre> -n <namespace>
# Agregar:
# spec:
#   advanced:
#     horizontalPodAutoscalerConfig:
#       behavior:
#         scaleDown:
#           stabilizationWindowSeconds: 300
#         scaleUp:
#           stabilizationWindowSeconds: 30

Conclusión

KEDA transforma el autoescalado de Kubernetes de reactivo a proactivo al conectar directamente con las fuentes de eventos que generan carga de trabajo. La capacidad de escalar a cero réplicas ahorra recursos en entornos de desarrollo y para cargas de trabajo intermitentes, mientras que los scalers para Kafka, RabbitMQ y Prometheus cubren la gran mayoría de patrones de procesamiento asíncrono en producción. La combinación de KEDA con escalado programado mediante cron es especialmente útil para gestionar picos de tráfico predecibles de forma eficiente.