Los tipos de Services de Kubernetes — guía ultra exhaustiva

13 min

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.

Tabla de contenidos

  1. Recordatorio: el papel de un Service
  2. Anatomía de un Service (todos los campos)
  3. Tipo 1 — ClusterIP
  4. Tipo 2 — NodePort
  5. Tipo 3 — LoadBalancer
  6. Tipo 4 — ExternalName
  7. Service Headless (sin ClusterIP)
  8. Service sin selector (Endpoints manuales)
  9. port vs targetPort vs nodePort
  10. Multi-puerto y puertos nombrados
  11. Cómo funciona por debajo: kube-proxy
  12. Endpoints y EndpointSlices
  13. El DNS de los Services (CoreDNS)
  14. Políticas de tráfico (externalTrafficPolicy / internalTrafficPolicy)
  15. Session affinity
  16. Protocolos: TCP, UDP, SCTP, appProtocol
  17. Service vs Ingress vs Gateway API
  18. Tabla recapitulativa de los tipos
  19. Trampas clásicas y diagnóstico
  20. Buenas prácticas
  21. Mini-ejercicios

1. Recordatorio: el papel de un Service

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:

  • ofrece una identidad de red permanente (una IP virtual y/o un nombre DNS);
  • selecciona un conjunto de Pods mediante sus labels;
  • reparte el tráfico (load balancing) entre esos Pods;
  • se actualiza automáticamente cuando los Pods aparecen o desaparecen.

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.


2. Anatomía de un Service (todos los campos)

yaml
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.


3. Tipo 1 — ClusterIP

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.

yaml
apiVersion: v1
kind: Service
metadata:
  name: demo-clusterip
spec:
  type: ClusterIP
  selector:
    app: demo-back
  ports:
    - port: 80
      targetPort: 5000

Características:

  • Inaccesible desde el exterior (sin EXTERNAL-IP).
  • Base de la comunicación interna (frontend → backend, app → base de datos, microservicios entre sí).
  • Alcanzable por nombre DNS: http://demo-clusterip (ver §13).

Cuándo usarlo: para todo lo que se queda en el clúster. Es el tipo más habitual.


4. Tipo 2 — NodePort

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).

yaml
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 nodo

Acceso: http://<ip-de-n-importe-quel-noeud>:30082 (con Docker Desktop: http://localhost:30082).

Puntos importantes:

  • Si no precisas nodePort, Kubernetes elige uno en el rango.
  • El mismo puerto se abre en todos los nodos (gracias al routing mesh / kube-proxy), incluso en los que no alojan ningún Pod del Service.
  • Poco elegante para producción (puertos no estándar, gestión manual), pero perfecto en dev/local y a menudo el ladrillo bajo un LoadBalancer.

5. Tipo 3 — LoadBalancer

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.

yaml
apiVersion: v1
kind: Service
metadata:
  name: demo-lb
spec:
  type: LoadBalancer
  selector:
    app: demo-back
  ports:
    - port: 8090
      targetPort: 5000

Según el entorno:

EntornoComportamiento
AWS / GCP / AzureCrea un LB gestionado real (ELB/NLB, GCP LB…) y rellena EXTERNAL-IP
Docker DesktopEXTERNAL-IP = localhosthttp://localhost:8090
minikubeminikube tunnel proporciona la IP externa
kind / bare-metalSe 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:

yaml
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).


6. Tipo 4 — ExternalName

Caso particular: ningún selector, ningún Pod, ninguna IP. Crea simplemente un alias DNS (registro CNAME) hacia un nombre externo.

yaml
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 Host HTTP particular.


7. Service Headless (sin ClusterIP)

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.

yaml
apiVersion: v1
kind: Service
metadata:
  name: demo-headless
spec:
  clusterIP: None        # <-- headless
  selector:
    app: demo-back
  ports:
    - port: 80
      targetPort: 5000

Para qué sirve:

  • Cuando el cliente quiere ver cada Pod individualmente (sin LB centralizado).
  • Imprescindible para los StatefulSet: cada Pod obtiene un nombre DNS estable (pod-0.demo-headless, pod-1.demo-headless…), útil para las bases de datos replicadas (Cassandra, Kafka, etc.).
