En la lección anterior actualizamos api-reservas sin cortar el servicio, pero para comprobar que respondía tuvimos que buscar a mano la IP de un pod concreto y esperar que siguiera existiendo cinco segundos después. Ese es el agujero que nos queda: cada despliegue, cada autorreparación y cada escalado cambian las IPs de los pods, así que ningún componente puede conocer a otro por su dirección. El Service es la respuesta de Kubernetes: una dirección virtual estable, con nombre propio, que reparte el tráfico entre todas las réplicas sanas de un componente y sobrevive a que los pods de detrás vayan y vengan. En esta lección entenderás el problema en profundidad, verás qué es exactamente un Service y cómo el controlador de endpoints mantiene actualizada la lista de destinos, leerás el manifiesto campo a campo distinguiendo port de targetPort, crearás los cuatro Servicios de Rutas Norte —tienda-web, api-reservas, postgres-reservas y redis-cache—, los verificarás desde dentro del clúster con pods efímeros y aprenderás a diagnosticar el fallo más frecuente de todo Kubernetes: un selector que no casa con las etiquetas de los pods.
Contenido
- El problema: IPs efímeras
- Qué es un Service y qué te da
- Endpoints y EndpointSlices: quién mantiene la lista
- Anatomía del manifiesto:
port,targetPort,protocol - Por qué
ClusterIPes el tipo por defecto - Los Servicios de Rutas Norte
- Verificación desde dentro del clúster
- El nombre DNS y los servicios headless
- El fallo más común: el selector que no casa
- El problema: IPs efímeras
Hagamos visible el problema con un experimento de treinta segundos. Anota las IPs actuales de api-reservas:
NAME READY STATUS RESTARTS AGE IP NODE
api-reservas-c5a9b47d8-h7kdn 1/1 Running 0 22m 10.244.0.48 rutas-norte
api-reservas-c5a9b47d8-w3pqx 1/1 Running 0 22m 10.244.0.49 rutas-norteAhora provoca un despliegue cualquiera y vuelve a mirar:
kubectl rollout restart deployment/api-reservas
kubectl rollout status deployment/api-reservas
kubectl get pods -l app=api-reservas -o wideNAME READY STATUS RESTARTS AGE IP NODE
api-reservas-7f1d3e942-b8mrs 1/1 Running 0 18s 10.244.0.52 rutas-norte
api-reservas-7f1d3e942-n4jvt 1/1 Running 0 12s 10.244.0.53 rutas-norteNombres nuevos, IPs nuevas. Si tienda-web tuviera escrita en su configuración la dirección 10.244.0.48, acabaría de perder la conexión con la API. Y los momentos en que esto ocurre son constantes:
| Situación | Efecto sobre las IPs |
|---|---|
| Despliegue de una versión nueva | Todos los pods se sustituyen: todas las IPs cambian |
| Autorreparación tras una caída | El pod sustituto tiene IP nueva |
| Escalado hacia arriba | Aparecen IPs que nadie conocía |
| Escalado hacia abajo | Desaparecen IPs que alguien tenía apuntadas |
| Desalojo por falta de memoria | El pod renace en otro nodo con otra IP |
Y hay un segundo problema, tan importante como el primero: el reparto de carga. Aunque congeláramos las IPs, tienda-web tendría que conocer las dos, tres o siete réplicas de api-reservas y repartir el tráfico entre ellas por su cuenta, comprobando cuáles están sanas. Eso es reinventar un balanceador dentro de cada cliente.
Rutas Norte necesita tres cosas de golpe:
- Una dirección que no cambie nunca, aunque los pods de detrás cambien todos.
- Reparto automático del tráfico entre las réplicas disponibles.
- Un nombre, no una IP, para no tener que configurar direcciones en ningún sitio.
- Qué es un Service y qué te da
Un Service es un objeto de Kubernetes que define un punto de acceso lógico y estable a un conjunto de pods, seleccionados por etiquetas.
flowchart LR
TW["Pods de tienda-web"]
SVC["<b>Service api-reservas</b><br/>ClusterIP 10.96.184.22<br/>nombre DNS: api-reservas"]
P1["Pod api-reservas<br/>10.244.0.52"]
P2["Pod api-reservas<br/>10.244.0.53"]
P3["Pod api-reservas<br/>10.244.0.61<br/>(creado tras un escalado)"]
TW -->|"http://api-reservas:3000"| SVC
SVC --> P1
SVC --> P2
SVC --> P3
Lo que aporta, punto por punto:
- Una IP virtual estable, la
ClusterIP. Se asigna al crear el Service y no cambia mientras el Service exista, independientemente de cuántos pods haya detrás o de si hay cero. - Un nombre DNS, que es lo que realmente usarás:
api-reservasdesde el mismo namespace. - Balanceo de carga entre todos los pods sanos que casen con el selector, por defecto aleatorio.
- Actualización automática de destinos: cuando un pod nace, entra; cuando muere o deja de estar listo, sale. Sin intervención humana.
- Un puerto lógico que puede ser distinto del puerto real del contenedor.
Un detalle que descoloca al principio: la ClusterIP no pertenece a ninguna máquina. No hay ninguna interfaz de red configurada con esa dirección, no responde a ping y no puedes hacerle ssh. Es una dirección virtual que existe únicamente como regla de reenvío instalada en cada nodo del clúster. Cuando un pod envía un paquete a 10.96.184.22, esa regla lo reescribe al vuelo hacia la IP real de uno de los pods de destino.
Quién instala esas reglas y cómo (iptables, IPVS, eBPF) es materia de Redes de Clúster. Para esta lección basta con el modelo mental: el Service es una regla de reenvío con nombre, no una máquina.
Un aviso sobre el balanceo, porque genera confusión: el reparto se hace por conexión, no por petición. Si un cliente abre una conexión HTTP persistente (keep-alive) o gRPC contra la ClusterIP, todas las peticiones de esa conexión acaban en el mismo pod. Es un comportamiento normal y previsible, pero conviene conocerlo antes de sorprenderse porque el tráfico "no se reparte".
- Endpoints y EndpointSlices: quién mantiene la lista
El Service, por sí solo, no sabe nada de pods. Solo declara un selector. Quien hace el trabajo sucio es otro controlador del kube-controller-manager: el controlador de endpoints.
Su bucle de reconciliación, que a estas alturas ya te resulta familiar:
- Lee el
selectordel Service. - Busca los pods del namespace que casan con esas etiquetas.
- De ellos, selecciona los que están listos (
Ready). - Escribe la lista de sus IPs y puertos en un objeto EndpointSlice asociado al Service.
flowchart TD
SVC["Service api-reservas<br/>selector: app=api-reservas, entorno=dev"]
EC["Controlador de endpoints<br/>(kube-controller-manager)"]
ES["EndpointSlice api-reservas-xk4p2<br/>10.244.0.52:3000 · ready<br/>10.244.0.53:3000 · ready"]
KP["kube-proxy en cada nodo<br/>traduce a reglas de reenvío"]
SVC --> EC
EC -->|"observa pods con esas etiquetas"| EC
EC --> ES
ES --> KP
El punto crucial, y la razón de que existan las sondas: solo entran en la lista los pods listos. Un pod que arranca, uno que está terminando o uno cuya readinessProbe falla queda fuera y no recibe tráfico. Ese es el mecanismo exacto que hace posibles los despliegues sin corte de la lección anterior, y también la pieza que todavía nos falta hasta Verificaciones de Salud y Sondas.
Históricamente esta lista vivía en un objeto Endpoints (uno por Service, con todas las direcciones dentro). En clústeres grandes, un Service con miles de pods generaba un objeto enorme que se reenviaba entero a todos los nodos cada vez que cambiaba una sola IP. Por eso desde Kubernetes 1.21 el mecanismo real son los EndpointSlice: fragmentos de hasta 100 direcciones cada uno.
| Aspecto | Endpoints |
EndpointSlice |
|---|---|---|
| Objetos por Service | 1 | Tantos como hagan falta (100 direcciones por rebanada) |
| Escalabilidad | Mala con miles de pods | Diseñado para escalar |
| Estado | Se mantiene por compatibilidad | El mecanismo real desde 1.21 |
| Comando | kubectl get endpoints |
kubectl get endpointslices |
En la práctica seguirás usando kubectl get endpoints para diagnosticar porque su salida es más compacta y legible, y Kubernetes lo mantiene sincronizado. Solo recuerda que por debajo son EndpointSlices.
- Anatomía del manifiesto:
port, targetPort, protocol
port, targetPort, protocolPrimer Service del proyecto, el de api-reservas:
# k8s/base/api-reservas-service.yaml
apiVersion: v1
kind: Service
metadata:
name: api-reservas
namespace: rutas-norte-dev
labels:
app: api-reservas
app.kubernetes.io/part-of: rutas-norte
entorno: dev
spec:
type: ClusterIP
selector:
app: api-reservas
entorno: dev
ports:
- name: http
port: 3000
targetPort: http
protocol: TCPCampo a campo:
apiVersion: v1: el Service pertenece al grupo core, como el Pod. No llevaapps/.metadata.name: api-reservas: importantísimo, porque el nombre del Service es el nombre DNS.http://api-reservas:3000funcionará gracias a esta línea.spec.type: ClusterIP: es el valor por defecto; lo escribimos explícito por claridad. Los demás tipos, en Tipos de Servicios.spec.selector: qué pods hay detrás. Aquí no haymatchLabels: el selector de un Service es un mapa plano de igualdad, más simple que el de un Deployment. Y una advertencia que desarrollaremos en el apartado 9: estas etiquetas deben coincidir con las de los pods, es decir, conspec.template.metadata.labelsdel Deployment, no con las del Deployment en sí.ports[].name: http: obligatorio si hay más de un puerto, recomendable siempre.ports[].port: 3000: el puerto del Service. Es al que se conectan los clientes:api-reservas:3000.ports[].targetPort: http: el puerto del pod al que se reenvía. Aquí usamos el nombre del puerto declarado en el contenedor.ports[].protocol: TCP: por defecto TCP. También admiteUDPySCTP.
port frente a targetPort
Es la confusión número uno con los Servicios. La regla es simple:
portes por donde entra el tráfico al Service.targetPortes por donde sale hacia el pod.
flowchart LR
C["Cliente<br/>pod de tienda-web"] -->|"http://api-reservas:3000"| S["Service api-reservas<br/>port: 3000"]
S -->|"reenvía a targetPort"| P["Pod api-reservas<br/>containerPort: 3000"]
No tienen por qué ser iguales, y a menudo no lo son. Un caso muy habitual en Rutas Norte:
ports:
- name: http
port: 80 # los clientes usan el puerto estandar
targetPort: 3000 # pero la aplicacion Node escucha en 3000Así tienda-web puede llamar a http://api-reservas sin especificar puerto, mientras la aplicación sigue escuchando donde le conviene.
targetPort con nombre
Compara las dos formas de apuntar al pod:
La segunda es claramente superior, y es la que adopta el proyecto. Ventajas:
- Desacopla el Service del contenedor. Si mañana
api-reservaspasa a escuchar en el 8080, basta cambiar elcontainerPorten el Deployment; el Service sigue siendo válido sin tocar una línea. - Permite un Service común a pods heterogéneos. Distintos pods pueden exponer el puerto
httpen números diferentes y el mismo Service los sirve a todos. - Se documenta solo.
targetPort: metricsdice más quetargetPort: 9090.
Requisito para poder usarla: el contenedor debe declarar el puerto con nombre. Nuestro Deployment ya lo hace:
Ojo con una asimetría que confunde: port no admite nombre, solo número. El nombre solo vale para targetPort.
- Por qué
ClusterIP es el tipo por defecto
ClusterIP es el tipo por defectoKubernetes ofrece cuatro tipos de Service. Esta lección se centra en ClusterIP; el resto se tratan en detalle en Tipos de Servicios.
| Tipo | Alcance | Uso típico |
|---|---|---|
ClusterIP (por defecto) |
Solo dentro del clúster | Comunicación entre componentes: la inmensa mayoría de los Servicios |
NodePort |
Puerto en cada nodo | Pruebas y entornos sin balanceador |
LoadBalancer |
Balanceador externo del proveedor cloud | Exposición pública, uno por servicio |
ExternalName |
Alias DNS a un host externo | Apuntar a un servicio de fuera del clúster |
Que ClusterIP sea el defecto no es casualidad: es una decisión de diseño alineada con el principio de mínimo privilegio. Un componente no debería ser accesible desde fuera salvo que alguien lo decida explícitamente.
Aplicado a los seis componentes de Rutas Norte:
| Componente | Tipo de Service | Por qué |
|---|---|---|
tienda-web |
ClusterIP |
Se expondrá al exterior mediante Ingress, no con un Service público |
api-reservas |
ClusterIP |
Igual: el Ingress enrutará api.rutasnorte.example hacia él |
postgres-reservas |
ClusterIP |
Jamás debe ser accesible desde internet: contiene datos personales |
redis-cache |
ClusterIP |
Solo lo consume api-reservas |
worker-notificaciones |
Ninguno | Nadie le habla; él sale a hablar con otros |
informes-ocupacion |
Ninguno | Tarea programada sin tráfico entrante |
Fíjate en que incluso los componentes públicos usan ClusterIP. La exposición al exterior la hará un único Controlador de Ingress que enruta por dominio hacia estos Servicios internos: una sola puerta de entrada, un solo sitio donde terminar el TLS y aplicar reglas.
Y fíjate también en que dos componentes no llevan Service en absoluto. Un Service solo es necesario si alguien tiene que iniciar conexiones hacia el componente. worker-notificaciones consume una cola y habla con PostgreSQL, pero nadie le llama a él.
- Los Servicios de Rutas Norte
Vamos a dar dirección estable a la plataforma. Empezamos por el que ya tenemos escrito:
service/api-reservas created
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
api-reservas ClusterIP 10.96.184.22 <none> 3000/TCP 4sAhí está la IP virtual: 10.96.184.22. Nunca cambiará mientras exista el Service.
tienda-web
# k8s/base/tienda-web-service.yaml
apiVersion: v1
kind: Service
metadata:
name: tienda-web
namespace: rutas-norte-dev
labels:
app: tienda-web
app.kubernetes.io/part-of: rutas-norte
entorno: dev
spec:
type: ClusterIP
selector:
app: tienda-web
entorno: dev
ports:
- name: http
port: 80
targetPort: http
protocol: TCPpostgres-reservas
Aún no hemos desplegado PostgreSQL —necesita almacenamiento persistente y llegará en el módulo 5—, pero podemos dejar preparado un Deployment provisional de una réplica para practicar la conectividad. El Service es lo que nos interesa:
# k8s/base/postgres-reservas-service.yaml
apiVersion: v1
kind: Service
metadata:
name: postgres-reservas
namespace: rutas-norte-dev
labels:
app: postgres-reservas
app.kubernetes.io/part-of: rutas-norte
entorno: dev
spec:
type: ClusterIP
selector:
app: postgres-reservas
entorno: dev
ports:
- name: postgres
port: 5432
targetPort: postgres
protocol: TCPY el Deployment provisional, con una contraseña que por ahora va en claro. Es una mala práctica deliberada y temporal: se corrige en Secrets.
# k8s/base/postgres-reservas-deployment.yaml (PROVISIONAL: sin persistencia)
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres-reservas
namespace: rutas-norte-dev
labels:
app: postgres-reservas
app.kubernetes.io/part-of: rutas-norte
entorno: dev
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: postgres-reservas
entorno: dev
template:
metadata:
labels:
app: postgres-reservas
app.kubernetes.io/part-of: rutas-norte
entorno: dev
spec:
containers:
- name: postgres
image: postgres:16
ports:
- name: postgres
containerPort: 5432
env:
- name: POSTGRES_DB
value: "reservas"
- name: POSTGRES_USER
value: "rutasnorte"
- name: POSTGRES_PASSWORD
value: "cambiame-en-modulo-3" # provisional: ver leccion 03-02
resources:
requests:
cpu: "100m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"redis-cache
# k8s/base/redis-cache-service.yaml
apiVersion: v1
kind: Service
metadata:
name: redis-cache
namespace: rutas-norte-dev
labels:
app: redis-cache
app.kubernetes.io/part-of: rutas-norte
entorno: dev
spec:
type: ClusterIP
selector:
app: redis-cache
entorno: dev
ports:
- name: redis
port: 6379
targetPort: redis
protocol: TCPAplícalo todo y contempla el resultado:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
api-reservas ClusterIP 10.96.184.22 <none> 3000/TCP 3m
postgres-reservas ClusterIP 10.96.201.140 <none> 5432/TCP 8s
redis-cache ClusterIP 10.96.77.33 <none> 6379/TCP 8s
tienda-web ClusterIP 10.96.12.209 <none> 80/TCP 8sCuatro direcciones estables para la plataforma Rutas Norte. A partir de ahora, ningún componente vuelve a conocer una IP de pod.
- Verificación desde dentro del clúster
Comprobar los endpoints
Antes de probar tráfico, verifica siempre que el Service ha encontrado pods:
NAME ENDPOINTS AGE
api-reservas 10.244.0.52:3000,10.244.0.53:3000 5m
postgres-reservas 10.244.0.58:5432 2m
redis-cache 10.244.0.55:6379 2m
tienda-web 10.244.0.44:80,10.244.0.45:80,10.244.0.46:80 2mEsa columna ENDPOINTS es el dato más valioso del diagnóstico de Servicios: son las IPs reales a las que se reenviará el tráfico. Tres pods de tienda-web, dos de api-reservas, uno de cada base de datos. Exactamente lo esperado.
La vista moderna, en EndpointSlices:
Probar el tráfico con un pod efímero
Usamos la técnica de la lección de Pods. Nota que ahora no necesitamos averiguar ninguna IP:
kubectl run test --rm -it --image=curlimages/curl:8.8.0 --restart=Never -- \
curl -s http://api-reservas:3000/disponibilidad{"servicio":"api-reservas","version":"2.4.0","pod":"api-reservas-7f1d3e942-b8mrs"}
pod "test" deletedHemos llamado a la API por su nombre. Ese http://api-reservas:3000 es exactamente la cadena de conexión que irá en la configuración de tienda-web, y no cambiará jamás.
Demostrar el balanceo de carga
Aquí es donde nos sirve haber incluido el nombre del pod en la respuesta de la API:
kubectl run test --rm -it --image=curlimages/curl:8.8.0 --restart=Never -- \
sh -c 'for i in $(seq 1 8); do curl -s http://api-reservas:3000/ | grep -o "api-reservas-[a-z0-9-]*"; done'api-reservas-7f1d3e942-b8mrs
api-reservas-7f1d3e942-n4jvt
api-reservas-7f1d3e942-n4jvt
api-reservas-7f1d3e942-b8mrs
api-reservas-7f1d3e942-b8mrs
api-reservas-7f1d3e942-n4jvt
api-reservas-7f1d3e942-b8mrs
api-reservas-7f1d3e942-n4jvtOcho peticiones repartidas entre las dos réplicas. El reparto es aleatorio, no estrictamente alterno: no esperes una distribución perfecta en pocas peticiones.
Comprobar la persistencia de la dirección
La prueba definitiva. Escala, redespliega y vuelve a llamar:
kubectl scale deployment api-reservas --replicas=4
kubectl rollout restart deployment/api-reservas
kubectl rollout status deployment/api-reservas
kubectl get svc api-reservas
kubectl get endpoints api-reservasNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
api-reservas ClusterIP 10.96.184.22 <none> 3000/TCP 12m
NAME ENDPOINTS
api-reservas 10.244.0.64:3000,10.244.0.65:3000,10.244.0.66:3000,10.244.0.67:3000La ClusterIP sigue siendo 10.96.184.22. Los cuatro endpoints son IPs completamente nuevas. Ese es el contrato del Service en una línea: la dirección de fuera no cambia nunca, la lista de dentro se actualiza sola.
Probar las bases de datos
kubectl run test-redis --rm -it --image=redis:7.2-alpine --restart=Never -- \
redis-cli -h redis-cache -p 6379 pingkubectl run test-pg --rm -it --image=postgres:16 --restart=Never --env="PGPASSWORD=cambiame-en-modulo-3" -- \
psql -h postgres-reservas -U rutasnorte -d reservas -c "SELECT 'conexion correcta' AS estado;"Los cuatro componentes son alcanzables por nombre desde cualquier punto del clúster. La plataforma ya tiene sistema circulatorio.
Vuelve a 2 réplicas antes de seguir:
- El nombre DNS y los servicios headless
El nombre completo
Cada Service obtiene automáticamente un registro DNS con esta forma:
Para nuestra API: api-reservas.rutas-norte-dev.svc.cluster.local. Y como el DNS del clúster configura sufijos de búsqueda en cada pod, desde rutas-norte-dev puedes usar cualquiera de estas formas:
| Forma | Funciona desde | Cuándo usarla |
|---|---|---|
api-reservas |
El mismo namespace | Lo habitual: configuración de componentes de Rutas Norte |
api-reservas.rutas-norte-dev |
Cualquier namespace | Cuando hay que cruzar entornos |
api-reservas.rutas-norte-dev.svc.cluster.local |
Cualquier namespace | Nombre completo, sin ambigüedad |
Compruébalo:
Server: 10.96.0.10
Address: 10.96.0.10:53
Name: api-reservas.rutas-norte-dev.svc.cluster.local
Address: 10.96.184.22
pod "test-dns" deletedEl nombre resuelve a la ClusterIP, no a las IPs de los pods. El funcionamiento interno de CoreDNS, los sufijos de búsqueda, los registros SRV y la resolución entre namespaces se estudian a fondo en DNS Interno y Descubrimiento de Servicios.
Servicios headless
Existe una variante que conviene conocer aunque no la usemos todavía: un Service con clusterIP: None, llamado headless.
Su comportamiento es distinto: no tiene IP virtual y no balancea nada. En su lugar, el DNS devuelve directamente las IPs de todos los pods que casan con el selector.
| Aspecto | Service normal | Service headless |
|---|---|---|
ClusterIP |
Sí, una IP virtual | None |
| Qué devuelve el DNS | La ClusterIP |
Todas las IPs de pod |
| Balanceo | Sí | No: elige el cliente |
| Uso típico | Cargas sin estado | Cargas con estado, clientes que necesitan hablar con una réplica concreta |
Para qué sirve: cuando un cliente necesita dirigirse a una réplica concreta en lugar de a "cualquiera". Es el caso de un clúster de PostgreSQL donde hay que escribir en el primario y leer de las réplicas, o de Kafka, o de cualquier sistema con quórum. Por eso los headless van casi siempre de la mano de los StatefulSets: la combinación da a cada réplica un nombre DNS propio y estable como postgres-reservas-0.postgres-reservas.rutas-norte-dev.svc.cluster.local.
Cuando en el módulo 6 convirtamos postgres-reservas en un StatefulSet, su Service pasará a ser headless. Los detalles, en DNS Interno y StatefulSets.
- El fallo más común: el selector que no casa
Si tuvieras que memorizar una sola cosa de esta lección, que sea esta. La avería número uno con Servicios en Kubernetes es un selector que no coincide con las etiquetas de los pods. Y es especialmente traicionera porque no produce ningún error: el Service se crea sin protestar, obtiene su ClusterIP, aparece en kubectl get svc con aspecto perfectamente sano... y no reenvía tráfico a ninguna parte.
Provocar la avería
# /tmp/servicio-roto.yaml
apiVersion: v1
kind: Service
metadata:
name: api-reservas-roto
namespace: rutas-norte-dev
spec:
type: ClusterIP
selector:
app: api-reserva # falta la "s" final
entorno: dev
ports:
- name: http
port: 3000
targetPort: 3000service/api-reservas-roto created
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
api-reservas-roto ClusterIP 10.96.155.71 <none> 3000/TCP 3sAspecto impecable. Pero:
kubectl run test --rm -it --image=curlimages/curl:8.8.0 --restart=Never -- \
curl -s --max-time 5 http://api-reservas-roto:3000/El procedimiento de diagnóstico
Cuatro pasos, siempre en este orden.
Paso 1: ¿tiene endpoints? Es la pregunta que resuelve el 80 % de los casos.
<none>. El Service no ha encontrado ni un solo pod. Confirmado: el problema está en el selector o en las etiquetas, no en la red ni en la aplicación.
Paso 2: ¿qué selector tiene el Service?
Paso 3: ¿qué etiquetas tienen los pods?
NAME READY STATUS LABELS
api-reservas-7f1d3e942-b8mrs 1/1 Running app=api-reservas,app.kubernetes.io/part-of=rutas-norte,entorno=dev,pod-template-hash=7f1d3e942Paso 4: comparar. app=api-reserva frente a app=api-reservas. Ahí está la ese que falta.
La prueba definitiva es usar el selector del Service como filtro de kubectl: si no devuelve pods, el Service tampoco los encontrará.
El catálogo completo de causas
Cuando ENDPOINTS está vacío o incompleto, la causa está siempre en esta tabla:
| Causa | Cómo se detecta | Solución |
|---|---|---|
| Etiqueta mal escrita en el selector | kubectl get pods -l <selector> no devuelve nada |
Corregir el selector |
| El selector apunta a las etiquetas del Deployment, no del pod | Las etiquetas de metadata y de template.metadata difieren |
El selector debe casar con spec.template.metadata.labels |
| Service y pods en namespaces distintos | kubectl get pods -n <ns> -l <selector> en el namespace del Service |
Un Service solo selecciona pods de su propio namespace |
Los pods existen pero no están Ready |
kubectl get pods muestra 0/1 |
Arreglar el pod: mirar describe y logs |
targetPort con un nombre que el contenedor no declara |
Hay endpoints, pero con puerto incorrecto o sin puerto | Declarar ports[].name en el contenedor |
| El contenedor no escucha realmente en ese puerto | Hay endpoints, pero la conexión se rechaza | kubectl exec y comprobar el puerto real |
De todas ellas, la segunda es la más sutil y la que más tiempo hace perder. Mira este Deployment:
metadata:
name: api-reservas
labels:
app: api-reservas-deploy # etiqueta del DEPLOYMENT
spec:
template:
metadata:
labels:
app: api-reservas # etiqueta de los PODSUn Service con selector: {app: api-reservas-deploy} no encontrará nada, porque esa etiqueta la lleva el Deployment, que no es un pod. El Service selecciona pods, siempre. La convención de Rutas Norte —usar las mismas tres etiquetas en el Deployment y en el template— evita este error por construcción.
Cuando sí hay endpoints y aun así falla
Si ENDPOINTS tiene direcciones pero la conexión no funciona, el problema está más abajo:
# ¿Responde el pod directamente, saltandose el Service?
kubectl run test --rm -it --image=curlimages/curl:8.8.0 --restart=Never -- \
curl -s --max-time 5 http://10.244.0.52:3000/
# ¿Escucha el proceso en el puerto que dice?
kubectl exec deploy/api-reservas -- netstat -tlnp 2>/dev/null || \
kubectl exec deploy/api-reservas -- wget -qO- http://localhost:3000/Si el pod responde por IP directa pero no por el Service, mira el targetPort. Si no responde ni por IP directa, el problema es la aplicación, no Kubernetes.
Y un último aviso que ahorra horas: si tu aplicación escucha en 127.0.0.1 en lugar de en 0.0.0.0, solo se responde a sí misma. El Service tendrá endpoints correctos y aun así todas las conexiones fallarán. Es un fallo de configuración de la aplicación que parece un fallo de Kubernetes.
Errores Comunes y Consejos
- Selector que no casa con las etiquetas de los pods. El fallo número uno. El Service se crea sin error y no sirve nada. Diagnóstico:
kubectl get endpoints. - Apuntar el selector a las etiquetas del Deployment. Los Services seleccionan pods: las etiquetas relevantes son las de
spec.template.metadata.labels. - Confundir
portcontargetPort.portes donde escucha el Service;targetPort, donde escucha el contenedor. - Usar un nombre en
port. SolotargetPortadmite nombres.portes siempre numérico. - Esperar que un Service llegue a pods de otro namespace. No lo hace: solo selecciona en el suyo. Para cruzar, se usa el nombre DNS completo, como veremos en Namespaces.
- Que la aplicación escuche en
127.0.0.1. Debe escuchar en0.0.0.0para ser alcanzable desde fuera del contenedor. - Crear un Service para
worker-notificaciones. No recibe conexiones entrantes; un Service sin clientes es ruido y superficie de ataque innecesaria. - Esperar un reparto perfectamente alterno. El balanceo es aleatorio por conexión, no por petición. Con
keep-alive, todas las peticiones de una conexión van al mismo pod. - Hacer
pinga unaClusterIP. No responde: es una regla de reenvío, no una interfaz. Usacurloncal puerto del Service. - Consejo: el diagnóstico de Servicios empieza siempre por
kubectl get endpoints <nombre>. Vacío significa problema de selector o de etiquetas; con direcciones, problema de puertos o de aplicación. - Consejo: usa siempre
targetPortpor nombre. Desacopla el Service del contenedor y te ahorra un cambio cada vez que la aplicación mueva su puerto. - Consejo: para una prueba rápida sin escribir manifiestos,
kubectl expose deployment api-reservas --port=3000 --target-port=http --dry-run=client -o yamlgenera un Service correcto que puedes revisar y guardar.
Ejercicios
Ejercicio 1: Crear y verificar el Service de tienda-web
- Aplica el Service de
tienda-webde esta lección y comprueba que tiene 3 endpoints. - Desde un pod efímero, haz 6 peticiones a
http://tienda-web/y verifica que responde nginx. - Anota la
ClusterIP, escala el Deployment a 5 réplicas, redespliégalo conrollout restarty demuestra con dos comandos que laClusterIPno ha cambiado y que los endpoints sí. - Explica por qué el Service usa
port: 80ytargetPort: httpen lugar detargetPort: 80.
Ejercicio 2: Diagnosticar tres Servicios rotos
Un compañero ha aplicado estos tres Servicios en rutas-norte-dev y ninguno funciona. Para cada uno, identifica la causa exacta, indica el comando que la revela y escribe la corrección.
# Servicio A
apiVersion: v1
kind: Service
metadata:
name: redis-cache-a
namespace: rutas-norte-dev
spec:
selector:
app: redis
entorno: dev
ports:
- port: 6379
targetPort: redis# Servicio B
apiVersion: v1
kind: Service
metadata:
name: api-reservas-b
namespace: rutas-norte-dev
spec:
selector:
app: api-reservas
entorno: dev
ports:
- port: 3000
targetPort: api # el contenedor declara el puerto como "http"# Servicio C
apiVersion: v1
kind: Service
metadata:
name: tienda-web-c
namespace: default # ojo
spec:
selector:
app: tienda-web
entorno: dev
ports:
- port: 80
targetPort: httpEjercicio 3: Conectar la plataforma de extremo a extremo
Objetivo: que api-reservas hable con redis-cache y con postgres-reservas solo por nombre, sin ninguna IP.
- Comprueba que los tres Servicios tienen endpoints.
- Desde un pod efímero de
redis:7.2-alpine, escribe en la caché la disponibilidad de una expedición: la claveplazas:BIL-SAN:2026-08-14con valor37. Recupérala después desde otro pod efímero distinto. - Desde un pod efímero de
postgres:16, crea la tablareservascon columnasid,clienteyexpedicion, inserta dos reservas ficticias y consúltalas. - Escribe el fragmento de variables de entorno que llevaría el Deployment de
api-reservaspara conectarse a ambos por nombre, y explica por qué esa configuración es idéntica enrutas-norte-dev,rutas-norte-preyrutas-norte-pro.
Soluciones
Solución 1
service/tienda-web created
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/tienda-web ClusterIP 10.96.12.209 <none> 80/TCP 5s
NAME ENDPOINTS AGE
endpoints/tienda-web 10.244.0.44:80,10.244.0.45:80,10.244.0.46:80 5skubectl run test --rm -it --image=curlimages/curl:8.8.0 --restart=Never -- \
sh -c 'for i in $(seq 1 6); do curl -s -o /dev/null -w "%{http_code} " http://tienda-web/; done; echo'kubectl scale deployment tienda-web --replicas=5
kubectl rollout restart deployment/tienda-web
kubectl rollout status deployment/tienda-web
kubectl get svc tienda-web
kubectl get endpoints tienda-webNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
tienda-web ClusterIP 10.96.12.209 <none> 80/TCP 4m
NAME ENDPOINTS
tienda-web 10.244.0.71:80,10.244.0.72:80,10.244.0.73:80,10.244.0.74:80,10.244.0.75:80La ClusterIP sigue siendo 10.96.12.209; los cinco endpoints son IPs nuevas.
targetPort: httpreferencia el nombre del puerto declarado en el contenedor (ports[].name: http) en lugar de su número. Así, si mañanatienda-webcambia su nginx para escuchar en el 8080, basta con actualizar elcontainerPorten el Deployment: el Service sigue siendo correcto sin tocarlo. Es un desacoplamiento entre la definición del servicio y el detalle de implementación del contenedor, y además hace el manifiesto autoexplicativo.
Solución 2
| Servicio | Causa | Comando que la revela | Corrección |
|---|---|---|---|
| A | El selector usa app: redis, pero los pods llevan app: redis-cache |
kubectl get endpoints redis-cache-a → <none>, y kubectl get pods -l app=redis no devuelve nada |
app: redis-cache |
| B | targetPort: api no existe: el contenedor declara el puerto como http |
Hay endpoints, pero la conexión falla; kubectl describe svc api-reservas-b muestra TargetPort: api/TCP y kubectl get pod -o jsonpath='{.spec.containers[0].ports}' muestra name: http |
targetPort: http |
| C | El Service está en el namespace default y los pods en rutas-norte-dev. Un Service solo selecciona pods de su propio namespace |
kubectl get endpoints tienda-web-c -n default → <none>, mientras que kubectl get pods -n default -l app=tienda-web no devuelve nada |
namespace: rutas-norte-dev |
# Comprobacion del B: hay endpoints pero el puerto es un nombre inexistente
kubectl describe svc api-reservas-b | grep -E "TargetPort|Endpoints"# Comprobacion del C
kubectl get endpoints tienda-web-c -n default
kubectl get pods -n default -l app=tienda-webSolución 3
# 1. Los tres Servicios con endpoints
kubectl get endpoints api-reservas redis-cache postgres-reservasNAME ENDPOINTS AGE
api-reservas 10.244.0.52:3000,10.244.0.53:3000 25m
redis-cache 10.244.0.55:6379 20m
postgres-reservas 10.244.0.58:5432 20m# 2. Escribir en la cache desde un pod y leer desde otro
kubectl run redis-esc --rm -it --image=redis:7.2-alpine --restart=Never -- \
redis-cli -h redis-cache SET plazas:BIL-SAN:2026-08-14 37kubectl run redis-lec --rm -it --image=redis:7.2-alpine --restart=Never -- \
redis-cli -h redis-cache GET plazas:BIL-SAN:2026-08-14Dos pods efímeros distintos, creados y destruidos, han compartido estado a través de redis-cache sin conocer ninguna IP.
# 3. PostgreSQL
kubectl run pg-cli --rm -it --image=postgres:16 --restart=Never \
--env="PGPASSWORD=cambiame-en-modulo-3" -- \
psql -h postgres-reservas -U rutasnorte -d reservas -c "
CREATE TABLE IF NOT EXISTS reservas (
id SERIAL PRIMARY KEY,
cliente TEXT NOT NULL,
expedicion TEXT NOT NULL
);
INSERT INTO reservas (cliente, expedicion) VALUES
('Marta Ibarra', 'BIL-SAN 2026-08-14 08:30'),
('Xabier Aroca', 'BIL-SAN 2026-08-14 08:30');
SELECT * FROM reservas;" id | cliente | expedicion
----+--------------+--------------------------
1 | Marta Ibarra | BIL-SAN 2026-08-14 08:30
2 | Xabier Aroca | BIL-SAN 2026-08-14 08:30
(2 rows)
pod "pg-cli" deleted- Configuración de
api-reservaspor nombre:
env:
- name: REDIS_HOST
value: "redis-cache"
- name: REDIS_PORT
value: "6379"
- name: DATABASE_HOST
value: "postgres-reservas"
- name: DATABASE_PORT
value: "5432"
- name: DATABASE_NAME
value: "reservas"
- name: DATABASE_USER
value: "rutasnorte"
# DATABASE_PASSWORD llegara desde un Secret en la leccion 03-02Esa configuración es idéntica en los tres entornos porque el nombre corto redis-cache se resuelve dentro del namespace en el que corre el pod. Un pod de api-reservas en rutas-norte-pro resolverá redis-cache como redis-cache.rutas-norte-pro.svc.cluster.local, mientras que el mismo pod en rutas-norte-dev lo resolverá como redis-cache.rutas-norte-dev.svc.cluster.local. El mismo manifiesto sirve para los tres entornos sin cambiar una sola línea, y cada uno habla con su propia base de datos y su propia caché. Es exactamente el mecanismo que explota la lección siguiente, Namespaces.
Conclusión
La plataforma Rutas Norte ya tiene sistema circulatorio. Has visto en primera persona por qué las IPs de pod son inservibles como punto de conexión —cambian en cada despliegue, cada autorreparación y cada escalado— y cómo el Service resuelve el problema con una ClusterIP virtual que no cambia jamás, un nombre DNS y balanceo automático entre las réplicas sanas. Sabes que esa IP no es una máquina sino una regla de reenvío, y que la lista de destinos reales la mantiene al día el controlador de endpoints, escribiendo en EndpointSlices únicamente las direcciones de los pods listos.
Lees un manifiesto de Service sin dudar: port es por donde entra el tráfico y targetPort por donde sale hacia el contenedor, preferiblemente por nombre para desacoplar el Service de la implementación. Entiendes por qué ClusterIP es el tipo por defecto y por qué en Rutas Norte lo son todos los Servicios, incluidos los de los componentes públicos, que se expondrán mediante una única puerta de entrada. Y has dado dirección estable a cuatro componentes: tienda-web, api-reservas, postgres-reservas y redis-cache, mientras que worker-notificaciones e informes-ocupacion no llevan Service porque nadie inicia conexiones hacia ellos. Lo has verificado sin trampas: pods efímeros llamando por nombre, ocho peticiones repartidas entre dos réplicas, y la prueba definitiva de escalar y redesplegar viendo cómo la ClusterIP aguanta mientras todos los endpoints se renuevan.
Por último, te llevas el procedimiento de diagnóstico más rentable de todo Kubernetes: cuando un Service no responde, lo primero es kubectl get endpoints. Vacío significa que el selector no casa con las etiquetas de los pods, y de ahí a comparar describe svc con get pods --show-labels hay un paso. Con direcciones, el problema está en los puertos o en la aplicación.
Has notado que en toda la lección hemos escrito rutas-norte-dev una y otra vez, y que la configuración de api-reservas no menciona el entorno por ninguna parte porque el DNS lo resuelve solo. Eso no es casualidad: es la propiedad que hace posible desplegar la misma plataforma tres veces sin tocar los manifiestos. En la lección siguiente, Namespaces, montaremos los tres entornos de Rutas Norte —dev, pre y pro—, desplegaremos el mismo manifiesto en varios de ellos, veremos qué aísla realmente un namespace y, lo más importante, qué no aísla en absoluto.
Curso de Kubernetes
Módulo 1: Introducción a Kubernetes
- ¿Qué es Kubernetes?
- Arquitectura de Kubernetes
- Conceptos y Terminología Clave
- Configuración de un Clúster de Kubernetes
- La CLI de Kubernetes: kubectl
- Objetos, Manifiestos YAML y el Modelo Declarativo
- El Proyecto del Curso: la Plataforma Rutas Norte
Módulo 2: Componentes Principales de Kubernetes
- Pods
- ReplicaSets
- Deployments
- Actualizaciones, Rollbacks y Estrategias de Despliegue
- Servicios
- Namespaces
- Etiquetas, Selectores y Anotaciones
Módulo 3: Gestión de Configuración y Secretos
- ConfigMaps
- Secrets
- Variables de Entorno
- Cuotas y Límites de Recursos
- LimitRanges y Clases de Calidad de Servicio (QoS)
- ServiceAccounts y Acceso a la API desde los Pods
Módulo 4: Redes en Kubernetes
- Redes de Clúster
- Tipos de Servicios
- DNS Interno y Descubrimiento de Servicios
- Controladores de Ingress
- TLS y Gestión de Certificados con cert-manager
- Políticas de Red
Módulo 5: Almacenamiento en Kubernetes
- Volúmenes
- Volúmenes Persistentes
- Reclamaciones de Volúmenes Persistentes
- Clases de Almacenamiento
- Aprovisionamiento Dinámico, Expansión y Snapshots
- Copias de Seguridad y Restauración de Datos
Módulo 6: Conceptos Avanzados de Kubernetes
- StatefulSets
- DaemonSets
- Trabajos y CronJobs
- Init Containers, Sidecars y Patrones Multi-Contenedor
- Planificación: Afinidad, Taints y Tolerations
- Definiciones de Recursos Personalizados (CRDs)
- Operadores y el Patrón Controlador
Módulo 7: Monitoreo y Registro
- Verificaciones de Salud y Sondas
- Servidor de Métricas y kubectl top
- Monitoreo con Prometheus
- Visualización y Alertas con Grafana y Alertmanager
- Registro Centralizado con Elasticsearch, Fluentd y Kibana (EFK)
- Depuración de Aplicaciones y Eventos del Clúster
Módulo 8: Seguridad en Kubernetes
- Control de Acceso Basado en Roles (RBAC)
- Contextos de Seguridad y Endurecimiento del Contenedor
- Políticas de Seguridad de Pods y Pod Security Standards
- Seguridad de Red
- Seguridad de Imágenes
- Auditoría, Escaneo y Gestión de Vulnerabilidades
Módulo 9: Escalado y Rendimiento
- Autoescalado Horizontal de Pods
- Autoescalado Vertical de Pods
- Autoescalado de Clúster
- Escalado por Eventos y Métricas Personalizadas con KEDA
- Alta Disponibilidad: PodDisruptionBudgets y Topología
- Ajuste de Rendimiento
Módulo 10: Ecosistema y Herramientas de Kubernetes
- Minikube y Entornos Locales con kind
- Kubeadm
- Helm
- Kustomize
- GitOps con Argo CD y Flux
- Kubernetes Gestionado: EKS, AKS y GKE
Módulo 11: Estudios de Caso y Aplicaciones del Mundo Real
- Despliegue de una Aplicación Web
- Ejecución de Aplicaciones con Estado
- CI/CD con Kubernetes
- Estrategias de Despliegue: Blue-Green y Canary
- Gestión Multi-Clúster
- Operación en Producción: Incidencias, Runbooks y Costes
