StackPractices
intermediate Por Mathias Paulenko

Plantilla de Pod Disruption Budget de Kubernetes

Una plantilla para definir Pod Disruption Budgets que controlan las disrupciones voluntarias durante drenajes de nodos, actualizaciones y ventanas de mantenimiento.

Temas: devops

Visión General

Un Pod Disruption Budget (PDB) le dice a Kubernetes cuánto de tu carga de trabajo puede interrumpirse voluntariamente a la vez. Cuando alguien ejecuta kubectl drain para sacar un nodo de servicio durante una actualización, la eviction API revisa cada PDB que cubre un pod antes de eliminarlo. Sin un PDB, nada impide que un drain desaloje todas las réplicas de un servicio en pocos segundos — y tu balanceador se entera antes de que los pods terminen de reagendarse en otro nodo.

Los PDB solo gobiernan las disrupciones voluntarias: drenajes, mantenimiento programado, scale-downs del cluster autoscaler y evictions manuales por la API. No hacen nada contra las involuntarias — un kernel panic, un disco muerto o una partición de red mata pods sin mirar tu presupuesto. Los replica controllers resuelven esos casos agendando reemplazos; el trabajo del PDB es más acotado: limitar la velocidad a la que un operador (o el autoscaler) puede tumbar pods sanos a propósito.

Esa distinción importa al escribir el presupuesto. Un PDB no es una garantía de disponibilidad — es un freno. Si un nodo falla en el mismo minuto en que arrancas un drain, igual puedes quedar por debajo de tu mínimo, porque las disrupciones voluntarias e involuntarias cuentan juntas. Dimensiona las réplicas con margen, y trata el PDB como protección contra errores de mantenimiento, no contra la física.

Esta plantilla te da un documento PDB completo y listo para copiar: selección de estrategia, ejemplos por servicio, interacción con drains, integración con Helm, monitoreo y troubleshooting. Los manifiestos listos para aplicar están en el repositorio de apoyo. Combínala con la checklist de revisión de Helm charts cuando publiques PDBs dentro de charts, y con los Resource Quotas de Kubernetes para la parte de capacidad en clusters multi-tenant.

Cuándo Usar

  • Deployments o StatefulSets con varias réplicas que no pueden permitirse perder todos los pods a la vez durante un drain
  • Flujos de actualización de cluster donde los nodos se drenan en una ventana programada
  • Kubernetes administrado (EKS, GKE, AKS) donde el proveedor rota nodos durante actualizaciones del control plane o del node pool
  • Servicios con requisitos de cumplimiento o SLO que documentan disponibilidad mínima durante mantenimiento
  • Equipos que usan cluster autoscaler o Karpenter, donde el scale-down drena nodos sin que nadie lo esté mirando

Evita los PDB en cargas de una sola réplica que toleran caídas (un PDB con minAvailable: 1 solo bloquea todos los drains), en DaemonSets (un pod por nodo, la eviction no aplica igual) y en Jobs o CronJobs cuyos pods son efímeros por diseño.

Solución

# Pod Disruption Budget — `<Nombre del Servicio>`

## Resumen del PDB

| Campo | Valor |
|-------|-------|
| Servicio | example-app |
| Namespace | production |
| Deployment | example-app |
| Réplicas Mínimas | 3 |
| Estrategia PDB | minAvailable |
| Mínimo Disponible | 2 |
| Máximo No Disponible | — |
| Versión de API | policy/v1 |
| Última Actualización | 2026-10-19 |
| Responsable | Equipo DevOps |

## 1. Definiciones de PDB

### PDB estándar (minAvailable)

```yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: example-app-pdb
  namespace: production
  labels:
    app.kubernetes.io/name: example-app
    app.kubernetes.io/instance: example-app
    app.kubernetes.io/managed-by: helm
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app.kubernetes.io/name: example-app
      app.kubernetes.io/instance: example-app
```

### PDB con maxUnavailable

```yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: example-app-pdb
  namespace: production
  labels:
    app.kubernetes.io/name: example-app
    app.kubernetes.io/instance: example-app
spec:
  maxUnavailable: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: example-app
      app.kubernetes.io/instance: example-app
```

### PDB con porcentaje

```yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: example-app-pdb
  namespace: production
spec:
  minAvailable: 50%
  selector:
    matchLabels:
      app.kubernetes.io/name: example-app
```

