Módulo 2: Pods Deployments And Services

5. Manos a la obra: el Deployment de `andes-cargo-status-api`

Descripción

Esta es la lección donde andes-cargo-status-api corre, por primera vez en toda esta guía, dentro de Kubernetes real. Vas a crear el namespace andes-cargo (el espacio donde va a vivir todo lo que construyas para Andes Cargo de aquí en adelante), declarar un Deployment con dos réplicas, y repetir el mismo experimento de la lección 4 —borrar un Pod a propósito— pero esta vez con un ReplicaSet real detrás. El resultado va a ser el opuesto exacto de lo que viste ahí. Todo lo que sigue corrió de verdad contra andes-cargo-cluster, con la imagen andes-cargo-status-api:latest que el Módulo 1 dejó cargada en los tres nodos.

Conexión con el módulo

Esta lección cierra el círculo que abrió la lección 1 de este módulo — el clúster listo, sin ningún Pod corriendo, ahora tiene el primer servicio real de Andes Cargo. El Deployment que creas aquí es el mismo que la lección 8 (proyecto de este módulo) va a escalar a tres réplicas, el mismo que el Módulo 3 le va a agregar ConfigMap/Secret/probes/HorizontalPodAutoscaler, y el mismo que el Módulo 4 va a exponer por Ingress — el hilo acumulativo completo de esta guía empieza exactamente aquí.


Antes de empezar: confirma que la imagen sigue cargada

docker exec andes-cargo-cluster-control-plane crictl images | grep andes-cargo-status-api

Qué esperar (si tu laboratorio sigue en el mismo estado que dejó el Módulo 1; IMAGE ID es tu valor variable si reconstruiste la imagen entre módulos):

docker.io/library/andes-cargo-status-api        latest               d07c069076658       186MB

Si no ves ninguna línea, vuelve al Módulo 1, lección 7, y repite kind load docker-image andes-cargo-status-api:latest --name andes-cargo-cluster antes de seguir.


Paso 1 — El namespace: andes-cargo

Hasta ahora, cada Pod que creaste (lección 4) vivió en el namespace default — el que Kubernetes usa quien no especifica ninguno. A partir de esta lección, todo lo que pertenece a Andes Cargo vive en su propio namespace, un límite lógico de organización que vas a usar constantemente en el resto de esta guía —incluidas las políticas de NetworkPolicy (Módulo 4) y de Gatekeeper/Kyverno (Módulo 6), que se aplican por namespace:

# namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: andes-cargo
kubectl apply -f namespace.yaml

Qué esperar:

namespace/andes-cargo created

Un Namespace no contiene nada por sí mismo todavía — es, literalmente, una etiqueta de alcance que vas a referenciar en cada manifiesto que declares de aquí en adelante, con metadata.namespace: andes-cargo.


Paso 2 — El Deployment: andes-cargo-status-api

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: andes-cargo-status-api
  namespace: andes-cargo
  labels:
    app: andes-cargo-status-api
spec:
  replicas: 2
  selector:
    matchLabels:
      app: andes-cargo-status-api
  template:
    metadata:
      labels:
        app: andes-cargo-status-api
    spec:
      containers:
        - name: andes-cargo-status-api
          image: andes-cargo-status-api:latest
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 8080

Cada campo de este archivo ya lo viste explicado a fondo en la lección 3 — la única novedad real es el valor: la imagen que el Módulo 1 cargó en los tres nodos, y el puerto 8080, el mismo que el Dockerfile heredado de aws-serverless-and-containers-guide declaró con EXPOSE 8080. Un detalle que sí merece atención: imagePullPolicy: IfNotPresent. Por defecto, Kubernetes intenta descargar (pull) una imagen etiquetada :latest cada vez que crea un Pod nuevo, asumiendo que "latest" cambia seguido — pero como esta imagen no vive en ningún registro remoto (la cargaste directo con kind load docker-image, Módulo 1, lección 7), forzar ese comportamiento produciría un error. IfNotPresent le dice al kubelet: usa la imagen que ya está en el containerd de este nodo, sin intentar descargarla de ningún lado.

kubectl apply -f deployment.yaml

Qué esperar:

deployment.apps/andes-cargo-status-api created

Paso 3 — Verifica: el Deployment y sus Pods

kubectl get deployments -n andes-cargo

Qué esperar (literal, ejecutado — AGE es tu valor variable):

NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
andes-cargo-status-api   2/2     2            2           6s

