transmissão ao vivo · o gp do kubernetes avançado, etapa por etapa

Kubernetes avançado
direto do paddock.

De volta à pista, e agora é campeonato de gente grande! Nove etapas da Fase 2: saúde e recursos, agendamento fino, rollouts, Helm, Blue/Green, Canary, Karpenter, KEDA e segurança. Você já sabe pilotar o carro — aqui a gente afina o setup pra vencer o mundial. Farol apagado, o piloto assume.

Este é o guia da Fase 2: assume que você já domina o básico (Pods, Deployments, Services, kubectl) e sobe o nível pra operar clusters de verdade — o problema que cada peça resolve, a analogia de F1 honesta, o YAML comentado, os comandos com a saída esperada e um mão na massa por etapa.

O quadro de-para — cole isto no volante

A mesma analogia vale do início ao fim do guia. Não tente decorar esta tabela agora — ela é o mapa do circuito, não a prova: siga direto para a Etapa 01 e volte aqui sempre que uma analogia aparecer no texto.

KubernetesFórmula 1
ContainerComponente do carro (a PU híbrida, a ERS, a caixa de câmbio) — a peça que faz o trabalho
PodO carro completo — menor unidade que vai pra pista; agrupa componentes que dividem o mesmo chassi e a mesma telemetria
Node (nó)O box/garagem na pit lane — hospeda vários carros e tem recurso finito (energia, ar comprimido, espaço)
ClusterA equipe inteira — todos os boxes + muro + operação
kubeletO chefe de mecânicos daquele box — vigia os carros ali, troca componente, chama pro reparo
DeploymentA ficha de operação que garante N carros idênticos prontos no grid
ReplicaSetO conjunto de carros de uma mesma especificação/versão de setup
RéplicaCada carro idêntico da equipe no grid
ServiceO muro/rádio com endereço fixo que direciona o trabalho pro carro certo
EndpointsA lista de carros no momento aptos a receber trabalho
Load balancingO engenheiro de estratégia distribuindo stints/voltas entre os carros
Liveness probeTelemetria "o carro travou?" → se sim, chama pro box e reinicia o componente
Readiness probeTelemetria "o carro está em ritmo?" → tira/põe no pelotão, sem chamar pro box
Startup probe"Terminou o out-lap de aquecimento?" (pneu e freio na janela de temperatura)
Requests / limitsEnergia garantida por volta vs. teto que não pode furar (envelope de ERS / cota)
OOMKillComponente estourou o envelope e queimou — falha catastrófica, carro fora
CPU throttlingDerate / engine mapping segurando potência pra ficar dentro da cota
Classes de QoSA ordem de quem a equipe sacrifica sob safety car / contenção
TaintPlaca no box: "reservado, só carro autorizado" (box de pneu de chuva, box de GPU)
TolerationA credencial do carro que aceita entrar naquele box marcado
Node affinityOrdem de estratégia: "esse carro TEM que ir pra esse box/setup"
Pod anti-affinityNunca colocar os dois carros da equipe no mesmo box (risco duplo)
Topology SpreadEspalhar os carros por boxes/zonas com garantia numérica
EvictionO chefe de mecânicos sacrificando um carro pra salvar o box quando falta recurso
PriorityClassCarro do líder do campeonato vs. carro de teste — quem se salva primeiro
RollingUpdateTrocar o setup carro a carro ao longo de pit stops, sem esvaziar a pista
maxUnavailableO freio: quantos carros posso ter no box ao mesmo tempo
maxSurgeA potência: quantos carros extras (T-car) monto pra acelerar a troca
RecreateChamar todos ao box de uma vez — para tudo, mas nunca mistura versões
rollout undoVoltar pro setup salvo da classificação (baseline de parc fermé)
Helm ChartA fôrma de montagem do carro — mesma base, ajuste por circuito
values.yamlA folha de setup por circuito (asa, pressão de pneu, mapa de motor)
helm template (função pura)O simulador: mesma entrada → mesma saída, roda antes de ir pra pista
Blue/GreenO T-car já montado e aquecido nos bastidores; promover = trocar de carro
CanaryMandar UM carro testar o upgrade numa fatia da corrida antes de aplicar nos dois
Peso de tráfego (service mesh)O engenheiro dosando a fração exata, independente do nº de carros
SLO gateO limite de delta/temperatura que aborta o teste na hora
KarpenterO gerente de logística montando box sob medida just-in-time
Cluster AutoscalerBoxes de tamanho fixo, pré-reservados por contrato
Spot instanceRecurso barato que pode ser requisitado de volta a qualquer momento
PodDisruptionBudgetRegra: nunca tirar réplicas demais juntas (nunca os dois carros no box)
KEDA / scale-to-zeroAbrir mecânicos de pit conforme a fila; zero quando não há carro chegando
ServiceAccountO crachá de acesso de cada membro da equipe
RBAC (Role/RoleBinding)O que aquele crachá abre — e só isso
IRSA / Workload IdentityCredencial temporária pra entrar no motorhome do fornecedor, sem chave-mestra
cert-manager / TLSLacres da FIA que expiram e se renovam sozinhos
Zero TrustO regulamento FIA: ninguém passa sem credencial verificada

Onde as analogias quebram (piloto que esconde limitação do carro quebra no muro):

  • Blue/Green (Etapa 05): trocar de carro em pista é limpo, mas o software pode "reescrever o traçado da pista" — o schema do banco. Quando a versão nova muda o esquema de forma incompatível, voltar pro carro antigo não salva: a pista já mudou embaixo dele. Por isso a migração retrocompatível (compatibilidade dupla temporária) vem ANTES da virada de chave — o T-car não cobre esse caso sozinho.
  • Réplicas e falha compartilhada (Etapas 01 e 02): dois carros no grid multiplicam disponibilidade SÓ se não dependerem da mesma peça de infra. Liveness amarrada ao mesmo banco, ou os dois pods no mesmo nó, quebram a independência que a analogia dos "dois carros" pressupõe.
SEU PROGRESSO0 de 9 etapas

Etapa 01 telemetria de bordo

Saúde da aplicação e gerenciamento de recursos

Senhoras e senhores, o pelotão sai da garagem pra primeira volta e a pergunta que abre TODA transmissão é a mais simples e a mais brutal: o carro está inteiro? Está em ritmo? Antes de qualquer estratégia mirabolante, é a telemetria que manda. Prende o cinto, porque a Etapa 01 é onde a gente aprende a LER o carro.

Imagine seu e-commerce numa Black Friday. Um dos pods da API entrou em deadlock: o processo continua de pé, o container não morreu, mas ele responde HTTP 500 para todo cliente que chega. O Kubernetes, sozinho, só sabe uma coisa: "o processo está vivo". Ele não reinicia nada, e você perde vendas por horas até alguém perceber no gráfico. No pod ao lado, um vazamento de memória vai comendo RAM até estourar o nó inteiro e derrubar os vizinhos saudáveis junto.

Como funciona

Foi exatamente o que rolou com o Antonelli em Silverstone: largou na pole (P1) e cruzou em P15. O carro estava vivo — motor rodando, dando voltas, terminou a corrida —, mas não estava em ritmo. Processo de pé, resposta 500. Você precisa de duas telemetrias diferentes: uma que grita "o carro travou" e manda pro box trocar componente (liveness), e outra que diz "o carro está de pé mas sem pace, tira ele da briga até resgatar" sem chamar pro reparo (readiness). E precisa definir de quanta energia de ERS e cota cada componente tem direito pra ninguém roubar o envelope do outro (requests/limits).

As três probes são a telemetria que o muro lê no fim de cada setor. A liveness reinicia o container quando falha N vezes (autocura); a readiness só tira o pod do balanceador, sem reiniciar; a startup protege apps de boot lento, suspendendo as outras duas enquanto o app não sobe. Confundir liveness com readiness causa reinícios em cascata — é a diferença entre chamar o carro pro box (perde 22s) e só mandar ele ceder posição e recuperar.

Requests e limits são o contrato de recursos. requests é o que o container reserva — o piso garantido que o scheduler usa pra decidir em qual nó o pod cabe (a energia de ERS garantida por volta). limits é o teto que ele não pode passar: estourar limite de memória = OOMKill (o kernel mata o container); estourar limite de CPU = throttling (o kernel atrasa o processo pra mantê-lo na cota). Furar o envelope térmico da PU queima o componente; bater no teto de energia é o derate que segura a potência.

Da combinação de requests e limits nasce a classe de QoS do pod — a ordem de sacrifício quando o box fica sem recurso. Guaranteed (requests == limits em todos os containers) são os últimos sacrificados (o carro do líder); Burstable (requests < limits) usam folga ociosa mas cedem sob contenção; BestEffort (sem requests nem limits) são os primeiros despejados (o carro de teste que ninguém protege).