## 2. Selección de Estrategia PDB

| Estrategia | Valor | Cuándo Usar | Ejemplo |
|------------|-------|-------------|---------|
| minAvailable (absoluto) | 2 | Mínimo fijo de réplicas necesarias | 3 réplicas, siempre hacen falta 2 |
| minAvailable (porcentaje) | 50% | Escala con el número de réplicas | 10 réplicas, hacen falta 5 |
| maxUnavailable (absoluto) | 1 | Controlar la tasa de disrupción | 5 réplicas, permitir 1 caída |
| maxUnavailable (porcentaje) | 25% | Disrupción proporcional | 8 réplicas, permitir 2 caídas |

### Matriz de Decisión

| Número de Réplicas | Estrategia Recomendada | Motivo |
|--------------------|------------------------|--------|
| 1 | Sin PDB (o minAvailable: 1) | Réplica única, el PDB bloquea todos los drains |
| 2 | maxUnavailable: 1 | Permite 1 caída, mantiene 1 corriendo |
| 3 | minAvailable: 2 | Mantiene 2 corriendo, permite 1 disrupción |
| 5 | maxUnavailable: 1 | Permite 1 disrupción a la vez |
| 10+ | maxUnavailable: 25% | Control de disrupción proporcional |

## 3. PDBs por Servicio

### Web Frontend

| Campo | Valor |
|-------|-------|
| Deployment | web-frontend |
| Réplicas | 6 |
| Estrategia | maxUnavailable: 1 |
| Motivo | Tráfico alto, tolera 1 pod caído |
| HPA | Sí (mín: 4, máx: 12) |

```yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: web-frontend-pdb
  namespace: production
spec:
  maxUnavailable: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: web-frontend
```

### Servicio API

| Campo | Valor |
|-------|-------|
| Deployment | api-service |
| Réplicas | 4 |
| Estrategia | minAvailable: 3 |
| Motivo | Ruta crítica, necesita 75% disponible |
| HPA | Sí (mín: 3, máx: 8) |

```yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api-service-pdb
  namespace: production
spec:
  minAvailable: 3
  selector:
    matchLabels:
      app.kubernetes.io/name: api-service
```

### Worker (Procesos en Segundo Plano)

| Campo | Valor |
|-------|-------|
| Deployment | background-worker |
| Réplicas | 3 |
| Estrategia | maxUnavailable: 1 |
| Motivo | Los jobs se reanudan, tolera 1 caída |
| HPA | No |

```yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: background-worker-pdb
  namespace: production
spec:
  maxUnavailable: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: background-worker
```

### Base de Datos (StatefulSet)

| Campo | Valor |
|-------|-------|
| StatefulSet | postgres-cluster |
| Réplicas | 3 |
| Estrategia | minAvailable: 2 |
| Motivo | Basado en quorum, necesita mayoría |
| HPA | No |

```yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: postgres-pdb
  namespace: production
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app.kubernetes.io/name: postgres-cluster
```

### Caché Redis

| Campo | Valor |
|-------|-------|
| Deployment | redis-cache |
| Réplicas | 1 |
| Estrategia | Sin PDB |
| Motivo | Réplica única, el PDB bloquearía todos los drains |

### Job Programado (CronJob)

| Campo | Valor |
|-------|-------|
| CronJob | data-export |
| Estrategia | Sin PDB |
| Motivo | Jobs efímeros, el CronJob controller los reagenda |

## 4. Interacción entre PDB y Node Drain

### Cómo el PDB Afecta a los Node Drains

| Escenario | Nº de Pods | PDB | Comportamiento del Drain |
|-----------|------------|-----|--------------------------|
| 3 pods, minAvailable: 2 | 3 | 2 | Desaloja 1 a la vez, espera al nuevo pod antes del siguiente |
| 3 pods, minAvailable: 3 | 3 | 3 | Drain bloqueado — no puede desalojar ningún pod |
| 5 pods, maxUnavailable: 1 | 5 | 1 | Desaloja 1 a la vez |
| 2 pods, maxUnavailable: 1 | 2 | 1 | Desaloja 1, espera al reemplazo |
| 1 pod, minAvailable: 1 | 1 | 1 | Drain bloqueado — no puede desalojar el único pod |

### Comandos de Drain

