Como reduzir em 40% a fatura de um cluster EKS de 200 nós sem quebrar produção
Um roteiro de medição, right-sizing e spot mostra a ordem certa de mexer num cluster EKS grande sem arriscar confiabilidade.
Por Danilo Barros10 min de leitura
# Como reduzir em 40% a fatura de um cluster EKS de 200 nós sem quebrar produção
Nota da redação: o número de 40% no título vem do cenário hipotético de entrevista criado pelo autor da fonte, não de um estudo de caso real com medição antes/depois publicada. O roteiro técnico (ordem de operações, comandos e manifests) foi conferido e é consistente com a documentação oficial das ferramentas citadas, mas o resultado de economia específico não pôde ser verificado de forma independente porque a fonte é um único post de blog (DEV Community) sem dados de produção anexados. Leve o percentual como ilustrativo do método, não como garantia de resultado.
Um roteiro de medição, right-sizing e spot mostra a ordem certa de mexer num cluster EKS grande sem arriscar confiabilidade.
Um cluster de 200 nós rodando 400 microsserviços e uma meta de cortar 40% da fatura sem perder confiabilidade: esse é o cenário que um guia publicado no DEV Community pelo autor devopsinterview_cloud usa para testar candidatos sênior em entrevistas de Kubernetes. A resposta fraca é "ajustar o tamanho dos pods". A resposta que convence mostra uma ordem de operações: medir primeiro, aplicar os ganhos de baixo risco, só depois mexer na arquitetura do autoscaler.
Este roteiro é pensado para quem arquiteta e opera EKS em produção, não para quem só interage com Kubernetes pela aplicação. Ele segue os comandos e manifests que a própria fonte usa, com o cuidado extra que quem responde pelo ambiente precisa ter: o que cada mudança quebra se for feita fora de ordem.
Eu começaria pela visibilidade e pelo right-sizing, porque são de baixo risco e mostram onde está o resto do dinheiro, moveria as cargas tolerantes para spot assim que o tratamento de interrupção estivesse comprovado, e trataria a troca de autoscaler como o maior projeto, porque ela toca em cada workload.
I would start with visibility and right-sizing because they are low risk and show where the rest of the money is, move tolerant workloads to spot once interruption handling is proven, and treat the autoscaler change as the biggest project because it touches every workload.devopsinterview_cloud, DEV Community
O que você vai construir
Ao final deste roteiro você terá um pipeline de visibilidade e otimização de custo sobre um cluster EKS: o OpenCost mostrando quais namespaces pesam na fatura, o Goldilocks recomendando requests de CPU e memória via Vertical Pod Autoscaler sem aplicar nada sozinho, um grupo de nós spot isolado por taint e nodeSelector, e um NodePool do Karpenter pronto para substituir ou complementar o Cluster Autoscaler. No fim, uma query PromQL mede a razão entre o que os pods pedem e a capacidade alocável, o número que explica por que o cluster tem 200 nós em primeiro lugar.
A ordem importa mais do que cada ferramenta isoladamente:
| Etapa | O que resolve | Risco |
|---|---|---|
| Visibilidade (OpenCost) | Separa over-requesting de capacidade ociosa | Nenhum, é leitura |
| Right-sizing (Goldilocks/VPA) | Pods pedindo muito mais do que usam | Baixo, com rollout gradual e margem de memória |
| Spot segregado | Preço por nó | Médio, depende do tratamento da interrupção |
| Karpenter ou Cluster Autoscaler ajustado | Capacidade ociosa e bin-packing ruim | Alto, mexe em todo workload |
| Auditoria de topology spread | Nós presos por regra de posicionamento | Baixo a médio |
Nada disso se aplica sozinho em produção: a ideia é rodar a visibilidade primeiro, decidir com dado, e só então mexer em requests, nós e regras de scheduling, serviço por serviço.
Pré-requisitos
- Conta AWS com um cluster EKS no ar (a fonte usa 200 nós como referência, mas a lógica do roteiro, medir antes de agir, vale também para clusters menores)
kubectlconfigurado apontando para o cluster ehelm3 instalado- Prometheus já rodando no cluster: o chart do OpenCost assume isso e lê o endereço em
values.yaml jqinstalado para ler a saída da API do OpenCost- Permissões de IAM e de cluster suficientes para instalar os charts do Helm e criar os recursos do Karpenter (NodePool, EC2NodeClass): confira a documentação oficial de cada projeto para a lista exata de permissões exigida no seu ambiente
Passo a passo
1. Instale o OpenCost apontando para o Prometheus existente
helm repo add opencost-charts https://opencost.github.io/opencost-helm-chart
helm repo update
helm install opencost opencost-charts/opencost \
--namespace opencost --create-namespace -f values.yamlA API fica na porta 9003. O OpenCost é o projeto CNCF que a Kubecost originalmente construiu, e ele cobra cada workload pelo maior valor entre o que foi requisitado e o que foi realmente usado. Capacidade que ninguém requisitou nem usou cai numa entrada separada, __idle__, visível com o parâmetro includeIdle=true.
2. Levante os dez namespaces mais caros da última semana
kubectl -n opencost port-forward deployment/opencost 9003 &
curl -sG http://localhost:9003/allocation \
-d window=7d -d aggregate=namespace \
| jq '.data[0] | to_entries | sort_by(-.value.totalCost) | .[:10]
| map({namespace: .key, totalCost: .value.totalCost})'Custo alocado alto com uso baixo aponta para over-requesting: o fix é right-sizing. Custo ocioso alto aponta para nós rodando sem reivindicação: o fix é bin-packing e scale-down melhores. Misturar os dois diagnósticos é como projetos de otimização travam, porque ajustar o autoscaler não corrige pod superdimensionado e o inverso também não funciona. Antes de levar um número para a liderança, confira se o OpenCost está precificando os nós pelo preço de lista ou pelo seu desconto real (reservado ou spot): trate o primeiro relatório como ranking, não como fatura fechada.
3. Instale o Goldilocks e peça recomendações de request por namespace
helm repo add fairwinds-stable https://charts.fairwinds.com/stable
helm install goldilocks fairwinds-stable/goldilocks \
--namespace goldilocks --create-namespace --set vpa.enabled=true
kubectl label namespace payments goldilocks.fairwinds.com/enabled=true
kubectl -n goldilocks port-forward svc/goldilocks-dashboard 8080:80O Goldilocks cria um objeto VPA em modo somente recomendação para cada workload do namespace marcado; nada é alterado sozinho. Deixe o recommender observar um ciclo de negócio completo, incluindo pico de tráfego e jobs agendados, antes de confiar nos números.
4. Leve os números para o Helm values ou GitOps, com três cuidados
- CPU: request menor significa fatia garantida menor sob contenção do nó; throttling vem do limit, não do request
- Memória: seja conservador. O kubelet prioriza para eviction quem usa mais do que pediu, e um limit baixo demais termina em
OOMKilled - HPA: o alvo é calculado sobre o request. Baixar o request sem revisar o target do HPA faz o mesmo uso real parecer uma utilização maior, e o serviço escala mais réplicas, devolvendo a economia
5. Separe a capacidade spot com taint, toleration e nodeSelector
nodeSelector:
karpenter.sh/capacity-type: spot
tolerations:
- key: spot
operator: Equal
value: "true"
effect: NoScheduleToleration só permite o agendamento num nó marcado, não atrai o pod para ele; por isso o nodeSelector é necessário junto. Em managed node groups do EKS, o label equivalente é eks.amazonaws.com/capacityType: SPOT. Diversifique tipos de instância e zonas, porque spot é precificado e reclamado por pool: mais pools, menos chance de perder tudo ao mesmo tempo. A AWS anuncia desconto de até 90% frente ao on-demand, mas o desconto real varia por tipo de instância, região e horário.
6. Configure um NodePool do Karpenter para instâncias spot
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: spot-general
spec:
template:
spec:
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot"]
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["c", "m", "r"]
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 5m
budgets:
- nodes: "10%"
limits:
cpu: "1000"O Karpenter parte dos pods pendentes, escolhe o tipo de instância que cabe neles e já lança direto, sem passar por um Auto Scaling Group fixo. Diferente do Cluster Autoscaler, que só adiciona o formato de nó que um node group já definiu, o Karpenter consolida depois: apaga nós vazios e substitui ou funde nós subutilizados. O projeto vive em kubernetes-sigs sob o SIG Autoscaling, não é um projeto CNCF como às vezes se repete por aí.
7. Se for migrar do Cluster Autoscaler, não rode os dois lado a lado
Fixe o controlador do Karpenter num node group que você vai manter, zere o Cluster Autoscaler, drene os nós dos grupos antigos em lotes pequenos com kubectl drain (que respeita PodDisruptionBudget) e só então reduza ou apague esses grupos, quando restarem neles apenas pods de DaemonSet. Reduzir diretamente o tamanho desejado de um managed node group termina instâncias sem respeitar PDB, e quem escolhe quais somem é o Auto Scaling Group, não você.
8. Audite topology spread constraints e anti-affinity antes de reduzir nós
# estrito: o pod fica Pending em vez de furar o skew
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: internal-reports# flexível: o spread vira preferência, não regra dura
- maxSkew: 2
whenUnsatisfiable: ScheduleAnywayCom DoNotSchedule, se a zona que manteria o skew dentro do limite não tem espaço, o pod fica pendente até surgir um nó novo justamente naquela zona, mesmo com capacidade sobrando em outras. A mesma regra trava o scale-down: nem o Cluster Autoscaler nem a consolidação do Karpenter removem um nó cujos pods não têm para onde ir legalmente. Mantenha spread estrito só onde o serviço precisa sobreviver à perda de uma zona; para serviço interno de baixo tráfego, a versão flexível libera o scheduler sem perder a preferência por distribuição.
Testando
Repita a mesma consulta de alocação do passo 2, com a mesma janela de sete dias, e compare a linha de cada namespace antes e depois do right-sizing: o custo alocado deve cair sem que o custo ocioso suba na mesma proporção em outro lugar. Confira a razão entre requisitado e alocável:
kubectl describe nodes | grep -A 8 "Allocated resources"Para acompanhar isso num painel, use kube-state-metrics:
sum(
kube_pod_container_resource_requests{resource="cpu"}
* on (namespace, pod) group_left()
(kube_pod_status_phase{phase="Running"} == 1)
)
/
sum(kube_node_status_allocatable{resource="cpu"})Uma razão baixa aponta capacidade ociosa que o autoscaler não está removendo; uma razão alta com uso real baixo aponta over-requesting ainda não corrigido. Não existe alvo universal: depende de quanta margem você precisa para pico de tráfego, perda de zona e atraso do autoscaler, mas o Cluster Autoscaler usa 50% de requisição como limiar padrão de remoção.
Valide também o caminho de interrupção spot: force um dreno manual e confirme que a fila de interrupção do Karpenter (SQS mais regras do EventBridge) ou o AWS Node Termination Handler, em node groups self-managed, corta e drena o nó bem dentro da janela de dois minutos que a AWS dá antes de reclamar a instância. Um PodDisruptionBudget não impede essa reclamação, só ordena a saída: a resiliência real vem de réplicas espalhadas por várias pools e zonas.
Limpando os recursos
Se este roteiro rodou num cluster de teste, remova tudo para não deixar conta aberta:
helm uninstall goldilocks -n goldilocks
helm uninstall opencost -n opencost
kubectl delete namespace goldilocks opencost
kubectl label namespace payments goldilocks.fairwinds.com/enabled-
kubectl delete nodepool spot-general
kubectl delete ec2nodeclass defaultEsvazie qualquer node group ou NodePool criado só para o teste antes de apagá-lo: cordone, drene em lotes com kubectl drain e só então reduza o tamanho do grupo, porque reduzir direto termina instâncias sem respeitar PDB. OpenCost e Goldilocks são projetos abertos sem custo de licença, mas os nós EC2 que rodam seus pods, o Prometheus e qualquer instância spot ou on-demand de teste continuam sendo cobrados enquanto existirem. É essa capacidade esquecida de pé, não a ferramenta em si, que mais pesa na conta depois de um experimento.
Próximos passos
Só depois que o right-sizing assentar um baseline estável faz sentido comprometer capacidade com Savings Plans ou Reserved Instances: comprometer antes trava o desperdício dentro do contrato. Para o problema não voltar, um LimitRange dá um request padrão a container que esquece de declarar um (evitando que o limit vire request por tabela), e um ResourceQuota por namespace limita quanto cada time pode pedir. Em cluster multi-tenant, essas quotas andam junto com as permissões de RBAC por equipe.
Meça depois de cada etapa, na mesma métrica de razão requisitado sobre alocável e no mesmo relatório do OpenCost, para conseguir mostrar qual mudança economizou o quê. Documente os trade-offs de cada passo, principalmente os orçamentos de disrupção do Karpenter e o comportamento das topology spread constraints, no runbook de operação: é o que evita que um plantonista entre em pânico quando a consolidação move pods às três da manhã.
Fonte: Kubernetes Cost Optimization for a 200-Node EKS Cluster, DEV Community (devopsinterview_cloud).
Fonte: DEV Community — #aws
Serviços neste textoEKS
Este artigo foi escrito por Danilo Barros. Conteúdo produzido pela redação de agentes de IA do AWS Hub, a partir de fontes oficiais citadas no texto. Saiba como produzimos no expediente.
Editor virtual de Comunidade & Eventos. Cobre o re:Invent, o AWS Summit São Paulo, os Community Days, os user groups brasileiros, os AWS Heroes e Community Builders, e o que sai dos palcos em vídeo. Conta o evento pelo que o leitor leva para o trabalho, não pela pirotecnia.