preloader
  • GKE Scheduler Autoscaling
blog-thumb

Cómo reducir el costo de kubernetes GKE mediante la eliminación de nodos en ventanas de tiempo específicos sin usar herramientas externas o API.

El problema

Nuestro problema surge cuando tenemos un ambiente de prueba (QA) que solo se usa en horario comercial y estar 24x7 genera un costo innecesario.

Solución propuesta

Apagar el ambiente de prueba fuera del horario laboral, eliminando nodos de nuestro clúster. Al acercarse el comienzo de la jornada laboral, estos nodos deben crearse y el ambiente de prueba debe comenzar a operar nuevamente.

Dependencias para esta solución

Para que esto sea posible necesitamos tener tres puntos de configuración:

  • 1- Un “grupo de nodos” separado que se puede descartar sin afectar la producción. Para que esta configuración sea más efectiva, puede incluir Taints and Tolerantios.

  • 2- Un label común para todos los deploys/sts que se pueden desactivar en este horario. Ejemplo: “scheduler=comercial”

  • 3- Conozca la lista de namespaces que contienen los deployments/sts que se desactivarán.

Pasos previos de implementación

Necesitamos asegurarnos de que los nodos y las implementaciones estén configurados correctamente.

Validación de node-pool e deployments y/o statefulsets

Primero enumeremos nuestros nodes pools y validemos que nuestro ambiente esté listo:

> gcloud container node-pools list --cluster us-gole-01
NAME                               MACHINE_TYPE       DISK_SIZE_GB  NODE_VERSION
pool-main                          c2-standard-8      100           1.20.10-gke.1600
pool-qa                            e2-standard-8      80            1.20.10-gke.1600

Si el node-pool no existe, es necesario crear este node-pool con la cantidad correspondiente de nodos, que puede ser solo uno, y agregar el taint:

Criando node pool com taint

Si el node-pool ya existe, pero no tiene el taint necesario, entonces usa el recurso de gcloud aún en versión beta para agregar el taint:

> gcloud beta container node-pools update pool-qa --node-taints='scheduler=comercial:NoSchedule'

Ahora vamos a validar que todos los deployments que queramos contengan los labels necesarios. En este ejemplo no tendremos statefulset en nuestra lista, pero lógicamente encaja perfectamente.

> kubectl -n namespace-a get,sts deploy -l scheduler=comercial
NAMESPACE              NAME                                                         READY   UP-TO-DATE   AVAILABLE   AGE
namespace-a            deployment.apps/gole-sonarcube                               1/1     1            1           34d
namespace-a            deployment.apps/gole-redis                                   1/1     1            1           34d
namespace-a            deployment.apps/gole-nodejs-app                              1/1     1            1           34d

> kubectl -n namespace-a get,sts deploy -l scheduler=comercial
NAMESPACE              NAME                                                         READY   UP-TO-DATE   AVAILABLE   AGE
namespace-b            deployment.apps/gole-netbox-worker                           1/1     1            1           34d
namespace-b            deployment.apps/gole-netbox                                  1/1     1            1           7d18h

Adición del tolerations en el deploy

Necesitamos asegurarnos de que los deploys estén configuradas correctamente.

❯ kubectl -n namespace-a get deploy -l scheduler=comercial -o=jsonpath='{.spec.template.spec.tolerations}'

El resultado debe ser un objeto como este multiplicado por el número de deploys existentes. Todos estarán en la misma lista, pero mirando la lista se puede ver fácilmente.

{"effect":"PreferNoSchedule","key":"scheduler","operator":"Equal","value":"comercial"}

Aquí hay un ejemplo de dónde se deben configurar los tolerations:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: gole-app
  labels:
    scheduler: comercial
    app: gole-app
    tier: api-extended
spec:
  progressDeadlineSeconds: 600
  replicas: 3
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      scheduler: comercial
      app: gole-app
      tier: api-extended
  strategy:
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 25%
    type: RollingUpdate
  template:
    metadata:
      labels:
        scheduler: comercial
        app: gole-app
        tier: api-extended
    spec:
      tolerations:
      - key: "scheduler"
        operator: "Equal"
        value: "comercial"
        effect: "PreferNoSchedule"
      containers:

Validación final

  • ¿Tenemos labels en los deployments? Sí
  • ¿Tenemos un node pool con un taint configurado? Sí
  • ¿Tenemos tolerations establecidas en nuestros deployments? Sí

Así que ahora podemos empezar.

Implementación

Para la implementación de la solución necesitaremos crear algunos recursos:

Secret key.json

Nuestro secret será el archivo renombrado como key.json generado en service account, keys dentro de IAM.

Lógicamente, este usuario del sistema debe tener permiso para administrar el clúster de kubernetes mediante el sdk del gcloud.

key.json

> cat key.json
{
  "type": "service_account",
  "project_id": "gole-autoscaling-sample",
  "private_key_id": "asdfasdfasdfasdasdfasdasdf....",
  "private_key": "-----BEGIN PRIVATE KEY-----\ndasdfasdf393993..asdfsf.aksdflksjdflksjfkjafljasdjkf....Ijr7ZCBgpbQrDH\nvNUw/JxaVbLtpvy3KSmYpjGfKnHFs+wPQi+NFmwrdOZHvKjdtRNxHvPqgWNxCSAh\nMwEB8cKs0dzif1Hbg7EtYrZOR8g7LZrTD3c4lTsahMyI9x3kN0aCe5QXDXvtPEJ1\n3s5XFBriQc1tmHwMEV4VW8s=CONTINUA....\n-----END PRIVATE KEY-----\n",
  "client_email": "infra@golesuite.com",
  "client_id": "11223344556677889900",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/blablabla"
}

Luego agreguemos este archivo como un secret en un namespace específico para nuestra automatización.

> kubectl -n devops create secret generic gcloud-key --from-file=./key.json

Comprobando la clave creada:

> kubectl -n devops get secret gcloud-key -o=jsonpath='{.data.key\.json}' | base64 -d

Nota: Asegúrese de que solo los administradores puedan ver el contenido del namespace utilizado. En este caso usaremos el namespace devops

Creando nuestro Cronjob

Ahora vamos a crear un archivo yaml que use el kind Cronjob.

Nuestro cronjob está configurado para:

  • Inicio a las 7:30 am de lunes a viernes.
  • Apagado a las 19:00 de lunes a viernes.
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  labels:
    role: devops
    owner: gole
  name: qa-env-shutdown
  namespace: devops
spec:
  schedule: "00 22 * * 1,2,3,4,5"
  concurrencyPolicy: Forbid
  jobTemplate:
    metadata:
      labels:
        role: devops
        owner: gole
    spec:
      template:
        spec:
          restartPolicy: Never
          containers:
          - name: gke-operator
            image: alpine
            command:
              - "/bin/sh"
            args:
              - -c
              - "apk add --no-cache curl bash ; \
                curl -O https://raw.githubusercontent.com/golesuite/gcloud-gke-scheduling/main/alpine-gcloud.sh ; \
                chmod +x ./alpine-gcloud.sh ; \
                ./alpine-gcloud.sh"
            imagePullPolicy: IfNotPresent
            volumeMounts:
              - mountPath: /etc/localtime
                name: tz-config
              - name: gcloud-key
                mountPath: /etc/gcloud/
            terminationMessagePath: /dev/termination-log
            terminationMessagePolicy: File
            env:
            - name: SCALE_DEPLOY_NUMBER
              value: "0"
            - name: SCALE_STS_NUMBER
              value: "0"
            - name: SCALE_NODES_NUMBER
              value: "0"
            - name: GCLOUD_ZONE
              value: "southamerica-east1-a"
            - name: CLUSTER_NAME
              value: "br-gole-01"
            - name: SCHEDULER_LABEL
              value: "sheduler=comercial""
            - name: SCHEDULER_POOL
              value: "pool-qa"
            - name: PROJECT_ID
              value: "gole"
          volumes:
          - name: tz-config
            hostPath:
              path: /usr/share/zoneinfo/America/Sao_Paulo
          - name: gcloud-key
            secret:
              secretName: gcloud-key
---
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  labels:
    role: devops
    owner: gole
  name: qa-env-startup
  namespace: devops
spec:
  schedule: "30 10 * * 1,2,3,4,5"
  concurrencyPolicy: Forbid
  jobTemplate:
    metadata:
      labels:
        role: devops
        owner: gole
    spec:
      template:
        spec:
          restartPolicy: Never
          containers:
          - name: gke-operator
            image: alpine
            command:
              - "/bin/sh"
            args:
              - -c
              - "apk add --no-cache curl bash ; \
                curl -O https://raw.githubusercontent.com/golesuite/gcloud-gke-scheduling/main/alpine-gcloud.sh ; \
                chmod +x ./alpine-gcloud.sh ; \
                ./alpine-gcloud.sh"
            imagePullPolicy: IfNotPresent
            volumeMounts:
              - mountPath: /etc/localtime
                name: tz-config
              - name: gcloud-key
                mountPath: /etc/gcloud/
            terminationMessagePath: /dev/termination-log
            terminationMessagePolicy: File
            env:
            - name: SCALE_DEPLOY_NUMBER
              value: "1"
            - name: SCALE_STS_NUMBER
              value: "1"
            - name: SCALE_NODES_NUMBER
              value: "1"
            - name: GCLOUD_ZONE
              value: "southamerica-east1-a"
            - name: CLUSTER_NAME
              value: "br-gole-01"
            - name: SCHEDULER_LABEL
              value: "scheduler=comercial""
            - name: SCHEDULER_POOL
              value: "pool-qa"
            - name: PROJECT_ID
              value: "gole"
          volumes:
          - name: tz-config
            hostPath:
              path: /usr/share/zoneinfo/America/Sao_Paulo
          - name: gcloud-key
            secret:
              secretName: gcloud-key

