Skip to main content
Featured image for GKE Scheduler Autoscaling

GKE Scheduler Autoscaling

·1541 words·8 mins
Author
Maurício Santana Gonçalves
CTO (Chief Technology Officer) e Fundador da Gole Conectividade de Dados

Como diminuir o custo do kubernetes GKE eliminando nodes em janelas específicas de tempo sem utilizar nenhuma ferramenta ou API externa.

O problema
#

Nosso problema se apresenta quando temos um ambiente de testes (QA) que é utilizado somente em horário comercial e ficando ligado 24x7 gera um custo desnecessário.

Solução proposta
#

Desligar o ambiente de testes fora do horário do expediente, eliminando nodes de nosso cluster. Chegando perto do horário de início do expediente estes nodes devem ser criados e o ambiente de teste deve voltar a operar.

Dependências para esta solução
#

Para que isso seja possível precisamos ter três pontos de configuração:

  • 1- Um “node pool” separado que possa ser eliminado sem afetar a produção. Para deixar esta configuração mais eficaz pode incluir Taints and Tolerantios.

  • 2- Um label em comum para todos os deploys/sts que poderão ser desligados neste horário. Exemplo: “scheduler=comercial”

  • 3- Conhecer a lista de namespaces que contenham os deploys/sts que serão desativados.

Passos prévios da implementação
#

Precisamos ter certeza de que os nodes e os deploys estão configurados corretamente.

Validação do node-pool e dos deployments e/ou statefulsets
#

Vamos primeiro listar nossos nodes pools e validar que nosso ambiente está preparado:

> 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

Caso o node-pool não exista é necessário criar este node pool com o número correspondente de nodes, podendo ser apenas um, e adicionar o taint:

Criando node pool com taint
Criando node pool com taint

Caso o node-pool já exista mas não tenha o taint necessário, então utilize o recurso do gcloud ainda em beta para adicionar o taint:

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

Agora vamos validar se todos os deploys que desejamos contém os labels necessários.

Neste exemplo não teremos statefulset em nossa lista, mas logicamente que se encaixa perfeitamente.

> 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

Adicionando o tolerations no deploy
#

Precisamos ter certeza de que os deploys estão configurados corretamente

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

A saída deve ser um objeto deste abaixo vezes o número de deploys existentes. Vão estar todos dentro da mesma lista, mas olhando a lista é possível observar facilmente.

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

Temos aqui um exemplo de onde deve estar configurado o 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:

Validação final
#

  • Temos labels nos deployments? sim

  • Temos um node pool com um taint configurado? sim

  • Temos tolerations setados em nossos deployments? sim

Então agora podemos começar.

Implementação
#

Para a implementação da solução precisaremos criar alguns recursos:

Secret key.json
#

Nosso secret será o arquivo renomeado para key.json gerada em service account, keys dentro de IAM.

Logicamente este usuário de sistema deverá ter permissão para administrar o cluster kubernetes utilizando o sdk do gcloud.

key.json
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"
}

Vamos então adicionar este arquivo como um secret em um namespace específico para nossa automação.

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

Verificando a chave criada:

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

Obs: Garanta que somente administradores possam visualizar o conteúdo do namespace utilizado. No caso utilizaremos o namespace devops

Criando nosso Cronjob
#

Vamos agora criar um arquivo yaml que utiliza o kind Cronjob.

Nosso cronjob está configurado para:

  • Iniciar às 07:30h de segunda-feira a sexta-feira.

  • Desligar às 19:00h de segunda-feira a sexta-feira.

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

É possível notar que nosso cronjob utiliza dois pontos de montagem externos:

  • tz-config

  • gcloud-key

Nosso tz-config é um apontamento para o arquivo de zoneinfo encontrado em qualquer S.O. Linux, no caso estamos comunicando a nosso cronjob que seu fuso-horário é America/Sao_Paulo.

Nosso gcloud-key utiliza o modelo de secret, e é aqui onde faremos o link com o secret criado no passo 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 criar e aplicar o arquivo de cronjob devemos alterar o valor das variáveis.

Isso está localizado 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"

Uma vez que todos os passos foram seguidos, estamos prontos para aplicar nosso cronjob.

Gostaria somente de lhes comentar onde estão os comandos que realmente realizam o trabalho de baixar e subir o ambiente, uma vez que tudo o que fizemos até agora foi somente preparar a infraestrutura.

Quais são os comandos que vamos executar?
#

Os comandos que serão executados para efetuar a tarefa do autoscaling estão descritos neste projeto git (é o mesmo projeto):

O arquivo alpine-gcloud.sh está sendo chamado dentro do Cronjob na sessão commands.

O conteúdo do arquivo contém a sequência de comandos que fará nosso trabalho.

São dois os passos principais:

  • 1- shutdown.
  • Escalar os deploys que contém as labels declaradas para 0

  • Escalar os nodes do node pool que contém os taints para 0

  • 2- startup
  • Escalar os deploys que contém as labels declaradas para 1 ou mais

  • Escalar os nodes do node pool que contém os taints para 1 ou mais

Todo resto que criamos contém a estrutura que utilizaremos para que estes comandos possam ser executados de dentro de nosso cluster.

Aplicando o cronjob.
#

Uma vez que já salvamos localmente nosso arquivo de cronjob, e já modificamos as variáveis, horários e namespaces, vamos então aplicá-los.

Notem que os horários de execução do cronjob apontam 3h a mais, isso porque as APIs de controle dos nodes GKE utilizam horário UTC. Uma vez que os valores de TZ para cronjobs ainda estão em beta, o mais seguro é adicionar 3h a mais e chegaremos no horário correto.

Ou seja, se deseja 8h e está em uma cidade GMT -3, logo 8 + 3 = 11.

Para iniciar teremos (07:30h):

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

Para desligar (19:00h):

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

Quando ele executar teremos um job correndo:

> kubectl -n devops get job

Logicamente o job inicia um pod e com isso podemos acompanhar os logs.

Testes
#

Altere os horários para que corram em momentos que está trabalhando, a fim de validar que os processos corram corretamente.

Considerações finais
#

Este artigo contempla:

  • Kubernetes Google GKE;

  • Gcloud;

  • Node-pool;

  • Taints;

  • Auto-scaling;

  • Cronjobs

  • Container commands

  • Container env variables

Existem diversas formas de criar e administrar um cluster kubernetes, por isso não se limite a criar soluções ou modos diferentes para aplicar suas soluções.

Use e abuse dos recursos das APIs.

Precisa de ajuda?
#

Acesse nossa página de Contato e converse conosco. Teremos prazer em servi-los.

Sucesso!