Módulo 2: Pods Deployments And Services

4. Manos a la obra: tu primer Pod

Descripción

Esta es la primera lección de este módulo donde ejecutas algo de verdad contra andes-cargo-cluster. Vas a crear un Pod de dos formas distintas —primero imperativa, después declarativa—, inspeccionarlo a fondo con kubectl describe, y confirmar con tus propias manos el punto central de la lección 2: cuando borras un Pod suelto, nadie lo repone. Todo lo que ves aquí corrió de verdad para escribir esta lección — los nombres, los estados, los eventos son literales, con la única variación esperada en la IP del Pod y el nodo exacto donde el scheduler lo asigne.

Conexión con el módulo

Esta lección es la mitad práctica de la lección 2 (el concepto de Pod) y, al mismo tiempo, la preparación exacta para la lección 5, donde vas a repetir el mismo "borrar un Pod" — pero esta vez con andes-cargo-status-api corriendo dentro de un Deployment. El contraste entre lo que ves aquí (nadie repone el Pod) y lo que vas a ver ahí (el ReplicaSet lo repone solo) es, a propósito, el corazón pedagógico de todo este módulo.


Antes de empezar: confirma que el clúster sigue arriba

kind get clusters
kubectl get nodes

Qué esperar (si tu clúster sigue en el mismo estado que dejaste al cerrar el Módulo 1; AGE es tu valor variable):

andes-cargo-cluster
NAME                                STATUS   ROLES           AGE   VERSION
andes-cargo-cluster-control-plane   Ready    control-plane   10m   v1.36.1
andes-cargo-cluster-worker          Ready    <none>          10m   v1.36.1
andes-cargo-cluster-worker2         Ready    <none>          10m   v1.36.1

Si kind get clusters no devuelve nada, tu clúster no sobrevivió (por ejemplo, si eliminaste explícitamente los contenedores de Docker, no solo los detuviste) — vuelve al Módulo 1, lección 5, y créalo de nuevo antes de seguir. Si los nodos aparecen, pero en NotReady, espera unos segundos y confirma con Docker (docker ps --filter "name=andes-cargo-cluster") que los tres contenedores siguen Up.


Paso 1 — La forma rápida: kubectl run (imperativo)

kubectl run crea un Pod con una sola línea, sin necesidad de escribir ningún YAML — la forma más rápida de tener algo corriendo cuando estás explorando o depurando. Vas a usarla exactamente una vez en esta lección, con una imagen mínima que no tiene ninguna relación con Andes Cargo — nginx:alpine, un servidor web liviano (25 MB), disponible públicamente en Docker Hub, que andes-cargo-cluster puede descargar sin ningún registro adicional porque sus nodos tienen salida a internet, igual que cualquier docker pull normal desde tu host:

kubectl run hello-pod --image=nginx:alpine

Qué esperar (literal, ejecutado):

pod/hello-pod created
kubectl get pods

Qué esperar (justo después de crearlo — todavía descargando la imagen; AGE es tu valor variable):

NAME        READY   STATUS              RESTARTS   AGE
hello-pod   0/1     ContainerCreating   0          0s

Este es el comando que vas a usar constantemente el resto de esta guía para verificar cualquier cosa que crees — recuérdalo, porque no lo vas a volver a explicar cada vez que aparezca.

Antes de seguir, limpia este primer Pod — la razón para no dejarlo es la que ves a continuación:

kubectl delete pod hello-pod

Qué esperar:

pod "hello-pod" deleted from default namespace

Por qué esta guía casi nunca usa kubectl run de aquí en adelante. kubectl run es cómodo, pero es imperativo — no queda ningún archivo que describa lo que creaste, así que si necesitas recrear exactamente lo mismo mañana, tendrías que recordar (o volver a escribir) el comando completo. El resto de esta guía usa manifiestos YAML aplicados con kubectl apply -f, siguiendo la misma disciplina declarativa de la lección 3 — un archivo versionable, que puedes revisar, comparar y, a partir del Módulo 5, entregarle a Git como fuente de verdad.


Paso 2 — La forma real: pod.yaml (declarativo)

Crea el manifiesto que vas a usar el resto de esta lección:

mkdir -p andes-cargo-k8s && cd andes-cargo-k8s
# pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: hello-pod
  labels:
    app: hello-pod
spec:
  containers:
    - name: hello-pod
      image: nginx:alpine
      ports:
        - containerPort: 80

