En la lección anterior desmontamos la ficción del ClusterIP: una dirección que no existe en ninguna interfaz y que solo vive como reglas de iptables programadas por kube-proxy. Pero ese descubrimiento deja intacto el problema que arrastramos desde el final del módulo 3: un ClusterIP solo es alcanzable desde dentro del clúster. Un cliente que quiere comprar un billete en www.rutasnorte.example no tiene ninguna forma de llegar a tienda-web. Esta lección recorre los cuatro tipos de Service —ClusterIP, NodePort, LoadBalancer y ExternalName— más dos variantes que no son tipos pero se comportan como si lo fueran: los servicios headless y los servicios sin selector. Al terminar sabrás exactamente cuál usar en cada situación, por qué LoadBalancer no es la solución para publicar la plataforma, y cómo dar un nombre interno del clúster a la pasarela de pagos externa.

Contenido

  1. Panorama: seis formas de declarar un Service
  2. Cómo cada tipo se construye sobre el anterior
  3. NodePort en detalle
  4. LoadBalancer y su coste
  5. externalTrafficPolicy: Cluster frente a Local
  6. ExternalName: la pasarela de pagos
  7. Servicios sin selector: endpoints a mano
  8. Servicios headless
  9. sessionAffinity, puertos con nombre y multipuerto
  10. Qué usa Rutas Norte y por qué

  1. Panorama: seis formas de declarar un Service

Antes de entrar en detalle, la tabla que conviene tener delante durante toda la lección.

Forma spec clave Alcance Quién lo implementa Cuándo usarlo
ClusterIP type: ClusterIP (por defecto) Solo dentro del clúster kube-proxy Comunicación entre componentes. El 90 % de los casos
NodePort type: NodePort IP de cualquier nodo, puerto 30000-32767 kube-proxy Pruebas, laboratorio, o como cimiento de un balanceador externo
LoadBalancer type: LoadBalancer IP pública/externa Un controlador del proveedor de nube Exponer un servicio TCP/UDP al exterior en la nube
ExternalName type: ExternalName + externalName Redirección de nombre DNS CoreDNS (registro CNAME) Dar un nombre interno a un servicio de fuera
Headless clusterIP: None Solo DNS, sin IP virtual CoreDNS (una A por pod) StatefulSets, clientes que balancean solos
Sin selector Sin selector, con EndpointSlice manual Igual que su tipo Tú, a mano Servicio externo con IPs fijas, migraciones

Las dos últimas filas no son valores de type: son modificaciones que se combinan con él. Un Service puede ser ClusterIP y headless, o ClusterIP y sin selector.

  1. Cómo cada tipo se construye sobre el anterior

Los tres primeros tipos no son alternativas: son capas acumulativas. Un NodePort sigue teniendo su ClusterIP y sigue funcionando desde dentro. Un LoadBalancer sigue teniendo su NodePort.

flowchart TB
    subgraph LB["type: LoadBalancer"]
      subgraph NP["type: NodePort"]
        subgraph CIP["type: ClusterIP"]
          A["IP virtual interna<br/>10.96.x.x:puerto<br/>reglas de kube-proxy"]
        end
        B["Ademas: puerto 30000-32767<br/>abierto en TODOS los nodos"]
      end
      C["Ademas: IP externa aprovisionada<br/>por el proveedor, que apunta<br/>a los NodePorts de los nodos"]
    end

Compruébalo con la salida de kubectl:

kubectl get svc -n rutas-norte-dev
NAME           TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
api-reservas   ClusterIP      10.96.201.14    <none>        3000/TCP       9d
tienda-web     NodePort       10.96.88.7      <none>        80:31080/TCP   4m
demo-lb        LoadBalancer   10.96.44.190    10.0.0.51     80:32410/TCP   2m

Fíjate en la columna PORT(S): 80:31080/TCP significa "puerto 80 del Service, publicado también en el 31080 de cada nodo". Y los tres tienen CLUSTER-IP, incluido el LoadBalancer. Esa acumulación es la razón de que el diagnóstico se haga siempre de dentro hacia fuera: si el ClusterIP no funciona, el NodePort tampoco lo hará.

  1. NodePort en detalle

