Como diminuir o custo do kubernetes GKE eliminando nodes em janelas específicas de tempo sem utilizar nenhuma ferramenta ou API externa.
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.
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.
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.
Precisamos ter certeza de que os nodes e os deploys estão configurados corretamente.
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:
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
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:
Então agora podemos começar.
Para a implementação da solução precisaremos criar alguns recursos:
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
.
> 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
Vamos agora criar um arquivo yaml que utiliza o kind Cronjob.
Nosso 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
É possível notar que nosso cronjob utiliza dois pontos de montagem externos:
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.
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 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.
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.
2- startup
Todo resto que criamos contém a estrutura que utilizaremos para que estes comandos possam ser executados de dentro de nosso cluster.
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.
Altere os horários para que corram em momentos que está trabalhando, a fim de validar que os processos corram corretamente.
Este artigo contempla:
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.
Acesse nossa página de Contato e converse conosco. Teremos prazer em servi-los.
Sucesso!