Cuatro columnas, cada una respondiendo una pregunta distinta: READY (2 de 2 Pods están listos para recibir tráfico), UP-TO-DATE (2 Pods corren la plantilla más reciente del Deployment), AVAILABLE (2 Pods llevan el tiempo mínimo corriendo sin fallar como para considerarse disponibles — un umbral configurable que esta guía no ajusta, y que por defecto es prácticamente inmediato).

kubectl get pods -n andes-cargo -o wide

Qué esperar (literal, ejecutado — los sufijos hash de cada nombre, las IPs y el AGE son variables por diseño; el prefijo andes-cargo-status-api-56856576d4- es literal para este build exacto del Deployment):

NAME                                      READY   STATUS    RESTARTS   AGE   IP           NODE                          NOMINATED NODE   READINESS GATES
andes-cargo-status-api-56856576d4-8lb5t   1/1     Running   0          7s    10.244.1.2   andes-cargo-cluster-worker2   <none>           <none>
andes-cargo-status-api-56856576d4-vjc9k   1/1     Running   0          7s    10.244.2.4   andes-cargo-cluster-worker    <none>           <none>

La bandera -o wide agrega dos columnas que kubectl get pods normal no muestra: IP (la dirección interna de cada Pod, asignada por el CNI — vas a profundizar en esto en el Módulo 4) y NODE (en cuál de los dos workers terminó cada uno). Fíjate en algo que no es casualidad: el scheduler puso un Pod en andes-cargo-cluster-worker2 y el otro en andes-cargo-cluster-worker — los repartió, en vez de poner ambos en el mismo nodo. Esta es exactamente la razón por la que el Módulo 1, lección 7, insistió en cargar la imagen en los tres nodos, no solo en uno: si la imagen solo hubiera estado en un nodo, el Pod asignado al otro habría fallado con ImagePullBackOff.

Confirma también el ReplicaSet que el Deployment creó automáticamente por debajo — el mismo mecanismo que la lección 3 explicó, ahora con evidencia real:

kubectl get replicasets -n andes-cargo

Qué esperar (andes-cargo-status-api-56856576d4 es el nombre literal de este build exacto — el sufijo hash deriva del contenido de la plantilla del Pod, no es aleatorio en el sentido de cambiar sin razón; AGE es variable):

NAME                                DESIRED   CURRENT   READY   AGE
andes-cargo-status-api-56856576d4   2         2         2       13s

Nunca declaraste este ReplicaSet — el Deployment lo creó por ti, exactamente como predijo la lección 3.


Paso 4 — Borra un Pod a propósito, y observa la diferencia con la lección 4

Este es el momento central de la lección: repites, literalmente, el mismo comando que en la lección 4 —kubectl delete pod—, pero esta vez sobre un Pod que sí tiene un ReplicaSet detrás.

kubectl delete pod andes-cargo-status-api-56856576d4-8lb5t -n andes-cargo

Sustituye el nombre exacto por el que te mostró tu propio kubectl get pods -o wide del Paso 3 — el sufijo hash de tu Pod va a ser distinto al de este ejemplo.

Qué esperar:

pod "andes-cargo-status-api-56856576d4-8lb5t" deleted from andes-cargo namespace

Confirma de inmediato:

kubectl get pods -n andes-cargo -o wide

Qué esperar (andes-cargo-status-api-56856576d4-7ztk9 es un Pod nuevo — nombre y AGE distintos al que borraste; vjc9k, el que no tocaste, sigue exactamente igual):

NAME                                      READY   STATUS    RESTARTS   AGE   IP           NODE                          NOMINATED NODE   READINESS GATES
andes-cargo-status-api-56856576d4-7ztk9   1/1     Running   0          31s   10.244.1.3   andes-cargo-cluster-worker2   <none>           <none>
andes-cargo-status-api-56856576d4-vjc9k   1/1     Running   0          44s   10.244.2.4   andes-cargo-cluster-worker    <none>           <none>

Sigues viendo dos Pods — nunca cayó a uno, ni siquiera por un instante que alcanzaras a capturar con un solo comando. El ReplicaSet de la lección 3 notó, en su siguiente vuelta del bucle de reconciliación (que corre cada muy pocos segundos), que el estado actual (1 Pod con la etiqueta app=andes-cargo-status-api) no coincidía con el estado deseado (replicas: 2), y creó uno nuevo de inmediato — con un nombre nuevo (sufijo hash distinto, 7ztk9 en vez de 8lb5t), pero con la misma etiqueta, la misma imagen, el mismo puerto: la misma plantilla que declaraste en deployment.yaml.

Confirma el resultado final con el propio Deployment:

kubectl get deployments -n andes-cargo

