Cerrábamos el módulo 1 con tienda-web corriendo en rutas-norte-dev y con una lección incómoda aprendida: al borrar aquel pod suelto, no volvió nunca. Antes de arreglarlo con controladores, hay que entender bien la pieza sobre la que se construye todo lo demás. El Pod es la unidad mínima de despliegue de Kubernetes: no despliegas contenedores, despliegas pods que contienen contenedores. Toda la maquinaria del curso —ReplicaSets, Deployments, Servicios, sondas, autoescalado, políticas de red— existe para crear, mantener, alcanzar, vigilar o proteger pods. En esta lección abrimos el pod en canal: por qué es la unidad mínima y no el contenedor, qué comparten exactamente los contenedores que viven dentro de uno, cómo se lee un manifiesto campo a campo, qué significa cada fase y cada estado que verás en kubectl get pods, cómo nace y cómo muere un pod, y cómo usar pods efímeros como herramienta de diagnóstico. Al terminar, sabrás interpretar un ImagePullBackOff o un CrashLoopBackOff sin buscar en internet.

Contenido

  1. Por qué la unidad mínima es el pod y no el contenedor
  2. Qué comparten los contenedores de un mismo pod (y qué no)
  3. El contenedor de pausa: el que sostiene el pod
  4. Anatomía de un manifiesto de pod: api-reservas campo a campo
  5. Fases del pod y estados de los contenedores
  6. Razones típicas: ContainerCreating, ImagePullBackOff, CrashLoopBackOff
  7. restartPolicy: quién reinicia qué
  8. Ciclo de vida completo: de la creación al SIGTERM
  9. Pods efímeros para depurar
  10. Por qué en producción nunca se gestionan pods sueltos

  1. Por qué la unidad mínima es el pod y no el contenedor

Es la primera pregunta que se hace todo el que viene de Docker. Si Kubernetes orquesta contenedores, ¿por qué introduce una envoltura intermedia en lugar de programar contenedores directamente?

La respuesta es que hay procesos que necesitan estar pegados. No "cerca" ni "en la misma máquina", sino literalmente compartiendo red y disco, arrancando y muriendo juntos, planificados como una sola unidad indivisible. Ejemplos que verás en cualquier plataforma seria:

  • Un servidor de aplicación y un recolector de logs que lee los ficheros que aquel escribe.
  • Un servicio y un proxy que le termina el TLS y le añade métricas (el patrón service mesh).
  • Un contenedor principal y otro que descarga configuración o certificados antes de que arranque.

Si la unidad mínima fuera el contenedor, el planificador podría colocar el recolector de logs en un nodo y la aplicación en otro, y el diseño se rompería. Kubernetes resuelve esto con un contrato claro:

El pod es un grupo de uno o más contenedores que se planifican juntos en el mismo nodo, comparten red y almacenamiento, y viven y mueren como una unidad.

Dos consecuencias prácticas que conviene grabar desde ya:

  • El pod es la unidad de planificación. El scheduler asigna pods a nodos, no contenedores. Los recursos que se suman para decidir si "cabe" en un nodo son los de todos sus contenedores.
  • El pod es la unidad de escalado. Cuando escalas api-reservas a 6 réplicas, creas 6 pods completos. No puedes escalar un contenedor dentro de un pod.

Esa segunda consecuencia es la regla de diseño más importante: si dos procesos escalan a ritmos distintos, no van en el mismo pod. En Rutas Norte, api-reservas y redis-cache podrían parecer buenos compañeros de pod (la API consulta la caché constantemente), pero sería un error grave: cada réplica de la API tendría su propia caché aislada, y escalar la API multiplicaría las cachés. Van en pods separados, y se hablan a través de un Servicio.

El caso abrumadoramente mayoritario, y el que usaremos casi siempre en el curso, es un contenedor por pod. Los patrones multicontenedor (init containers y sidecars) tienen su propia lección: Init Containers, Sidecars y Patrones Multi-Contenedor.

  1. Qué comparten los contenedores de un mismo pod (y qué no)

Un pod no es una metáfora: es una construcción muy concreta de namespaces de Linux compartidos entre procesos. Esta tabla es la referencia exacta.