Un NodePort abre el mismo puerto en todos los nodos del clúster, independientemente de dónde estén los pods. Si llamas a ese puerto en un nodo que no aloja ninguna réplica, kube-proxy reenvía el tráfico al nodo que sí la tiene.

# k8s/entornos/dev/tienda-web-service-nodeport.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: NodePort
  selector:            # recuerda: solo app y entorno en los selectores
    app: tienda-web
    entorno: dev
  ports:
    - name: http
      port: 80         # puerto del ClusterIP
      targetPort: 8080 # puerto del contenedor nginx
      nodePort: 31080  # OPCIONAL: si se omite, lo asigna el clúster

Puntos a tener claros:

  • El rango por defecto es 30000-32767 (unos 2.700 puertos). Se puede cambiar con --service-node-port-range en el apiserver, pero es una decisión del clúster, no del equipo de aplicación.
  • Si omites nodePort, Kubernetes elige uno libre. Es lo recomendable: fijarlo introduce un conflicto potencial entre equipos y un Invalid value: provided port is already allocated cuando alguien se adelanta.
  • El puerto se abre en todos los nodos, aunque solo uno tenga pods. Con externalTrafficPolicy: Local esto cambia (apartado 5).
  • Prueba en minikube:
minikube -p rutas-norte service tienda-web -n rutas-norte-dev --url
http://192.168.49.2:31080

Por qué no se usa en producción

Problema Consecuencia
Puertos feos y arbitrarios Nadie va a escribir http://rutasnorte.example:31080 en el navegador
Sin nombre de dominio ni TLS Habría que resolver el HTTPS por fuera
Sin alta disponibilidad Apuntas a la IP de un nodo; si ese nodo cae, el servicio deja de responder aunque el pod viva
Requiere abrir el cortafuegos 2.700 puertos potencialmente accesibles en todos los nodos
No agrupa servicios Un puerto por Service, sin enrutado por host ni por ruta

El uso legítimo de NodePort es como cimiento: los balanceadores de las nubes y muchos controladores de Ingress se apoyan precisamente en él. Para desarrollo y demostraciones rápidas, es perfecto.

  1. LoadBalancer y su coste

type: LoadBalancer le dice al clúster: "pide al proveedor de infraestructura un balanceador externo que apunte a este servicio". Kubernetes no lo materializa: lo hace un controlador que forma parte del cloud-controller-manager de la nube, o un complemento como MetalLB en instalaciones propias.

apiVersion: v1
kind: Service
metadata:
  name: tienda-web
  namespace: rutas-norte-pro
spec:
  type: LoadBalancer
  selector:
    app: tienda-web
    entorno: pro
  ports:
    - name: http
      port: 80
      targetPort: 8080

En un clúster sin proveedor de nube, el resultado es este:

NAME         TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
tienda-web   LoadBalancer   10.96.44.190   <pending>     80:32410/TCP   5m

Ese <pending> eterno es una de las preguntas más repetidas por quien empieza. No es un fallo: es que nadie ha recogido la petición. El objeto está bien creado, pero no existe ningún controlador capaz de aprovisionar una IP externa. En minikube se resuelve con:

# En otra terminal; pide contraseña de administrador y hay que dejarlo abierto
minikube -p rutas-norte tunnel
NAME         TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)
tienda-web   LoadBalancer   10.96.44.190   10.96.44.190  80:32410/TCP

minikube tunnel crea rutas en tu máquina para que las IPs asignadas sean alcanzables. Es una emulación local, no un balanceador real.

El argumento económico que lleva a Ingress

Cada Service de tipo LoadBalancer provoca el aprovisionamiento de un balanceador independiente en la nube, con su IP pública y su factura. Para Rutas Norte:

Servicio a publicar ¿Balanceador propio? Coste mensual aproximado
tienda-web 1 balanceador
api-reservas 1 balanceador
Panel de administración interno 1 balanceador
Métricas de Grafana (07-04) 1 balanceador

Cuatro balanceadores, cuatro IPs públicas, cuatro certificados TLS que gestionar por separado y ninguna posibilidad de enrutar por ruta o por cabecera. Multiplicado por tres entornos, doce. La alternativa es un único LoadBalancer delante de un controlador de Ingress, que reparte internamente por dominio y por ruta. De ahí viene 04-04.