Um detalhe contraintuitivo (caso real da Buffer): colocar limite de CPU pode piorar a latência. O algoritmo de quota do CFS pode throttlar um pod mesmo com uso médio baixo — pequenos picos consomem a cota de 100ms e o processo congela até o próximo período. A Buffer removeu os limits de CPU de serviços sensíveis a latência e viu o p99 cair de 5 a 10x. Em multi-tenant você quer limits pra isolamento; em serviço latency-sensitive, às vezes menos é mais. E réplicas em paralelo multiplicam disponibilidade (A = MTBF / (MTBF + MTTR)): 2 pods a 99% chegam a ~99,99%, porque o Service age como um "OU lógico" — basta um pod Ready. A equipe com dois carros: se um abandona, o outro pontua.

Os conceitos-chave desta etapa

Liveness probe

"Você travou?" Falhou N vezes seguidas → o kubelet reinicia o container. É a autocura; use pra deadlock, nunca pra lentidão passageira (senão vira ciclo de reinícios). Na pista: o DNF irrecuperável, tira da pista.

Readiness probe

"Está pronto pra tráfego?" Falhou → o pod sai do balanceador (dos endpoints), SEM reiniciar. Volta sozinho quando a dependência se recupera. Na pista: o carro cede posição e recupera, sem ir ao box.

Startup probe

"Já terminou de subir?" Enquanto não passa, suspende liveness e readiness — protege apps de boot lento de morrer durante a inicialização. Na pista: o out-lap de aquecimento; ninguém cobra pace com pneu frio.

Requests e limits

requests = piso reservado (o scheduler usa pra alocar); limits = teto que não pode passar. Estourar memória = OOMKill; estourar CPU = throttling. Energia de ERS garantida vs. cota que não pode furar.

Classes de QoS

Guaranteed (requests==limits) é o último sacrificado; Burstable (requests<limits) cede sob contenção; BestEffort (sem nada) é o primeiro despejado. A ordem de sacrifício sob safety car.

YAML e comandos na prática

apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 3
selector: { matchLabels: { app: api } }
template:
metadata: { labels: { app: api } }
spec:
containers:
- name: api
image: myregistry/api:1.0.0
ports: [ { containerPort: 8080 } ]
startupProbe: # 1) protege o boot lento
httpGet: { path: /healthz, port: 8080 }
failureThreshold: 30 # até 30 x 10s = 5min pra subir
periodSeconds: 10 # checa a cada 10s
livenessProbe: # 2) travou? reinicia o container
httpGet: { path: /healthz, port: 8080 }
periodSeconds: 10
failureThreshold: 3 # 3 falhas seguidas -> restart
readinessProbe: # 3) pronto? entra/sai do balanceador
httpGet: { path: /ready, port: 8080 }
periodSeconds: 5
resources:
requests: { cpu: 100m, memory: 128Mi } # piso garantido (scheduling)
limits: { memory: 256Mi } # teto de RAM; SEM limit de CPU
# (evita throttling do CFS)

💡 Este Deployment é Burstable (tem requests de CPU/memória, mas limit só de memória). Pra virar Guaranteed, requests teria que ser igual a limits em todos os recursos de todos os containers.

Comandos essenciais desta etapa

$kubectl describe pod <pod> # eventos de probe, OOMKill, reinícios (Liveness probe failed, OOMKilled)
$kubectl top pod / kubectl top node # uso real de CPU/memória vs. requests/limits (precisa do metrics-server)
$kubectl get pod <pod> -o jsonpath='{.status.qosClass}' # mostra a classe de QoS do pod

Mão na massa

Prove na prática que a liveness reinicia um container travado — a sua primeira volta rápida do fim de semana:

  1. Crie um pod cuja liveness aponta pra uma porta que NÃO existe (porta 9999) — ele vai falhar sempre. Use --restart=Always (com --restart=Never o kubelet não reinicia ao falhar a liveness).
  2. Rode kubectl get pod teste-liveness -w e observe a coluna RESTARTS subindo a cada ~15s.
  3. Confirme o motivo: kubectl describe pod teste-liveness | grep -i liveness — veja "Liveness probe failed".
  4. Isso é a autocura: o carro chamado pro box automaticamente porque a telemetria acusou travamento. Limpe com kubectl delete pod teste-liveness.

Dica: Não coloque checagem de banco de dados na liveness probe. Se o banco cair, todas as réplicas falham a liveness ao mesmo tempo, reiniciam juntas e você tem falha em cascata. Cheque dependências na startup (pra não subir sem elas) e degrade graciosamente. É como amarrar a liveness dos dois carros à mesma peça de infra — se aquilo falha, os dois entram no box juntos e você fica sem ninguém na pista.

Anota aí: Liveness reinicia, readiness só tira do tráfego, e todo pod merece pelo menos requests — pod sem request é BestEffort e o primeiro a ser despejado. Liveness é chamar pro box, readiness é ceder posição e recuperar; carro sem energia reservada é o primeiro sacrificado sob safety car.

SETOR 1 NO VERDE! Cruzamos o primeiro parcial e o carro está inteiro, telemetria limpa, recursos sob controle — que largada! Mas agora o traçado se fecha: vem o complexo técnico onde a gente decide EM QUAL box cada carro entra. A estratégia entra em cena. Segura firme que a Etapa 02 é pura curva de precisão.

Etapa 02 complexo técnico da estratégia

Agendamento avançado e comportamento de nós

A pista agora exige cabeça fria. Não basta ter carro rápido: é preciso pôr o carro CERTO no box CERTO e saber quem a equipe sacrifica quando o recurso aperta. É aqui que o engenheiro de estratégia ganha ou perde o domingo. Rádio ligado com o muro!

Você tem um cluster misto: alguns nós têm GPU (caros, para treinar modelos), a maioria é comum. Por padrão o scheduler joga o pod em qualquer nó onde ele caiba — então um pod qualquer pode ocupar o nó de GPU e travar o time de ML, enquanto um job de treino pode cair num nó comum sem GPU e nunca funcionar. Pior: quando um nó fica sem memória, quem o Kubernetes mata primeiro? Se for o pod errado, seu serviço crítico vai junto.

Como funciona

Pense na pit lane. Tem box comum e tem o box especializado — o que tem a manta térmica pros pneus de chuva, o rig de calibração de GPU. Taints são a placa "box reservado, só entra carro autorizado" (repele quem não tem credencial). Affinity é a ordem de estratégia no rádio: "leva esse carro pro box da GPU" (atrai). E eviction é o chefe de mecânicos que, quando falta energia no box, começa a sacrificar o carro de teste antes de mexer no carro do líder do campeonato.

Taints e Tolerations repelem. Um taint é uma marca no nó ("não coloque pods aqui a menos que tolerem X"); a toleration é a marca no pod ("eu sei lidar com X, posso ir"). Os efeitos: NoSchedule (não agenda não-tolerantes, os que já rodam ficam), PreferNoSchedule (evita, mas não é absoluto) e NoExecute (barra novos e expulsa os pods sem toleration que já estavam lá — útil pra esvaziar um nó pra manutenção). Ponto crucial: taint só repele, não atrai. Um pod que tolera o nó de GPU pode acabar num nó comum. Pra garantir que ele vá só pra GPU, combine com affinity.

Node Affinity é a contraparte positiva ("quero ir pra nós com o label X"): required (hard — sem o label, não agenda) vs. preferred (soft, com peso; se não achar, agenda em outro lugar). Já Pod Affinity/Anti-Affinity relaciona pods entre si: affinity aproxima ("coloque o cache perto do front-end"); anti-affinity separa ("não coloque duas réplicas do banco no mesmo nó/zona") — nunca os dois carros no mesmo box. Inter-pod affinity é caro no scheduler acima de centenas de nós; pra espalhamento com garantia numérica, prefira Topology Spread Constraints (maxSkew), que escala melhor.

Eviction sob pressão é a autoproteção do nó. Quando falta memória, disco ou PIDs, o kubelet primeiro tenta limpar (containers mortos, imagens não usadas); se não resolve, despeja pods nesta ordem: primeiro os que excedem suas requests (BestEffort e Burstable estourados), depois por PriorityClass, por último os Guaranteed. Detalhes que salvam: eviction por pressão de nó ignora o PodDisruptionBudget (é emergência); eviction ≠ OOMKill (OOMKill é o cgroup matando um container que passou do limit; eviction é o kubelet retirando pods pra salvar o nó inteiro). Alocar pods com múltiplas restrições é bin packing, NP-difícil — o scheduler usa heurística gulosa (filtra nós → pontua → escolhe): rápida, nunca ótima.

Os conceitos-chave desta etapa

Taints e Tolerations

Taint é a marca no nó ("não coloque pods aqui a menos que tolerem X"); toleration é a marca no pod ("eu lido com X, posso ir"). A placa no box e o crachá do carro. Só repele, não atrai.

Efeitos do taint

NoSchedule (não agenda não-tolerantes), PreferNoSchedule (evita, não é absoluto) e NoExecute (barra novos E expulsa os sem toleration que já estavam — esvazia o nó pra manutenção).

