Módulo 2: Pods Deployments And Services

6. Services: `ClusterIP`, `NodePort`, y por qué un Pod no es una dirección estable

Descripción

La lección 5 dejó andes-cargo-status-api corriendo con dos réplicas — pero si quisieras hacerle curl ahora mismo, tendrías un problema real: ¿a cuál de las dos IPs le hablas? ¿Qué pasa si borras un Pod, como hiciste en la lección 5, y su reemplazo llega con una IP distinta? Esta lección resuelve exactamente ese problema con el tercer objeto central de este módulo: un Service, la pieza que te da una dirección que nunca cambia, sin importar cuántas veces roten los Pods detrás de ella.

Conexión con el módulo

Esta lección es la última pieza conceptual antes de que la lección 7 la ponga en práctica de verdad. Todo lo que construyas de aquí en adelante en esta guía —Ingress en el Módulo 4, NetworkPolicy en ese mismo módulo, hasta el AWS Load Balancer Controller del Módulo 7— existe encima de este mismo concepto: un Service es la unidad básica de descubrimiento dentro de un clúster de Kubernetes, la pieza que hace posible que dos partes de un sistema se hablen sin conocerse la IP mutuamente.


El problema exacto: las IPs de Pod son efímeras

Ya lo confirmaste con tus propias manos en la lección 5: cuando borraste andes-cargo-status-api-56856576d4-8lb5t (IP 10.244.1.2), su reemplazo llegó con una IP distinta (10.244.1.3). Esto no es un detalle de implementación — es una garantía explícita del modelo de Kubernetes: la IP de un Pod solo es válida mientras ese Pod específico existe. En cuanto se recrea —por cualquier razón: lo borraste tú, falló el nodo, un RollingUpdate lo reemplazó— la IP anterior deja de tener sentido, y la nueva es, en principio, impredecible.

Esto convierte a "la IP de un Pod" en la peor dirección posible para que cualquier otra cosa (otro servicio, un cliente externo, tu propio navegador) la use directamente. Necesitas algo estable — un nombre, una dirección, que no cambie sin importar cuántas veces el ReplicaSet reemplace Pods por debajo. Ese "algo" es un Service.

                  EL PROBLEMA: DOS PODS, DOS DIRECCIONES QUE CAMBIAN

  Momento 1                              Momento 2 (después de un delete)

  Pod-8lb5t  10.244.1.2                  Pod-8lb5t  ── ya no existe
  Pod-vjc9k  10.244.2.4                  Pod-7ztk9  10.244.1.3  ← IP nueva
                                          Pod-vjc9k  10.244.2.4  ← sin cambios

  ¿A cuál IP le habla un cliente que necesita "el servicio de status-api"?
  Ninguna de las dos es una respuesta segura ni permanente.

Cómo un Service resuelve esto: por etiqueta, no por nombre de Pod

Un Service no apunta a Pods específicos por nombre — apunta a cualquier Pod que tenga una etiqueta determinada, el mismo mecanismo de metadata.labels que ya usaste en cada Pod y Deployment de este módulo. Cuando declaras un Service con selector: app: andes-cargo-status-api, Kubernetes mantiene, de forma automática y continua, una lista actualizada de todas las IPs de los Pods que en ese momento tienen esa etiqueta — sin que tengas que decírselo cada vez que un Pod nace o muere.

                    CÓMO UN Service ENCUENTRA SUS PODS

  Service: status-api-service
  selector: app=andes-cargo-status-api
      │
      │   Kubernetes busca, constantemente, todos los Pods con esa etiqueta
      ▼
  ┌─────────────────────────────────────────────┐
  │  Pod (app=andes-cargo-status-api)  10.244.1.3  │  ← esta lista se
  │  Pod (app=andes-cargo-status-api)  10.244.2.4  │     actualiza sola,
  └─────────────────────────────────────────────┘     cada vez que un
                                                          Pod nace o muere

Esa lista de IPs actualizada tiene un nombre propio dentro de Kubernetes: un objeto Endpoints (o, en versiones recientes, EndpointSlice), que el propio clúster mantiene por ti — nunca lo declaras a mano, pero sí lo vas a inspeccionar con kubectl get endpoints en la lección 7, para confirmar que el Service de verdad "ve" a tus dos Pods.

