Managed Node Groups no EKS: On-Demand e Spot no Mesmo Cluster com Terraform

Com managed node groups, um mesmo cluster EKS pode ter nós On-Demand para o que não pode parar e nós Spot para o que aguenta interrupção. A diferença de preço é grande: instâncias Spot costumam sair bem mais baratas que as sob demanda. O preço disso é que a AWS pode pedir a instância de volta com dois minutos de aviso.

Este é o segundo artigo da série artigos-eks. O primeiro montou a VPC, o control plane e um node group único. Aqui o cluster ganha dois node groups com papéis diferentes, e as cargas são separadas com labels, taints e tolerations.

Segurança: os comandos deste guia usam o perfil do AWS CLI para autenticar. Nunca coloque access keys no código ou em .tfvars versionado; prefira aws sso login ou um perfil com credenciais temporárias.

Sumário

Estratégia: dois managed node groups com papéis diferentes

A regra prática é simples: o que mantém o cluster de pé fica em On-Demand, e o que pode ser reiniciado sem dor vai para Spot.

  • system (ON_DEMAND): CoreDNS, controladores, bancos dentro do cluster e qualquer serviço com estado ou réplica única.
  • spot (SPOT): workers de fila, jobs em lote, ambientes de teste e APIs sem estado com várias réplicas.

O ambiente reaproveita os módulos do primeiro artigo. A VPC, o cluster e os add-ons são os mesmos; o que muda é a chamada do terraform-eks-node-group-modules duas vezes, com parâmetros diferentes. O cluster sobe na versão 1.35.

Node group On-Demand para o sistema

module "ng_system" {
  source = "../../terraform-eks-node-group-modules"

  cluster_name   = module.eks.name
  name           = "system"
  subnet_ids     = module.vpc.private_subnet_ids
  capacity_type  = "ON_DEMAND"
  instance_types = ["t3.medium"]

  min_size     = 2
  desired_size = 2
  max_size     = 3

  labels = {
    "workload" = "system"
  }
}

O mínimo de dois nós garante que o CoreDNS e os controladores tenham onde rodar mesmo durante a troca de um nó. Esse node group não tem taint: qualquer pod sem preferência cai aqui.

Os add-ons dependem só do ng_system. Assim o CoreDNS fica ACTIVE sem esperar o node group Spot, que pode até começar com zero nós.

Node group Spot com vários tipos de instância

module "ng_spot" {
  source = "../../terraform-eks-node-group-modules"

  cluster_name  = module.eks.name
  name          = "spot"
  subnet_ids    = module.vpc.private_subnet_ids
  capacity_type = "SPOT"

  instance_types = var.spot_instance_types

  min_size     = 0
  desired_size = 2
  max_size     = 6

  max_unavailable_percentage = 25

  labels = {
    "workload" = "spot"
  }

  taints = [
    {
      key    = "workload"
      value  = "spot"
      effect = "NO_SCHEDULE"
    },
  ]
}

O ponto mais importante de um node group Spot é a lista de tipos de instância. Cada tipo em cada AZ é um “pool” de capacidade, e um pool pode esgotar. Quanto mais pools, menor a chance de o node group ficar sem nó:

variable "spot_instance_types" {
  description = "Tipos de instância do node group Spot, todos com o mesmo tamanho"
  type        = list(string)
  default     = ["m5.large", "m5a.large", "m6i.large", "m6a.large", "m7i.large"]
}

Todos têm 2 vCPUs e 8 GiB de memória. Misturar tamanhos diferentes no mesmo managed node group atrapalha: o autoscaler calcula a capacidade de um nó pelo primeiro tipo da lista, e os requests dos pods deixam de caber de forma previsível.

Labels, taints e tolerations

Label e taint fazem coisas opostas, e você precisa das duas:

  • Taint no node group Spot (workload=spot:NoSchedule) afasta quem não declarou que aceita Spot. Sem ele, o scheduler coloca qualquer pod lá, inclusive os que não podem ser interrompidos.
  • Label + nodeSelector atrai o pod para o Spot. Só a toleration não basta: ela permite rodar no nó com taint, mas não obriga.

Além do label que definimos, o EKS coloca nos nós de managed node groups o label eks.amazonaws.com/capacityType com o valor ON_DEMAND ou SPOT. Ele também serve como seletor.

Um Deployment tolerante a interrupção fica assim (salve como deployment-spot.yaml):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: worker-relatorios
spec:
  replicas: 4
  selector:
    matchLabels:
      app: worker-relatorios
  template:
    metadata:
      labels:
        app: worker-relatorios
    spec:
      nodeSelector:
        workload: spot
      tolerations:
        - key: workload
          operator: Equal
          value: spot
          effect: NoSchedule
      terminationGracePeriodSeconds: 60
      containers:
        - name: worker
          image: public.ecr.aws/docker/library/busybox:stable
          command: ["sh", "-c", "while true; do echo processando; sleep 30; done"]
          resources:
            requests:
              cpu: 250m
              memory: 256Mi
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: worker-relatorios
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: worker-relatorios

Repare que no Terraform o efeito do taint é NO_SCHEDULE (o formato da API do EKS) e no manifesto é NoSchedule (o formato do Kubernetes). É a mesma coisa escrita de dois jeitos.

kubectl apply -f deployment-spot.yaml
kubectl get pods -l app=worker-relatorios -o wide
kubectl get nodes -L workload,eks.amazonaws.com/capacityType

Interrupção Spot e update_config

Quando a AWS precisa da capacidade de volta, a instância Spot recebe um aviso de interrupção com dois minutos de antecedência. Nos managed node groups, o EKS liga o Capacity Rebalancing no Auto Scaling Group: ao receber a recomendação de rebalanceamento, ele tenta subir um nó substituto e drena o nó em risco antes da interrupção.

O drenar só é suave se a aplicação colaborar:

  • Várias réplicas espalhadas por nós diferentes. Uma réplica só em Spot é indisponibilidade garantida.
  • PodDisruptionBudget, como o do exemplo, para que o drain não derrube todas as réplicas ao mesmo tempo.
  • terminationGracePeriodSeconds curto e tratamento de SIGTERM para terminar o trabalho em andamento dentro da janela.

O max_unavailable_percentage = 25 controla outra situação: updates do próprio node group, como mudar a versão do Kubernetes ou a AMI. Com 25%, o EKS troca os nós aos poucos, respeitando os PodDisruptionBudgets. O plan confirma o valor no node group Spot:

      + taint {
          + effect = "NO_SCHEDULE"
          + key    = "workload"
          + value  = "spot"
        }

      + update_config {
          + max_unavailable_percentage = 25
        }

Se você já viu PVC travado em Terminating depois de trocar nós, o post Disks pvc status is terminating eks aws mostra como destravar. É mais um motivo para deixar cargas com volume persistente no node group On-Demand.

terraform plan

terraform init
terraform validate
terraform plan -var 'public_access_cidrs=["SEU_IP/32"]' -out=tfplan

O código foi validado com terraform validate e terraform plan contra uma conta AWS real, sem apply. São 34 recursos: os 28 do primeiro artigo mais o segundo node group, com IAM role própria e as quatro políticas anexadas.

  # module.ng_system.aws_eks_node_group.this will be created
  # module.ng_spot.aws_eks_node_group.this will be created
  # module.ng_spot.aws_iam_role.node will be created
  ...
Plan: 34 to add, 0 to change, 0 to destroy.

Outputs do Terraform

O ambiente tem três outputs. Como não houve apply, os valores abaixo são os que o próprio plan mostrou no bloco Changes to Outputs:

Changes to Outputs:
  + cluster_name       = "eks-blog-castilho"
  + kubeconfig_command = "aws eks update-kubeconfig --region us-east-1 --name eks-blog-castilho"
  + node_groups        = {
      + spot   = "spot"
      + system = "system"
    }
  • cluster_name: nome do cluster EKS. Útil para scripts e para outros comandos do AWS CLI, como aws eks describe-cluster --name $(terraform output -raw cluster_name).
  • node_groups: nomes dos dois node groups. Use para consultar o estado de cada um, por exemplo aws eks describe-nodegroup --cluster-name eks-blog-castilho --nodegroup-name spot.
  • kubeconfig_command: o comando pronto para configurar o kubectl. Rode com $(terraform output -raw kubeconfig_command).

Custos e limpeza

O control plane e o NAT Gateway custam o mesmo do primeiro artigo. A diferença está nos nós: dois t3.medium sob demanda no node group do sistema e até seis instâncias Spot, cobradas pelo preço Spot do momento. Com min_size = 0, o node group Spot pode ser reduzido a zero fora do horário de uso.

kubectl delete -f deployment-spot.yaml
terraform destroy

Conclusão

Dois managed node groups resolvem a maior parte dos casos de Spot no EKS sem ferramenta extra: On-Demand para o que sustenta o cluster, Spot com vários tipos de instância para o resto, e taint mais label para que cada pod caia no lugar certo. O restante é disciplina na aplicação: réplicas, PodDisruptionBudget e encerramento limpo.

Quando a lista de tipos e tamanhos começar a crescer demais, o próximo passo natural é deixar que o Karpenter escolha as instâncias. Esse tema tem um artigo próprio mais adiante na série.

Referências oficiais

Interessado em saber mais sobre esse assunto?

Deixe uma resposta

Descubra mais sobre Blog do Castilho - Tecnologia | FinOps | DevOps | Cloud

Assine agora mesmo para continuar lendo e ter acesso ao arquivo completo.

Continuar lendo