Recurso ¿Se comparte dentro del pod? Qué implica en la práctica
Namespace de red Sí Todos los contenedores tienen la misma IP y el mismo espacio de puertos; se ven entre sí por localhost
Namespace IPC Sí Pueden usar memoria compartida y semáforos de System V entre ellos
Volúmenes (spec.volumes) Sí, los que cada uno monte Un contenedor escribe un fichero y otro lo lee, si ambos montan el mismo volumen
Ciclo de vida y nodo Sí Se planifican en el mismo nodo; el pod se borra entero, nunca a medias
Namespace de UTS (hostname) Sí Mismo hostname, que por defecto es el nombre del pod
Sistema de ficheros raíz No Cada contenedor tiene el suyo, el de su imagen. /app de uno no es /app del otro
Namespace de PID No por defecto Cada contenedor ve solo sus procesos, salvo que actives shareProcessNamespace: true
Recursos (CPU, memoria) No requests y limits se declaran por contenedor, no por pod
Estado de reinicio No El kubelet reinicia contenedores individualmente; no reinicia el pod entero
flowchart TB
    subgraph POD["Pod api-reservas · IP 10.244.0.34"]
        direction TB
        NET["Namespace de red compartido<br/>una sola IP, un solo espacio de puertos"]
        subgraph C1["Contenedor: api"]
            FS1["Sistema de ficheros propio<br/>imagen node:20-alpine"]
        end
        subgraph C2["Contenedor: recolector-logs"]
            FS2["Sistema de ficheros propio<br/>imagen fluent-bit"]
        end
        VOL[("Volumen compartido<br/>/var/log/app")]
    end
    C1 -.->|escribe| VOL
    C2 -.->|lee| VOL
    C1 --- NET
    C2 --- NET

La consecuencia más útil y la más peligrosa del namespace de red compartido:

  • Útil: dentro de un pod, dos contenedores se hablan por http://localhost:8080. Sin DNS, sin Service, sin latencia de red.
  • Peligrosa: no puede haber dos contenedores escuchando en el mismo puerto dentro de un pod. Si metes dos nginx en el pod, el segundo fallará con "address already in use".

  1. El contenedor de pausa: el que sostiene el pod

¿Cómo consigue Kubernetes que varios contenedores compartan la red si cada uno arranca y muere por su cuenta? Con un truco elegante: por cada pod, el kubelet arranca un contenedor invisible llamado contenedor de pausa (pause, también llamado contenedor de infraestructura o sandbox).

Su comportamiento:

  1. Se crea el primero, antes que cualquier contenedor tuyo.
  2. Crea y posee los namespaces de red, IPC y UTS del pod. Es quien recibe la IP.
  3. Los demás contenedores del pod se arrancan uniéndose a los namespaces del contenedor de pausa.
  4. Su único trabajo es dormir. Su código fuente ocupa unas pocas decenas de líneas: se queda bloqueado esperando señales y hace reap de los procesos huérfanos.

Su valor está en que sobrevive a los reinicios de tus contenedores. Si el proceso de api-reservas se cae y el kubelet lo reinicia, el contenedor de pausa sigue vivo, así que la IP del pod no cambia. Solo cuando se destruye el pod entero desaparece el sandbox y se libera la IP. Esto explica exactamente lo que observaste en el ejercicio final del módulo 1: al matar el proceso, RESTARTS subía a 1 pero la IP seguía siendo la misma.

Puedes verlo en el nodo, aunque nunca aparece en kubectl get pods:

minikube ssh --profile=rutas-norte -- "sudo crictl pods | head -5"
POD ID              CREATED             STATE     NAME              NAMESPACE
7b2c9d1f4a8e3       12 minutes ago      Ready     tienda-web        rutas-norte-dev

No tienes que gestionarlo nunca. Pero conocerlo evita confusiones cuando leas documentación o depures a bajo nivel.

  1. Anatomía de un manifiesto de pod: api-reservas campo a campo

Vamos a escribir el pod de api-reservas, el segundo componente de Rutas Norte. Como registry.rutasnorte.example es ficticio, simulamos la API con una imagen pública de Node.js que levanta un servidor mínimo. Guárdalo en el repositorio del proyecto.

# k8s/base/api-reservas-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: api-reservas
  namespace: rutas-norte-dev
  labels:
    app: api-reservas
    app.kubernetes.io/part-of: rutas-norte
    entorno: dev
  annotations:
    rutasnorte.example/responsable: [email protected]
spec:
  restartPolicy: Always
  terminationGracePeriodSeconds: 30
  containers:
    - name: api
      image: node:20-alpine
      command: ["node", "-e"]
      args:
        - |
          const http = require('http');
          http.createServer((req, res) => {
            res.writeHead(200, {'Content-Type': 'application/json'});
            res.end(JSON.stringify({servicio: 'api-reservas', ruta: req.url}));
          }).listen(3000, () => console.log('api-reservas escuchando en 3000'));
      ports:
        - name: http
          containerPort: 3000
          protocol: TCP
      env:
        - name: ENTORNO
          value: "dev"
        - name: NODO
          valueFrom:
            fieldRef:
              fieldPath: spec.nodeName
      resources:
        requests:
          cpu: "100m"
          memory: "128Mi"
        limits:
          cpu: "500m"
          memory: "256Mi"
      volumeMounts:
        - name: temporal
          mountPath: /tmp/reservas
  volumes:
    - name: temporal
      emptyDir: {}