Cada campo de este archivo ya lo explicó la lección 2 a fondo — nada nuevo que aprender aquí, solo confirmarlo funcionando de verdad:

kubectl apply -f pod.yaml

Qué esperar:

pod/hello-pod created

Espera unos segundos, y confirma que llegó a Running:

kubectl get pods

Qué esperar (AGE es tu valor variable; el resto, literal para esta imagen):

NAME        READY   STATUS    RESTARTS   AGE
hello-pod   1/1     Running   0          5s

1/1 en la columna READY significa: de los contenedores declarados dentro de este Pod (uno, en este caso), uno está listo. Si este Pod tuviera dos contenedores (el patrón sidecar de la lección 2), verías 2/2 cuando ambos estén listos.


Paso 3 — Inspecciona el Pod a fondo: kubectl describe

kubectl get pods te da un resumen de una línea; kubectl describe pod te da todo lo que Kubernetes sabe sobre ese objeto específico — el comando que vas a usar constantemente para diagnosticar cualquier problema en el resto de esta guía:

kubectl describe pod hello-pod

Qué esperar (Node, IP, Start Time, Container ID e Image ID son tu valor variable — dependen de a qué nodo asignó el scheduler este Pod y del hash exacto de la imagen; el resto es literal para este manifiesto):

Name:             hello-pod
Namespace:        default
Priority:         0
Service Account:  default
Node:             andes-cargo-cluster-worker/172.19.0.4
Start Time:       Fri, 14 Aug 2026 13:32:55 -0600
Labels:           app=hello-pod
Annotations:      <none>
Status:           Running
IP:               10.244.2.3
IPs:
  IP:  10.244.2.3
Containers:
  hello-pod:
    Container ID:   containerd://2b26d030a64387ef61b50dae0542740a2c351610334b1eb58ea3e05ddf790596
    Image:          nginx:alpine
    Image ID:       docker.io/library/nginx@sha256:4a73073bd557c65b759505da037898b61f1be6cbcc3c2c3aeac22d2a470c1752
    Port:           80/TCP
    Host Port:      0/TCP
    State:          Running
      Started:      Fri, 14 Aug 2026 13:32:55 -0600
    Ready:          True
    Restart Count:  0
    Environment:    <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-rqd87 (ro)
Conditions:
  Type                        Status
  PodReadyToStartContainers   True
  Initialized                 True
  Ready                       True
  ContainersReady             True
  PodScheduled                True
Volumes:
  kube-api-access-rqd87:
    Type:                    Projected (a volume that contains injected data from multiple sources)
    TokenExpirationSeconds:  3607
    ConfigMapName:           kube-root-ca.crt
    Optional:                false
    DownwardAPI:             true
QoS Class:                   BestEffort
Node-Selectors:              <none>
Tolerations:                 node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
                             node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events:
  Type    Reason     Age   From               Message
  ----    ------     ----  ----               -------
  Normal  Scheduled  5s    default-scheduler  Successfully assigned default/hello-pod to andes-cargo-cluster-worker
  Normal  Pulled     5s    kubelet            spec.containers{hello-pod}: Container image "nginx:alpine" already present on machine and can be accessed by the pod
  Normal  Created    5s    kubelet            spec.containers{hello-pod}: Container created
  Normal  Started    5s    kubelet            spec.containers{hello-pod}: Container started

Tres secciones merecen una lectura detenida, porque las vas a usar para diagnosticar problemas reales en el resto de esta guía:

  • Node — te dice, sin ambigüedad, en cuál de los tres nodos de andes-cargo-cluster terminó este Pod. En este caso, andes-cargo-cluster-worker — el kube-scheduler (Módulo 1, lección 6) tomó esa decisión sin que nadie se la pidiera explícitamente; en la lección 5 vas a ver que, con más de un Pod, el scheduler los reparte entre los nodos disponibles.
  • Conditions — cinco chequeos independientes, todos en True cuando un Pod está completamente sano. PodScheduled (el scheduler ya lo asignó a un nodo), Initialized, PodReadyToStartContainers, ContainersReady y Ready (el resumen final). Cuando algo falla, esta tabla es el primer lugar donde ves cuál de los cinco pasos se atascó, no solo que "algo está mal".
  • Events, al final — la cronología completa de lo que pasó con este Pod, en orden. ScheduledPulledCreatedStarted, cuatro pasos, cada uno con su propio mensaje. Fíjate en el mensaje de Pulled: "already present on machine" — porque ya habías descargado nginx:alpine en el Paso 1 con kubectl run, así que este segundo Pod no necesitó descargarla de nuevo. Esta sección de Events es, sin excepción, el primer lugar donde vas a mirar cuando un Pod no arranque como esperabas en el resto de esta guía — incluido el ImagePullBackOff que ya se nombró en el Módulo 1.

Paso 4 — Borra el Pod, y observa que nadie lo recrea

Aquí está el momento central de esta lección — la confirmación con evidencia real de todo lo que la lección 2 explicó en teoría:

kubectl delete pod hello-pod

Qué esperar:

pod "hello-pod" deleted from default namespace

Confirma de inmediato:

kubectl get pods

Qué esperar:

No resources found in default namespace.

Espera unos segundos más, y corre el mismo comando otra vez — no por desconfianza del resultado anterior, sino porque es exactamente la disciplina que necesitas para la lección 5, donde esta misma espera sí va a mostrar un cambio:

kubectl get pods

Qué esperar (idéntico al anterior, sin importar cuánto esperes):

No resources found in default namespace.

Nada va a cambiar, nunca, por más que esperes. No hay ningún ReplicaSet, ningún Deployment, ningún controlador vigilando este Pod específico — lo declaraste, corrió, lo borraste, y con eso terminó su existencia completa. Guarda mentalmente este resultado exacto: es el que vas a contrastar directamente en la lección 5.


Analogía: la oficina sin departamento de administración

Retomando la analogía del Módulo: hello-pod fue, durante los minutos que corrió, una oficina individual completa —con todo lo que necesitaba adentro (el contenedor nginx:alpine, su propia dirección de red)—, pero sin ningún departamento de administración del edificio (Deployment) que llevara la cuenta de si esa oficina debía seguir ocupada o no. Cuando "cerraste" esa oficina (kubectl delete pod), nadie en el edificio lo notó, porque nadie tenía instrucciones de vigilarla. La lección 5 agrega exactamente esa pieza faltante: un departamento de administración real, con instrucciones explícitas de mantener un número fijo de oficinas ocupadas, pase lo que pase con cualquiera de ellas individualmente.


Errores comunes

Esperar que kubectl get pods muestre algo relacionado con kubectl run después de borrarlo con kubectl delete pod de otro nombre (de flujo, un descuido fácil de cometer en esta lección específica). Qué pasa: alguien corre kubectl run hello-pod en el Paso 1, lo borra, y después en el Paso 2 aplica pod.yaml — que declara el mismo nombre, hello-pod — y se confunde pensando que es "el mismo Pod que sigue vivo" en vez de un objeto completamente nuevo. Por qué pasa: el nombre coincide a propósito en esta lección, para que el Paso 2 se sienta como una continuación natural del Paso 1, no un objeto sin relación. Cómo detectarlo: si comparas el Start Time del kubectl describe pod del Paso 3 contra cuándo corriste el Paso 1 —van a ser momentos distintos, porque son objetos distintos, aunque compartan nombre—. Cómo corregirlo: no es un error funcional (Kubernetes no tiene problema con reusar un nombre después de borrar el objeto anterior), solo un malentendido conceptual — cada kubectl apply/kubectl run que ves en esta lección crea un objeto nuevo, sin memoria del anterior con el mismo nombre.

Confundir READY: 0/1 con un error, en vez de un estado transitorio normal (conceptual, ya viste una variante de esto en el Módulo 1 con NotReady en los nodos). Qué pasa: alguien corre kubectl get pods inmediatamente después del Paso 1 o el Paso 2, ve 0/1 en la columna READY y ContainerCreating en STATUS, y asume que algo falló. Por qué pasa: es fácil leer "0 de 1" como una fracción incompleta de forma negativa. Cómo detectarlo: si el mismo comando, corrido unos segundos después, muestra 1/1 y Running — eso confirma que era transitorio, no un error. Cómo corregirlo: espera siempre unos instantes después de crear un Pod, antes de diagnosticar un problema; ContainerCreating significa, literalmente, que el proceso de arranque (descargar la imagen si hace falta, crear el contenedor) sigue en curso — es el mismo patrón que ya viste con NotReady en los nodos del Módulo 1.

No revisar la sección Events de kubectl describe pod antes de asumir que algo está roto (de flujo, el hábito más valioso de esta lección para el resto de la guía). Qué pasa: alguien ve un Pod que no llega a Running y empieza a adivinar la causa —revisando la imagen, el YAML, cualquier cosa— antes de mirar la sección Events, que casi siempre ya tiene la respuesta explícita. Por qué pasa: Events aparece al final del describe, después de secciones más técnicas (Volumes, Tolerations) que pueden sentirse más "importantes" a primera vista. Cómo detectarlo: si pasaste más de un minuto revisando el YAML de un Pod problemático antes de correr kubectl describe pod y leer su sección Events completa. Cómo corregirlo: instala el hábito desde esta lección — kubectl describe pod <nombre>, y lee Events primero, de arriba hacia abajo. Vas a repetir este mismo reflejo, sin excepción, cada vez que algo no arranque como esperabas en el resto de esta guía.


Ejercicios

Ejercicio 1 — Reconstruye el flujo completo de memoria. Sin volver a la lección, enumera los cuatro pasos, en orden, que seguiste en esta lección, y qué demostró cada uno.

Ver solución
  1. kubectl run hello-pod --image=nginx:alpine — la forma imperativa, rápida, de crear un Pod sin YAML.
  2. kubectl apply -f pod.yaml — la forma declarativa, con un manifiesto versionable, que esta guía usa de aquí en adelante.
  3. kubectl describe pod hello-pod — inspección completa: nodo asignado, condiciones, y la cronología de Events.
  4. kubectl delete pod hello-pod, seguido de kubectl get pods repetido — demostró que un Pod suelto, sin ningún controlador detrás, no se repone nunca por sí mismo.

Ejercicio 2 — Interpreta un describe pod ajeno. Un compañero te pasa la salida de un kubectl describe pod cuya sección Conditions muestra PodScheduled: True pero Initialized: False, con el resto en blanco. ¿Qué te dice esto sobre en qué punto exacto se atascó ese Pod, y qué sección revisarías primero para saber por qué?

Ver solución

Le dice que el kube-scheduler sí asignó el Pod a un nodo (PodScheduled: True), pero que el proceso de inicialización de ese Pod en ese nodo no ha terminado (Initialized: False) — el problema está en algún paso posterior a la asignación, probablemente relacionado con descargar la imagen o preparar el contenedor, no con encontrar un nodo disponible. La sección a revisar primero es siempre Events, al final del describe — ahí debería aparecer el mensaje explícito de qué está bloqueando ese paso (por ejemplo, un error de Pulling/Pulled si la imagen no se puede descargar).

Ejercicio 3 — Predice un caso con dos contenedores. Si pod.yaml declarara dos contenedores dentro de spec.containers en vez de uno, ¿qué esperarías ver en la columna READY de kubectl get pods mientras solo uno de los dos ha terminado de arrancar? ¿Y cuando ambos estén listos?

Ver solución

Mientras solo uno de los dos contenedores esté listo: READY mostraría 1/2 — el primer número cuenta cuántos contenedores del Pod están listos, el segundo cuántos hay declarados en total. Cuando ambos terminen de arrancar: READY mostraría 2/2, y recién ahí el Pod completo se consideraría listo (Ready: True en Conditions) — un Pod con múltiples contenedores no se considera "listo" hasta que todos sus contenedores lo están, no basta con que uno funcione.


Resumen y siguiente paso

En esta lección creaste tu primer Pod real de dos formas —imperativa con kubectl run, declarativa con kubectl apply -f pod.yaml—, lo inspeccionaste a fondo con kubectl describe pod (nodo asignado, condiciones, cronología de eventos), y confirmaste con evidencia real el punto central de la lección 2: un Pod suelto, sin ningún controlador detrás, desaparece para siempre en cuanto lo borras — nadie lo repone, sin importar cuánto esperes.

Antes de avanzar deberías poder: crear un Pod con ambos métodos y explicar cuándo usar cada uno; leer la sección Events de un kubectl describe pod para diagnosticar en qué paso se atascó un Pod problemático; y describir, con evidencia propia, por qué un Pod suelto no se auto-repara.

Siguiente lección: manos a la obra, el Deployment de andes-cargo-status-api. Ahí repites el mismo experimento de borrar un Pod — pero esta vez con el servicio real de Andes Cargo, corriendo dentro de un Deployment. El resultado va a ser el opuesto exacto del que acabas de confirmar aquí.

Recursos

  1. Kubernetes — kubectl run — referencia oficial del comando imperativo usado en el Paso 1.
  2. Kubernetes — Debug Running Pods — la guía oficial de diagnóstico con kubectl describe, la herramienta central de esta lección.
  3. Kubernetes — Pod Lifecycle: Container states — referencia oficial de los estados que viste en Conditions y en la columna STATUS.