En la lección anterior cambiamos la imagen de tienda-web y observamos algo que dejamos deliberadamente sin explicar: apareció un segundo ReplicaSet, el viejo se quedó a cero réplicas sin borrarse, y durante unos segundos convivieron pods de dos versiones. Ese mecanismo es la razón de ser del Deployment y la respuesta directa al primero de los cuatro objetivos de Rutas Norte: cero cortes en los despliegues, frente al minuto o dos de parada que hoy obliga a desplegar los martes de madrugada. En esta lección tomamos el control de ese trasvase. Verás las dos estrategias nativas del Deployment y cuándo exige cada una, calcularás pod a pod lo que ocurre durante una actualización de api-reservas, ajustarás el ritmo con maxSurge y maxUnavailable, distinguirás qué cambios generan una revisión nueva y cuáles no, y manejarás el juego completo de kubectl rollout: status, history, undo, pause y resume. Y terminarás rompiendo un despliegue a propósito con una imagen inexistente, diagnosticándolo y deshaciéndolo.
Contenido
- Las dos estrategias nativas:
RollingUpdateyRecreate - Por qué
postgres-reservasno admiteRollingUpdate maxSurgeymaxUnavailable, paso a paso conapi-reservas- Ritmo y tolerancia:
minReadySeconds,progressDeadlineSeconds,revisionHistoryLimit - Qué dispara una revisión nueva y qué no
- El juego completo de
kubectl rollout - La anotación
kubernetes.io/change-cause - Despliegue sin corte: qué hace falta de verdad
- Diagnóstico y rollback de un despliegue atascado
- Las dos estrategias nativas:
RollingUpdate y Recreate
RollingUpdate y Recreatespec.strategy.type solo admite dos valores. No hay más estrategias nativas en Kubernetes: blue-green y canary, que verás en 11-04, se construyen combinando estos ladrillos con Servicios e Ingress.
RollingUpdate (por defecto)
Sustituye los pods de forma progresiva: levanta pods de la versión nueva y retira los de la vieja poco a poco, manteniendo siempre un número mínimo de pods sirviendo.
flowchart LR
subgraph T0["Inicio"]
A1["v1"]; A2["v1"]; A3["v1"]; A4["v1"]
end
subgraph T1["A mitad"]
B1["v1"]; B2["v1"]; B3["v2"]; B4["v2"]
end
subgraph T2["Fin"]
C1["v2"]; C2["v2"]; C3["v2"]; C4["v2"]
end
T0 --> T1 --> T2
Recreate
Mata todos los pods viejos, espera a que desaparezcan, y solo entonces crea los nuevos. Hay un intervalo, normalmente de segundos a un par de minutos, sin ningún pod sirviendo.
flowchart LR
subgraph R0["Inicio"]
D1["v1"]; D2["v1"]
end
subgraph R1["CORTE DE SERVICIO"]
X["Ningún pod"]
end
subgraph R2["Fin"]
E1["v2"]; E2["v2"]
end
R0 --> R1 --> R2
Comparativa
| Aspecto | RollingUpdate |
Recreate |
|---|---|---|
| Corte de servicio | No | Sí, mientras dura la sustitución |
| Conviven dos versiones | Sí, es inevitable | No, nunca |
| Capacidad extra necesaria | Sí, si maxSurge > 0 |
No |
| Velocidad | Más lento (progresivo) | Más rápido |
| Consumo de recursos en el pico | Hasta replicas + maxSurge |
Nunca supera replicas |
| Rollback | Progresivo, también sin corte | Con otro corte |
| Cuándo usarla | Cargas sin estado: tienda-web, api-reservas, worker-notificaciones |
Cuando dos versiones no pueden coexistir |
Cómo se declara explícitamente:
- Por qué
postgres-reservas no admite RollingUpdate
postgres-reservas no admite RollingUpdateEs la pregunta clave de este apartado, y la respuesta enseña más que la estrategia en sí. Imagina que postgres-reservas fuera un Deployment de 1 réplica con RollingUpdate y maxSurge: 1. Durante la actualización, esto es lo que pasaría:
- Kubernetes crea el pod nuevo antes de retirar el viejo, porque
maxSurge: 1lo permite. - Durante unos segundos hay dos procesos PostgreSQL simultáneos.
- Ambos intentan montar y escribir en el mismo directorio de datos.
Consecuencias reales, en orden de gravedad:
- Bloqueo del arranque: PostgreSQL detecta el fichero
postmaster.pidde la instancia viva y se niega a arrancar. El despliegue se atasca (el caso benigno). - Corrupción de datos si el bloqueo falla o el almacenamiento no lo garantiza: dos procesos escribiendo en los mismos ficheros WAL.
- Escrituras perdidas: reservas confirmadas al cliente que no quedan registradas.
Y hay un segundo motivo, independiente del almacenamiento: la migración de esquema. Si la versión nueva de api-reservas requiere una columna que la vieja no conoce, tener las dos versiones hablando a la vez con la misma base de datos rompe una de las dos. Con RollingUpdate esa coexistencia dura minutos; con Recreate, no existe.
| Componente | Estrategia | Motivo |
|---|---|---|
tienda-web |
RollingUpdate |
Sin estado; dos versiones de una SPA conviven sin problema |
api-reservas |
RollingUpdate |
Sin estado; el contrato de la API se mantiene compatible entre versiones consecutivas |
worker-notificaciones |
RollingUpdate |
Sin estado; consume de una cola, dos versiones pueden consumir a la vez |
redis-cache |
Recreate |
Una sola réplica con un volumen; además la caché se repuebla sola |
postgres-reservas |
Ni una ni otra: StatefulSet | Necesita identidad y almacenamiento estables (06-01) |
La conclusión honesta: para postgres-reservas, la respuesta correcta no es Recreate, es no usar un Deployment. Recreate es la estrategia adecuada para cargas de una sola réplica que no toleran duplicidad pero tampoco necesitan identidad estable, como nuestro redis-cache.
Aplícalo a redis-cache, que ya desplegaste en la lección anterior:
kubectl patch deployment redis-cache --type=merge \
-p '{"spec":{"strategy":{"type":"Recreate","rollingUpdate":null}}}'
kubectl get deploy redis-cache -o jsonpath='{.spec.strategy.type}{"\n"}'Recuerda llevar también ese cambio al manifiesto en Git.
maxSurge y maxUnavailable, paso a paso con api-reservas
maxSurge y maxUnavailable, paso a paso con api-reservasEstos dos parámetros gobiernan por completo el ritmo de una actualización progresiva.
| Parámetro | Qué limita | Valor por defecto | Efecto de subirlo |
|---|---|---|---|
maxSurge |
Cuántos pods de más puede haber por encima de replicas |
25 % | Actualización más rápida, más recursos en el pico |
maxUnavailable |
Cuántos pods pueden estar no disponibles por debajo de replicas |
25 % | Actualización más rápida, menos capacidad durante el proceso |
Ambos admiten un número entero o un porcentaje. Los porcentajes se redondean así: maxSurge hacia arriba y maxUnavailable hacia abajo, para pecar siempre por exceso de capacidad. Y hay una restricción: no pueden valer 0 los dos a la vez, porque entonces el despliegue no podría dar ni un paso.
El escenario
api-reservas con 4 réplicas, maxSurge: 1 y maxUnavailable: 1. Actualizamos de la versión 2.4.0 a la 2.5.0. Los números que rigen todo el proceso:
- Máximo de pods simultáneos =
replicas + maxSurge= 4 + 1 = 5 - Mínimo de pods disponibles =
replicas - maxUnavailable= 4 − 1 = 3
Prepara el escenario:
kubectl scale deployment api-reservas --replicas=4
kubectl patch deployment api-reservas --type=merge \
-p '{"spec":{"strategy":{"type":"RollingUpdate","rollingUpdate":{"maxSurge":1,"maxUnavailable":1}}}}'
kubectl get deploy api-reservasEl recuento exacto, paso a paso
| Paso | Acción del controlador | Pods v2.4.0 | Pods v2.5.0 | Total | Disponibles | ¿Respeta los límites? |
|---|---|---|---|---|---|---|
| 0 | Estado inicial | 4 listos | 0 | 4 | 4 | Sí |
| 1 | Crea 1 pod nuevo (usa el maxSurge) |
4 listos | 1 creándose | 5 | 4 | Total = 5 = máximo |
| 2 | Retira 1 pod viejo (usa el maxUnavailable) |
3 listos | 1 creándose | 4 | 3 | Disponibles = 3 = mínimo |
| 3 | El pod nuevo pasa a Ready |
3 listos | 1 listo | 4 | 4 | Sí |
| 4 | Crea otro pod nuevo | 3 listos | 1 listo + 1 creándose | 5 | 4 | Total = 5 |
| 5 | Retira otro viejo | 2 listos | 1 listo + 1 creándose | 4 | 3 | Disponibles = 3 |
| 6 | El segundo nuevo pasa a Ready |
2 listos | 2 listos | 4 | 4 | Sí |
| 7 | Crea el tercero | 2 listos | 2 listos + 1 creándose | 5 | 4 | Total = 5 |
| 8 | Retira el tercer viejo | 1 listo | 2 listos + 1 creándose | 4 | 3 | Disponibles = 3 |
| 9 | El tercero pasa a Ready |
1 listo | 3 listos | 4 | 4 | Sí |
| 10 | Crea el cuarto | 1 listo | 3 listos + 1 creándose | 5 | 4 | Total = 5 |
| 11 | Retira el último viejo | 0 | 3 listos + 1 creándose | 4 | 3 | Disponibles = 3 |
| 12 | El cuarto pasa a Ready. Fin |
0 | 4 listos | 4 | 4 | Completado |
Las dos lecturas importantes de esa tabla:
- En ningún momento hay menos de 3 pods sirviendo. Rutas Norte nunca pierde capacidad total, solo el 25 % durante unos segundos.
- En ningún momento hay más de 5 pods. El clúster necesita hueco para un pod extra, no para ocho.
flowchart TD
P0["Paso 0<br/>4 viejos · 0 nuevos<br/>total 4"] --> P1["Paso 1<br/>4 viejos · 1 nuevo<br/>total 5 ← tope de maxSurge"]
P1 --> P2["Paso 2<br/>3 viejos · 1 nuevo<br/>disponibles 3 ← suelo de maxUnavailable"]
P2 --> P3["Paso 3<br/>el nuevo pasa a Ready<br/>disponibles 4"]
P3 --> PN["Se repite el ciclo<br/>crear · retirar · esperar Ready"]
PN --> PF["Paso 12<br/>0 viejos · 4 nuevos<br/>despliegue completado"]
Verlo en directo
En una terminal:
En otra, lanza la actualización:
NAME READY STATUS RESTARTS AGE
api-reservas-6b4c9d7f5-x2jkp 1/1 Running 0 41m
api-reservas-6b4c9d7f5-k7wpd 1/1 Running 0 41m
api-reservas-6b4c9d7f5-m4rzt 1/1 Running 0 41m
api-reservas-6b4c9d7f5-q8vnc 1/1 Running 0 41m
api-reservas-9f2a7c531-t3xkb 0/1 Pending 0 0s
api-reservas-9f2a7c531-t3xkb 0/1 ContainerCreating 0 0s
api-reservas-6b4c9d7f5-x2jkp 1/1 Terminating 0 41m
api-reservas-9f2a7c531-t3xkb 1/1 Running 0 3s
api-reservas-9f2a7c531-w9djm 0/1 ContainerCreating 0 0s
api-reservas-6b4c9d7f5-k7wpd 1/1 Terminating 0 41m
...Y el recuento de réplicas de cada ReplicaSet durante el proceso:
El Deployment está trasvasando réplicas de un ReplicaSet a otro. Eso es, literalmente, todo lo que hace un despliegue progresivo.
Combinaciones habituales
maxSurge |
maxUnavailable |
Comportamiento | Cuándo usarla |
|---|---|---|---|
25% |
25% |
Por defecto: equilibrio entre velocidad y seguridad | La mayoría de los casos |
1 |
0 |
Nunca baja de la capacidad nominal. Primero levanta, luego retira | Producción con capacidad crítica: api-reservas en rutas-norte-pro |
0 |
1 |
Nunca supera la capacidad nominal. Primero retira, luego levanta | Clústeres con recursos muy justos o cuotas ajustadas |
100% |
0 |
Levanta todos los nuevos a la vez y luego retira todos los viejos | Despliegue muy rápido, exige el doble de recursos |
0 |
0 |
Inválido: la API lo rechaza | — |
Para producción de Rutas Norte, la elección recomendada es maxSurge: 1 y maxUnavailable: 0: en un puente con el tráfico multiplicado por seis, perder un 25 % de capacidad durante un despliegue no es aceptable.
- Ritmo y tolerancia:
minReadySeconds, progressDeadlineSeconds, revisionHistoryLimit
minReadySeconds, progressDeadlineSeconds, revisionHistoryLimitTres campos que completan el control del despliegue.
| Campo | Por defecto | Qué hace | Por qué importa |
|---|---|---|---|
minReadySeconds |
0 |
Segundos que un pod debe llevar listo sin caerse antes de contarse como disponible | Evita que el despliegue avance sobre pods que arrancan y mueren a los 5 segundos |
progressDeadlineSeconds |
600 |
Segundos sin progreso tras los cuales el despliegue se declara fallido | Convierte un atasco silencioso en una condición Progressing: False detectable |
revisionHistoryLimit |
10 |
Cuántos ReplicaSets antiguos (a 0 réplicas) se conservan | Es tu capacidad de rollback; a 0, no puedes deshacer nada |
minReadySeconds merece una explicación aparte porque su efecto es sutil pero muy valioso. Con el valor por defecto de 0, en cuanto un pod nuevo dice "estoy listo", el controlador retira otro viejo y sigue adelante. Si ese pod se cae dos segundos después, el daño ya está hecho: el despliegue avanza sobre una versión rota. Con minReadySeconds: 15, cada pod nuevo debe sobrevivir 15 segundos listo antes de contar. Si se cae antes, el despliegue se frena.
progressDeadlineSeconds es la diferencia entre un despliegue que falla en silencio y uno que avisa. Sin él, un despliegue atascado por una imagen inexistente se quedaría intentándolo eternamente sin que ningún sistema de alertas se enterase.
Manifiesto de api-reservas con los tres campos y la estrategia de producción:
# k8s/base/api-reservas-deployment.yaml (fragmento actualizado)
spec:
replicas: 4
revisionHistoryLimit: 5
minReadySeconds: 15
progressDeadlineSeconds: 300
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0Con esta configuración, una actualización de api-reservas en rutas-norte-pro tarda como mínimo 4 × 15 = 60 segundos, y si algo falla se declara fallida a los 5 minutos en lugar de a los 10. Lentitud a cambio de seguridad: en un componente que gestiona pagos de billetes, es un cambio excelente.
- Qué dispara una revisión nueva y qué no
Esta es la regla más importante de la lección y la más olvidada:
Solo los cambios bajo
spec.templatedisparan una revisión nueva. Todo lo demás, no.
La lógica es transparente: una revisión es un ReplicaSet, y un ReplicaSet se distingue de otro por el hash de su plantilla de pod. Si la plantilla no cambia, el hash no cambia, y no hay ReplicaSet nuevo que crear.
| Cambio | ¿Crea revisión? | Qué ocurre |
|---|---|---|
spec.template.spec.containers[].image |
Sí | ReplicaSet nuevo; sustitución progresiva de pods |
spec.template.spec.containers[].env |
Sí | Ídem: los pods se recrean con las variables nuevas |
spec.template.spec.containers[].resources |
Sí | Ídem |
spec.template.metadata.labels o annotations |
Sí | Ídem, aunque el contenedor sea idéntico |
spec.template.spec.terminationGracePeriodSeconds |
Sí | Ídem |
spec.replicas |
No | El ReplicaSet actual sube o baja su número de pods |
spec.strategy |
No | Se aplicará en la próxima actualización |
spec.minReadySeconds |
No | Ídem |
spec.revisionHistoryLimit |
No | Solo limpia ReplicaSets antiguos |
metadata.annotations del Deployment |
No | Incluida change-cause: es metadato del Deployment, no del pod |
Compruébalo tú mismo:
kubectl rollout history deployment/api-reservas | tail -3
kubectl scale deployment api-reservas --replicas=5
kubectl rollout history deployment/api-reservas | tail -3REVISION CHANGE-CAUSE
1 Despliegue inicial de api-reservas 2.4.0
2 <none>
deployment.apps/api-reservas scaled
REVISION CHANGE-CAUSE
1 Despliegue inicial de api-reservas 2.4.0
2 <none>Mismo historial: escalar no es desplegar.
Una consecuencia práctica que sorprende a mucha gente: cambiar un ConfigMap no reinicia los pods que lo consumen, porque el ConfigMap no está en el template, solo se referencia desde él. El truco habitual, que verás en el módulo 3, es forzar un cambio en el template con una anotación calculada, o usar directamente:
rollout restart no cambia nada funcional: escribe una anotación kubectl.kubernetes.io/restartedAt dentro del template, lo que altera el hash y provoca una sustitución progresiva de todos los pods. Es la forma correcta y sin corte de "reiniciar" un Deployment.
- El juego completo de
kubectl rollout
kubectl rolloutkubectl set image: actualizar la imagen
La sintaxis es <nombre-del-contenedor>=<imagen>. El nombre del contenedor es el de spec.template.spec.containers[].name, no el del pod. Con varios contenedores se pueden actualizar a la vez separándolos por espacios.
Aviso de disciplina: en un proyecto declarativo, la vía correcta es editar el manifiesto y aplicarlo. set image es para emergencias, y su cambio se pierde en el siguiente apply.
kubectl rollout status: esperar
Waiting for deployment "api-reservas" rollout to finish: 1 out of 4 new replicas have been updated...
Waiting for deployment "api-reservas" rollout to finish: 2 out of 4 new replicas have been updated...
Waiting for deployment "api-reservas" rollout to finish: 3 out of 4 new replicas have been updated...
Waiting for deployment "api-reservas" rollout to finish: 1 old replicas are pending termination...
deployment "api-reservas" successfully rolled outkubectl rollout history: el historial
deployment.apps/api-reservas
REVISION CHANGE-CAUSE
1 Despliegue inicial de api-reservas 2.4.0
2 Actualizacion a node 20.15 por parche de seguridad
3 Actualizacion a node 20.16Y el detalle completo de una revisión concreta:
deployment.apps/api-reservas with revision #2
Pod Template:
Labels: app=api-reservas
app.kubernetes.io/part-of=rutas-norte
entorno=dev
pod-template-hash=9f2a7c531
Annotations: kubernetes.io/change-cause: Actualizacion a node 20.15 por parche de seguridad
Containers:
api:
Image: node:20.15-alpine
Port: 3000/TCP
Limits: cpu: 500m, memory: 256Mi
Requests: cpu: 100m, memory: 128MiEsa información sale íntegramente del ReplicaSet a 0 réplicas que quedó guardado. Por eso revisionHistoryLimit: 0 te dejaría sin historial y sin rollback.
kubectl rollout undo: deshacer
Vuelve a la revisión inmediatamente anterior. Para ir a una concreta:
Dos precisiones importantes:
- Un rollback es un despliegue más: usa la misma estrategia, respeta
maxSurgeymaxUnavailable, y por tanto tampoco corta el servicio. - Deshacer no borra la revisión mala: crea una revisión nueva con el contenido de la antigua. Si estabas en la 3 y haces
undo, apareces en la 4, cuyo contenido es el de la 2. Los números nunca retroceden.
REVISION CHANGE-CAUSE
1 Despliegue inicial de api-reservas 2.4.0
3 Actualizacion a node 20.16
4 Actualizacion a node 20.15 por parche de seguridadFíjate en que la revisión 2 ha desaparecido de la lista: su ReplicaSet ha sido reutilizado como revisión 4.
kubectl rollout pause y resume: congelar a mitad
Con el Deployment pausado, los cambios que hagas no se aplican. Es la herramienta para agrupar varias modificaciones en un solo despliegue:
kubectl rollout pause deployment/api-reservas
kubectl set image deployment/api-reservas api=node:20.17-alpine
kubectl set resources deployment/api-reservas -c=api --limits=cpu=600m,memory=384Mi
kubectl scale deployment api-reservas --replicas=5
# hasta aqui no ha pasado NADA en el cluster
kubectl rollout resume deployment/api-reservas
kubectl rollout status deployment/api-reservasdeployment.apps/api-reservas paused
deployment.apps/api-reservas image updated
deployment.apps/api-reservas resource requirements updated
deployment.apps/api-reservas scaled
deployment.apps/api-reservas resumed
Waiting for deployment "api-reservas" rollout to finish: 3 of 5 updated replicas are available...
deployment "api-reservas" successfully rolled outSin pause, esos tres comandos habrían provocado tres despliegues encadenados, cada uno interrumpiendo al anterior. Con pause, uno solo y una sola revisión.
El otro uso de pause es de emergencia: si ves que un despliegue está sacando pods rotos, kubectl rollout pause lo congela en el acto, dejándote los pods viejos que quedan sirviendo mientras decides si arreglar o deshacer.
Tabla resumen del juego completo:
| Comando | Para qué |
|---|---|
rollout status |
Esperar y verificar; devuelve código != 0 si falla |
rollout history |
Ver revisiones; con --revision=N, el detalle |
rollout undo |
Volver atrás, con o sin --to-revision |
rollout pause / resume |
Congelar y reanudar; agrupar cambios |
rollout restart |
Recrear todos los pods sin cambiar nada |
- La anotación
kubernetes.io/change-cause
kubernetes.io/change-causeHabrás notado los <none> en la columna CHANGE-CAUSE. Un historial de revisiones sin motivos es casi inútil: a las tres semanas nadie recuerda qué era la revisión 7.
kubernetes.io/change-cause es una anotación del Deployment cuyo valor se copia al ReplicaSet de esa revisión y se muestra en el historial. Tres formas de rellenarla, de peor a mejor:
Con kubectl annotate (rápida, imperativa):
kubectl annotate deployment/api-reservas \
kubernetes.io/change-cause="Actualizacion a node 20.17 y subida de limites por picos de agosto" \
--overwriteEn el manifiesto (la correcta según las convenciones del proyecto):
metadata:
name: api-reservas
annotations:
kubernetes.io/change-cause: "v2.5.0 - cache de disponibilidad por expedicion (RN-482)"Automatizada en CI/CD, que es lo ideal:
kubectl annotate deployment/api-reservas \
kubernetes.io/change-cause="${CI_COMMIT_SHA} · ${CI_COMMIT_MESSAGE} · ${CI_USER}" \
--overwriteUn detalle que ya conoces por el apartado 5, pero que conviene repetir porque genera confusión: cambiar solo esta anotación no dispara una revisión nueva, porque vive en el metadata del Deployment y no en el template. Anótala junto con el cambio real, no después.
Buenas prácticas para el texto de Rutas Norte: versión de la imagen, resumen del cambio en lenguaje llano y referencia al ticket. Ejemplo real del proyecto:
REVISION CHANGE-CAUSE
1 v2.4.0 - despliegue inicial
2 v2.4.1 - correccion de calculo de plazas libres (RN-455)
3 v2.5.0 - cache de disponibilidad por expedicion (RN-482)
4 v2.4.1 - ROLLBACK: la 2.5.0 devolvia 500 al reservar (RN-489)
- Despliegue sin corte: qué hace falta de verdad
Aquí llega el aviso más importante de la lección. Podrías pensar que con RollingUpdate bien configurado ya tienes despliegues sin corte de servicio. No es así, y esta es la causa número uno de errores 502 durante despliegues supuestamente perfectos.
El problema es que, con lo que sabemos hasta ahora, Kubernetes considera que un pod está "listo" en cuanto el proceso arranca. Pero un proceso de Node.js tarda unos segundos en cargar dependencias, conectar a PostgreSQL y quedar operativo. Durante esa ventana:
- El pod ya cuenta como
Ready. - El Deployment retira un pod viejo, confiado.
- El Service empieza a enviarle tráfico real.
- El pod nuevo todavía no puede responder: errores 502.
flowchart TD
A["Pod nuevo arranca"] --> B["Kubernetes lo marca Ready<br/>solo porque el proceso vive"]
B --> C["El Service le envia trafico"]
C --> D{"¿La aplicacion esta<br/>realmente operativa?"}
D -->|Sí| E["Todo correcto"]
D -->|No: aun conectando a PostgreSQL| F["502 a clientes reales<br/>durante varios segundos"]
La pieza que falta se llama sonda de disponibilidad (readinessProbe): una comprobación que el kubelet ejecuta contra tu aplicación y que decide si el pod está realmente listo para recibir tráfico. Con ella, el paso 1 del esquema no ocurre hasta que la aplicación responde de verdad.
El checklist completo de un despliegue realmente sin corte en Rutas Norte:
| Requisito | Estado en este momento | Dónde se resuelve |
|---|---|---|
Estrategia RollingUpdate bien parametrizada |
Hecho en esta lección | 02-04 |
| Más de una réplica | Hecho | 02-03 |
minReadySeconds como colchón |
Hecho | 02-04 |
readinessProbe que refleje la disponibilidad real |
Pendiente | 07-01 |
livenessProbe para detectar pods colgados |
Pendiente | 07-01 |
Apagado ordenado que capture SIGTERM |
Hecho | 02-01 |
| Un Service que enrute a los pods listos | Pendiente | 02-05 |
| Compatibilidad entre versiones consecutivas de la API | Responsabilidad del desarrollo | — |
Es decir: RollingUpdate es necesario pero no suficiente. Sin sondas, el despliegue progresivo avanza a ciegas. Hasta que lleguemos al módulo 7, minReadySeconds es un sustituto pobre pero útil.
- Diagnóstico y rollback de un despliegue atascado
Ejercicio final y el más realista de todos. Un viernes por la tarde, alguien despliega en rutas-norte-dev una versión que no existe en el registro.
El desastre
kubectl annotate deployment/api-reservas \
kubernetes.io/change-cause="v2.6.0 - version que no existe (RN-501)" --overwrite
kubectl set image deployment/api-reservas api=node:99.99-inexistente
kubectl rollout status deployment/api-reservas --timeout=90sdeployment.apps/api-reservas annotated
deployment.apps/api-reservas image updated
Waiting for deployment "api-reservas" rollout to finish: 1 out of 5 new replicas have been updated...
error: timed out waiting for the conditionPaso 1: el estado general
Lectura: hay 5 pods listos y disponibles —el servicio sigue funcionando con la versión anterior— pero solo 1 tiene la versión nueva. El despliegue ha dado un paso y se ha parado. Esto es el sistema funcionando bien: maxUnavailable: 0 ha impedido que se retirase ningún pod viejo hasta que el nuevo estuviera listo, y como nunca lo estará, no se ha retirado ninguno.
Paso 2: los ReplicaSets
El ReplicaSet nuevo tiene 1 pod que no llega a READY.
Paso 3: el pod culpable
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Pulling 2m kubelet Pulling image "node:99.99-inexistente"
Warning Failed 2m kubelet Failed to pull image: manifest for node:99.99-inexistente not found
Warning Failed 2m kubelet Error: ErrImagePull
Normal BackOff 30s (x6 over 2m) kubelet Back-off pulling image "node:99.99-inexistente"Causa raíz identificada, exactamente con el procedimiento de la lección de Pods.
Paso 4: las conditions
Conditions:
Type Status Reason
---- ------ ------
Available True MinimumReplicasAvailable
Progressing False ProgressDeadlineExceededAvailable: True (seguimos sirviendo) + Progressing: False con ProgressDeadlineExceeded (el despliegue está muerto) es la firma exacta de un despliegue atascado. En un clúster monitorizado, esa combinación debe generar una alerta.
Paso 5: decidir y actuar
Dos opciones, según el momento:
# Opcion A: congelar mientras se investiga (no revierte, solo detiene)
kubectl rollout pause deployment/api-reservas
# Opcion B: deshacer ya (lo normal un viernes por la tarde)
kubectl rollout undo deployment/api-reservas
kubectl rollout status deployment/api-reservasAviso importante: si el Deployment está pausado, undo no surte efecto. Hay que hacer resume antes.
Paso 6: verificar y documentar
kubectl get deploy,rs -l app=api-reservas
kubectl annotate deployment/api-reservas \
kubernetes.io/change-cause="ROLLBACK a v2.5.0: la imagen de v2.6.0 no existe en el registro (RN-501)" \
--overwrite
kubectl rollout history deployment/api-reservasNAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/api-reservas 5/5 5 5 1h
NAME DESIRED CURRENT READY AGE
replicaset.apps/api-reservas-3d8e1f602 0 0 0 6m
replicaset.apps/api-reservas-c5a9b47d8 5 5 5 22m
REVISION CHANGE-CAUSE
1 Despliegue inicial de api-reservas 2.4.0
4 v2.6.0 - version que no existe (RN-501)
5 ROLLBACK a v2.5.0: la imagen de v2.6.0 no existe en el registro (RN-501)El ReplicaSet roto se queda a 0 réplicas, sin consumir nada, como testimonio del incidente. Y el historial cuenta la historia completa a quien la lea dentro de seis meses.
La moraleja de todo el apartado: durante los seis pasos anteriores, api-reservas no dejó de atender ni una sola reserva. Con el Docker Compose de partida, ese mismo error habría dejado la plataforma caída hasta que alguien lo notara. Eso es lo que compra un Deployment bien configurado.
Deja el clúster ordenado antes de continuar:
NAME READY UP-TO-DATE AVAILABLE AGE
api-reservas 2/2 2 2 1h
redis-cache 1/1 1 1 50m
tienda-web 3/3 3 3 1h
worker-notificaciones 2/2 2 2 45mErrores Comunes y Consejos
- Creer que
RollingUpdategarantiza cero cortes por sí solo. SinreadinessProbe, el tráfico llega a pods que aún no pueden responder. Es la causa número uno de 502 durante despliegues. - Poner
maxSurge: 0ymaxUnavailable: 0. La API lo rechaza: el despliegue no podría avanzar. - Usar
RollingUpdatecon una carga que no tolera dos versiones simultáneas. Bases de datos de una réplica, procesos con bloqueo exclusivo o migraciones de esquema incompatibles: ahí vaRecreate, o directamente un StatefulSet. - Poner
revisionHistoryLimit: 0para "ahorrar". Los ReplicaSets a 0 réplicas no consumen ni CPU ni memoria, solo unos kilobytes en etcd. A cambio, pierdes por completo la capacidad de deshacer. - Esperar que cambiar
spec.strategyospec.replicasdispare un despliegue. No están en eltemplate. Solo se aplican en la siguiente actualización real. - Esperar que modificar un ConfigMap reinicie los pods. No lo hace. Usa
kubectl rollout restart. - Olvidar
change-cause. Un historial de<none>no sirve de nada cuando hay que decidir a qué revisión volver a las tres de la madrugada. - Anotar el
change-causedespués del despliegue. Como no dispara revisión, se queda asociado al Deployment pero puede no reflejar lo que hizo esa revisión. Anótalo en el mismoapply. - Hacer
undosobre un Deployment pausado. No pasa nada hasta que hagasresume. Es un clásico de la depuración a ciegas. - Confiar en
kubectl applycomo verificación.applysolo dice que la API aceptó el objeto. Encadena siemprekubectl rollout status --timeout=...en tus scripts. - Consejo: en producción,
maxSurge: 1ymaxUnavailable: 0. Cuesta un pod extra de capacidad y garantiza que nunca bajas de la capacidad nominal. - Consejo:
kubectl rollout statusdevuelve código de salida distinto de 0 si el despliegue falla. Es lo que convierte un pipeline de CI/CD en algo fiable:kubectl apply -f k8s/ && kubectl rollout status deploy/api-reservas --timeout=300s || kubectl rollout undo deploy/api-reservas.
Ejercicios
Ejercicio 1: Calcular un despliegue sobre el papel
tienda-web está en rutas-norte-pro con 6 réplicas, maxSurge: 2 y maxUnavailable: 1.
- ¿Cuál es el número máximo de pods simultáneos y el mínimo de pods disponibles?
- Construye una tabla con los primeros cinco pasos de la actualización, indicando en cada uno: pods viejos, pods nuevos, total y disponibles.
- Si el equipo de plataforma exige que nunca se baje de 6 pods sirviendo, ¿qué valores pondrías y qué recursos extra necesita el clúster?
- Con la configuración del punto 3 y
minReadySeconds: 20, ¿cuál es la duración mínima teórica del despliegue si cada pod tarda 5 segundos en arrancar?
Ejercicio 2: Ciclo completo de actualización y rollback
Sobre tienda-web en rutas-norte-dev:
- Configura
RollingUpdateconmaxSurge: 1,maxUnavailable: 0,minReadySeconds: 10yrevisionHistoryLimit: 5, aplicándolo desde el manifiesto. - Actualiza a
nginx:1.27.1-alpinecon suchange-causecorrespondiente y espera a que termine. - Actualiza a
nginx:1.26-alpinecon otrochange-cause. - Muestra el historial y el detalle de la revisión 2.
- Simula que la 1.26 tiene un fallo grave: vuelve a la 1.27.1 y verifica que las tres réplicas la sirven.
- Explica por qué el historial no muestra la numeración que esperarías.
Ejercicio 3: Diagnosticar un despliegue atascado en worker-notificaciones
- Rompe
worker-notificacionesa propósito desplegando la imagenbusybox:9.9.9-inexistente, conchange-causeincluido. - Con cuatro comandos, diagnostica el problema siguiendo el orden correcto: estado del Deployment, ReplicaSets, pod culpable y conditions.
- Responde: ¿siguen enviándose los correos de confirmación durante el atasco? Justifícalo con la salida de los comandos.
- Congela el despliegue, comprueba que
undono hace nada mientras está pausado, reanúdalo y deshaz. - Documenta el rollback con una anotación y muestra el historial final.
Soluciones
Solución 1
-
Máximo de pods =
replicas + maxSurge= 6 + 2 = 8. Mínimo disponibles =replicas - maxUnavailable= 6 − 1 = 5. -
Primeros cinco pasos:
| Paso | Acción | Viejos | Nuevos | Total | Disponibles |
|---|---|---|---|---|---|
| 0 | Estado inicial | 6 listos | 0 | 6 | 6 |
| 1 | Crea 2 pods nuevos (tope de maxSurge) |
6 listos | 2 creándose | 8 | 6 |
| 2 | Retira 1 viejo (tope de maxUnavailable) |
5 listos | 2 creándose | 7 | 5 |
| 3 | Los 2 nuevos pasan a Ready |
5 listos | 2 listos | 7 | 7 |
| 4 | Crea 1 nuevo más (7 + 1 = 8, tope) | 5 listos | 2 listos + 1 creándose | 8 | 7 |
| 5 | Retira 2 viejos (disponibles 7 − 2 = 5, suelo) | 3 listos | 2 listos + 1 creándose | 6 | 5 |
-
Para no bajar nunca de 6 sirviendo:
maxUnavailable: 0, ymaxSurgea 1 o 2 según lo rápido que se quiera ir. ConmaxSurge: 2, el clúster debe tener hueco para 8 pods detienda-weba la vez: conrequestsde 50m de CPU y 64Mi por pod, eso son 100m de CPU y 128Mi de memoria libres de más durante el despliegue. ConmaxUnavailable: 0el clúster debe tener esa capacidad; si no la tiene, los pods nuevos se quedan enPendingy el despliegue no avanza nunca. -
Con
maxSurge: 2ymaxUnavailable: 0, los pods se sustituyen en tandas de 2. Cada tanda tarda 5 s (arranque) + 20 s (minReadySeconds) = 25 s. Seis pods en tandas de dos son 3 tandas: 75 segundos como mínimo teórico, sin contar descargas de imagen ni el tiempo de terminación de los viejos.
Solución 2
# k8s/base/tienda-web-deployment.yaml (fragmento)
spec:
replicas: 3
revisionHistoryLimit: 5
minReadySeconds: 10
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0kubectl apply -f k8s/base/tienda-web-deployment.yaml
# 2. Actualizacion a 1.27.1
kubectl annotate deployment/tienda-web \
kubernetes.io/change-cause="v1.27.1 - parche de seguridad de nginx (RN-470)" --overwrite
kubectl set image deployment/tienda-web nginx=nginx:1.27.1-alpine
kubectl rollout status deployment/tienda-web
# 3. Actualizacion a 1.26
kubectl annotate deployment/tienda-web \
kubernetes.io/change-cause="v1.26 - vuelta a rama anterior por compatibilidad (RN-473)" --overwrite
kubectl set image deployment/tienda-web nginx=nginx:1.26-alpine
kubectl rollout status deployment/tienda-web# 4. Historial y detalle
kubectl rollout history deployment/tienda-web
kubectl rollout history deployment/tienda-web --revision=2deployment.apps/tienda-web
REVISION CHANGE-CAUSE
1 Despliegue inicial de tienda-web 1.27.0
2 v1.27.1 - parche de seguridad de nginx (RN-470)
3 v1.26 - vuelta a rama anterior por compatibilidad (RN-473)
deployment.apps/tienda-web with revision #2
Pod Template:
Labels: app=tienda-web
pod-template-hash=6f9c4b8d7
Annotations: kubernetes.io/change-cause: v1.27.1 - parche de seguridad de nginx (RN-470)
Containers:
nginx:
Image: nginx:1.27.1-alpine# 5. Rollback a la revision 2
kubectl rollout undo deployment/tienda-web --to-revision=2
kubectl rollout status deployment/tienda-web
kubectl get pods -l app=tienda-web \
-o custom-columns=NOMBRE:.metadata.name,IMAGEN:.spec.containers[0].imagedeployment.apps/tienda-web rolled back
deployment "tienda-web" successfully rolled out
NOMBRE IMAGEN
tienda-web-6f9c4b8d7-42kxr nginx:1.27.1-alpine
tienda-web-6f9c4b8d7-8vnwq nginx:1.27.1-alpine
tienda-web-6f9c4b8d7-t9mzd nginx:1.27.1-alpineREVISION CHANGE-CAUSE
1 Despliegue inicial de tienda-web 1.27.0
3 v1.26 - vuelta a rama anterior por compatibilidad (RN-473)
4 v1.27.1 - parche de seguridad de nginx (RN-470)- La numeración sorprende porque las revisiones nunca retroceden. Al deshacer a la revisión 2, Kubernetes no "vuelve" a ella: reutiliza su ReplicaSet y lo renumera como revisión 4, la más reciente. Por eso la 2 desaparece de la lista y aparece una 4 con su mismo
change-cause. El número de revisión identifica el orden en que se aplicaron los cambios, no el contenido: un rollback es un cambio más.
Solución 3
# 1. Romperlo
kubectl annotate deployment/worker-notificaciones \
kubernetes.io/change-cause="v1.3.0 - imagen que no existe (RN-512)" --overwrite
kubectl set image deployment/worker-notificaciones worker=busybox:9.9.9-inexistente# 2. Diagnostico en cuatro pasos
kubectl get deploy worker-notificaciones
kubectl get rs -l app=worker-notificaciones
kubectl get pods -l app=worker-notificaciones
kubectl describe pod <pod-en-ImagePullBackOff> | tail -6
kubectl describe deployment worker-notificaciones | grep -A4 ConditionsNAME READY UP-TO-DATE AVAILABLE AGE
worker-notificaciones 2/2 1 2 1h
NAME DESIRED CURRENT READY AGE
worker-notificaciones-5a3f9d21c 1 1 0 90s
worker-notificaciones-8c7b5d94f 2 2 2 1h
NAME READY STATUS RESTARTS AGE
worker-notificaciones-5a3f9d21c-r6bkt 0/1 ImagePullBackOff 0 90s
worker-notificaciones-8c7b5d94f-k2xrt 1/1 Running 0 1h
worker-notificaciones-8c7b5d94f-p9mzq 1/1 Running 0 1h
Warning Failed 80s kubelet Failed to pull image "busybox:9.9.9-inexistente": manifest unknown
Conditions:
Type Status Reason
---- ------ ------
Available True MinimumReplicasAvailable
Progressing False ProgressDeadlineExceeded- Sí, los correos se siguen enviando. La prueba está en tres sitios:
READY 2/2yAVAILABLE 2en el Deployment, los 2 pods de la revisión anterior enRunning, y sobre todoAvailable: TrueconMinimumReplicasAvailableen las conditions. SoloUP-TO-DATE 1delata que hay una versión nueva intentando entrar sin conseguirlo. Confirmación directa:
[pod/worker-notificaciones-8c7b5d94f-k2xrt/worker] enviando lote de confirmaciones desde worker-notificaciones-8c7b5d94f-k2xrt# 4. Pausar, comprobar que undo no hace nada, reanudar y deshacer
kubectl rollout pause deployment/worker-notificaciones
kubectl rollout undo deployment/worker-notificaciones
kubectl get rs -l app=worker-notificacionesdeployment.apps/worker-notificaciones paused
deployment.apps/worker-notificaciones rolled back
NAME DESIRED CURRENT READY AGE
worker-notificaciones-5a3f9d21c 1 1 0 4m
worker-notificaciones-8c7b5d94f 2 2 2 1hEl ReplicaSet roto sigue con 1 réplica: el undo se ha registrado pero no se ha ejecutado, porque el Deployment está pausado.
kubectl rollout resume deployment/worker-notificaciones
kubectl rollout status deployment/worker-notificaciones
kubectl get rs -l app=worker-notificacionesdeployment.apps/worker-notificaciones resumed
deployment "worker-notificaciones" successfully rolled out
NAME DESIRED CURRENT READY AGE
worker-notificaciones-5a3f9d21c 0 0 0 5m
worker-notificaciones-8c7b5d94f 2 2 2 1h# 5. Documentar
kubectl annotate deployment/worker-notificaciones \
kubernetes.io/change-cause="ROLLBACK a v1.2.0: la imagen de v1.3.0 no existe en el registro (RN-512)" \
--overwrite
kubectl rollout history deployment/worker-notificacionesREVISION CHANGE-CAUSE
1 Despliegue inicial de worker-notificaciones 1.2.0
2 v1.3.0 - imagen que no existe (RN-512)
3 ROLLBACK a v1.2.0: la imagen de v1.3.0 no existe en el registro (RN-512)Conclusión
Has cerrado el círculo que abrimos en la lección anterior con aquel segundo ReplicaSet que apareció sin explicación. Ahora sabes que las dos estrategias nativas del Deployment son RollingUpdate y Recreate, y por qué la elección no es de gusto sino de naturaleza de la carga: tienda-web, api-reservas y worker-notificaciones toleran dos versiones conviviendo y van con RollingUpdate; redis-cache no, y va con Recreate; y postgres-reservas no admite ninguna de las dos porque el Deployment no es su sitio: dos procesos PostgreSQL sobre el mismo directorio de datos significan bloqueo en el mejor caso y corrupción en el peor.
Sabes calcular un despliegue sobre el papel antes de ejecutarlo: replicas + maxSurge es el techo de pods y replicas - maxUnavailable el suelo de disponibles, y con esos dos números puedes reconstruir paso a paso lo que hará el controlador con las 4 réplicas de api-reservas. Conoces las combinaciones útiles y la recomendada para producción, maxSurge: 1 con maxUnavailable: 0, que protege la capacidad nominal en un puente de agosto. Ajustas el ritmo con minReadySeconds, detectas los atascos con progressDeadlineSeconds y conservas tu capacidad de deshacer con revisionHistoryLimit. Tienes clara la regla que gobierna todo: solo los cambios bajo spec.template crean una revisión, y por eso escalar no despliega, cambiar un ConfigMap no reinicia nada y existe kubectl rollout restart. Y manejas el juego completo —status, history, undo, pause, resume— con change-cause documentando cada paso, incluido el rollback de un despliegue atascado por una imagen inexistente, resuelto sin que un solo cliente dejara de comprar su billete.
Queda una advertencia pendiente y una carencia gorda. La advertencia: RollingUpdate es necesario pero no suficiente para desplegar sin corte real; sin una readinessProbe, Kubernetes marca como listo a un pod cuyo proceso acaba de arrancar y le manda tráfico antes de que pueda atenderlo. Esa pieza llega en Verificaciones de Salud y Sondas. Y la carencia: todo el tráfico del que llevamos dos lecciones hablando aún no existe. Nuestros pods tienen IPs que cambian en cada despliegue, y para hablar con api-reservas hemos tenido que buscar a mano la IP de un pod concreto. En la lección siguiente, Servicios, le daremos a cada componente de Rutas Norte una dirección estable que sobreviva a los despliegues, con balanceo de carga entre réplicas incluido.
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
