preloader
  • GKE Scheduler Autoscaling
blog-thumb

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

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

> 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-pools
  • 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!