El Service en sí, mientras tanto, expone una única dirección IP fija (ClusterIP, ver la siguiente sección) y un nombre DNS resoluble dentro del clúster —status-api-service.andes-cargo.svc.cluster.local, siguiendo el patrón <nombre-del-service>.<namespace>.svc.cluster.local que CoreDNS (el servidor de nombres interno, ya nombrado en el Módulo 1, lección 5) resuelve automáticamente—. Cuando un cliente le habla a esa dirección o a ese nombre, algo dentro del clúster (kube-proxy, un componente que corre en cada nodo) enruta esa conexión hacia una de las IPs de la lista de Endpoints, repartiendo el tráfico entre todas las disponibles. Ese reparto es, literalmente, el balanceo de carga básico que vas a confirmar con evidencia real en la lección 8.


Los tipos de Service, y cuándo usar cada uno

Kubernetes ofrece varios tipos de Service, cada uno resolviendo un alcance distinto de "quién puede alcanzar esta dirección estable":

TipoAlcanceCuándo usarlo
ClusterIP (el que usa esta guía)Solo alcanzable dentro del clúster — otros Pods, u otro proceso que hable con el kube-apiserver vía kubectl port-forwardEl default, y el correcto para un servicio interno que no necesita tráfico directo de fuera del clúster — exactamente el caso de status-api-service hasta que el Módulo 4 le agregue un Ingress
NodePortAbre un puerto fijo (en el rango 30000-32767) en cada nodo del clúster, alcanzable desde fuera con <IP-de-cualquier-nodo>:<puerto>Útil para pruebas rápidas o laboratorios sin un balanceador externo; poco común en un clúster de producción real, porque expone directamente la IP de los nodos
LoadBalancerLe pide al proveedor de nube subyacente (AWS, GCP, Azure) que aprovisione un balanceador de carga real, con IP pública propiaEl tipo que usarías en EKS real para exponer un servicio directo a internet sin Ingressno aplicable en kind, porque no hay ningún proveedor de nube por debajo que sepa crear ese balanceador; el Módulo 4 resuelve el mismo problema con Ingress + ingress-nginx en su lugar
ExternalNameNo enruta a ningún Pod — mapea el nombre del Service a un nombre DNS externoUn caso especial, poco común, fuera del alcance de esta guía

Esta guía usa ClusterIP para status-api-service en esta lección y en la lección 7, y se queda con ese tipo durante todo el resto de la guía — incluso después del Módulo 4, cuando Ingress exponga el servicio a tráfico externo. Esto no es una limitación temporal: es el patrón real de producción. Ingress no reemplaza al Service, se coloca delante de él — el Service sigue siendo ClusterIP, y es Ingress quien recibe el tráfico externo y lo reenvía hacia adentro. Vas a ver esto exacto, con evidencia real, en el Módulo 4.


Anatomía mínima de un Service

apiVersion: v1
kind: Service
metadata:
  name: <nombre-del-service>
  namespace: <namespace>
spec:
  type: ClusterIP
  selector:
    app: <etiqueta>
  ports:
    - port: <puerto-del-service>
      targetPort: <puerto-del-contenedor>
      protocol: TCP

Dos campos merecen atención antes de la lección 7:

  • spec.selector — exactamente el mismo mecanismo de etiquetas que ya conoces del Deployment (lección 3) y del Pod (lección 2). Debe coincidir con spec.template.metadata.labels del Deployment cuyos Pods quieres exponer — si no coincide ni un carácter, el Service existe, pero no encuentra ningún Pod, y cualquier tráfico que reciba no tiene a dónde ir.
  • spec.ports[].port frente a targetPort — dos números que casi siempre se confunden la primera vez que se ven juntos. port es el puerto en el que el Service escucha (el que usa cualquier cliente para hablarle); targetPort es el puerto en el que el contenedor dentro del Pod escucha de verdad (el mismo containerPort que declaraste en el Deployment). Pueden ser el mismo número, pero no tienen que serlo — la lección 7 usa port: 80 con targetPort: 8080, precisamente para mostrar que son independientes.

Analogía: la centralita telefónica, no el número directo de un empleado