Qué esperar (idéntico al del Paso 3 — el Deployment nunca dejó de reportar 2 réplicas disponibles, porque la reposición fue prácticamente inmediata):

NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
andes-cargo-status-api   2/2     2            2           3m5s

Este es, en una frase, el contraste completo de este módulo: en la lección 4, borrar un Pod dejó el clúster con cero Pods, para siempre. Aquí, borrar un Pod dejó el clúster con dos Pods, siempre — uno de ellos distinto al de antes, pero el número declarado, intacto.


Analogía: el termostato en acción, no solo en teoría

La lección 3 describió a un Deployment como el termostato de un edificio; esta lección es la primera vez que lo ves reaccionar de verdad. Borrar andes-cargo-status-api-56856576d4-8lb5t fue, en esa analogía, como cerrar de golpe una de las dos oficinas que el termostato tenía instrucción de mantener ocupadas — el termostato no preguntó por qué se cerró, ni esperó a que alguien se lo reportara: notó la diferencia entre "2 oficinas deberían estar ocupadas" y "ahora solo hay 1", y abrió una oficina nueva (con un número de puerta distinto, pero el mismo tipo de oficina) para volver al número correcto. Ese es, exactamente, el trabajo que hace un Deployment cada vez que la realidad se desvía de lo declarado — sin excepción, sin que nadie se lo pida cada vez.


Errores comunes

Intentar borrar el Deployment en vez del Pod, esperando ver el mismo comportamiento de reposición (conceptual, un error de alcance). Qué pasa: alguien, confundido sobre qué objeto se está borrando, corre kubectl delete deployment andes-cargo-status-api en vez de kubectl delete pod <nombre-específico>, y se sorprende de que todos los Pods desaparecen, sin ninguna reposición. Por qué pasa: es fácil pensar que "borrar algo relacionado con el Deployment" siempre produce el mismo resultado de auto-reparación. Cómo detectarlo: si kubectl get pods -n andes-cargo queda completamente vacío después de un delete, y kubectl get deployments -n andes-cargo tampoco muestra nada. Cómo corregirlo: el Deployment es la fuente de verdad — borrarlo elimina también, en cascada, el ReplicaSet y todos sus Pods (a menos que uses kubectl delete deployment ... --cascade=orphan, un caso avanzado fuera del alcance de esta lección). El comportamiento de auto-reparación de esta lección aplica a borrar Pods individuales, no al Deployment que los administra. Si esto te pasa, simplemente vuelve a aplicar deployment.yaml con kubectl apply -f para recrearlo desde cero.

Esperar que el Pod de reemplazo tenga el mismo nombre que el que se borró (de expectativa, la confusión más común de esta lección específica). Qué pasa: alguien busca, con kubectl get pods, un Pod con exactamente el mismo nombre que acaba de borrar, y no lo encuentra —entra en pánico pensando que la reposición no funcionó—, sin notar que sí hay un Pod nuevo, solo que con un sufijo hash distinto. Por qué pasa: en otros sistemas (por ejemplo, reiniciar un servicio con systemd) el "mismo servicio" suele conservar el mismo identificador después de reiniciarse. Cómo detectarlo: si cuentas el número total de Pods con la etiqueta app=andes-cargo-status-api (deberían seguir siendo 2) en vez de buscar un nombre específico. Cómo corregirlo: recuerda la tabla de honestidad de esta guía (nombrada explícitamente desde el diseño de esta guía): el sufijo hash de un Pod creado por un ReplicaSet siempre varía — lo único literal y predecible es el prefijo, que corresponde al Deployment. Nunca busques por el nombre completo de un Pod administrado; busca por su etiqueta con kubectl get pods -l app=andes-cargo-status-api.

Olvidar -n andes-cargo y buscar el Deployment en el namespace equivocado (de configuración, silencioso porque kubectl no da un error, solo una lista vacía). Qué pasa: alguien corre kubectl get deployments (sin la bandera -n), ve una lista vacía o distinta a la esperada, y asume que el Deployment no se creó. Por qué pasa: sin -n, kubectl asume el namespace del contexto activo — que sigue siendo default, el mismo que usaste en la lección 4, no andes-cargo. Cómo detectarlo: kubectl get deployments sin bandera, comparado con kubectl get deployments -n andes-cargo, muestra resultados distintos. Cómo corregirlo: cada comando de esta lección en adelante que toque recursos de Andes Cargo necesita -n andes-cargo explícito — vas a ver este patrón constantemente en el resto de la guía. (Existe una forma de cambiar el namespace por defecto del contexto activo con kubectl config set-context --current --namespace=andes-cargo, pero esta guía prefiere ser explícita en cada comando, para que quede claro en qué namespace opera cada uno mientras aprendes.)