```bash
kubectl drain node-1 --ignore-daemonsets --delete-emptydir-data

## Drain con timeout (el PDB puede bloquear)
kubectl drain node-1 --ignore-daemonsets --delete-emptydir-data --timeout=5m

## Drain forzado (ignora el PDB — usar con cuidado)
kubectl drain node-1 --ignore-daemonsets --delete-emptydir-data --force

## Revisar el estado del drain
kubectl get pods -o wide --field-selector spec.nodeName=node-1

## Reactivar el nodo tras el mantenimiento
kubectl uncordon node-1
```

### Revisar el Estado del PDB

```bash
## Revisar el estado del PDB
kubectl get pdb -n production

## Estado detallado del PDB
kubectl describe pdb example-app-pdb -n production

## Revisar qué pods están protegidos
kubectl get pods -n production -l app.kubernetes.io/name=example-app
```

## 5. PDB en Helm Charts

### values.yaml

```yaml
podDisruptionBudget:
  enabled: true
  minAvailable: 2
  # maxUnavailable: 1  # Alternativa a minAvailable
```

### Template

```yaml
{{- if .Values.podDisruptionBudget.enabled }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: {{ include "chart.fullname" . }}
  labels:
    {{- include "chart.labels" . | nindent 4 }}
spec:
  {{- if .Values.podDisruptionBudget.minAvailable }}
  minAvailable: {{ .Values.podDisruptionBudget.minAvailable }}
  {{- end }}
  {{- if .Values.podDisruptionBudget.maxUnavailable }}
  maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }}
  {{- end }}
  selector:
    matchLabels:
      {{- include "chart.selectorLabels" . | nindent 6 }}
{{- end }}
```

## 6. Monitoreo de PDBs

### Métricas Clave

| Métrica | Descripción | Umbral de Alerta |
|---------|-------------|------------------|
| Disrupciones permitidas del PDB | Número de pods que pueden interrumpirse | < 0 (bloqueado) |
| Pods sanos actuales del PDB | Pods sanos en este momento | < minAvailable |
| Pods sanos deseados del PDB | Objetivo de pods sanos | — |
| Pods esperados del PDB | Total de pods que coinciden con el selector | — |
| Duración de drain bloqueado | Tiempo que un drain queda bloqueado por el PDB | > 10 min |

### Reglas de Alerta

```yaml
## Reglas de alerta de Prometheus
groups:
  - name: pdb-alerts
    rules:
      - alert: PDBBlocked
        expr: kube_poddisruptionstatus_allowed_disruptions == 0
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "El PDB {{ $labels.pdb }} bloquea todas las disrupciones"
          description: "Ningún pod puede interrumpirse para {{ $labels.namespace }}/{{ $labels.pdb }}"

      - alert: PDBBelowMinAvailable
        expr: kube_poddisruptionstatus_current_healthy < kube_poddisruptionstatus_desired_healthy
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "El PDB {{ $labels.pdb }} por debajo del mínimo disponible"
          description: "Solo {{ $value }} pods sanos, se necesitan {{ $labels.desired_healthy }}"
```

## 7. Troubleshooting de PDBs

### Problemas Comunes

| Problema | Causa | Solución |
|----------|-------|----------|
| Drain atascado | El PDB bloquea la eviction (minAvailable = réplicas) | Escalar el deployment, luego drenar |
| Drain atascado | No hay réplicas suficientes para satisfacer el PDB | Escalar antes de drenar |
| PDB no encontrado | El PDB no existe o está en otro namespace | Verificar: `kubectl get pdb -n <ns>` |
| Selector del PDB no coincide | El selector no coincide con las labels de los pods | Verificar labels: `kubectl get pods --show-labels` |
| Timeout de eviction | El pod tarda demasiado en terminar | Añadir terminationGracePeriodSeconds |

### Pasos de Depuración

```bash
## 1. Revisar el estado del PDB
kubectl get pdb -n production
kubectl describe pdb example-app-pdb -n production

## 2. Revisar el conteo de pods vs. los requisitos del PDB
kubectl get pods -n production -l app.kubernetes.io/name=example-app --no-headers | wc -l

## 3. Revisar si los pods están sanos
kubectl get pods -n production -l app.kubernetes.io/name=example-app

## 4. Revisar el estado del drain del nodo
kubectl get nodes -o wide
kubectl describe node node-1 | grep -A 10 "Conditions:"

## 5. Revisar los eventos de eviction
kubectl get events -n production --field-selector reason=Eviction
```

