Provisionando um cluster de EKS sem Node Groups com Karpenter

Matheus Fidelis
9 min readAug 5, 2022

A proposta dessa PoC é criar e gerenciar um cluster de EKS utilizando apenas (ou quase) o Karpenter como provisionamento de recursos computacionais pro Workload produtivo, tirando a necessidade de Node Groups e Auto Scale Groups. Trazendo todo o gerenciamento de recursos pra dentro de CRD's do Karpenter.

Nesse cenário vamos assumir algumas premissas importantes:

  • O objetivo do Karpenter como tecnologia é prover um "just in time" scale, o que faz dele uma proposta interessante para workloads que tenham picos de acesso, processamentos agendados mais pesados e tenham um delta de escalabilidade computacional mais agressivos.
  • Essa proposta é excelente para muitos casos de uso, mas também é preciso assumir que gera uma volatilidade muito brusca na quantidade de nós e pods. Por isso é ideal que as aplicações e suas dependências estejam preparadas para morrer com segurança e aumentar ou diminuir o consumo de recursos na mesma proporção.
  • Como um cluster de Kubernetes é composto por várias "pecinhas de lego" muito importantes, e que muitas vezes não estão preparadas para lidar com essa volatilidade agressiva para qual essa PoC está sendo desenhada, o modo mais intuitivo que trouxe para resolver esse cenário foi colocar os namespaces de serviço, como prometheus, kube-system e outras aplicações "satélites" em nodes Fargate, para que eles sejam poupados dessas mudanças bruscas de capacity.

Provisionamento

Vou omitir bastante detalhes do código como um todo para não transformar esse artigo numa bíblia, mas fique tranquilo que todo o desenvolvimento está sendo documentado neste repositório do GitHub.

Roles de IAM — Cluster

Como qualquer cluster de Kubernetes vamos precisar providenciar com antecedência 3 tipos de roles. Uma para o Control Plane, outra que será usada como Instance Profile para as instâncias dos Nodes e outra para os Fargate Profiles. Esse provisionamento não muda nada dos casos que você provavelmente já conhece.

Iniciando pela role do Control Plane precisamos associar 2 managed policies, AmazonEKSClusterPolicy e AmazonEKSServicePolicy.

Roles de IAM — Nodes / Instance Profile

O provisionamento da Instance Profile dos nodes também não muda caso você fosse usar com Node Groups, com exceção de que vamos precisar criar a instance profile propriamente dita. Quando utilizamos Node Groups o próprio serviço do EKS se encarrega de fazer a criação desse recurso caso não exista previamente. Mas é simples.

Vamos precisar associar algumas Managed Policies padrão também para funcionar. Mas sem segredo de outros tipos de provisionamento.

Vamos criar uma associação de instance profile na role criada para os nodes para posteriormente criamos o Launch Configuration com ela.

Roles de IAM — Fargate Profiles

O provisionamento da role dos Fargate Profiles também é padrão. Escrevendo esse artigo me vem aquela sensação de "pow, essas roles já poderiam existir na conta por default né? Chatão". Pois é.

Funciona no mesmo esquema das anteriores, precisamos anexar algumas managed policies padrão para que o serviço funcione.

EKS Cluster

O provisionamento do cluster foi feito sem a base de um modulo ou facilitador. Até mesmo porque não seria interessante provisionar nada além do próprio control plane do EKS para PoC nesse primeiro momento.

Vamos utilizar o recurso base do aws_eks_cluster nos atentando as tags de discovery do Karpenter que precisam estar presentes.

Fargate Profile — Kube System

Como dito anteriormente nas premissas da PoC, tudo que se encaixar como um componente sistêmico do funcionamento da plataforma, e não como parte do workload será provisionado em Fargate Profiles para poupá-los da volatilidade de scale in / scale out que iremos trazer para o cluster com o Karpenter. Dito isso, vamos provisionar o fargate profile para o namespace do kube-system.

CoreDNS Fix — Workaround

Uma das coisas mais chatas e sem sentido do uso de cluster Full Fargate é a limitação do CoreDNS de subir naturalmente em nodes que não sejam EC2 efetivamente. Até a data desse artigo, é necessário utilizar de algum artificio automatizado ou manual para remover a label de eks.amazonaws.com/compute-type de ec2 para que ele consiga ser provisonado em nodes fargate.

Você pode fazer isso manualmente sem problemas diretamente com o kubectl.

kubectl patch deployment coredns -n kube-system --type json -p='[{"op": "remove", "path": "/spec/template/metadata/annotations/eks.amazonaws.com~1compute-type"}]

Porém como preguiça pouca é besteira, eu vou utilizar uma lambda que após o provisionamento do cluster se encarrega de remover essa label através da API do control plane.

Peguei a base inicial dessa lambda através do artigo Deploy CoreDNS on Amazon EKS with Fargate automatically using Terraform and Python escrito por Kevin Vaughan e Lorenzo Couto da AWS. Porém fiz algumas modificações de stepback e retry para realização dessa configuração porque algumas vezes falhava nas primeiras tentativas pela API não estar tão disponível quanto deveria.

O provisionamento dela está em um repositório de exemplo separado para ajudar em casos isolados e também futuramente transformar em módulo que resolve esse problema. Dor de cabeça pro Matheus do futuro.

No caso no Terraform iremos empacotar o script normalmente e criar a lambda na VPC que entregamos o cluster.

Nenhuma trigger é necessária para esse primeiro momento. Ao invés disso na pipeline vamos chamar um aws_lambda_invocation passando o endpoint do cluster e um token temporário que será utilizado para fazer o request com o Patch removendo a label. Isso irá forçar um redeploy do coreDNS, porém fazendo ele subir em nodes fargate com o planejado na própria execução da pipeline. Ganhando bastante tempo e diminuindo "gols de mão" do processo.

