KEDA (Kubernetes Event-Driven Autoscaling) es un proyecto de la CNCF que escala Deployments, StatefulSets y Jobs en función de fuentes externas: mensajes pendientes en una cola, el lag de un consumer group de Kafka, el resultado de una consulta de Prometheus o un horario. A diferencia del HorizontalPodAutoscaler estándar, que solo mira CPU y memoria, KEDA puede dejar una carga en cero réplicas mientras no hay trabajo y arrancarla cuando llega. En este tutorial instalarás KEDA, escalarás un worker a partir de una cola de RabbitMQ, comprobarás el escalado desde y hacia cero, y verás cómo escalar por horario y por métricas de Prometheus.

Requisitos previos

Para seguir esta guía necesitas:

  • Un clúster Kubernetes en una versión soportada por la release actual de KEDA (la tabla de compatibilidad está en la documentación de KEDA), por ejemplo sobre VPS de CubePath con Ubuntu 24.04.
  • kubectl con permisos de administrador del clúster y Helm 3 en tu equipo.
  • Ningún otro adaptador de métricas externas instalado (como prometheus-adapter sirviendo external.metrics.k8s.io), ya que solo puede haber uno registrado para esa API.

Cómo funciona KEDA

KEDA no sustituye al HPA, lo alimenta. Cuando creas un ScaledObject para un Deployment:

  • El operador de KEDA consulta la fuente de eventos cada pollingInterval segundos.
  • Mientras no hay eventos, mantiene el Deployment en minReplicaCount, que puede ser 0. Al detectar el primer evento, lo activa y pasa a una réplica.
  • De una réplica en adelante, KEDA crea un HPA llamado keda-hpa-<nombre> y le sirve la métrica a través de su propio servidor de métricas. El HPA calcula las réplicas como el valor de la métrica dividido por el objetivo de cada trigger.
  • Cuando la fuente vuelve a estar a cero durante cooldownPeriod segundos, KEDA baja el Deployment a cero.

Paso 1: Instalar KEDA

Añade el repositorio oficial de charts de KEDA:

helm repo add kedacore https://kedacore.github.io/charts
helm repo update

Instala KEDA en su propio namespace:

helm install keda kedacore/keda --namespace keda --create-namespace

Comprueba que los tres componentes están en marcha: el operador, el servidor de métricas y el webhook de admisión que valida los recursos:

kubectl -n keda get pods
NAME                                               READY   STATUS    RESTARTS   AGE
keda-admission-webhooks-6d7b8c9f5d-kx2mz           1/1     Running   0          50s
keda-operator-7c9d6b8f4-q7lp2                      1/1     Running   1          50s
keda-operator-metrics-apiserver-5f8d7c6b9-rt4wz    1/1     Running   0          50s

Verifica que el servidor de métricas de KEDA está registrado en la API de Kubernetes:

kubectl get apiservice v1beta1.external.metrics.k8s.io
NAME                              SERVICE                                AVAILABLE   AGE
v1beta1.external.metrics.k8s.io   keda/keda-operator-metrics-apiserver   True        60s

Paso 2: Desplegar RabbitMQ y un worker de prueba

Para ver KEDA en acción necesitas una fuente de eventos. Despliega un RabbitMQ de prueba, sin persistencia, en un namespace propio. Sustituye tu_password por una contraseña real:

kubectl create namespace keda-demo
nano rabbitmq.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: rabbitmq
  namespace: keda-demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: rabbitmq
  template:
    metadata:
      labels:
        app: rabbitmq
    spec:
      containers:
        - name: rabbitmq
          image: rabbitmq:4-management
          env:
            - name: RABBITMQ_DEFAULT_USER
              value: keda
            - name: RABBITMQ_DEFAULT_PASS
              value: tu_password
          ports:
            - containerPort: 5672
            - containerPort: 15672
---
apiVersion: v1
kind: Service
metadata:
  name: rabbitmq
  namespace: keda-demo
spec:
  selector:
    app: rabbitmq
  ports:
    - name: amqp
      port: 5672
    - name: management
      port: 15672
kubectl apply -f rabbitmq.yaml
kubectl -n keda-demo rollout status deployment/rabbitmq

Cuando RabbitMQ esté listo (espera unos segundos tras el rollout para que arranque la API de gestión), crea la cola tareas que usará el worker con la API HTTP de gestión de RabbitMQ desde un pod temporal. La respuesta vacía indica que se ha creado:

kubectl -n keda-demo run crear-cola --rm -i --restart=Never --image=curlimages/curl --command -- \
  curl -s -u keda:tu_password -X PUT -H "content-type: application/json" -d '{"durable":true}' \
  http://rabbitmq:15672/api/queues/%2F/tareas

Ahora crea el worker que KEDA va a escalar. En un caso real sería tu consumidor de la cola; aquí basta con un contenedor que espera, porque lo que interesa es ver cómo cambia el número de réplicas. Empieza con replicas: 0:

nano worker.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: worker
  namespace: keda-demo
spec:
  replicas: 0
  selector:
    matchLabels:
      app: worker
  template:
    metadata:
      labels:
        app: worker
    spec:
      containers:
        - name: worker
          image: busybox:1.36
          command: ["sh", "-c", "sleep infinity"]
          resources:
            requests:
              cpu: 10m
              memory: 16Mi
kubectl apply -f worker.yaml

Paso 3: Crear el ScaledObject para la cola

Las credenciales de la fuente de eventos no van en el ScaledObject, sino en un Secret referenciado desde un TriggerAuthentication. Así varios ScaledObjects pueden reutilizarlas y no quedan en texto plano en el manifiesto de escalado.

nano scaledobject-rabbitmq.yaml
apiVersion: v1
kind: Secret
metadata:
  name: rabbitmq-keda
  namespace: keda-demo
type: Opaque
stringData:
  host: amqp://keda:[email protected]:5672
---
apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
  name: rabbitmq-auth
  namespace: keda-demo
spec:
  secretTargetRef:
    - parameter: host
      name: rabbitmq-keda
      key: host
---
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: worker
  namespace: keda-demo
spec:
  scaleTargetRef:
    name: worker
  minReplicaCount: 0
  maxReplicaCount: 10
  pollingInterval: 10
  cooldownPeriod: 60
  triggers:
    - type: rabbitmq
      metadata:
        protocol: amqp
        queueName: tareas
        mode: QueueLength
        value: "5"
      authenticationRef:
        name: rabbitmq-auth

Los campos principales:

  • minReplicaCount: 0 permite escalar a cero. maxReplicaCount es el techo de réplicas.
  • pollingInterval es cada cuántos segundos KEDA consulta la cola, y cooldownPeriod cuánto espera con la cola vacía antes de volver a cero.
  • mode: QueueLength con value: "5" pide una réplica por cada 5 mensajes pendientes. Con 30 mensajes, el HPA calculará 6 réplicas.
kubectl apply -f scaledobject-rabbitmq.yaml
kubectl -n keda-demo get scaledobject worker

La columna READY debe mostrar True, lo que significa que KEDA ha podido conectar con RabbitMQ, y ACTIVE debe mostrar False, porque la cola está vacía. Comprueba también que KEDA ha creado el HPA:

kubectl -n keda-demo get hpa
NAME              REFERENCE           TARGETS             MINPODS   MAXPODS   REPLICAS   AGE
keda-hpa-worker   Deployment/worker   <unknown>/5 (avg)   1         10        0          20s

El HPA muestra un mínimo de 1 porque solo actúa de una réplica en adelante; el paso entre cero y uno lo gestiona el operador de KEDA.

Paso 4: Probar el escalado desde cero

Publica 30 mensajes en la cola tareas desde un pod temporal, de nuevo con la API HTTP de gestión:

kubectl -n keda-demo run publicador --rm -i --restart=Never --image=curlimages/curl --command -- sh -c '
  API=http://rabbitmq:15672/api
  for i in $(seq 1 30); do
    curl -s -o /dev/null -u keda:tu_password -X POST -H "content-type: application/json" \
      -d "{\"properties\":{},\"routing_key\":\"tareas\",\"payload\":\"trabajo $i\",\"payload_encoding\":\"string\"}" \
      "$API/exchanges/%2F/amq.default/publish"
  done
  echo "30 mensajes publicados"'

Observa el Deployment. En unos segundos pasará de 0 a 1 réplica por la activación de KEDA, y después el HPA lo subirá a 6:

kubectl -n keda-demo get deployment worker -w
NAME     READY   UP-TO-DATE   AVAILABLE   AGE
worker   0/0     0            0           5m
worker   0/1     0            0           5m
worker   1/1     1            1           5m
worker   1/4     1            1           5m
worker   6/6     6            6           5m

Pulsa Ctrl+C. El ScaledObject aparece ahora con ACTIVE en True, y el HPA muestra la métrica real:

kubectl -n keda-demo get hpa keda-hpa-worker
NAME              REFERENCE           TARGETS     MINPODS   MAXPODS   REPLICAS   AGE
keda-hpa-worker   Deployment/worker   5/5 (avg)   1         10        6          3m

Paso 5: Probar el escalado a cero

Como el worker de prueba no consume mensajes, vacía la cola a mano para simular que el trabajo ha terminado:

kubectl -n keda-demo run purga --rm -i --restart=Never --image=curlimages/curl --command -- \
  curl -s -u keda:tu_password -X DELETE http://rabbitmq:15672/api/queues/%2F/tareas/contents

Cuando la cola lleve cooldownPeriod segundos (60 en este ejemplo) vacía, KEDA bajará el Deployment directamente a 0:

kubectl -n keda-demo get deployment worker -w

Al final verás 0/0 réplicas y el ScaledObject con ACTIVE en False.

