Actualizar y revertir aplicaciones

KubernetesBeginner
Practicar Ahora

Introducción

Ya puedes desplegar, exponer y escalar una aplicación. La siguiente cuestión operativa es: ¿cómo reemplazar la versión de una aplicación sin dejar todo el servicio fuera de línea?

Kubernetes responde a esta necesidad mediante una actualización progresiva. Cuando cambia la plantilla de Pods, el Deployment crea un nuevo ReplicaSet, inicia gradualmente nuevos Pods y elimina los antiguos solo cuando el reemplazo está disponible. Durante todo el proceso, el Service conserva una dirección estable.

En este laboratorio, cambiarás una aplicación NGINX de una imagen fijada a otra, relacionarás las revisiones del Deployment con sus ReplicaSets y Pods, provocarás deliberadamente una publicación defectuosa y volverás a la última revisión saludable. También mantendrás el manifiesto guardado sincronizado con el estado recuperado del clúster, una práctica importante para evitar que un kubectl apply posterior vuelva a introducir el fallo.

Leer el entorno inicial

Inicio del entorno: Este laboratorio inicia un clúster de Kubernetes completo. La configuración del plano de control, el nodo y los componentes de red suele tardar 2–3 minutos. Espera pacientemente a que el entorno termine de cargarse antes de comenzar.

En este paso, confirmarás la conexión con el clúster y prepararás un cliente HTTP dentro del clúster.

El entorno de este curso ya incluye un clúster de Kubernetes en ejecución, por lo que no necesitas crear uno. Primero, verifica dónde enviará los comandos kubectl. config current-context muestra la conexión activa, get node consulta el estado del nodo y version informa de las versiones tanto del cliente local como del servidor API accesible:

kubectl config current-context
kubectl get node
kubectl version

El contexto y el nodo se llaman labex-v135, el nodo está en estado Ready y el servidor informa de Kubernetes v1.35.x. Un contexto es la selección de kubeconfig que conecta kubectl con un clúster, un usuario y un espacio de nombres predeterminado concretos.

Crea ahora un pequeño Pod cliente. Más adelante lo utilizarás para acceder a la aplicación a través de su Service. kubectl run crea un Pod; --image selecciona BusyBox, --restart=Never lo mantiene como un Pod independiente y el separador -- introduce su comando de larga duración sleep 3600. Después, kubectl wait espera como máximo 30 segundos a que alcance la condición Ready:

kubectl run release-client --image=busybox:1.36 --image-pull-policy=IfNotPresent \
  --restart=Never -- sleep 3600
kubectl wait --for=condition=Ready pod/release-client --timeout=30s

Así, el cliente queda separado de los Pods web, de forma similar a como una carga de trabajo llama a otra dentro de un clúster.

Desplegar la versión estable de referencia

En este paso, establecerás una revisión conocida y funcional antes de realizar cambios.

Antes de practicar una actualización, establece una revisión que sepas que funciona correctamente. Crea un manifiesto que contenga un Deployment con tres réplicas y un Service estable de tipo ClusterIP.

cd entra en el espacio de trabajo preparado. La sintaxis del documento aquí cat <<'EOF' > release-web.yaml escribe en el archivo todo el contenido hasta el EOF de cierre. La línea --- separa dos objetos de Kubernetes dentro del mismo archivo YAML:

cd /home/labex/project/update-lab
cat <<'EOF' > release-web.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: release-web
  annotations:
    kubernetes.io/change-cause: "Initial release: nginx 1.26"
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
  selector:
    matchLabels:
      app: release-web
  template:
    metadata:
      labels:
        app: release-web
    spec:
      containers:
        - name: nginx
          image: nginx:1.26-alpine
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: release-web
spec:
  selector:
    app: release-web
  ports:
    - name: http
      port: 80
      targetPort: http
EOF
kubectl apply -f release-web.yaml
kubectl rollout status deployment/release-web --timeout=60s
kubectl get deployment,service,pods -l app=release-web

El Deployment administra los Pods, mientras que el Service los selecciona de forma independiente mediante su etiqueta. Actualizar el Deployment no cambiará la dirección del Service.

Confirma la versión de referencia desde el cliente. En kubectl exec POD -- COMMAND, -- separa los argumentos de kubectl del comando que se ejecutará dentro del contenedor. wget -qO- obtiene el contenido de forma silenciosa y lo envía a la salida estándar; la tubería pasa la respuesta a head para mostrar solo el principio:

kubectl exec release-client -- wget -qO- http://release-web | head

Publicar una nueva imagen de forma declarativa

En este paso, cambiarás la plantilla de Pods guardada y dejarás que el Deployment realice una actualización progresiva.

Un Deployment inicia una actualización cuando cambia su plantilla de Pods. La imagen se encuentra dentro de esa plantilla, por lo que modificarla crea una nueva revisión y un nuevo ReplicaSet.

Actualiza tanto la imagen como la descripción legible del cambio en el manifiesto guardado. Cada sustitución sed -i 's/old/new/' file edita el archivo directamente. Después, grep -nE muestra los números de línea correspondientes a cualquiera de los dos términos de la expresión regular extendida (change-cause o image:), para que puedas revisar ambos cambios antes de aplicarlos:

cd /home/labex/project/update-lab
sed -i 's/Initial release: nginx 1.26/Release nginx 1.27/' release-web.yaml
sed -i 's/nginx:1.26-alpine/nginx:1.27-alpine/' release-web.yaml
grep -nE 'change-cause|image:' release-web.yaml
kubectl apply -f release-web.yaml
kubectl rollout status deployment/release-web --timeout=60s

kubectl apply modifica el estado deseado. Después, el controlador del Deployment trabaja de forma asíncrona hasta que las tres réplicas actualizadas estén disponibles. Para obtener una tabla centrada en los Pods, -l selecciona la etiqueta de la aplicación y -o custom-columns relaciona los encabezados con las rutas de los campos del objeto:

kubectl get deployment release-web
kubectl get pods -l app=release-web \
  -o custom-columns='NAME:.metadata.name,IMAGE:.spec.containers[0].image,READY:.status.containerStatuses[0].ready'

Las tres réplicas actuales utilizan la nueva imagen. Tras finalizar la actualización, es posible que durante unos instantes también veas un Pod con la imagen antigua en estado Terminating. Ese Pod ya no es una réplica deseada; Kubernetes está completando su apagado ordenado en segundo plano.

Relacionar revisiones, ReplicaSets y Pods

En este paso, inspeccionarás los objetos que respaldan la actualización completada y confirmarás que el Service siguió disponible.

La actualización ha terminado, pero comprender qué cambió es más útil que ver únicamente “success”. kubectl rollout history consulta las revisiones almacenadas del Deployment. El siguiente comando get replicasets utiliza -l para conservar los objetos relacionados y custom-columns para comparar sus cantidades de réplicas y sus imágenes:

kubectl rollout history deployment/release-web
kubectl get replicasets -l app=release-web \
  -o custom-columns='NAME:.metadata.name,DESIRED:.spec.replicas,CURRENT:.status.replicas,READY:.status.readyReplicas,IMAGE:.spec.template.spec.containers[0].image'

Deberías ver dos ReplicaSets. El nuevo administra tres Pods; el antiguo permanece con cero réplicas para que su plantilla de Pods esté disponible en caso de una reversión. El nombre de un ReplicaSet incluye un hash derivado de la plantilla de Pods, por eso cambiaron los nombres de los Pods al cambiar la imagen.

Comprueba la imagen activa y el número de backends del Service. Ambas expresiones JSONPath utilizan range para repetir una plantilla de salida sobre una lista. Los espacios literales y {"\n"} generan una línea legible por cada Pod o endpoint:

kubectl get pods -l app=release-web \
  -o jsonpath='{range .items[*]}{.metadata.name}{"  "}{.spec.containers[0].image}{"\n"}{end}'
kubectl get endpointslices -l kubernetes.io/service-name=release-web \
  -o jsonpath='{range .items[*].endpoints[*]}{.addresses[0]}{" ready="}{.conditions.ready}{"\n"}{end}'
kubectl exec release-client -- wget -qO- http://release-web | head

La revisión cambió, pero el Service sigue teniendo tres backends listos y conserva el mismo nombre estable.

Diagnosticar una publicación defectuosa

En este paso, introducirás un error controlado en la imagen y utilizarás los eventos del Pod para identificar por qué la actualización no puede completarse.

Simula ahora un error habitual durante una publicación: una etiqueta de imagen de contenedor que no existe. Mantén este fallo en el mismo Deployment para que el mecanismo de actualización demuestre una propiedad de seguridad importante. Los dos comandos sed -i modifican directamente el texto de la anotación y de la imagen. Normalmente, el tiempo de espera de la actualización devolvería un estado de error; || true permite deliberadamente que la lección continúe:

cd /home/labex/project/update-lab
sed -i 's/Release nginx 1.27/Broken release: missing image/' release-web.yaml
sed -i 's/nginx:1.27-alpine/nginx:does-not-exist-course/' release-web.yaml
kubectl apply -f release-web.yaml
kubectl rollout status deployment/release-web --timeout=20s || true
kubectl get deployment release-web
kubectl get pods -l app=release-web

La actualización no termina porque un Pod nuevo no puede descargar su imagen. || true permite continuar después del tiempo de espera esperado.

Localiza el Pod que falla e inspecciona sus eventos. $(...) guarda la salida de un comando en BAD_POD. La primera tubería pasa el JSON de Kubernetes a jq; select(...) conserva el Pod cuya imagen es defectuosa y -r devuelve su nombre como texto plano. Una segunda tubería hacia head -n1 conserva un solo nombre. Finalmente, sed -n '/Events:/,$p' muestra la salida de describe desde Events: hasta el final:

BAD_POD=$(kubectl get pods -l app=release-web \
  -o json | jq -r '.items[] | select(.spec.containers[0].image == "nginx:does-not-exist-course") | .metadata.name' | head -n1)
echo "$BAD_POD"
kubectl describe pod "$BAD_POD" | sed -n '/Events:/,$p'
kubectl get replicasets -l app=release-web

