13 de março de 2026 • 24 min de leitura
Revolucionando Auto Scaling no EKS com Karpenter
O Cluster Autoscaler serviu bem por anos, mas tem limitações fundamentais que impactam custo e performance. O Karpenter muda o jogo ao provisionar nodes sob demanda em segundos, escolher automaticamente os melhores tipos de instância, e consolidar recursos de forma inteligente. Entenda como funciona, quando migrar, e as práticas essenciais para extrair o máximo dessa ferramenta.
Escalar nodes no Kubernetes sempre foi um desafio. O Cluster Autoscaler resolve o problema básico, mas com limitações: você precisa pré-definir tipos de instância, esperar minutos para provisionar nodes, e conviver com desperdício de recursos. Em ambientes dinâmicos com workloads variados, essas limitações custam caro.
O Karpenter muda essa equação. Desenvolvido pela AWS e doado à CNCF, ele provisiona nodes sob demanda em ~60 segundos, escolhe automaticamente os melhores tipos de instância para cada workload, e consolida recursos continuamente para eliminar desperdício. O resultado: redução de 30-60% nos custos de compute e melhor utilização de recursos.
Neste artigo, você vai aprender:
- Como o Karpenter funciona internamente
- Diferenças fundamentais entre Karpenter e Cluster Autoscaler
- O que são EC2NodeClass e NodePool
- Boas práticas para configurar NodePools
- Como preparar workloads para Karpenter
- Estratégias de consolidação e interrupção
- Uso eficiente de Spot instances
- Multi-AZ e alta disponibilidade
No final, você terá o conhecimento necessário para implementar Karpenter com confiança e otimizar seus custos de EKS significativamente.
O problema com o Cluster Autoscaler
Antes de entender o Karpenter, é importante reconhecer as limitações do Cluster Autoscaler:
Limitação 1: Node Groups pré-definidos
O Cluster Autoscaler só pode escalar node groups que você criou previamente:
# Você precisa criar node groups específicos
NodeGroup 1: t3.medium (2 vCPU, 4 GB RAM)
NodeGroup 2: m5.large (2 vCPU, 8 GB RAM)
NodeGroup 3: c5.xlarge (4 vCPU, 8 GB RAM)Problema: Se um pod precisa de 3 vCPU, nenhum desses node groups é ideal. Você vai desperdiçar recursos ou o pod ficará pending.
Limitação 2: Escala lenta
Pod fica pending
↓
Cluster Autoscaler detecta (30-60s)
↓
Solicita nova instância ao ASG
↓
EC2 provisiona instância (2-5 min)
↓
Node se registra no cluster (30s)
↓
Pod é agendado
↓
Total: 3-6 minutosLimitação 3: Bin-packing ineficiente
O Cluster Autoscaler não reorganiza pods para consolidar nodes. Resultado:
Node 1: 30% utilizado
Node 2: 25% utilizado
Node 3: 40% utilizado
Você paga por 3 nodes, mas poderia usar apenas 1Limitação 4: Spot instances complexas
Usar Spot com Cluster Autoscaler exige:
- Criar node groups separados para cada tipo de instância
- Gerenciar interrupções manualmente
- Configurar diversificação de instâncias
Como o Karpenter funciona
O Karpenter inverte a lógica: ao invés de escalar node groups pré-definidos, ele provisiona nodes sob demanda baseado nas necessidades dos pods.
Arquitetura do Karpenter
Fluxo de provisionamento
1. Pod fica pending:
apiVersion: v1
kind: Pod
metadata:
name: app-pod
spec:
containers:
- name: app
resources:
requests:
cpu: 2000m
memory: 4Gi
nodeSelector:
workload-type: compute-intensive2. Karpenter analisa requirements:
Requirements detectados:
- CPU: 2000m
- Memory: 4Gi
- Node selector: workload-type=compute-intensive
- Topology: nenhuma restrição
- Taints/Tolerations: nenhum3. Karpenter consulta NodePool:
# NodePool define regras de provisionamento
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
name: default
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot", "on-demand"]
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["c", "m", "r"]4. Karpenter escolhe melhor instância:
Candidatos:
- c5.xlarge: 4 vCPU, 8 GB → Atende (compute-optimized)
- m5.large: 2 vCPU, 8 GB → CPU insuficiente
- c5.large: 2 vCPU, 4 GB → Atende (mais barato)
- c5.2xlarge: 8 vCPU, 16 GB → Atende (oversized)
Escolhido: c5.large (menor custo que atende)5. Provisiona em ~60 segundos:
Karpenter → EC2 API: Lançar c5.large spot
↓
EC2 provisiona instância
↓
Instância inicia com user-data do Karpenter
↓
Kubelet se registra no cluster
↓
Node fica Ready
↓
Pod é agendado
↓
Total: ~60-90 segundosKarpenter vs Cluster Autoscaler
Comparação detalhada
| Aspecto | Cluster Autoscaler | Karpenter |
|---|---|---|
| Provisionamento | Escala node groups pré-definidos | Provisiona nodes sob demanda |
| Velocidade | 3-6 minutos | 60-90 segundos |
| Escolha de instância | Limitado aos tipos do node group | Escolhe automaticamente entre centenas |
| Bin-packing | Básico | Avançado com simulação |
| Consolidação | Manual (scale down) | Automática e contínua |
| Spot instances | Complexo (múltiplos node groups) | Nativo e simples |
| Diversificação | Manual | Automática |
| Overhead | Alto (múltiplos ASGs) | Baixo (direto via EC2 API) |
| Custo | Maior (desperdício) | 30-60% menor |
| Configuração | Node groups + ASG | NodePool + EC2NodeClass |
Ganhos reais com Karpenter
1. Redução de custos (30-60%):
Antes (Cluster Autoscaler):
- 10 nodes m5.xlarge on-demand
- Utilização média: 40%
- Custo: $1,536/mês
Depois (Karpenter):
- 4 nodes diversos (m5.large, c5.xlarge, r5.large)
- 80% spot instances
- Utilização média: 75%
- Custo: $620/mês
Economia: 60% ($916/mês)2. Provisionamento mais rápido:
Cluster Autoscaler: 3-6 minutos
Karpenter: 60-90 segundos
Ganho: 3-5x mais rápido3. Melhor utilização de recursos:
Cluster Autoscaler:
- Node 1: 30% CPU, 45% RAM
- Node 2: 25% CPU, 35% RAM
- Node 3: 40% CPU, 50% RAM
Média: 32% CPU, 43% RAM
Karpenter (com consolidação):
- Node 1: 70% CPU, 75% RAM
- Node 2: 65% CPU, 80% RAM
Média: 68% CPU, 78% RAM
Ganho: 2x melhor utilização4. Spot instances simplificadas:
Cluster Autoscaler:
- Criar node group para cada tipo
- Gerenciar interrupções manualmente
- Configurar diversificação
Karpenter:
- capacity-type: ["spot", "on-demand"]
- Diversificação automática
- Fallback automático para on-demandEC2NodeClass: Configuração de infraestrutura
O EC2NodeClass define como os nodes serão configurados na AWS. É o equivalente ao Launch Template do ASG, mas gerenciado pelo Karpenter.
Anatomia do EC2NodeClass
apiVersion: karpenter.k8s.aws/v1beta1
kind: EC2NodeClass
metadata:
name: default
spec:
# AMI a ser usada
amiFamily: AL2
# IAM Role para os nodes
role: KarpenterNodeRole
# Subnets onde provisionar (tags)
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: my-cluster
# Security Groups
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: my-cluster
# User data customizado (opcional)
userData: |
#!/bin/bash
echo "Custom setup"
# Block device mappings
blockDeviceMappings:
- deviceName: /dev/xvda
ebs:
volumeSize: 100Gi
volumeType: gp3
iops: 3000
throughput: 125
deleteOnTermination: true
# Metadata options
metadataOptions:
httpEndpoint: enabled
httpProtocolIPv6: disabled
httpPutResponseHopLimit: 2
httpTokens: required
# Tags para instâncias EC2
tags:
Environment: production
ManagedBy: karpenterCampos importantes do EC2NodeClass
1. amiFamily:
Define qual AMI usar. Opções:
amiFamily: AL2 # Amazon Linux 2 (padrão)
amiFamily: AL2023 # Amazon Linux 2023
amiFamily: Bottlerocket # Bottlerocket OS
amiFamily: Ubuntu # UbuntuRecomendação: Use AL2023 para novos clusters (suporte estendido, melhor performance).
2. subnetSelectorTerms:
Seleciona subnets via tags:
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: my-cluster
Environment: productionBoa prática: Use tags específicas para controlar onde nodes são provisionados.
3. securityGroupSelectorTerms:
Seleciona security groups:
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: my-cluster
- id: sg-0123456789abcdefBoa prática: Use o security group criado pelo EKS + security groups adicionais se necessário.
4. blockDeviceMappings:
Configura discos:
blockDeviceMappings:
- deviceName: /dev/xvda
ebs:
volumeSize: 100Gi # Tamanho do disco
volumeType: gp3 # Tipo (gp3 é mais barato que gp2)
iops: 3000 # IOPS (gp3 permite customizar)
throughput: 125 # Throughput MB/s
encrypted: true # Criptografia
deleteOnTermination: trueBoa prática: Use gp3 (mais barato e performático que gp2). Para workloads I/O intensivos, aumente IOPS.
5. metadataOptions:
Configurações de segurança do IMDS:
metadataOptions:
httpEndpoint: enabled
httpTokens: required # IMDSv2 obrigatório (segurança)
httpPutResponseHopLimit: 2Boa prática: Sempre use httpTokens: required (IMDSv2) para segurança.
Exemplo completo de EC2NodeClass
apiVersion: karpenter.k8s.aws/v1beta1
kind: EC2NodeClass
metadata:
name: production
spec:
amiFamily: AL2023
role: KarpenterNodeRole-production
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: prod-cluster
tier: private
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: prod-cluster
blockDeviceMappings:
- deviceName: /dev/xvda
ebs:
volumeSize: 100Gi
volumeType: gp3
iops: 3000
throughput: 125
encrypted: true
deleteOnTermination: true
metadataOptions:
httpEndpoint: enabled
httpTokens: required
httpPutResponseHopLimit: 2
tags:
Environment: production
ManagedBy: karpenter
CostCenter: engineeringNodePool: Regras de provisionamento
O NodePool define quais tipos de instâncias o Karpenter pode provisionar e como gerenciar o ciclo de vida dos nodes.
Anatomia do NodePool
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
name: default
spec:
# Template para nodes
template:
spec:
# Requirements: restrições de instâncias
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot", "on-demand"]
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["c", "m", "r"]
- key: karpenter.k8s.aws/instance-generation
operator: Gt
values: ["4"]
# NodeClass a usar
nodeClassRef:
name: default
# Taints (opcional)
taints:
- key: workload-type
value: batch
effect: NoSchedule
# Limites de recursos
limits:
cpu: 1000
memory: 1000Gi
# Políticas de disrupção
disruption:
consolidationPolicy: WhenUnderutilized
expireAfter: 720h # 30 diasRequirements: Controlando tipos de instância
Requirements definem quais instâncias o Karpenter pode escolher:
1. Capacity Type (Spot vs On-Demand):
# Apenas Spot (máxima economia)
- key: karpenter.sh/capacity-type
operator: In
values: ["spot"]
# Spot com fallback para On-Demand (recomendado)
- key: karpenter.sh/capacity-type
operator: In
values: ["spot", "on-demand"]
# Apenas On-Demand (workloads críticos)
- key: karpenter.sh/capacity-type
operator: In
values: ["on-demand"]Boa prática: Use ["spot", "on-demand"] para workloads stateless. Karpenter tenta Spot primeiro e faz fallback para On-Demand se necessário.
2. Instance Category (Família de instâncias):
# Compute-optimized (c5, c6i, c7g)
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["c"]
# General purpose (m5, m6i, m7g)
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["m"]
# Memory-optimized (r5, r6i, r7g)
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["r"]
# Múltiplas categorias (recomendado para diversificação)
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["c", "m", "r"]Boa prática: Permita múltiplas categorias para aumentar diversificação de Spot e reduzir interrupções.
3. Instance Generation (Geração):
# Apenas gerações 5 ou superior
- key: karpenter.k8s.aws/instance-generation
operator: Gt
values: ["4"]
# Apenas geração 6
- key: karpenter.k8s.aws/instance-generation
operator: In
values: ["6"]Boa prática: Use Gt: ["4"] para permitir gerações modernas (melhor custo-benefício).
4. Instance Size (Tamanho):
# Apenas small e medium
- key: karpenter.k8s.aws/instance-size
operator: In
values: ["small", "medium", "large"]
# Excluir tamanhos muito grandes
- key: karpenter.k8s.aws/instance-size
operator: NotIn
values: ["8xlarge", "12xlarge", "16xlarge"]Boa prática: Evite instâncias muito grandes (blast radius menor, mais flexibilidade).
5. Architecture (Arquitetura):
# Apenas AMD64
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
# AMD64 e ARM64 (Graviton - mais barato)
- key: kubernetes.io/arch
operator: In
values: ["amd64", "arm64"]Boa prática: Se suas imagens suportam ARM64, inclua para reduzir custos (~20% mais barato).
Limits: Controlando crescimento
Limits evitam que o Karpenter provisione recursos ilimitados:
limits:
cpu: 1000 # Máximo de 1000 vCPUs
memory: 1000Gi # Máximo de 1000 GB RAMBoa prática: Sempre defina limits para evitar custos inesperados.
Cálculo de limits:
Workload esperado: 50 pods
Cada pod: 2 vCPU, 4 GB RAM
Total: 100 vCPU, 200 GB RAM
Limite recomendado (2x para burst):
cpu: 200
memory: 400GiDisruption: Consolidação e expiração
Disruption controla quando o Karpenter pode remover ou substituir nodes:
1. consolidationPolicy:
# Consolidar quando nodes estão subutilizados
disruption:
consolidationPolicy: WhenUnderutilized
# Nunca consolidar automaticamente
disruption:
consolidationPolicy: WhenEmptyComo funciona WhenUnderutilized:
Cenário:
- Node 1: 20% CPU, 30% RAM
- Node 2: 25% CPU, 35% RAM
- Node 3: 30% CPU, 40% RAM
Karpenter detecta:
"Posso mover todos os pods para 2 nodes"
Ação:
1. Cordona Node 3
2. Drena pods para Node 1 e 2
3. Termina Node 3
Resultado:
- Node 1: 45% CPU, 60% RAM
- Node 2: 50% CPU, 65% RAM
- Economia: 1 nodeBoa prática: Use WhenUnderutilized para maximizar economia. Karpenter respeita PDBs durante consolidação.
2. expireAfter:
# Nodes expiram após 30 dias
disruption:
expireAfter: 720h
# Nodes expiram após 7 dias
disruption:
expireAfter: 168h
# Nunca expirar
disruption:
expireAfter: NeverPor que expirar nodes:
- Atualizar AMI automaticamente
- Aplicar patches de segurança
- Renovar Spot instances (reduz interrupções)
Boa prática: Use 720h (30 dias) para produção. Garante nodes atualizados sem disrupção frequente.
Exemplo completo de NodePool
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
name: general-purpose
spec:
template:
spec:
requirements:
# Spot com fallback para On-Demand
- key: karpenter.sh/capacity-type
operator: In
values: ["spot", "on-demand"]
# AMD64 e ARM64 (Graviton)
- key: kubernetes.io/arch
operator: In
values: ["amd64", "arm64"]
# Categorias: compute, general, memory
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["c", "m", "r"]
# Gerações modernas (5+)
- key: karpenter.k8s.aws/instance-generation
operator: Gt
values: ["4"]
# Tamanhos: small até 2xlarge
- key: karpenter.k8s.aws/instance-size
operator: In
values: ["small", "medium", "large", "xlarge", "2xlarge"]
nodeClassRef:
name: default
limits:
cpu: 500
memory: 1000Gi
disruption:
consolidationPolicy: WhenUnderutilized
expireAfter: 720hNodePools especializados
NodePool para workloads batch:
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
name: batch
spec:
template:
spec:
requirements:
# Apenas Spot (máxima economia)
- key: karpenter.sh/capacity-type
operator: In
values: ["spot"]
# Compute-optimized
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["c"]
# Taints para isolar workloads
taints:
- key: workload-type
value: batch
effect: NoSchedule
nodeClassRef:
name: default
limits:
cpu: 200
disruption:
consolidationPolicy: WhenUnderutilized
expireAfter: 168h # 7 dias (batch pode tolerar mais disrupção)NodePool para workloads críticos:
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
name: critical
spec:
template:
spec:
requirements:
# Apenas On-Demand (zero interrupções)
- key: karpenter.sh/capacity-type
operator: In
values: ["on-demand"]
# General purpose
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["m"]
# Apenas AMD64 (máxima compatibilidade)
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
# Taints para garantir isolamento
taints:
- key: workload-type
value: critical
effect: NoSchedule
nodeClassRef:
name: default
limits:
cpu: 100
memory: 200Gi
disruption:
consolidationPolicy: WhenEmpty # Apenas consolida nodes vazios
expireAfter: Never # Nunca expira automaticamentePreparando workloads para Karpenter
O Karpenter funciona melhor quando seus workloads seguem boas práticas do Kubernetes.
1. Resource Requests e Limits
Por que é crítico:
O Karpenter usa requests para decidir qual instância provisionar. Sem requests, o Karpenter não sabe o tamanho necessário.
Problema sem requests:
# Sem requests
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
replicas: 10
template:
spec:
containers:
- name: app
image: myapp:latest
# Sem resources!Resultado: Karpenter provisiona instâncias pequenas (não sabe que precisa de mais recursos). Pods ficam com OOMKilled ou throttled.
Solução com requests:
# Com requests e limits
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
replicas: 10
template:
spec:
containers:
- name: app
image: myapp:latest
resources:
requests:
cpu: 500m # Karpenter usa isso
memory: 1Gi # Karpenter usa isso
limits:
cpu: 1000m # Kubernetes usa isso
memory: 2Gi # Kubernetes usa issoComo definir requests corretos:
1. Monitorar uso real
kubectl top pods -n production2. Analisar histórico (Prometheus)
Queries úteis:
- container_cpu_usage_seconds_total
- container_memory_working_set_bytes
3. Definir requests = P95 do uso
- Se P95 CPU = 400m, use request: 500m
- Se P95 Memory = 800Mi, use request: 1Gi
Boa prática:
resources:
requests:
cpu: P95 + 20% # Margem de segurança
memory: P95 + 20%
limits:
cpu: 2x requests # Permite burst
memory: 1.5x requests # Evita OOM2. Pod Disruption Budgets (PDB)
Por que é crítico:
Durante consolidação ou expiração de nodes, o Karpenter drena pods. PDBs garantem que você não perca disponibilidade.
Problema sem PDB:
# Sem PDB
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 3
# Sem PDB!Cenário:
Karpenter consolida nodes:
- Drena Node 1 (2 pods da API)
- Drena Node 2 (1 pod da API)
- Todos os 3 pods terminam ao mesmo tempo
- API fica indisponível por 30-60s
Downtime!
Solução com PDB:
# Com PDB
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api-pdb
spec:
minAvailable: 2 # Sempre manter 2 pods rodando
selector:
matchLabels:
app: apiCenário com PDB:
Karpenter consolida nodes:
- Tenta drenar Node 1 (2 pods)
- PDB permite drenar apenas 1 pod (mantém minAvailable: 2)
- Aguarda novo pod ficar Ready
- Drena segundo pod
- Aguarda novo pod ficar Ready
- Drena terceiro pod
Zero downtime!
Estratégias de PDB:
minAvailable (recomendado para alta disponibilidade):
# Sempre manter 2 pods
minAvailable: 2
# Sempre manter 80% dos pods
minAvailable: 80%maxUnavailable (recomendado para rolling updates):
# Permitir 1 pod indisponível
maxUnavailable: 1
# Permitir 25% dos pods indisponíveis
maxUnavailable: 25%Boa prática:
# Para APIs críticas
minAvailable: N-1 # Se replicas=3, minAvailable=2
# Para workloads batch
maxUnavailable: 50% # Pode tolerar mais disrupção
# Para single replica (não recomendado, mas se necessário)
maxUnavailable: 0 # Bloqueia consolidaçãoExemplo completo:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 5
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: api:latest
resources:
requests:
cpu: 500m
memory: 1Gi
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api-pdb
spec:
minAvailable: 3 # Sempre 3 de 5 rodando
selector:
matchLabels:
app: api3. Pod Affinity e Anti-Affinity
Por que é importante:
Affinity controla como pods são distribuídos entre nodes. Isso impacta disponibilidade e custo.
Anti-Affinity: Distribuir pods
# Distribuir pods entre nodes diferentes
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 3
template:
metadata:
labels:
app: api
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- api
topologyKey: kubernetes.io/hostname
containers:
- name: api
image: api:latestResultado:
Node 1: api-pod-1 Node 2: api-pod-2 Node 3: api-pod-3
Alta disponibilidade (falha de 1 node não derruba tudo)
Sem anti-affinity:
Node 1: api-pod-1, api-pod-2, api-pod-3 Node 2: (vazio) Node 3: (vazio)
Falha de Node 1 = downtime total
Anti-Affinity preferencial (mais flexível):
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- api
topologyKey: kubernetes.io/hostnameDiferença:
required: Obrigatório (pod fica pending se não conseguir)preferred: Preferencial (tenta, mas não bloqueia)
Boa prática: Use preferred para evitar pods pending. Use required apenas para workloads críticos.
Affinity: Co-localizar pods
# Co-localizar cache com API (reduz latência)
apiVersion: apps/v1
kind: Deployment
metadata:
name: cache
spec:
template:
spec:
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- api
topologyKey: kubernetes.io/hostname
containers:
- name: redis
image: redis:latestResultado:
Node 1: api-pod-1, cache-pod-1 Node 2: api-pod-2, cache-pod-2
Latência mínima entre API e cache
4. Topology Spread Constraints
Por que é importante:
Distribui pods entre zonas de disponibilidade (AZs) para alta disponibilidade.
Problema sem topology spread:
AZ us-east-1a: api-pod-1, api-pod-2, api-pod-3
AZ us-east-1b: (vazio)
AZ us-east-1c: (vazio)
Falha de AZ = downtime totalSolução com topology spread:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 6
template:
metadata:
labels:
app: api
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: api
containers:
- name: api
image: api:latestResultado:
AZ us-east-1a: api-pod-1, api-pod-2
AZ us-east-1b: api-pod-3, api-pod-4
AZ us-east-1c: api-pod-5, api-pod-6
Distribuição uniforme entre AZsParâmetros:
-
maxSkew: 1: Diferença máxima de pods entre AZs -
topologyKey: Chave para agrupar (zone, hostname) -
whenUnsatisfiable: O que fazer se não conseguir distribuirDoNotSchedule: Pod fica pendingScheduleAnyway: Agenda mesmo sem distribuição ideal
Boa prática:
# Para produção (alta disponibilidade)
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: api
# Para desenvolvimento (flexibilidade)
topologySpreadConstraints:
- maxSkew: 2
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: apiCombinando com anti-affinity:
# Distribuir entre AZs E entre nodes
spec:
topologySpreadConstraints:
# Distribuir entre AZs
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: api
# Distribuir entre nodes
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: api5. Multi-AZ: Garantindo alta disponibilidade
Por que é crítico:
Falhas de AZ acontecem. Seus workloads precisam sobreviver a elas.
Configuração do Karpenter para Multi-AZ:
# EC2NodeClass: Subnets em múltiplas AZs
apiVersion: karpenter.k8s.aws/v1beta1
kind: EC2NodeClass
metadata:
name: default
spec:
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: my-cluster
# Subnets em us-east-1a, us-east-1b, us-east-1cKarpenter distribui nodes automaticamente:
Cenário: 10 pods pending
Karpenter provisiona:
- 3 nodes em us-east-1a
- 4 nodes em us-east-1b
- 3 nodes em us-east-1c
Distribuição automática entre AZsForçar distribuição de pods:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 9
template:
spec:
# Distribuir entre AZs
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: api
# Distribuir entre nodes
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- api
topologyKey: kubernetes.io/hostnameResultado:
us-east-1a:
Node 1: api-pod-1
Node 2: api-pod-2
Node 3: api-pod-3
us-east-1b:
Node 4: api-pod-4
Node 5: api-pod-5
Node 6: api-pod-6
us-east-1c:
Node 7: api-pod-7
Node 8: api-pod-8
Node 9: api-pod-9
Falha de 1 AZ = 66% de capacidade mantidaTestando resiliência a falha de AZ:
# 1. Simular falha de AZ (cordon nodes)
kubectl cordon -l topology.kubernetes.io/zone=us-east-1a
# 2. Verificar distribuição de pods
kubectl get pods -o wide | grep api
# 3. Verificar se aplicação continua funcionando
curl https://api.example.com/health
# 4. Remover simulação
kubectl uncordon -l topology.kubernetes.io/zone=us-east-1aBoa prática para Multi-AZ:
- Sempre use 3 AZs (mínimo para quorum)
- Replicas múltiplas de 3 (3, 6, 9, 12) para distribuição uniforme
- Use topology spread constraints com
maxSkew: 1 - Configure PDBs para manter disponibilidade durante falhas
- Teste falhas de AZ regularmente (chaos engineering)
Estratégias avançadas com Karpenter
1. Spot Instances: Maximizando economia
Por que usar Spot:
- 70-90% mais barato que On-Demand
- Ideal para workloads stateless
- Karpenter gerencia interrupções automaticamente
Configuração básica:
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
name: spot-optimized
spec:
template:
spec:
requirements:
# Spot com fallback
- key: karpenter.sh/capacity-type
operator: In
values: ["spot", "on-demand"]
# Múltiplas categorias (diversificação)
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["c", "m", "r"]
# Múltiplas gerações
- key: karpenter.k8s.aws/instance-generation
operator: In
values: ["5", "6", "7"]
nodeClassRef:
name: default
disruption:
consolidationPolicy: WhenUnderutilized
expireAfter: 168h # Renovar semanalmenteDiversificação automática:
Karpenter escolhe entre dezenas de tipos de instância Spot:
Candidatos Spot disponíveis:
- c5.large, c5.xlarge, c5.2xlarge
- c6i.large, c6i.xlarge, c6i.2xlarge
- m5.large, m5.xlarge, m5.2xlarge
- m6i.large, m6i.xlarge, m6i.2xlarge
- r5.large, r5.xlarge
- r6i.large, r6i.xlarge
Karpenter escolhe: c5.xlarge (menor preço no momento)Tratamento de interrupções:
Spot instance recebe aviso de interrupção (2 min):
↓
Karpenter detecta via AWS API
↓
Cordona node imediatamente
↓
Drena pods (respeitando PDBs)
↓
Provisiona novo node (Spot ou On-Demand)
↓
Pods são reagendados
↓
Total downtime: ~30-60s (com PDBs corretos)Boa prática para Spot:
# Workloads que toleram Spot
- APIs stateless (com múltiplas réplicas)
- Workers de fila
- Batch jobs
- Ambientes de desenvolvimento
# Workloads que NÃO devem usar Spot
- Bancos de dados
- Caches (Redis, Memcached)
- Workloads stateful
- Single replica críticosExemplo: 80% Spot, 20% On-Demand:
# NodePool para workloads gerais (Spot)
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
name: general-spot
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot"]
nodeClassRef:
name: default
limits:
cpu: 400
weight: 80 # 80% do peso
---
# NodePool para workloads críticos (On-Demand)
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
name: critical-ondemand
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["on-demand"]
taints:
- key: workload-type
value: critical
effect: NoSchedule
nodeClassRef:
name: default
limits:
cpu: 100
weight: 20 # 20% do peso2. Consolidação inteligente
Como funciona:
Karpenter monitora continuamente a utilização dos nodes e consolida quando possível.
Cenário de consolidação:
Estado inicial:
Node 1 (m5.xlarge): 30% CPU, 40% RAM
Node 2 (m5.xlarge): 25% CPU, 35% RAM
Node 3 (m5.xlarge): 20% CPU, 30% RAM
Karpenter analisa:
"Posso mover todos os pods para 2 nodes m5.xlarge"
Ação:
1. Provisiona Node 4 (m5.xlarge)
2. Move pods de Node 3 para Node 4
3. Termina Node 3
4. Aguarda 15 minutos
5. Consolida Node 1 e Node 2 em Node 5 (m5.2xlarge)
6. Termina Node 1 e Node 2
Estado final:
Node 4 (m5.xlarge): 50% CPU, 60% RAM
Node 5 (m5.2xlarge): 45% CPU, 55% RAM
Economia: 1 node (33%)Configuração de consolidação:
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
name: default
spec:
disruption:
# Consolidar quando subutilizado
consolidationPolicy: WhenUnderutilized
# Tempo mínimo antes de consolidar
consolidateAfter: 30sConsolidação respeitando PDBs:
Cenário:
- Node 1: api-pod-1, api-pod-2 (PDB: minAvailable=2)
- Node 2: api-pod-3
Karpenter tenta consolidar:
1. Tenta mover api-pod-1 de Node 1
2. PDB permite (ainda tem 2 pods: api-pod-2, api-pod-3)
3. Move api-pod-1 para Node 2
4. Tenta mover api-pod-2 de Node 1
5. PDB bloqueia (só restaria 1 pod: api-pod-3)
6. Karpenter aguarda
PDB respeitado, zero downtimeMonitorando consolidação:
# Ver eventos de consolidação
kubectl logs -n karpenter -l app.kubernetes.io/name=karpenter | grep consolidation
# Exemplo de log:
# "consolidation delete, terminating 1 nodes"
# "launched node with 1 pods requesting"Boa prática:
- Use
consolidationPolicy: WhenUnderutilizedpara maximizar economia - Configure PDBs em todos os deployments críticos
- Monitore eventos de consolidação
- Ajuste
consolidateAfterse houver muita volatilidade (padrão: 30s)
3. Drift: Atualizações automáticas
O que é drift:
Drift detecta quando nodes estão desatualizados (AMI antiga, configuração mudou) e os substitui automaticamente.
Cenário de drift:
Situação:
- Nodes rodando com AMI v1.28.0
- Você atualiza EC2NodeClass para AMI v1.29.0
Karpenter detecta drift:
1. Marca nodes como "drifted"
2. Provisiona novos nodes com AMI v1.29.0
3. Drena pods dos nodes antigos
4. Termina nodes antigos
Resultado: Cluster atualizado sem intervenção manualConfiguração:
apiVersion: karpenter.k8s.aws/v1beta1
kind: EC2NodeClass
metadata:
name: default
spec:
amiFamily: AL2023
# Quando você muda amiFamily ou amiSelectorTerms,
# Karpenter detecta drift automaticamenteForçar drift manualmente:
# Adicionar annotation para forçar drift
kubectl annotate nodepool default karpenter.sh/do-not-disrupt=false
# Karpenter vai substituir todos os nodes desse NodePoolBoa prática:
- Deixe drift habilitado (padrão)
- Use
expireAfterpara renovação periódica - Teste mudanças em staging primeiro
- Monitore eventos de drift
Instalando e configurando Karpenter
Pré-requisitos
- Cluster EKS (versão 1.23+)
- IAM roles configuradas
- VPC com subnets taggeadas
- Security groups taggeados
Instalação via Helm
# 1. Adicionar repositório Helm
helm repo add karpenter https://charts.karpenter.sh
helm repo update
# 2. Criar namespace
kubectl create namespace karpenter
# 3. Instalar Karpenter
helm install karpenter karpenter/karpenter \
--namespace karpenter \
--set settings.clusterName=my-cluster \
--set settings.clusterEndpoint=$(aws eks describe-cluster --name my-cluster --query "cluster.endpoint" --output text) \
--set serviceAccount.annotations."eks\.amazonaws\.com/role-arn"=arn:aws:iam::123456789012:role/KarpenterControllerRole \
--set controller.resources.requests.cpu=1 \
--set controller.resources.requests.memory=1Gi \
--set controller.resources.limits.cpu=1 \
--set controller.resources.limits.memory=1Gi
# 4. Verificar instalação
kubectl get pods -n karpenter
kubectl logs -n karpenter -l app.kubernetes.io/name=karpenterConfiguração inicial
1. Taggear subnets:
# Taggear subnets privadas
aws ec2 create-tags \
--resources subnet-abc123 subnet-def456 \
--tags Key=karpenter.sh/discovery,Value=my-cluster2. Taggear security groups:
# Taggear security group do cluster
aws ec2 create-tags \
--resources sg-abc123 \
--tags Key=karpenter.sh/discovery,Value=my-cluster3. Criar EC2NodeClass:
apiVersion: karpenter.k8s.aws/v1beta1
kind: EC2NodeClass
metadata:
name: default
spec:
amiFamily: AL2023
role: KarpenterNodeRole
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: my-cluster
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: my-cluster
blockDeviceMappings:
- deviceName: /dev/xvda
ebs:
volumeSize: 100Gi
volumeType: gp3
deleteOnTermination: true4. Criar NodePool:
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
name: default
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot", "on-demand"]
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["c", "m", "r"]
- key: karpenter.k8s.aws/instance-generation
operator: Gt
values: ["4"]
nodeClassRef:
name: default
limits:
cpu: 1000
memory: 1000Gi
disruption:
consolidationPolicy: WhenUnderutilized
expireAfter: 720h5. Aplicar configurações:
kubectl apply -f ec2nodeclass.yaml
kubectl apply -f nodepool.yaml6. Testar provisionamento:
# Criar deployment de teste
kubectl create deployment test --image=nginx --replicas=10
# Verificar nodes sendo provisionados
kubectl get nodes -w
# Ver logs do Karpenter
kubectl logs -n karpenter -l app.kubernetes.io/name=karpenter -fMigrando do Cluster Autoscaler para Karpenter
Estratégia de migração
Fase 1: Preparação (1-2 semanas)
- Auditar workloads:
# Verificar pods sem resource requests
kubectl get pods -A -o json | \
jq -r '.items[] | select(.spec.containers[].resources.requests == null) | .metadata.name'
# Verificar deployments sem PDBs
kubectl get deployments -A -o json | \
jq -r '.items[] | select(.spec.replicas > 1) | .metadata.name'- Adicionar resource requests:
# Antes
containers:
- name: app
image: app:latest
# Depois
containers:
- name: app
image: app:latest
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: 1000m
memory: 2Gi- Criar PDBs:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: app-pdb
spec:
minAvailable: 2
selector:
matchLabels:
app: appFase 2: Instalação (1 dia)
- Instalar Karpenter (mantém Cluster Autoscaler)
- Criar EC2NodeClass e NodePool
- Testar em namespace isolado
Fase 3: Migração gradual (1-2 semanas)
- Criar NodePool com taint:
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
name: migration
spec:
template:
spec:
taints:
- key: karpenter.sh/migration
value: "true"
effect: NoSchedule
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot", "on-demand"]
nodeClassRef:
name: default- Migrar workloads por namespace:
# Adicionar toleration aos deployments
spec:
template:
spec:
tolerations:
- key: karpenter.sh/migration
operator: Equal
value: "true"
effect: NoSchedule- Drenar nodes antigos:
# Cordon nodes do Cluster Autoscaler
kubectl cordon -l eks.amazonaws.com/nodegroup=old-nodegroup
# Drenar gradualmente
kubectl drain node-1 --ignore-daemonsets --delete-emptydir-dataFase 4: Finalização (1 dia)
- Remover taints do NodePool
- Deletar node groups antigos
- Desinstalar Cluster Autoscaler
# Deletar node groups
aws eks delete-nodegroup \
--cluster-name my-cluster \
--nodegroup-name old-nodegroup
# Desinstalar Cluster Autoscaler
kubectl delete deployment cluster-autoscaler -n kube-systemMonitoramento e troubleshooting
Métricas importantes
1. Provisionamento de nodes:
# Ver eventos de provisionamento
kubectl logs -n karpenter -l app.kubernetes.io/name=karpenter | grep "launched node"
# Métricas Prometheus
karpenter_nodes_created_total
karpenter_nodes_terminated_total
karpenter_provisioner_scheduling_duration_seconds2. Consolidação:
# Ver eventos de consolidação
kubectl logs -n karpenter -l app.kubernetes.io/name=karpenter | grep consolidation
# Métricas
karpenter_deprovisioning_actions_performed_total
karpenter_deprovisioning_replacement_node_initialized_seconds3. Utilização de recursos:
# Ver utilização de nodes
kubectl top nodes
# Métricas
karpenter_nodes_allocatable{resource="cpu"}
karpenter_nodes_allocatable{resource="memory"}Troubleshooting comum
Problema 1: Pods ficam pending
# Verificar eventos do pod
kubectl describe pod <pod-name>
# Causas comuns:
# 1. Limits do NodePool atingidos
kubectl get nodepool -o yaml | grep limits
# 2. Requirements muito restritivos
kubectl get nodepool -o yaml | grep requirements
# 3. Taints sem tolerations
kubectl describe pod <pod-name> | grep -A5 TolerationsProblema 2: Nodes não consolidam
# Verificar configuração de consolidação
kubectl get nodepool -o yaml | grep -A5 disruption
# Verificar PDBs bloqueando
kubectl get pdb -A
# Ver logs de consolidação
kubectl logs -n karpenter -l app.kubernetes.io/name=karpenter | grep "cannot consolidate"Problema 3: Spot instances interrompidas frequentemente
# Ver histórico de interrupções
kubectl logs -n karpenter -l app.kubernetes.io/name=karpenter | grep interruption
# Solução: Aumentar diversificação
# Adicionar mais categorias e gerações ao NodePool
requirements:
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["c", "m", "r", "t"] # Mais opções
- key: karpenter.k8s.aws/instance-generation
operator: In
values: ["5", "6", "7"] # Múltiplas geraçõesProblema 4: Custos maiores que esperado
# Verificar tipos de instância provisionados
kubectl get nodes -o json | \
jq -r '.items[] | .metadata.labels["node.kubernetes.io/instance-type"]' | \
sort | uniq -c
# Verificar capacity type (Spot vs On-Demand)
kubectl get nodes -o json | \
jq -r '.items[] | .metadata.labels["karpenter.sh/capacity-type"]' | \
sort | uniq -c
# Solução: Ajustar requirements para instâncias menores
requirements:
- key: karpenter.k8s.aws/instance-size
operator: In
values: ["small", "medium", "large"] # Limitar tamanhosMelhores práticas: Checklist completo
Configuração de NodePools
Diversificação de instâncias:
# Múltiplas categorias
instance-category: ["c", "m", "r"]
# Múltiplas gerações
instance-generation: ["5", "6", "7"]
# Múltiplas arquiteturas (se suportado)
arch: ["amd64", "arm64"]Limits definidos:
limits:
cpu: 1000
memory: 1000GiConsolidação habilitada:
disruption:
consolidationPolicy: WhenUnderutilized
expireAfter: 720hSpot com fallback:
capacity-type: ["spot", "on-demand"]Configuração de Workloads
Resource requests em todos os pods:
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: 1000m
memory: 2GiPDBs para deployments com múltiplas réplicas:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: app-pdb
spec:
minAvailable: 2
selector:
matchLabels:
app: appTopology spread para Multi-AZ:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotScheduleAnti-affinity para alta disponibilidade:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
topologyKey: kubernetes.io/hostnameSegurança
IMDSv2 obrigatório:
metadataOptions:
httpTokens: requiredDiscos criptografados:
blockDeviceMappings:
- deviceName: /dev/xvda
ebs:
encrypted: trueIAM roles com least privilege:
- KarpenterControllerRole: Apenas permissões necessárias
- KarpenterNodeRole: Apenas permissões para nodes
Monitoramento
Logs centralizados:
kubectl logs -n karpenter -l app.kubernetes.io/name=karpenter -fMétricas no Prometheus:
- karpenter_nodes_created_total
- karpenter_nodes_terminated_total
- karpenter_deprovisioning_actions_performed_total
Alertas configurados:
- Pods pending por mais de 5 minutos
- Nodes não consolidando
- Custos acima do esperado
Custos
Uso máximo de Spot (70-90% dos workloads)
Consolidação habilitada
Instâncias Graviton (ARM64) quando possível
Monitoramento de custos:
# Ver distribuição de capacity type
kubectl get nodes -o json | \
jq -r '.items[] | .metadata.labels["karpenter.sh/capacity-type"]' | \
sort | uniq -cConclusão
O Karpenter representa uma evolução significativa no auto scaling de Kubernetes. Ao provisionar nodes sob demanda em segundos, escolher automaticamente os melhores tipos de instância, e consolidar recursos continuamente, ele resolve as limitações fundamentais do Cluster Autoscaler.
Os ganhos são concretos:
- 30-60% de redução de custos através de melhor utilização e Spot instances
- 3-5x mais rápido no provisionamento de nodes (60-90s vs 3-6 min)
- 2x melhor utilização de recursos através de consolidação inteligente
- Simplicidade operacional com configuração declarativa via NodePools
Mas o Karpenter não é mágico. Ele funciona melhor quando seus workloads seguem boas práticas:
- Resource requests corretos: Karpenter precisa saber o tamanho necessário
- PDBs configurados: Garantem zero downtime durante consolidação
- Multi-AZ com topology spread: Alta disponibilidade automática
- Diversificação de instâncias: Reduz interrupções de Spot
A migração do Cluster Autoscaler para Karpenter deve ser gradual e planejada. Comece auditando seus workloads, adicione resource requests e PDBs, teste em staging, e migre por namespace em produção.
O investimento vale a pena. Empresas que migraram para Karpenter reportam não apenas economia significativa de custos, mas também melhor experiência operacional e maior confiabilidade.
O Karpenter é o presente e futuro do auto scaling no EKS. Quanto antes você migrar, mais cedo começará a economizar.