ClusterIP normalHeadless (clusterIP: None)
IP virtualSí (una sola)No
Respuesta DNS1 IP (la del Service)N IP (las de los Pods)
RepartoPor kube-proxyA cargo del cliente
Caso de usoWeb/API sin estadoBases replicadas, StatefulSet

8. Service sin selector (Endpoints manuales)

Un Service puede no tener selector. En ese caso, Kubernetes no rellena los Endpoints solo: los defines a mano. Práctico para exponer un recurso externo bajo una IP interna estable.

yaml
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: 8080

Diferencia con ExternalName: aquí se enruta por IP (con load balancing posible sobre varias IP), no por CNAME DNS.


9. port vs targetPort vs nodePort

Es la fuente de confusión. Tres puertos distintos, tres papeles:

CampoDóndeSignificado
portEn el ServiceEl puerto que usan los clientes para alcanzar el Service
targetPortEn el contenedorEl puerto donde escucha de verdad la aplicación en el Pod
nodePortEn 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».

targetPort puede referenciar un nombre de puerto definido en el contenedor (ver §10), lo que evita codificar el número a pelo.


10. Multi-puerto y puertos nombrados

Un Service puede exponer varios puertos (p. ej. HTTP + métricas). En ese caso, cada entrada debe tener un name.

yaml
spec:
  selector:
    app: demo
  ports:
    - name: http
      port: 80
      targetPort: web          # referencia un puerto NOMBRADO del contenedor
    - name: metrics
      port: 9090
      targetPort: 9090

En el lado del contenedor, se nombran los puertos:

yaml
containers:
  - name: app
    ports:
      - name: web              # <-- reutilizado por targetPort: web
        containerPort: 5000
      - name: metrics
        containerPort: 9090

Ventaja de los puertos nombrados: si cambia el puerto del contenedor, no hay que modificar nada en el Service.


11. Cómo funciona por debajo: kube-proxy

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:

ModoPrincipioNotas
iptables (por defecto)Reglas iptables, selección aleatoria de un PodSimple, robusto, muy extendido
IPVSTabla hash del núcleo, algoritmos reales de LB (rr, lc, sh…)Más performante en clústeres grandes
nftablesSucesor de iptablesMás reciente

Consecuencias prácticas:

  • El reparto iptables es aleatorio (no un round-robin ordenado de verdad).
  • kube-proxy no ve la capa HTTP: es L3/L4 (IP/puerto). Para enrutado HTTP (por ruta, por host), hace falta un Ingress (§17).

12. Endpoints y EndpointSlices

El enlace Service ↔ Pods se materializa con objetos:

  • Endpoints (histórico): un solo objeto que lista todos los IP:port de los Pods listos.
  • EndpointSlices (moderno, recomendado): la lista se parte en trozos (máx. ~100 endpoints cada uno) → escalabilidad mucho mejor en los services grandes.
bash
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: true publica también los Pods no listos — uso headless específico.)


13. El DNS de los Services (CoreDNS)

Kubernetes hace correr CoreDNS. Cada Service recibe un nombre DNS determinista:

<service>                                   # mismo namespace
<service>.<namespace>                        # otro namespace
<service>.<namespace>.svc.cluster.local      # FQDN completo

Ejemplo desde un Pod:

bash
curl http://demo-clusterip                       # mismo namespace
curl http://demo-clusterip.default               # explícito
curl http://demo-clusterip.default.svc.cluster.local

Registros producidos:

  • Service normal → un registro A hacia la ClusterIP.
  • Service headlessvarios A, uno por Pod.
  • Puertos nombrados → registros SRV: _http._tcp.demo-clusterip….
  • ExternalNameCNAME hacia el destino.

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).


14. Políticas de tráfico

externalTrafficPolicy (tráfico entrante externo, NodePort/LoadBalancer)

ValorEfectoCompromiso
Cluster (por defecto)El tráfico puede redirigirse a otro nodo para alcanzar un PodBuen reparto, pero la IP origen del cliente queda enmascarada (SNAT) y un salto de red extra
LocalSolo atiende los Pods del nodo que recibe el paquetePreserva la IP origen del cliente, sin salto; pero desequilibrio si los Pods están mal repartidos