Repaso campo a campo:

  • apiVersion: v1 / kind: Pod: el Pod pertenece al grupo core de la API, por eso no lleva prefijo de grupo. Lo vimos en Objetos, Manifiestos YAML y el Modelo Declarativo.
  • metadata.name: único dentro del namespace. Será también el hostname del pod.
  • metadata.labels: las tres etiquetas obligatorias del proyecto. Aún no las usa nadie, pero en la lección siguiente serán la clave con la que un ReplicaSet reconoce a "sus" pods. Su sintaxis completa está en Etiquetas, Selectores y Anotaciones.
  • spec.restartPolicy: Always: es el valor por defecto; lo escribimos para hacerlo explícito. Apartado 7.
  • spec.terminationGracePeriodSeconds: 30: segundos que se le conceden al contenedor para terminar ordenadamente antes de matarlo a la fuerza. También es el valor por defecto. Apartado 8.
  • containers[].name: identificador del contenedor dentro del pod. Es lo que pasas a kubectl logs -c o kubectl exec -c cuando hay varios.
  • image: etiqueta explícita, jamás latest, según la convención del proyecto.
  • command / args: sobrescriben el ENTRYPOINT y el CMD de la imagen respectivamente. Aquí los usamos para que una imagen genérica de Node se comporte como nuestra API. Ojo con la equivalencia, que es una fuente clásica de errores:
Docker Kubernetes
ENTRYPOINT command
CMD args
  • ports: informativo, no abre nada. Sirve para documentar y, sobre todo, para dar un nombre al puerto (http), que luego un Service podrá referenciar por nombre en lugar de por número.
  • env: variables de entorno. La primera es un valor literal; la segunda usa fieldRef para inyectar un dato del propio pod (la downward API). Las fuentes serias de configuración —ConfigMaps y Secrets— llegan en Variables de Entorno.
  • resources: requests es lo que el scheduler reserva; limits es el techo que impone el kubelet. Detalle completo en Cuotas y Límites de Recursos.
  • volumeMounts / volumes: el volumen emptyDir es un directorio vacío que vive y muere con el pod. Útil para ficheros temporales y para compartir datos entre contenedores del mismo pod. El almacenamiento persistente es el módulo 5.

Aplícalo y compruébalo:

kubectl apply -f k8s/base/api-reservas-pod.yaml
kubectl get pods
pod/api-reservas created

NAME           READY   STATUS    RESTARTS   AGE
api-reservas   1/1     Running   0          6s
tienda-web     1/1     Running   0          22m
kubectl exec api-reservas -- wget -qO- http://localhost:3000/disponibilidad
{"servicio":"api-reservas","ruta":"/disponibilidad"}

Fíjate en que hemos llamado a localhost:3000 desde dentro del propio pod: eso es el namespace de red compartido en acción.

  1. Fases del pod y estados de los contenedores

Aquí hay dos conceptos que se confunden constantemente y que conviene separar con nitidez.

5.1. La fase del pod (status.phase)

Es un resumen de alto nivel del pod entero. Solo tiene cinco valores posibles:

Fase Significado Situación típica
Pending Aceptado por la API, pero algún contenedor aún no está en marcha Esperando al scheduler, o descargando imágenes
Running Asignado a un nodo, todos los contenedores creados y al menos uno en ejecución Estado normal de una carga de servicio
Succeeded Todos los contenedores terminaron con éxito y no se reiniciarán Un Job o un pod con restartPolicy: Never que acabó bien
Failed Todos terminaron y al menos uno falló (código de salida distinto de 0) Un proceso que abortó con restartPolicy: Never
Unknown No se puede determinar el estado, normalmente porque no hay contacto con el nodo Nodo caído o partición de red

Un detalle importante: Running no significa "funcionando bien". Significa "hay procesos vivos". Un pod puede estar Running y devolver errores 500 a todo el mundo. Distinguir "vivo" de "sano" es exactamente el trabajo de las sondas, en Verificaciones de Salud y Sondas.

5.2. El estado de cada contenedor (status.containerStatuses[].state)

Cada contenedor tiene su propio estado, con solo tres valores, cada uno con una razón (reason) que es la que de verdad te dice qué pasa:

Estado Significado Razones habituales
Waiting Aún no se ejecuta; está haciendo algo previo o esperando ContainerCreating, ImagePullBackOff, ErrImagePull, CrashLoopBackOff, CreateContainerConfigError
Running Se está ejecutando; incluye startedAt —
Terminated Terminó; incluye exitCode, reason, startedAt y finishedAt Completed (código 0), Error, OOMKilled

