Tu primer Pod en Kubernetes: de la imagen al manifiesto

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

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.
💡 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?

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
💡 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.
🤔 ¿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
PORTyMENSAJEdesdeprocess.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/:
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:
- Arriba a la derecha del playground, hacé clic en el menú de tres puntos (
⋮) y elegí Expose HTTP port.

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

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.
⚠️ 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.
✅ 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
🧠 ¿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ámetro | Qué hace |
|---|---|
--image | La imagen que publicamos en el registry |
--port | Documenta el puerto del contenedor |
--env | Variable de entorno para la app |
--labels | Etiquetas 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:
- En el árbol de la izquierda, desplegá Workloads → Pod y elegí
hola-clidentro denamespace "default". - En el panel derecho, pestaña Insights, confirmá
Phase: Running (Ready), hace cuánto arrancó y la IP del Pod. - Pasá a la pestaña Manifest: es el mismo YAML que devuelve
kubectl get pod hola-cli -o yaml. - Pasá a Events: son los mismos eventos que viste al final de
kubectl describe.

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?
🔍 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.
🖥️ 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

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
💡 --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:
- Eliminá las líneas que no aportan:
creationTimestamp: null,resources: {},dnsPolicyystatus: {}. - Agregá la variable
VERSIONcon valor"1.0". - Agregá una sección
resourcesconrequestsylimits.
[object Object]
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
🧩 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[*].imagesí 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.
🤔 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
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
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.
⚠️ La IA propone, vos verificás. Antes de aplicar una corrección, confirmala con describe, logs o explain.
Mapa de diagnóstico rápido
| Estado | Qué significa | Dónde mirar |
|---|---|---|
Pending | No se pudo ubicar en un nodo | kubectl describe pod → Events |
ErrImagePull / ImagePullBackOff | No se pudo descargar la imagen | Nombre, tag y registry |
CrashLoopBackOff | El contenedor arranca y se cae | kubectl logs --previous |
Running | El contenedor está ejecutándose | kubectl logs |
Paso 7: Cierre y limpieza
Imperativo vs. declarativo
| CLI (imperativo) | YAML (declarativo) | |
|---|---|---|
| Cómo funciona | Le das una orden al clúster | Describís el estado deseado |
| Ideal para | Pruebas rápidas y diagnóstico | Equipos, producción, GitOps |
| Reproducible | Depende de recordar el comando | Siempre, desde el archivo |
| Versionable en Git | No | Sí |
🧭 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
More tutorials you might like

How Kubernetes Reinvented Virtual Machines - In a Good Sense
How Virtual Machines were used to deploy services. What old problems containers solve and what new problems create. How Kubernetes used containers to recreate Virtual Machines in a better way?

Docker Containers vs. Kubernetes Pods - Taking a Deeper Look
Can a Kubernetes Pod be created with plain Docker commands? Learn the difference between Containers and Pods by exploring how they are implemented under the hood.

Making Sense Out of Native Sidecar Containers in Kubernetes
Understand the "native" sidecar containers, learn their difference from regular and init containers and discover their advantages in this focused and highly practical tutorial.

Getting Started with VictoriaMetrics on Kubernetes
Deploy VictoriaMetrics on Kubernetes using the VM Operator, configure metrics scraping with CRDs, and query cluster metrics.
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.