Paso 6: Ajustar la velocidad de escalado

Si el escalado hacia abajo es demasiado lento o demasiado agresivo para tu carga, ajusta el comportamiento del HPA que genera KEDA desde el propio ScaledObject, en la sección advanced. Añade este bloque dentro de spec en scaledobject-rabbitmq.yaml, al mismo nivel que triggers:

  advanced:
    horizontalPodAutoscalerConfig:
      behavior:
        scaleDown:
          stabilizationWindowSeconds: 60
          policies:
            - type: Percent
              value: 50
              periodSeconds: 30
        scaleUp:
          stabilizationWindowSeconds: 0

Con esta configuración, el HPA sube en cuanto hay más mensajes y baja como mucho la mitad de las réplicas cada 30 segundos, tras un minuto de estabilidad. Aplica el cambio con kubectl apply -f scaledobject-rabbitmq.yaml.

Otros scalers habituales

El patrón es siempre el mismo: cambiar el bloque triggers. Si un ScaledObject tiene varios triggers, el HPA usa el que pida más réplicas.

Escalado por horario con cron

El scaler cron fija un número de réplicas durante una franja horaria. Es útil para adelantarse a picos conocidos. Este ejemplo mantiene 10 réplicas de api-backend en horario laboral de Madrid y 2 el resto del tiempo:

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: api-backend-horario
  namespace: produccion
spec:
  scaleTargetRef:
    name: api-backend
  minReplicaCount: 2
  maxReplicaCount: 20
  triggers:
    - type: cron
      metadata:
        timezone: Europe/Madrid
        start: "0 8 * * 1-5"
        end: "0 20 * * 1-5"
        desiredReplicas: "10"

Fuera de la franja, el Deployment vuelve a minReplicaCount. Puedes combinarlo con un trigger de carga para que el horario marque un mínimo y la carga real decida por encima.

Escalado por métricas de Prometheus

El scaler prometheus ejecuta una consulta PromQL y usa su resultado como métrica. Este ejemplo escala para que cada réplica atienda unas 100 peticiones por segundo, y no sale de minReplicaCount mientras el tráfico total no supere 10 peticiones por segundo:

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: api-backend-rps
  namespace: produccion
spec:
  scaleTargetRef:
    name: api-backend
  minReplicaCount: 2
  maxReplicaCount: 20
  triggers:
    - type: prometheus
      metadata:
        serverAddress: http://prometheus-operated.monitoring.svc.cluster.local:9090
        query: sum(rate(http_requests_total{service="api-backend"}[2m]))
        threshold: "100"
        activationThreshold: "10"

La consulta debe devolver un único valor. Pruébala antes en la interfaz de Prometheus y ajusta serverAddress al Service de tu instalación.

Jobs en lugar de Deployments

Para tareas largas en las que cada mensaje es un trabajo independiente (transcodificar un vídeo, generar un informe), usa un ScaledJob en lugar de un ScaledObject. KEDA crea un Job de Kubernetes por cada mensaje pendiente, hasta maxReplicaCount, y evita que el HPA mate un pod a mitad de tarea al reducir réplicas.

Solución de problemas

El ScaledObject tiene READY en False. KEDA no puede consultar la fuente. Los eventos del recurso y los logs del operador indican el motivo, normalmente credenciales, un nombre de host o un puerto incorrectos:

kubectl -n keda-demo describe scaledobject worker
kubectl -n keda logs deploy/keda-operator --tail=50

ACTIVE está en True pero no escala por encima de 1. Revisa el HPA con kubectl -n keda-demo describe hpa keda-hpa-worker. Si muestra errores al obtener la métrica externa, comprueba que el APIService v1beta1.external.metrics.k8s.io está True y apunta a keda/keda-operator-metrics-apiserver.

No vuelve a cero. El Deployment solo baja a cero cuando la métrica está por debajo del umbral de activación durante cooldownPeriod. Comprueba que la cola está realmente vacía y que minReplicaCount es 0. Con el protocolo amqp KEDA cuenta los mensajes listos para entregar; si tus consumidores retienen muchos mensajes sin confirmar, usa protocol: http para que también se tengan en cuenta.

El ScaledObject se rechaza al crearlo. El webhook de admisión de KEDA lo bloquea si el Deployment ya tiene un HPA o si otro ScaledObject apunta al mismo destino. El mensaje de error de kubectl apply indica cuál.

Conclusión

Has instalado KEDA, has escalado un worker de 0 a 6 réplicas según los mensajes de una cola RabbitMQ y lo has devuelto a cero cuando la cola se vació, además de ver cómo escalar por horario y por métricas de Prometheus. Como siguientes pasos, sustituye el worker de prueba por tu consumidor real, define un bloque fallback para que el Deployment mantenga un número fijo de réplicas si la fuente de eventos no responde, y combina KEDA con el Cluster Autoscaler para que los nodos también crezcan con la carga.