Cómo Funciona el Flujo de Eviction

Cuando ejecutas kubectl drain, no borra pods — envía evictions a través del subrecurso /eviction de la API de pods. Esa indirección es el punto clave: un kubectl delete pod directo ignora los presupuestos por completo, mientras que una eviction se valida contra cada PDB cuyo selector coincida con el pod. La eviction API responde 429 Too Many Requests cuando se violaría el presupuesto, y el drain reintenta hasta que el reemplazo del pod esté listo en otro nodo.

Mermaid flowchart LR diagram

Hay tres detalles de ese bucle que sorprenden en producción. Primero, el chequeo cuenta pods sanos actuales, no réplicas deseadas — si un pod está en crash-loop, ya descuenta tu presupuesto, así que un drain puede atascarse aunque tengas réplicas deseadas de sobra. Segundo, la eviction es por pod y secuencial: el drain no desaloja el pod dos hasta que el reemplazo del pod uno reporta Ready, y por eso un servicio que tarda en arrancar convierte un drain de dos minutos en veinte. Tercero, tanto kubectl drain --force como kubectl delete pod se saltan este camino — por eso forzar un drain es la forma más rápida de violar tu propio piso de disponibilidad.

Explicación

Los dos campos del presupuesto, minAvailable y maxUnavailable, son mutuamente excluyentes — elige uno por PDB. minAvailable fija un piso: “nunca bajes de N pods sanos”. maxUnavailable fija un techo sobre la tasa de disrupción: “nunca tumbes más de N a la vez”. Ambos aceptan números absolutos o porcentajes, y los porcentajes redondean hacia arriba en minAvailable y hacia abajo en maxUnavailable — un detalle que importa con pocas réplicas, donde minAvailable: 50% sobre tres pods exige dos sanos.

Para la mayoría de los servicios, maxUnavailable: 1 es el default más sensato. No depende de cuántas réplicas corras, sobrevive a los scale-ups del HPA sin ediciones, y garantiza que los drains avancen de a un pod. Usa minAvailable cuando el piso en sí es el requisito — un StatefulSet basado en quorum como etcd o una base de datos que necesita mayoría para seguir escribiendo.

Los deployments de una sola réplica son la trampa clásica. Un PDB con minAvailable: 1 sobre un solo pod hace que cualquier drain se cuelgue para siempre, porque desalojar la única réplica violaría el presupuesto. O escalas a dos o más réplicas antes de activar el PDB, o lo omites y aceptas que los drains causarán una caída breve. Un drain bloqueado también frena lo que lo disparó — incluido el cluster autoscaler, que se queda esperando en vez de recuperar un nodo vacío.

La interacción con el autoscaler corta en los dos sentidos. Cuando el autoscaler (o Karpenter) decide que un nodo está infrautilizado, lo drena primero y ese drain respeta los PDBs. Si tu PDB no puede satisfacerse, el scale-down aborta y sigues pagando el nodo. unhealthyPodEvictionPolicy: AlwaysAllow (policy/v1, estable desde Kubernetes 1.31) suaviza esto permitiendo que la eviction avance sobre pods que todavía no están Ready — útil para cargas en crash-loop, peligroso para todo lo demás.

No confundas el presupuesto del PDB con el maxUnavailable del rollingUpdate del Deployment. Parecen iguales pero viven en planos distintos: el campo de rolling-update controla cuántos pods pueden estar caídos durante un deploy, y los pods que se están reemplazando no cuentan contra el PDB porque el Deployment corre réplicas extra temporalmente. El PDB te protege durante el mantenimiento; el rolling-update te protege durante los deploys. Necesitas los dos, ajustados por separado.

Variantes

ContextoEnfoqueNotas
Réplica únicaSin PDB o minAvailable: 0Acepta caída durante los drains
StatefulSetminAvailable para quorumLas bases de datos necesitan mayoría
DaemonSetNo necesita PDBLos pods de DaemonSet corren en cada nodo
Job/CronJobNo necesita PDBPods efímeros, se reagendan
Escalado con HPAUsa PDB porcentualEscala con el número de réplicas
Multi-AZUn PDB por AZEvita disrupción en todas las AZ

