O blog da AWS

Gerencie cenários de escala até zero com Karpenter e Serverless

Por Giacomo Margaria, Marco Ballerini e Federica Ciuffo

Introdução

O Cluster autoscaler tem sido, de fato, o mecanismo de escalonamento automático padrão do setor no kubernetes desde a versão inicial da plataforma. No entanto, com a crescente complexidade e o número de cargas de trabalho em contêineres, nossos clientes que usam o Amazon Elastic Kubernetes Service (Amazon EKS) começaram a solicitar uma forma mais flexível de alocar recursos computacionais para pods e flexibilidade no tamanho e heterogeneidade das instâncias. Atendemos a essas necessidades com o karpenter, um produto que lança automaticamente os recursos computacionais certos para lidar com os aplicativos do seu cluster. O Karpenter foi projetado para aproveitar ao máximo o Amazon Elastic Compute Cloud (Amazon EC2). Embora tenham o mesmo propósito, o cluster autoscaler e o karpenter adotam uma abordagem muito diferente em relação ao escalonamento automático. Neste post, não focaremos nas diferenças das duas soluções, mas analisaremos como elas podem ser usadas para atender a um requisito específico: escalar um cluster Amazon EKS para zero nós. A escalabilidade de um cluster Amazon EKS para zero nós pode ser útil por vários motivos. Por exemplo, talvez você queira reduzir seu cluster para zero nós quando não houver tráfego, ou talvez queira reduzir seu cluster para zero nós ao realizar a manutenção. Isso não apenas reduz os custos, mas aumenta a sustentabilidade da utilização dos recursos.

Visão geral da solução

Considerações de custo da redução de clusters

O pilar de otimização de custos do AWS Well-Architected Framework inclui uma seção específica que se concentra nas vantagens financeiras da implementação de uma estratégia de fornecimento just-in-time. O escalonamento automático geralmente é a abordagem preferida para combinar a oferta com a demanda.

Figura 1: Ajustando a capacidade conforme necessário.

Escalonamento Automático no Amazon EKS

Quando se trata do Amazon EKS, precisamos pensar no escalonamento automático do plano de controle e no escalonamento automático do Data Plane como duas preocupações distintas.

Quando o Amazon EKS foi lançado em 2018, o objetivo era reduzir a sobrecarga operacional dos usuários fornecendo um Control Plane gerenciado para kubernetes. Inicialmente, isso incluía atualizações, patches e backups automatizados, mas com capacidade fixa. Hoje, o Control Plane é escalado automaticamente quando determinadas métricas são excedidas.

O Data Plane, por outro lado, não é totalmente gerenciado pela AWS (com exceção do AWS Fargate). Os grupos de nós gerenciados reduzem a carga operacional ao automatizar o provisionamento e o gerenciamento do ciclo de vida dos nós. No entanto, atualizações, patches, backups e escalonamento automático são de responsabilidade do cliente.

Neste post, abordaremos a escalabilidade automática do Data Plane e, mais especificamente, como existem diferentes maneiras de executar nós do Amazon EKS — usando instâncias do Amazon EC2, AWS Fargate ou AWS Outposts — vamos nos concentrar nos nós do Amazon EKS executados no Amazon EC2.

Antes de prosseguirmos, vamos examinar mais de perto como o kubernetes tradicionalmente lida com o escalonamento automático para pods e nós.

Pods de escalonamento Automático

No kubernetes, o escalonamento automático de pods é feito por meio do Horizontal Pod Autoscaler (HPA), que atualiza automaticamente um recurso de carga de trabalho (como um Deployment ou StatefulSet), com o objetivo de escalar automaticamente a carga de trabalho para atender a demanda.

O dimensionamento horizontal significa que a resposta ao aumento da carga é implantar mais pods. Isso é diferente do escalonamento vertical, que, para o kubernetes, significa atribuir mais recursos (por exemplo, memória ou unidades centrais de processo [CPUs]) aos pods que já estão em execução para a carga de trabalho.

Figura 2: Escalonamento automático de pods com o escalonador automático de pods horizontais.

Quando a carga diminui e o número de pods está acima do mínimo configurado, o escalonador automático de pods horizontal instrui o recurso de carga de trabalho (ou seja, o deployment, o StatefulSet ou outro recurso similar) a ser escalado novamente.

No entanto, o Horizontal Pod Autoscaler não oferece suporte nativo à redução para zero pods.

Existem alguns operadores que permitem superar essa limitação interceptando as solicitações que chegam aos seus pods ou verificando algumas métricas específicas, como Knative ou Keda. No entanto, esses são mecanismos sofisticados para obter um comportamento sem servidor e estão além do escopo desta postagem sobre escalonamento para zero baseado em nós.

Nós de escalonamento automático

No kubernetes, o escalonamento automático de nós pode ser tratado usando o cluster autoscaler, que é uma ferramenta que ajusta automaticamente o tamanho do cluster kubernetes quando uma das seguintes condições é verdadeira:

• há pods que falharam ao serem executados no cluster devido a recursos insuficientes.
• há nós no cluster que foram subutilizados por um longo período de tempo e seus pods podem ser colocados em outros nós existentes.

Figura 3: Escalonamento automático de nós com o Cluster Autoscaler.

O Cluster Autoscaler diminui o tamanho do cluster quando alguns nós são constantemente desnecessários por um determinado período de tempo. Um nó é desnecessário quando tem baixa utilização e todos os seus pods importantes podem ser movidos para outro lugar.

Quando se trata de grupos de nós baseados no Amazon EC2 (supondo que seu tamanho mínimo esteja definido como 0), o cluster autoscaler escala o grupo de nós para 0 se não houver pods impedindo a operação da escala.

Modelo de preços e considerações de custo

Para cada cluster do Amazon EKS, você pagaQuando se trata de grupos de nós baseados no Amazon EC2 (supondo que seu tamanho mínimo esteja definido como 0), o cluster autoscaler escala o grupo de nós para 0 se não houver pods impedindo a operação da escala.uma taxa horária básica para cobrir o Control Plane gerenciado, bem como o custo de execução do Data Plane baseado no Amazon EC2 e quaisquer volumes associados. Os custos horários do Amazon EC2 variam de acordo com o tamanho do data plane e os tipos de instância subjacentes.

Embora continuemos pagando a taxa horária do Control Plane para os clusters que não são de produção usados para fins de teste ou garantia de qualidade, talvez não precisemos que o data plane esteja disponível 24 horas por dia, incluindo fins de semana. Ao estabelecer uma abordagem baseada em cronograma para escalar os grupos de nós para 0 quando desnecessário, podemos otimizar significativamente os custos gerais de computação do Amazon EC2.

A economia de custos pode ir além dos custos básicos do Amazon EC2. Por exemplo, se você usar o Amazon CloudWatch Container Insights para monitoramento, você não será cobrado quando os nós ficarem inativos, já que os custos associados à ingestão de métricas são rateados por hora.

Neste post, mostraremos como você pode alcançar uma escala baseada em cronograma de 0 para seu plano de dados com o Horizontal Pod Autoscaler (HPA) e o cluster autoscaler, bem como com o karpenter.

Mecanismos atuais para escalar até zero usando HPA e Cluster Autoscaler

Vimos como o kubernetes tradicionalmente lida com o escalonamento automático para pods e nós. Também vimos como as implementações atuais do Horizontal Pod Autoscaler não conseguem lidar com a escala baseada em cronograma para 0 pods. No entanto, os recursos nativos podem ser complementados com Kubernetes CronJobs dedicados ou soluções de código aberto voltadas para a comunidade, como cron-hpa ou kube downscaler, que podem escalar os pods para 0 em horários específicos.

Além disso, precisamos ter certeza de que não apenas podemos escalar até 0, mas também podemos escalar para fora de 0. Desde a versão 1.24 do kubernetes, um novo recurso foi integrado ao Cluster Autoscaler, o que torna isso mais fácil.

Citando o anúncio oficial:

Para o Kubernetes 1.24, contribuímos com um recurso para o projeto upstream Cluster Autoscaler que simplifica a escalabilidade do grupo de nós gerenciados (MNG) do Amazon EKS de e para zero nós. Antes, era necessário marcar o EC2 Autoscaling Group (ASG) subjacente para que o Cluster Autoscaler reconhecesse os recursos, rótulos e manchas de um MNG que foi escalado para zero nós.

A partir da versão 1.24 do kubernetes, quando não há nós em execução no MNG, o cluster autoscaler chama a API DescribeNodeGroup do Amazon EKS para obter as informações necessárias sobre recursos, labels e taints do MNG. Quando o valor de uma tag do cluster autoscaler no ASG que alimenta um Amazon EKS MNG entra em conflito com o valor do próprio MNG, o cluster autoscaler prefere a tag ASG para que os clientes possam substituir os valores conforme necessário.

Graças a esse novo recurso, o cluster autoscaler, determina qual grupo de nós precisa ser escalado a partir de 0 com base na definição dos pods não programáveis, mas para que possa fazer isso, ele deve estar instalado e funcionando. Em outras palavras: não podemos escalar todos os nossos grupos de nós para 0, pois precisamos garantir que uma quantidade mínima de componentes principais esteja constantemente em funcionamento. Essa stack incluiria, no mínimo: o cluster autoscaler, o Core DNS e a ferramenta de código aberto de nossa escolha para cobrir o escalonamento baseado em agendamento de pods. Idealmente, talvez também precisemos acomodar o autoescalador proporcional de cluster (CPA) para abordar a escalabilidade do DNS principal.

Para sermos econômicos, podemos decidir criar um grupo de nós dedicado para os componentes principais, que seria apoiado por tipos de instância baratos e grupos de nós separados para cargas de trabalho de aplicativos.
Juntando tudo isso:

  1. O kube downscaler ou cron-hpa aplicam um escalonamento baseado em cronograma de ou para 0 para cargas de trabalho de aplicativos.
  2. Cluster autoscaler percebe se os nós podem ser escalados (inclusive para 0) como subutilizados ou se alguns pods não podem ser programados devido insuficiência de recursos e se os nós precisam ser expandidos (inclusive a partir de 0).
  3. Cluster autoscaler interage com a API AWS ASG (Application Programming Interface) para encerrar ou provisionar novos nós.
  4. O grupo de nós é escalado de ou para 0 conforme o esperado.

Figura 4: Escala baseada em cronograma para 0 usando um grupo de nós técnicos apoiado pelo EC2 para componentes principais.

Eventualmente, esse padrão pode ser ainda mais otimizado movendo a pilha mínima de componentes principais para o AWS Fargate. Isso significa que nenhuma instância do Amazon EC2 está em execução quando o Data Plane é desnecessário.

As implicações de custo de hospedar os componentes principais no AWS Fargate devem ser cuidadosamente avaliadas. Manter os tipos de instância mais baratos do Amazon EC2 pode resultar em uma solução menos elegante, porém mais econômica.

Figura 5: Escala baseada em cronograma para 0 usando um perfil Fargate para componentes principais.

Como isso é feito com o karpenter

Com o karpenter, temos o conceito de provisionador. Os provisionadores definem restrições nos nós que podem ser criados pelo Karpenter e nos pods que podem ser executados nesses nós. Com a versão atual do karpenter (0.28.x), há três maneiras de reduzir o número de nós para zero usando provisionadores:

  1. Exclua todos os provisionadores. A exclusão de provisionadores faz com que todos os nós sejam excluídos. Essa opção é a mais simples de implementar, mas pode não ser viável em todas as situações. Por exemplo, se você tiver vários tenants compartilhando o mesmo cluster, talvez não queira excluir todos os provisionadores, pois isso impediria que qualquer tenant executasse cargas de trabalho.
  2. Dimensione todas as cargas de trabalho para zero. O Karpenter então exclui os nós não utilizados. Essa opção é mais flexível do que excluir todos os provisionadores, mas pode não ser ideal se suas cargas de trabalho forem gerenciadas por equipes diferentes e talvez seja difícil de implementar em uma configuração do GitOps.
  3. Adicione um limite zero de CPU aos provisionadores e, em seguida, exclua todos os nós. Essa opção é a mais flexível, pois permite que você mantenha suas cargas de trabalho em execução e, ao mesmo tempo, reduza o número de nós para zero. Para fazer isso, você precisa atualizar o campo spec.limits.cpu dos seus provisionadores.

As duas primeiras opções descritas anteriormente podem ser difíceis de implementar em configurações de vários locatários ou usando estruturas GitOps. Portanto, este post se concentra na terceira opção.

Passo a passo

Considerações técnicas

A escalabilidade programática dos limites do provisionador para zero pode ser feita de várias maneiras. Um padrão comum é usar o kubernetes CronJobs. Por exemplo, o Cronjob a seguir escala os limites do provisionador para zero todos os dias úteis às 22h30:
---
apiVersion: batch/v1
kind: CronJob
metadata:
  name: scale-down-karpenter
spec:
  schedule: "30 22 * * 1-5"
  jobTemplate:
    spec:
[...]
        command:
          - /bin/sh
          - -c
          - |
            kubectl patch provisioner test-provisioner --type merge --patch '{"spec": {"limits": {"resources": {"cpu": "0"}}}}' && echo "test-provisioner patchd at $(date)";
[...]

Esse trabalho é executado todas as noites às 22h30 e escala os limites do provisionador para zero, o que efetivamente desabilita a criação de novos nós até que eles sejam redimensionados manualmente.

O CronJobs pode ser usado com o AWS Lambda para encerrar nós em execução ou implementar uma lógica mais complexa, como escalar outros componentes da infraestrutura, lidar com erros e notificações ou qualquer padrão orientado por eventos que possa ser conectado a um aplicativo ou carga de trabalho.

O AWS Step Functions pode incluir uma camada adicional de orquestração a isso, permitindo que você interaja com seu cluster usando a API kubernetes e execute jobs como parte do fluxo de trabalho do seu aplicativo. Mais informações sobre como usar as integrações da API kubernetes com o AWS Step Functions podem ser encontradas aqui.

Este é um exemplo simplificado de uma função do AWS Lambda que pode ser usada para encerrar os nós restantes do karpenter:

def lambda_handler(event, context):
   [...]
    filters = [
        {'Name': 'instance-state-name','Values': ['running']},
        {'Name': f'tag:{"karpenter.sh/provisioner-name"}', 'Values':"example123"},
        {"Name": "tag:aws:eks:cluster-name", "Values": "example123"}
    ]
    try:
        instances = ec2.instances.filter(Filters=filters)
        RunningInstances = [instance.id for instance in instances]
    except botocore.exceptions.ClientError as error:
        logging.error("Some error message here")
        raise error

    if len(RunningInstances) > 0:
        for instances in RunningInstances:
            logging.info('Found Karpenter node: {}'.format(instances))
        try:
            ec2.instances.filter(InstanceIds=RunningInstances).terminate()
        except botocore.exceptions.ClientError as error:
            logging.error("Some error message here")
            raise error
   [...]

Observação: essas etapas podem ser difíceis de orquestrar em uma configuração do GitOps. O conselho geral é criar condições específicas para os limites do provisionador. Isso é (puramente) como exemplo, como isso pode ser feito com o ArgoCD:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: karpenter
  namespace: argocd
spec:
  ignoreDifferences:
    - group: karpenter.sh
      kind: Provisioner
      jsonPointers:
        - /spec/limits/resources/cpu

Como mover componentes principais para o AWS Fargate para maior otimização

O Karpenter e o cluster autoscaler executam um controlador dentro de um pod em execução no cluster. Esse controlador precisa estar ativo e funcionando para orquestrar operações de escala para cima ou para baixo. Isso significa que pelo menos um nó deve estar em execução no cluster para hospedar esses controladores. No entanto, se você estiver interessado em cenários de escala até zero, há uma opção que deve ser levada em consideração: AWS Fargate.

O AWS Fargate é um mecanismo de computação sem servidor que permite executar contêineres sem precisar gerenciar nenhuma infraestrutura subjacente. Isso significa que você pode escalar seu aplicativo para cima e para baixo conforme necessário, sem precisar se preocupar com a falta de recursos.

Os perfis do AWS Fargate que executam o karpenter podem ser configurados via AWS Command Line Interface (AWS CLI), AWS Management Console, CDK (Cloud Development Kit), Terraform, AWS CloudFormation e eksctl. O exemplo a seguir mostra como configurar esses perfis com ekstcl:

apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig

metadata:
  name: <cluster-name>
  region: <aws-region>

fargateProfiles:
[...]
  - name: karpenter
    podExecutionRoleARN: arn:aws:iam::12345678910:role/FargatePodExecutionRole
    selectors:
    - labels:
        app.kubernetes.io/name: karpenter
      namespace: karpenter
    subnets:
    - subnet-12345
    - subnet-67890
  - name: karpenter-scaledown
    podExecutionRoleARN: arn:aws:iam::12345678910:role/FargatePodExecutionRole
    selectors:
    - labels:
        job-name: scale-down-karpenter*
      namespace: karpenter
    subnets:
    - subnet-12345
    - subnet-67890
[...]

Nota: Por padrão, o CoreDNS está configurado para ser executado na infraestrutura do Amazon EC2 em clusters do Amazon EKS. Se você quiser executar apenas seus pods no AWS Fargate em seu cluster, consulte o guia Como começar a usar o AWS Fargate usando o Amazon EKS.

Conclusões

Neste post, mostramos como escalar seus clusters do Amazon EKS para economizar dinheiro e reduzir seu impacto ambiental. Usando o cluster autoscaler e o karpenter, você pode escalar seus clusters para cima e para baixo de forma fácil e eficaz, conforme necessário. Essas ferramentas podem ajudar você a escalar seus clusters do Amazon EKS para zero nós e economizar na utilização de recursos e na pegada de carbono.

Se você quiser começar a usar o karpenter, poderá encontrar a documentação oficial aqui. A documentação inclui instruções sobre a instalação do Kubernetes e a configuração dos provisionadores e todos os outros componentes necessários para orquestrar o escalonamento automático. Este guia se concentra no Amazon EKS, mas os mesmos conceitos podem ser aplicados às soluções de kubernetes self-hosted ou gerenciadas pelo cliente.

Sobre os Autores

Giacomo Margaría
Giacomo Margaria é arquiteto sênior de DevOps na AWS. Ele trabalha com clientes de telecomunicações fornecendo orientação técnica para arquitetar e criar soluções usando os serviços de nuvem da AWS. Ele pode ser contatado em mgiaco@amazon.fr.

Marco Ballerini
Marco é consultor sênior de DevOps na Amazon Web Services, com foco em Kubernetes e entrega de recursos que ajudam os clientes a acelerar a adoção de contêineres.

Federica Ciuffo
Federica é arquiteta de soluções na Amazon Web Services. Ela é especializada em serviços de contêineres e é apaixonada por criar infraestrutura com código. Fora do escritório, ela gosta de ler, desenhar e passar tempo com as amigas, de preferência em restaurantes, experimentando novos pratos de diferentes cozinhas.

Norberto Hideaki Enomoto – Tradutor
Norberto Hideaki Enomoto atua como arquiteto de soluções corporativo na Amazon Web Services. Ele está à frente de iniciativas de Transformação Digital, utilizando tecnologias avançadas, incluindo arquitetura de microsserviços, APIs, soluções nativas em nuvem, DevSecOps e Internet das Coisas (IoT). Norberto desempenha um papel crucial no suporte a clientes corporativos, orientando-os eficientemente em suas transições e estratégias para a computação em nuvem.