Node Affinity

A contraparte positiva ("quero ir pra nós com o label X"). required (hard: sem o label, não agenda) vs. preferred (soft, com peso). Só a affinity força o carro pro box certo.

Pod (anti-)affinity e Topology Spread

Anti-affinity separa réplicas ("nunca os dois carros no mesmo box"); é caro no scheduler acima de centenas de nós — pra espalhar com garantia numérica, prefira Topology Spread (maxSkew).

Eviction sob pressão

Sem memória/disco/PIDs, o kubelet despeja pods: primeiro os que excedem requests, depois por PriorityClass, por último os Guaranteed. Ignora o PodDisruptionBudget (emergência). ≠ OOMKill (cgroup mata um container).

YAML e comandos na prática

# 1) Marca o nó de GPU (uma vez, via kubectl):
# kubectl taint nodes gpu-node-1 nvidia.com/gpu=true:NoSchedule
apiVersion: v1
kind: Pod
metadata: { name: treino-ml }
spec:
tolerations: # PARTE 1: tolera o taint da GPU (permite entrar)
- key: "nvidia.com/gpu"
operator: "Equal"
value: "true"
effect: "NoSchedule"
affinity:
nodeAffinity: # PARTE 2: E exige ir pra um nó com GPU (força ir)
requiredDuringSchedulingIgnoredDuringExecution: # hard: sem label, não agenda
nodeSelectorTerms:
- matchExpressions:
- key: "accelerator"
operator: In
values: ["nvidia-gpu"]
containers:
- name: treino
image: myregistry/treino:1.0
resources:
limits: { nvidia.com/gpu: 1 } # pede 1 GPU (recurso estendido)

💡 Só a toleration não bastaria (o pod poderia cair num nó comum). É a combinação toleration + nodeAffinity que garante isolamento forte: credencial pra entrar no box de chuva MAIS a ordem de estratégia mandando ir pra lá.

Comandos essenciais desta etapa

$kubectl taint nodes <nó> chave=valor:NoSchedule # aplica taint (adicione - no fim pra remover)
$kubectl label nodes <nó> accelerator=nvidia-gpu # rotula o nó pra affinity
$kubectl describe node <nó> # mostra taints, labels, pods e pressão de recursos
$kubectl get events --field-selector reason=Evicted # lista despejos recentes

Mão na massa

Veja o taint repelindo um pod em tempo real (funciona em minikube/kind de 1 nó):

  1. Descubra o nome do nó: kubectl get nodes.
  2. Aplique o taint: kubectl taint nodes <NOME_DO_NO> demo=sim:NoSchedule — nada sem toleration pode ser agendado.
  3. Suba um pod comum sem toleration: kubectl run repelido --image=nginx.
  4. Ele fica Pending; veja o motivo: kubectl describe pod repelido | grep -A2 Events — "had untolerated taint".
  5. Limpe: remova o taint (note o "-" no fim) com kubectl taint nodes <NOME_DO_NO> demo=sim:NoSchedule- e delete o pod.

Dica: Monitore uso de disco/memória dos nós e alerte em 85%, antes dos ~95% que disparam eviction. Junto com rotação de logs, isso evita o "eviction storm". Prefira reagir manualmente (drenar o nó com kubectl drain) a deixar o kubelet despejar em pânico. É a diferença entre chamar o carro pro box na sua janela planejada e ser forçado a parar sob safety car junto com todo mundo.

Anota aí: Taint repele, affinity atrai — pra reservar um nó de verdade, você precisa dos dois juntos. Credencial de box + ordem de estratégia: um sem o outro não garante que o carro certo vá pro box certo.

SETOR 2 COMPLETO, e que aula de estratégia! Você colocou o carro certo no box certo e já sabe quem a equipe sacrifica sob pressão. Mas agora vem a parte que faz o coração da equipe de pit bater forte: a corrida está em andamento e você PRECISA atualizar o carro sem tirar ninguém da briga. Bem-vindo à arte do pit stop. Etapa 03, LÁ VAMOS NÓS!

Etapa 03 a reta de boxes

Estratégias de atualização e rollouts

Sexta-feira, produção rodando, e você precisa subir a v2 sem que o público perceba. Este é o setor dos nervos de aço: um pit stop mal calculado joga a liderança fora. Aqui a gente aprende a trocar tudo com o carro em movimento. RESPIRA e vem comigo.

Sexta-feira, 16h. Você precisa subir a v2 da API. Se derrubar tudo de uma vez pra subir a nova, o serviço fica fora do ar no meio do expediente. Se subir errado e não tiver como voltar rápido, o fim de semana vira plantão. Pior ainda: se a v2 for incompatível com a v1 (por causa de uma migração de banco), deixar as duas rodando ao mesmo tempo corrompe dados.

Como funciona

É o dilema do pit stop numa corrida longa. RollingUpdate é trocar o setup dos carros um pit stop de cada vez, com o resto da equipe ainda pontuando na pista (a operação nunca para). Recreate é a bandeira vermelha: chama todos ao box de uma vez, para tudo, mas garante que nenhum carro roda com meia-versão de peça — nunca há mistura de spec velha e nova em pista ao mesmo tempo.

RollingUpdate (padrão) dá zero downtime: substitui os pods aos poucos, criando um novo ReplicaSet e reduzindo o antigo proporcionalmente. Dois parâmetros controlam o ritmo. maxUnavailable é o freio — quantos pods podem ficar indisponíveis durante a troca (padrão 25%), garantindo o piso de disponibilidade. maxSurge é a potência — quantos pods a mais que o desejado podem existir temporariamente (padrão 25%), os T-cars que aceleram a rodada de trocas. Com N réplicas e os padrões, durante o rollout você tem no mínimo 75% disponíveis e no máximo 125% rodando. Não é permitido maxUnavailable e maxSurge ambos em zero (senão a fila de troca trava).

Recreate garante consistência total, com downtime: derruba tudo da v1 antes de subir a v2. Garante que as duas versões nunca coexistem — necessário quando v1 e v2 são incompatíveis (migração de banco que quebra a versão antiga). O trade-off do tuning: maxUnavailable alto acelera mas comprime a capacidade (a latência explode); maxSurge alto acelera mas dobra pods → dobra conexões → pode saturar o banco. Duas receitas: disponibilidade-primeiro (maxUnavailable: 0, maxSurge generoso) ou tempo-mínimo sob orçamento (satura o pico e a degradação mínima aceitável).

Ferramentas de garantia: minReadySeconds (o pod precisa ficar Ready por X segundos antes de contar como disponível); progressDeadlineSeconds (padrão 600s — se o rollout empaca, marca ProgressDeadlineExceeded, mas o Deployment NÃO faz rollback sozinho: quem decide voltar é você); revisionHistoryLimit (padrão 10 — quantas revisões guardar pra rollback). O rollout só dispara com mudança em .spec.template — só escalar réplicas não conta. Estratégias mais sofisticadas (Blue/Green, Canary) não são nativas do Deployment; controladores como Argo Rollouts e Flagger as adicionam com gates de promoção automáticos.

Os conceitos-chave desta etapa

RollingUpdate (padrão)

Substitui pods aos poucos mantendo o serviço no ar: cria um novo ReplicaSet e reduz o antigo proporcionalmente. Zero downtime — troca o setup carro a carro enquanto os outros marcam tempo.

maxUnavailable (o freio)

Quantos pods podem ficar indisponíveis durante a troca. Garante o piso de disponibilidade. Padrão 25%. Quantos carros você aceita no box sem esvaziar a pista.

maxSurge (a potência)

Quantos pods a mais que o desejado podem existir temporariamente. Acelera criando extras (os T-cars). Padrão 25%. Proibido zerar os dois juntos (a fila de troca trava).

Recreate

Derruba TUDO da v1 antes de subir a v2. Causa downtime, mas garante que as duas versões nunca coexistem — necessário quando v1 e v2 são incompatíveis (migração de banco). A bandeira vermelha.

Garantias do rollout

minReadySeconds (só conta como pronto após X s estável), progressDeadlineSeconds (detecta o travamento, mas NÃO faz rollback sozinho), revisionHistoryLimit (revisões guardadas). Rollout só dispara com mudança em .spec.template.

YAML e comandos na prática

apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
annotations:
kubernetes.io/change-cause: "Rollout v2.0.0" # aparece no rollout history
spec:
replicas: 10
minReadySeconds: 30 # pod só "vale" após 30s estável Ready
progressDeadlineSeconds: 600 # se travar por 10min, marca ProgressDeadlineExceeded
revisionHistoryLimit: 10 # guarda 10 revisões pra rollback
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0 # FREIO: nunca reduz a capacidade mínima
maxSurge: "50%" # POTÊNCIA: cria até 5 pods extras pra acelerar
selector: { matchLabels: { app: myapp } }
template:
metadata: { labels: { app: myapp } }
spec:
containers:
- name: app
image: myregistry/myapp:2.0.0
readinessProbe: # sem readiness, o rolling "mente" que está pronto
httpGet: { path: /healthz, port: 8080 }