La columna STATUS de kubectl get pods es un invento de la CLI que mezcla las dos cosas para ser útil: muestra la fase, salvo que haya una razón de contenedor más informativa, en cuyo caso la muestra a ella. Por eso ves ImagePullBackOff en esa columna aunque no sea una fase.

Para ver los datos reales, sin la mezcla:

kubectl get pod api-reservas -o jsonpath='{.status.phase}{"\n"}'
kubectl get pod api-reservas -o jsonpath='{range .status.containerStatuses[*]}{.name}{" -> "}{.state}{"\n"}{end}'
Running
api -> map[running:map[startedAt:2026-08-05T10:41:07Z]]

  1. Razones típicas: ContainerCreating, ImagePullBackOff, CrashLoopBackOff

Estas tres razones cubren la gran mayoría de los problemas que verás. Merecen tratamiento individual porque el diagnóstico de cada una es distinto.

6.1. ContainerCreating

Estado transitorio y normal: el kubelet está descargando la imagen, montando volúmenes y configurando la red del pod. Solo es un problema si no pasa de ahí. Si se queda minutos ahí, kubectl describe te dirá por qué: casi siempre un volumen que no se puede montar o un Secret/ConfigMap que no existe.

6.2. ImagePullBackOff y ErrImagePull

El kubelet no puede descargar la imagen. Primero verás ErrImagePull (fallo inmediato) y después ImagePullBackOff (reintentos con espera creciente). Provoquémoslo a propósito, usando el registro ficticio del proyecto:

kubectl run prueba-registro --image=registry.rutasnorte.example/api-reservas:2.4.0
kubectl get pod prueba-registro
pod/prueba-registro created

NAME              READY   STATUS             RESTARTS   AGE
prueba-registro   0/1     ImagePullBackOff   0          35s

El diagnóstico está siempre en los eventos:

kubectl describe pod prueba-registro | tail -8
Events:
  Type     Reason     Age                From               Message
  ----     ------     ----               ----               -------
  Normal   Scheduled  40s                default-scheduler  Successfully assigned rutas-norte-dev/prueba-registro to rutas-norte
  Normal   Pulling    39s                kubelet            Pulling image "registry.rutasnorte.example/api-reservas:2.4.0"
  Warning  Failed     24s                kubelet            Failed to pull image: dial tcp: lookup registry.rutasnorte.example: no such host
  Warning  Failed     24s                kubelet            Error: ErrImagePull
  Normal   BackOff    10s (x2 over 23s)  kubelet            Back-off pulling image

Las cuatro causas posibles, en orden de frecuencia:

  1. Nombre o etiqueta mal escritos (la más común con diferencia).
  2. Registro privado sin credenciales: falta un imagePullSecrets. Se ve en el módulo 8.
  3. Registro inalcanzable desde el nodo, como en este ejemplo.
  4. La etiqueta no existe en el registro: típico al desplegar una versión que aún no se ha publicado.

Limpia la prueba:

kubectl delete pod prueba-registro

6.3. CrashLoopBackOff

El más malinterpretado. No es un error en sí mismo: significa que el contenedor arranca, termina, el kubelet lo reinicia, y vuelve a terminar. Para no consumir el nodo, el kubelet espera cada vez más entre reintentos: 10s, 20s, 40s, 80s… hasta un máximo de 5 minutos. CrashLoopBackOff es el nombre de esa espera.

kubectl run api-rota --image=node:20-alpine -- node -e "console.error('falta DATABASE_URL'); process.exit(1)"
kubectl get pod api-rota -w
NAME       READY   STATUS             RESTARTS      AGE
api-rota   0/1     ContainerCreating  0             0s
api-rota   0/1     Error              0             3s
api-rota   0/1     CrashLoopBackOff   1 (5s ago)    8s
api-rota   0/1     Error              2 (18s ago)   21s
api-rota   0/1     CrashLoopBackOff   2 (14s ago)   35s

La clave del diagnóstico: la razón nunca está en describe, está en los logs del intento anterior.

kubectl logs api-rota --previous
falta DATABASE_URL

Ahí está la causa real. Recuerda el flag --previous (o -p): sin él, kubectl logs intenta leer el contenedor actual, que puede estar aún en la espera y no tener nada que contar.

Las causas más frecuentes de CrashLoopBackOff:

Causa Cómo se detecta
Error de configuración (falta una variable, credenciales inválidas) kubectl logs --previous muestra el mensaje
El proceso termina en cuanto arranca porque no es un servidor exitCode: 0 y aun así reinicia: restartPolicy: Always mal elegida
Falta de memoria reason: OOMKilled en describe; sube el limits.memory
Un command mal escrito exitCode: 127 (comando no encontrado) o 126 (no ejecutable)
Sonda de vitalidad mal configurada Reinicios cíclicos con la aplicación aparentemente sana (07-01)
kubectl delete pod api-rota

  1. restartPolicy: quién reinicia qué

