Un baseline de producción para workloads en Kubernetes
Requests, probes, disruption budgets, apagado ordenado y seguridad de pods: la configuración que un Deployment de producción debería tener antes de su primer incidente.
Los ejemplos son genéricos e ilustrativos. No provienen del entorno de ningún cliente: adapte umbrales y configuraciones a su propio sistema.
Por qué un baseline
Kubernetes ejecuta casi cualquier cosa con casi ninguna configuración, y ese es el problema: los valores por defecto están pensados para empezar, no para seguir en pie. La configuración de abajo cuesta poco el primer día y mucho si se descubre durante una caída. Se aplica a Deployments sin estado; las cargas con estado necesitan más.
Requests y limits
El scheduler ubica los pods según sus requests, y el Horizontal Pod Autoscaler mide la utilización contra ellos. Un pod sin requests se ubica a ciegas y no puede escalar por CPU.
La memoria no se puede comprimir: un contenedor que supera su límite de memoria es terminado. Fije el límite de memoria igual o cercano al request, para que un pod nunca se programe con memoria que no puede conservar. La CPU es distinta — un contenedor por encima de su límite de CPU sufre throttling, que se nota como latencia. Muchos equipos definen requests de CPU y omiten el límite de CPU en servicios sensibles a la latencia; elija lo que elija, mida el throttling.
Tres probes, tres funciones
- Readiness decide si el pod recibe tráfico. Cuidado con las dependencias compartidas: si todos los pods fallan la readiness a la vez, el servicio se queda sin ningún endpoint.
- Liveness decide si el contenedor se reinicia. Limítela al propio proceso: si comprueba la base de datos, una caída de la base reinicia todos los pods a la vez.
- Startup retiene las otras probes mientras arranca una aplicación lenta, en lugar de inflar el retraso de la liveness.
Sobrevivir a drenajes de nodos y caídas de zona
Las actualizaciones de nodos y el autoescalado desalojan pods constantemente. Un PodDisruptionBudget limita cuántas réplicas puede tumbar a la vez una interrupción voluntaria, y las topology spread constraints mantienen las réplicas en zonas distintas, para que la caída de una zona no arrastre todo el servicio.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api
spec:
maxUnavailable: 1
selector:
matchLabels: { app: api }
Apagado ordenado
Cuando se elimina un pod, su retirada de los endpoints del servicio y el apagado de sus contenedores (el hook preStop y después SIGTERM) empiezan a la vez. Una aplicación que termina de inmediato pierde solicitudes que todavía se le están enrutando. Añada una pausa corta en preStop para que los balanceadores se pongan al día, gestione SIGTERM terminando el trabajo en curso y mantenga terminationGracePeriodSeconds por encima de la pausa más el drenaje.
Contexto de seguridad e imágenes
Ejecute con un usuario no root, elimine todas las capabilities de Linux, bloquee la escalada de privilegios y monte el sistema de archivos raíz en solo lectura; el Pod Security Standard restricted impone la mayor parte de esto por namespace. Referencie las imágenes por digest y no por una etiqueta mutable, para que lo probado sea exactamente lo que se ejecuta.
Todo junto
Un Deployment con el baseline aplicado. Los valores son ilustrativos: dimensione los requests a partir del uso medido, no de este ejemplo.
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"] }