Busca ErrImagePull o ImagePullBackOff. Observa que el ReplicaSet anterior, que estaba saludable, todavía tiene Pods disponibles, por lo que el Service puede seguir respondiendo:

kubectl exec release-client -- wget -qO- http://release-web | head

Esto demuestra disponibilidad durante una actualización fallida, no que la nueva publicación funcione.

Revertir y reconciliar el manifiesto

En este paso, restaurarás la revisión saludable anterior y después corregirás el manifiesto guardado.

El Deployment activo conserva el historial de revisiones, por lo que Kubernetes puede restaurar la plantilla de Pods anterior. rollout undo solicita al controlador del Deployment que reutilice la revisión previa; rollout status espera a que termine la recuperación y custom-columns muestra la imagen recuperada y el número de réplicas listas:

kubectl rollout history deployment/release-web
kubectl rollout undo deployment/release-web
kubectl rollout status deployment/release-web --timeout=60s
kubectl get deployment release-web \
  -o custom-columns='NAME:.metadata.name,IMAGE:.spec.template.spec.containers[0].image,READY:.status.readyReplicas'

La imagen vuelve a estar saludable y hay tres réplicas listas. rollout undo crea una nueva revisión a partir de una plantilla de Pods antigua; no rebobina el contador de revisiones.

Queda una tarea más. El archivo YAML todavía contiene la imagen defectuosa, por lo que un kubectl apply futuro volvería a romper el Deployment. Reconcilia el estado deseado guardado con el estado activo recuperado. Las sustituciones corrigen el archivo, apply lo sincroniza y el comando final del historial confirma la trayectoria resultante de revisiones:

cd /home/labex/project/update-lab
sed -i 's/Broken release: missing image/Rollback to nginx 1.27/' release-web.yaml
sed -i 's/nginx:does-not-exist-course/nginx:1.27-alpine/' release-web.yaml
kubectl apply -f release-web.yaml
kubectl rollout status deployment/release-web --timeout=60s
grep -nE 'change-cause|image:' release-web.yaml
kubectl rollout history deployment/release-web

Ahora tanto el clúster como el archivo describen la misma publicación saludable.

Elegir un presupuesto de actualización más seguro

En este paso, harás explícitos los límites de capacidad y disponibilidad de las actualizaciones progresivas del Deployment.

La configuración de la estrategia del Deployment controla la capacidad temporal permitida durante futuras actualizaciones:

  • maxUnavailable indica cuántas réplicas deseadas pueden estar no disponibles durante una actualización.
  • maxSurge indica cuántos Pods adicionales pueden existir temporalmente por encima del número de réplicas deseado.

Para esta aplicación pequeña de tres réplicas, exige que las tres réplicas deseadas permanezcan disponibles y permite un Pod adicional.

El comando sed utiliza una dirección, /type: RollingUpdate/, para localizar la línea de la estrategia. Su acción a\ añade el YAML de las líneas siguientes, con la indentación necesaria. Después de aplicarlo, JSONPath extrae ambos valores de la estrategia para que puedas verificar el cambio sin revisar todo el objeto:

cd /home/labex/project/update-lab
sed -i '/type: RollingUpdate/a\    rollingUpdate:\n      maxUnavailable: 0\n      maxSurge: 1' release-web.yaml
kubectl apply -f release-web.yaml
kubectl get deployment release-web \
  -o jsonpath='maxUnavailable={.spec.strategy.rollingUpdate.maxUnavailable}{"\n"}maxSurge={.spec.strategy.rollingUpdate.maxSurge}{"\n"}'
kubectl get deployment release-web

Cambiar únicamente los campos de la estrategia no reemplaza los Pods, porque la plantilla de Pods no ha cambiado. En una futura actualización de imagen, Kubernetes podría crear un Pod adicional y no debería reducir intencionadamente por debajo de tres el número de réplicas disponibles.

Esta configuración favorece la disponibilidad, pero requiere capacidad libre en el clúster. No existe un valor universalmente óptimo: las aplicaciones más grandes suelen utilizar porcentajes y, en entornos reales, la elección depende de la capacidad, el tiempo de inicio y la interrupción aceptable.

Resumen

Has seguido la publicación de una aplicación a lo largo de todo su ciclo operativo:

  • estableciste una versión de referencia saludable con un Deployment y un Service estable;
  • cambiaste una imagen fijada de forma declarativa y esperaste a que terminara la actualización;
  • relacionaste las revisiones del Deployment con los ReplicaSets antiguo y nuevo;
  • diagnosticaste un fallo al descargar la imagen mientras la versión anterior seguía atendiendo solicitudes;
  • revertiste a la última plantilla de Pods saludable;
  • sincronizaste el manifiesto para impedir que una aplicación posterior restaure la imagen defectuosa; y
  • configuraste un presupuesto explícito de disponibilidad y Pods adicionales para futuras actualizaciones.

La idea central es que un Deployment administra el estado deseado a lo largo del tiempo. Una actualización no consiste simplemente en editar una imagen: es una transición controlada entre ReplicaSets que debes observar, verificar y estar preparado para revertir.