Además, LoadBalancer opera en L4: no entiende HTTP, no puede enrutar por Host, no puede reescribir rutas. Para servicios que no son HTTP (un broker MQTT, un postgres expuesto a otra red) sigue siendo la respuesta correcta.

  1. externalTrafficPolicy: Cluster frente a Local

Aplica a NodePort y LoadBalancer, y decide qué hace un nodo cuando le llega tráfico externo para un Service.

flowchart TB
    subgraph CL["externalTrafficPolicy: Cluster (por defecto)"]
      direction LR
      C1["Cliente 203.0.113.9"] --> N1["Nodo A<br/>(sin pods)"]
      N1 -->|"SNAT: origen pasa a IP de nodo A"| N2["Nodo B<br/>pod tienda-web"]
      N2 --> R1["El pod ve origen = 192.168.49.2"]
    end
    subgraph LO["externalTrafficPolicy: Local"]
      direction LR
      C2["Cliente 203.0.113.9"] --> M1["Nodo A<br/>(sin pods)"]
      M1 -->|"DESCARTA"| X1["Sin respuesta"]
      C2 --> M2["Nodo B<br/>pod tienda-web"]
      M2 --> R2["El pod ve origen = 203.0.113.9"]
    end
Cluster (por defecto) Local
Salto extra entre nodos Sí, si el nodo no tiene pods No, nunca
IP de origen del cliente Se pierde (SNAT al nodo) Se conserva
Reparto de carga Uniforme entre todos los pods Proporcional a los pods de ese nodo
Comprobación de salud del balanceador Todos los nodos responden Solo los que tienen pods (por healthCheckNodePort)
Si un nodo no tiene pods Reenvía Descarta el paquete

El desequilibrio de Local merece un ejemplo. Si tienda-web tiene 3 réplicas y el nodo A aloja 2 y el nodo B aloja 1, un balanceador que reparta 50/50 entre nodos hará que cada pod del nodo A reciba un 25 % del tráfico y el del nodo B un 50 %. Se corrige con antiafinidad de pods (06-05).

Para Rutas Norte, la IP de origen importa: los registros de acceso, la limitación de peticiones en los picos de puentes y el bloqueo de direcciones abusivas dependen de ella. Con Cluster todas las peticiones parecerían venir de los nodos.

spec:
  type: LoadBalancer
  externalTrafficPolicy: Local   # conserva la IP real del cliente

Cuando el tráfico llega por un Ingress HTTP hay una alternativa: el controlador añade la cabecera X-Forwarded-For con la IP real. Pero eso solo sirve para HTTP y solo si la aplicación la lee. Existe también internalTrafficPolicy, con la misma idea aplicada al tráfico interno del clúster: Local obliga a que un pod se conecte solo a réplicas de su propio nodo.

  1. ExternalName: la pasarela de pagos

api-reservas tiene que cobrar los billetes contra pagos.proveedorexterno.example, que está fuera del clúster. Se podría poner ese dominio en un ConfigMap y listo, pero ExternalName da una capa de indirección muy útil.

# k8s/entornos/pro/pasarela-pagos-externalname.yaml
apiVersion: v1
kind: Service
metadata:
  name: pasarela-pagos
  namespace: rutas-norte-pro
  labels:
    app.kubernetes.io/part-of: rutas-norte
    entorno: pro
spec:
  type: ExternalName
  externalName: pagos.proveedorexterno.example

Qué ocurre por debajo: CoreDNS crea un registro CNAME. Cuando api-reservas resuelve pasarela-pagos.rutas-norte-pro.svc.cluster.local, la respuesta es un alias hacia pagos.proveedorexterno.example, que se resuelve por el DNS externo (04-03).

kubectl run t --rm -it --restart=Never -n rutas-norte-pro \
  --image=nicolaka/netshoot -- nslookup pasarela-pagos
Server:    10.96.0.10
Address:   10.96.0.10#53

pasarela-pagos.rutas-norte-pro.svc.cluster.local
    canonical name = pagos.proveedorexterno.example.
Name:      pagos.proveedorexterno.example
Address:   198.51.100.44

Ventajas para nuestro caso:

  • La aplicación siempre apunta a pasarela-pagos, en los tres entornos. Lo que cambia es el externalName de cada namespace: en rutas-norte-dev apuntaría a pagos-sandbox.proveedorexterno.example, en pro al de producción. Cambiar de proveedor no toca ni una línea de código.
  • No hay proxy: el tráfico va directo del pod al destino externo, sin pasar por kube-proxy.

Y sus tres limitaciones, que causan sorpresas:

  1. No traduce puertos. El ports de un ExternalName es decorativo. Si el proveedor escucha en el 8443, tu cliente debe pedir el 8443.
  2. No hace TLS. El certificado del proveedor será el de pagos.proveedorexterno.example, no el de pasarela-pagos, así que la validación SNI/hostname usará el nombre real. Con HTTP y Host reescrito puede fallar la comprobación en el otro extremo.
  3. Depende del DNS externo. Si CoreDNS no puede reenviar al resolutor del nodo, esto no funciona.

Nota importante para 04-06: un ExternalName no cuenta como excepción en una política de red. Para permitir la salida hacia el proveedor habrá que autorizar su rango de IPs con ipBlock.

  1. Servicios sin selector: endpoints a mano

Un Service normal averigua sus destinos mediante el selector. Si omites el selector, el controlador de endpoints no crea nada y puedes escribir tú mismo el EndpointSlice. El resultado es un ClusterIP normal, con su nombre DNS y su balanceo, que apunta a direcciones que no son pods.

Caso realista de Rutas Norte: durante la migración, la base de datos de facturación sigue en una máquina virtual de la oficina, en 10.20.30.40:5432.

# k8s/entornos/pro/facturacion-externa.yaml
apiVersion: v1
kind: Service
metadata:
  name: facturacion-legado
  namespace: rutas-norte-pro
spec:
  ports:
    - name: postgres
      port: 5432
      targetPort: 5432
      protocol: TCP
  # SIN selector: nadie rellena los endpoints automaticamente
---
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: facturacion-legado-manual
  namespace: rutas-norte-pro
  labels:
    # OBLIGATORIA: enlaza el slice con el Service
    kubernetes.io/service-name: facturacion-legado
addressType: IPv4
ports:
  - name: postgres      # debe coincidir con el name del Service
    port: 5432
    protocol: TCP
endpoints:
  - addresses:
      - "10.20.30.40"
    conditions:
      ready: true

Detalles críticos:

  • La etiqueta kubernetes.io/service-name es lo que une el slice con el Service. Sin ella, el Service se queda sin endpoints.
  • El campo ports[].name del EndpointSlice debe coincidir con el ports[].name del Service.
  • Nadie comprueba la salud de esa dirección. Si la máquina cae, el Service sigue enviando tráfico. La responsabilidad de mantener la lista es tuya.
  • Las direcciones no pueden ser IPs de Services (10.96.x.x) ni de bucle local.

Ventaja principal: la aplicación conecta a facturacion-legado:5432 desde el primer día. El día que esa base de datos se migre a un pod del clúster, se añade el selector, se borra el slice manual y la aplicación no se entera. Es el patrón de indirección que hace las migraciones tolerables.

  1. Servicios headless

Un Service con clusterIP: None es headless: no tiene IP virtual, no crea reglas en kube-proxy y no balancea nada. Lo único que hace es publicar en el DNS una entrada A por cada pod que casa con el selector.

apiVersion: v1
kind: Service
metadata:
  name: postgres-reservas-headless
  namespace: rutas-norte-pro
spec:
  clusterIP: None          # esto lo convierte en headless
  selector:
    app: postgres-reservas
    entorno: pro
  ports:
    - name: postgres
      port: 5432
Service normal Service headless
CLUSTER-IP 10.96.x.x None
Reglas de kube-proxy No
Respuesta DNS Una A: la IP virtual Una A por pod listo
Balanceo Lo hace el kernel Lo hace el cliente
Uso típico Todo lo demás Bases de datos en clúster, StatefulSets
$ nslookup postgres-reservas-headless.rutas-norte-pro.svc.cluster.local
Name:  postgres-reservas-headless...  Address: 10.244.0.14
Name:  postgres-reservas-headless...  Address: 10.244.0.18
Name:  postgres-reservas-headless...  Address: 10.244.0.23

¿Para qué sirve esto? Para los casos en que el cliente necesita distinguir los pods entre sí:

  • Una réplica de PostgreSQL con un primario y dos secundarios: el cliente escribe en el primario y lee de los secundarios. Un ClusterIP que reparta al azar lo rompería.
  • Bases de datos distribuidas (Cassandra, Elasticsearch, Kafka) donde cada nodo debe conocer a los demás por su nombre.
  • Clientes con balanceo propio (gRPC), que quieren la lista de destinos para repartir por petición y no por conexión.

El detalle completo de los registros DNS que genera está en 04-03, y su uso real —con identidades estables tipo postgres-reservas-0.postgres-reservas-headless— llega con los StatefulSets de 06-01, donde por fin postgres-reservas dejará de ser un Deployment.

Un matiz: un Service headless y sin selector no genera registros A; en su lugar CoreDNS devuelve los nombres del externalName si lo hubiera, o los endpoints manuales que definas.

  1. sessionAffinity, puertos con nombre y multipuerto

sessionAffinity: ClientIP

Por defecto kube-proxy reparte las conexiones al azar. Con afinidad, todas las conexiones de la misma IP de origen van al mismo pod durante un tiempo.

spec:
  sessionAffinity: ClientIP
  sessionAffinityConfig:
    clientIP:
      timeoutSeconds: 10800   # 3 horas; por defecto también 10800

Es un martillo tosco y hay que decirlo claro:

  • Funciona por IP, no por usuario. Cien clientes detrás del NAT de una empresa son una sola IP y caen todos en el mismo pod.
  • Rompe el reparto de carga y complica los despliegues progresivos.
  • Con externalTrafficPolicy: Cluster, la IP de origen ya viene enmascarada, así que la afinidad se aplica sobre la IP del nodo: inútil.

Para Rutas Norte no lo usamos: api-reservas es sin estado, y la sesión del comprador va en un JWT y en redis-cache. Esa es la solución correcta; la afinidad de sesión es un parche para aplicaciones que guardan estado en memoria.

Puertos con nombre y multipuerto

Cuando un Service expone más de un puerto, name es obligatorio en cada uno.

apiVersion: v1
kind: Service
metadata:
  name: api-reservas
  namespace: rutas-norte-pro
spec:
  selector:
    app: api-reservas
    entorno: pro
  ports:
    - name: http          # obligatorio al haber varios
      port: 3000
      targetPort: api     # apunta a un puerto CON NOMBRE del contenedor
      protocol: TCP
    - name: metrics       # lo consumirá Prometheus en 07-03
      port: 9090
      targetPort: metrics
      protocol: TCP

Con el contenedor declarando:

    ports:
      - name: api
        containerPort: 3000
      - name: metrics
        containerPort: 9090

Ventajas de usar targetPort con nombre: si mañana la aplicación cambia de puerto, se toca solo el Deployment. Y los nombres de puerto son los que habilitan los registros SRV del DNS (04-03). Restricción: el nombre debe tener 15 caracteres o menos y ser minúsculas, dígitos y guiones.

  1. Qué usa Rutas Norte y por qué

Componente Tipo elegido Motivo
tienda-web ClusterIP Se publicará por Ingress (04-04), no directamente
api-reservas ClusterIP multipuerto (http + metrics) Igual; las métricas nunca se exponen fuera
postgres-reservas ClusterIP (y headless en 06-01) Nunca debe salir del clúster
redis-cache ClusterIP Uso interno exclusivo
worker-notificaciones Sin Service No recibe conexiones entrantes; solo consume de la cola
pasarela-pagos ExternalName Indirección hacia el proveedor externo
facturacion-legado Sin selector + EndpointSlice manual Base de datos aún fuera del clúster
Controlador de Ingress LoadBalancer (uno solo) Un balanceador para toda la plataforma

Esa última fila es la conclusión práctica de la lección: la plataforma entera acaba teniendo un solo punto de entrada de pago, y todo lo demás vive en ClusterIP.

Un detalle sobre worker-notificaciones que conviene interiorizar: no todo Deployment necesita un Service. El Service existe para recibir conexiones. Un proceso que solo hace conexiones salientes no necesita ninguno. (Sí necesitará uno si más adelante expone métricas para Prometheus.)

