Como Usar FinOps em Kubernetes: Kubecost, Right-sizing e Spot Instances em Produção Já leu

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

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.

Referências


Artigos relacionados