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.
  • kubectl configurado 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

ObjetoQué haceQué pasa si falla un nodo
PodEjecuta uno o varios contenedores que comparten IP y volúmenesUn Pod suelto desaparece y nadie lo recrea
DeploymentDeclara cuántas réplicas de un Pod quieres y con qué plantilla; gestiona las actualizaciones a través de ReplicaSetsCrea Pods nuevos en otros nodos para mantener el número de réplicas
ServiceDa un nombre DNS y una IP fija a un grupo de Pods, y reparte el tráfico entre ellosDeja 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.matchLabels y template.metadata.labels deben coincidir: así el Deployment reconoce sus Pods.
  • resources.requests es lo que el planificador reserva en el nodo; limits.memory es el máximo antes de que el contenedor muera por falta de memoria.
  • readinessProbe decide si el Pod recibe tráfico del Service. livenessProbe reinicia 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

TipoAccesible desdeUso típico
ClusterIPSolo dentro del clústerComunicación entre servicios (API, bases de datos)
NodePortIP_de_cualquier_nodo:30000-32767Pruebas, o detrás de un balanceador externo
LoadBalancerIP externa asignada por un controladorClú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
EstadoCausa habitualQué revisar
ImagePullBackOff / ErrImagePullImagen o etiqueta inexistente, o registro privado sin credencialesNombre de la imagen; imagePullSecrets si es privada
CrashLoopBackOffEl contenedor arranca y termina con errorkubectl logs nombre --previous para ver el log del intento anterior
PendingNingún nodo tiene CPU o memoria suficientes para las requestsEventos FailedScheduling en describe; baja las requests o añade nodos
Running pero 0/1 en READYFalla la readinessProbeRuta y puerto de la sonda; logs de la aplicación
OOMKilled en describeEl contenedor superó limits.memorySube 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.