Errores Comunes y Consejos

  • Poner type: LoadBalancer en un clúster local y esperar una IP. El <pending> es lo esperado sin proveedor. En minikube, minikube tunnel; en un servidor propio, MetalLB.
  • Fijar nodePort a mano "para que sea fácil de recordar". Genera colisiones entre equipos y despliegues que fallan con provided port is already allocated. Deja que lo asigne el clúster.
  • Cambiar de ClusterIP a NodePort y creer que se pierde el ClusterIP. No se pierde: los tipos son acumulativos. El tráfico interno sigue igual.
  • Usar ExternalName con puertos distintos. No traduce puertos. Si el proveedor escucha en 8443, tu cliente debe pedir 8443 explícitamente.
  • Un Service sin selector sin EndpointSlice. Se crea sin ningún error y no responde nunca. Comprueba siempre con kubectl get endpointslices -l kubernetes.io/service-name=<svc>.
  • Olvidar name en un Service multipuerto. El error es claro (must specify name for multiple ports), pero llega tarde: acostúmbrate a nombrar siempre los puertos, incluso con uno solo.
  • Usar sessionAffinity para arreglar una aplicación con estado en memoria. Estás tapando el problema. Saca el estado a redis-cache.
  • Esperar que un Service headless balancee. No lo hace. El cliente recibe todas las IPs y decide él. Muchas bibliotecas HTTP cogen solo la primera.
  • Consejo: kubectl get svc -A -o custom-columns=NS:.metadata.namespace,NOMBRE:.metadata.name,TIPO:.spec.type es una auditoría de un vistazo. Cualquier LoadBalancer inesperado es dinero saliendo.
  • Consejo: al depurar, ve siempre de dentro afuera. ClusterIP → NodePort → LoadBalancer. Un fallo en la capa interna se manifiesta en todas las externas.

Ejercicios

Ejercicio 1: Publicar tienda-web con NodePort y comprobar las capas

En rutas-norte-dev, cambia el Service de tienda-web a NodePort sin fijar el puerto. Comprueba que sigue funcionando desde dentro por su ClusterIP y desde fuera por la IP del nodo. Explica qué columna de kubectl get svc te dice ambos puertos.

Ejercicio 2: La pasarela de pagos con ExternalName

Crea el Service pasarela-pagos en rutas-norte-dev apuntando a un sandbox (pagos-sandbox.proveedorexterno.example) y en rutas-norte-pro al dominio real. Verifica desde un pod efímero que la resolución devuelve un CNAME distinto en cada namespace. Explica por qué esto permite que el código de api-reservas sea idéntico en ambos entornos.

Ejercicio 3: Base de datos de facturación fuera del clúster

Crea facturacion-legado como Service sin selector con un EndpointSlice manual apuntando a 10.20.30.40:5432. Comprueba que el Service tiene endpoints y que el nombre resuelve. Después, provoca el fallo típico: borra la etiqueta kubernetes.io/service-name del slice y observa qué pasa.

Soluciones

Ejercicio 1

kubectl patch svc tienda-web -n rutas-norte-dev -p '{"spec":{"type":"NodePort"}}'
kubectl get svc tienda-web -n rutas-norte-dev
NAME         TYPE       CLUSTER-IP    EXTERNAL-IP   PORT(S)        AGE
tienda-web   NodePort   10.96.88.7    <none>        80:30417/TCP   9d
# Desde dentro: el ClusterIP sigue vivo
kubectl run t --rm -it --restart=Never -n rutas-norte-dev \
  --image=nicolaka/netshoot -- curl -s -o /dev/null -w '%{http_code}\n' http://tienda-web

# Desde fuera
curl -s -o /dev/null -w '%{http_code}\n' http://$(minikube -p rutas-norte ip):30417

La columna PORT(S) con formato 80:30417/TCP da los dos: a la izquierda el puerto del ClusterIP, a la derecha el NodePort. Demuestra que NodePort añade una capa sobre ClusterIP.

Ejercicio 2

for NS in dev pro; do
  DEST=$( [ $NS = pro ] && echo pagos.proveedorexterno.example \
                        || echo pagos-sandbox.proveedorexterno.example )
  kubectl create service externalname pasarela-pagos \
    --external-name=$DEST -n rutas-norte-$NS
done

kubectl run t --rm -it --restart=Never -n rutas-norte-dev \
  --image=nicolaka/netshoot -- nslookup pasarela-pagos
