Um baseline de produção para workloads no Kubernetes
Requests, probes, disruption budgets, desligamento gracioso e segurança de pods: as configurações que um Deployment de produção deve ter antes do primeiro incidente.
Os exemplos são genéricos e ilustrativos. Não vêm do ambiente de nenhum cliente: ajuste limites e configurações ao seu próprio sistema.
Por que um baseline
O Kubernetes roda quase qualquer coisa com quase nenhuma configuração, e esse é o problema: os padrões foram escolhidos para começar, não para continuar no ar. As configurações abaixo custam pouco no primeiro dia e caro quando descobertas durante uma queda. Elas valem para Deployments sem estado; workloads com estado precisam de mais.
Requests e limits
O scheduler posiciona os pods pelos requests, e o Horizontal Pod Autoscaler mede a utilização em relação a eles. Um pod sem requests é posicionado às cegas e não pode escalar por CPU.
Memória não é compressível: um contêiner que passa do limite de memória é encerrado. Defina o limite de memória igual ou próximo ao request, para que um pod nunca seja agendado com memória que não pode manter. CPU é diferente — um contêiner acima do limite de CPU sofre throttling, que aparece como latência. Muitas equipes definem requests de CPU e deixam o limite de CPU de fora em serviços sensíveis à latência; qualquer que seja a escolha, meça o throttling.
Três probes, três funções
- Readiness decide se o pod recebe tráfego. Cuidado com dependências compartilhadas: se todos os pods falharem na readiness ao mesmo tempo, o serviço fica sem nenhum endpoint.
- Liveness decide se o contêiner é reiniciado. Limite-a ao próprio processo: se ela verifica o banco de dados, uma queda do banco reinicia todos os pods de uma vez.
- Startup segura as outras probes enquanto uma aplicação lenta inicializa, em vez de inflar o atraso da liveness.
Sobreviver a drenagens de nós e a falhas de zona
Atualizações de nós e o autoscaling removem pods o tempo todo. Um PodDisruptionBudget limita quantas réplicas uma interrupção voluntária pode derrubar de uma vez, e as topology spread constraints mantêm as réplicas em zonas diferentes, para que a falha de uma zona não leve o serviço inteiro junto.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api
spec:
maxUnavailable: 1
selector:
matchLabels: { app: api }
Desligamento gracioso
Quando um pod é excluído, a sua remoção dos endpoints do serviço e o desligamento dos contêineres (o hook preStop e depois o SIGTERM) começam ao mesmo tempo. Uma aplicação que encerra imediatamente perde requisições que ainda estão sendo roteadas para ela. Adicione uma pausa curta no preStop para os balanceadores se atualizarem, trate o SIGTERM concluindo o trabalho em andamento e mantenha o terminationGracePeriodSeconds maior que a pausa somada à drenagem.
Contexto de segurança e imagens
Execute como usuário não root, remova todas as capabilities do Linux, bloqueie a escalada de privilégios e monte o sistema de arquivos raiz como somente leitura; o Pod Security Standard restricted impõe a maior parte disso por namespace. Referencie imagens por digest em vez de uma tag mutável, para que o que foi testado seja exatamente o que roda.
Juntando tudo
Um Deployment com o baseline aplicado. Os valores são ilustrativos: dimensione os requests a partir do uso medido, não deste exemplo.
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 3
selector:
matchLabels: { app: api }
template:
metadata:
labels: { app: api }
spec:
terminationGracePeriodSeconds: 45
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels: { app: api }
securityContext:
runAsNonRoot: true
seccompProfile: { type: RuntimeDefault }
containers:
- name: api
image: registry.example.com/api@sha256:<digest>
resources:
requests: { cpu: 250m, memory: 256Mi }
limits: { memory: 256Mi }
readinessProbe:
httpGet: { path: /ready, port: 8080 }
periodSeconds: 5
livenessProbe:
httpGet: { path: /live, port: 8080 }
periodSeconds: 10
failureThreshold: 3
startupProbe:
httpGet: { path: /live, port: 8080 }
periodSeconds: 5
failureThreshold: 30
lifecycle:
preStop:
sleep: { seconds: 5 } # native sleep action, Kubernetes 1.30+
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: ["ALL"] }