Pods, Deployments y Services son los tres objetos de Kubernetes que usarás en casi cualquier aplicación: el Pod ejecuta los contenedores, el Deployment mantiene el número de Pods que quieres y los actualiza sin cortes, y el Service les da una dirección estable. En este tutorial crearás cada uno con manifiestos YAML, verás cómo se relacionan mediante etiquetas y harás una actualización con rollback. Los comandos funcionan en cualquier clúster de Kubernetes reciente.
Requisitos previos
- Un clúster de Kubernetes en funcionamiento con al menos un nodo worker, por ejemplo uno creado con kubeadm sobre VPS de CubePath con Ubuntu 24.04.
kubectlconfigurado para ese clúster. Compruébalo con:
kubectl get nodes
NAME STATUS ROLES AGE VERSION
k8s-cp Ready control-plane 2d v1.36.1
k8s-worker1 Ready <none> 2d v1.36.1
k8s-worker2 Ready <none> 2d v1.36.1
Crea un directorio para los manifiestos y un namespace para aislar las pruebas:
mkdir -p ~/k8s-demo && cd ~/k8s-demo
kubectl create namespace demo
kubectl config set-context --current --namespace=demo
El último comando hace que kubectl use demo por defecto, así no tendrás que añadir -n demo a cada orden.
Cómo se relacionan los tres objetos
| Objeto | Qué hace | Qué pasa si falla un nodo |
|---|---|---|
| Pod | Ejecuta uno o varios contenedores que comparten IP y volúmenes | Un Pod suelto desaparece y nadie lo recrea |
| Deployment | Declara cuántas réplicas de un Pod quieres y con qué plantilla; gestiona las actualizaciones a través de ReplicaSets | Crea Pods nuevos en otros nodos para mantener el número de réplicas |
| Service | Da un nombre DNS y una IP fija a un grupo de Pods, y reparte el tráfico entre ellos | Deja de enviar tráfico a los Pods que no están listos |
El Deployment y el Service no apuntan a Pods concretos por nombre, sino por etiquetas (labels): cualquier Pod con app: web pertenece al grupo.
Paso 1: Crear un Pod
Empieza con un Pod aislado para ver la unidad básica:
nano pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: web-suelto
labels:
app: prueba
spec:
containers:
- name: nginx
image: nginx:1.27
ports:
- containerPort: 80
Aplica el manifiesto y observa el Pod:
kubectl apply -f pod.yaml
kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE
web-suelto 1/1 Running 0 8s 10.244.1.5 k8s-worker1
El Pod recibe una IP de la red de pods y se programa en un nodo. kubectl describe pod web-suelto muestra sus eventos (descarga de imagen, arranque) y es el primer comando a usar cuando algo no arranca. Para ver sus logs o entrar en él:
kubectl logs web-suelto
kubectl exec -it web-suelto -- sh
Ahora bórralo y comprueba que nadie lo vuelve a crear:
kubectl delete pod web-suelto
kubectl get pods
No resources found in demo namespace.
Por eso en producción casi nunca se crean Pods directamente: se crean a través de un Deployment.
Paso 2: Crear un Deployment
El Deployment incluye una plantilla de Pod (template) y un selector que indica qué Pods son suyos. Este ejemplo añade además las piezas que cualquier Deployment real debería llevar: recursos y sondas de salud.
nano deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: nginx
image: nginx:1.27
ports:
- containerPort: 80
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
memory: 128Mi
readinessProbe:
httpGet:
path: /
port: 80
periodSeconds: 5
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 10
periodSeconds: 10
Qué significa cada bloque:
selector.matchLabelsytemplate.metadata.labelsdeben coincidir: así el Deployment reconoce sus Pods.resources.requestses lo que el planificador reserva en el nodo;limits.memoryes el máximo antes de que el contenedor muera por falta de memoria.readinessProbedecide si el Pod recibe tráfico del Service.livenessProbereinicia el contenedor si deja de responder.
Aplica el manifiesto y espera a que termine el despliegue:
kubectl apply -f deployment.yaml
kubectl rollout status deployment/web
Waiting for deployment "web" rollout to finish: 0 of 3 updated replicas are available...
deployment "web" successfully rolled out
Mira los objetos que ha creado. El Deployment crea un ReplicaSet, y este crea los Pods:
kubectl get deployment,replicaset,pods -l app=web
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/web 3/3 3 3 30s
NAME DESIRED CURRENT READY AGE
replicaset.apps/web-6d8f7b9c5d 3 3 3 30s
NAME READY STATUS RESTARTS AGE
pod/web-6d8f7b9c5d-8kq2x 1/1 Running 0 30s
pod/web-6d8f7b9c5d-f7n4p 1/1 Running 0 30s
pod/web-6d8f7b9c5d-tx9lm 1/1 Running 0 30s
Comprobar la autorreparación
Borra uno de los Pods (usa uno de tus nombres):
kubectl delete pod web-6d8f7b9c5d-8kq2x
kubectl get pods -l app=web
En segundos aparece un Pod nuevo con otro sufijo: el ReplicaSet detecta que hay dos réplicas en lugar de tres y crea la que falta.
Escalar
Para cambiar el número de réplicas, edita replicas en el YAML y vuelve a aplicarlo. Es preferible a kubectl scale, porque el archivo sigue reflejando el estado real:
sed -i 's/replicas: 3/replicas: 4/' deployment.yaml
kubectl apply -f deployment.yaml
kubectl get deployment web
NAME READY UP-TO-DATE AVAILABLE AGE
web 4/4 4 4 2m
Paso 3: Exponer el Deployment con un Service
Los Pods cambian de IP cada vez que se recrean. Un Service de tipo ClusterIP (el tipo por defecto) les da una IP virtual fija y un nombre DNS dentro del clúster:
nano service.yaml
apiVersion: v1
kind: Service
metadata:
name: web
spec:
type: ClusterIP
selector:
app: web
ports:
- port: 80
targetPort: 80
selector elige los Pods con app: web, port es el puerto del Service y targetPort el del contenedor. Aplícalo y comprueba sus endpoints, es decir, las IP de los Pods listos a los que envía tráfico:
kubectl apply -f service.yaml
kubectl get service web
kubectl get endpointslices -l kubernetes.io/service-name=web
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web ClusterIP 10.98.143.20 <none> 80/TCP 5s
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
web-x7kd2 IPv4 80 10.244.1.7,10.244.2.4,10.244.1.8 + 1 more... 5s
Prueba el acceso por nombre desde un Pod temporal:
kubectl run cliente --rm -it --image=busybox:1.36 --restart=Never -- wget -qO- http://web
La salida es el HTML de bienvenida de Nginx. Desde otro namespace el nombre completo sería web.demo.svc.cluster.local.
Tipos de Service
| Tipo | Accesible desde | Uso típico |
|---|---|---|
ClusterIP | Solo dentro del clúster | Comunicación entre servicios (API, bases de datos) |
NodePort | IP_de_cualquier_nodo:30000-32767 | Pruebas, o detrás de un balanceador externo |
LoadBalancer | IP externa asignada por un controlador | Clústeres con integración de balanceador (en kubeadm necesitas instalar uno como MetalLB) |
Para probar el acceso externo, cambia el tipo a NodePort:
sed -i 's/type: ClusterIP/type: NodePort/' service.yaml
kubectl apply -f service.yaml
kubectl get service web
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web NodePort 10.98.143.20 <none> 80:31503/TCP 2m
Desde un nodo del clúster (sustituye el puerto por el que te haya tocado):
curl -sI http://your_worker1_ip:31503 | head -1
HTTP/1.1 200 OK
Para exponer aplicaciones web en producción lo habitual es un Service ClusterIP detrás de un Ingress o Gateway, que añade dominios y HTTPS.
Paso 4: Actualizar y hacer rollback
Cuando cambias la plantilla del Pod (por ejemplo, la imagen), el Deployment crea un ReplicaSet nuevo y sustituye los Pods poco a poco. Gracias a la readinessProbe, un Pod nuevo solo recibe tráfico cuando responde, así que no hay corte.
Cambia la versión de Nginx en el manifiesto y aplícalo:
sed -i 's/image: nginx:1.27/image: nginx:1.28/' deployment.yaml
kubectl apply -f deployment.yaml
kubectl rollout status deployment/web
Comprueba la imagen en uso y el historial de revisiones:
kubectl get deployment web -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
kubectl rollout history deployment/web
nginx:1.28
deployment.apps/web
REVISION CHANGE-CAUSE
1 <none>
2 <none>
Ahora simula un despliegue erróneo con una etiqueta de imagen que no existe:
sed -i 's/image: nginx:1.28/image: nginx:9.99-no-existe/' deployment.yaml
kubectl apply -f deployment.yaml
kubectl get pods -l app=web
NAME READY STATUS RESTARTS AGE
web-5b9d6c7f48-2xkpl 1/1 Running 0 3m
web-5b9d6c7f48-9fqzd 1/1 Running 0 3m
web-5b9d6c7f48-lm4ns 1/1 Running 0 3m
web-7f5c8d9b6a-qw8rt 0/1 ImagePullBackOff 0 20s
web-7f5c8d9b6a-v2c7x 0/1 ImagePullBackOff 0 20s
Fíjate en que la aplicación sigue funcionando: la estrategia RollingUpdate por defecto no retira más Pods antiguos mientras los nuevos no estén listos. Vuelve a la revisión anterior:
kubectl rollout undo deployment/web
kubectl rollout status deployment/web
Tras el rollback, restaura también la imagen correcta en deployment.yaml para que el archivo no vuelva a desplegar la versión rota la próxima vez que lo apliques:
sed -i 's/image: nginx:9.99-no-existe/image: nginx:1.28/' deployment.yaml
kubectl apply -f deployment.yaml
Solución de problemas
Para cualquier Pod que no esté Running y READY, empieza por los eventos:
kubectl describe pod nombre_del_pod
| Estado | Causa habitual | Qué revisar |
|---|---|---|
ImagePullBackOff / ErrImagePull | Imagen o etiqueta inexistente, o registro privado sin credenciales | Nombre de la imagen; imagePullSecrets si es privada |
CrashLoopBackOff | El contenedor arranca y termina con error | kubectl logs nombre --previous para ver el log del intento anterior |
Pending | Ningún nodo tiene CPU o memoria suficientes para las requests | Eventos FailedScheduling en describe; baja las requests o añade nodos |
Running pero 0/1 en READY | Falla la readinessProbe | Ruta y puerto de la sonda; logs de la aplicación |
OOMKilled en describe | El contenedor superó limits.memory | Sube el límite o reduce el consumo |
Si el Service no responde, comprueba que tiene endpoints. Un Service sin endpoints casi siempre tiene un selector que no coincide con las etiquetas de los Pods:
kubectl get endpointslices -l kubernetes.io/service-name=web
kubectl get pods --show-labels
Limpieza
Borra el namespace de pruebas, que elimina todo lo que hay dentro, y vuelve al namespace por defecto:
kubectl config set-context --current --namespace=default
kubectl delete namespace demo
Conclusión
Has creado un Pod y has visto por qué no se usa solo, has desplegado una aplicación con un Deployment que se autorrepara, escala y se actualiza sin cortes, y la has publicado con un Service tanto dentro como fuera del clúster. También has recuperado un despliegue fallido con kubectl rollout undo.
Como siguientes pasos puedes:
- Separar la configuración y las credenciales de la imagen con ConfigMaps y Secrets.
- Publicar la aplicación con un Ingress Controller y certificados HTTPS con cert-manager.
- Guardar los manifiestos en Git y aplicarlos con Kustomize o Helm para gestionar varios entornos.