Provisionamento do Karpenter

O provisionamento do Karpenter com Terraform e Helm não tem segredo. Exemplo foi adaptado direto da excelente documentação do projeto. São passos bem semelhantes dos que vimos até agora. Onde será necessário providenciar uma Role para o serviço com um assume role federado para o OIDC do cluster (exemplo completo no repositório).

IAM Role

A role do karpenter precisa ter algumas permissões bem semelhantes ao Cluster Autoscaler caso você já tenha utilizado. Ele precisa de algumas permissões de controle de EC2 para poder lançar e deletar elas sob demanda. Sem segredo por aqui.

Karpenter — Fargate

Segundo passo é colocar o Karpenter para rodar em Fargate Profile semelhante como fizemos com o kube-system, para impedir de que um drain de nodes afete o próprio karpenter e dê algum problema no processo de uma forma geral. Então, seguindo a premissa de que se não é workload, está seguro em fargate, vamos subir ele também.

Karpenter — Helm

O Setup do Helm é baseado no da documentação do Karpenter com Terraform também. Vamos passar a role que criamos amarrada ao OIDC e ao WebIdentityProvider na Service Account para que o controlador possa executar operações nas API's da AWS.

Após o provisionamento teremos também o pod do Karpenter com os dois containers internos em estado de Ready/Running rodando em um Node Fargate idêntico aos do exemplo do kube-system.

Karpenter — Provisioner e Templates

Agora vamos trabalhar com o real diferencial dessa PoC com os demais tipos de provisionamento mais comuns. Caso você já tenha trabalhado com o provisionamento de clusters de EKS com Node Groups com Launch Templates customizados, essa parte será bem parecida.

No caso vamos criar um launch template versionado utilizando as AMI's recomendadas da AWS e a instance profile que criamos de antemão. Alguns passos de configuração como user-data foram omitidos do artigo, mas ressaltando que podem ser consultados no repositório de exemplo do artigo.

Em seguida vamos criar dois objetos pelo objeto kubectl_manifest do provider do kubectl para fazer deploy de dois recursos do CRD do Karpenter, um deles sendo o Provisioner onde vamos especificar os tamanhos de instancias, limites de CPU e memória e coisas relacionadas a capacity e outro sendo um AWSNodeTemplate onde vamos especificar os launch templates dos nodes.

Para facilitar eu optei por usar templatefile para criar os manifestos que seriam aplicados pelo provider do kubectl. No caso para ficar mais evidente, coloquei algumas variáveis para fazer o build dos YAMLs via template dessa forma:

No final será criado um resource parecido com o abaixo:

Disclaimer: Durante a PoC tentei utilizar o provider do kubernetes para criar os objetos customizados do Karpenter diretamente pelo kubernetes_manifests, porém existe um bug de dependências no resource que inviabiliza o provisionamento de CRD's juntamente com o cluster. Por isso precisei utilizar o kubectl provider para que continue sendo possível o provisionamento de toda a infraestrutura de uma única vez.

Abri uma issue que permanece aberta (até esse momento) pra isso:

Aplicação de Testes

Agora que temos todos os recursos do cluster minimamente provisionados, vamos testar o funcionamento do Karpenter. Vamos fazer deploy de uma aplicação de exemplo para ver se os nodes vão ser provisionados para suprir o novo capacity solicitado.

❯ kubectl apply -f files/deploy/demo/chip.yaml
namespace/chip created
deployment.apps/chip created
service/chip created
horizontalpodautoscaler.autoscaling/chip created

No caso foi provisionado para suprir os 2 novos pods solicitados para a aplicação de exemplo. Agora vamos executar os cenários de scale in e out para ver como o ambiente se comporta.

Cenário 1 — Scale In

Vamos exemplificar o cenário onde temos 4 nodes iniciais no cluster, e vamos fazer o scale de um deployment de 2 replicas para 100 de forma brusca para ver como o Karpenter vai lidar com essa mudança de capacity solicitado.

kubectl scale --replicas 100 deployment/chip -n chip

Replicas iniciais do deployment: 4
Replicas desejadas do deployment: 100
CPU Requests: 250m
RAM Requests: 512m
Quantidade de Nodes Iniciais: 4
Quantidade de Nodes final: 25
Horário do Apply: 17:10:35
Horário do scale finalizado: 17:13:50
Tempo Total: 00:03:25

Conseguimos provisionar um capacity para suprir uma demanda brusca de 4 para 100 unidades computacionais que necessitavam de nodes em 3 minutos.

Cenário 2 — Scale Out

Agora vamos testar o cenário inverso, onde vamos fazer o scale out do ambiente de forma brusca para avaliar como karpenter vai lidar com esse capacity fora de uso

kubectl scale --replicas 4 deployment/chip -n chip

Replicas iniciais do deployment: 100
Replicas desejadas do deployment: 4
CPU Requests: 250m
RAM Requests: 512m
Quantidade de Nodes Iniciais: 25
Quantidade de Nodes final: 4
Horário do Apply: 17:18:55
Horário do scale finalizado: 17:19:50
Tempo Total: 00:00:55

Para o scale out de nodes em desuso foi ainda melhor que scale in, fazendo um desligamento em massa de 25 nodes para 4 em 55 segundos.

Lembrando que toda a PoC foi disponibilizada no Github.

Obrigado aos Revisores:

Referencias:

Me sigam no Twitter para acompanhar as paradinhas que eu compartilho por lá!

Te ajudei de alguma forma? Me pague um café (mentira, todas as doações são convertidas para abrigos de animais da minha cidade)

Chave Pix: fe60fe92-ecba-4165-be5a-3dccf8a06bfc

--

--