Retomando la analogía del edificio de oficinas por última vez en este módulo: si le dieras a un cliente el número de teléfono directo de un empleado específico (el equivalente a "la IP de un Pod"), ese número dejaría de servir en cuanto ese empleado cambiara de puesto, se fuera de la empresa, o simplemente estuviera en el escritorio de al lado un día distinto. Una centralita telefónica, en cambio, tiene un número de extensión fijo —digamos, "marca 100 para Atención al Cliente"— que nunca cambia, sin importar quién esté sentado detrás de ese puesto hoy. Quien llama nunca necesita saber quién responde; solo necesita el número de la extensión. Eso es, exactamente, lo que un Service le da a cualquiera que necesite hablarle a andes-cargo-status-api: una extensión fija, status-api-service, detrás de la cual pueden rotar tantos Pods como el Deployment decida, sin que quien llama note ninguna diferencia.


Errores comunes

Asumir que un Service es un proceso que corre en algún lado, como un Pod más (conceptual). Qué pasa: alguien busca un Service con kubectl get pods, esperando encontrar algún proceso corriendo detrás de esa dirección estable, además de los Pods reales de andes-cargo-status-api. Por qué pasa: la palabra "Service" suena a "algo que corre", como cualquier otro servicio de software. Cómo detectarlo: si buscas, sin éxito, algo llamado status-api-service en la lista de Pods. Cómo corregirlo: un Service es, principalmente, una regla de red mantenida por kube-proxy en cada nodo (reglas de iptables o IPVS, según la configuración del clúster) — no hay ningún Pod ni contenedor corriendo "como" el Service en sí. Es una dirección y una regla de enrutamiento, no un proceso.

Olvidar que port y targetPort son independientes, y usarlos como si fueran el mismo número por costumbre (de configuración). Qué pasa: alguien copia un ejemplo donde port y targetPort coinciden, y asume que siempre tienen que ser iguales — para después confundirse cuando necesita exponer un Service en el puerto 80 (el estándar HTTP, sin necesidad de especificarlo en la URL) mientras el contenedor real escucha en un puerto distinto, como 8080. Cómo detectarlo: si intentas conectar a un Service en un puerto y obtienes "conexión rechazada", a pesar de que el Pod detrás está sano — revisa si targetPort coincide con el puerto real que el contenedor expone (containerPort en el Deployment), no con el puerto en el que le hablas al Service. Cómo corregirlo: la lección 7 de este módulo usa exactamente este caso —port: 80, targetPort: 8080— para que la distinción quede instalada con un ejemplo real, no solo en teoría.

Esperar que LoadBalancer funcione en kind igual que en un clúster de nube real (de plataforma, un malentendido común en quien aprende con kind por primera vez). Qué pasa: alguien declara un Service de tipo LoadBalancer, esperando obtener una IP externa asignada automáticamente, como pasaría en EKS, GKE o AKS — y en kind esa IP queda permanentemente en estado <pending>, sin resolverse nunca. Por qué pasa: LoadBalancer depende de que el proveedor de nube subyacente implemente la lógica de aprovisionar un balanceador real; kind corre sobre Docker en tu máquina, sin ningún proveedor de nube que cumpla ese contrato. Cómo detectarlo: kubectl get service muestra EXTERNAL-IP: <pending> indefinidamente para un Service de tipo LoadBalancer. Cómo corregirlo: esta guía nunca usa LoadBalancer contra kind por esta razón exacta — el Módulo 4 resuelve el mismo problema (tráfico externo hacia el clúster) con Ingress + ingress-nginx, un patrón que sí funciona completo en kind, y el Módulo 7 explica, con honestidad completa, qué cambia cuando el clúster es EKS real y LoadBalancer sí aprovisiona un balanceador ALB de verdad.


Ejercicios

Ejercicio 1 — Explica el mecanismo de selección sin el diagrama. Sin volver a la sección correspondiente, explica en tus propias palabras cómo un Service sabe a qué Pods debe enrutar tráfico, y qué pasa automáticamente cuando un Pod con la etiqueta correcta nace o muere.

Ver solución

