FinOps em Kubernetes: Uma Introdução Prática
FinOps é a disciplina que une finanças, operações e engenharia para otimizar os gastos em infraestrutura cloud. No contexto de Kubernetes, essa prática se torna crítica porque clusters podem consumir recursos de forma ineficiente rapidamente, gerando contas inesperadas. A maioria das organizações descobre que está gastando 30-40% a mais do que deveria apenas porque não há visibilidade sobre o que cada aplicação consome.
O desafio fundamental do FinOps em Kubernetes é que você tem múltiplos layers de abstração: nodes, pods, containers, volumes, ingresses, e cada um tem um custo associado. Sem as ferramentas certas, é impossível saber se um pod está desperdiçando CPU ou se um node rodando com 5% de utilização deveria ser desligado. É por isso que ferramentas como Kubecost existem — para trazer luz a essa caixa preta. Neste artigo, vamos explorar como implementar FinOps de verdade, partindo de uma visão clara dos custos até a otimização prática com right-sizing e spot instances.
Kubecost: Visibilidade Total de Custos
O que é Kubecost e por que usar
Kubecost é uma plataforma open-source que oferece visibilidade em tempo real dos custos de Kubernetes. Ela integra-se nativamente com seu cluster e coleta métricas de consumo de recursos, relaciona com preços de cloud providers (AWS, GCP, Azure) e te dá um dashboard completo. A beleza do Kubecost é que ele não é invasivo — roda como um pod como qualquer outro, mas tem acesso às métricas do Prometheus e aos dados de billing da sua cloud.
A ferramenta te responde perguntas essenciais: quanto custa rodar meu namespace production? Qual pod está consumindo mais CPU? Qual deployment seria mais barato se migrássemos para spot instances? Se você não tem respostas claras para essas perguntas, está operando Kubernetes às cegas.
Instalando Kubecost em seu cluster
Para instalar Kubecost, você vai usar Helm, que é o gerenciador de pacotes do Kubernetes. Vou assumir que você já tem um cluster funcional com Prometheus rodando (Kubecost precisa disso).
# Adicionar o repositório Helm do Kubecost
helm repo add kubecost https://kubecost.github.io/cost-analyzer/
helm repo update
# Instalar Kubecost no namespace kubecost-system
helm install kubecost kubecost/cost-analyzer \
--namespace kubecost-system \
--create-namespace \
--set kubecostModel.warmCache=true \
--set kubecostModel.warmSavingsCache=true \
--set prometheus.server.global.external_labels.cluster_id=production-cluster
Após a instalação, você pode acessar o dashboard do Kubecost via port-forward:
kubectl port-forward -n kubecost-system svc/kubecost-cost-analyzer 9090:9090
# Acesse http://localhost:9090 no seu navegador
No dashboard, você verá imediatamente seus gastos agregados, custos por namespace, custos por label e muito mais. Mas conhecer os custos é apenas o primeiro passo — agora você precisa agir.
Usando a API do Kubecost para relatórios customizados
Além do dashboard, Kubecost expõe uma API REST que você pode usar para extrair dados programaticamente. Isso é particularmente útil se você quer integrar relatórios de custo em seu pipeline de CI/CD ou em ferramentas internas.
import requests
import json
from datetime import datetime, timedelta
# Função para buscar custos de um namespace específico
def obter_custo_namespace(namespace, dias=7):
kubecost_url = "http://kubecost-cost-analyzer.kubecost-system.svc.cluster.local:9090"
agora = datetime.utcnow()
inicio = agora - timedelta(days=dias)
# Formatar datas no padrão Unix timestamp
inicio_ts = int(inicio.timestamp())
fim_ts = int(agora.timestamp())
endpoint = f"{kubecost_url}/model/allocation"
params = {
"window": f"{inicio_ts}s,{fim_ts}s",
"aggregate": "namespace",
"namespace": namespace
}
response = requests.get(endpoint, params=params)
dados = response.json()
if "data" in dados and len(dados["data"]) > 0:
custo_total = dados["data"][0].get("totalCost", 0)
return {
"namespace": namespace,
"custo_total_usd": round(float(custo_total), 2),
"periodo_dias": dias
}
return {"erro": "Nenhum dado encontrado"}
# Exemplo de uso
resultado = obter_custo_namespace("production", dias=30)
print(json.dumps(resultado, indent=2))
# Output esperado:
# {
# "namespace": "production",
# "custo_total_usd": 1245.67,
# "periodo_dias": 30
# }
Este script conecta à API do Kubecost e extrai o custo de um namespace específico nos últimos 7 dias. Você pode expandir isso para monitorar custos crescentes e disparar alertas — por exemplo, se um namespace custar mais de 50% a mais que a média histórica, algo anormal está acontecendo.
Right-sizing: O Alicerce da Otimização
Entendendo requests e limits
Right-sizing é a prática de configurar requests e limits de CPU e memória nas suas aplicações de forma realista. Muitos engenheiros ou deixam esses valores vazios (o que faz o Kubernetes não conseguir fazer scheduling correto) ou definem valores absurdamente altos "por segurança" (o que desperdiça recursos e dinheiro).
Um request é a quantidade de recurso que o Kubernetes reserva para o pod — é uma garantia. Um limit é o máximo que o container pode usar — ultrapassar isso resulta em throttling ou kill do container. A diferença entre o que você requisita e o que realmente usa é desperdício puro.
Vamos à verdade: a maioria das aplicações usa 10-30% do que foi alocado para elas. Um microserviço que requisita 2 CPUs provavelmente usa 200m (0,2 CPUs) durante operação normal. Isso significa que você está pagando por 1.8 CPUs de desperdício.
Coletando dados reais de uso
Antes de fazer qualquer mudança, você precisa coletar dados reais. O Prometheus já está armazenando essas métricas no seu cluster. O Kubecost usa essas mesmas métricas, mas você também pode consultá-las diretamente.
# Port-forward para o Prometheus
kubectl port-forward -n prometheus svc/prometheus 9091:9090
# Agora acesse http://localhost:9091 e execute esta query para encontrar
# o uso máximo de CPU por pod em uma janela de 7 dias
max(max_over_time(rate(container_cpu_usage_seconds_total{pod!=""}[5m])[7d:5m])) by (pod, namespace)
Essa query retorna o pico de uso de CPU que cada pod atingiu nos últimos 7 dias. Se um pod tem um request de 2000m (2 CPUs) mas o pico observado foi 400m, você acabou de identificar um desperdício de 75%.
Para uma análise mais robusta e visível no Kubecost, você pode usar este dashboard/query para percentis de uso:
# 95o percentil de uso de CPU por pod (recomendado para right-sizing)
quantile(0.95, max_over_time(rate(container_cpu_usage_seconds_total{pod!=""}[5m])[30d:5m])) by (pod, namespace)
# 95o percentil de uso de memória por pod
quantile(0.95, max_over_time(container_memory_working_set_bytes{pod!=""}[30d:5m])) by (pod, namespace)
O 95º percentil é a métrica certa para usar aqui — significa que 95% do tempo o pod usa essa quantidade ou menos. Baseando seus requests no 95º percentil, você garante que terá headroom para picos ocasionais sem desperdiçar recursos na maioria do tempo.
Implementando right-sizing em produção
Vamos a um exemplo prático. Suponha que você tem um deployment de API chamado user-service e descobriu via Prometheus que:
- 95º percentil de CPU: 300m
- 95º percentil de memória: 512Mi
- Atual request de CPU: 1000m
- Atual request de memória: 1Gi
# Antes (desperdiçando recursos)
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: user-service
template:
metadata:
labels:
app: user-service
spec:
containers:
- name: user-service
image: myregistry.azurecr.io/user-service:v1.2.0
resources:
requests:
cpu: "1000m"
memory: "1Gi"
limits:
cpu: "2000m"
memory: "2Gi"
Agora vamos otimizar baseado em dados reais:
# Depois (right-sized)
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: user-service
template:
metadata:
labels:
app: user-service
spec:
containers:
- name: user-service
image: myregistry.azurecr.io/user-service:v1.2.0
resources:
requests:
cpu: "350m" # 95º percentil + 50m de buffer
memory: "600Mi" # 95º percentil + 88Mi de buffer
limits:
cpu: "800m" # 2x o request para permitir picos curtos
memory: "1Gi" # Limite conservador
Essa mudança simples reduz o custo de CPU desse deployment em ~65% e o de memória em ~40%. Multiplicado por múltiplos deployments, o impacto financeiro é significativo. Mas atenção: sempre faça essas mudanças gradualmente em produção e monitore métricas de erro e latência após as alterações.
Spot Instances: Otimização Agressiva para Workloads Toleráveis
O que são spot instances e quando usá-las
Spot instances (ou preemptible VMs no GCP, ou EC2 Spot no AWS) são máquinas virtuais que a cloud vende a um desconto de até 70-90% em troca de poderem ser desligadas com poucas horas (ou minutos) de aviso. Para cargas de trabalho que podem tolerar interrupções — como jobs em batch, processamento de dados, testes, ambientes de desenvolvimento — spot instances são ouro puro financeiramente.
A chave é identificar quais workloads são tolerantes a interrupções. Um banco de dados que perde toda sua memória quando o pod é desligado não é tolerante. Um job de ETL que pode ser reiniciado sem perder dados é perfeitamente tolerante. A maioria das aplicações web também é tolerante — quando um pod morre, o Kubernetes inicia outro em outro node.
Configurando Kubernetes para Spot Instances com Karpenter
Gerenciar spot instances manualmente é uma dor de cabeça. O Karpenter é um projeto open-source que automatiza o provisionamento e desprovisionamento inteligente de nodes. Ele entende o custo de diferentes tipos de instância e prioriza spot instances automaticamente.
# Instalar Karpenter no cluster (assumindo AWS)
helm repo add karpenter https://charts.karpenter.sh
helm repo update
helm install karpenter karpenter/karpenter \
--namespace karpenter \
--create-namespace \
--set serviceAccount.annotations."eks\.amazonaws\.com/role-arn"=arn:aws:iam::ACCOUNT_ID:role/karpenter \
--set settings.clusterName=my-cluster \
--set settings.interruptionQueue=karpenter-interruption-queue
Agora vamos criar um Provisioner do Karpenter que prefere spot instances mas pode cair back para on-demand se necessário:
apiVersion: karpenter.sh/v1alpha5
kind: Provisioner
metadata:
name: default
spec:
# Prioridade: spot instances em primeiro lugar, depois on-demand
providerRef:
name: default
# Limpar nodes ociosos após 30 segundos
ttlSecondsAfterEmpty: 30
# Limpar nodes após 30 dias para forçar renovação e puxar imagens atualizadas
ttlSecondsUntilExpired: 2592000
consolidation:
enabled: true
# Consolidar nodes a cada 30 segundos
ttlSecondsAfterLastProvisioned: 30
limits:
# Limite de CPU total que esse provisioner pode alocar
resources:
cpu: 1000
memory: 1000Gi
# Requerimentos de capacity type
requirements:
- key: "karpenter.sh/capacity-type"
operator: In
values: ["spot", "on-demand"]
- key: "kubernetes.io/arch"
operator: In
values: ["amd64"]
- key: "node.kubernetes.io/instance-type"
operator: In
values: ["t3.medium", "t3.large", "t3a.medium", "t3a.large", "m5.large", "m5.xlarge"]
- key: "karpenter.sh/do-not-evict"
operator: DoesNotExist
---
apiVersion: karpenter.k8s.aws/v1alpha1
kind: EC2NodeClass
metadata:
name: default
spec:
subnetSelector:
karpenter.sh/discovered: "true"
securityGroupSelector:
karpenter.sh/discovered: "true"
tags:
ManagedBy: "Karpenter"
Com essa configuração, o Karpenter tentará sempre provisionar nodes de spot instances, economizando significativamente. Quando o Karpenter detecta que uma spot instance vai ser interrompida (AWS envia um aviso), ele drena o node gracefully, migrando os pods para outras máquinas.
Taints e tolerations para workloads spot-only
Para certos workloads que você quer que rodem exclusivamente em spot instances (porque tem tolerância máxima), você pode usar taints e tolerations:
apiVersion: batch/v1
kind: CronJob
metadata:
name: batch-processor
namespace: processing
spec:
schedule: "0 2 * * *" # Roda todo dia às 2 da manhã
jobTemplate:
spec:
template:
spec:
# Tolera apenas nodes com spot instances
tolerations:
- key: karpenter.sh/capacity-type
operator: Equal
value: spot
effect: NoSchedule
# Prefere nodes de spot (soft requirement)
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: karpenter.sh/capacity-type
operator: In
values:
- spot
containers:
- name: processor
image: myregistry.azurecr.io/batch-processor:latest
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2000m"
memory: "2Gi"
restartPolicy: OnFailure
Esse CronJob rodará um job que processa dados em batch. Como é um job que pode ser reiniciado sem problemas se a instância for interrompida, ele fica isolado em nodes de spot, economizando até 80% em relação ao custo on-demand.
Monitorando economia com Spot Instances
Para medir o impacto financeiro real de usar spot instances, você pode usar uma query do Kubecost combinada com a API:
import requests
import json
from datetime import datetime, timedelta
def comparar_custo_spot_vs_ondemand():
kubecost_url = "http://kubecost-cost-analyzer.kubecost-system.svc.cluster.local:9090"
agora = datetime.utcnow()
inicio = agora - timedelta(days=30)
inicio_ts = int(inicio.timestamp())
fim_ts = int(agora.timestamp())
# Custos reais (o que você pagou)
params_reais = {
"window": f"{inicio_ts}s,{fim_ts}s",
"aggregate": "node"
}
response_reais = requests.get(f"{kubecost_url}/model/allocation", params=params_reais)
dados_reais = response_reais.json()
# Parsear dados para encontrar custos de spot vs on-demand
custo_spot = 0
custo_ondemand = 0
if "data" in dados_reais:
for node_data in dados_reais["data"]:
node_name = list(node_data.keys())[0]
custo = float(node_data[node_name].get("totalCost", 0))
# Isso é uma simplificação - no mundo real, você consultaria tags do node
if "spot" in node_name.lower():
custo_spot += custo
else:
custo_ondemand += custo
# Estimar custo se tudo fosse on-demand (multiplicar spot por ~4x)
custo_spot_se_ondemand = custo_spot * 4
economia = custo_spot_se_ondemand - custo_spot
return {
"custo_spot_real_usd": round(custo_spot, 2),
"custo_ondemand_real_usd": round(custo_ondemand, 2),
"economia_estimada_spot_usd": round(economia, 2),
"percentual_economia": round((economia / (economia + custo_ondemand)) * 100, 1) if economia > 0 else 0
}
resultado = comparar_custo_spot_vs_ondemand()
print(json.dumps(resultado, indent=2))
# Output esperado:
# {
# "custo_spot_real_usd": 245.33,
# "custo_ondemand_real_usd": 1823.45,
# "economia_estimada_spot_usd": 730.99,
# "percentual_economia": 28.6
# }
Conclusão
Dominar FinOps em Kubernetes envolve três pilares essenciais que aprendemos aqui. Primeiro, visibilidade absoluta através do Kubecost — você não otimiza o que não mede. Ter um dashboard mostrando exatamente onde vai cada dólar gasto é o ponto de partida inegociável para qualquer organização séria com Kubernetes. Segundo, right-sizing disciplinado baseado em dados reais, não em palpites de engenheiros — usar Prometheus para extrair percentis de consumo e ajustar requests e limits consequentemente reduz custos em 40-70% sem impactar performance. Terceiro, aproveitamento agressivo de spot instances para workloads tolerantes através de ferramentas como Karpenter — esse é o multiplicador final que pode levar sua economia a 80% em comparação com on-demand puro. A combinação dos três — visibilidade clara, alocação realista de recursos e aproveitamento de instâncias baratas — transforma FinOps de uma prática abstrata em um programa concreto que adiciona milhares de dólares em valor ao seu negócio a cada mês.