O blog da AWS

Escalonamento de Nós com Amazon EKS e Karpenter

Por Douglas Ramiro, Arquiteto de Soluções especializado em Spot e Graviton na AWS e
Por Lucas Duarte, Arquiteto Especialista em Containers na AWS 

 

Quando falamos de Kubernetes, um dos principais desafios é o escalonamento das aplicações. Uma boa definição de configuração de escalonamento traz como benefícios performance, resiliência e também economia de custos.

Um cluster de Kubernetes, pode ser configurado para escalar dinamicamente uma aplicação tanto de forma horizontal, através do Horizontal Pod Autoscaler (HPA) quanto de forma vertical através do uso do Vertical Pod Autoscaler (VPA).

Durante o escalonamento, um novo Pod pode ficar com o status de pendente caso o cluster não tenha recursos disponíveis no momento como por exemplo CPU ou memória. Nesse momento, novos nós precisam ser provisionados e disponibilizados no cluster. Esse processo pode ser automatizado através do uso do Cluster Autoscaler.

O Cluster Autoscaler no EKS se integra com o AWS Auto Scaling através da criação de novos nós em um node group modificando o desired state. Um cluster EKS pode necessitar de diversos node groups com estratégias diferentes. Por exemplo, pode ter um node group definindo instâncias reservadas, outro que utiliza instâncias SPOT e mais um com instâncias sob demanda.

Alguns desafios podem ser encontrados nessas configurações. Por exemplo, quais tipos de instâncias EC2 devem fazer parte de cada node group pensando nas aplicações? Dependendo da quantidade de novos pods a serem criados durante um pico de acessos da aplicação, pode ser necessário a criação de vários nodes ao mesmo tempo, impactando assim no tempo necessário para a criação de vários nós devido ao cool-down do Cluster Autoscaler.

Pensando em algumas limitações existentes com a necessidade de node groups para escalar a quantidade de nós de um cluster, o Karpenter foi criado. Ele é uma solução de gerenciamento de nós incubada no AWS Labs e que foi construída para funcionar em qualquer cluster Kubernetes sendo uma solução open source.

O Karpenter observa os pods que estão no status de pendente por falta de recursos e automaticamente provisiona um novo nó com a capacidade necessária através da seleção da melhor instância para aquele momento. Para realizar essa função, ele analisa a definição de resource requests do Pod e também as suas constraints, como por exemplo, a utilização de nodeSelector para definir uma label específica no qual o Karpenter vai analisar e levar em connsideração para o provisionamento de novas instâncias.

Em um cenário em que se tenham 8 pods que necessitem cada um de 1 vCPU e 1GiB de memória, o Karpenter pode provisionar apenas uma única máquina que atenda a esses pods, como por exemplo uma instância c1.xlarge.

O Karpenter otimiza o scheduling de eficiência e latência utilizando dois control-loops. Em primeiro lugar, está o allocator, um controller sensível à latência de ação rápida responsável por garantir que os pods pendentes sejam alocados o mais rápido possível. Em segundo lugar, está o reallocator, um controller sensível ao custo de ação lenta que substitui os nós conforme as solicitações dos pods e os preços da capacidade mudam com o tempo. Juntos, eles maximizam a disponibilidade e a eficiência do seu cluster.

Além disso, o Karpenter é capaz de terminar nós que sejam considerados expirados e também mover pods de nós para uma melhor alocação de recursos. Suas configurações são definidas em um CRD chamado Provisioner.

O Karpenter é suportado em versões do Kubernetes v.1.19 ou superiores. Entretanto, isso pode mudar no futuro já que o Karpenter depende de novas funcionalidades do Kubernetes.

Instalando e Configurando um cluster EKS para utilizar o Karpenter

  1. Antes de instalar um cluster EKS e o Karpenter, os seguintes utilitários precisam estar instalados e configurados:
    • AWS CLI – AWS Command Line Interface
    • kubectl – Kubernetes Command Line Interface
    • eksctl – AWS EKS Command Line Interface
    • helm – Gerenciador de pacotes do Kubernetes
  1. Após a instalação e configuração dos utilitários, defina as seguintes variáveis de ambiente:
export CLUSTER_NAME=karpenter-demo 
export AWS_DEFAULT_REGION=us-east-1 
AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
  1. Crie um novo cluster EKS com o utilitário eksctl. Nesse exemplo, um cluster simples será criado, definindo um IAM OIDC Essa configuração é necessária para habilitar IAM roles para pods.
