Kubernetes · Service · diagnóstico

Tu Service de Kubernetes no responde y kubectl get endpoints sale vacío

El Deployment dice Running, el Service existe, y la petición se queda colgada o devuelve "connection refused". Casi siempre no es la red: es una etiqueta mal escrita. Estos son los tres comandos que lo demuestran, en este orden, y el YAML corregido.

El síntoma exacto

$ kubectl get endpoints mi-servicio
NAME          ENDPOINTS   AGE
mi-servicio   <none>      4m

Esa columna ENDPOINTS en <none> es el diagnóstico completo. Un Service no envía tráfico a Pods: envía tráfico a los endpoints que Kubernetes construye con el selector. Sin endpoints no hay nada detrás del Service, y da igual cuántos Pods estén corriendo al lado.

Paso 1. Confirmar que el problema es el selector, no la red

$ kubectl get endpoints mi-servicio
$ kubectl describe svc mi-servicio | grep -A2 -E 'Selector|Endpoints'

Si Endpoints está vacío, deja la red en paz: ni DNS, ni Ingress, ni NetworkPolicy. El problema está entre el selector del Service y las etiquetas del Pod. Si Endpoints tiene direcciones y aún así no responde, el problema sí es otro y lo cuento al final.

Paso 2. Comparar letra por letra

$ kubectl get pods --show-labels
NAME                     READY   STATUS    RESTARTS   AGE   LABELS
mi-app-7d9f8c6b5-k2xqp   1/1     Running   0          5m    app=mi-app,pod-template-hash=7d9f8c6b5

$ kubectl get svc mi-servicio -o jsonpath='{.spec.selector}'; echo
{"app":"mi-aplicacion"}

Ahí está: el Pod tiene app=mi-app y el Service busca app=mi-aplicacion. Kubernetes no avisa de esto. No es un error de sintaxis, es un Service perfectamente válido que no selecciona nada. Las variantes que más cuestan encontrar son el guion de más, la mayúscula (app=Mi-App no es app=mi-app) y el espacio al final que deja un copiar y pegar.

Una forma rápida de confirmarlo es pedir los Pods con el mismo selector del Service:

$ kubectl get pods -l app=mi-aplicacion
No resources found in default namespace.

Paso 3. Verificar el puerto, que es el segundo culpable

Si las etiquetas coinciden y los endpoints aparecen pero la petición sigue fallando, el sospechoso es targetPort. El port es el puerto del Service; el targetPort es el puerto del contenedor, y tienen que ser el real, no el que uno esperaba:

$ kubectl get pod mi-app-7d9f8c6b5-k2xqp -o jsonpath='{.spec.containers[*].ports[*].containerPort}'; echo
8080
$ kubectl get svc mi-servicio -o jsonpath='{.spec.ports[*]}'; echo
{"port":80,"protocol":"TCP","targetPort":80}

El contenedor escucha en 8080 y el Service entrega en 80. Endpoints llenos, servicio muerto.

El YAML corregido

La regla que evita las dos fallas: las etiquetas se escriben una sola vez y se copian, y targetPort se escribe con nombre, no con número, para que lo resuelva el contenedor.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: mi-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: mi-app          # 1. aquí
  template:
    metadata:
      labels:
        app: mi-app        # 2. igual que 1, siempre
    spec:
      containers:
        - name: web
          image: nginx:1.27-alpine
          ports:
            - name: http   # nombre del puerto, lo usa el Service
              containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: mi-servicio
spec:
  selector:
    app: mi-app            # 3. igual que 1 y 2
  ports:
    - port: 80
      targetPort: http     # por nombre: si el contenedor cambia de puerto, esto sigue sirviendo

Y la verificación, que es lo que convierte "parece que funcionó" en "funciona":

$ kubectl apply -f mi-app.yaml
$ kubectl get endpoints mi-servicio
NAME          ENDPOINTS                       AGE
mi-servicio   10.42.0.14:80,10.42.0.15:80     5s
$ kubectl run prueba --rm -it --image=busybox:1.36 --restart=Never -- wget -qO- http://mi-servicio
<!DOCTYPE html>...

Tabla de errores comunes

Lo que vesCausa más probableCómo confirmarlo
ENDPOINTS <none>El selector no coincide con las etiquetas del Podkubectl get pods -l <selector> no devuelve nada
Endpoints con direcciones y aun así no respondetargetPort distinto del containerPort realComparar los dos con jsonpath como en el paso 3
Funciona dentro del namespace y no desde otroSe usó el nombre corto en vez del nombre completoUsar mi-servicio.<namespace>.svc.cluster.local
Endpoints aparecen y desaparecenLa readinessProbe está fallando de forma intermitentekubectl describe pod, sección Events
connection refused desde el Pod de pruebaEl contenedor escucha en 127.0.0.1, no en 0.0.0.0Revisar la configuración de la aplicación dentro del contenedor

Por qué pasa siempre

Porque el acoplamiento entre Service y Pod es por texto libre y no hay nada que lo valide. Es la primera lección real de Kubernetes: el clúster no comprueba tus intenciones, comprueba tus etiquetas. Por eso en los laboratorios que preparo, cada ejercicio termina con un comando de verificación y no con una captura de pantalla.

Practica esto en tu propio clúster, gratis

Te envío al correo dos laboratorios completos con su solución: el 01 (namespace y deployment) y el 08 (ResourceQuota). Al terminarlos vas a tener tu primera aplicación corriendo en k3s y vas a saber ponerle tope de CPU y memoria a un equipo dentro del clúster.

Quiero los 2 laboratorios gratis