Una variante más que vale la pena conocer: minAvailable: 0 o maxUnavailable: 100% desactiva el presupuesto en la práctica pero mantiene el objeto PDB. Los equipos lo usan como opt-out documentado — el objeto existe, los revisores ven la decisión, y nadie confunde la ausencia de PDB con un descuido durante una auditoría. Si la adoptas, deja un comentario en el manifiesto explicando por qué el presupuesto es permisivo a propósito, o la siguiente persona lo va a “arreglar” por ti.

Qué Funciona

  1. Pon siempre un PDB en deployments multi-réplica — evita que un drain desaloje todo a la vez.
  2. Usa maxUnavailable: 1 como default salvo que un servicio necesite de verdad un piso fijo.
  3. Prefiere presupuestos porcentuales en cargas con HPA para que el presupuesto siga al número de réplicas.
  4. Escala antes de drenar cuando el conteo actual no puede satisfacer el PDB.
  5. Alerta sobre allowedDisruptions == 0 — un PDB bloqueado descubierto en pleno incidente llega tarde.
  6. Ejercita los PDBs en game days antes de confiar en ellos en una actualización real.
  7. Publica los PDBs dentro de los Helm charts para que sea imposible olvidarlos por servicio.

Errores Comunes

  1. PDB en un deployment de una réplica — todos los drains se cuelgan para siempre.
  2. minAvailable igual al número de réplicas — ningún pod puede desalojarse jamás.
  3. Omitir PDBs en servicios críticos — un drain tumba el servicio entero.
  4. Un selector que no coincide con las labels de los pods — el PDB existe pero no protege nada.
  5. Sin alertas sobre el estado del PDB — los drains bloqueados se descubren en pleno incidente.
  6. Olvidar el PDB al hacer scale-down — el presupuesto puede frenar al autoscaler.
  7. Usar policy/v1beta1 — está deprecado y eliminado; usa policy/v1.
  8. Esperar que el PDB cubra fallos de nodo — solo gobierna evictions voluntarias.

Véase También

Lecturas Recomendadas

Preguntas frecuentes

¿Qué pasa si un PDB bloquea un node drain?

El drain espera — por defecto, indefinidamente. Usa --timeout para acotar la espera. Si el presupuesto nunca puede satisfacerse (menos réplicas que minAvailable), el drain se queda bloqueado hasta el timeout o hasta que escales, relajes el PDB o uses --force, que ignora el presupuesto y puede causar caída.

¿Debería usar minAvailable o maxUnavailable?

Usa minAvailable cuando necesitas un piso garantizado — "mantén siempre dos pods corriendo". Usa maxUnavailable cuando quieres controlar la tasa de disrupción — "nunca tumbes más de uno a la vez". Para la mayoría de los servicios maxUnavailable: 1 es un buen default porque sobrevive a cambios en el número de réplicas sin ediciones.

¿Los PDBs protegen contra fallos de hardware?

No. Los PDBs solo aplican a disrupciones voluntarias como drains y evictions por API. Un nodo muerto lo gestiona el replica controller reagendando pods en otro sitio. Si un nodo muere mientras un drain también está en curso, ambos descuentan de tu conteo de pods sanos — dimensiona las réplicas con margen para ese solapamiento.

¿Puedo tener minAvailable y maxUnavailable en el mismo PDB?

No — son mutuamente excluyentes dentro de un mismo PDB. Elige el que coincida con tu forma de pensar la disponibilidad: un piso que no puedes cruzar, o una tasa que no puedes superar.

¿Cómo interactúan los PDBs con el cluster autoscaler?

El autoscaler drena un nodo antes de eliminarlo, y ese drain respeta los PDBs. Un PDB que no puede satisfacerse frena el scale-down hasta el timeout del autoscaler — ajustable con --max-graceful-termination-sec — y entonces el nodo sobrevive. Es una característica: el autoscaler no puede matar tu quorum sin ruido. Define unhealthyPodEvictionPolicy: AlwaysAllow si los pods que no llegan a Ready son la razón por la que tus drains se atascan.

¿Por qué kubectl delete pod funcionó si el PDB bloqueaba mi drain?

Porque delete no es una eviction. El PDB solo protege el camino de la API /eviction — los drains y las evictions del autoscaler pasan por ahí, los borrados directos no. Si necesitas quitar un pod durante mantenimiento, desalójalo (kubectl drain, o POST al subrecurso de eviction) en vez de borrarlo.