--- title: "Kubernetes: Pods, Deployments y Services con ejemplos" description: "Aprende qué son los Pods, Deployments y Services de Kubernetes creando una aplicación con YAML: réplicas, health checks, actualizaciones y rollback con kubectl." category: "Docker Kubernetes" language: es source: https://cubepath.com/es/docs/docker-kubernetes/kubernetes-pods-servicios-deployments updated: 2026-09-25 --- # Kubernetes: Pods, Deployments y Services con ejemplos 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: ```bash kubectl get nodes ``` ```output NAME STATUS ROLES AGE VERSION k8s-cp Ready control-plane 2d v1.36.1 k8s-worker1 Ready 2d v1.36.1 k8s-worker2 Ready 2d v1.36.1 ``` Crea un directorio para los manifiestos y un namespace para aislar las pruebas: ```bash 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: ```bash nano pod.yaml ``` ```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: ```bash kubectl apply -f pod.yaml kubectl get pods -o wide ``` ```output 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: ```bash kubectl logs web-suelto kubectl exec -it web-suelto -- sh ``` Ahora bórralo y comprueba que nadie lo vuelve a crear: ```bash kubectl delete pod web-suelto kubectl get pods ``` ```output 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. ```bash nano deployment.yaml ``` ```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: ```bash kubectl apply -f deployment.yaml kubectl rollout status deployment/web ``` ```output 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: ```bash kubectl get deployment,replicaset,pods -l app=web ``` ```output 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): ```bash 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: ```bash sed -i 's/replicas: 3/replicas: 4/' deployment.yaml kubectl apply -f deployment.yaml kubectl get deployment web ``` ```output 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: ```bash nano service.yaml ``` ```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: ```bash kubectl apply -f service.yaml kubectl get service web kubectl get endpointslices -l kubernetes.io/service-name=web ``` ```output NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE web ClusterIP 10.98.143.20 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: ```bash 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`: ```bash sed -i 's/type: ClusterIP/type: NodePort/' service.yaml kubectl apply -f service.yaml kubectl get service web ``` ```output NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE web NodePort 10.98.143.20 80:31503/TCP 2m ``` Desde un nodo del clúster (sustituye el puerto por el que te haya tocado): ```bash curl -sI http://your_worker1_ip:31503 | head -1 ``` ```output 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: ```bash 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: ```bash kubectl get deployment web -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}' kubectl rollout history deployment/web ``` ```output nginx:1.28 deployment.apps/web REVISION CHANGE-CAUSE 1 2 ``` Ahora simula un despliegue erróneo con una etiqueta de imagen que no existe: ```bash 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 ``` ```output 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: ```bash 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: ```bash 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: ```bash 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: ```bash 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: ```bash 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.