spec.restartPolicy se aplica a todos los contenedores del pod y solo admite tres valores. Es un campo inmutable: no se puede cambiar en un pod ya creado.

Valor Comportamiento Cuándo se usa
Always (por defecto) Reinicia el contenedor siempre que termine, con éxito o con error Servicios de larga duración: tienda-web, api-reservas, redis-cache
OnFailure Reinicia solo si termina con código distinto de 0 Tareas que deben completarse: informes-ocupacion
Never No reinicia nunca Tareas de un solo intento, depuración

Tres precisiones que evitan confusiones muy comunes:

  1. Quien aplica la política es el kubelet del nodo, no un controlador del plano de control. Por eso funciona incluso en pods sueltos, sin ningún Deployment detrás.
  2. Reiniciar un contenedor no es recrear el pod. El pod sigue siendo el mismo objeto, con el mismo nombre, el mismo nodo y la misma IP. Solo sube el contador RESTARTS. Esta es la diferencia exacta que comprobaste en el ejercicio 3 del módulo 1.
  3. Los Deployments exigen Always. Como veremos en Deployments, su plantilla de pod no admite otro valor. OnFailure y Never son terreno de Trabajos y CronJobs.

  1. Ciclo de vida completo: de la creación al SIGTERM

8.1. Nacimiento

Este es el recorrido que ya viste en Arquitectura de Kubernetes, ahora centrado en el pod:

flowchart TD
    A["kubectl apply<br/>manifiesto del pod"] --> B["kube-apiserver valida,<br/>aplica valores por defecto<br/>y persiste en etcd"]
    B --> C["Fase: Pending<br/>spec.nodeName está vacío"]
    C --> D["kube-scheduler elige nodo<br/>y escribe spec.nodeName"]
    D --> E["kubelet del nodo lo detecta<br/>y crea el contenedor de pausa"]
    E --> F["Estado del contenedor: Waiting<br/>razón ContainerCreating"]
    F --> G["Descarga de imagen<br/>y montaje de volúmenes"]
    G --> H["Arranque de los contenedores"]
    H --> I["Fase: Running<br/>estado del contenedor: Running"]

8.2. Muerte: el apagado ordenado

Esta parte es la que casi nadie estudia y la que causa los errores 502 durante los despliegues. Cuando borras un pod, esto es lo que ocurre exactamente:

  1. La API marca el pod con deletionTimestamp y le asigna un plazo de gracia (terminationGracePeriodSeconds, 30 s por defecto). La fase pasa a Terminating.
  2. En paralelo, el pod se elimina de los Endpoints de todos los Servicios que lo seleccionaban, para dejar de recibir tráfico nuevo (02-05).
  3. El kubelet envía SIGTERM al proceso PID 1 de cada contenedor. Aquí es donde tu aplicación debe dejar de aceptar conexiones nuevas, terminar las que tenga en curso y cerrar limpiamente.
  4. Se espera hasta agotar el plazo de gracia.
  5. Si al terminar el plazo el proceso sigue vivo, el kubelet envía SIGKILL. Sin negociación posible.
  6. Se destruye el sandbox y el objeto desaparece de la API.
kubectl delete pod api-reservas
pod "api-reservas" deleted

Para verlo en cámara lenta, borra con un plazo largo desde otra terminal y observa:

kubectl delete pod api-reservas --grace-period=60 &
kubectl get pod api-reservas
NAME           READY   STATUS        RESTARTS   AGE
api-reservas   1/1     Terminating   0          4m12s

Consecuencias prácticas para Rutas Norte:

  • worker-notificaciones debe capturar SIGTERM. Si lo ignora, cada despliegue lo mata a los 30 segundos con SIGKILL a mitad de un envío de correo, y un cliente se queda sin su confirmación.
  • --grace-period=0 --force es peligroso. Le dice a la API que dé el pod por muerto sin esperar confirmación del kubelet. En una base de datos como postgres-reservas puede provocar corrupción o dos instancias escribiendo a la vez. Úsalo solo con pods atascados en un nodo perdido.
  • Un plazo de gracia demasiado corto corta peticiones en curso. Si api-reservas tiene peticiones que tardan 20 segundos, un plazo de 10 las cortará en seco.

  1. Pods efímeros para depurar

Un pod no es solo una carga de trabajo: también es la mejor navaja suiza de diagnóstico dentro del clúster. kubectl run con --rm -it crea un pod, te da una terminal dentro de él y lo borra al salir.

kubectl run depurador --rm -it --image=nicolaka/netshoot --restart=Never -- /bin/bash

Dentro tienes curl, dig, nslookup, ping, tcpdump, netstat... todo lo que la imagen de tu aplicación no lleva. Desglose de los flags:

Flag Qué hace
--rm Borra el pod al terminar la sesión
-it Terminal interactiva conectada (stdin + tty)
--restart=Never Crea un Pod suelto en lugar de un Deployment
-- /bin/bash Comando a ejecutar dentro del contenedor

Un ejemplo real que usaremos mucho a partir de la lección de Servicios: comprobar si api-reservas responde desde dentro del clúster.

kubectl run test-api --rm -it --image=curlimages/curl:8.8.0 --restart=Never -- \
  curl -s http://10.244.0.34:3000/disponibilidad
{"servicio":"api-reservas","ruta":"/disponibilidad"}
pod "test-api" deleted

Si lo que quieres es depurar un pod que ya existe y cuya imagen no tiene herramientas (nginx:alpine no trae ni curl), la solución moderna son los contenedores efímeros, que se inyectan en un pod vivo sin reiniciarlo:

kubectl debug -it tienda-web --image=nicolaka/netshoot --target=nginx

Comparte el namespace de red del contenedor nginx, así que puedes hacer curl http://localhost:80 como si fueras él. Es la herramienta que profundizaremos en Depuración de Aplicaciones y Eventos del Clúster.

  1. Por qué en producción nunca se gestionan pods sueltos

Ya lo experimentaste al final del módulo 1, pero ahora que conoces el ciclo de vida puedes entender la razón profunda: el pod no tiene mecanismo de recuperación propio. El kubelet reinicia sus contenedores, sí, pero el pod como objeto solo existe mientras alguien lo declare.

Y hay situaciones, muy frecuentes, en las que el pod desaparece sin que nadie lo borre:

Situación Qué le pasa al pod suelto
El nodo se apaga o se reinicia Desaparece para siempre; nadie lo recrea en otro nodo
El nodo se queda sin memoria El kubelet lo desaloja (Evicted) y no vuelve
Se drena el nodo para mantenimiento Se elimina y no se reprograma
Alguien lo borra por error No vuelve

Para Rutas Norte, cualquiera de esas cuatro filas equivale a la caída nocturna de cuatro horas y media que sufrió la empresa en marzo. Por eso la regla del proyecto es tajante:

En Rutas Norte, el único pod suelto admitido es un pod efímero de depuración con --rm. Todo lo demás va gobernado por un controlador.

Lo que necesitamos es algo que vigile permanentemente y diga "quiero 3 pods como este, siempre". Eso es un ReplicaSet, y es la lección siguiente.

Antes de continuar, deja el clúster con los dos pods en marcha:

kubectl apply -f k8s/base/api-reservas-pod.yaml
kubectl get pods
NAME           READY   STATUS    RESTARTS   AGE
api-reservas   1/1     Running   0          5s
tienda-web     1/1     Running   0          38m

Errores Comunes y Consejos

  • Meter varios procesos sin relación en el mismo pod. La pregunta de control es: "¿escalarían al mismo ritmo y morirían juntos?". Si la respuesta es no, son pods separados. Un api-reservas con su redis-cache dentro sería un error de diseño grave.
  • Confundir "reiniciar el contenedor" con "recrear el pod". El primero lo hace el kubelet, conserva nombre e IP y sube RESTARTS. El segundo lo hace un controlador y genera un objeto nuevo con IP nueva.
  • Buscar la causa de un CrashLoopBackOff en kubectl describe. Está en kubectl logs --previous. describe te dirá que reinicia, no por qué.
  • Interpretar Running como "funciona". Solo significa que hay procesos vivos. La salud real la determinan las sondas.
  • Olvidar --previous con contenedores que reinician. Sin él pierdes justo el log que te interesa.
  • Ignorar SIGTERM en la aplicación. Es la causa número uno de peticiones cortadas durante los despliegues. Captura la señal, deja de aceptar conexiones nuevas, drena las abiertas y sal con código 0.
  • Abusar de --force --grace-period=0. Salta el apagado ordenado. En cargas con estado puede corromper datos.
  • Poner dos contenedores escuchando en el mismo puerto dentro de un pod. Comparten el espacio de puertos: el segundo fallará al arrancar.
  • Consejo: genera el esqueleto de cualquier manifiesto con kubectl run api-reservas --image=node:20-alpine --dry-run=client -o yaml > pod.yaml y edítalo. Es mucho más rápido y seguro que escribir YAML desde cero.
  • Consejo: kubectl explain pod.spec.containers.lifecycle --recursive es tu documentación offline y siempre coincide con la versión de tu clúster.

Ejercicios

Ejercicio 1: Diagnosticar tres pods rotos

Crea deliberadamente estos tres pods en rutas-norte-dev y, para cada uno, identifica la fase del pod, el estado y la razón del contenedor y el comando exacto que revela la causa raíz:

  1. pod-a: imagen nginx:9.9.9-inventada.
  2. pod-b: imagen redis:7.2-alpine con command: ["redis-server", "--fichero-inexistente"].
  3. pod-c: imagen busybox:1.36 con command: ["sh", "-c", "echo informe generado"] y restartPolicy: Never.