Comandos essenciais desta etapa

$kubectl rollout status deployment/myapp # acompanha o rollout (sai com código ≠ 0 se estourar o deadline)
$kubectl rollout history deployment/myapp # lista as revisões (com a change-cause)
$kubectl rollout undo deployment/myapp # reverte pra revisão anterior (--to-revision=N pra uma específica)
$kubectl rollout pause/resume deployment/myapp # congela/retoma no meio (útil pra canário manual)

Mão na massa

Faça um rollout e um rollback do zero e observe os ReplicaSets:

  1. Crie e espere ficar pronto: kubectl create deployment web --image=nginx:1.25 --replicas=3 e kubectl rollout status deployment/web.
  2. Atualize a imagem (dispara o rollout porque muda o template): kubectl set image deployment/web nginx=nginx:1.27 e acompanhe com rollout status.
  3. Veja os DOIS ReplicaSets (o antigo zerado, o novo com 3): kubectl get rs -l app=web.
  4. Reverta e confirme que voltou pra 1.25: kubectl rollout undo deployment/web e kubectl describe deployment web | grep Image.
  5. Limpe: kubectl delete deployment web.

Dica: Sempre preencha kubernetes.io/change-cause (ou use --record). Sem ela, o rollout history fica cego e você não sabe pra qual revisão voltar quando o incidente chegar às 3h da manhã. É o seu log de setup: sem anotar "o que mudei e por quê", quando o carro ficar ruim você vai adivinhar no escuro no meio da corrida.

Anota aí: RollingUpdate = zero-downtime com freio (maxUnavailable) e potência (maxSurge); o undo é instantâneo porque o ReplicaSet antigo continua guardado, só zerado. Igual ao setup da classificação que fica salvo — voltar pra ele é recarregar, não remontar.

QUE PIT STOP, SENHORAS E SENHORES! Setor 3 no verde, o carro trocou de setup em movimento e ainda temos o botão de voltar atrás na mão. Mas agora a gente sobe de nível na organização da equipe: como montar TODOS os carros a partir da mesma fôrma, mudando só a folha de setup? O Helm assume o comando. Etapa 04!

Etapa 04 os bastidores da fábrica

Gerenciador de pacotes com Helm Charts

Longe das câmeras, é na fábrica que o campeonato é construído. Mesmo chassi o ano inteiro, folha de setup diferente a cada circuito. Chega de copiar e colar YAML e rezar pra não esquecer nada. O Helm é o apt do Kubernetes e vai organizar sua garagem. Vem pra oficina!

Você tem dev, staging e prod. Cada ambiente precisa dos mesmos ~15 manifestos YAML, mudando só réplicas, tag de imagem e recursos. Você copia e cola tudo três vezes. Aí um dia altera um label no Deployment de prod, esquece de replicar em staging, e passa a semana caçando por que "no staging funciona e em prod não". Isso viola DRY e é uma fábrica de erros.

Como funciona

O Helm é o apt/yum do Kubernetes. Pensa na fôrma de montagem do carro: o chassi, a filosofia de suspensão, a arquitetura aero são a mesma base o ano inteiro. O que muda de circuito pra circuito é a folha de setup — nível de asa em Monza (baixa) vs. Mônaco (alta), pressão de pneu, mapa de motor. Você não redesenha o carro em cada GP; troca só os ingredientes que variam (os values) na mesma fôrma (o template).

Anatomia de um Chart: Chart.yaml são os metadados — version é a versão do chart e appVersion é a versão da aplicação empacotada (ambos SemVer; separá-los ajuda a governança). values.yaml são os valores padrão configuráveis (réplicas, imagem, porta, recursos) — tudo que varia por ambiente mora aqui. templates/ são os manifestos como templates Go + funções Sprig, onde {{ .Values.replicaCount }} insere o valor e helpers em _helpers.tpl evitam repetir lógica.

A sacada teórica: um template Helm é uma função pura T(valores) → YAML — mesmos inputs, mesma saída, previsível e testável (helm template renderiza sem aplicar). É o simulador: você joga a folha de setup e ele te devolve exatamente como o carro vai ficar, sem tocar na pista. Isso encarna dois pilares de Infraestrutura como Código: idempotência (aplicar N vezes tem o mesmo efeito que aplicar uma) e imutabilidade (não muta o objeto existente, cria uma nova versão e mantém o histórico — o que torna o rollback confiável).

Ciclo de vida: helm install (cria e registra a release num Secret) → helm upgrade (calcula o diff e aplica só as mudanças) → helm rollback (volta pra uma revisão salva). O Helm 3 não tem servidor central (Tiller): o cliente usa direto as credenciais do kubectl, então o RBAC do usuário limita o que o Helm pode fazer. Em escala, charts seguem SemVer, são distribuídos por repositórios (Artifact Hub, privados), podem ser assinados digitalmente (GPG + .prov) pra proteger a supply chain, e devem embutir RBAC de menor privilégio. Combinado com GitOps (ArgoCD/Flux), cada release vira um commit auditável.

Os conceitos-chave desta etapa

Chart.yaml

Metadados. version é a versão do chart (a fôrma); appVersion é a versão da aplicação empacotada. Ambos SemVer — separá-los evolui o chart sem trocar o app.

values.yaml

Os valores padrão configuráveis (réplicas, imagem, porta, recursos). Tudo que varia por ambiente mora aqui. A folha de setup por circuito.

templates/

Os manifestos como templates Go + funções Sprig. {{ .Values.replicaCount }} insere o valor; helpers em _helpers.tpl evitam repetição. A fôrma que recebe os números da folha de setup.

Função pura

Um template é T(valores) → YAML: mesmos inputs, mesma saída. helm template renderiza sem aplicar (o simulador). Sustenta idempotência (aplicar N vezes = aplicar 1) e imutabilidade (nova versão, histórico preservado).

Helm 3 sem Tiller

Não há servidor central: o cliente usa direto as credenciais do kubectl, então o RBAC do usuário limita o que o Helm pode fazer. O crachá de quem opera define o que a fôrma monta.

YAML e comandos na prática

# Chart.yaml — a identidade do pacote
apiVersion: v2
name: myapp
version: 0.1.0 # versão do CHART (a fôrma)
appVersion: "1.0.0" # versão da APLICAÇÃO empacotada (o bolo)
type: application

Comandos essenciais desta etapa

$helm create <chart> # gera a estrutura básica
$helm template ./chart # renderiza o YAML sem aplicar (dry-run local; prova que é função pura)
$helm install <release> ./chart -n <ns> # instala
$helm upgrade <release> ./chart --set image.tag=1.1.0 --atomic # atualiza (--atomic reverte tudo se falhar)
$helm history <release> / helm rollback <release> <rev> # histórico e reversão
$helm lint ./chart # valida boas práticas (a inspeção técnica antes de ir pra pista)

Mão na massa

Veja a "função pura" com os próprios olhos, sem tocar em cluster nenhum:

  1. Crie um chart de exemplo: helm create demo.
  2. Renderize com o valor padrão: helm template demo | grep -i replicas (repare em replicas: 1).
  3. Renderize mudando SÓ o input: helm template demo --set replicaCount=4 | grep -i replicas — a saída muda de forma previsível.
  4. Valide o chart: helm lint demo. Você testou tudo antes de aplicar em cluster nenhum — rodou no simulador antes de mandar o carro pra pista.

Dica: Rode helm diff upgrade (plugin helm-diff) antes de aplicar em produção. Ver exatamente o que vai mudar no cluster evita a surpresa de aplicar template "às cegas" — o medo número um de quem começa com Helm. É comparar a folha de setup nova com a que está no carro antes de mexer no macaco.

Anota aí: Chart = fôrma (templates) + ingredientes (values); como o template é função pura, helm template te deixa conferir o resultado antes de mexer no cluster. Mesma fôrma de carro, folha de setup por circuito, e o simulador te mostra o resultado antes da pista.

SETOR 4 FECHADO com a garagem organizada! A fábrica está redonda, cada carro sai da mesma fôrma com a folha de setup certa. Agora a gente volta pra ADRENALINA do domingo: como lançar a v2 com um botão de desfazer INSTANTÂNEO? O T-car está aquecido nos bastidores. Etapa 05, Blue/Green — segura essa emoção!

Etapa 05 o T-car quente na garagem

Deploy Blue/Green

Imagina ter um carro reserva, mesma equipe, setup novo, motor na temperatura, PRONTO pra assumir num estalar de dedos. Era assim na era dos carros reserva, antes de 2008, quando um piloto trocava pro spare no grid e largava mesmo assim. É esse o espírito do Blue/Green: virar a chave e voltar em segundos. Rádio na garagem!