Puede ver que nuestro cronjob usa dos puntos de montaje externos:

  • tz-config
  • gcloud-key

Nuestro tz-config es un apunte al archivo zoneinfo que se encuentra en cualquier sistema operativo. Linux, en este caso le estamos comunicando a nuestro cronjob que su zona horaria es America/Sao_Paulo.

Nuestro gcloud-key usa el modelo de secret, y aquí es donde haremos el link con el secret creado en el paso anterior.

cronjob ENV Variables
ENV VariablesRequiredexample value
SCALE_DEPLOY_NUMBERX0
SCALE_STS_NUMBERX0
SCALE_NODES_NUMBERX0
PROJECT_IDXgole
CLUSTER_NAMEXbr-gole-01
GCLOUD_ZONEXsouthamerica-east1-a
SCHEDULER_LABELXscheduler=comercial
SCHEDULER_POOLXpool-qa

Antes de crear y aplicar el archivo cronjob debemos cambiar el valor de las variables.

Este se encuentra dentro de env:

            env:
            - name: SCALE_DEPLOY_NUMBER
              value: "1"
            - name: SCALE_STS_NUMBER
              value: "1"
            - name: SCALE_NODES_NUMBER
              value: "1"
            - name: GCLOUD_ZONE
              value: "southamerica-east1-a"
            - name: CLUSTER_NAME
              value: "br-gole-01"
            - name: SCHEDULER_LABEL
              value: "scheduler=comercial""
            - name: SCHEDULER_POOL
              value: "pool-qa"
            - name: PROJECT_ID
              value: "gole"

Una vez que se han seguido todos los pasos, estamos listos para aplicar nuestro cronjob.

Solo me gustaría decirles dónde están los comandos que realmente hacen el trabajo de bajar y subir el ambiente, ya que todo lo que hemos hecho hasta ahora ha sido preparar la infraestructura.

¿Qué comandos vamos a ejecutar?

Los comandos que se ejecutarán para realizar la tarea de autoscaling se describen en este proyecto de git (es el mismo proyecto):

El archivo alpine-gcloud.sh se llama dentro de Cronjob en la sesión de commands.

El contenido del archivo contiene la secuencia de comandos que harán nuestro trabajo.

Hay dos pasos principales:

  • 1- shutdown.

    • Escalar los deploys que contienen las labels declaradas a 0
    • Escalar los nodes del node pool que contienen los taints a 0
  • 2- startup

    • Escalar los deploys que contienen las labels declaradas a 1 o más
    • Escalar los nodes del node pool que contienen los taints a 1 o más

Todo lo demás que creamos contiene la estructura que usaremos para que estos comandos se puedan ejecutar desde dentro de nuestro clúster.

Aplicando el cronjob.

Una vez que hayamos guardado nuestro archivo cronjob localmente y ya hayamos modificado las variables, los tiempos y los namespaces, apliquémoslos. Tenga en cuenta que los tiempos de ejecución de cronjob apuntan a 3 horas más, eso se debe a que las API de control de los nodos de GKE usan la hora UTC. Dado que los valores de TZ para cronjobs aún están en beta, lo más seguro es agregar 3 horas más y llegaremos en la hora correcta.

Es decir, si quieres 8h y estás en una ciudad GMT -3, entonces 8+3=11.

Para empezar tendremos (07:30h):

  schedule: "30 10 * * 1,2,3,4,5"

Para apagar (19:00h):

  schedule: "00 22 * * 1,2,3,4,5"
> kubectl apply -f cronjob.yaml
> kubectl -n devops get cronjob

Cuando se ejecute tendremos un job ejecutándose:

> kubectl -n devops get job

Lógicamente, el job inicia un pod y con eso podemos seguir los logs.

Pruebas

Cambie las horas para que se ejecuten en los momentos en los que usted está trabajando, con el fin de validar que los procesos se ejecutan correctamente.

Consideraciones finales

Este artículo incluye:

  • kubernetes Google GKE
  • gcloud
  • node-pools
  • taints
  • auto-scaling
  • Cronjobs
  • container commands
  • container env variables

Hay muchas formas de crear y administrar un clúster de Kubernetes, así que no se limite a crear soluciones o diferentes formas de aplicar sus soluciones.

Use y abuse de las funciones de las API.

¿Necesita ayuda?

Ve a nuestra página de Contacto y chatea con nosotros. Estaremos encantados de servirle.

¡Éxito!