Un Service no apunta a Pods por nombre — apunta a cualquier Pod cuya etiqueta coincida con su selector. Kubernetes mantiene, de forma automática y continua, una lista de las IPs de todos los Pods que en ese momento tienen esa etiqueta (el objeto Endpoints/EndpointSlice); cuando un Pod nuevo nace con la etiqueta correcta, se agrega solo a esa lista; cuando un Pod con esa etiqueta muere, se quita solo. El Service, mientras tanto, nunca cambia su propia dirección — solo cambia la lista de Pods a los que enruta por debajo.

Ejercicio 2 — Elige el tipo correcto. Para cada uno de los siguientes tres casos, indica qué tipo de Service (ClusterIP, NodePort o LoadBalancer) sería el más apropiado, y por qué: (a) un servicio interno que solo otro Pod del mismo clúster necesita alcanzar; (b) un servicio de laboratorio que necesitas probar rápido desde tu máquina, sin instalar ningún controlador adicional; (c) un servicio de producción en EKS real que debe recibir tráfico directo de internet.

Ver solución

(a) ClusterIP — es exactamente el caso para el que existe: alcanzable solo dentro del clúster, sin exponer nada hacia afuera innecesariamente. (b) NodePort — abre un puerto fijo en cada nodo, alcanzable directo desde fuera sin necesitar Ingress ni un balanceador externo, aunque con las limitaciones de seguridad y de rango de puertos que lo hacen poco común en producción real. (c) LoadBalancer — en un proveedor de nube real como AWS, este tipo le pide al proveedor que aprovisione un balanceador real (un ALB o NLB, tema del Módulo 7) con IP pública propia, el patrón esperado para tráfico externo de producción.

Ejercicio 3 — Diagnostica un Service sin tráfico. Un Service con selector: app=my-api no está enrutando ningún tráfico, aunque hay Pods corriendo y sanos en el namespace. Revisas el Deployment y ves que sus Pods tienen la etiqueta app: myapi (sin guion). ¿Cuál es el problema, y cómo lo confirmarías con un comando que ya conoces de este módulo?

Ver solución

El problema es que las etiquetas no coinciden exactamente: el Service busca app=my-api (con guion), pero los Pods tienen app=myapi (sin guion) — para Kubernetes, estas son dos etiquetas completamente distintas, aunque a simple vista se parezcan. El Service existe, está sano, pero no encuentra ningún Pod que coincida, así que no tiene a dónde enrutar. Se confirmaría con kubectl get endpoints <nombre-del-service> -n <namespace> (mencionado en esta lección, usado con evidencia real en la lección 7) — si la lista de ENDPOINTS aparece vacía, es la confirmación directa de que el selector no está encontrando ningún Pod.


Resumen y siguiente paso

En esta lección entendiste el problema exacto que resuelve un Service: las IPs de Pod son efímeras, cambian cada vez que un Pod se recrea, así que necesitas una dirección estable que no dependa de ningún Pod individual. Viste cómo un Service encuentra sus Pods por etiqueta —el mismo mecanismo de metadata.labels de toda esta guía—, los cuatro tipos principales (ClusterIP, NodePort, LoadBalancer, ExternalName) y cuándo usar cada uno, y por qué esta guía usa ClusterIP de forma consistente, incluso después de que Ingress entre en escena en el Módulo 4.

Antes de avanzar deberías poder: explicar por qué un cliente nunca debería depender de la IP de un Pod directamente; distinguir port de targetPort sin confundirlos; y justificar por qué LoadBalancer no funciona en kind de la misma forma que en un clúster de nube real.

Siguiente lección: manos a la obra, exponiendo status-api-service. Ahí declaras el Service real, con el mismo nombre que aws-serverless-and-containers-guide dejó documentado en ECS, y le haces curl de verdad — la primera vez, en toda esta guía, que hablas con andes-cargo-status-api a través de una dirección que no depende de ningún Pod específico.

Recursos

  1. Kubernetes — Service — la referencia oficial completa, incluidos los cuatro tipos desarrollados en esta lección.
  2. Kubernetes — DNS for Services and Pods — referencia oficial del patrón de nombres <service>.<namespace>.svc.cluster.local, mencionado en esta lección.
  3. Kubernetes — Connecting Applications with Services — tutorial oficial paso a paso sobre el mismo mecanismo de selección por etiqueta desarrollado aquí.