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.
Nuestro problema surge cuando tenemos un ambiente de prueba (QA) que solo se usa en horario comercial y estar 24x7 genera un costo innecesario.
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.
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.
Necesitamos asegurarnos de que los nodos y las implementaciones estén configurados correctamente.
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:
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
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:
Así que ahora podemos empezar.
Para la implementación de la solución necesitaremos crear algunos recursos:
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
.
> 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
Ahora vamos a crear un archivo yaml que use el kind Cronjob.
Nuestro cronjob está configurado para:
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:
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.
ENV Variables | Required | example value |
---|---|---|
SCALE_DEPLOY_NUMBER | X | 0 |
SCALE_STS_NUMBER | X | 0 |
SCALE_NODES_NUMBER | X | 0 |
PROJECT_ID | X | gole |
CLUSTER_NAME | X | br-gole-01 |
GCLOUD_ZONE | X | southamerica-east1-a |
SCHEDULER_LABEL | X | scheduler=comercial |
SCHEDULER_POOL | X | pool-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.
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.
2- startup
Todo lo demás que creamos contiene la estructura que usaremos para que estos comandos se puedan ejecutar desde dentro de nuestro clúster.
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.
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.
Este artículo incluye:
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.
Ve a nuestra página de Contacto y chatea con nosotros. Estaremos encantados de servirle.
¡Éxito!