Você lançou a v2, ela passou em todos os testes, mas em produção — com tráfego e dados reais — apareceu um bug crítico 3 minutos depois. Com RollingUpdate, reverter significa fazer outro rollout completo, que leva minutos enquanto os clientes sofrem. Você queria um "botão de desfazer" instantâneo.

Como funciona

É o T-car (carro reserva) já montado e aquecido nos bastidores. O carro Azul é o que está em pista servindo o público (tráfego); o Verde é o reserva — mesma equipe, setup novo, motor já na temperatura, pronto pra assumir. Quando você valida o Verde, troca de carro na hora: o público passa a ver o Verde imediatamente. Deu ruim? Você volta pro Azul em segundos — ele nunca foi desmontado, ficou quente à espera.

A ideia central: mantenha dois ambientes de produção idênticos. O Blue é a versão atual servindo 100% do tráfego; o Green é a nova versão, pronta e "quente", mas sem tráfego. Quando o Green é validado, você vira a chave e ele passa a servir tudo. No Kubernetes, os dois conjuntos de pods têm labels diferentes (env: blue, env: green) e um único Service aponta pra um deles via selector. A promoção é só um patch no selector do Service — o kube-proxy reconfigura o roteamento em instantes. O rollback é o patch inverso. É uma transição atômica entre dois estados (B → G) com rollback determinístico.

As armadilhas: a consistência de dados/esquema é o calcanhar de Aquiles. Se a Green muda o banco de forma incompatível, reverter pra Blue quebra. A solução é compatibilidade dupla temporária (Martin Fowler): primeiro migrar o banco pra suportar as duas versões, validar, e só então trocar o código. Aqui a analogia do T-car para um pouco: se o carro novo "reescreve o traçado da pista" (o schema do banco), voltar pro carro antigo não adianta — a pista já mudou embaixo dele. Por isso a migração retrocompatível vem antes.

Custo: dois ambientes completos = ~100% de overhead durante a transição (a Etsy chegou a 200% de capacidade). Boa prática: escalar o ambiente inativo pra zero depois de estabilizar. Tempestade de conexões: trocar 100% de uma vez joga toda a carga na Green de repente — caches frios podem gerar um pico; mitigue aquecendo a Green com tráfego sombra antes. Antipadrão clássico: não tratar a Green como produção o tempo todo — se você acumula mudanças esperando o "momento certo", o salto Blue→Green vira gigante e arriscado. Ferramentas: Argo Rollouts suporta blueGreen nativo; Flagger integra com service mesh (também chamado Red/Black na Netflix/Spinnaker).

Os conceitos-chave desta etapa

Dois ambientes idênticos

Blue = versão atual servindo 100% do tráfego; Green = nova versão pronta e "quente", sem tráfego. Valida o Green, vira a chave, ele serve tudo. Deu problema? Vira de volta em segundos.

A virada de chave

Os dois conjuntos têm labels diferentes (env: blue/green); um Service aponta pra um via selector. A promoção é um patch no selector — o kube-proxy reconfigura em instantes. Rollback é o patch inverso.

Consistência de dados (o calcanhar de Aquiles)

Se a Green muda o banco de forma incompatível, reverter pra Blue quebra. Solução: compatibilidade dupla temporária — migrar o banco pra suportar as DUAS versões ANTES de trocar o código.

Custo e tempestade de conexões

Dois ambientes = ~100% de overhead (escale o inativo pra zero após estabilizar). Trocar 100% de uma vez com caches frios gera pico — aqueça a Green com tráfego sombra antes.

Antipadrão

Não tratar a Green como produção o tempo todo: acumular mudanças esperando o "momento certo" torna o salto gigante e arriscado. Blue/Green de verdade = deploy contínuo no ambiente passivo, só sem tráfego.

YAML e comandos na prática

# Service inicialmente apontando pro BLUE
apiVersion: v1
kind: Service
metadata: { name: myapp-service }
spec:
selector: { app: myapp, env: blue } # <- a "chave": troca pra green na promoção
ports: [ { port: 80, targetPort: 8080 } ]

Comandos essenciais desta etapa

$kubectl get endpoints myapp-service # confirma pra quais pods o Service está roteando (valida a troca)
$kubectl patch service ... # executa a virada de chave (patch no selector)
$kubectl scale deployment myapp-blue --replicas=0 # desliga o ambiente antigo após estabilizar

Mão na massa

Monte um Blue/Green de brinquedo e vire a chave observando os endpoints:

  1. Crie dois "ambientes": kubectl create deployment blue --image=nginx:1.25 e green --image=nginx:1.27.
  2. Marque os labels no TEMPLATE do pod (o Service seleciona pods): patch em blue com env=blue e green com env=green (app=bg).
  3. Exponha um Service apontando pro BLUE: kubectl expose deployment blue --name=bg-svc --port=80 --selector=app=bg,env=blue.
  4. Veja qual pod recebe tráfego: kubectl get endpoints bg-svc (o IP do pod blue).
  5. Vire a chave pro GREEN: kubectl patch service bg-svc -p ... env green — e olhe os endpoints mudarem, sem reiniciar nada.
  6. Limpe: kubectl delete deploy blue green; kubectl delete svc bg-svc.

Dica: Antes de virar a chave, garanta que o banco aceita as duas versões. A pergunta é sempre: "se eu precisar reverter pra Blue daqui a 5 minutos, os dados que a Green escreveu vão quebrar a Blue?". Se sim, você não está pronto pro cutover. Só troca pro T-car se você tem certeza de que consegue voltar pro titular sem que a pista tenha mudado embaixo dele.

Anota aí: Blue/Green é "tudo ou nada": a promoção e o rollback são um patch no selector do Service — instantâneos, desde que o banco seja retrocompatível. É trocar de carro na hora, com o antigo quente na garagem — só funciona se a pista (o schema) não mudou embaixo.

SETOR 5 NO VERDE, virada de chave perfeita! Você tem agora o botão de desfazer instantâneo no bolso. Mas o Blue/Green joga 100% do público na v2 de uma vez — e se o bug só aparecer com 1 usuário em 20? Aí a gente não arrisca a dupla toda: manda UM carro testar a peça nova em pista. O canário está no cockpit. Etapa 06!

Etapa 06 um carro testa a peça nova

Deploy Canário

A Ferrari não coloca a asa nova nos dois carros de uma vez. Ela bota no do Leclerc, lê a telemetria por algumas voltas, e só então libera pro Hamilton. Um sente o gás antes de todo mundo — o velho canário na mina. É a estratégia mais cirúrgica do grid, e é a próxima curva. Telemetria ligada!

Blue/Green vira 100% do tráfego de uma vez. Mas e se a v2 tiver um bug que só aparece sob carga real, com 1 usuário em cada 20? Virar tudo de uma vez expõe todo mundo ao mesmo tempo. Você queria testar a v2 com uma fatia pequena e real de usuários, medir, e só então expandir.

Como funciona

É mandar um único carro testar o upgrade em condição de corrida antes de aplicar na dupla. A Ferrari não coloca a asa nova nos dois carros de uma vez — bota no do Leclerc por algumas voltas, lê a telemetria (delta de tempo, temperatura de pneu, degradação), e só então libera pro Hamilton também. Se o delta piorar, tira a peça daquele carro e a base inteira nunca foi contaminada. O nome vem do "canário na mina": uma fração pequena sente o gás tóxico antes de todo mundo.

A ideia central: em vez de expor todo mundo à v2, você manda uma fração pequena de usuários reais pra ela, monitora métricas, e só aumenta se estiver tudo bem. Blue/Green é "tudo ou nada" (100% de uma vez); Canary é progressivo (1% → 5% → 25% → 50% → 100%), validando a cada passo com carga real. As duas versões rodam juntas e você migra o tráfego aos poucos.

Roteamento fino de tráfego: feito por um service mesh (Istio, Linkerd) ou ingress com peso — você define percentuais exatos (ex.: 90% v1 / 10% v2) independentes do número de réplicas, via proxies sidecar. No Istio, um VirtualService divide o tráfego entre subsets (v1/v2) e você ajusta os pesos dinamicamente. O que decide promover ou reverter são métricas em tempo real: taxa de erro (5xx), latência (p95, p99), uso de recursos, métricas de negócio. Isso fecha um laço de realimentação automatizável: se o erro do canário < threshold, aumenta o peso; se viola, rollback imediato pra v1.

O vocabulário que vale conhecer (aprofundamento teórico): o canário é um teste A/B contínuo, um experimento estatístico entre v1 (controle) e v2 — testado com significância (α = 0,05) via teste z, qui-quadrado ou Mann-Whitney (o Kayenta da Netflix usa Mann-Whitney). Testes sequenciais (SPRT de Wald) param cedo quando há evidência suficiente. Teoria de filas: mesmo 10% de tráfego pode ter latência ruim se aqueles 10% quase saturam os pods v2 (W = 1/(μ−λ) explode perto da saturação). Multi-armed bandits (Thompson Sampling) alocam tráfego adaptativamente. Casos reais: a Shopify manda ~5% por ~10min com análise automática; a Netflix automatizou tudo com Kayenta; o Nubank usa feature flags + canary pra centenas de deploys/dia.

Os conceitos-chave desta etapa

Rollout progressivo

Em vez de expor todo mundo, manda uma fração pequena de usuários reais pra v2, monitora e só aumenta se estiver tudo bem (1% → 5% → 25% → 50% → 100%). As duas versões rodam juntas.

Canary vs Blue/Green

Blue/Green é "tudo ou nada" (100% de uma vez); Canary é progressivo, validando a cada passo com carga real. Liberar a peça de carro em carro, medindo a cada stint.

Roteamento fino por peso

Feito por service mesh (Istio, Linkerd) ou ingress com peso: percentuais exatos (ex.: 90/10) independentes do número de réplicas, via proxies sidecar. O engenheiro dosa a fração exata.

Decisão orientada a dados

O que decide promover ou reverter são métricas em tempo real: taxa de erro (5xx), latência (p95/p99), recursos, negócio. Fecha um laço: erro < threshold sobe o peso; violou, rollback imediato.

SLO gate

Amarre o canário aos SLOs, não a métricas arbitrárias. Ferramentas como o Kayenta (Netflix) e o Argo Rollouts aplicam a análise estatística por você. Defina o delta-limite ANTES de mandar o carro pra pista.

YAML e comandos na prática

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata: { name: myapp }
spec:
hosts: [ myapp ]
http:
- route:
- destination: { host: myapp, subset: v1 }
weight: 90 # estável recebe 90%
- destination: { host: myapp, subset: v2 }
weight: 10 # canário recebe 10% — sobe gradualmente

Comandos essenciais desta etapa

$kubectl -n istio-system get virtualservice # inspeciona as regras de tráfego
$kubectl argo rollouts get rollout myapp --watch # acompanha o canário (plugin do Argo Rollouts)
$kubectl argo rollouts promote myapp / abort myapp # promove ou aborta manualmente

Mão na massa

Sem Istio instalado, você consegue aproximar o efeito canário pela razão de réplicas atrás do mesmo Service (o balanceamento fica proporcional ao número de pods):

  1. Crie v1 com 9 réplicas e v2 com 1 (~10% do tráfego cai na v2): kubectl create deployment app-v1 --image=nginx:1.25 --replicas=9 e app-v2 --image=nginx:1.27 --replicas=1.
  2. Dê o mesmo label app=canary aos dois, inclusive no template do pod (via patch).
  3. Exponha um Service que cobre AMBAS as versões: kubectl expose deployment app-v1 --name=canary-svc --port=80 --selector=app=canary.
  4. Confirme 10 endpoints (9 v1 + 1 v2): kubectl get endpoints canary-svc -o wide.
  5. "Promova" ajustando réplicas: kubectl scale deployment app-v2 --replicas=3 e app-v1 --replicas=1.
  6. Limpe: kubectl delete deploy app-v1 app-v2; kubectl delete svc canary-svc.

Dica: Amarre o canário aos seus SLOs, não a métricas arbitrárias. Se o SLO de erro é 0,1%, o canário deve frear automaticamente quando a v2 violar isso. Assim o monitoramento de produção e o de deploy viram a mesma coisa — e promover deixa de ser "achismo". Defina o delta-limite antes: "se passar de +0,3s por volta ou 110°C de pneu, aborta" — número claro, decisão automática.

Anota aí: Canary é rollout progressivo com gate de métricas a cada passo; o peso ideal vem de service mesh (independe de réplicas), e a decisão de promover deve nascer do seu SLO. Um carro testa a peça nova por umas voltas, a telemetria decide, e só então a equipe toda recebe.

SETOR 6 CONCLUÍDO, canário aprovado! Você já sabe lançar sem medo, do jeito mais cirúrgico do grid. Chegamos à metade final da prova, e agora a pergunta muda de escala: e quando não tem BOX suficiente pros carros que chegam? Quem monta a garagem sob demanda? A logística assume o controle. Etapa 07, acelera na reta longa!

Etapa 07 potência de infraestrutura sob demanda

Escalabilidade de nós com Karpenter

Numa promoção relâmpago, o HPA pede 20 pods e... não tem box pra ninguém. Os carros ficam na fila da pit lane. É como um fim de semana back-to-back onde a equipe precisa montar a estrutura na hora, no lugar certo, com o material mais barato disponível. O Karpenter é o gerente de logística mais rápido do paddock. Pé no fundo!

Até agora você escalou pods. Mas numa promoção relâmpago o HPA pede 20 novos pods e... não há nó onde colocá-los. Os pods ficam Pending e o serviço não aguenta a demanda. O Cluster Autoscaler tradicional resolveria, mas é lento e depende de grupos de nós fixos (ASGs) pré-configurados por tipo/zona — uma dor de manter, e ele costuma acionar um nó gigante pra um pod pequeno, desperdiçando dinheiro.

Como funciona

É a diferença entre dois jeitos de montar box na pit lane. O Cluster Autoscaler é ter contratos fixos de box de um tamanho só, reservados com meses de antecedência — chegou um carro pequeno, você é obrigado a abrir um box gigante, pagando pelo espaço vazio. O Karpenter é o gerente de logística que olha exatamente quantos carros e de que tamanho chegaram e monta na hora o box do tamanho certo, escolhendo até o material mais barato disponível, e desmonta assim que esvazia.

Karpenter = autoscaling de nós just-in-time. Criado pela AWS (open source, hoje multicloud), provisiona nós sob medida pros pods pendentes, sem grupos fixos: mais rápido (segundos a <1min), escolhe a instância ótima (inclusive Spot) e remove nós ociosos sozinho. Empresas relatam até 30% de economia. O fluxo: pods ficam unschedulable (o scheduler não achou nó) → o Karpenter observa → calcula qual instância acomoda melhor esses pods → provisiona → os pods são agendados. No sentido inverso, ele identifica nós subutilizados e consolida: move os pods pra outros nós e remove o ocioso, ou troca um On-Demand caro por um Spot mais barato.

Configuração por NodePools (antes chamados Provisioners): definem as regras dos nós que o Karpenter pode lançar — tipos de instância, zonas, Spot vs On-Demand, limites. Recomendação: restrinja o mínimo possível, pra dar liberdade de otimização ao Karpenter. Por que ele é esperto: o problema é bin packing multidimensional (NP-difícil); ele usa heurística gulosa (filtra tipos que não servem, ordena por custo/adequação, faz algo próximo de First-Fit Decreasing). Diferença-chave: o Cluster Autoscaler preserva homogeneidade dentro do grupo (pod pequeno pode acionar nó grande); o Karpenter faz right-sizing — nó pequeno pra pod pequeno.

Spot e custo: instâncias Spot custam ~70-90% menos, mas podem ser retiradas com aviso curto (2min na AWS). São o material emprestado que o fornecedor pode pedir de volta a qualquer hora — barato, mas você precisa de plano B. O Karpenter trata isso com interrupção proativa: detecta o aviso, marca o nó como terminating, aplica taint e drena os pods pra outro lugar antes do corte. E respeita PodDisruptionBudgets — nunca consolida/termina nós se isso violar o budget. Boas práticas: PDB pra controlar quantos pods caem juntos, Topology Spread pra distribuir réplicas, e NodePools separados pra cargas críticas (On-Demand) vs. tolerantes (Spot).

Os conceitos-chave desta etapa

Autoscaling de nós just-in-time

Provisiona nós sob medida pros pods pendentes, SEM grupos fixos: rápido (segundos a <1min), escolhe a instância ótima (inclusive Spot) e remove nós ociosos sozinho. Até ~30% de economia.

O fluxo

Pods ficam unschedulable → o Karpenter observa → calcula a instância que acomoda melhor → provisiona → os pods são agendados. No inverso, consolida nós subutilizados: move pods e remove o ocioso.

NodePools

Definem as regras dos nós que o Karpenter pode lançar (tipos, zonas, Spot vs On-Demand, limites). Recomendação: restrinja o mínimo possível, pra dar liberdade de otimização.

Right-sizing vs Cluster Autoscaler

O CA preserva homogeneidade do grupo (pod pequeno pode acionar nó grande); o Karpenter faz right-sizing — nó pequeno pra pod pequeno. É bin packing NP-difícil resolvido por heurística gulosa (First-Fit Decreasing).

Spot e PodDisruptionBudget

Spot custa ~70-90% menos mas pode ser retirado com aviso curto (2min). O Karpenter faz interrupção proativa (drena antes do corte) e respeita o PDB — nunca tira réplicas demais juntas.

YAML e comandos na prática

apiVersion: karpenter.sh/v1
kind: NodePool
metadata: { name: default }
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot", "on-demand"] # pode usar spot; cai pra on-demand se faltar
- key: kubernetes.io/arch
operator: In
values: ["amd64"] # restringe arquitetura
nodeClassRef: { name: default } # aponta pra config de infra (AMI, subnets...)
limits: { cpu: "1000", memory: 1000Gi } # teto de custo: nunca passa disto
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized # consolida nós vazios OU subutilizados
consolidateAfter: 30s # histerese: espera 30s antes de agir

Comandos essenciais desta etapa

$kubectl get nodeclaims # lista os nós que o Karpenter provisionou (os boxes montados sob medida)
$kubectl get pods --field-selector=status.phase=Pending # vê os pods que disparam o provisionamento (os carros esperando box)
$kubectl describe nodepool default # inspeciona as regras e limites
$kubectl get nodes -L karpenter.sh/capacity-type # mostra quais nós são Spot vs On-Demand

Mão na massa

Karpenter roda em nuvem, mas você pode ver o gatilho que ele observa (o pod Pending por falta de nó) em qualquer cluster:

  1. Peça mais CPU do que qualquer nó tem: kubectl create deployment faminto --image=nginx e kubectl set resources deployment faminto --requests=cpu=100.
  2. O pod fica Pending (não há nó que comporte a request): kubectl get pods -l app=faminto.
  3. Veja o motivo — o sinal que o Karpenter escuta: kubectl describe pod -l app=faminto | grep -A3 Events → "Insufficient cpu" (FailedScheduling).
  4. Limpe: kubectl delete deployment faminto.

Dica: Rode cargas stateless e tolerantes a interrupção em Spot, e reserve On-Demand só pro que é crítico/stateful — em NodePools separados. Combine com PodDisruptionBudget e Topology Spread pra que uma interrupção de Spot nunca derrube réplicas demais de um serviço ao mesmo tempo. Material emprestado (Spot) só nos componentes que você aguenta perder; no carro do líder, só material próprio garantido.

Anota aí: Karpenter provisiona o nó certo just-in-time reagindo a pods Pending, faz right-sizing e economiza com Spot — sempre respeitando o PodDisruptionBudget. É o gerente de logística montando box na medida do carro, com material barato onde dá, sem nunca esvaziar a equipe de uma vez.

SETOR 7 NO VERDE, que ultrapassagem na reta! Você resolveu a escala dos BOXES. Agora falta o outro lado da moeda: dimensionar a EQUIPE DE PIT pela fila de carros chegando — e recolher todo mundo a zero na calmaria da madrugada. Escala por evento, não por esforço. O KEDA entra em cena. Etapa 08!

Etapa 08 escala orientada a evento

Escalabilidade de aplicações com KEDA

Sem carro entrando no box, zero mecânico parado no macaco recebendo à toa. Chegou uma fila de 40 pedidos? Abre 4 estações na hora. Esvaziou? Recolhe todo mundo. O que dispara não é o suor de um mecânico — é quantos carros estão na fila. Escala orientada a evento. Vamos ao KEDA!

Você tem workers que consomem pedidos de uma fila. O HPA padrão só sabe escalar por CPU/memória — mas o seu gargalo não é CPU, é o tamanho da fila de pedidos. Pior: de madrugada a fila fica vazia, e mesmo assim você paga por 3 pods parados sem nada pra fazer. Você queria escalar pelo tamanho da fila e cair até zero quando não há trabalho.

Como funciona

É a equipe de pit stop dimensionada pela fila de carros chegando. Sem carro entrando no box? Zero mecânicos parados no macaco (a equipe não paga gente à toa). Chegou um carro? Um conjunto de mecânicos assume na hora. Fila de 40 pedidos e cada estação atende de 10 em 10? Abre 4 estações. Esvaziou e ficou vazia um tempo? Recolhe todos. O que dispara não é o esforço de um mecânico (CPU) — é quantos carros estão na fila (o evento).

KEDA = autoscaling orientado a eventos. Escala com base em eventos externos (filas RabbitMQ/SQS/Kafka, métricas Prometheus, cron, etc.) e permite scale-to-zero — nenhum pod consumindo recursos quando a fila está vazia. Ele complementa o HPA, não substitui. Como funciona: você cria um ScaledObject (o CRD central) que aponta pra um Deployment alvo e define um trigger (ex.: fila RabbitMQ, 1 pod a cada 10 mensagens). O KEDA cria um HPA por trás. Fila vazia → mantém o Deployment em 0 pods; chega mensagem → ativa 1 pod imediatamente; fila cresce → o HPA calcula os pods (40 mensagens = 4 pods); fila esvazia pelo cooldownPeriod → volta a zero. Só o KEDA tem autoridade pra ir a zero; o HPA sozinho vai de 1 a N (minReplicas ≥ 1).

A teoria (teoria de filas): modele eventos chegando a taxa λ e cada pod processando a μ; com c pods, a capacidade é c·μ. Condição de estabilidade: λ < c·μ (senão a fila cresce sem limite — nem autoscaling salva). A Lei de Little (L = λ·W) relaciona backlog médio L, taxa λ e tempo de resposta W: se o backlog cresce, ou as chegadas estão rápidas demais, ou faltam pods. O autoscaler ideal controla L pra manter W dentro do SLO.

O desafio: oscilação/jitter (subir e descer pods repetidamente), causado por atraso de realimentação e thresholds estáticos. Mitigações do KEDA/HPA: histerese (~10% — só escala se passar de 110% ou cair abaixo de 90% do alvo); janelas de estabilização e cooldownPeriod; banda morta (limiares distintos de subida e descida — escala up em 100 msgs mas só down abaixo de 20). O KEDA é agnóstico de nuvem (AKS, EKS, GKE, on-prem) e extensível via scalers externos gRPC. Caso típico: e-commerce escalando workers de pedidos por fila durante promoções relâmpago, voltando a zero na madrugada.

Os conceitos-chave desta etapa

Autoscaling orientado a eventos

Escala por eventos externos (filas RabbitMQ/SQS/Kafka, Prometheus, cron) e permite scale-to-zero — nenhum pod quando a fila está vazia. Complementa o HPA, não substitui.

ScaledObject e trigger

O CRD central aponta pra um Deployment e define um trigger (ex.: 1 pod a cada 10 mensagens). O KEDA cria um HPA por trás. 40 mensagens = 4 pods; fila vazia pelo cooldownPeriod → volta a zero.

Só o KEDA vai a zero

O HPA sozinho vai de 1 a N (minReplicas ≥ 1); só o KEDA tem autoridade pra ir a zero. O HPA sempre deixa um mecânico de plantão; só o KEDA fecha o box quando não vem carro.

Condição de estabilidade

Modele chegadas a λ e processamento a μ com c pods: capacidade c·μ. Estável só se λ < c·μ — senão a fila cresce sem limite e nenhum autoscaling salva. Lei de Little: L = λ·W.

Oscilação (jitter)

Subir e descer pods repetidamente. Mitigações: histerese (~10%: sobe acima de 110%, desce abaixo de 90%), janelas de estabilização/cooldownPeriod e banda morta (limiares distintos de subida e descida).

YAML e comandos na prática

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata: { name: worker-scaler }
spec:
scaleTargetRef: { name: meu-deployment } # o Deployment que será escalado
minReplicaCount: 0 # scale-to-zero: zero pod quando não há trabalho
maxReplicaCount: 5 # teto de segurança
cooldownPeriod: 60 # espera 60s de fila vazia antes de zerar
pollingInterval: 15 # checa a fila a cada 15s
triggers:
- type: rabbitmq
metadata:
queueName: minha-fila
protocol: amqp
mode: QueueLength
value: "10" # alvo: 1 pod a cada 10 mensagens na fila

Comandos essenciais desta etapa

$kubectl get scaledobject # lista os ScaledObjects e o HPA gerado por trás
$kubectl describe hpa <nome> # mostra os eventos de escalonamento (Scaled up to 4 replicas...)
$kubectl get deployment meu-deployment -w # observa as réplicas subindo/descendo em tempo real

Mão na massa

Você pode testar o KEDA sem fila externa, usando o trigger cron (que não depende de nenhum sistema) pra ver scale-to-zero e reativação:

  1. Pré-requisito: KEDA instalado (helm install keda kedacore/keda -n keda --create-namespace).
  2. Crie um Deployment qualquer: kubectl create deployment cron-demo --image=nginx.
  3. Aplique um ScaledObject com trigger cron: minReplicaCount 0, maxReplicaCount 3, sobe no minuto 0 e desce no minuto 30 de cada hora (timezone America/Sao_Paulo).
  4. Observe o Deployment: fora da janela fica em 0; na janela vai pra 3 — kubectl get deployment cron-demo -w.
  5. Limpe: kubectl delete scaledobject cron-demo; kubectl delete deployment cron-demo.

Dica: Ajuste pollingInterval e cooldownPeriod pensando na dinâmica da carga. Cooldown curto demais mata pods bons no meio de um burst intermitente; longo demais desperdiça recursos. E lembre: se λ ≥ c·μ de forma sustentada, nenhum autoscaling resolve — o gargalo é capacidade ou código, não número de pods. Contratar mecânico não conserta um box que atende mais devagar do que os carros chegam.

Anota aí: KEDA escala por eventos (fila, cron, métrica) e é o único que leva a zero; mas se λ ≥ c·μ de forma sustentada, o problema é capacidade, não autoscaler. Equipe de pit dimensionada pela fila de carros, recolhida a zero na calmaria — mas nenhum reforço salva um box que atende mais devagar do que os carros chegam.

SETOR 8 CONCLUÍDO, escala sob domínio total! Boxes e equipe agora respiram junto com a demanda. Falta o último setor — e é o mais implacável. Aqui não se perde corrida na pista: se perde FORA dela, na inspeção técnica. É a chicane final da segurança. Concentração máxima, porque um crachá errado custa o campeonato. Etapa 09, a volta decisiva!

Etapa 09 a chicane final

Segurança no cluster

A FIA não perdoa. Já vimos carros vencerem na pista e serem desclassificados na inspeção por um detalhe de regulamento. No cluster é igual: um container furado com a chave-mestra no bolso entrega o reino inteiro. Zero Trust, crachá nominal, lacre que expira sozinho. É a última curva, e é aqui que os campeões provam que merecem o troféu. FOCO!

Seu cluster tem vários times e serviços. Um deles roda uma dependência com vulnerabilidade e é comprometido. Se aquele pod usava a ServiceAccount default com permissões amplas — ou pior, tinha uma chave AWS estática embutida no container — o invasor acabou de virar dono do reino: lê segredos de todos os namespaces, cria pods de mineração, acessa buckets. Um único container furado não pode custar o cluster inteiro.

Como funciona

É o controle de acesso do paddock da FIA. Cada membro da equipe recebe um crachá que abre só a zona dele — o mecânico de pneu não entra na sala de estratégia, o engenheiro de dados não mexe na PU (ServiceAccount + RBAC de menor privilégio). Ninguém anda com a chave-mestra do autódromo. Os crachás expiram no fim do fim de semana e as áreas se trancam sozinhas (TLS e tokens de curta duração). A filosofia é a mesma do regulamento: Zero Trust — ninguém passa por uma cancela sem credencial verificada, nem que seja o chefe de equipe. Segurança em Kubernetes se apoia em três pilares — identidade, autorização e criptografia.

Identidade — ServiceAccounts: cada aplicação deve ter a sua própria ServiceAccount, nunca a default. Ela dá ao pod um token pra falar com o API Server. Isolar identidades por workload é o que permite delimitar o estrago quando um pod é comprometido — cada função da equipe com o seu crachá nominal, e se um cai em mãos erradas, você sabe exatamente qual zona foi exposta e revoga só ele.

Autorização — RBAC: concede permissões via Roles (namespaced) ou ClusterRoles (cluster-wide), ligadas a sujeitos por RoleBindings. O modelo é um grafo: Subject → RoleBinding → Role → Permissão. Menor privilégio na prática: prefira Role namespaced a ClusterRole; conceda só os verbos necessários (get, list — nunca *) e só os recursos necessários. O RBAC do Kubernetes é estático e aditivo (só soma permissões, não há negações), o que o torna auditável. Pra políticas condicionais ("negar pod sem limits", "só imagem stable"), usa-se OPA/Gatekeeper com a linguagem Rego (Policy as Code).

Identidade federada com a nuvem: aplicações precisam acessar S3, buckets, Key Vault — sem embutir chaves estáticas no container. A solução é federar a ServiceAccount do K8s com uma identidade cloud via OIDC, recebendo tokens de curta duração: IRSA (IAM Roles for Service Accounts) no EKS, Workload Identity no GKE e no AKS. Os incidentes provam o valor: no ataque SCARLETEEL (2023), o dano foi limitado porque a Role do pod tinha escopo restrito; no caso da Tesla (2018), um dashboard sem senha + credenciais AWS amplas num pod levou a cryptojacking. Criptografia — cert-manager: automatiza o ciclo de vida do TLS via CRDs (Issuer/ClusterIssuer emite; Certificate define CN, SANs, duração e renewBefore) — gera o CSR, assina, preenche o Secret e renova sozinho. E a rotação de segredos usa período de convivência: nova e antiga válidas ao mesmo tempo até todos migrarem, depois invalida a antiga.

Os conceitos-chave desta etapa

Identidade — ServiceAccounts

Cada app com a SUA ServiceAccount, nunca a default. Ela dá ao pod um token pra falar com o API Server. Isolar identidades por workload delimita o estrago quando um pod é comprometido. Crachá nominal.

Autorização — RBAC

Permissões via Roles (namespaced) ou ClusterRoles, ligadas por RoleBindings (Subject → RoleBinding → Role → Permissão). Menor privilégio: prefira Role namespaced, só os verbos/recursos necessários, nunca curinga *.

RBAC aditivo e auditável

Estático e aditivo (só soma, não nega), o que o torna auditável: dá pra listar quem pode o quê deterministicamente. Pra políticas condicionais, use OPA/Gatekeeper com Rego (Policy as Code).

Identidade federada (IRSA / Workload Identity)

Acessar S3/buckets/Key Vault sem chaves estáticas: federe a SA com uma identidade cloud via OIDC, recebendo tokens de curta duração. IRSA no EKS, Workload Identity no GKE/AKS. O crachá temporário do fornecedor.

Criptografia — cert-manager

Automatiza o ciclo de vida do TLS via CRDs: Issuer/ClusterIssuer (quem emite) e Certificate (o que emitir, com renewBefore). Gera CSR, assina, preenche o Secret e renova sozinho. Os lacres da FIA que expiram e se renovam.

YAML e comandos na prática

apiVersion: v1
kind: ServiceAccount
metadata:
name: analytics-sa
namespace: analytics
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/S3Reader # IRSA:
# federa esta SA com uma IAM Role
# (token curto, zero chave estática)
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role # namespaced (limita ao namespace analytics), NÃO ClusterRole
metadata: { name: analytics-read, namespace: analytics }
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list"] # só leitura de pods, sem "*"
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get"] # só get de configmaps
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding # amarra a Role ao sujeito (a SA)
metadata: { name: bind-analytics-read, namespace: analytics }
roleRef: { apiGroup: rbac.authorization.k8s.io, kind: Role, name: analytics-read }
subjects:
- kind: ServiceAccount
name: analytics-sa
namespace: analytics

Comandos essenciais desta etapa

$kubectl auth can-i <verbo> <recurso> --as=system:serviceaccount:<ns>:<sa> # testa se uma SA tem uma permissão (auditoria de RBAC)
$kubectl get rolebindings,clusterrolebindings -A -o wide # mapeia quem tem acesso a quê
$kubectl get certificate -A # estado dos certificados gerenciados pelo cert-manager
$kubectl create token <sa> # gera um token de curta duração pra uma ServiceAccount

Mão na massa

Prove o menor privilégio com kubectl auth can-i, sem precisar de nuvem:

  1. Crie namespace, SA e Role de só-leitura de pods: kubectl create namespace lab; kubectl create serviceaccount leitor -n lab; kubectl create role ler-pods --verb=get,list --resource=pods -n lab; kubectl create rolebinding bind-leitor --role=ler-pods --serviceaccount=lab:leitor -n lab.
  2. A SA PODE listar pods? kubectl auth can-i list pods -n lab --as=system:serviceaccount:lab:leitor → yes.
  3. A SA PODE deletar pods? (não concedemos "delete") → no.
  4. A SA PODE ler Secrets? (não concedemos esse recurso) → no.
  5. Limpe: kubectl delete namespace lab. Crachá que só abre uma porta: quem o rouba fica preso naquela sala.

Dica: Use kubectl auth can-i --as=... no CI pra validar que suas ServiceAccounts têm exatamente as permissões esperadas e nada além. Combine com IRSA/Workload Identity de escopo mínimo — assim, mesmo que um container seja comprometido, o invasor fica preso ao que aquela identidade específica pode fazer. Faça a vistoria de credenciais antes do fim de semana começar, não depois do vazamento.

Anota aí: Uma SA por app + Role namespaced de verbos específicos + tokens de curta duração (IRSA/Workload Identity): é isso que transforma "container comprometido" em incidente contido, não em cluster perdido. Crachá nominal, que abre só a zona certa e expira sozinho — o Zero Trust do paddock aplicado ao cluster.

BANDEIRA QUADRICULADA À VISTA! O carro passou na chicane final sem tocar em nada, a vistoria da FIA aprovou, e o troféu é SEU. Nove setores completados de ponta a ponta. Você não é mais passageiro nesse cluster — você é o piloto no comando do muro. Foi uma honra narrar essa corrida!