pasarela-pagos.rutas-norte-dev.svc.cluster.local
    canonical name = pagos-sandbox.proveedorexterno.example.

El código de api-reservas usa siempre http://pasarela-pagos/cobros. El namespace determina a qué proveedor real se traduce, sin variables de entorno distintas ni ramas en el código. Cambiar de proveedor es editar un manifiesto.

Ejercicio 3

kubectl apply -f k8s/entornos/pro/facturacion-externa.yaml

kubectl get endpointslices -n rutas-norte-pro \
  -l kubernetes.io/service-name=facturacion-legado
NAME                        ADDRESSTYPE   PORTS   ENDPOINTS     AGE
facturacion-legado-manual   IPv4          5432    10.20.30.40   8s
# Provocar el fallo: quitar la etiqueta que une slice y Service
kubectl label endpointslice facturacion-legado-manual \
  -n rutas-norte-pro kubernetes.io/service-name-

kubectl get endpointslices -n rutas-norte-pro \
  -l kubernetes.io/service-name=facturacion-legado
# No resources found

El Service sigue existiendo, su ClusterIP sigue asignado, kubectl get svc no muestra nada raro, y toda conexión se queda colgada hasta agotar el tiempo de espera. Es el mismo cuadro clínico que un selector que no casa, visto en 02-05: el Service parece sano y no tiene destinos. Por eso la primera comprobación ante "el Service no responde" es siempre mirar sus endpoints.

Conclusión

Ya conoces el catálogo completo. ClusterIP es el tipo por defecto y el que usa casi toda la plataforma: solo alcanzable desde dentro. NodePort añade encima un puerto del rango 30000-32767 abierto en todos los nodos, útil para probar y como cimiento de otros mecanismos, pero inaceptable en producción por sus puertos arbitrarios, su falta de TLS y su dependencia de la IP de un nodo concreto. LoadBalancer añade encima una IP externa aprovisionada por el proveedor —de ahí el <pending> eterno cuando no hay ninguno, y minikube tunnel para emularlo—, y su coste de un balanceador por servicio es exactamente el argumento que justifica el Ingress. ExternalName no enruta tráfico: hace que CoreDNS devuelva un CNAME, y con él hemos dado a la pasarela de pagos un nombre interno estable, distinto por entorno, sin tocar el código.

Sabes que los tres primeros tipos son capas acumulativas, lo que impone diagnosticar siempre de dentro hacia fuera. Entiendes externalTrafficPolicy: Cluster reenvía entre nodos pero destruye la IP de origen del cliente, mientras que Local la conserva a cambio de un reparto de carga desigual. Y dominas las dos variantes que no son tipos: los servicios sin selector, donde escribes tú el EndpointSlice para apuntar a una base de datos externa y donde la etiqueta kubernetes.io/service-name es el eslabón que todo el mundo olvida; y los servicios headless, que renuncian a la IP virtual para publicar una entrada A por pod, imprescindibles cuando el cliente necesita distinguir réplicas.

Hay un hilo que ha atravesado toda la lección sin desarrollarse: el DNS. ExternalName funciona porque CoreDNS crea un CNAME. Headless funciona porque CoreDNS crea una A por pod. api-reservas encuentra a postgres-reservas porque hay un nombre que se resuelve. La siguiente lección, 04-03, abre esa caja: qué es CoreDNS, qué formas cortas de nombre funcionan y cuándo, por qué ndots: 5 puede multiplicar por cinco las consultas a la pasarela de pagos, y cómo diagnosticar los tres fallos de DNS que verás una y otra vez en tu carrera.

Curso de Kubernetes

Módulo 1: Introducción a Kubernetes

Módulo 2: Componentes Principales de Kubernetes

Módulo 3: Gestión de Configuración y Secretos

Módulo 4: Redes en Kubernetes

Módulo 5: Almacenamiento en Kubernetes

Módulo 6: Conceptos Avanzados de Kubernetes

Módulo 7: Monitoreo y Registro

Módulo 8: Seguridad en Kubernetes

Módulo 9: Escalado y Rendimiento

Módulo 10: Ecosistema y Herramientas de Kubernetes

Módulo 11: Estudios de Caso y Aplicaciones del Mundo Real

Módulo 12: Preparación para la Certificación de Kubernetes

© Copyright 2026. Todos los derechos reservados