Explica por qué el tercero no es un error, aunque su STATUS no sea Running.

Ejercicio 2: Compartición dentro de un pod

Escribe k8s/base/pod-compartido.yaml con un pod llamado demo-compartido en rutas-norte-dev que tenga dos contenedores y un volumen emptyDir llamado intercambio:

  • escritor: imagen busybox:1.36, escribe cada 5 segundos una línea con la fecha en /datos/ocupacion.log.
  • lector: imagen busybox:1.36, hace tail -f de ese mismo fichero montado en /entrada/ocupacion.log.

Después demuestra con comandos: (a) que el lector ve lo que escribe el escritor, (b) que ambos contenedores comparten la misma IP y (c) que no comparten el sistema de ficheros raíz.

Ejercicio 3: Apagado ordenado de worker-notificaciones

Crea un pod worker-notificaciones con busybox:1.36 que capture SIGTERM, escriba en el log cerrando: terminando envios pendientes, espere 5 segundos y salga. Fija terminationGracePeriodSeconds: 30.

  1. Bórralo y comprueba en los logs que el mensaje se escribió antes de morir.
  2. Repite el experimento con terminationGracePeriodSeconds: 2 y explica qué cambia y por qué es peligroso para Rutas Norte.

Soluciones

Solución 1

kubectl run pod-a --image=nginx:9.9.9-inventada
kubectl run pod-b --image=redis:7.2-alpine -- redis-server --fichero-inexistente
kubectl run pod-c --image=busybox:1.36 --restart=Never -- sh -c "echo informe generado"
kubectl get pods
NAME    READY   STATUS             RESTARTS      AGE
pod-a   0/1     ImagePullBackOff   0             45s
pod-b   0/1     CrashLoopBackOff   3 (28s ago)   75s
pod-c   0/1     Completed          0             45s
Pod Fase Estado y razón del contenedor Comando de diagnóstico
pod-a Pending Waiting / ImagePullBackOff kubectl describe pod pod-a (sección Events)
pod-b Running Waiting / CrashLoopBackOff kubectl logs pod-b --previous
pod-c Succeeded Terminated / Completed, exitCode: 0 kubectl logs pod-c
kubectl describe pod pod-a | grep -A3 Events
kubectl logs pod-b --previous
kubectl logs pod-c
  Warning  Failed  30s  kubelet  Failed to pull image "nginx:9.9.9-inventada": not found

Bad directive: --fichero-inexistente

informe generado

pod-c no es un error: la etiqueta 9.9.9-inventada de pod-a no existe en el registro y el redis-server de pod-b rechaza el parámetro. En cambio pod-c hizo exactamente lo que se le pidió, terminó con código 0 y, gracias a restartPolicy: Never, no se reinició: fase Succeeded. Es el comportamiento normal de una tarea finita como informes-ocupacion. Si le hubiéramos dejado restartPolicy: Always, entraría en CrashLoopBackOff pese a no fallar nunca, porque Always reinicia también las salidas con éxito.

kubectl delete pod pod-a pod-b pod-c

Solución 2

# k8s/base/pod-compartido.yaml
apiVersion: v1
kind: Pod
metadata:
  name: demo-compartido
  namespace: rutas-norte-dev
  labels:
    app: demo-compartido
    app.kubernetes.io/part-of: rutas-norte
    entorno: dev
spec:
  containers:
    - name: escritor
      image: busybox:1.36
      command: ["sh", "-c"]
      args: ["while true; do echo \"$(date) ocupacion calculada\" >> /datos/ocupacion.log; sleep 5; done"]
      volumeMounts:
        - name: intercambio
          mountPath: /datos
    - name: lector
      image: busybox:1.36
      command: ["sh", "-c"]
      args: ["sleep 3; tail -f /entrada/ocupacion.log"]
      volumeMounts:
        - name: intercambio
          mountPath: /entrada
  volumes:
    - name: intercambio
      emptyDir: {}
kubectl apply -f k8s/base/pod-compartido.yaml
# (a) el lector ve lo que escribe el escritor
kubectl logs demo-compartido -c lector --tail=3
Wed Aug  5 11:02:14 UTC 2026 ocupacion calculada
Wed Aug  5 11:02:19 UTC 2026 ocupacion calculada
Wed Aug  5 11:02:24 UTC 2026 ocupacion calculada
# (b) misma IP: la del pod
kubectl exec demo-compartido -c escritor -- hostname -i
kubectl exec demo-compartido -c lector -- hostname -i
10.244.0.37
10.244.0.37
# (c) sistemas de ficheros raiz distintos
kubectl exec demo-compartido -c escritor -- ls /datos
kubectl exec demo-compartido -c lector -- ls /datos
ocupacion.log
ls: /datos: No such file or directory