cat <<EOF > cluster.yaml
---
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
  name: ${CLUSTER_NAME}
  region: ${AWS_DEFAULT_REGION}
  version: "1.20"
managedNodeGroups:
  - instanceType: m5.large
    amiFamily: AmazonLinux2
    name: ${CLUSTER_NAME}-nodegroup
    desiredCapacity: 1
    minSize: 1
    maxSize: 10
iam:
  withOIDC: true
EOF

eksctl create cluster -f cluster.yaml
NOTE
O Karpenter pode ser executado em self-managed groups, managed node groups ou AWS Fargate. Nessa demonstração, um self-managed group está sendo utilizado.
  1. O Karpenter identifica em quais zonas de disponibilidade ele vai poder provisionar novos nós para o cluster através das subnets que possuam a tag io/cluster/$CLUSTER_NAME. Adicione a tag as subnets privadas do cluster:
SUBNET_IDS=$(aws cloudformation describe-stacks \
 --stack-name eksctl-${CLUSTER_NAME}-cluster \
 --query 'Stacks[].Outputs[?OutputKey==`SubnetsPrivate`].OutputValue' \
 --output text)

aws ec2 create-tags \
 --resources $(echo $SUBNET_IDS | tr ',' '\n') \
 --tags Key="kubernetes.io/cluster/${CLUSTER_NAME}",Value=
  1. Instâncias criadas no serviço do EC2 pelo Karpenter precisam rodar com um perfil (InstanceProfile)que tenha permissão para rodar containers e configurar a parte de rede. O Karpenter vai procurar por um perfil seguindo a seguinte nomenclatura:

KarpenterNodeRole-${ClusterName}

Utilize os comandos abaixo para criar os recursos IAM necessários:

TEMPOUT=$(mktemp)
curl -fsSL https://karpenter.sh/docs/getting-started/cloudformation.yaml > $TEMPOUT \
&& aws cloudformation deploy \  
--stack-name Karpenter-${CLUSTER_NAME} \  
--template-file ${TEMPOUT} \  
--capabilities CAPABILITY_NAMED_IAM \  
--parameter-overrides ClusterName=${CLUSTER_NAME}
  1. As novas instâncias precisam de permissão para se conectar ao cluster. Execute o comando abaixo para adicionar a role do Karpenter criada no passo anterior no aws-authconfigmap para que assim as novas instâncias EC2 possam se conectar ao cluster.
eksctl create iamidentitymapping \  
--username system:node:{{EC2PrivateDNSName}} \  
--cluster  ${CLUSTER_NAME} \  
--arn arn:aws:iam::${AWS_ACCOUNT_ID}:role/KarpenterNodeRole-${CLUSTER_NAME} \  
--group system:bootstrappers \  
--group system:nodes
  1. O Karpenter precisa de permissões como por exemplo para criar novas instâncias. No passo 5, uma policy foi criada com as permissões necessárias. Cria uma nova service account para utilizar essa policy:
eksctl create iamserviceaccount \
  --cluster $CLUSTER_NAME --name karpenter --namespace karpenter \
  --attach-policy-arn arn:aws:iam::$AWS_ACCOUNT_ID:policy/KarpenterControllerPolicy-$CLUSTER_NAME \
  --approve
  1. Caso instâncias Spot nunca tenham sido utilizados na sua conta AWS, é necessário criar a EC2 Spot Service Linked Role:

aws iam create-service-linked-role --aws-service-name spot.amazonaws.com

Caso essa role já tenha sido criado previamente, o seguinte erro será mostrado e pode ser ignorado:

An error occurred (InvalidInput) when calling the CreateServiceLinkedRole operation: Service role name AWSServiceRoleForEC2Spot has been taken in this account, please try a different suffix.

  1. A instalação do Karpenter é feita através do deployment de um chart utilizando o helm:
helm repo add karpenter https://charts.karpenter.sh
helm repo update
helm upgrade --install --skip-crds karpenter karpenter/karpenter --namespace karpenter \  
--create-namespace --set serviceAccount.create=false --version 0.4.1 \  
--set controller.clusterName=${CLUSTER_NAME} \  
--set controller.clusterEndpoint=$(aws eks describe-cluster --name ${CLUSTER_NAME} --query "cluster.endpoint" --output json) \  
--wait
  1. O Karpenter utiliza para provisionar novos nós no cluster um Custom Resource Definition (CRD) chamado de provisioner CRD.  Esse CRD pode definir diversas configurações, como por exemplo os taintsque o nó vai ter, os labels, quais instâncias podem ser utilizadas, em quais zonas de disponibilidade o novo nó pode ser criado, qual arquitetura é suportada, entre outros. Crie um novo provisioner:
cat <<EOF | kubectl apply -f -
apiVersion: karpenter.sh/v1alpha5
kind: Provisioner
metadata:
  name: default
spec:
  requirements:
    - key: karpenter.sh/capacity-type
      operator: In
      values: ["spot","on-demand"]
  provider:
    instanceProfile: KarpenterNodeInstanceProfile-${CLUSTER_NAME}
  ttlSecondsAfterEmpty: 30
EOF

Suportando diferentes arquiteturas de processadores

Quando um provisioner não especifica a arquitetura e também os tipos de instância em que ele poderá criar novos nós, por padrão, todas as instâncias e todas as arquiteturas (amd64 e arm64) serão utilizadas. Isso significa que uma instância Graviton pode ser criada, e com isso, a aplicação que for ser executada pode não suportar a arquitetura da instância, isso pode ocasionar erros na hora da execução.

Por questões de custos e performance, é interessante rodar containers que suportam mutil-arquitetura ao invés de não permitir instâncias Graviton. Utilize o seguinte blog post para verificar como se criar imagens de container multi-arquitetura.

Em alguns casos não é possível utilizar arquitetura ARM seja por não ter acesso a construção da imagem, ou mesmo por a aplicação não suportar a arquitetura. Para essas aplicações, duas técnicas podem ser utilizadas para especificar uma arquitetura.

A primeira consiste em se criar um provisioner alternativo em que apenas a arquitetura AMD seja definida. Dessa forma, ao se criar um novo deployment, é possível especificar esse novo provisioner através de um node selector.  Para utilizar essa técnica, defina um node selector em que a chave seja karpenter.sh/provisioner-name e o valor seja o nome do provisioner criado:

spec:
  template:
    spec:
      nodeSelector:
        karpenter.sh/provisioner-name: novo-provisioner

A segunda forma consiste em utilizar o well-known label kubernetes.io/arch no manifesto da aplicação (deployment / Pod) também utilizando um node selector. Dessa forma, o Karpenter vai provisionar um nó que suporte a arquitetura especificada.

spec:
  template:
    spec:
      nodeSelector:
        kubernetes.io/arch: amd64

Essa técnica pode ser utilizada com outros labels, como por exemplo para especificar em qual zona de disponibilidade o novo nó deve ser criado.

Como o Karpenter gerencia interrupções SPOT em comparação com o AWS Managed Node Group

Para simular a interrupção de uma instância SPOT, podemos utilizar o FIS (AWS Fault Injection Simulator). O FIS um serviço totalmente gerenciado para a execução de experimentos de injeção de falha na AWS que torna mais fácil melhorar a performance, observabilidade e resiliência de uma aplicação. Os experimentos de injeção de falha são usados em engenharia do caos, que é a prática de estressar uma aplicação ao testar ou produzir ambientes criando eventos disruptivos, como um aumento repentino no consumo da CPU ou memória, observando como o sistema responde e implementando melhorias. O experimento de injeção de falha ajuda as equipes a criar condições reais necessárias para descobrir bugs escondidos, monitorando pontos cegos e gargalos de performance que são difíceis de achar em sistemas distribuídos.

Dessa forma, podemos simular uma interrupção de uma instância SPOT e verificar o seu comportamento quando ela é interrompida, indepentende se foi criada pelo Karpenter ou pelo Managed Node Group.

Quando uma instância foi criada a partir de um Managed Node Group,  ao simular uma interrupção, assim que o evento de rebalance foi propagado, como resposta ao evento, o drain é feito de forma automática. Dessa forma o nó é marcado como unschedulable, fazendo com que não seja possível implantar um novo Pod nesse nó. Caso outros nós tenham capacidade para receber os Pods que foram finalizados e a quantidade de nós sejam maior ou igual ao definido como mínimo no Managed Node Group, nenhum outro nó será criado. Caso contrário, o Managed Node Group vai provisionar uma nova instância EC2 de acordo com o tipo de instância definido, adiciona-la ao cluster e os Pods serão implantados no novo nó.

Esse fluxo é diferente quando uma instância é criada a partir do Karpenter. O Karpenter não tem integração nativa com os eventos de SPOT. Dessa forma, assim que uma interrupção acontece, o nó se torna NotReady de forma imediata e os Pods que estavam nela são terminados. O EKS então provisiona novos Pods para substituir os antigos. Caso tenham outros nós com capacidade para receber os Pods que foram finalizados, nenhum outro nó será criado. Caso contrário, o Karpenter vai analisar qual a melhor instância a ser utilizada para esses Pods e vai provisionar uma nova instância EC2 para que os Pods possam ser implantados.

É possível ter um comportamento parecido com o do Managed Node Group ao se utilizar o Karpenter, ou seja, fazer o drain do nó quando o evento de rebalance for lançado. Isso traz como vantagem a possibilidade de fazer o graceful shutdown da aplicação.  Para isso, é possível utilizar o AWS Node Termination Handler. Com ele, assim que o evento de rebalance é enviado, o drain é feito e o nó marcado como unschedulable. O EKS então provisiona novos Pods para substituir os antigos. Em caso de necessidade, o Karpenter provisiona um novo nó para receber os Pods. A diferença dessa solução para o Managed Node Group é que o AWS Node Termination Handler implanta um Pod em cada nó para ficar monitorando se a instância recebeu um evento enquanto que o Managed Node Group faz isso de maneira nativa.  

Como o Karpenter lida com o upgrade de versões do Control Plane

O Amazon EKS trás uma maneira preescritiva de realizar o update do Control Plane. O processo de atualização consiste em o Amazon EKS iniciar novos nodes do API Server com a versão atualizada do Kubernetes para substituir os nós existentes. Por se tratar de um serviço gerenciado, o Amazon EKS executa verificações de integridade nesses novos nodes para validar o seu funcionamento. É importante salientar que , no momento de realizar o update da versão, a versão dos Node Groups deve ser a mesma que a versão do Control Plane. Mais informações sobre o update preescritivo podem ser encontradas neste link.

Para os Data Plane Nodes, o Karpenter verificar a versão do Control Plane automaticamente. Dessa forma, assim que um novo node é lançado pelo Karpenter, a versão do kubelet já está atualizada para refletir a mesma versão do Control Plane do Amazon EKS, facilitando assim o processo de upgrade. Isso acontece porque o Karpenter utiliza automaticamente uma nova AMI com a versão atualizada do kubelet. É importante lembrar que apenas o kubelet é atualizado, os add-ons instalados separadamente bem como os gerenciados pelo EKS precisam ser atualizados caso necessário.

Conclusão

O Karpenter como visto nesse blogpost é uma solução para o lifecycle dos nós no Kubernetes, instalado como add-on no nosso cluster com Custom Resource Definitions (CRDs). Para facilitar a sua configuração, pode ser utilizado em conjunto com outros add-ons como o AWS Node Termination Handler e se integra perfeitamente com o Amazon EKS e outros serviços da AWS, podemos utilizar o Karpenter como um substituto para o Cluster Autoscaler visto que as suas funcionalidades são similares.

Porém com o Karpenter temos a opção de selecionar a instância com o melhor custo benefício para atender as nossas demandas, além de poder utilizar instâncias Graviton que tem um custo menor que comparadas com x86 e Spot de forma automática, apenas definindo essa específicação no provisioner.

Existem várias formas de resolver o problema de lifecycle dos nodes e o Karpenter é apenas uma delas, é importante destacar que cada caso é um caso e que os testes com a solução escolhida são muito importantes para garantir um bom funcionamento em seu cluster Kubernetes, seja utilizando Karpenter, Cluster Autoscaler ou qualquer outra ferramenta disponível.

 


Sobre os autores

Douglas Ramiro é arquiteto de soluções com especialização em Spot e Graviton na AWS. Todos os dias, os clientes da AWS economizam ao adotar essas tecnologias. Douglas tem a missão de disseminar essas tecnologias em LATAM para que os clientes AWS possam reduzir seus custos.

 

 

 

 

Lucas Duarte é um Arquiteto Especialista em Containers na AWS com focos em clientes LATAM. Entusiasta de automação, Cloud, e cultura DevOps. Com experiência prévia em projetos focados nesse segmento em empresas como IFood, Guiabolso e Mandic. Tem trabalhado em diferentes projetos relacionados principalmente a orquestração de containers e microsserviços.

 

 

 

Explore mais conteúdos sobre Computação na página de Sessions On Demand.

Acesse >