Kubernetes básico: Pod, Deployment y Service

1. Descripción general

Kubernetes tiene decenas de tipos de recursos, pero el 80% del trabajo diario gira en torno a tres conceptos: Pod, Deployment, Service. Este artículo te ayudará a entender la naturaleza de cada recurso, cómo se interconectan, y a escribir un conjunto de manifiestos YAML que desplieguen una aplicación real y funcional en tu cluster.

Después de leerlo, podrás desplegar por ti mismo una aplicación Spring Boot en Kubernetes, escalarla a múltiples réplicas y exponerla al exterior a través de un Service — todo simplemente con kubectl y unos pocos archivos YAML.

Nota: Este artículo asume que ya tienes un cluster (Minikube, Kind, o en la nube) y que kubectl apunta al contexto correcto.

2. Pod — La unidad de despliegue más pequeña

2.1. ¿Qué es un Pod?

Un Pod es un grupo de uno o más contenedores que comparten el namespace de red (misma IP, mismo espacio de puertos) y volúmenes. En la práctica, la mayoría de los Pods contienen un único contenedor de aplicación.

apiVersion: v1
kind: Pod
metadata:
  name: order-service-pod
  labels:
    app: order-service
spec:
  containers:
    - name: order-service
      image: nonladev/order-service:1.0
      ports:
        - containerPort: 8080
      resources:
        requests:
          cpu: "250m"
          memory: "256Mi"
        limits:
          cpu: "500m"
          memory: "512Mi"

Explicación:

  • metadata.labels asigna la etiqueta app: order-service — así es como Kubernetes “identifica” el Pod; los Services y Deployments usan esta etiqueta para seleccionar objetos relacionados.
  • resources.requests es el recurso mínimo que el scheduler necesita disponible para colocar el Pod en un nodo; limits es el techo máximo, excederlo resultará en throttling (CPU) o terminación (memoria — OOMKilled).
  • containerPort: 8080 es solo una declaración informativa, no abre automáticamente el puerto al exterior.

Aplicar y verificar:

kubectl apply -f order-service-pod.yaml
kubectl get pods
NAME                 READY   STATUS    RESTARTS   AGE
order-service-pod    1/1     Running   0          5s

2.2. Por qué no crear Pods directamente

Un Pod creado “crudamente” como el anterior tiene un problema importante: si el nodo que lo contiene falla, o el Pod se bloquea, nadie creará uno nuevo automáticamente. Los Pods no tienen mecanismo de auto-sanación ni escalado.

Nota: En entornos de producción, prácticamente nunca crearás Pods directamente. Siempre usa Deployment (o StatefulSet, Job) para gestionar los Pods.

3. Deployment — Gestionar el ciclo de vida de los Pods

3.1. Declarar un Deployment

Un Deployment crea y gestiona un conjunto de Pods a través de un ReplicaSet, asegurando que siempre hay un número exacto de réplicas ejecutándose, y soporta actualizaciones rolling sin tiempo de inactividad.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
    spec:
      containers:
        - name: order-service
          image: nonladev/order-service:1.0
          ports:
            - containerPort: 8080
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 5
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
            initialDelaySeconds: 15
            periodSeconds: 10

Explicación:

  • replicas: 3 requiere que el Deployment mantenga siempre 3 Pods ejecutándose — si uno falla, el Deployment crea automáticamente uno nuevo como reemplazo.
  • selector.matchLabels debe coincidir con template.metadata.labels. Este es el vínculo obligatorio entre el Deployment y los Pods que gestiona.
  • readinessProbe determina si un Pod está listo para recibir tráfico (usado por el Service); livenessProbe determina si el Pod necesita reiniciarse. Confundir estos dos probes es un error muy común para principiantes en Kubernetes.

Aplicar y observar:

kubectl apply -f order-service-deployment.yaml
kubectl get deployments
kubectl get pods -l app=order-service
NAME            READY   UP-TO-DATE   AVAILABLE   AGE
order-service   3/3     3            3           12s

NAME                             READY   STATUS    RESTARTS   AGE
order-service-7d4f9c8b6d-2kx9p   1/1     Running   0          12s
order-service-7d4f9c8b6d-7h2mn   1/1     Running   0          12s
order-service-7d4f9c8b6d-xq1lw   1/1     Running   0          12s

3.2. Escalar y actualización rolling

Escalar el número de réplicas hacia arriba o abajo sin necesidad de editar el YAML:

kubectl scale deployment order-service --replicas=5

Actualización rolling cuando cambias la imagen — el Deployment reemplaza automáticamente los Pods antiguos por otros nuevos en forma de onda, sin tiempo de inactividad:

kubectl set image deployment/order-service order-service=nonladev/order-service:1.1
kubectl rollout status deployment/order-service
Waiting for deployment "order-service" rollout to finish: 2 out of 3 new replicas have been updated...
deployment "order-service" successfully rolled out

Si la nueva versión tiene errores, revierte a la versión anterior instantáneamente:

kubectl rollout undo deployment/order-service

3.3. Configurar la estrategia de actualización rolling

spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 1

maxUnavailable: 1 permite que como máximo 1 Pod se “pierda” durante la actualización; maxSurge: 1 permite crear como máximo 1 Pod adicional más allá del número de réplicas declaradas. Con replicas: 3, el cluster ejecutará entre 2 y 4 Pods durante la transición, pero nunca menos de 2.

4. Service — Exponer Pods de forma estable

4.1. Por qué necesitas un Service

Los Pods tienen una IP propia, pero esta IP cambia cada vez que el Pod se recrea (crash, actualización rolling, escalado). Un Service proporciona una dirección estable (ClusterIP + nombre DNS) frente a un grupo de Pods, distribuye automáticamente el tráfico según el selector de etiquetas — sin preocuparse por cuántas veces cambien los Pods subyacentes.

apiVersion: v1
kind: Service
metadata:
  name: order-service
spec:
  selector:
    app: order-service
  ports:
    - port: 80
      targetPort: 8080
  type: ClusterIP

Explicación:

  • selector.app: order-service coincide con la etiqueta del Pod (que el Deployment asignó en template.metadata.labels) — el Service busca y monitorea automáticamente estos Pods a través de un objeto intermedio llamado Endpoints.
  • port: 80 es el puerto en el que el Service escucha dentro del cluster; targetPort: 8080 es el puerto que el contenedor realmente expone. Estos dos números no tienen que ser iguales.
  • type: ClusterIP (predeterminado) solo es accesible desde dentro del cluster — adecuado para servicios internos entre microservicios.

Verificar el Service y los Endpoints:

kubectl apply -f order-service-service.yaml
kubectl get svc order-service
kubectl get endpoints order-service
NAME            TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
order-service   ClusterIP   10.96.142.201   <none>        80/TCP    4s

NAME            ENDPOINTS                                   AGE
order-service   10.244.1.5:8080,10.244.1.6:8080,+1 more     4s

4.2. Tipos de Service

Tipo Alcance de acceso Cuándo usarlo
ClusterIP Solo dentro del cluster Comunicación entre microservicios internos
NodePort Puerto fijo (30000-32767) en todos los nodos Pruebas rápidas, entorno dev/on-prem
LoadBalancer IP pública a través del proveedor de nube Exponer servicio a Internet en la nube

Ejemplo para exponer al exterior para pruebas rápidas en Minikube:

apiVersion: v1
kind: Service
metadata:
  name: order-service-external
spec:
  selector:
    app: order-service
  ports:
    - port: 80
      targetPort: 8080
      nodePort: 30080
  type: NodePort
minikube service order-service-external --url

Nota: Usar NodePort en producción es un anti-patrón — el rango de puertos es limitado, no hay terminación TLS ni enrutamiento basado en salud como con Ingress. En producción en la nube, prioriza LoadBalancer o un Ingress Controller.

5. Combinar los tres en una aplicación real

El flujo completo: el cliente llama al Service → el Service distribuye el tráfico entre los Pods que gestiona el Deployment.

Cliente
  │
  ▼
Service (order-service, ClusterIP 10.96.142.201:80)
  │  (selector de etiquetas: app=order-service)
  ▼
┌─────────────┬─────────────┬─────────────┐
│  Pod #1     │  Pod #2     │  Pod #3     │
│  :8080      │  :8080      │  :8080      │
└─────────────┴─────────────┴─────────────┘
       ▲ gestionado por
  Deployment (order-service, replicas: 3)

Para facilitar el despliegue en una sola operación, puedes combinar el Deployment y el Service en un mismo archivo, separándolos con ---:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
    spec:
      containers:
        - name: order-service
          image: nonladev/order-service:1.0
          ports:
            - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: order-service
spec:
  selector:
    app: order-service
  ports:
    - port: 80
      targetPort: 8080
kubectl apply -f order-service-all.yaml

6. Escribir tests para el endpoint de readiness antes de desplegar

Antes de que el probe de Kubernetes se base en /actuator/health/readiness, debes asegurarte de que este endpoint devuelve el estado correcto en tu código Spring Boot.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
management.endpoint.health.probes.enabled=true
management.health.readiness-state.enabled=true
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class OrderServiceHealthTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void readinessProbe_shouldReturnUp() {
        ResponseEntity<String> response =
                restTemplate.getForEntity("/actuator/health/readiness", String.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).contains("\"status\":\"UP\"");
    }
}

Este test llama a TestRestTemplate en el endpoint de readiness real, verificando que devuelve HTTP 200 y que el body contiene UP. Este es exactamente el endpoint que el readinessProbe en el Deployment llamará periódicamente — si el test falla aquí, el Pod tampoco estará nunca “Ready” en el cluster.

7. Errores comunes

  • Selector no coincide con la etiqueta: El Service o Deployment no encuentra ningún Pod (Endpoints vacío) porque selector y labels están escritos incorrectamente o son distintos. Siempre ejecuta kubectl get endpoints <svc-name> para confirmar.
  • Falta readinessProbe: El Service sigue enrutando tráfico a Pods que aún no han iniciado la aplicación completamente, causando errores 502/503 justo después del despliegue.
  • Requests/limits de recursos faltantes: El Pod podría ser colocado en el nodo incorrecto por el scheduler, o ser terminado inesperadamente (OOMKilled) cuando el tráfico aumenta.
  • Usar Pods crudos en producción: Sin auto-sanación, perder un Pod es perderlo completamente — tendrías que recrearlo manualmente.
  • Tag de imagen latest: La actualización rolling no detecta cambios, Kubernetes cree que la imagen no ha cambiado y no la descarga de nuevo. Siempre usa una etiqueta de versión específica.

8. Conclusión

Un Pod es la unidad más pequeña para ejecutar un contenedor, un Deployment gestiona el ciclo de vida y el escalado de un conjunto de Pods, y un Service proporciona una dirección estable para acceder a ellos aunque los Pods subyacentes cambien constantemente. Dominar estos tres pilares es la base para avanzar después hacia Ingress, ConfigMap/Secret, o charts de Helm. El siguiente paso debería ser intentar desplegar una aplicación Spring Boot real, observar cómo funciona la actualización rolling, y probar a eliminar un Pod manualmente para ver cómo el Deployment se recupera automáticamente.

kubernetesdevopscontainerorchestration