Um time que roda seis serviços em ECS Fargate ou Cloud Run chega, em algum momento, na mesma pergunta: vale a pena assumir um cluster Kubernetes? A resposta honesta não é “depende do tamanho”. É: depende de quantas vezes por trimestre o runtime gerenciado obriga a contornar uma limitação com gambiarra, e de quanto tempo de engenharia essa gambiarra consome.
Este comparativo trata das duas opções como elas se apresentam hoje para times de 5 a 40 pessoas: de um lado, runtimes gerenciados de container (ECS/Fargate, Cloud Run, Azure Container Apps); do outro, Kubernetes — seja cluster gerenciado do provedor, seja auto-hospedado.
A diferença real não é orquestração, é superfície de extensão
Os dois modelos resolvem o básico: sobem containers, fazem rolling deploy, escalam por métrica, reiniciam o que morreu. Quem compara pelo básico conclui que Kubernetes é complexidade desnecessária — e está certo, enquanto o básico for suficiente.
A diferença aparece quando o requisito sai do básico. Kubernetes expõe um modelo de recursos extensível: Custom Resource Definitions, controladores, admission webhooks, operadores. Isso significa que padrões operacionais viram objeto declarativo versionado no repositório. Um operador de Postgres cuida de failover e backup; um controlador de certificados renova TLS; uma policy engine bloqueia deploy de container rodando como root — tudo pela mesma API.
Vale um exemplo concreto de como isso se materializa. Impedir que qualquer container suba com o filesystem raiz gravável é, em Kubernetes, um objeto declarativo aplicado ao namespace inteiro — quem tentar violar recebe erro no kubectl apply, não um comentário em code review. Fora do cluster, a mesma regra vira um passo de pipeline que alguém pode pular com skip ci num deploy de emergência às onze da noite, que é exatamente quando a regra importa.
Runtime gerenciado não tem esse ponto de extensão. Se o comportamento desejado não existe como flag do serviço, ele vira script de CI, função serverless auxiliar ou processo manual documentado no Notion. Cada contorno desses é barato isoladamente e caro no agregado.
O custo da fatura e o custo do calendário
A comparação de preço costuma ser feita errado porque compara apenas computação. Em runtime gerenciado, paga-se por vCPU e memória alocados por segundo, com sobretaxa embutida pela abstração — a ordem de grandeza usual é um adicional relevante sobre o preço de máquina equivalente, e a proporção varia bastante entre provedores e regiões. Em Kubernetes gerenciado, paga-se pelas instâncias do node pool, mais uma taxa fixa de control plane por cluster.
Três efeitos mudam a conta quando a carga cresce:
- Bin packing. Kubernetes empacota vários pods pequenos numa mesma instância. Runtime gerenciado cobra por task, e task pequena com pico raro paga alocação ociosa o tempo todo.
- Instâncias spot. Node pools spot com PodDisruptionBudget e tolerations bem configurados cortam boa parte do custo de computação de cargas tolerantes a interrupção. Runtimes gerenciados oferecem equivalentes mais limitados.
- Escala a zero. Aqui o gerenciado ganha. Cloud Run e Container Apps zeram serviço sem tráfego; num cluster, manter o node pool de pé para um serviço ocioso custa mais do que o serviço vale.
Um exercício útil antes de decidir: pegue os serviços atuais, some o CPU e a memória efetivamente usados no percentil 95 e compare com o que está alocado. Em ambientes com muitos serviços pequenos, é comum que a alocação seja várias vezes o uso — a proporção varia demais entre workloads para citar um número, mas ela é mensurável em qualquer painel de métricas e não deveria ser estimada de cabeça. Essa diferença é exatamente o que o bin packing recupera, e é o único ganho de custo do cluster que aparece na fatura sem depender de renegociação com o provedor.
O custo de calendário é o que decide de verdade. Um cluster exige alguém responsável por upgrade de versão a cada poucos meses, por atualização de CRDs e controladores, por gestão de nodes e por dívida de configuração. Não é um projeto — é uma rotina permanente. Times que adotaram cluster sem designar essa responsabilidade acabam com versão desatualizada, controlador quebrado no upgrade seguinte e uma manhã perdida por incidente.
Onde cada modelo trava
| Requisito | Runtime gerenciado | Kubernetes |
|---|---|---|
| Deploy simples com rollback | Nativo, sem configuração | Nativo, mas exige manifesto e pipeline |
| Escala a zero | Nativo em Cloud Run/Container Apps | Só com KEDA ou Knative por cima |
| Job batch com dependência entre etapas | Depende de orquestrador externo | Job, CronJob e operadores de workflow |
| Workload com estado (banco, fila, cache) | Normalmente inviável — usa serviço gerenciado | StatefulSet e operador, com custo operacional real |
| Rede: mTLS entre serviços, roteamento por header | Limitado ao que o serviço expõe | Service mesh ou Gateway API |
| Política de segurança obrigatória no deploy | Verificação no CI, contornável | Admission controller, bloqueia no cluster |
| Portabilidade entre provedores | Reescrita da camada de deploy | Manifests majoritariamente reaproveitáveis |
| Tempo até o primeiro serviço em produção | Horas | Dias a semanas, contando pipeline e observabilidade |
A linha mais importante da tabela é a última. Runtime gerenciado entrega valor no primeiro dia; cluster cobra adiantado e devolve depois. Adotar Kubernetes antes de ter os requisitos das linhas do meio é pagar adiantado por algo que talvez nunca seja usado.
Há ainda um custo que a tabela não captura: contratação e rotatividade. Manifesto Kubernetes é conhecimento transferível — quem opera cluster num lugar opera em outro. Configuração de runtime gerenciado é conhecimento de provedor, e o time que trocar de nuvem reescreve a camada de deploy inteira. Para produto que roda em ambiente do cliente, ou para empresa com cláusula de portabilidade em contrato, isso deixa de ser preferência técnica e vira requisito comercial.
Os quatro sinais de que o cluster passou a compensar
Não existe número de serviços que funcione como gatilho. Existem sintomas:
- Workload com estado entrou no roadmap. Um ClickHouse, um Kafka, um Elasticsearch que o time quer operar em vez de contratar. Runtime gerenciado não tem resposta razoável para isso.
- Requisito de rede entre serviços. mTLS obrigatório, roteamento canário por header, política de saída restrita por serviço. São coisas que um mesh resolve declarativamente e que, fora do cluster, viram configuração de load balancer espalhada.
- Multi-ambiente com paridade exigida. Quando dev, staging e produção precisam ser o mesmo objeto com valores diferentes, e não três configurações mantidas à mão.
- Conta de computação em que o desperdício por alocação já é visível. Quando o time consegue apontar que boa parte do que se paga é CPU alocada e não usada, o bin packing começa a pagar o esforço.
Um sinal isolado não justifica a migração. Dois sinais simultâneos, com pessoa designada para operar o cluster, justificam. Nenhum sinal e a resposta é ficar onde está — o custo de manter um cluster subutilizado é pago em atenção de engenharia, que é o recurso mais escasso do time.
Migrar sem parar o produto
A migração que funciona não é big bang. É mover um serviço de baixo risco, estabilizar a plataforma em cima dele e só então trazer o resto. Na prática:
O primeiro serviço a migrar deve ser stateless, ter tráfego real (para exercitar escala e observabilidade) e não estar no caminho crítico de receita. Um worker de processamento assíncrono costuma servir bem: se ele atrasar cinco minutos durante o ajuste, ninguém perde venda.
Antes de migrar o segundo serviço, o cluster precisa ter três coisas prontas: coleta de métricas e logs equivalente à que já existia, política de recursos definida por namespace e um caminho de rollback testado — não descrito, testado.
Vale fixar limites desde o primeiro deploy, porque adicionar depois exige rediscutir capacidade com o time todo:
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 512Mi
Dois erros aparecem com frequência nessa fase. O primeiro é copiar os valores de CPU e memória da configuração antiga sem revisar: alocação herdada de runtime gerenciado costuma estar superdimensionada, e trazê-la para o cluster anula o ganho de empacotamento que motivou a migração. O segundo é rodar os dois ambientes em paralelo por tempo indefinido “só por garantia” — o custo dobra, o time mantém duas pipelines e a decisão de desligar o ambiente antigo vai sendo adiada. Defina a data de desligamento junto com a data de corte de tráfego.
A escolha de definir request de CPU sem limit de CPU é deliberada: limite de CPU gera throttling em picos curtos e degrada latência sem economizar nada relevante. Memória é diferente — sem limite, um vazamento derruba o node inteiro em vez de derrubar só o pod.
O passo seguinte, para quem está avaliando agora: pegue a fatura de computação dos últimos três meses, separe quanto é alocação e quanto é uso efetivo, e coloque ao lado a lista de contornos que o time escreveu para driblar limitação do runtime atual. Se o desperdício de alocação sozinho não paga um dia de engenharia por semana, o cluster ainda não compensa — e essa conta deve ser refeita a cada trimestre, não a cada reunião de arquitetura.
Deixe um comentário