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
- Por qué la unidad mínima es el pod y no el contenedor
- Qué comparten los contenedores de un mismo pod (y qué no)
- El contenedor de pausa: el que sostiene el pod
- Anatomía de un manifiesto de pod:
api-reservascampo a campo - Fases del pod y estados de los contenedores
- Razones típicas:
ContainerCreating,ImagePullBackOff,CrashLoopBackOff restartPolicy: quién reinicia qué- Ciclo de vida completo: de la creación al
SIGTERM - Pods efímeros para depurar
- Por qué en producción nunca se gestionan pods sueltos
- 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-reservasa 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.
- 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
nginxen el pod, el segundo fallará con "address already in use".
- 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:
- Se crea el primero, antes que cualquier contenedor tuyo.
- Crea y posee los namespaces de red, IPC y UTS del pod. Es quien recibe la IP.
- Los demás contenedores del pod se arrancan uniéndose a los namespaces del contenedor de pausa.
- Su único trabajo es dormir. Su código fuente ocupa unas pocas decenas de líneas: se queda bloqueado esperando señales y hace
reapde 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:
No tienes que gestionarlo nunca. Pero conocerlo evita confusiones cuando leas documentación o depures a bajo nivel.
- Anatomía de un manifiesto de pod:
api-reservas campo a campo
api-reservas campo a campoVamos 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 akubectl logs -cokubectl exec -ccuando hay varios.image: etiqueta explícita, jamáslatest, según la convención del proyecto.command/args: sobrescriben elENTRYPOINTy elCMDde 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 usafieldRefpara inyectar un dato del propio pod (la downward API). Las fuentes serias de configuración —ConfigMaps y Secrets— llegan en Variables de Entorno.resources:requestses lo que el scheduler reserva;limitses el techo que impone el kubelet. Detalle completo en Cuotas y Límites de Recursos.volumeMounts/volumes: el volumenemptyDires 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:
pod/api-reservas created
NAME READY STATUS RESTARTS AGE
api-reservas 1/1 Running 0 6s
tienda-web 1/1 Running 0 22mFíjate en que hemos llamado a localhost:3000 desde dentro del propio pod: eso es el namespace de red compartido en acción.
- 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}'
- Razones típicas:
ContainerCreating, ImagePullBackOff, CrashLoopBackOff
ContainerCreating, ImagePullBackOff, CrashLoopBackOffEstas 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-registropod/prueba-registro created
NAME READY STATUS RESTARTS AGE
prueba-registro 0/1 ImagePullBackOff 0 35sEl diagnóstico está siempre en los eventos:
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 imageLas cuatro causas posibles, en orden de frecuencia:
- Nombre o etiqueta mal escritos (la más común con diferencia).
- Registro privado sin credenciales: falta un
imagePullSecrets. Se ve en el módulo 8. - Registro inalcanzable desde el nodo, como en este ejemplo.
- La etiqueta no existe en el registro: típico al desplegar una versión que aún no se ha publicado.
Limpia la prueba:
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 -wNAME 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) 35sLa clave del diagnóstico: la razón nunca está en describe, está en los logs del intento anterior.
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) |
restartPolicy: quién reinicia qué
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:
- 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.
- 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. - Los Deployments exigen
Always. Como veremos en Deployments, su plantilla de pod no admite otro valor.OnFailureyNeverson terreno de Trabajos y CronJobs.
- Ciclo de vida completo: de la creación al
SIGTERM
SIGTERM8.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:
- La API marca el pod con
deletionTimestampy le asigna un plazo de gracia (terminationGracePeriodSeconds, 30 s por defecto). La fase pasa aTerminating. - 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).
- El kubelet envía
SIGTERMal 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. - Se espera hasta agotar el plazo de gracia.
- Si al terminar el plazo el proceso sigue vivo, el kubelet envía
SIGKILL. Sin negociación posible. - Se destruye el sandbox y el objeto desaparece de la API.
Para verlo en cámara lenta, borra con un plazo largo desde otra terminal y observa:
Consecuencias prácticas para Rutas Norte:
worker-notificacionesdebe capturarSIGTERM. Si lo ignora, cada despliegue lo mata a los 30 segundos conSIGKILLa mitad de un envío de correo, y un cliente se queda sin su confirmación.--grace-period=0 --forcees peligroso. Le dice a la API que dé el pod por muerto sin esperar confirmación del kubelet. En una base de datos comopostgres-reservaspuede 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-reservastiene peticiones que tardan 20 segundos, un plazo de 10 las cortará en seco.
- 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.
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/disponibilidadSi 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:
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.
- 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:
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-reservascon suredis-cachedentro 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
CrashLoopBackOffenkubectl describe. Está enkubectl logs --previous.describete dirá que reinicia, no por qué. - Interpretar
Runningcomo "funciona". Solo significa que hay procesos vivos. La salud real la determinan las sondas. - Olvidar
--previouscon contenedores que reinician. Sin él pierdes justo el log que te interesa. - Ignorar
SIGTERMen 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.yamly edítalo. Es mucho más rápido y seguro que escribir YAML desde cero. - Consejo:
kubectl explain pod.spec.containers.lifecycle --recursivees 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:
pod-a: imagennginx:9.9.9-inventada.pod-b: imagenredis:7.2-alpineconcommand: ["redis-server", "--fichero-inexistente"].pod-c: imagenbusybox:1.36concommand: ["sh", "-c", "echo informe generado"]yrestartPolicy: 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: imagenbusybox:1.36, escribe cada 5 segundos una línea con la fecha en/datos/ocupacion.log.lector: imagenbusybox:1.36, hacetail -fde 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.
- Bórralo y comprueba en los logs que el mensaje se escribió antes de morir.
- Repite el experimento con
terminationGracePeriodSeconds: 2y 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 podsNAME 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 |
Warning Failed 30s kubelet Failed to pull image "nginx:9.9.9-inventada": not found
Bad directive: --fichero-inexistente
informe generadopod-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.
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=3Wed 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# (c) sistemas de ficheros raiz distintos
kubectl exec demo-compartido -c escritor -- ls /datos
kubectl exec demo-compartido -c lector -- ls /datosEl 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.
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; donekubectl apply -f k8s/base/worker-notificaciones-pod.yaml
kubectl delete pod worker-notificaciones --wait=false
sleep 3 && kubectl logs worker-notificacionesEl 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.
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
- ¿Qué es Kubernetes?
- Arquitectura de Kubernetes
- Conceptos y Terminología Clave
- Configuración de un Clúster de Kubernetes
- La CLI de Kubernetes: kubectl
- Objetos, Manifiestos YAML y el Modelo Declarativo
- El Proyecto del Curso: la Plataforma Rutas Norte
Módulo 2: Componentes Principales de Kubernetes
- Pods
- ReplicaSets
- Deployments
- Actualizaciones, Rollbacks y Estrategias de Despliegue
- Servicios
- Namespaces
- Etiquetas, Selectores y Anotaciones
Módulo 3: Gestión de Configuración y Secretos
- ConfigMaps
- Secrets
- Variables de Entorno
- Cuotas y Límites de Recursos
- LimitRanges y Clases de Calidad de Servicio (QoS)
- ServiceAccounts y Acceso a la API desde los Pods
Módulo 4: Redes en Kubernetes
- Redes de Clúster
- Tipos de Servicios
- DNS Interno y Descubrimiento de Servicios
- Controladores de Ingress
- TLS y Gestión de Certificados con cert-manager
- Políticas de Red
Módulo 5: Almacenamiento en Kubernetes
- Volúmenes
- Volúmenes Persistentes
- Reclamaciones de Volúmenes Persistentes
- Clases de Almacenamiento
- Aprovisionamiento Dinámico, Expansión y Snapshots
- Copias de Seguridad y Restauración de Datos
Módulo 6: Conceptos Avanzados de Kubernetes
- StatefulSets
- DaemonSets
- Trabajos y CronJobs
- Init Containers, Sidecars y Patrones Multi-Contenedor
- Planificación: Afinidad, Taints y Tolerations
- Definiciones de Recursos Personalizados (CRDs)
- Operadores y el Patrón Controlador
Módulo 7: Monitoreo y Registro
- Verificaciones de Salud y Sondas
- Servidor de Métricas y kubectl top
- Monitoreo con Prometheus
- Visualización y Alertas con Grafana y Alertmanager
- Registro Centralizado con Elasticsearch, Fluentd y Kibana (EFK)
- Depuración de Aplicaciones y Eventos del Clúster
Módulo 8: Seguridad en Kubernetes
- Control de Acceso Basado en Roles (RBAC)
- Contextos de Seguridad y Endurecimiento del Contenedor
- Políticas de Seguridad de Pods y Pod Security Standards
- Seguridad de Red
- Seguridad de Imágenes
- Auditoría, Escaneo y Gestión de Vulnerabilidades
Módulo 9: Escalado y Rendimiento
- Autoescalado Horizontal de Pods
- Autoescalado Vertical de Pods
- Autoescalado de Clúster
- Escalado por Eventos y Métricas Personalizadas con KEDA
- Alta Disponibilidad: PodDisruptionBudgets y Topología
- Ajuste de Rendimiento
Módulo 10: Ecosistema y Herramientas de Kubernetes
- Minikube y Entornos Locales con kind
- Kubeadm
- Helm
- Kustomize
- GitOps con Argo CD y Flux
- Kubernetes Gestionado: EKS, AKS y GKE
Módulo 11: Estudios de Caso y Aplicaciones del Mundo Real
- Despliegue de una Aplicación Web
- Ejecución de Aplicaciones con Estado
- CI/CD con Kubernetes
- Estrategias de Despliegue: Blue-Green y Canary
- Gestión Multi-Clúster
- Operación en Producción: Incidencias, Runbooks y Costes
