La lección anterior terminó con una carencia muy concreta: un ReplicaSet mantiene con vida N copias de una plantilla, pero no sabe cambiar de versión. Cambiabas la imagen del template y los pods existentes seguían tan tranquilos con la imagen antigua. El Deployment es la pieza que cierra ese hueco y, con diferencia, el objeto que más vas a escribir en tu vida profesional con Kubernetes. Gobierna ReplicaSets, y a través de ellos, pods: te da réplicas, autorreparación, escalado, actualizaciones controladas, historial de revisiones y marcha atrás. En esta lección entenderás la cadena Deployment → ReplicaSet → Pod y qué aporta exactamente cada eslabón, convertirás por fin el pod suelto de tienda-web en un Deployment de 3 réplicas, crearás el de api-reservas, aprenderás a leer con precisión las columnas de kubectl get deploy y las conditions del estado, escalarás de dos formas distintas y observarás qué ocurre en el clúster al cambiar una imagen. Terminarás sabiendo también cuándo un Deployment no es la carga de trabajo adecuada.
Contenido
- La cadena Deployment → ReplicaSet → Pod
- De pod suelto a Deployment:
tienda-web - El manifiesto completo, comentado
- El Deployment de
api-reservas - Leer
kubectl get deploy:READY,UP-TO-DATE,AVAILABLE status,conditionsykubectl rollout status- Escalado:
kubectl scaley manifiesto - Observación: qué ocurre al cambiar la imagen
- Cuándo un Deployment no es la carga adecuada
- La cadena Deployment → ReplicaSet → Pod
Kubernetes podría haber metido toda la funcionalidad en un solo objeto. No lo hizo, y la razón es buena: cada nivel tiene una responsabilidad y solo una.
flowchart TD
D["<b>Deployment</b> tienda-web<br/>Gestiona VERSIONES<br/>historial, actualización, rollback"]
RS1["<b>ReplicaSet</b> tienda-web-7d9f8c6b4<br/>versión 1.27.0 · réplicas: 3<br/>Gestiona CANTIDAD"]
RS2["<b>ReplicaSet</b> tienda-web-5c8b7a2d1<br/>versión 1.26.0 · réplicas: 0<br/>revisión anterior, conservada"]
P1["Pod tienda-web-7d9f8c6b4-4kx7d"]
P2["Pod tienda-web-7d9f8c6b4-9wq2m"]
P3["Pod tienda-web-7d9f8c6b4-pv6cl"]
D --> RS1
D --> RS2
RS1 --> P1
RS1 --> P2
RS1 --> P3
La división de trabajo, en una tabla:
| Nivel | Su única pregunta | Qué sabe hacer | Qué no sabe hacer |
|---|---|---|---|
| Pod | ¿Están vivos mis contenedores? | Reiniciar contenedores caídos (vía kubelet y restartPolicy) |
Recrearse si desaparece |
| ReplicaSet | ¿Hay N pods con estas etiquetas? | Crear y borrar pods hasta cuadrar el número | Cambiar de versión |
| Deployment | ¿Qué versión debe estar sirviendo, y cómo llego a ella? | Crear ReplicaSets, trasvasar réplicas entre ellos, guardar historial, deshacer | Nada más: delega todo lo demás |
La clave para entender los Deployments es esta frase: un Deployment no gestiona pods, gestiona ReplicaSets. Cada vez que cambias algo del template, el Deployment crea un ReplicaSet nuevo y va moviendo réplicas del viejo al nuevo. Un ReplicaSet por revisión, y cada uno sabe mantener su propia cantidad de pods. El Deployment orquesta el trasvase.
Ese sufijo hexadecimal de los nombres (tienda-web-7d9f8c6b4) no es aleatorio: es el pod-template-hash, un hash calculado sobre el contenido del template. El Deployment lo añade como etiqueta a cada pod y al selector de cada ReplicaSet, y así consigue que dos ReplicaSets del mismo Deployment nunca se peleen por los mismos pods. Es exactamente la clase de selector de conjuntos que, como vimos, el viejo ReplicationController no podía expresar.
- De pod suelto a Deployment:
tienda-web
tienda-webAl final del módulo 1 dejamos tienda-web como un pod suelto, y comprobamos que al borrarlo no volvía. Ha llegado el momento de saldar esa deuda.
Partimos del namespace vacío, tal como quedó al final de la lección anterior:
La conversión de un pod a un Deployment sigue siempre el mismo patrón mecánico:
| Pod suelto | Deployment |
|---|---|
apiVersion: v1 |
apiVersion: apps/v1 |
kind: Pod |
kind: Deployment |
metadata |
metadata (del Deployment) |
| — | spec.replicas |
| — | spec.selector |
spec: (los contenedores) |
spec.template.spec |
metadata.labels |
spec.template.metadata.labels |
Es decir: el pod entero se hunde un nivel y se convierte en el template, y por encima aparecen replicas y selector.
- El manifiesto completo, comentado
# k8s/base/tienda-web-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: tienda-web
namespace: rutas-norte-dev
labels:
app: tienda-web
app.kubernetes.io/part-of: rutas-norte
entorno: dev
annotations:
kubernetes.io/change-cause: "Despliegue inicial de tienda-web 1.27.0"
spec:
replicas: 3
revisionHistoryLimit: 5
selector:
matchLabels:
app: tienda-web
entorno: dev
template:
metadata:
labels:
app: tienda-web
app.kubernetes.io/part-of: rutas-norte
entorno: dev
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
ports:
- name: http
containerPort: 80
resources:
requests:
cpu: "50m"
memory: "64Mi"
limits:
cpu: "200m"
memory: "128Mi"Lo nuevo respecto al ReplicaSet de la lección anterior, campo a campo:
kind: Deployment, mismo grupoapps/v1.metadata.annotationsconkubernetes.io/change-cause: texto libre que quedará registrado en el historial de revisiones como el "motivo" de este despliegue. Lo explotaremos a fondo en la lección siguiente.spec.replicas: 3: tres réplicas detienda-web. El Deployment no las crea él; se lo encarga a su ReplicaSet.spec.revisionHistoryLimit: 5: cuántos ReplicaSets antiguos (con 0 réplicas) se conservan para poder deshacer. Por defecto son 10.spec.selector: obligatorio e inmutable, igual que en el ReplicaSet. Y con el mismo aviso: que sea preciso,app+entorno.spec.template: idéntico al del ReplicaSet. Aquí está la clave conceptual: cualquier cambio bajotemplatedispara una nueva revisión; los cambios fuera de él (comoreplicas) no.
No aparece spec.strategy porque el valor por defecto, RollingUpdate, es el que queremos. Ese campo y todos sus parámetros son el contenido íntegro de la lección siguiente, Actualizaciones, Rollbacks y Estrategias de Despliegue.
Aplícalo:
deployment.apps/tienda-web created
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/tienda-web 3/3 3 3 9s
NAME DESIRED CURRENT READY AGE
replicaset.apps/tienda-web-7d9f8c6b4 3 3 3 9s
NAME READY STATUS RESTARTS AGE
pod/tienda-web-7d9f8c6b4-4kx7d 1/1 Running 0 9s
pod/tienda-web-7d9f8c6b4-9wq2m 1/1 Running 0 9s
pod/tienda-web-7d9f8c6b4-pv6cl 1/1 Running 0 9sAhí está la cadena completa, materializada en tres bloques de salida. Has creado un objeto y han aparecido tres niveles. Comprueba la genealogía:
kubectl get rs tienda-web-7d9f8c6b4 -o jsonpath='{.metadata.ownerReferences[0].kind}/{.metadata.ownerReferences[0].name}{"\n"}'
kubectl get pod tienda-web-7d9f8c6b4-4kx7d -o jsonpath='{.metadata.ownerReferences[0].kind}/{.metadata.ownerReferences[0].name}{"\n"}'Y mira la etiqueta que ha añadido el Deployment por su cuenta a los pods:
NAME READY STATUS RESTARTS AGE LABELS
tienda-web-7d9f8c6b4-4kx7d 1/1 Running 0 2m app=tienda-web,app.kubernetes.io/part-of=rutas-norte,entorno=dev,pod-template-hash=7d9f8c6b4Tú no escribiste pod-template-hash. Lo añadió el Deployment, y es lo que le permitirá distinguir los pods de una revisión de los de otra.
Verifica que ahora sí se autorrepara:
pod "tienda-web-7d9f8c6b4-4kx7d" deleted
NAME READY STATUS RESTARTS AGE
tienda-web-7d9f8c6b4-9wq2m 1/1 Running 0 3m
tienda-web-7d9f8c6b4-hs4bd 1/1 Running 0 2s
tienda-web-7d9f8c6b4-pv6cl 1/1 Running 0 3mEl pod suelto del módulo 1 no volvía. Este vuelve en dos segundos, y quien lo devuelve no es el Deployment sino su ReplicaSet, exactamente como aprendiste en la lección anterior.
- El Deployment de
api-reservas
api-reservasSegundo componente. Mismo esquema, con las particularidades de la API.
# k8s/base/api-reservas-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-reservas
namespace: rutas-norte-dev
labels:
app: api-reservas
app.kubernetes.io/part-of: rutas-norte
entorno: dev
annotations:
kubernetes.io/change-cause: "Despliegue inicial de api-reservas 2.4.0"
spec:
replicas: 2
revisionHistoryLimit: 5
selector:
matchLabels:
app: api-reservas
entorno: dev
template:
metadata:
labels:
app: api-reservas
app.kubernetes.io/part-of: rutas-norte
entorno: dev
spec:
terminationGracePeriodSeconds: 30
containers:
- name: api
image: node:20-alpine
command: ["node", "-e"]
args:
- |
const http = require('http');
const pod = process.env.HOSTNAME;
http.createServer((req, res) => {
res.writeHead(200, {'Content-Type': 'application/json'});
res.end(JSON.stringify({servicio: 'api-reservas', version: '2.4.0', pod}));
}).listen(3000, () => console.log(`api-reservas 2.4.0 lista en ${pod}`));
ports:
- name: http
containerPort: 3000
env:
- name: ENTORNO
value: "dev"
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"Detalles deliberados de este manifiesto:
replicas: 2: en desarrollo bastan dos. En producción el número lo decidirá el autoescalado del módulo 9.terminationGracePeriodSeconds: 30dentro deltemplate.spec: recuerda de la lección de Pods que es lo que permite que una reserva en curso no se corte durante un despliegue.- La respuesta incluye el nombre del pod (
process.env.HOSTNAME, que en un pod es su nombre). Esto nos servirá en la lección de Servicios para demostrar el balanceo de carga: cada petición responderá con un pod distinto.
deployment.apps/api-reservas created
NAME READY UP-TO-DATE AVAILABLE AGE
api-reservas 2/2 2 2 14s
tienda-web 3/3 3 3 9mComprueba que la API responde, usando un pod efímero como los de la lección de Pods:
kubectl run test --rm -it --image=curlimages/curl:8.8.0 --restart=Never -- \
curl -s http://$(kubectl get pod -l app=api-reservas -o jsonpath='{.items[0].status.podIP}'):3000/disponibilidad{"servicio":"api-reservas","version":"2.4.0","pod":"api-reservas-6b4c9d7f5-x2jkp"}
pod "test" deletedFunciona, pero fíjate en la incomodidad: hemos tenido que averiguar la IP de un pod concreto para hablarle. Esa IP cambiará en el próximo despliegue. El problema está servido para la lección de Servicios.
- Leer
kubectl get deploy: READY, UP-TO-DATE, AVAILABLE
kubectl get deploy: READY, UP-TO-DATE, AVAILABLEEstas tres columnas son la información más densa de todo Kubernetes, y confundirlas lleva a diagnósticos equivocados. Las tres cuentan pods, pero cuentan cosas distintas.
| Columna | Qué cuenta exactamente | Campo del status |
|---|---|---|
READY |
pods listos / pods deseados. "Listo" = todos sus contenedores pasan sus comprobaciones |
readyReplicas / replicas |
UP-TO-DATE |
Pods creados con la revisión actual del template |
updatedReplicas |
AVAILABLE |
Pods que llevan listos al menos minReadySeconds seguidos |
availableReplicas |
La forma útil de leerlas es preguntarse qué significa cada discrepancia:
| Situación | Interpretación |
|---|---|
READY 3/3, UP-TO-DATE 3, AVAILABLE 3 |
Todo correcto y estable |
READY 2/3 |
Un pod no está listo: arrancando, o fallando |
UP-TO-DATE 1 de 3 |
Actualización en curso: solo 1 pod tiene la versión nueva |
READY 3/3 pero AVAILABLE 2 |
Un pod está listo pero aún no ha cumplido minReadySeconds; se considera "joven" |
READY 0/3 durante minutos |
Despliegue roto: mira los pods con describe |
Un ejemplo de actualización a medias, que verás mucho:
Eso se lee así: hay 4 pods listos aunque el deseado sean 4, pero solo 2 tienen la versión nueva, y solo 3 llevan el tiempo suficiente para contarse como disponibles. Estamos en mitad de un despliegue progresivo, y todavía conviven pods de dos revisiones.
Para ver más, -o wide añade imágenes y selector:
NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
api-reservas 2/2 2 2 14m api node:20-alpine app=api-reservas,entorno=dev
tienda-web 3/3 3 3 23m nginx nginx:1.27-alpine app=tienda-web,entorno=dev
status, conditions y kubectl rollout status
status, conditions y kubectl rollout statusComo todo objeto de Kubernetes, un Deployment tiene un spec (lo que pides) y un status (lo que hay). El status completo:
{
"availableReplicas": 3,
"conditions": [
{
"lastTransitionTime": "2026-08-05T12:03:11Z",
"lastUpdateTime": "2026-08-05T12:03:11Z",
"message": "Deployment has minimum availability.",
"reason": "MinimumReplicasAvailable",
"status": "True",
"type": "Available"
},
{
"lastTransitionTime": "2026-08-05T12:03:05Z",
"lastUpdateTime": "2026-08-05T12:03:11Z",
"message": "ReplicaSet \"tienda-web-7d9f8c6b4\" has successfully progressed.",
"reason": "NewReplicaSetAvailable",
"status": "True",
"type": "Progressing"
}
],
"observedGeneration": 1,
"readyReplicas": 3,
"replicas": 3,
"updatedReplicas": 3
}Las conditions son el mecanismo estándar de Kubernetes para expresar estados parciales, y un Deployment tiene tres tipos:
| Condition | status: True significa |
status: False significa |
|---|---|---|
Available |
Hay suficientes réplicas disponibles para dar servicio | El Deployment no está sirviendo con garantías |
Progressing |
El despliegue avanza (o terminó bien) | Se ha atascado: superó progressDeadlineSeconds |
ReplicaFailure |
(solo aparece si hay problema) No se pueden crear pods: cuota, permisos, límites | — |
La combinación Available: True + Progressing: True con razón NewReplicaSetAvailable es la firma de un Deployment sano y terminado.
Otro campo que merece atención es observedGeneration. Cada cambio en el spec incrementa metadata.generation; el controlador copia ese número a status.observedGeneration cuando ha procesado el cambio. Si ves generation: 5 y observedGeneration: 4, el controlador aún no ha reaccionado a tu último cambio.
En el día a día no lees el JSON: usas el comando que lo interpreta por ti.
kubectl rollout status bloquea la terminal hasta que el despliegue termina y devuelve código de salida 0 si va bien, distinto de 0 si falla. Por eso es la instrucción que se pone en cualquier pipeline de CI/CD justo detrás del kubectl apply: convierte "he enviado el cambio" en "el cambio está funcionando". Admite --timeout:
kubectl apply -f k8s/base/tienda-web-deployment.yaml
kubectl rollout status deployment/tienda-web --timeout=120s
- Escalado:
kubectl scale y manifiesto
kubectl scale y manifiestoRutas Norte tiene un puente a la vista y hay que subir réplicas de api-reservas. Dos vías, las mismas que con el ReplicaSet.
Imperativa, para reaccionar ya:
Observa un detalle importantísimo:
No ha aparecido un ReplicaSet nuevo. Escalar no cambia el template, así que no es una revisión nueva: el mismo ReplicaSet pasa de 2 a 4. Esta distinción —cambios que crean revisión frente a cambios que no— es el eje de la lección siguiente.
Declarativa, la correcta según las convenciones del proyecto: edita replicas: 4 en k8s/base/api-reservas-deployment.yaml, haz commit y aplica.
Y una advertencia práctica muy común: si escalas con kubectl scale y no llevas el cambio al fichero, el siguiente kubectl apply devolverá las réplicas al valor del manifiesto. Se te caerá la capacidad extra en el peor momento posible. Es la razón de la regla del proyecto: el clúster refleja Git, no al revés.
Un tercer camino, útil para automatizar sin editar ficheros a mano:
Volvamos a 2 antes de continuar:
- Observación: qué ocurre al cambiar la imagen
Esta es la observación que motiva toda la lección siguiente. Cambia la versión de nginx de tienda-web y mira la estructura que aparece, sin preocuparte todavía del mecanismo.
deployment.apps/tienda-web image updated
NAME DESIRED CURRENT READY AGE
tienda-web-5f6d8b9c7 3 3 3 25s
tienda-web-7d9f8c6b4 0 0 0 31mHay dos ReplicaSets. El nuevo (5f6d8b9c7) tiene las 3 réplicas; el viejo (7d9f8c6b4) se ha quedado a 0 pero no se ha borrado. Y ahí está la clave del historial: ese ReplicaSet vacío conserva la definición completa de la versión anterior, así que volver atrás consiste simplemente en devolverle sus réplicas.
Compara con lo que ocurría en la lección anterior al hacer kubectl set image sobre un ReplicaSet: nada, los pods seguían con la imagen vieja. Aquí, en cambio, los pods son nuevos y tienen la imagen nueva:
kubectl get pods -l app=tienda-web -o custom-columns=NOMBRE:.metadata.name,IMAGEN:.spec.containers[0].imageNOMBRE IMAGEN
tienda-web-5f6d8b9c7-2xk4m nginx:1.27.1-alpine
tienda-web-5f6d8b9c7-7q9wd nginx:1.27.1-alpine
tienda-web-5f6d8b9c7-nb3rt nginx:1.27.1-alpineY el historial ya tiene dos entradas:
De momento, quédate con estas cuatro observaciones:
- Cambiar el
templatecrea un ReplicaSet nuevo, con unpod-template-hashdistinto. - El Deployment trasvasa réplicas del viejo al nuevo de forma progresiva, no de golpe.
- Los ReplicaSets antiguos se conservan a 0 réplicas como historial (hasta
revisionHistoryLimit). - Durante el trasvase conviven pods de dos versiones, que es lo que hace posible desplegar sin corte de servicio.
Cómo se controla ese trasvase paso a paso —maxSurge, maxUnavailable, Recreate, pause, undo— es exactamente el contenido de Actualizaciones, Rollbacks y Estrategias de Despliegue. Y las variantes avanzadas, blue-green y canary, esperan en 11-04.
Devuelve tienda-web a la imagen original para empezar la lección siguiente en un punto conocido:
- Cuándo un Deployment no es la carga adecuada
El Deployment es la opción por defecto, pero no la única. Está diseñado para cargas sin estado, intercambiables y de larga duración: sus pods son ganado, no mascotas. Se llaman con sufijos aleatorios, se crean y se destruyen en cualquier orden y ninguno tiene identidad propia.
Cuando eso no encaja, hay otro objeto:
| Necesidad | Objeto correcto | Por qué falla el Deployment | Lección |
|---|---|---|---|
| Identidad y almacenamiento estables por réplica (bases de datos, colas, clústeres con quórum) | StatefulSet | Nombres aleatorios, orden de arranque no garantizado, todas las réplicas compartirían el mismo volumen | 06-01 |
| Exactamente un pod en cada nodo (agentes de logs, métricas, red) | DaemonSet | Un Deployment reparte N réplicas donde quepan, sin garantizar cobertura de nodos | 06-02 |
| Tarea que se ejecuta, termina y ya está | Job | Un Deployment reiniciaría el pod eternamente al terminar (restartPolicy: Always obligatoria) |
06-03 |
| Tarea programada por horario | CronJob | Un Deployment no tiene noción de calendario | 06-03 |
Aplicado a los seis componentes de Rutas Norte:
| Componente | Carga de trabajo | Motivo |
|---|---|---|
tienda-web |
Deployment | Sin estado, réplicas idénticas e intercambiables |
api-reservas |
Deployment | Sin estado; toda la persistencia está en PostgreSQL y Redis |
postgres-reservas |
StatefulSet | Necesita disco propio, nombre de red estable y arranque ordenado |
redis-cache |
Deployment (una réplica) o StatefulSet | Con una sola instancia y estado prescindible, un Deployment basta |
worker-notificaciones |
Deployment | Proceso de larga duración sin estado; no recibe tráfico, así que no llevará Service |
informes-ocupacion |
CronJob | Se ejecuta a las 02:30, termina y no vuelve hasta el día siguiente |
Un aviso que evita un error clásico: redis-cache como Deployment solo es aceptable con una réplica. Si escalaras a 3, tendrías tres cachés independientes detrás del mismo nombre, y cada petición de api-reservas iría a una caché distinta con contenido distinto. El resultado serían aciertos de caché erráticos e imposibles de diagnosticar. Escalar no es gratis: solo funciona si el componente es realmente sin estado.
Errores Comunes y Consejos
- Olvidar
spec.selector. Es obligatorio enapps/v1. Sin él, la API rechaza el manifiesto. - Intentar cambiar el
selectorde un Deployment existente. Es inmutable. Si lo necesitas, hay que borrar el Deployment (con--cascade=orphansi no quieres cortar el servicio) y crear uno nuevo. - Poner las etiquetas solo en el
metadatade arriba. Las que gobiernan los pods son las despec.template.metadata.labels. Es el error de principiante más repetido. - Escalar con
kubectl scaley no tocar el fichero. El siguienteapplyrevertirá el cambio, probablemente en mal momento. - Confundir
READYconAVAILABLE.READYes "puede atender";AVAILABLEes "puede atender y lleva estable el tiempo mínimo exigido". - Creer que
kubectl applyespera a que termine el despliegue. No espera: solo envía el objeto. Para esperar de verdad, encadenakubectl rollout status. - Usar un Deployment para PostgreSQL. Con más de una réplica, varias instancias intentarían montar el mismo volumen y escribir a la vez. Corrupción garantizada.
- Escalar
redis-cachea varias réplicas con un Deployment. Tendrás N cachés incoherentes, no una caché replicada. - Consejo: genera el esqueleto con
kubectl create deployment api-reservas --image=node:20-alpine --replicas=2 --dry-run=client -o yaml > deploy.yamly añade después etiquetas, recursos y anotaciones. - Consejo: para ver la cadena completa de un componente de un vistazo,
kubectl get deploy,rs,pods -l app=tienda-web. Tres niveles en una sola orden.
Ejercicios
Ejercicio 1: Deployment de worker-notificaciones
worker-notificaciones es un proceso de fondo que consume una cola y envía correos. No recibe tráfico entrante, así que no lleva ports ni Service.
Escribe k8s/base/worker-notificaciones-deployment.yaml con 2 réplicas en rutas-norte-dev, imagen busybox:1.36, un comando que escriba cada 10 segundos enviando lote de confirmaciones desde <nombre del pod>, las tres etiquetas del proyecto, terminationGracePeriodSeconds: 45 y requests de 50m/64Mi. Después:
- Aplícalo y espera con el comando que bloquea hasta que termine el despliegue.
- Muestra la cadena Deployment → ReplicaSet → Pod con un solo comando.
- Comprueba en los logs que ambos pods trabajan.
- Explica por qué este componente lleva un plazo de gracia mayor que
tienda-web.
Ejercicio 2: Leer el estado de un despliegue
Un compañero te envía esta captura de un clúster de preproducción y te pide diagnóstico:
Conditions:
Type Status Reason
---- ------ ------
Available True MinimumReplicasAvailable
Progressing False ProgressDeadlineExceededResponde, justificando cada respuesta:
- ¿Cuántos pods deberían existir y cuántos están listos?
- ¿Cuántos tienen la versión nueva?
- ¿Está el servicio caído?
- ¿Qué ha pasado con el despliegue?
- ¿Qué dos comandos ejecutarías, en este orden, para encontrar la causa raíz?
Ejercicio 3: De pod suelto a Deployment, y decidir la carga correcta
- Crea a mano un pod suelto
redis-cacheenrutas-norte-devconredis:7.2-alpiney las tres etiquetas del proyecto. - Conviértelo en un Deployment de 1 réplica llamado
redis-cache, escribiendo el manifiesto y borrando el pod suelto. Verifica que la caché responde aredis-cli ping. - Escala el Deployment a 3 réplicas y explica por qué esto es un error de diseño para Rutas Norte, aunque técnicamente funcione. Vuelve a 1.
- Para cada uno de los seis componentes de Rutas Norte, indica el objeto correcto y una frase de justificación.
Soluciones
Solución 1
# k8s/base/worker-notificaciones-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: worker-notificaciones
namespace: rutas-norte-dev
labels:
app: worker-notificaciones
app.kubernetes.io/part-of: rutas-norte
entorno: dev
annotations:
kubernetes.io/change-cause: "Despliegue inicial de worker-notificaciones 1.2.0"
spec:
replicas: 2
selector:
matchLabels:
app: worker-notificaciones
entorno: dev
template:
metadata:
labels:
app: worker-notificaciones
app.kubernetes.io/part-of: rutas-norte
entorno: dev
spec:
terminationGracePeriodSeconds: 45
containers:
- name: worker
image: busybox:1.36
command: ["sh", "-c"]
args:
- |
trap 'echo "cerrando: terminando envios pendientes"; sleep 5; exit 0' TERM
while true; do
echo "enviando lote de confirmaciones desde $HOSTNAME"
sleep 10
done
resources:
requests:
cpu: "50m"
memory: "64Mi"
limits:
cpu: "200m"
memory: "128Mi"kubectl apply -f k8s/base/worker-notificaciones-deployment.yaml
kubectl rollout status deployment/worker-notificaciones
kubectl get deploy,rs,pods -l app=worker-notificaciones
kubectl logs -l app=worker-notificaciones --tail=2 --prefixdeployment.apps/worker-notificaciones created
deployment "worker-notificaciones" successfully rolled out
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/worker-notificaciones 2/2 2 2 12s
NAME DESIRED CURRENT READY AGE
replicaset.apps/worker-notificaciones-8c7b5d94f 2 2 2 12s
NAME READY STATUS RESTARTS AGE
pod/worker-notificaciones-8c7b5d94f-k2xrt 1/1 Running 0 12s
pod/worker-notificaciones-8c7b5d94f-p9mzq 1/1 Running 0 12s
[pod/worker-notificaciones-8c7b5d94f-k2xrt/worker] enviando lote de confirmaciones desde worker-notificaciones-8c7b5d94f-k2xrt
[pod/worker-notificaciones-8c7b5d94f-p9mzq/worker] enviando lote de confirmaciones desde worker-notificaciones-8c7b5d94f-p9mzq- El plazo de gracia es mayor porque
worker-notificacionespuede estar a mitad de un envío de correo cuando recibe elSIGTERM. Si el kubelet lo mata antes de terminar, un cliente que ha pagado su billete se queda sin justificante y el mensaje puede quedar en un estado ambiguo (consumido de la cola pero no enviado).tienda-websolo sirve ficheros estáticos: sus peticiones duran milisegundos y 30 segundos sobran.
Solución 2
- Deberían existir 5 pods (
spec.replicas = 5, denominador deREADY) y hay 3 listos. Faltan 2. - Solo 2 tienen la versión nueva (
UP-TO-DATE). Los otros pods listos son de la revisión anterior. - No está caído.
Available: Truecon razónMinimumReplicasAvailableindica que hay suficientes réplicas disponibles para atender tráfico. Sí está degradado: sirve con 3 pods en lugar de 5. - El despliegue se ha atascado:
Progressing: Falsecon razónProgressDeadlineExceededsignifica que superóprogressDeadlineSeconds(600 s por defecto) sin avanzar. Los 2 pods de la versión nueva no llegan a estar listos, así que el Deployment no puede seguir retirando los viejos. Causa típica: imagen inexistente,CrashLoopBackOffpor configuración o una sonda mal ajustada. - Los dos comandos, en este orden:
# 1. Ver que pods estan fallando y con que razon
kubectl get pods -l app=api-reservas -n rutas-norte-pre
# 2. La causa raiz del pod problematico
kubectl describe pod <pod-que-no-arranca> -n rutas-norte-pre
# y, si el contenedor reinicia:
kubectl logs <pod-que-no-arranca> -n rutas-norte-pre --previousSolución 3
# 1. Pod suelto
kubectl run redis-cache --image=redis:7.2-alpine \
--labels="app=redis-cache,app.kubernetes.io/part-of=rutas-norte,entorno=dev"# 2. k8s/base/redis-cache-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis-cache
namespace: rutas-norte-dev
labels:
app: redis-cache
app.kubernetes.io/part-of: rutas-norte
entorno: dev
annotations:
kubernetes.io/change-cause: "Despliegue inicial de redis-cache 7.2"
spec:
replicas: 1
selector:
matchLabels:
app: redis-cache
entorno: dev
template:
metadata:
labels:
app: redis-cache
app.kubernetes.io/part-of: rutas-norte
entorno: dev
spec:
containers:
- name: redis
image: redis:7.2-alpine
ports:
- name: redis
containerPort: 6379
resources:
requests:
cpu: "50m"
memory: "64Mi"
limits:
cpu: "200m"
memory: "256Mi"kubectl delete pod redis-cache
kubectl apply -f k8s/base/redis-cache-deployment.yaml
kubectl rollout status deployment/redis-cache
kubectl exec deploy/redis-cache -- redis-cli pingpod "redis-cache" deleted
deployment.apps/redis-cache created
deployment "redis-cache" successfully rolled out
PONGNota práctica: kubectl exec deploy/redis-cache elige automáticamente uno de los pods del Deployment, así no hay que copiar el nombre con su sufijo aleatorio.
# 3. Escalar a 3 y volver
kubectl scale deployment redis-cache --replicas=3
kubectl get pods -l app=redis-cache
kubectl scale deployment redis-cache --replicas=1Es un error de diseño porque las tres réplicas no forman una caché replicada: son tres cachés independientes y vacías, cada una con su propia memoria. Cuando en la lección siguiente pongamos un Service delante, cada consulta de api-reservas caerá en una réplica distinta, así que la disponibilidad de plazas que se escribió en la réplica A no se encontrará al leer de la B. El resultado sería una tasa de aciertos de caché en torno al 33 %, consultas extra a postgres-reservas y, lo peor, respuestas incoherentes según a qué réplica caiga la petición. Replicar Redis de verdad requiere un StatefulSet con configuración de réplica o cluster, tema del módulo 6.
- Objeto correcto por componente:
| Componente | Objeto | Justificación |
|---|---|---|
tienda-web |
Deployment | Sin estado; cualquier réplica sirve cualquier petición |
api-reservas |
Deployment | Sin estado; persiste todo en PostgreSQL y Redis |
postgres-reservas |
StatefulSet | Necesita volumen propio, identidad de red estable y arranque ordenado |
redis-cache |
Deployment de 1 réplica | Estado prescindible y reconstruible; con más réplicas haría falta StatefulSet |
worker-notificaciones |
Deployment | Proceso de larga duración, sin estado y sin tráfico entrante |
informes-ocupacion |
CronJob | Ejecución programada y finita cada noche a las 02:30 |
Conclusión
Ya dominas el objeto que sostiene la mayor parte de cualquier plataforma en Kubernetes. Has visto la cadena Deployment → ReplicaSet → Pod con una responsabilidad nítida en cada eslabón: el pod mantiene vivos sus contenedores, el ReplicaSet mantiene la cantidad, y el Deployment gestiona las versiones. Sabes que un Deployment no manda sobre pods sino sobre ReplicaSets, y que el pod-template-hash es el truco que permite que dos revisiones convivan sin pelearse por los mismos pods.
Has saldado la deuda que arrastrábamos desde el módulo 1: tienda-web ya no es un pod frágil sino un Deployment de 3 réplicas que se recupera solo en dos segundos, y api-reservas tiene el suyo con 2 réplicas. Sabes leer las tres columnas de kubectl get deploy sin confundirlas —READY es lo que atiende, UP-TO-DATE lo que tiene la versión actual, AVAILABLE lo que además lleva estable el tiempo mínimo— e interpretar las conditions Available y Progressing, que son las que te dirán en producción si un despliegue va bien, va degradado o se ha quedado atascado. Escalas con kubectl scale para apagar fuegos y con el manifiesto para que el cambio perdure, y sabes que el clúster refleja Git y no al revés. Además, tienes el criterio para no forzar la herramienta: PostgreSQL pide StatefulSet, un agente por nodo pide DaemonSet, el informe nocturno pide CronJob, y escalar redis-cache a tres réplicas rompería la coherencia de la caché.
Queda por explicar lo más interesante de lo que has visto: al cambiar la imagen apareció un segundo ReplicaSet, el viejo se quedó a 0 réplicas sin borrarse y durante unos segundos convivieron pods de dos versiones. Eso no es un efecto secundario, es el mecanismo con el que Rutas Norte va a conseguir por fin su objetivo de cero cortes en los despliegues, frente a los uno o dos minutos de parada de los martes de madrugada. En la lección siguiente, Actualizaciones, Rollbacks y Estrategias de Despliegue, controlaremos ese trasvase pod a pod con maxSurge y maxUnavailable, veremos por qué postgres-reservas no admitiría una actualización progresiva, y aprenderemos a diagnosticar y deshacer un despliegue roto con kubectl rollout undo.
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
