Tutorial

Tu primer Pod en Kubernetes: de la imagen al manifiesto

Construí una app con ayuda de IA, empaquetala en una imagen, publicala en un registry y ejecutala como Pod en Kubernetes de dos formas: con kubectl (imperativo) y con un manifiesto YAML (declarativo). Cerrá diagnosticando errores reales.

Contenedores conectados

En este laboratorio vas a recorrer el camino completo de una aplicación hasta Kubernetes:

Laboratorio

Vas a crear un Pod de dos formas: con comandos (imperativo) y con un manifiesto YAML (declarativo). Al final, vas a romper cosas a propósito para aprender a diagnosticar.

Note

💡 Tiempo estimado: 60 minutos. Cada bloque tiene una tarea que se verifica automáticamente: cuando la completes, se marca en verde.

¿Qué es un Pod?

pod

En Docker trabajabas con contenedores. Kubernetes no ejecuta contenedores sueltos: los envuelve en un Pod, que es la unidad mínima que el clúster sabe crear, ubicar en un nodo y vigilar.

Una analogía útil: si el contenedor es una persona, el Pod es el departamento donde vive. Tiene su propia dirección (IP), y si hubiera varias personas adentro, comparten esa dirección y pueden hablarse por localhost. En este lab usamos un Pod con un solo contenedor, que es el caso más común.

Paso 0: Conocer el entorno

Tu playground tiene un clúster de Kubernetes (k3s) con un nodo de control y nodos de trabajo, más una máquina de desarrollo (dev-machine) desde donde vas a trabajar.

Abrí la terminal de dev-machine y ejecutá:

# ¿A qué clúster estoy apuntando?
kubectl config current-context

# ¿Qué nodos tiene el clúster?
kubectl get nodes

# Versión del cliente y del servidor
kubectl version
Note

💡 Abrí también la pestaña Explorer. Es un visualizador del clúster: vas a ver aparecer tus Pods en tiempo real a medida que los crees.

Paso 1: Construir la app con IA

Vamos a pedirle a un asistente de IA una app pequeña, pero pensada para Kubernetes: que muestre en qué Pod corre y que se configure con variables de entorno.

Abrí tu asistente de IA preferido en otra pestaña del navegador y usá este prompt:

Creá una aplicación web mínima en Node.js usando solo el módulo nativo http
(sin dependencias externas) con estos requisitos:

- Escucha en el puerto definido por la variable PORT (por defecto 3000).
- Ruta "/" devuelve una página HTML simple que muestre:
  - un mensaje tomado de la variable MENSAJE (por defecto "¡Hola desde Kubernetes!")
  - el hostname del contenedor (os.hostname())
  - la versión tomada de la variable VERSION (por defecto "1.0")
- Ruta "/health" devuelve JSON {"status":"ok"}.
- Registra cada petición por consola con fecha, método y ruta.
- Maneja la señal SIGTERM cerrando el servidor de forma ordenada.

Entregá server.js y un package.json mínimo. Explicá brevemente cada parte.
Note

🤔 ¿Por qué pedimos os.hostname()? Porque en Kubernetes el hostname del contenedor es el nombre del Pod. Así vas a ver con tus propios ojos dónde se ejecuta la app.

Abrí el IDE y guardá los archivos en ~/lab-01/app/.

La IA ayuda, pero vos validás. Antes de seguir, comprobá que el código:

  • lee PORT y MENSAJE desde process.env;
  • tiene la ruta /health;
  • escucha la señal SIGTERM.
Versión de respaldo (si la IA no te dio algo funcional)

Ejecutá esto en la terminal para crear los archivos:

cd ~/lab-01/app

cat > server.js <<'EOF'
const http = require('http');
const os = require('os');

const PORT = process.env.PORT || 3000;
const MENSAJE = process.env.MENSAJE || '¡Hola desde Kubernetes!';
const VERSION = process.env.VERSION || '1.0';

const server = http.createServer((req, res) => {
  console.log(`${new Date().toISOString()} ${req.method} ${req.url}`);

  if (req.url === '/health') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    return res.end(JSON.stringify({ status: 'ok' }));
  }

  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
  res.end(`
    <html>
      <body style="font-family: sans-serif; text-align: center; padding: 60px;
                   background: #326ce5; color: white;">
        <h1>☸️ ${MENSAJE}</h1>
        <p>Pod: <strong>${os.hostname()}</strong></p>
        <p>Versión: ${VERSION}</p>
      </body>
    </html>
  `);
});

server.listen(PORT, () => {
  console.log(`Servidor escuchando en el puerto ${PORT}`);
});

process.on('SIGTERM', () => {
  console.log('SIGTERM recibido: cerrando el servidor...');
  server.close(() => process.exit(0));
});
EOF

cat > package.json <<'EOF'
{
  "name": "hola-k8s",
  "version": "1.0.0",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  }
}
EOF

Paso 2: Crear la imagen y publicarla

Creá el Dockerfile en ~/lab-01/app/:

~/lab-01/app/Dockerfile
FROM node:22-alpine

WORKDIR /app
COPY package.json server.js ./

# No ejecutar como root
USER node

EXPOSE 3000
CMD ["node", "server.js"]

Construí la imagen. Fijate que el nombre ya incluye la dirección del registry:

cd ~/lab-01/app

docker build -t registry.iximiuz.com/hola-k8s:1.0 .

docker images | grep hola-k8s

Probala con Docker antes de llevarla a Kubernetes:

docker run --rm -p 3000:3000 registry.iximiuz.com/hola-k8s:1.0

El contenedor queda corriendo y ocupa la terminal. Dejalo así: lo vamos a probar desde el navegador.

Probar la app desde el navegador

Primero confirmá, desde una nueva terminal, que la app responde dentro de la máquina:

curl -s localhost:3000 | head -5

Deberías ver: el HTML de la app. Si esto falla, no tiene sentido seguir: el problema está en el contenedor, no en el navegador.

El puerto responde adentro del playground, pero tu navegador está afuera. Para alcanzarlo hay que exponerlo:

  1. Arriba a la derecha del playground, hacé clic en el menú de tres puntos (⋮) y elegí Expose HTTP port.

Menú del playground: Expose HTTP port

  1. En Machine dejá dev-machine, en Port escribí 3000, dejá Public y HTTPS apagados y hacé clic en EXPOSE.
  2. En la tabla Exposed ports aparece una fila dev-machine:3000 con una URL del tipo xxxx.node-eu-XXXX.iximiuz.com. Abrila en una pestaña nueva.

Diálogo de puertos expuestos

En la captura la máquina es docker-01 porque viene de otro laboratorio; acá la fila va a decir dev-machine:3000.

Deberías ver: la página azul con el mensaje, el hostname del contenedor y la versión. Ese hostname es el ID del contenedor de Docker: en el Paso 3 va a pasar a ser el nombre del Pod.

Cuando termines, volvé a la terminal donde corre docker run y detenelo con Ctrl+C. La URL expuesta deja de responder, así que borrala con el ícono de tacho en la tabla Exposed ports.

Important

⚠️ El playground solo expone puertos abiertos en la interfaz de red principal de la VM. Ojo con esto: curl localhost:3000 funciona igual aunque el servicio escuche solo en localhost, así que esa prueba sola no alcanza para saber si vas a poder exponerlo. Por eso publicamos con -p 3000:3000 (todas las interfaces) y no con -p 127.0.0.1:3000:3000. Es la misma razón del --address 0.0.0.0 que vas a usar más adelante en kubectl port-forward.

Note

✅ Regla de oro: si la imagen no funciona en Docker, tampoco va a funcionar en Kubernetes.

Ahora publicala en el registry del playground, para que los nodos del clúster puedan descargarla:

docker push registry.iximiuz.com/hola-k8s:1.0
Note

🧠 ¿Por qué un registry? Los Pods no corren en dev-machine, sino en los nodos del clúster. Cada nodo necesita descargar la imagen desde algún lugar accesible: ese lugar es el registry.

Paso 3: Pod por CLI (imperativo)

La forma imperativa es darle una orden directa al clúster: "creá esto ahora".

kubectl run hola-cli \
  --image=registry.iximiuz.com/hola-k8s:1.0 \
  --port=3000 \
  --env="MENSAJE=Hola desde la CLI" \
  --labels="app=hola-k8s,forma=cli"
ParámetroQué hace
--imageLa imagen que publicamos en el registry
--portDocumenta el puerto del contenedor
--envVariable de entorno para la app
--labelsEtiquetas para identificar y filtrar el Pod

Observá el Pod:

# Estado del Pod
kubectl get pods

# Verlo cambiar en vivo (Ctrl+C para salir)
kubectl get pods -w

# Más detalle: IP y nodo donde quedó
kubectl get pods -o wide

# La "historia clínica" del Pod
kubectl describe pod hola-cli

Validarlo en el Explorer

Todo lo que viste en la terminal también se puede mirar de forma visual. Abrí la pestaña Explorer y seguí este recorrido:

  1. En el árbol de la izquierda, desplegá Workloads → Pod y elegí hola-cli dentro de namespace "default".
  2. En el panel derecho, pestaña Insights, confirmá Phase: Running (Ready), hace cuánto arrancó y la IP del Pod.
  3. Pasá a la pestaña Manifest: es el mismo YAML que devuelve kubectl get pod hola-cli -o yaml.
  4. Pasá a Events: son los mismos eventos que viste al final de kubectl describe.

El Pod hola-cli visto desde el Explorer

Deberías ver: el Pod hola-cli en verde (Running (Ready)) y, en el diagrama del centro, su cápsula junto a los Pods del sistema. La leyenda de colores te dice de un vistazo el estado de cada uno: gris Waiting, verde claro Starting, verde Running (Ready), amarillo Running (Not Ready), rojo Terminated KO.

Para pensar: el Explorer no es una base de datos aparte — le pregunta a la misma API que kubectl. ¿Por qué entonces puede mostrarte los cambios en vivo sin que vos ejecutes nada?

Note

🔍 Mirá la sección Events al final del describe. Ahí ves los pasos que hizo Kubernetes: Scheduled → Pulling → Pulled → Created → Started. Es la primera herramienta de diagnóstico que vas a usar siempre.

Note

🖥️ En kubectl get pods -o wide, la columna NODE te muestra en qué nodo quedó el Pod. Vos no elegiste el nodo: lo decidió el scheduler de Kubernetes.

Acceder a la aplicación

El Pod tiene una IP interna del clúster. Para probarlo desde afuera, creamos un túnel temporal:

kubectl port-forward --address 0.0.0.0 pod/hola-cli 8080:3000

El comando queda ocupando la terminal. Como antes, exponé el puerto desde el menú ⋮ → Expose HTTP port, esta vez con Machine dev-machine y Port 8080, y abrí la URL que aparece en Exposed ports.

Deberías ver: "Hola desde la CLI" y, donde antes figuraba el ID del contenedor de Docker, ahora el nombre del Pod: hola-cli.

Abrí una nueva terminal y explorá:

# Logs de la app (las peticiones que hiciste desde el navegador)
kubectl logs hola-cli

# Entrar al contenedor
kubectl exec -it hola-cli -- sh

Dentro del contenedor:

hostname          # el nombre del Pod
env | grep MENSAJE
whoami            # node, no root
exit

Ahora seguí los logs en vivo y, en otra terminal, eliminá el Pod:

kubectl logs -f hola-cli
kubectl delete pod hola-cli

En los logs vas a ver el mensaje de SIGTERM: Kubernetes le avisó a la app que se iba a detener, y ella cerró de forma ordenada.

Paso 4: Del comando al YAML

pod

Este es el truco favorito para aprender YAML: le pedís a kubectl que no cree nada y solo te muestre el manifiesto que generaría.

cd ~/lab-01

kubectl run hola-yaml \
  --image=registry.iximiuz.com/hola-k8s:1.0 \
  --port=3000 \
  --env="MENSAJE=Hola desde YAML" \
  --labels="app=hola-k8s,forma=yaml" \
  --dry-run=client -o yaml > k8s/pod.yaml

cat k8s/pod.yaml
Note

💡 --dry-run=client significa "simulá, no ejecutes". -o yaml muestra el resultado en formato YAML.

Paso 5: Pod por YAML (declarativo)

La forma declarativa es describir el estado que querés y dejar que Kubernetes lo alcance. El archivo queda versionado en Git, se revisa en un pull request y se reproduce siempre igual.

Abrí ~/lab-01/k8s/pod.yaml en el IDE y mejoralo:

  1. Eliminá las líneas que no aportan: creationTimestamp: null, resources: {}, dnsPolicy y status: {}.
  2. Agregá la variable VERSION con valor "1.0".
  3. Agregá una sección resources con requests y limits.
[object Object]
~/lab-01/k8s/pod.yaml
apiVersion: v1          # Versión de la API para este objeto
kind: Pod               # Qué tipo de objeto creamos
metadata:
  name: hola-yaml       # Nombre único del Pod en el namespace
  labels:               # Etiquetas para identificar y filtrar
    app: hola-k8s
    forma: yaml
spec:                   # El estado que deseamos
  containers:
    - name: hola-yaml
      image: registry.iximiuz.com/hola-k8s:1.0
      ports:
        - containerPort: 3000
      env:
        - name: MENSAJE
          value: "Hola desde YAML"
        - name: VERSION
          value: "1.0"
      resources:
        requests:       # Lo mínimo que el Pod necesita
          cpu: "50m"
          memory: "64Mi"
        limits:         # Lo máximo que puede usar
          cpu: "200m"
          memory: "128Mi"
  restartPolicy: Always
Note

🧩 Las 4 secciones que vas a ver en casi todo manifiesto: apiVersion, kind, metadata y spec. Si entendés estas cuatro, entendés la estructura de Kubernetes.

Validá y aplicá:

cd ~/lab-01

# Validar contra el servidor sin crear nada
kubectl apply -f k8s/pod.yaml --dry-run=server

# Crear el Pod
kubectl apply -f k8s/pod.yaml

# Filtrar por etiqueta
kubectl get pods -l app=hola-k8s --show-labels

Mirá todo lo que Kubernetes agregó a tu manifiesto:

kubectl get pod hola-yaml -o yaml

Vas a encontrar campos que vos no escribiste: uid, nodeName, podIP y toda la sección status. Tu YAML describe lo que querés; status muestra lo que hay.

¿No sabés qué significa un campo? Consultá la documentación desde la terminal:

kubectl explain pod.spec.containers
kubectl explain pod.spec.containers.resources

El Pod es casi inmutable

Cambiá el mensaje en el manifiesto y aplicá de nuevo. Usamos sed para asegurarnos de que el archivo realmente cambió:

cd ~/lab-01

sed -i 's/Hola desde YAML/Hola, YAML actualizado/' k8s/pod.yaml
grep -A1 "name: MENSAJE" k8s/pod.yaml    # confirmá el valor nuevo

kubectl apply -f k8s/pod.yaml

Deberías ver un error parecido a este:

The Pod "hola-yaml" is invalid: spec: Forbidden: pod updates may not change fields other than
`spec.containers[*].image`, `spec.initContainers[*].image`, ...

La mayoría de los campos de un Pod no se pueden modificar una vez creado: la lista de excepciones que menciona el error es casi todo lo que se puede tocar en caliente. Para aplicar el cambio hay que recrear el Pod:

kubectl replace --force -f k8s/pod.yaml

Deberías ver: pod "hola-yaml" deleted seguido de pod/hola-yaml replaced. Ese "deleted" no es un detalle menor: durante unos segundos la app no existió.

Me dijo "pod/hola-yaml configured" en vez de dar error

Significa que el Pod vivo y el manifiesto no eran distintos en ningún campo prohibido. Las causas más comunes:

  • El archivo no cambió. Verificá el valor que tiene el manifiesto y el que tiene el Pod vivo, y compará:
    grep -A1 "name: MENSAJE" k8s/pod.yaml
    kubectl get pod hola-yaml -o jsonpath='{.spec.containers[0].env[?(@.name=="MENSAJE")].value}{"\n"}'
    
  • Cambiaste solo la imagen. spec.containers[*].image sí es mutable: ese cambio se aplica sin recrear nada.

Si además apareció este aviso:

Warning: resource pods/hola-yaml is missing the kubectl.kubernetes.io/last-applied-configuration
annotation which is required by kubectl apply...

es porque ese Pod no fue creado con kubectl apply (lo creaste con kubectl run, o lo recreaste con kubectl replace --force). apply guarda en esa anotación una copia del último manifiesto aplicado para calcular las diferencias; si no está, la crea sola y sigue. Es un aviso, no un error.

Important

🤔 Para pensar: si cada cambio implica borrar y recrear el Pod, y si al borrarlo nadie lo vuelve a crear... ¿cómo se actualiza una app en producción sin cortes? La respuesta está en el próximo laboratorio: Deployments.

Paso 6: Romper cosas a propósito

Aprender a diagnosticar es tan importante como aprender a crear. Vamos a provocar dos errores clásicos.

Error 1: la imagen no existe

~/lab-01/k8s/pod-imagen-rota.yaml
apiVersion: v1
kind: Pod
metadata:
  name: hola-roto
spec:
  containers:
    - name: app
      image: registry.iximiuz.com/hola-k8s:9.9   # Esta versión no existe
kubectl apply -f k8s/pod-imagen-rota.yaml
kubectl get pod hola-roto
kubectl describe pod hola-roto

Error 2: la app se cae al iniciar

~/lab-01/k8s/pod-crash.yaml
apiVersion: v1
kind: Pod
metadata:
  name: hola-crash
spec:
  containers:
    - name: app
      image: registry.iximiuz.com/hola-k8s:1.0
      command: ["node", "no-existe.js"]   # Archivo que no está en la imagen
kubectl apply -f k8s/pod-crash.yaml
kubectl get pod hola-crash -w

Cuando veas el estado repetirse, salí con Ctrl+C y revisá los logs:

kubectl logs hola-crash
kubectl logs hola-crash --previous    # Logs del intento anterior

Diagnosticar con IA

Copiá la salida de kubectl describe o de kubectl logs y usá este prompt en tu asistente:

Soy principiante en Kubernetes. Mi Pod quedó en estado <ESTADO>.
Esta es la salida de kubectl describe / kubectl logs:

<PEGAR SALIDA>

Explicame en lenguaje simple qué significa el error, cuál es la causa
más probable y qué comando uso para confirmarlo antes de corregirlo.
Important

⚠️ La IA propone, vos verificás. Antes de aplicar una corrección, confirmala con describe, logs o explain.

Mapa de diagnóstico rápido

EstadoQué significaDónde mirar
PendingNo se pudo ubicar en un nodokubectl describe pod → Events
ErrImagePull / ImagePullBackOffNo se pudo descargar la imagenNombre, tag y registry
CrashLoopBackOffEl contenedor arranca y se caekubectl logs --previous
RunningEl contenedor está ejecutándosekubectl logs

Paso 7: Cierre y limpieza

Imperativo vs. declarativo

CLI (imperativo)YAML (declarativo)
Cómo funcionaLe das una orden al clústerDescribís el estado deseado
Ideal paraPruebas rápidas y diagnósticoEquipos, producción, GitOps
ReproducibleDepende de recordar el comandoSiempre, desde el archivo
Versionable en GitNoSí
Note

🧭 En la práctica: usá la CLI para explorar y para generar el YAML con --dry-run=client -o yaml. Usá el YAML para todo lo que deba perdurar.

Limpiá el entorno:

kubectl delete pod hola-yaml hola-roto hola-crash --ignore-not-found
kubectl get pods

Comandos del laboratorio

kubectl config current-context           # ¿Dónde estoy?
kubectl get nodes                        # ¿El clúster responde?
kubectl run <pod> --image=<img>          # Crear Pod (imperativo)
kubectl run ... --dry-run=client -o yaml # Generar YAML
kubectl apply -f <archivo>.yaml          # Crear o actualizar (declarativo)
kubectl get pods [-o wide] [-w]          # Listar Pods
kubectl describe pod <pod>               # Detalle y eventos
kubectl logs [-f] [--previous] <pod>     # Logs
kubectl exec -it <pod> -- sh             # Entrar al contenedor
kubectl port-forward pod/<pod> 8080:3000 # Acceso temporal
kubectl explain pod.spec                 # Documentación en terminal
kubectl delete pod <pod>                 # Eliminar

Desafío extra

Pedile a la IA que agregue al pod.yaml un readinessProbe y un livenessProbe que usen la ruta /health. Aplicalo, revisá con kubectl describe pod que aparezcan en la configuración y explicá con tus palabras la diferencia entre ambos.

Próximo laboratorio

De un Pod a un Deployment: réplicas, autorreparación y actualizaciones sin cortes. Lo que hoy viste como limitación del Pod se convierte en la razón de ser del Deployment.

About the Author

rossana suarez

rossana suarez

Roxs🔥| NaranjaX | ROXS FEST 🤩 | AWS Hero⭐| Docker Captain🐳 | DevOps Ambassador⭐|DevOps🔥| DevSecOps🔥|Content Creator roxs.dev | Developer👩‍💻|Mentor👩‍🏫 🇻🇪🇦🇷

Find this author online

More tutorials you might like

Learn by doing, not just by reading or watching

Sign up for a free account to start a VM playground right on this page, track your progress, and get notified about new learning materials.

Sign up for free