Ejercicios

Ejercicio 1 — Reconstruye el contraste con la lección 4. En una tabla de dos columnas, sin volver a ninguna de las dos lecciones, escribe qué pasó al borrar un Pod en la lección 4 (Pod suelto) frente a qué pasó al borrar un Pod en esta lección (Pod dentro de un Deployment).

Ver solución
Lección 4 (Pod suelto)Lección 5 (Pod en Deployment)
Objetos antes de borrar1 Pod2 Pods
Objetos después de borrar0 Pods, para siempre2 Pods (uno nuevo, con nombre distinto)
Quién actuóNadie — no había ningún controladorEl ReplicaSet del Deployment, sin que nadie lo pidiera

Ejercicio 2 — Predice un caso con tres nodos, tres réplicas. Si escalaras este mismo Deployment a replicas: 3 (el número exacto que la lección 8, el proyecto de este módulo, va a usar), ¿cómo esperarías que el scheduler distribuya los tres Pods entre los tres nodos de andes-cargo-cluster (un control-plane y dos worker)? Ten en cuenta lo que ya sabes del Módulo 1 sobre el rol de cada tipo de nodo.

Ver solución

Por defecto, el kube-scheduler no asigna Pods de carga de trabajo normal al nodo control-plane — ese nodo tiene una marca especial (un taint) que lo reserva para los componentes del propio plano de control (kube-apiserver, etcd, scheduler, controller-manager, ya vistos en el Módulo 1, lección 6). Con solo dos nodos worker disponibles y tres réplicas deseadas, el resultado esperado es que ambos worker reciban al menos un Pod cada uno, y el tercero se sume a cualquiera de los dos — nunca al control-plane. Vas a confirmar esto con evidencia real en la lección 8.

Ejercicio 3 — Explica imagePullPolicy: IfNotPresent a un colega. Un colega, revisando deployment.yaml, te pregunta por qué no dejaron el comportamiento por defecto de Kubernetes para una imagen :latest. Explica, en dos o tres frases, qué pasaría si quitaras esa línea del manifiesto.

Ver solución

Sin imagePullPolicy: IfNotPresent, Kubernetes usa su comportamiento por defecto para cualquier imagen etiquetada :latest: intentar descargarla (pull) de un registro remoto cada vez que crea un Pod nuevo, asumiendo que "latest" puede haber cambiado desde la última vez. Como andes-cargo-status-api:latest no vive en ningún registro remoto —se cargó directo al containerd de cada nodo con kind load docker-image, en el Módulo 1—, ese intento de descarga fallaría, y el Pod quedaría atascado en ImagePullBackOff, el mismo error que el Módulo 1, lección 7, advirtió por adelantado.


Resumen y siguiente paso

En esta lección andes-cargo-status-api corrió, por primera vez en esta guía, dentro de Kubernetes real: creaste el namespace andes-cargo, declaraste un Deployment con dos réplicas, confirmaste que el scheduler las repartió entre los dos nodos worker, y —el punto central— borraste un Pod a propósito y viste al ReplicaSet reponerlo de inmediato, sin que nadie lo pidiera. El contraste con la lección 4 (donde el mismo comando dejó el clúster sin ningún Pod, para siempre) es, en evidencia real, la diferencia completa entre un Pod suelto y un Pod administrado.

Antes de avanzar deberías poder: explicar por qué este Deployment necesita imagePullPolicy: IfNotPresent; predecir en qué nodos va a terminar un Deployment escalado, sabiendo que el control-plane normalmente no recibe carga de trabajo; y reconstruir, de memoria, el contraste completo entre esta lección y la lección 4.

Siguiente lección: Services, ClusterIP, NodePort, y por qué un Pod no es una dirección estable. andes-cargo-status-api ya corre con dos réplicas — pero todavía no tienes ninguna forma de hablarle desde fuera sin conocer, a mano, la IP interna de un Pod específico. Ahí entra la última pieza de este módulo.

Recursos

  1. Kubernetes — Deployments — referencia oficial, la misma de la lección 3, ahora confirmada con evidencia real.
  2. Kubernetes — Namespaces — referencia oficial del objeto Namespace creado en el Paso 1.
  3. Kubernetes — Images: Updating images — referencia oficial de imagePullPolicy y su comportamiento por defecto con la etiqueta :latest.
  4. aws-serverless-and-containers-guide (NIEVA), Módulo 7 — el status-api-service ECS-solo-documentado que esta guía finalmente pone a correr, empezando por esta lección.