Tech Writers

Como o Karpenter otimizou a gestão da nossa infraestrutura EKS na AWS

6 minutos

Empresas enfrentam desafios diários na gestão de infraestrutura Kubernetes, especialmente para manter eficiência e reduzir custos. Aqui na Softplan, descobrimos uma solução que transforma a maneira de gerenciar nossos clusters EKS na AWS: o Karpenter.

Desafios na gestão de instâncias

Antes de falar de Karpenter é preciso dar alguns passos atrás e explicar um pouco do que se trata um auto escalonamento de nodes. Suponha que temos nosso cluster com algumas máquinas (instâncias) disponíveis executando nossos workloads. O que acontece se por acaso houver um pico de uso em nossas aplicações e seja necessário subir mais instâncias (réplicas) de nossos pods? Sem um autoscaling precisaríamos provisionar um node, orientá-lo a juntar-se ao nosso cluster para aí sim nossos pods estarem aptos a serem iniciados nessa nova instância. Lembrando que o provisionamento de uma instância não é instantâneo, há todo um bootstrapping da máquina, configurações de rede e muitas outras coisas antes dela ficar totalmente disponível.

Certo, falamos sobre pico de usuários em nossas aplicações, mas e quando houver ociosidade? Queremos mesmo deixar esses nodes em pé com poder computacional subutilizado? Para resolver essa e outras questões, entra em cena o conceito de auto scalers.

Auto Scalers

As implementações de auto scalers são responsáveis basicamente pelo provisionamento e consolidação de nodes. Aqui estamos falando de escalonamento horizontal, ou seja, adicionando mais máquinas em nosso cluster. Há diversas implementações de node autoscaling, mas neste artigo o foco será na implementação da AWS e por que decidimos migrar para uma outra solução. Abaixo uma figura exemplificando o funcionamento do node autoscaling:

Figura 01: autoscaling

Figura 01: autoscaling

AWS – Auto Scaling Groups

Ao definir um grupo de escalonamento na AWS precisamos definir diversas propriedades, como o número mínimo/máximo de instâncias de nodes permitidas para este grupo, recursos utilizados, tipo de disco, configurações de rede (subnets, etc) e muitos outros detalhes. Por exemplo, para um determinado tipo de aplicação que utilize mais CPU vamos configurar um grupo que contenha tipos de instância com mais CPU do que memória. No fim possivelmente teremos alguns grupos distintos para certos tipos de aplicações.

Juntando as peças – Cluster Auto Scaler

Para que meu cluster consiga “conversar” com meu cloud provider (neste exemplo AWS), precisamos de um componente chamado Cluster Auto Scaler, ou CAS.Este componente foi criado pela própria comunidade que mantém o Kubernetes, e está disponível aqui.

Uma configuração padrão do CAS pode ser vista abaixo, utilizando o helm para instalação:

nameOverride: cluster-autoscaler
awsRegion: us-east-1
autoDiscovery:
  clusterName: meu-cluster
image:
  repository: registry.k8s.io/autoscaling/cluster-autoscaler
  tag: v1.30.1
tolerations:
  - key: infra
    operator: Exists
    effect: NoSchedule
nodeSelector:
  environment: "infra"
rbac:
  create: true
  serviceAccount:
    name: cluster-autoscaler
    annotations:
      eks.amazonaws.com/role-arn: "role-aws"
extraArgs:
  v: 1
  stderrthreshold: info

Com isso configurado e instalado e nossos autoscaling groups criados acabamos de habilitar o gerenciamento automático de nossos nodes!

Por que decidimos migrar para o Karpenter

Nosso caso de uso aqui na Projuris é o seguinte: temos um cluster de desenvolvimento e outro de produção. Depois da migração para o Gitlab SaaS tínhamos um desafio de como provisionar os runners para a execução de nossas pipelines. Ficou decidido que usaríamos o cluster de desenvolvimento para provisionamento desses runners. Na “primeira versão” optamos pelo cluster auto scaler por ser uma configuração mais simples e que já atendia nosso setup em produção. Mas aí começamos a enfrentar alguns problemas com esta escolha:

  1. Tempo de provisionamento: ao iniciar uma pipeline o tempo de provisionamento da máquina era um pouco lento. O grande ponto é que o cluster auto scaler paga um “pedágio” no cloud provider para provisionamento de um novo node.
  2. Dificuldade na configuração de grupos: como temos alguns “perfis” de pipeline essa gestão ficou um pouco complicada, porque para cada novo perfil um novo node group precisa ser criado.
  3. Custo: para mitigar o problema de lentidão no startup de um novo node tínhamos um perfil de máquina “online” que ficava o tempo todo de pé, mesmo sem executar nenhuma pipeline.

O que é o Karpenter?

É uma solução de cluster autoscaling criada pela AWS, que promete o provisionamento e consolidação de nodes sempre com o menor custo possível. Ele é inteligente o suficiente para saber que por exemplo, ao comprar uma máquina na AWS do tipo on-demand, dependendo da situação, é mais em conta do que se fosse uma máquina spot. E essa é apenas uma das características dessa ferramenta.

O Karpenter também trabalha com a ideia de “grupos” de máquinas (que no mundo do Karpenter chamamos de NodePools), só que a diferença é que fazemos isso através de CRDs (custom resource definitions) do próprio Karpenter, ou seja, temos manifestos dentro de nosso cluster com todas essas configurações, eliminando a necessidade de qualquer node group criado na AWS. Exemplo de um NodePool no Karpenter:

apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: karpenter-gitlab-runner-small-online
spec:
  template:
    metadata:
      labels:
        workload: gitlab-runners
        environment: karpenter-nodes-gitlab-runner-build-small-online
    spec:
      requirements:
        - key: "karpenter.sh/capacity-type"
          operator: In
          values: ["spot", “on-demand”]
        - key: "node.kubernetes.io/instance-type"
          operator: In
          values: ["m5d.large", "m5n.large", "m6id.large", "m6in.large"]
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: my-node-class
      taints:
        - key: "gitlab-runner-karpenter"
          value: "true"
          effect: NoSchedule
      expireAfter: Never
  disruption:
    consolidationPolicy: WhenEmpty
    consolidateAfter: 5m
    budgets:
      - nodes: "20%"
  limits:
    cpu: "500"
    memory: 500Gi

Além do NodePool precisamos criar um NodeClass para definir detalhes específicos de instâncias AWS:

apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
  name: my-node-class
spec:
  amiFamily: AL2
  role: "aws-role"
  tags:
    Name: nodes-k8s-nodes-gitlab-runner-small-online
  subnetSelectorTerms:
    - tags:
        karpenter.sh/subnet: "my-subnet"
  securityGroupSelectorTerms:
    - id: "sg-123"
    - id: "sg-456"
    - id: "sg-789"
  amiSelectorTerms:
    - id: "imagem ami"
  kubelet:
    clusterDNS: ["111.222.333.44"]
  blockDeviceMappings:
    - deviceName: /dev/xvda
      ebs:
        volumeSize: 40Gi
        volumeType: gp3
        encrypted: true

OBS: perceba que o nome “my-node-class” precisa bater com o node class configurado no node pool.

Como o Karpenter nos ajudou a superar os desafios apresentados?

  1. Tempo de provisionamento: como o Karpenter conversa diretamente com as APIs do cloud provider não é necessário pagar o pedágio do cluster auto scaler. Tínhamos muitos problemas de timeout no provisionamento de novos nodes, após a troca pelo Karpenter esse problema simplesmente desapareceu justamente porque o provisionamento é mais eficiente.
  2. Dificuldade na configuração de grupos: com a solução de NodePools e NodeClass do Karpenter essa configuração ficou trivial, e o mais importante, versionada em nosso controle de versões no Gitlab. Ou seja, precisa incluir um perfil de máquina novo no NodePool? Sem problemas, basta um commit e o Karpenter já irá considerar isso nos novos provisionamentos.
  3. Custo: Conseguimos utilizar a utilização de máquinas, pois agora runners com características semelhantes são alocados em nodes que suportem os requisitos de memória e CPU exigidos. Ou seja, estamos realmente usando todo o poder computacional que aquele node proporciona. Isso vale também para a consolidação de nodes. Com o cluster auto scaler haviam scripts complexos para fazer o drain dos nodes antes da consolidação. Com o Karpenter isso é configurado no NodePool de maneira muito simplificada.

Um ótimo argumento para a gestão que justifique o investimento nesse tipo de mudança é custo. Abaixo temos um comparativo do custo utilizando o Cluster AutoScaler e o Karpenter em Janeiro/25, onde conseguimos uma economia de 16% no total:

Figura 02: Período de 01/01 à 15/01 com ClusterAutoScaler

Figura 02: Período de 01/01 à 15/01 com ClusterAutoScaler

Figura 03: Período de 16/01 à 31/01 com o Karpenter

Figura 03: Período de 16/01 à 31/01 com o Karpenter

Considerações finais

A migração para o Karpenter foi uma escolha acertada. Conseguimos simplificar a gestão de nossos nodes com diferentes perfis de forma bastante simplificada. Ainda há espaço para algumas melhorias, como por exemplo a utilização de um único NodePool para simplificar ainda mais, e deixar que os runners configurem labels específicas para o perfil de máquina que deve ser provisionado para o runner (mais em https://kubernetes.io/docs/reference/labels-annotations-taints/).

Referências

Karpenter (doc oficial): https://karpenter.sh/

Node Auto Scaling (doc oficial k8s): https://kubernetes.io/docs/concepts/cluster-administration/node-autoscaling/

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *