Teu progresso
0 / 83 módulos0%
Estágio 03 · 03-03
BloqueadoKubernetes é o orquestrador padrão de produção em empresas, e também a fonte mais comum de complexidade não-justificada. Times rodam clusters caros pra projetos que caberiam em Railway. Outros, com necessidade real de K8s, tratam como "Docker Compose com mais YAML", não entendem control loops, declaration vs reality, scheduler decisions, resource pressure, ingress paths.
Este módulo é K8s real: arquitetura (control plane, kubelet, etcd), modelo declarativo, primitives (Pod, Deployment, StatefulSet, Service, Ingress, ConfigMap, Secret), scheduling, autoscaling, observability, helm/kustomize, operators. Você sai sabendo decidir se K8s, e, quando sim, operar com competência.
Sinais que K8s vale:
Sinais que K8s é overkill:
Alternativas razoáveis: Railway, Render, Fly.io, AWS App Runner, Cloud Run. Logística v2 vai pra K8s pra você aprender; em projeto real, decida com base em escala.
Control plane:
Nodes (workers):
Tudo que você cria é objeto declarativo armazenado em etcd. Controllers comparam estado desejado vs atual e reconciliam.
Unidade de scheduling. Geralmente 1 container, podendo ter sidecars.
apiVersion: v1
kind: Pod
metadata:
name: api
spec:
containers:
- name: api
image: ghcr.io/me/logistica-api:v1.2.3
ports:
- containerPort: 3000
resources:
requests: { cpu: 100m, memory: 256Mi }
limits: { cpu: 500m, memory: 512Mi }
Em geral você não cria Pod direto, usa Deployment.
Pods são efêmeros: caem, IP some, novo Pod nasce com IP novo.
Roda N réplicas idênticas com rolling updates.
apiVersion: apps/v1
kind: Deployment
metadata: { name: api }
spec:
replicas: 3
selector:
matchLabels: { app: api }
template:
metadata:
labels: { app: api }
spec:
containers: [...]
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
Strategy:
Controller subjacente do Deployment. Você não mexe direto.
Pra workloads com identidade estável (DBs, brokers).
Diferenças de Deployment:
pod-0, pod-1, pod-2).Em produção, prefer Operators específicos (Postgres Operator, Strimzi pra Kafka) que cuidam de detalhes (failover, replicação, backups). StatefulSet "puro" raramente é o que você quer pra DB.
Garante 1 Pod em cada node (ou subconjunto). Usado pra:
Pra batch processing, migrations, backups.
Pods são efêmeros. Service dá IP virtual estável + DNS pra grupo de Pods (selecionados por label).
apiVersion: v1
kind: Service
metadata: { name: api }
spec:
selector: { app: api }
ports:
- port: 80
targetPort: 3000
type: ClusterIP
Tipos:
DNS interno: api.namespace.svc.cluster.local.
Roteamento HTTP/HTTPS L7 pra Services. Necessita um Ingress Controller (Nginx, Traefik, AWS ALB, Cloud Run). Sem controller, Ingress objects ficam parados.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: logistica
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
tls:
- hosts: [api.logistica.com]
secretName: logistica-tls
rules:
- host: api.logistica.com
http:
paths:
- path: /
pathType: Prefix
backend:
service: { name: api, port: { number: 80 } }
Gateway API é a evolução do Ingress (mais expressivo, role-based). Em 2026, GA e crescente.
apiVersion: v1
kind: Secret
metadata: { name: api-secrets }
type: Opaque
data:
DATABASE_URL: cG9zdGdyZXM6Ly8uLi4= # base64
QoS classes:
Em prod, sempre defina requests realistas. Sem isso, scheduler chuta, e você overcommit ou under-utiliza.
Backend deve ter /healthz (liveness) e /readyz (readiness com checks de DB/Redis).
HPA (Horizontal Pod Autoscaler): scale réplicas baseado em métricas (CPU, memória, custom).
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: { name: api }
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target: { type: Utilization, averageUtilization: 70 }
Custom metrics via KEDA (Kubernetes Event-Driven Autoscaling): scale por queue depth (Kafka, SQS), Redis lists, latency p99, etc. Padrão moderno.
VPA (Vertical Pod Autoscaler): ajusta requests/limits. Combinar com HPA é tricky (conflito). Usado em alguns workloads stateful.
Cluster Autoscaler: adiciona/remove nodes baseado em pending Pods.
Service Mesh (Istio, Linkerd, Cilium Service Mesh) adiciona, sem mexer em código de aplicação:
Modelos de implementação:
| Mesh | Modelo | Overhead | Quando |
|---|---|---|---|
| Istio | Sidecar (Envoy por pod) | ~30-100MB RAM + ~0.5ms latency por hop | Features máximas, multi-cluster, ambient mode (2024+) reduz overhead |
| Linkerd | Sidecar (Rust micro-proxy) | ~10MB RAM, < 1ms p99 | Simplicidade, perf, mTLS sem complexidade |
| Cilium Service Mesh | eBPF kernel-level | ~zero por hop, sem sidecar | Performance crítica, já usa Cilium CNI |
| Istio ambient mode | ztunnel L4 + waypoint L7 sob demanda | Médio | Quem quer Istio mas sem custo de sidecar em todo pod |
Cilium ganhou tração 2024-2026 por eliminar sidecar (eBPF intercepta no kernel). Trade-off: tooling menos maduro, debugging eBPF é mais hardcore.
Decisão:
Cuidado: cada mesh introduz failure mode novo (control plane down ≠ data plane down em mesh maduro, mas debug fica mais profundo). Game day obrigatório antes de prod.
Pod monta via PVC. PV pode sobreviver Pod morte e ser remontado.
Modos de acesso: ReadWriteOnce (1 node), ReadWriteMany (vários nodes, só alguns drivers, EFS), ReadOnlyMany.
Configuration management:
kubectl, overlay-based. Menos abstração, sem Go templates. Convive com Helm em pipelines.Em 2026 muitos projetos preferem Helm pra installs de terceiros (charts oficiais) e Kustomize pra config própria. Outros preferem Helm pra tudo.
Argo CD / Flux: GitOps, repo Git é fonte de verdade; controller syncs cluster com repo. Padrão moderno em times maduros.
CRD (Custom Resource Definition) + Controller. Permite estender K8s com objetos custom. É o pattern que torna K8s extensível além de "container orchestrator", vira plataforma onde stateful systems se autoadministram.
Anatomia de um operator:
PostgresCluster, KafkaTopic).desired_state - current_state = action. Loop infinito..status do recurso.Reconcile loop pseudo-code:
for event := range watch(api, "PostgresCluster") {
desired := event.spec
current := observe(cluster, desired.name)
diff := compare(desired, current)
apply(diff) // create StatefulSet, ConfigMap, Secret, etc.
updateStatus(desired.name, observed)
}
Idempotência é essencial: mesma reconcile rodando 100x deve convergir pra mesmo estado. Sem efeitos colaterais cumulativos.
Quando vale escrever operator próprio:
Quando NÃO escrever:
Ferramentas pra escrever operators:
Operators famosos em produção:
Anti-patterns conhecidos:
ownerReferences, leak ao deletar CR.K8s é poderoso, é caro de manter. Senior real escolhe consciente. Tabela completa de alternativas (ECS, Nomad, Fly.io, Cloud Run, Kamal, etc.) e heurística por tamanho de time vive em framework/03-producao/README.md — ali fica visível antes do aluno entrar no módulo, pra evitar que K8s seja escolhido por default.
Resumo bruto:
Pra aprender: K3s local ou GKE Autopilot/EKS Auto Mode.
Default K8s scheduling parece "just works" mas é frágil em production. Node drain durante upgrade pode tirar 100% das replicas de um service. Pods se acumulam em 1 node (single point of failure). OOMKill cascade derruba services não-related. PriorityClass mal-config = critical pod evicted antes do batch job. Esta seção cobre 5 patterns de resiliência operacional: PDB, topology spread, descheduler, priority classes, eviction tuning — com YAML production-ready.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api-pdb
namespace: prod
spec:
minAvailable: 2 # at least 2 pods up always
# OR maxUnavailable: 1 # at most 1 pod down at a time
selector:
matchLabels:
app: api
unhealthyPodEvictionPolicy: AlwaysAllow # Kubernetes 1.27+
kubectl drain, node upgrade, cluster autoscaler scale-down. PDB blocks if would violate.unhealthyPodEvictionPolicy: AlwaysAllow (1.27+): permite evict de pods unhealthy mesmo se violaria PDB. Sem isso, unhealthy pod travado em terminating bloqueia drain forever.apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 6
selector: { matchLabels: { app: api } }
template:
metadata: { labels: { app: api } }
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector: { matchLabels: { app: api } }
matchLabelKeys: [pod-template-hash] # Kubernetes 1.27+
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector: { matchLabels: { app: api } }
containers: [...]
maxSkew: 1 + zone topologyKey: max diferença de 1 pod entre zones. 6 pods em 3 zones = 2/2/2.whenUnsatisfiable: DoNotSchedule (zone): hard constraint — não schedule se violaria. ScheduleAnyway (hostname): soft, prefer mas não bloqueia.matchLabelKeys: [pod-template-hash] (1.27+): considera só pods da mesma ReplicaSet (rolling update não confunde counting).spec:
template:
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector: { matchLabels: { app: api } }
topologyKey: kubernetes.io/hostname
required: hard rule, scheduler refuses. preferred: soft, weighted preference.apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: critical-prod
value: 100000
globalDefault: false
description: "API + DB clients; never evict before batch"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: best-effort-batch
value: 1000
description: "Batch jobs; first to evict on node pressure"
---
# Apply em deployment
spec:
template:
spec:
priorityClassName: critical-prod
system-cluster-critical (2000000000) e system-node-critical (2000001000) — não use pra workload normal.spec:
containers:
- name: api
resources:
requests: { cpu: 250m, memory: 512Mi }
limits: { cpu: 1, memory: 1Gi }
# descheduler-policy.yaml
apiVersion: descheduler/v1alpha2
kind: DeschedulerPolicy
profiles:
- name: ProfileName
pluginConfig:
- name: DefaultEvictor
args:
evictLocalStoragePods: false
nodeFit: true
- name: RemoveDuplicates
- name: LowNodeUtilization
args:
thresholds: { cpu: 20, memory: 20, pods: 20 }
targetThresholds: { cpu: 50, memory: 50, pods: 50 }
- name: RemovePodsViolatingTopologySpreadConstraint
plugins:
balance:
enabled: [RemoveDuplicates, LowNodeUtilization, RemovePodsViolatingTopologySpreadConstraint]
# kubelet config (per node)
evictionHard:
memory.available: "200Mi"
nodefs.available: "10%"
imagefs.available: "10%"
evictionSoft:
memory.available: "500Mi"
nodefs.available: "15%"
evictionSoftGracePeriod:
memory.available: "1m30s"
evictionMaxPodGracePeriod: 60
terminationGracePeriodSeconds até evictionMaxPodGracePeriod.apiVersion: apps/v1
kind: Deployment
metadata:
name: orders-api
namespace: prod
spec:
replicas: 6
strategy:
rollingUpdate: { maxSurge: 2, maxUnavailable: 0 }
template:
metadata: { labels: { app: orders-api, tier: critical } }
spec:
priorityClassName: critical-prod
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector: { matchLabels: { app: orders-api } }
matchLabelKeys: [pod-template-hash]
containers:
- name: api
image: registry/orders-api:v2.3.1
resources:
requests: { cpu: 500m, memory: 1Gi }
limits: { cpu: 1, memory: 1Gi } # Guaranteed QoS
livenessProbe: { httpGet: { path: /healthz, port: 8080 }, periodSeconds: 10 }
readinessProbe: { httpGet: { path: /ready, port: 8080 }, periodSeconds: 5 }
startupProbe: { httpGet: { path: /healthz, port: 8080 }, failureThreshold: 30, periodSeconds: 2 }
terminationGracePeriodSeconds: 60
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: orders-api
namespace: prod
spec:
minAvailable: 4
selector: { matchLabels: { app: orders-api } }
unhealthyPodEvictionPolicy: AlwaysAllow
minAvailable igual a replicas: drain bloqueia infinito; nenhum pod pode ser evicted. Set replicas - 1 ou maxUnavailable: 1.matchLabelKeys: rolling update tem rolling pods + new pods counted; spread aparece violado constantemente.maxUnavailable: 25% em deployment de 4 replicas: 1 pod evictable, mas se PDB diz minAvailable: 3, drain trava.terminationGracePeriodSeconds: SIGTERM + 30s default; long-running requests truncated. Set baseado em request p99 latency.minAvailable: 1 (ou maxUnavailable: 50%) pra cada deploy multi-replica.kubectl drain --dry-run: simula drain pra ver se PDB bloqueia.kube-no-trouble (kubent): detecta deprecated APIs.kubescape ou polaris: scans de best practices em deployment manifests.chaos-mesh ou litmus: chaos engineering — kill node, inject latency, partition network. Valida defenses funcionam.Cruza com 03-03 §2.14 (autoscaling), 03-03 §2.16 (persistent volumes — PDB protege StatefulSet também), 03-03 §2.18 (operators que gerenciam PDB programaticamente), 03-15 §2.11 (chaos engineering valida resilience), 04-04 §2.x (resilience patterns aplicam stack inteiro).
Stack 2026: kubebuilder 4.2+, controller-runtime 0.18+, K8s 1.30+, Go 1.22+. Operator Pattern (CoreOS, 2016) codifica conhecimento operacional em software; substitui playbooks manuais por reconciliation contínua.
Reconcile(req). Compara desired (Spec) vs actual (cluster state); muta para convergir.Create substituído por CreateOrUpdate ou Get-then-Create.RequeueAfter ou Requeue: true. Não tente "tudo de uma vez"./status separa RBAC e preserva user spec em status updates./scale: integra HPA/kubectl scale com CRD.apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: logisticatenants.logistica.example.com
spec:
group: logistica.example.com
versions:
- name: v1alpha1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
required: [tier, region]
properties:
tier: { type: string, enum: [free, pro, enterprise] }
region: { type: string, enum: [us-east, eu-west, br-sao-paulo] }
replicas: { type: integer, minimum: 1, maximum: 100, default: 3 }
status:
type: object
properties:
phase: { type: string, enum: [Pending, Provisioning, Ready, Failed] }
readyReplicas: { type: integer }
conditions:
type: array
items:
type: object
properties:
type: { type: string }
status: { type: string }
reason: { type: string }
message: { type: string }
lastTransitionTime: { type: string, format: date-time }
subresources:
status: {}
scale:
specReplicasPath: .spec.replicas
statusReplicasPath: .status.readyReplicas
scope: Namespaced
names:
plural: logisticatenants
singular: logisticatenant
kind: LogisticaTenant
shortNames: [ltenant]
kubebuilder init --domain logistica.example.com --repo github.com/logistica/operator + kubebuilder create api --group logistica --version v1alpha1 --kind LogisticaTenant gera scaffolding completo (Makefile, Dockerfile, RBAC, manager).
func (r *LogisticaTenantReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := log.FromContext(ctx)
var tenant logisticav1alpha1.LogisticaTenant
if err := r.Get(ctx, req.NamespacedName, &tenant); err != nil {
if errors.IsNotFound(err) { return ctrl.Result{}, nil } // deleted; nothing to do
return ctrl.Result{}, err
}
// Step 1: ensure Namespace (idempotente: IsAlreadyExists tolerado)
ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "tenant-" + tenant.Name}}
if err := r.Create(ctx, ns); err != nil && !errors.IsAlreadyExists(err) {
return ctrl.Result{}, err
}
// Step 2: ensure Deployment (com OwnerReference para GC)
deploy := r.deploymentFor(&tenant)
if err := ctrl.SetControllerReference(&tenant, deploy, r.Scheme); err != nil {
return ctrl.Result{}, err
}
if err := r.Create(ctx, deploy); err != nil && !errors.IsAlreadyExists(err) {
return ctrl.Result{}, err
}
// Step 3: update Status via subresource (não toca Spec)
tenant.Status.Phase = "Ready"
tenant.Status.ReadyReplicas = deploy.Status.ReadyReplicas
if err := r.Status().Update(ctx, &tenant); err != nil {
return ctrl.Result{}, err
}
log.Info("reconciled", "tenant", tenant.Name, "phase", tenant.Status.Phase)
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
}
func (r *LogisticaTenantReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&logisticav1alpha1.LogisticaTenant{}).
Owns(&appsv1.Deployment{}).
Owns(&corev1.Service{}).
WithEventFilter(predicate.GenerationChangedPredicate{}). // ignora status-only updates
Complete(r)
}
ctrl.SetControllerReference(parent, child, scheme) adiciona OwnerReference com controller: true.kubectl delete logisticatenant my-tenant derruba Namespace, Deployment, Service, ConfigMap em sequência sem código adicional.Owns, Watches, predicatesFor(&LogisticaTenant{}): primary CRD; reconcile triggered em qualquer mudança.Owns(&Deployment{}): watch resources criados pelo controller; mudanças em status do Deployment re-triggera reconcile do tenant.Watches(&corev1.ConfigMap{}, handler.EnqueueRequestsFromMapFunc(...)): resources não-owned (ex: shared config); função custom mapeia evento → list de Requests.predicate.GenerationChangedPredicate{}: filtra eventos onde só .metadata.resourceVersion muda (status writes); reconcile só em Spec changes. Reduz CPU 10x+.Convenção K8s API (metav1.Condition):
Available — operator atingiu desired state.Progressing — convergindo (provisioning, scaling).Degraded — falha parcial; service ainda up mas deteriorado.Cada condition: type, status (True/False/Unknown), reason (CamelCase code), message (human-readable), lastTransitionTime. Tooling genérico (kubectl, ArgoCD, OpenShift console) renderiza health automaticamente.
helm install logistica-operator; mais simples para users já em Helm.kubectl apply -f operator.yaml; zero overhead, sem packaging extras.LogisticaTenant operatorLogisticaTenant {tier, region, replicas}.tenant-<name> + Deployment orders-api (replicas conforme Spec) + Service ClusterIP + ConfigMap (tenant-specific config: feature flags, billing tier) + RoleBinding (tenant admin acesso ao namespace).pro+; tier upgrade free → pro muta HPA minReplicas; deletion archiva dados em S3 antes do GC cascade.Get-then-Create ou CreateOrUpdate.RequeueAfter.r.Update(ctx, &obj): conflita com subresource /status; sempre r.Status().Update(ctx, &obj).GenerationChangedPredicate.--leader-elect obrigatória em prod./status separa o write path.WATCH_NAMESPACE env.Cruza com 03-03 §2.18 (operators pattern intro), 03-03 §2.21 (PDB/topology spread aplicáveis a workloads gerenciados pelo operator), 03-04 §2.x (CI/CD, operator deploy + image promotion), 04-08 §2.x (services, operators automatizam stateful services), 04-04 §2.x (resilience, reconcile = self-healing built-in), 03-15 §2.x (incident response, operator-managed recovery reduz MTTR).
Kubernetes 1.32 (Q4 2024) estabilizou patterns que ficaram alpha por anos. Stack de produção 2026 não é mais Ingress + Cluster Autoscaler + HPA simples: é Gateway API v1.2 GA (Q3 2024) substituindo Ingress, KEDA 2.16 para scaling event-driven (60+ scalers), Karpenter v1.0 GA (Q3 2024) como node autoprovisioner com consolidation contínua, cgroups v2 (default desde 1.25) habilitando memory.high soft-limit + PSI metrics, e sidecar containers GA (1.29+) resolvendo race conditions de startup/shutdown que atormentavam Istio/Vault/Fluent Bit há anos. Quem está em 2026 ainda em Ingress + Cluster Autoscaler + HPA-only opera com stack de 2020.
Gateway API GA — vs Ingress. Ingress era HTTP-only, anotações vendor-specific (nginx.ingress.kubernetes.io/*), sem typed policies. Gateway API é typed CRD-based, multi-protocolo (HTTPRoute, GRPCRoute, TLSRoute, TCPRoute), e split por role: infra team gerencia GatewayClass + Gateway (qual controller, IPs, certificados, listeners), app team gerencia HTTPRoute (rotas, backends, filters). GAMMA initiative (parte do Gateway API) padroniza service mesh sobre as mesmas APIs — Istio, Linkerd, Cilium implementam.
# Infra team: GatewayClass + Gateway (cluster-scoped + namespace-scoped)
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: envoy-gateway
spec:
controllerName: gateway.envoyproxy.io/gatewayclass-controller
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: prod-gateway
namespace: gateway-system
spec:
gatewayClassName: envoy-gateway
listeners:
- name: https
protocol: HTTPS
port: 443
tls:
mode: Terminate
certificateRefs:
- name: wildcard-tls # cross-namespace via ReferenceGrant
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels: { gateway-access: prod }
---
# App team: HTTPRoute (orders) + GRPCRoute (couriers)
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: orders-route
namespace: orders
labels: { gateway-access: prod }
spec:
parentRefs:
- name: prod-gateway
namespace: gateway-system
hostnames: ["api.logistica.com"]
rules:
- matches:
- path: { type: PathPrefix, value: /v1/orders }
backendRefs:
- name: orders-svc
port: 8080
weight: 90
- name: orders-svc-canary
port: 8080
weight: 10 # canary 10% nativo, sem Flagger/Argo Rollouts pra split simples
filters:
- type: RequestHeaderModifier
requestHeaderModifier:
add: [{ name: X-Gateway, value: envoy }]
parentRefs = quais Gateways adotam essa rota; backendRefs = pesos para canary nativo. Status do HTTPRoute reporta Accepted: True quando o Gateway aceita — debug primeiro lugar quando rota não funciona.
KEDA — event-driven autoscaling (scale-to-zero). HPA built-in escala por CPU/memory/custom metrics, mas não escala para zero e não conhece queues. KEDA é deployment + CRD ScaledObject que cria HPA por baixo dos panos com External Metrics API, e adiciona scale-to-zero. 60+ scalers oficiais: SQS, Kafka, RabbitMQ, NATS, Postgres (queries), Prometheus, Cron, Azure Service Bus, GCP PubSub.
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: courier-assigner
namespace: logistica
spec:
scaleTargetRef:
name: courier-assigner-deploy
minReplicaCount: 0 # scale-to-zero quando queue vazia
maxReplicaCount: 50
pollingInterval: 15 # checa scaler a cada 15s (NÃO use 1s — rate-limit no SQS)
cooldownPeriod: 300 # espera 5min sem load antes de scale-down (evita flapping)
fallback:
failureThreshold: 3
replicas: 5 # se scaler falhar 3x, fixa em 5 (não desce a zero por bug)
triggers:
- type: aws-sqs-queue
metadata:
queueURL: https://sqs.us-east-1.amazonaws.com/123/courier-assignment
queueLength: "30" # 1 replica por 30 mensagens visíveis
awsRegion: us-east-1
identityOwner: pod # usa IRSA da pod, não credenciais do KEDA operator
- type: prometheus # OR — escala se QUALQUER trigger pedir mais
metadata:
serverAddress: http://prometheus.monitoring:9090
threshold: "100"
query: sum(rate(http_requests_total{service="courier-assigner"}[2m]))
Scale-from-zero típico: ~30s (poll interval + pod startup). Para jobs batch que precisam terminar (não loop), use ScaledJob em vez de ScaledObject — cria Job por mensagem.
Karpenter v1 — node autoprovisioner. Cluster Autoscaler escalava ASGs pré-definidos. Karpenter (GA Q3 2024 após 3 anos alpha) provisiona nodes diretamente via EC2 Fleet API olhando para pods Pending — escolhe instance type ótimo (CPU/memory/spot/zone) em ~30s, vs Cluster Autoscaler ~2-3min. Consolidation roda contínua: se workload cabe em nodes menores ou menos nodes, Karpenter dreina e termina automaticamente.
apiVersion: karpenter.sh/v1
kind: NodePool
metadata: { name: spot-batch }
spec:
template:
metadata:
labels: { workload: batch, capacity: spot }
spec:
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot"]
- key: karpenter.k8s.aws/instance-family
operator: In
values: ["c7i", "c7a", "m7i"] # Karpenter escolhe a mais barata que cabe
- key: kubernetes.io/arch
operator: In
values: ["amd64", "arm64"] # multi-arch pra Graviton
taints:
- key: workload
value: batch
effect: NoSchedule
expireAfter: 720h # rotaciona node a cada 30d (drift + AMI updates)
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 1m
budgets:
- nodes: "10%" # NUNCA dreina mais que 10% dos nodes simultâneos
- nodes: "0"
schedule: "0 9 * * mon-fri" # zero disruption durante business hours
duration: 8h
Savings reais 2026: 30-60% no custo EC2 vs Cluster Autoscaler (mistura spot + right-sizing contínuo + bin-packing). Sem disruption.budgets, Karpenter faz mass-eviction durante consolidation — production outage garantido.
cgroups v2 + memory.high + PSI. cgroups v2 é default desde K8s 1.25 (kernel 5.8+). Mudanças relevantes: memory.max é hard limit (OOMKill), mas agora há memory.high — soft limit que faz throttling de alocação antes do OOM. requests.memory em K8s 1.27+ vira memory.low (proteção contra reclaim sob pressure global), limits.memory vira memory.max. PSI (Pressure Stall Information) — /proc/pressure/{cpu,memory,io} — exposto via cAdvisor, pode ser scraped pelo Prometheus e usado como External Metric pra KEDA escalar antes do CPU% saturar. Verifique cgroup version no node:
stat -fc %T /sys/fs/cgroup/ # cgroup2fs = v2; tmpfs = v1 (migre)
cat /sys/fs/cgroup/kubepods.slice/.../memory.pressure # PSI metrics
Em clusters ainda em cgroups v1 (RHEL 7, Amazon Linux 2 antigo), perde memory.high + PSI — não há como ter soft-limit de memória, qualquer pico = OOMKill.
Sidecar containers GA (1.29+, stable 1.32). Antes: sidecars eram containers regulares no spec.containers — sem ordering garantido (Istio proxy não estava pronto quando app fazia primeiro request), sem lifecycle separado (Job não terminava porque sidecar ficava rodando). Solução 2026: sidecar é init container com restartPolicy: Always. Ordering garantido (todos init containers até o sidecar precisam estar Ready), lifecycle separado (sidecar é terminado DEPOIS dos main containers), restart independente.
spec:
initContainers:
- name: istio-proxy
image: docker.io/istio/proxyv2:1.24
restartPolicy: Always # <-- isso faz dele sidecar
lifecycle:
postStart:
exec: { command: ["pilot-agent", "wait"] } # bloqueia até proxy ready
- name: log-shipper
image: fluent/fluent-bit:3.1
restartPolicy: Always
volumeMounts: [{ name: logs, mountPath: /var/log/app }]
containers:
- name: app
image: logistica/orders:v2.4.1
# app só inicia depois que istio-proxy + log-shipper estão Ready
Topology Aware Routing. service.kubernetes.io/topology-mode: Auto na Service: kube-proxy roteia preferencialmente para endpoints na mesma AZ — reduz cross-AZ traffic costs (NAT $0.01/GB AWS) e latência. Combine com internalTrafficPolicy: Local para forçar same-node routing quando aplicável (DaemonSet log shippers).
Stack Logística aplicada. Gateway API: prod-gateway único termina TLS para api.logistica.com, HTTPRoute para /v1/orders (REST), GRPCRoute para couriers.v1.CourierService (gRPC interno mas exposto). KEDA: courier-assigner escala 0→50 em SQS courier-assignment; notification-worker escala em Kafka topic order.events lag; report-generator ScaledJob escala em Postgres query (jobs pendentes). Karpenter: NodePool default (on-demand m7i para api/db) + spot-batch (spot c7a/Graviton para workers KEDA, tolerations match). cgroups v2 + PSI: HPA do orders-svc usa memory.pressure PSI como custom metric (escala antes do CPU saturar). Sidecar GA: Istio proxy + Vault agent injector + Fluent Bit todos como init restartPolicy: Always — Jobs de migration não pendem mais por sidecar leak.
10 anti-patterns 2026.
cooldownPeriod baixo (< 60s). Flapping: scale-up → load drena → scale-down → load volta → scale-up. Cada ciclo = pod startup + image pull. Mínimo 300s em workloads HTTP, 600s+ em batch.disruption.budgets. Consolidation agressivo dreina 50% dos nodes em 1min — outage. Sempre limite a 10-20% simultâneo + zero durante business hours.memory.high (sem soft-limit, só OOMKill), perde PSI metrics, perde memory QoS. Migre nodes para AL2023/Bottlerocket/Ubuntu 22.04+.restartPolicy: Always. Race condition: app faz request antes do Istio proxy estar pronto → 503. Job nunca termina porque Fluent Bit fica rodando.GatewayClass matching. Gateway fica Accepted: False silenciosamente — sem rota funciona. Sempre verifique kubectl get gateway -o jsonpath='{.status.conditions}'.pollingInterval: 1. Rate-limit no SQS/Kafka API, custo de chamadas explode. 15-30s é o sweet spot para HTTP workloads, 60s para batch.instance-family muito restrito (1 família). Fleet falha sob spot interruption mass — sem capacity. Permita 3-5 famílias compatíveis (c7i, c7a, m7i, m7a).minAvailable: 100% + Karpenter consolidation. PDB bloqueia drain forever, Karpenter não consolida nada — paga node ocioso. Use maxUnavailable: 1 ou 25%.Cruza com 03-03 §2.10 (Ingress — predecessor que Gateway API substitui), 03-03 §2.14 (HPA — KEDA cria HPA por baixo, mesma External Metrics API), 03-03 §2.18 (operators — KEDA, Karpenter, Istio são operators), 03-03 §2.21 (PDB + topology spread — interagem com Karpenter consolidation), 03-03 §2.15 (service mesh — GAMMA padroniza sobre Gateway API), 03-05 §2.x (AWS — Karpenter é EKS-native, IRSA + EC2 Fleet API), 04-08 §2.11 (service mesh + platform engineering — split GatewayClass/HTTPRoute = role-based platform), 04-09 §2.x (scaling — KEDA event-driven é categoria distinta de HPA/VPA), 03-15 §2.x (incident response — disruption budgets + PDB controlam blast radius de consolidation).
Você precisa, sem consultar:
Migrar Logística v1 pra K8s. Não significa abandonar Railway, significa demonstrar competência. Pode rodar em K3s local ou GKE Autopilot free tier.
api.logistica.local, app.logistica.local).postgres-rw.default.svc.cluster.local)./healthz e readiness /readyz.courier:updates. Quando há muitos eventos pendentes, scale up.charts/logistica/./metrics (instrumentado em 02-08); ServiceMonitor coleta.latest tag.runAsRoot.kubectl mínimo pra reproduzir tudo (ou helm install).Tenant que cria namespace, quota, network policy ao ser criado.Threshold de Maestria
Acerte todas as 5 pra marcar o módulo como concluído. Sem pressa, sem timer. Tudo fica salvo no teu navegador.
Q1Qual o problema principal de não definir um PodDisruptionBudget em Deployment com múltiplas réplicas?
Q2Por que Topology Spread Constraints com `topologyKey: topology.kubernetes.io/zone` é importante em produção?
Q3No Gateway API GA (1.2), qual a justificativa principal para split entre `Gateway` e `HTTPRoute`?
Q4Quando KEDA é claramente superior a HPA built-in?
Q5Por que sidecar containers GA (1.29+) com `restartPolicy: Always` em initContainer resolve problemas de Istio/Vault/Fluent Bit?
Destrava
03-03 é prereq dos seguintes módulos: