Día 48 - Desplegar un Pod en un Cluster de Kubernetes
Problema / Desafío
El equipo de Nautilus arranca con Kubernetes para gestionar aplicaciones. La consigna es crear un Pod con estos requisitos exactos:
- Nombre del pod:
pod-nginx - Imagen:
nginx:latest(el tag debe especificarse explícitamente) - Label:
app: nginx_app - Nombre del contenedor:
nginx-container
kubectl ya viene configurado en el jump-host y apunta al cluster del lab.
Conceptos clave
El Pod: la unidad atómica de Kubernetes
Un Pod es la unidad más pequeña que Kubernetes sabe orquestar — no un contenedor. Un pod encapsula uno o más contenedores que comparten:
- Network namespace — misma IP del cluster, mismos puertos, se ven entre sí en
localhost - Storage volumes — pueden montar los mismos volumes para compartir archivos
- Lifecycle — nacen y mueren juntos como una unidad
En el 95% de los casos un pod tiene un solo contenedor. Los multi-container son para patrones específicos (sidecar para logging, init container para preparar storage, ambassador para proxying).
Kubernetes nunca crea contenedores sueltos — todo va dentro de un pod, aunque el pod tenga un solo contenedor.
Imperativo vs Declarativo
kubectl acepta dos estilos para crear recursos:
| Enfoque | Cómo se ve | Cuándo usarlo |
|---|---|---|
| Imperativo | kubectl run pod-nginx --image=nginx:latest ... |
Comandos rápidos, debugging interactivo, labs de examen |
| Declarativo | YAML manifest + kubectl apply -f pod.yml |
Producción, GitOps, reproducibilidad, code review |
El imperativo es directo pero limitado: muchas opciones (multi-container, init containers, probes, volumes complejos, affinity rules) solo se pueden expresar en YAML. El declarativo es la forma "real" en cualquier setup serio porque:
- El manifest se versiona en git → toda la infraestructura es revisable, auditable y reproducible
kubectl applyes idempotente: aplicar el mismo manifest dos veces no rompe nada — converge al estado deseado- Es la base de GitOps (ArgoCD, Flux): el cluster reconcilia contra los manifests en un repo
Anatomía de cualquier manifest de Kubernetes
Esta estructura de 4 campos top-level se repite en todos los recursos (Pods, Deployments, Services, ConfigMaps, etc.):
apiVersion: <grupo/versión> # qué API estás usando
kind: <Tipo> # qué tipo de recurso es
metadata: # identidad: nombre, labels, namespace, annotations
name: ...
labels: ...
spec: # el deseo (qué quieres que exista)
...
apiVersion— Para Pods (recurso core estable):v1. Para Deployments:apps/v1. Para CronJobs:batch/v1. La versión cambia cuando el API evoluciona.kind— El tipo de recurso. Capitalizado:Pod,Deployment,Service.metadata— Quién es este recurso. Aquí vanname,namespace,labels,annotations.spec— Qué quieres que sea. Esta sección cambia totalmente según elkind.
Labels: el sistema nervioso de Kubernetes
Los labels (metadata.labels) son pares clave-valor que se asignan al recurso para:
- Filtrar:
kubectl get pods -l app=nginx_app - Vincular recursos: un Service usa un
selectorque matchea labels para saber a qué pods enviar tráfico - Organizar: entornos (
env=prod), equipos (team=platform), versiones (version=v2)
Los labels son la forma en que Services, Deployments, ReplicaSets y NetworkPolicies "encuentran" a los pods que deben gestionar. Sin labels, un Service no sabría a qué pods balancear.
Los contenedores dentro de
spec.containersno tienen labels propios. Los labels viven a nivel del recurso, enmetadata.
Pasos
- Escribir el manifest
pod.ymlcon la estructuraapiVersion / kind / metadata / spec - Aplicar con
kubectl apply -f pod.yml - Verificar que el pod esté en estado
Running - Inspeccionar detalles y filtrar por label
Comandos / Código
Solución utilizada (declarativa)
apiVersion: v1
kind: Pod
metadata:
name: pod-nginx
labels:
app: nginx_app
spec:
containers:
- name: nginx-container
image: nginx:latest
Aplicar:
Alternativa: imperativa con kubectl run
Para esta misma consigna, también se puede crear el pod sin YAML usando flags directos. Esto es útil para labs rápidos o debugging en una sesión interactiva.
Nota histórica importante: en versiones antiguas de kubectl (pre-v1.18),
kubectl runcreaba un Deployment por default y había que pasarle--restart=Neverpara forzar un Pod puro. Desde kubectl v1.18+ el comportamiento cambió:kubectl runcrea solo Pods, sin Deployment ni--restart=Nevernecesario. Mucha documentación vieja todavía menciona ese flag — ya no aplica.
El reto aparente: kubectl run no expone un flag --container-name para renombrar el container dentro del pod (por default le pone el mismo nombre que el pod). La solución es --overrides, que inyecta un fragmento de JSON sobre el manifest generado:
kubectl run pod-nginx \
--image=nginx:latest \
--labels="app=nginx_app" \
--overrides='{
"apiVersion": "v1",
"spec": {
"containers": [
{
"image": "nginx:latest",
"name": "nginx-container"
}
]
}
}'
Por qué hay que repetir image dentro del --overrides
El JSON de --overrides reemplaza completamente la sección que toca (no hace merge profundo del array containers[]). Si solo pones {"name": "nginx-container"}, el container resultante quedaría sin imagen y el pod no arrancaría. Por eso hay que repetir image: nginx:latest dentro del override aunque también esté en --image.
Esto ilustra exactamente por qué el manifest YAML es preferible: para una sola personalización (renombrar el container) se necesita JSON inline, repetir campos, escapar comillas, y aún así perder validación de schema en el editor. El
pod.ymldeclarativo es más corto, más legible y diff-eable en code review.
Verificación
READY 1/1 significa que de 1 contenedor esperado en el pod, 1 está listo (passed readiness). El segundo número refleja la spec.containers declarada; el primero es cuántos están live. Si vieras 0/1, el contenedor existe pero todavía no pasó el readiness probe (o no arrancó).
# Ver logs del nginx (debería mostrar el banner de arranque del entrypoint oficial)
kubectl logs pod-nginx
Cuando un pod tiene un solo contenedor,
kubectl logs pod-nginxbasta. Para pods multi-container hay que pasar-c <container>(kubectl logs pod-nginx -c nginx-container), si no kubectl pide que elijas explícitamente.
# Ver detalles completos: events, IP, container ID, image digest, etc.
kubectl describe pod pod-nginx
Name: pod-nginx
Namespace: default
Priority: 0
Service Account: default
Node: jump-host/10.244.97.141
Start Time: Fri, 08 May 2026 01:59:48 +0000
Labels: app=nginx_app
Annotations: <none>
Status: Running
IP: 10.22.0.9
Containers:
nginx-container:
Container ID: containerd://f9d882095e9df7d33dbd626289145c5c52f06ee4e65287ea57b5ec97ec28b239
Image: nginx:latest
Image ID: docker.io/library/nginx@sha256:6e23479198b998e5e25921dff8455837c7636a67111a04a635cf1bb363d199dc
State: Running
Started: Fri, 08 May 2026 01:59:52 +0000
Ready: True
Restart Count: 0
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-nwpv8 (ro)
QoS Class: BestEffort
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 3m19s default-scheduler Successfully assigned default/pod-nginx to jump-host
Normal Pulling 3m19s kubelet Pulling image "nginx:latest"
Normal Pulled 3m15s kubelet Successfully pulled image "nginx:latest" in 3.304s
Normal Created 3m15s kubelet Created container: nginx-container
Normal Started 3m15s kubelet Started container nginx-container
Cosas dignas de subrayar de este output:
Image:vsImage ID:—Imagees lo que pediste (nginx:latest, un tag mutable).Image IDes el digest SHA256 inmutable que el kubelet realmente bajó. Si el taglatestcambia mañana, este pod sigue corriendo este digest; solo al recrear el pod resolvería ellatestnuevo.QoS Class: BestEffort— porque el pod no tienerequestsnilimitsdefinidos. Es el QoS más bajo: si el nodo se queda sin memoria, este es el primero en ser evictado. En producción real conviene al menosBurstable(con requests) oGuaranteed(requests = limits).Eventses la sección más útil para debugging — la línea de tiempo de qué le pasó al pod desde su creación. Si algo va mal (ImagePullBackOff,CrashLoopBackOff), aparece acá con el motivo.
# Filtrar por label — confirma que el label se aplicó correctamente
kubectl get pods -l app=nginx_app
Validar el manifest sin aplicar (dry-run)
Antes de aplicar al cluster real, conviene validar la sintaxis y la estructura:
--dry-run=client valida localmente sin contactar el API server. --dry-run=server lo valida contra el cluster (incluye admission controllers) pero sin persistir.
Comparación: Imperativo vs Declarativo en este caso
| Aspecto | kubectl run (imperativo) |
YAML + kubectl apply (declarativo) |
|---|---|---|
| Velocidad para crear | Muy rápido (una línea) | Hay que escribir el manifest |
| Versionable en git | No (es una invocación efímera) | Sí (el .yml se commitea) |
| Idempotente | No (kubectl run falla si ya existe) |
Sí (apply actualiza si existe, crea si no) |
| Permite multi-container, probes, etc. | Limitado o imposible | Sí, todo es expresable |
| Ideal para | Labs, debugging, exámenes (CKAD, CKA) | Producción, GitOps, code review |
Troubleshooting
| Problema | Causa y solución |
|---|---|
pod/pod-nginx unchanged al aplicar |
No es error — apply es idempotente: el cluster ya tiene ese estado y no hace nada |
Error from server (NotFound): pods "pod-nifnx" not found |
Typo en el nombre del pod (típico al transcribir). kubectl no hace fuzzy match: el nombre debe coincidir exacto. Ejecutar kubectl get pods para ver los nombres reales |
Pod en ImagePullBackOff |
El nodo no pudo descargar nginx:latest. Revisar conectividad del nodo al registry o si el tag existe |
Pod en Pending por mucho tiempo |
kubectl describe pod pod-nginx → revisar events. Suele ser falta de recursos del nodo o un nodeSelector imposible |
Error error validating "pod.yml" |
Sintaxis YAML incorrecta o campo mal indentado. Validar con kubectl apply --dry-run=client -f pod.yml |
Cambié el image: en el YAML pero no actualiza |
Un Pod no hace rolling update — para eso se usa Deployment. Borrar el pod (kubectl delete pod pod-nginx) y reaplicar |
kubectl run falla con "already exists" |
A diferencia de apply, run es one-shot. Borrar primero con kubectl delete pod pod-nginx o usar apply |