El fichero solo existe bajo /datos para el escritor y bajo /entrada para el lector: el volumen se comparte, el sistema de ficheros raíz no. Cada contenedor ve el volumen en la ruta donde lo montó, y nada más del otro.

kubectl delete -f k8s/base/pod-compartido.yaml

Solución 3

# k8s/base/worker-notificaciones-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: worker-notificaciones
  namespace: rutas-norte-dev
  labels:
    app: worker-notificaciones
    app.kubernetes.io/part-of: rutas-norte
    entorno: dev
spec:
  terminationGracePeriodSeconds: 30
  containers:
    - name: worker
      image: busybox:1.36
      command: ["sh", "-c"]
      args:
        - |
          trap 'echo "cerrando: terminando envios pendientes"; sleep 5; exit 0' TERM
          echo "worker-notificaciones listo"
          while true; do sleep 1; done
kubectl apply -f k8s/base/worker-notificaciones-pod.yaml
kubectl delete pod worker-notificaciones --wait=false
sleep 3 && kubectl logs worker-notificaciones
worker-notificaciones listo
cerrando: terminando envios pendientes

El proceso recibió SIGTERM, ejecutó su rutina de cierre y salió por su propio pie dentro del plazo de 30 segundos.

Con terminationGracePeriodSeconds: 2, el kubelet envía SIGTERM, espera solo 2 segundos y manda SIGKILL cuando el worker todavía está en su sleep 5. El proceso muere a mitad del cierre. Para Rutas Norte eso significa un correo de confirmación de reserva que se queda a medio enviar en cada despliegue: el cliente ha pagado su billete y no recibe justificante. La regla es dimensionar el plazo de gracia por encima del tiempo real que tarda el componente en drenar su trabajo pendiente.

kubectl delete pod worker-notificaciones --ignore-not-found

Conclusión

El pod ha dejado de ser una caja negra. Sabes por qué Kubernetes eligió el pod y no el contenedor como unidad mínima: hay procesos que necesitan compartir red, volúmenes y ciclo de vida, y el planificador debe tratarlos como una sola cosa indivisible. Conoces exactamente qué comparten sus contenedores —namespace de red con una única IP, localhost, IPC, volúmenes, nodo y destino— y qué no —sistema de ficheros raíz, PIDs y recursos—, y sabes que quien sostiene esos namespaces es el discreto contenedor de pausa, la razón de que la IP sobreviva a los reinicios.

Has escrito el manifiesto de api-reservas campo a campo, distinguiendo command/args de ENTRYPOINT/CMD, y has aprendido a leer el estado real de un pod separando las cinco fases (Pending, Running, Succeeded, Failed, Unknown) de los tres estados de contenedor (Waiting, Running, Terminated) con sus razones: ContainerCreating es normal, ImagePullBackOff se diagnostica en los eventos y CrashLoopBackOff en kubectl logs --previous. Sabes qué hace restartPolicy y quién la aplica, y has seguido el ciclo de vida completo hasta el final incómodo: SIGTERM, plazo de gracia y SIGKILL, la diferencia entre un despliegue limpio y un cliente sin su confirmación de reserva. Y tienes ya en el cinturón los pods efímeros --rm -it y kubectl debug como herramientas de diagnóstico.

Queda pendiente lo que nos trajo hasta aquí: el pod, por sí solo, no se recupera. Si el nodo cae, si lo desalojan por falta de memoria o si alguien lo borra, api-reservas desaparece del clúster y no vuelve. En la lección siguiente, ReplicaSets, conoceremos al primer controlador del curso: el que ejecuta sin descanso el bucle de reconciliación que estudiamos en la arquitectura, cuenta cuántos pods hay frente a cuántos deberían existir y crea los que faltan. Borraremos un pod de tienda-web a propósito y lo veremos renacer en segundos.

Curso de Kubernetes

Módulo 1: Introducción a Kubernetes

Módulo 2: Componentes Principales de Kubernetes

Módulo 3: Gestión de Configuración y Secretos

Módulo 4: Redes en Kubernetes

Módulo 5: Almacenamiento en Kubernetes

Módulo 6: Conceptos Avanzados de Kubernetes

Módulo 7: Monitoreo y Registro

Módulo 8: Seguridad en Kubernetes

Módulo 9: Escalado y Rendimiento

Módulo 10: Ecosistema y Herramientas de Kubernetes

Módulo 11: Estudios de Caso y Aplicaciones del Mundo Real

Módulo 12: Preparación para la Certificación de Kubernetes

© Copyright 2026. Todos los derechos reservados