Proyecto projet11-kubernetes-services · Documento de referencia en profundidad.
Este documento va mucho más lejos que la corrección: detalla todos los tipos de Services, las nociones internas (kube-proxy, Endpoints, EndpointSlices, DNS), los campos YAML importantes, las políticas de tráfico, la session affinity, el multi-puerto, las trampas clásicas y las buenas prácticas.
Un Pod es efímero: puede recrearse en cualquier momento, con una IP nueva. Por tanto no se puede apoyarse en la IP de un Pod para comunicar.
Un Service es una abstracción estable que:
Idea maestra: el Service no «contiene» los Pods. Los encuentra en continuo gracias al selector de labels, y mantiene la lista de sus direcciones en los Endpoints.
apiVersion: v1
kind: Service
metadata:
name: mon-service
labels:
app: demo
annotations: {} # metadatos (a menudo usados por los LoadBalancer cloud)
spec:
type: ClusterIP # ClusterIP | NodePort | LoadBalancer | ExternalName
selector: # qué Pods apunta este Service (por labels)
app: demo
ports:
- name: http # nombre del puerto (útil si hay varios puertos)
protocol: TCP # TCP (por defecto) | UDP | SCTP
port: 80 # puerto del Service (lo que ven los clientes)
targetPort: 5000 # puerto del contenedor (o nombre de puerto del contenedor)
nodePort: 30080 # (NodePort/LoadBalancer) puerto abierto en el nodo
clusterIP: 10.96.0.10 # (opcional) IP fija; "None" = headless
sessionAffinity: None # None | ClientIP
externalTrafficPolicy: Cluster # Cluster | Local (NodePort/LoadBalancer)
internalTrafficPolicy: Cluster # Cluster | Local
ipFamilyPolicy: SingleStack # SingleStack | PreferDualStack | RequireDualStack
externalIPs: [] # IP externas enrutadas hacia este Service (avanzado)Cada campo se detalla más abajo. Se puede crear un Service mínimo en 8 líneas; todos los demás campos tienen valores por defecto razonables.
El tipo por defecto. Asigna una IP virtual interna (en el rango Service CIDR, p. ej. 10.96.0.0/12), alcanzable solo desde el interior del clúster.
apiVersion: v1
kind: Service
metadata:
name: demo-clusterip
spec:
type: ClusterIP
selector:
app: demo-back
ports:
- port: 80
targetPort: 5000Características:
EXTERNAL-IP).http://demo-clusterip (ver §13).Cuándo usarlo: para todo lo que se queda en el clúster. Es el tipo más habitual.
Hace todo lo que hace ClusterIP (obtiene una IP interna), más: abre un puerto estático en cada nodo del clúster (rango por defecto 30000–32767).
apiVersion: v1
kind: Service
metadata:
name: demo-nodeport
spec:
type: NodePort
selector:
app: demo-back
ports:
- port: 80 # puerto del Service (interno)
targetPort: 5000 # puerto del contenedor
nodePort: 30082 # puerto abierto en CADA nodoAcceso: http://<ip-de-n-importe-quel-noeud>:30082 (con Docker Desktop: http://localhost:30082).
Puntos importantes:
nodePort, Kubernetes elige uno en el rango.Hace todo lo que hace NodePort, más: pide a la infraestructura (la nube) que provisione un equilibrador de carga externo con una IP pública.
apiVersion: v1
kind: Service
metadata:
name: demo-lb
spec:
type: LoadBalancer
selector:
app: demo-back
ports:
- port: 8090
targetPort: 5000Según el entorno:
| Entorno | Comportamiento |
|---|---|
| AWS / GCP / Azure | Crea un LB gestionado real (ELB/NLB, GCP LB…) y rellena EXTERNAL-IP |
| Docker Desktop | EXTERNAL-IP = localhost → http://localhost:8090 |
| minikube | minikube tunnel proporciona la IP externa |
| kind / bare-metal | Se queda en <pending> sin un controlador como MetalLB |
Cadena completa: LoadBalancer → NodePort → ClusterIP → Endpoints → Pods.
Annotations (específicas del proveedor) pilotan el LB, p. ej. en AWS:
metadata:
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
service.beta.kubernetes.io/aws-load-balancer-internal: "true"Un LoadBalancer por service = caro en la nube. En producción, se prefiere a menudo un solo punto de entrada (Ingress/Gateway) delante de varios services (ver §17).
Caso particular: ningún selector, ningún Pod, ninguna IP. Crea simplemente un alias DNS (registro CNAME) hacia un nombre externo.
apiVersion: v1
kind: Service
metadata:
name: base-externe
spec:
type: ExternalName
externalName: db.exemple.com # los Pods que llaman a "base-externe" se redirigen aquíUso: hacer que un nombre interno estable (base-externe) apunte a un service fuera del clúster (una base gestionada, una API de terceros). Si la dirección cambia, se modifica un solo sitio.
Límite: es DNS puro, sin reparto de carga ni control de puerto. No sirve si el service externo espera un
HostHTTP particular.
Al poner clusterIP: None, se obtiene un Service sin IP virtual. El DNS devuelve entonces directamente las IP de todos los Pods (una lista de registros A), en vez de una IP única.
apiVersion: v1
kind: Service
metadata:
name: demo-headless
spec:
clusterIP: None # <-- headless
selector:
app: demo-back
ports:
- port: 80
targetPort: 5000Para qué sirve:
pod-0.demo-headless, pod-1.demo-headless…), útil para las bases de datos replicadas (Cassandra, Kafka, etc.).| ClusterIP normal | Headless (clusterIP: None) | |
|---|---|---|
| IP virtual | Sí (una sola) | No |
| Respuesta DNS | 1 IP (la del Service) | N IP (las de los Pods) |
| Reparto | Por kube-proxy | A cargo del cliente |
| Caso de uso | Web/API sin estado | Bases replicadas, StatefulSet |
Un Service puede no tener selector. En ese caso, Kubernetes no rellena los Endpoints solo: tú los defines a mano. Práctico para exponer un recurso externo bajo una IP interna estable.
apiVersion: v1
kind: Service
metadata:
name: api-legacy
spec:
ports:
- port: 80
targetPort: 8080
---
apiVersion: v1
kind: Endpoints # (o EndpointSlice, más moderno)
metadata:
name: api-legacy # MISMO nombre que el Service
subsets:
- addresses:
- ip: 192.168.1.50 # servidor externo
ports:
- port: 8080Diferencia con ExternalName: aquí se enruta por IP (con load balancing posible sobre varias IP), no por CNAME DNS.
Es la fuente de confusión. Tres puertos distintos, tres papeles:
| Campo | Dónde | Significado |
|---|---|---|
port | En el Service | El puerto que usan los clientes para alcanzar el Service |
targetPort | En el contenedor | El puerto donde escucha de verdad la aplicación en el Pod |
nodePort | En el nodo | (NodePort/LoadBalancer) el puerto abierto en la máquina |
Ejemplo leído en voz alta: «los clientes pegan en el puerto 80 del Service, que transmite hacia el puerto 5000 del contenedor; en NodePort, se entra también por el puerto 30082 de la máquina».
targetPortpuede referenciar un nombre de puerto definido en el contenedor (ver §10), lo que evita codificar el número a pelo.
Un Service puede exponer varios puertos (p. ej. HTTP + métricas). En ese caso, cada entrada debe tener un name.
spec:
selector:
app: demo
ports:
- name: http
port: 80
targetPort: web # referencia un puerto NOMBRADO del contenedor
- name: metrics
port: 9090
targetPort: 9090En el lado del contenedor, se nombran los puertos:
containers:
- name: app
ports:
- name: web # <-- reutilizado por targetPort: web
containerPort: 5000
- name: metrics
containerPort: 9090Ventaja de los puertos nombrados: si cambia el puerto del contenedor, no hay que modificar nada en el Service.
El Service es un objeto abstracto: no es un proceso que reciba el tráfico. La magia la hace kube-proxy, un componente presente en cada nodo, que programa las reglas de red del núcleo para redirigir «IP:puerto del Service» hacia «IP:puerto de un Pod».
Modos de kube-proxy:
| Modo | Principio | Notas |
|---|---|---|
| iptables (por defecto) | Reglas iptables, selección aleatoria de un Pod | Simple, robusto, muy extendido |
| IPVS | Tabla hash del núcleo, algoritmos reales de LB (rr, lc, sh…) | Más performante en clústeres grandes |
| nftables | Sucesor de iptables | Más reciente |
Consecuencias prácticas:
El enlace Service ↔ Pods se materializa con objetos:
IP:port de los Pods listos.kubectl get endpoints demo-clusterip
kubectl get endpointslices -l kubernetes.io/service-name=demo-clusterip¿Quién actualiza la lista? El endpoint controller: en cuanto un Pod pasa a Ready (readinessProbe OK) y coincide con el selector, su IP entra; si se cae, sale.
Un Pod no Ready se retira de los Endpoints → no recibe tráfico. Por eso la readinessProbe es esencial: controla quién está «dentro» del Service. (Excepción:
publishNotReadyAddresses: truepublica también los Pods no listos — uso headless específico.)
Kubernetes hace correr CoreDNS. Cada Service recibe un nombre DNS determinista:
<service> # mismo namespace
<service>.<namespace> # otro namespace
<service>.<namespace>.svc.cluster.local # FQDN completoEjemplo desde un Pod:
curl http://demo-clusterip # mismo namespace
curl http://demo-clusterip.default # explícito
curl http://demo-clusterip.default.svc.cluster.localRegistros producidos:
_http._tcp.demo-clusterip….Históricamente, Kubernetes también inyectaba variables de entorno (
DEMO_CLUSTERIP_SERVICE_HOST,..._PORT) en los Pods creados después del Service. El DNS sigue siendo el método recomendado (funciona sea cual sea el orden de creación).
| Valor | Efecto | Compromiso |
|---|---|---|
| Cluster (por defecto) | El tráfico puede redirigirse a otro nodo para alcanzar un Pod | Buen reparto, pero la IP origen del cliente queda enmascarada (SNAT) y un salto de red extra |
| Local | Solo atiende los Pods del nodo que recibe el paquete | Preserva la IP origen del cliente, sin salto; pero desequilibrio si los Pods están mal repartidos |
| Valor | Efecto |
|---|---|
| Cluster (por defecto) | Enruta hacia cualquier Pod del Service |
| Local | Enruta solo hacia los Pods del mismo nodo (útil para la latencia / la localidad) |
externalTrafficPolicy: Locales el ajuste clave cuando necesitas conocer la IP real del cliente (logs, seguridad, geolocalización).
Por defecto, cada petición puede ir a cualquier Pod. Para «pegar» un cliente a un mismo Pod:
spec:
sessionAffinity: ClientIP
sessionAffinityConfig:
clientIP:
timeoutSeconds: 10800 # 3 hNone (por defecto): reparto en cada petición.ClientIP: todas las peticiones de una misma IP van al mismo Pod (sticky sessions básicas L4).Para sesiones HTTP más finas (por cookie), se usa más bien un Ingress (L7).
protocol: TCP (por defecto), UDP (DNS, juegos, streaming), SCTP (telecomunicaciones).appProtocol (indicativo) precisa el protocolo de aplicación (http, https, grpc) para las herramientas/LB.ports:
- name: dns-udp
port: 53
protocol: UDP
targetPort: 53
- name: dns-tcp
port: 53
protocol: TCP
targetPort: 53Un Service trabaja en L4 (IP/puerto). No sabe enrutar según la URL, el nombre de host, ni gestionar el TLS. Para eso:
| Objeto | Capa | Papel |
|---|---|---|
| Service (ClusterIP/NodePort/LB) | L3/L4 | Dirección estable + LB simple hacia Pods |
| Ingress | L7 (HTTP/HTTPS) | Enrutado por host y ruta, TLS, un solo punto de entrada para varios services |
| Gateway API | L7 (sucesor de Ingress) | Más expresivo, separación de papeles, multi-protocolo |
Modelo típico en producción: un solo LoadBalancer → Ingress → varios ClusterIP internos. Se ahorran los LB caros y se centralizan TLS/enrutado.
| Tipo | IP interna | Acceso externo | DNS | Selector | Caso de uso |
|---|---|---|---|---|---|
| ClusterIP | Sí | No | 1 A (ClusterIP) | Sí | Comunicación interna (el más habitual) |
| NodePort | Sí | Puerto del nodo | 1 A | Sí | Dev/local, ladrillo de un LB |
| LoadBalancer | Sí | IP pública | 1 A | Sí | Service público en cloud |
| ExternalName | No | — (CNAME) | CNAME | No | Alias hacia un service externo |
Headless (clusterIP: None) | No | No | N A (Pods) | Sí | StatefulSet, bases replicadas |
| Sin selector | Sí | según tipo | 1 A | No | Endpoints manuales (recurso externo) |
| Síntoma | Causa frecuente | Solución |
|---|---|---|
| El Service no responde | Selector ≠ labels de los Pods | Alinear spec.selector y los labels del template del Pod |
| Endpoints vacíos | Ningún Pod Ready o ningún Pod coincidente | kubectl get endpoints <svc>; verificar readinessProbe y labels |
| Conexión rechazada en interno | targetPort incorrecto | targetPort = puerto real del contenedor |
EXTERNAL-IP se queda en <pending> | No hay controlador LB (kind/bare-metal) | Docker Desktop OK; si no, MetalLB / port-forward |
| IP origen del cliente enmascarada | externalTrafficPolicy: Cluster | Pasar a Local |
| NodePort inaccesible | Puerto fuera de rango / ocupado | Usar 30000–32767, cambiar nodePort |
| El DNS no resuelve | Namespace incorrecto / CoreDNS KO | Probar el FQDN; kubectl -n kube-system get pods (coredns) |
Comandos de diagnóstico:
kubectl get svc <nom> -o wide
kubectl describe svc <nom>
kubectl get endpoints <nom>
kubectl get endpointslices -l kubernetes.io/service-name=<nom>
kubectl run test --rm -it --image=busybox:1.36 -- sh # nslookup <svc>, wget -qO- http://<svc>targetPort por nombre → desacoplamiento).app, tier, version).externalTrafficPolicy: Local cuando cuenta la IP real del cliente.Quita type: NodePort (o pon ClusterIP), vuelve a aplicar y demuestra que ya no es accesible desde el anfitrión pero sí por nombre desde un Pod (curl http://demo-nodeport... renombrado). Observa kubectl get svc: ya no hay columna nodePort.
Cambia el selector del Service a app: inexistant, vuelve a aplicar y comprueba kubectl get endpoints vacío + service inalcanzable. Restaura app: demo-back: los Endpoints vuelven.
Crea un Service con clusterIP: None y, desde un Pod: nslookup demo-headless. Debes ver varias IP (una por Pod) en vez de una sola.
Añade un puerto metrics (9090) nombrado en el contenedor y en el Service. Verifica con kubectl describe svc que aparecen los dos puertos y que targetPort referencia bien el nombre del puerto.
Crea un Service ExternalName hacia example.com. Desde un Pod: nslookup mon-alias debe devolver un CNAME hacia example.com.
Vuelta a la corrección del proyecto · Conceptos básicos: 01-CONCEPTS-SERVICES.md · Comandos: 02-COMMANDES.md.
Curso creado por el Dr. Haythem REHOUMA — Desarrollo y despliegue de soluciones de datos