Autoescalar tus aplicaciones en Kubernetes con KEDA

En los últimos artículos que he escrito sobre Kubernetes me estoy centrando en todo lo que tiene que ver con la monitorización, tanto con Prometheus como con Azure Monitor por ahora, y el autoescalado. Hoy quiero hablarte de KEDA, otra forma de autoescalar tus aplicaciones en base a eventos externos.

¿Qué es KEDA?

KEDA son las siglas de Kubernetes-based Event Driven Autoscaling. Se trata de un componente que puedes desplegar en tu clúster de Kubernetes, esté donde esté, y que te ayuda a autoescalar tus aplicaciones en base a diferentes eventos, utilizando lo que llama scalers. Estos pueden ser mensajes en colas de mensajería de diferentes plataformas (Apache Kafka, AWS SQS Queue, RabbitMQ Queue, Azure Event Hubs, etc), bases de datos (MySQL, PostgreSQL) o incluso servidores de métricas como Prometheus, AWS Cloudwatch o Azure Monitor. La lista va creciendo cada vez más y más.

Una vez decidido cuál será el evento que hará escalar tu aplicación, se crea un recurso llamado ScaledObject con el scaler oportuno y KEDA, por debajo, crea un HorizontalPodAutoscaler por nosotros y lo asocia a dicho recurso para saber cuándo tiene que subir o bajar el número de réplicas de nuestro Deployment o Job.

Arquitectura de KEDA

Instalar KEDA en tu clúster

Antes de comenzar a utilizar KEDA necesitas desplegarlo en tu Kubernetes. La forma más sencilla es a través de Helm:

#Install helm 3 if you don't have it
brew install helm

#Add KEDA repo to Helm
helm repo add kedacore https://kedacore.github.io/charts

#Update Helm repo
helm repo update

#My cluster info
RESOURCE_GROUP="KEDA"
AKS_NAME="aks-keda"

#Get AKS credentials
az aks get-credentials --resource-group $RESOURCE_GROUP --name $AKS_NAME

#Create a namespace called keda
kubectl create namespace keda
#Install keda components inside the namespace
helm install keda kedacore/keda --namespace keda
#See pods
kubectl get pods --namespace keda

En este ejemplo estoy utilizando Helm 3 en Mac. Si no lo tienes instalado puedes hacerlo a través de Homebrew con la primera línea del código anterior. Añade el repo de KEDA y asigna a kubectl las credenciales del clúster donde quieres desplegarlo. Crea un namespace llamado keda y en él instala los componentes a través del chart kedacore/keda. Si todo ha salido bien deberías de ver dos pods dentro de ese namespace:

➜ kubectl get pods -n keda
NAME                                               READY   STATUS    RESTARTS   AGE
keda-operator-5895ff46b9-jbxbn                     1/1     Running   0          20s
keda-operator-metrics-apiserver-6774776dbc-4nnhs   1/1     Running   0          20s

Desplegar una aplicación de ejemplo

Ahora que ya tienes KEDA desplegado lo siguiente que necesitas es una aplicación que autoescalar. Da igual lo que sea, ya que vamos a hacerla subir o bajar en réplicas en base a eventos ajenos a ella, en este caso. Para este ejemplo voy a utilizar el siguiente despliegue:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-deployment
  labels:
    app: web
spec:
  replicas: 1
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: nginx
          image: nginx:latest
          ports:
            - name: http
              containerPort: 80

Como puedes ver es un Nginx básico con una sola réplica. Al desplegarlo deberías de ver algo como lo siguiente:

➜ kubectl apply -f web-deployment.yaml
deployment.apps/web-deployment created
➜ kubectl get pods
NAME                             READY   STATUS    RESTARTS   AGE
web-deployment-7bfddfc4f-9kxjn   1/1     Running   0          6s

Lo último que te queda por definir es en base a qué escalamos esta aplicación.

Crear un ScaledObject para tu despliegue

Como te decía al principio de este artículo, existen diferentes scalers sobre los que puedes basar el autoescalado. Para este ejemplo voy a utilizar el tipo Azure Storage Queue. Por ello, antes que nada, necesito crear una cola de mensajería en Azure Storage, que llamaré tasks.

#Create a storage account on Azure
STORAGE_NAME="boxoftasks"
az storage account create --name $STORAGE_NAME --resource-group $RESOURCE_GROUP
ACCOUNT_KEY=$(az storage account keys list --resource-group $RESOURCE_GROUP --account-name $STORAGE_NAME --query "[0].value" --output tsv)
#Create a queue
az storage queue create --name "tasks" \
--account-name $STORAGE_NAME \
--account-key $ACCOUNT_KEY
#Get storage connection string
CONNECTION_STRING=$(az storage account show-connection-string --name $STORAGE_NAME --resource-group $RESOURCE_GROUP -o tsv)
#Create secret with the storage connection string
kubectl create secret generic storage-secret --from-literal=connection-string=$CONNECTION_STRING

Además, como puedes ver en el código anterior, lo último que hago es crear un secreto que guardará la cadena de conexión de la cuenta de almacenamiento donde está mi cola. Por último, creo el recurso ScaledObject que indicara los umbrales asociados a la misma:

apiVersion: keda.k8s.io/v1alpha1
kind: TriggerAuthentication
metadata:
  name: trigger-auth-queue
spec:
  secretTargetRef:
    - parameter: connection
      name: storage-secret
      key: connection-string

---

apiVersion: keda.k8s.io/v1alpha1
kind: ScaledObject
metadata:
  name: queue-scaledobject  
  labels:
    deploymentName: web-deployment
spec:
  scaleTargetRef:
    deploymentName: web-deployment
  pollingInterval: 15
  coolDownPeriod: 30
  minReplicaCount: 0
  maxReplicaCount: 10
  triggers:
   - type: azure-queue
     metadata:
      queueName: tasks
      queueLength: '5' # Optional. Queue length target for HPA. Default: 5 messages
     authenticationRef:
       name: trigger-auth-queue

Para poder asociar la cadena de conexión al ScaledObject necesito hacer uso de otro recurso llamado TriggerAuthentication, al que a su vez asocio al secreto creado anteriormente, aunque existen otras formas de autenticación.

Una vez que apliques los cambios ocurrirán dos cosas: por un lado se creará un recurso del tipo ScaledObject con el trigger azure-queue:

➜ kubectl get scaledobject                           
NAME                 DEPLOYMENT       TRIGGERS      AGE
queue-scaledobject   web-deployment   azure-queue   59s

Y por otro un HPA asociado a dicho objecto:

➜ kubectl get hpa 
NAME                      REFERENCE                   TARGETS             MINPODS   MAXPODS   REPLICAS   AGE
keda-hpa-web-deployment   Deployment/web-deployment   <unknown>/5 (avg)   1         10        0          79s

Probar el autoescalado con KEDA

Para comprobar que todo funciona correctamente, con Azure CLI puedes generar varios mensajes de una forma sencilla:

#Generate messages
while true; do az storage message put --content "Hello from KEDA" --queue-name "tasks" --account-name $STORAGE_NAME --account-key $STORAGE_KEY; done

Si vigilas el HPA asociado comprobarás que poco a poco va subiendo en replicas:

➜ kubectl get hpa -w
NAME                      REFERENCE                   TARGETS             MINPODS   MAXPODS   REPLICAS   AGE
keda-hpa-web-deployment   Deployment/web-deployment   <unknown>/5 (avg)   1         10        0          23m
keda-hpa-web-deployment   Deployment/web-deployment   4/5 (avg)           1         10        1          23m
keda-hpa-web-deployment   Deployment/web-deployment   13/5 (avg)          1         10        1          23m
keda-hpa-web-deployment   Deployment/web-deployment   7334m/5 (avg)       1         10        3          24m
keda-hpa-web-deployment   Deployment/web-deployment   5800m/5 (avg)       1         10        5          24m
keda-hpa-web-deployment   Deployment/web-deployment   6334m/5 (avg)       1         10        6          24m
keda-hpa-web-deployment   Deployment/web-deployment   5750m/5 (avg)       1         10        8          24m
keda-hpa-web-deployment   Deployment/web-deployment   5600m/5 (avg)       1         10        10         25m
keda-hpa-web-deployment   Deployment/web-deployment   6200m/5 (avg)       1         10        10         25m

Para comprobar el efecto contrario, elimina los mensajes de la cola con el comando clear:

#Clean queue
az storage message clear --queue-name "tasks" --account-name $STORAGE_NAME --account-key $STORAGE_KEY

Verás que a los pocos minutos volverás a tener cero réplicas de tu aplicación:

➜ kubectl get hpa -w
NAME                      REFERENCE                   TARGETS             MINPODS   MAXPODS   REPLICAS   AGE
keda-hpa-web-deployment   Deployment/web-deployment   <unknown>/5 (avg)   1         10        0          23m
keda-hpa-web-deployment   Deployment/web-deployment   4/5 (avg)           1         10        1          23m
keda-hpa-web-deployment   Deployment/web-deployment   13/5 (avg)          1         10        1          23m
keda-hpa-web-deployment   Deployment/web-deployment   7334m/5 (avg)       1         10        3          24m
keda-hpa-web-deployment   Deployment/web-deployment   5800m/5 (avg)       1         10        5          24m
keda-hpa-web-deployment   Deployment/web-deployment   6334m/5 (avg)       1         10        6          24m
keda-hpa-web-deployment   Deployment/web-deployment   5750m/5 (avg)       1         10        8          24m
keda-hpa-web-deployment   Deployment/web-deployment   5600m/5 (avg)       1         10        10         25m
keda-hpa-web-deployment   Deployment/web-deployment   6200m/5 (avg)       1         10        10         25m
keda-hpa-web-deployment   Deployment/web-deployment   6900m/5 (avg)       1         10        10         25m
keda-hpa-web-deployment   Deployment/web-deployment   7/5 (avg)           1         10        10         25m
keda-hpa-web-deployment   Deployment/web-deployment   0/5 (avg)           1         10        10         26m
keda-hpa-web-deployment   Deployment/web-deployment   0/5 (avg)           1         10        10         31m
keda-hpa-web-deployment   Deployment/web-deployment   <unknown>/5 (avg)   1         10        0          31m

¡Saludos!