internalTrafficPolicy (tráfico interno, entre Pods)

ValorEfecto
Cluster (por defecto)Enruta hacia cualquier Pod del Service
LocalEnruta solo hacia los Pods del mismo nodo (útil para la latencia / la localidad)

externalTrafficPolicy: Local es el ajuste clave cuando necesitas conocer la IP real del cliente (logs, seguridad, geolocalización).


15. Session affinity

Por defecto, cada petición puede ir a cualquier Pod. Para «pegar» un cliente a un mismo Pod:

yaml
spec:
  sessionAffinity: ClientIP
  sessionAffinityConfig:
    clientIP:
      timeoutSeconds: 10800     # 3 h
  • None (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).


16. Protocolos: TCP, UDP, SCTP, appProtocol

  • protocol: TCP (por defecto), UDP (DNS, juegos, streaming), SCTP (telecomunicaciones).
  • Se pueden mezclar varios protocolos en un mismo Service (puertos distintos).
  • appProtocol (indicativo) precisa el protocolo de aplicación (http, https, grpc) para las herramientas/LB.
yaml
ports:
  - name: dns-udp
    port: 53
    protocol: UDP
    targetPort: 53
  - name: dns-tcp
    port: 53
    protocol: TCP
    targetPort: 53

17. Service vs Ingress vs Gateway API

Un Service trabaja en L4 (IP/puerto). No sabe enrutar según la URL, el nombre de host, ni gestionar el TLS. Para eso:

ObjetoCapaPapel
Service (ClusterIP/NodePort/LB)L3/L4Dirección estable + LB simple hacia Pods
IngressL7 (HTTP/HTTPS)Enrutado por host y ruta, TLS, un solo punto de entrada para varios services
Gateway APIL7 (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.


18. Tabla recapitulativa de los tipos

TipoIP internaAcceso externoDNSSelectorCaso de uso
ClusterIPNo1 A (ClusterIP)Comunicación interna (el más habitual)
NodePortPuerto del nodo1 ADev/local, ladrillo de un LB
LoadBalancerIP pública1 AService público en cloud
ExternalNameNo— (CNAME)CNAMENoAlias hacia un service externo
Headless (clusterIP: None)NoNoN A (Pods)StatefulSet, bases replicadas
Sin selectorsegún tipo1 ANoEndpoints manuales (recurso externo)

19. Trampas clásicas y diagnóstico

SíntomaCausa frecuenteSolución
El Service no respondeSelector ≠ labels de los PodsAlinear spec.selector y los labels del template del Pod
Endpoints vacíosNingún Pod Ready o ningún Pod coincidentekubectl get endpoints <svc>; verificar readinessProbe y labels
Conexión rechazada en internotargetPort incorrectotargetPort = 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 enmascaradaexternalTrafficPolicy: ClusterPasar a Local
NodePort inaccesiblePuerto fuera de rango / ocupadoUsar 30000–32767, cambiar nodePort
El DNS no resuelveNamespace incorrecto / CoreDNS KOProbar el FQDN; kubectl -n kube-system get pods (coredns)

Comandos de diagnóstico:

bash
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>

20. Buenas prácticas

  • Por defecto, ClusterIP. Expón al exterior solo lo que deba estarlo.
  • Un solo LoadBalancer + Ingress delante de varios ClusterIP (coste + TLS centralizado).
  • Nombra tus puertos (multi-puerto, y targetPort por nombre → desacoplamiento).
  • Cuida las readinessProbe: deciden quién está dentro de los Endpoints.
  • Coherencia labels/selectores: es el error n.º 1. Mantén labels estables (app, tier, version).
  • Usa externalTrafficPolicy: Local cuando cuenta la IP real del cliente.
  • Prefiere los EndpointSlices (activados por defecto en las versiones recientes) para la escalabilidad.
  • Nunca codifiques una IP de Pod a pelo: usa el nombre DNS del Service.

21. Mini-ejercicios

Ejercicio 1 — Transformar un NodePort en ClusterIP

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.

Ejercicio 2 — Romper y luego reparar los Endpoints

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.

Ejercicio 3 — Service headless

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.

Ejercicio 4 — Multi-puerto

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.

Ejercicio 5 — ExternalName

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