Día 51 - Rolling Update en Kubernetes Deployments
Problema / Desafío
El equipo de Nautilus tiene una app corriendo en el cluster con nginx. Sacaron una nueva versión del web server (nginx:1.18) y hay que actualizar el Deployment sin downtime — los usuarios no deben notar la actualización.
- Deployment:
nginx-deployment - Imagen actual:
nginx:1.16 - Imagen nueva:
nginx:1.18 - Container name:
nginx-container - Requisito: todos los pods operativos al terminar
Conceptos clave
Qué es un Rolling Update
Es la estrategia default de un Deployment para reemplazar pods cuando cambia algo del Pod Template (típicamente la imagen). En vez de matar todos los pods viejos y arrancar los nuevos a la vez (lo que causaría downtime), Kubernetes:
- Crea un ReplicaSet nuevo con la imagen actualizada
- Escala el RS nuevo de a poco hacia arriba
- Escala el RS viejo de a poco hacia abajo
- Repite hasta que el RS nuevo tenga el total de replicas y el viejo quede en 0
El RS viejo no se borra — queda en 0 replicas para poder hacer rollout undo.
Antes: RS-old (nginx:1.16) ████████ 3 pods RS-new (nginx:1.18) ░░░ 0 pods
Durante: RS-old (nginx:1.16) ████░░░░ 2 pods RS-new (nginx:1.18) █░░ 1 pod
Durante: RS-old (nginx:1.16) ██░░░░░░ 1 pod RS-new (nginx:1.18) ██░ 2 pods
Después: RS-old (nginx:1.16) ░░░░░░░░ 0 pods RS-new (nginx:1.18) ███ 3 pods (history)
maxSurge y maxUnavailable: el ritmo del update
El ritmo del rolling update se controla con dos campos bajo spec.strategy.rollingUpdate:
| Campo | Qué controla | Default |
|---|---|---|
maxSurge |
Cuántos pods extra puede crear arriba del total deseado durante el update | 25% |
maxUnavailable |
Cuántos pods pueden estar caídos simultáneamente (debajo del total deseado) | 25% |
Aceptan número absoluto (1, 2) o porcentaje (25%, 50%). Para 3 replicas con 25%:
- 25% × 3 = 0.75 → redondea hacia arriba en surge (1), hacia abajo en unavailable (0... pero K8s lo trata como 1 en este caso por la regla "al menos 1")
- Durante el update el cluster puede tener entre 2 y 4 pods corriendo
Para una app crítica con muchos replicas a veces se baja a maxUnavailable: 0 y maxSurge: 1 → cero downtime real, pero el update es más lento.
Deployment → ReplicaSet → Pod: por qué importan las revisiones
Cada cambio del Pod Template hace que el Deployment cree un ReplicaSet nuevo con un pod-template-hash distinto. Por eso aparecen pods como nginx-deployment-fc677cbc9-9846v (hash del RS viejo) y después nginx-deployment-<otro-hash>-xxxxx (hash del RS nuevo).
kubectl rollout history te muestra esa cadena de ReplicaSets — cada uno es una revisión. kubectl rollout undo no hace magia: simplemente escala al RS de la revisión anterior y baja el actual.
Estrategias alternativas (strategy.type)
| Tipo | Comportamiento | Cuándo usarla |
|---|---|---|
RollingUpdate |
Reemplazo gradual (default) | El 90% de los casos — apps stateless con health checks bien hechos |
Recreate |
Mata todos los pods viejos antes de crear los nuevos. Hay downtime, pero garantiza que jamás conviven dos versiones | Apps que no toleran dos versiones a la vez (típicamente migraciones de schema, locks exclusivos) |
Pasos
- Inspeccionar estado actual del Deployment (imagen, replicas, strategy)
- Ejecutar el rolling update con
kubectl set image - Monitorear el rollout con
kubectl rollout status - Verificar que todos los pods tengan la nueva imagen
- Revisar el historial con
kubectl rollout history
Comandos / Código
1. Estado inicial: nginx:1.16
NAME READY STATUS RESTARTS AGE
nginx-deployment-fc677cbc9-9846v 1/1 Running 0 4m8s
nginx-deployment-fc677cbc9-q7s97 1/1 Running 0 4m8s
nginx-deployment-fc677cbc9-sp27v 1/1 Running 0 4m8s
Name: nginx-deployment
Namespace: default
Labels: app=nginx-app
type=front-end
Annotations: deployment.kubernetes.io/revision: 1
Selector: app=nginx-app
Replicas: 3 desired | 3 updated | 3 total | 3 available | 0 unavailable
StrategyType: RollingUpdate
RollingUpdateStrategy: 25% max unavailable, 25% max surge
Pod Template:
Labels: app=nginx-app
Containers:
nginx-container:
Image: nginx:1.16
...
NewReplicaSet: nginx-deployment-fc677cbc9 (3/3 replicas created)
Tres cosas importantes acá:
- El container name es
nginx-container(nonginx) → es lo que hay que usar enkubectl set image StrategyType: RollingUpdate→ no necesitamos cambiar la strategy, ya viene como queremos- El RS actual es
nginx-deployment-fc677cbc9(revisión1)
2. Ejecutar el rolling update
Sintaxis general: kubectl set image <resource>/<name> <container_name>=<new_image>. Podés pasar varios pares container=image separados por espacios si el pod tiene múltiples contenedores.
Equivalentes: los siguientes hacen lo mismo y todos disparan un rolling update:
Comando Característica kubectl set image deployment/nginx-deployment nginx-container=nginx:1.18Imperativo, rápido, una sola línea kubectl edit deployment/nginx-deploymentAbre $EDITOR con el YAML, editás image:a manokubectl apply -f deployment.yaml(conimage: nginx:1.18en el archivo)Declarativo, ideal si el YAML está en git (GitOps) kubectl patch deployment nginx-deployment -p '{"spec":{"template":{"spec":{"containers":[{"name":"nginx-container","image":"nginx:1.18"}]}}}}'Para automatización, no para tipeo humano
3. Monitorear el rollout
Al correrlo mientras está activo, mostraría líneas tipo Waiting for deployment ... 1 out of 3 new replicas have been updated.... Acá ya estaba terminado, por eso salió la línea final directamente.
Este comando es bloqueante: se queda esperando hasta que el rollout termina o falla. En CI/CD se usa como gate después de un deploy para confirmar éxito antes de seguir.
Para ver en vivo cómo entran y salen los pods:
Aparecen pods con un hash distinto (nginx-deployment-<hash-nuevo>-xxxxx) y desaparecen los viejos.
4. Confirmar que el update terminó
NAME READY STATUS RESTARTS AGE
nginx-deployment-79b79679fc-j5427 1/1 Running 0 9m14s
nginx-deployment-79b79679fc-k5clr 1/1 Running 0 9m20s
nginx-deployment-79b79679fc-rcktq 1/1 Running 0 9m15s
Los 3 pods ahora tienen el hash 79b79679fc (del ReplicaSet nuevo). Comparar con el inicial, donde todos tenían hash fc677cbc9. Esa diferencia de hash es la huella visible del rolling update.
Inspección completa del Deployment:
Name: nginx-deployment
Annotations: deployment.kubernetes.io/revision: 2
Replicas: 3 desired | 3 updated | 3 total | 3 available | 0 unavailable
StrategyType: RollingUpdate
RollingUpdateStrategy: 25% max unavailable, 25% max surge
Pod Template:
Containers:
nginx-container:
Image: nginx:1.18
Conditions:
Available True MinimumReplicasAvailable
Progressing True NewReplicaSetAvailable
OldReplicaSets: nginx-deployment-fc677cbc9 (0/0 replicas created)
NewReplicaSet: nginx-deployment-79b79679fc (3/3 replicas created)
Tres detalles clave de este output:
revision: 2— antes era1, ahora subió porque hicimos un cambio del Pod TemplateOldReplicaSets: nginx-deployment-fc677cbc9 (0/0 replicas created)— el RS viejo sigue ahí en cero replicas, listo para unrollout undoNewReplicaSet: nginx-deployment-79b79679fc (3/3)— el RS nuevo está al 100%
Para extraer solo la imagen actual (más estable que parsear describe):
Para extraer solo el campo (más estable que parsear describe):
5. Revisar el historial de revisiones
Cada revisión corresponde a un ReplicaSet. La 1 es el RS fc677cbc9 (nginx:1.16, ahora en 0 replicas) y la 2 es el RS 79b79679fc (nginx:1.18, sirviendo tráfico).
Detalle de una revisión específica:
Pod Template:
Labels: app=nginx-app
pod-template-hash=79b79679fc
Containers:
nginx-container:
Image: nginx:1.18
Tip —
CHANGE-CAUSE: se puede anotar cada update para querollout historylo muestre, agregando la anotaciónkubernetes.io/change-cause:kubectl annotate deployment/nginx-deployment kubernetes.io/change-cause="Update to nginx 1.18 for CVE patches"Aplicar después del
set image, no antes — la anotación viaja con la revisión actual.
Cómo deshacer (si algo sale mal)
# Rollback a la revisión anterior (la 1, en este caso)
kubectl rollout undo deployment/nginx-deployment
# Rollback a una revisión específica
kubectl rollout undo deployment/nginx-deployment --to-revision=1
undo dispara otro rolling update — esta vez del RS nuevo al viejo. Lleva el mismo tiempo que el deploy original.
Qué pasa por debajo durante el rolling update
Al correr kubectl get rs mientras el update está en curso aparecen dos ReplicaSets coexistiendo:
NAME DESIRED CURRENT READY AGE
nginx-deployment-fc677cbc9 2 2 2 5m ← el viejo bajando
nginx-deployment-79b79679fc 1 1 1 30s ← el nuevo subiendo
Cronograma real de este rolling update
La sección Events del describe registra cada movimiento de scale con timestamp. Para este rollout específico (3 replicas, maxSurge=25%, maxUnavailable=25%):
8m33s Scaled up replica set nginx-deployment-79b79679fc from 0 to 1
8m28s Scaled down replica set nginx-deployment-fc677cbc9 from 3 to 2
8m28s Scaled up replica set nginx-deployment-79b79679fc from 1 to 2
8m27s Scaled down replica set nginx-deployment-fc677cbc9 from 2 to 1
8m27s Scaled up replica set nginx-deployment-79b79679fc from 2 to 3
8m26s Scaled down replica set nginx-deployment-fc677cbc9 from 1 to 0
Lo que muestra esto:
- El primer surge va solo (
8m33s): K8s sube el nuevo RS de 0 a 1 sin tocar el viejo. Hay 4 pods totales durante 5 segundos. - A partir del segundo
8m28slos pares van juntos: scale-down del viejo + scale-up del nuevo en el mismo timestamp. Esto solo pasa cuando el primer pod nuevo ya estáReady— ahí K8s gana confianza para mover los pares en paralelo. - Duración total: ~7 segundos (8m33s → 8m26s). El rolling update completo de 3 pods de nginx es casi instantáneo porque la imagen ya estaba cacheada en el nodo y nginx arranca rápido. Una app Java o un container con muchas migraciones podría tardar minutos.
El gating en cada paso es el readiness probe: si el pod nuevo nunca queda Ready, el rollout se traba (no avanza, no rompe). Por eso tener readiness probes bien configuradas es lo que hace seguro al rolling update — sin ellas, Kubernetes asume Ready=true apenas el proceso arranca, y se puede terminar reemplazando pods sanos por pods rotos sin notarlo.
Detalle a notar en este lab: el Deployment no tiene readiness probe configurado (ver el
Pod Templateen el describe — no aparece ningúnReadinessniLiveness). Por eso el update fue tan rápido: K8s consideró cada podReadyapenas arrancó, sin verificar nada. En producción esto sería riesgoso — para nginx serviría algo tipohttpGet /en el puerto 80.
Troubleshooting
| Problema | Causa y solución |
|---|---|
error: unable to find container named "nginx" |
El nombre del contenedor en el Pod Template no es nginx sino nginx-container. Verificar con kubectl describe deployment <name> antes de hacer set image |
rollout status se queda colgado en Waiting for rollout to finish |
El pod nuevo no está pasando readiness. Inspeccionar con kubectl describe pod <pod-nuevo> y kubectl logs <pod-nuevo> — típicamente probe falla o imagen no se encuentra |
Pods nuevos en ImagePullBackOff |
El tag de la imagen no existe en el registry (typo, o nunca se pusheó). El RS viejo sigue sirviendo tráfico — Kubernetes no mata pods sanos por pods rotos. Corregir la imagen y volver a set image |
| Rolling update aparenta éxito pero la app se rompió | La readiness probe es demasiado permisiva (ej: solo chequea que el puerto esté abierto, no que la app responda). Mejorar el probe — el rolling update es solo tan seguro como el probe |
rollout history muestra CHANGE-CAUSE: <none> |
Nadie anotó la revisión. Usar kubectl annotate deployment/... kubernetes.io/change-cause="..." después de cada cambio importante |
| Necesitás pausar un rollout a mitad de camino para validar | kubectl rollout pause deployment/<name> — congela el estado actual (típicamente con un mix de pods viejos y nuevos). Reanudar con kubectl rollout resume |