Introducción
Ya puedes desplegar y solucionar problemas de aplicaciones, pero los clientes todavía necesitan una forma fiable de acceder a ellas. La IP de un Pod es temporal: un Deployment puede reemplazar un Pod en cualquier momento y asignar a la nueva instancia una dirección IP diferente.
Kubernetes resuelve este problema mediante un Service. Un Service selecciona un grupo cambiante de Pods y proporciona a los clientes una identidad de red estable. En este laboratorio seguirás todo el recorrido de la conexión: desde las etiquetas hasta los EndpointSlices, el DNS del clúster y dos tipos de Service: ClusterIP para el acceso dentro del clúster y NodePort para el acceso a través de un nodo.
Este laboratorio se detiene deliberadamente en los Services. Ingress añade enrutamiento HTTP y requiere un controlador independiente; es más fácil comprenderlo después de dominar la selección y la accesibilidad de los Services.
Desplegar los backends del Service
Inicio del entorno: Este laboratorio inicia un clúster de Kubernetes completo. La configuración del plano de control, el nodo y los componentes de red suele tardar 2–3 minutos. Espera pacientemente a que el entorno termine de cargarse antes de comenzar.
En este paso crearás la aplicación replicada que los Services expondrán más adelante. Aquí, un backend es un Pod capaz de recibir tráfico destinado a un Service.
Ve al espacio de trabajo. cd cambia el directorio actual del shell; normalmente no muestra ninguna salida cuando se ejecuta correctamente:
cd /home/labex/project/service-lab
Crea un Deployment con dos réplicas. La etiqueta de Pod app: course-nginx es especialmente importante, ya que los Services la utilizarán como regla de selección.
La sintaxis de shell cat <<'EOF' > filename es un here-document. Todo lo que aparece hasta el EOF de cierre se escribe en el archivo, y > crea o reemplaza ese archivo. Las comillas del primer EOF evitan que el shell expanda accidentalmente el contenido del YAML.
cat <<'EOF' > course-nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: course-nginx
spec:
replicas: 2
selector:
matchLabels:
app: course-nginx
template:
metadata:
labels:
app: course-nginx
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 80
protocol: TCP
EOF
Aplícalo y espera a que estén listos ambos Pods. -f indica a apply qué archivo debe leer; rollout status espera al controlador del Deployment; --timeout=60s limita el tiempo de espera. En el último comando, -l filtra por etiqueta y -o wide añade las columnas de IP del Pod y del nodo:
kubectl apply -f course-nginx-deployment.yaml
kubectl rollout status deployment/course-nginx --timeout=60s
kubectl get pods -l app=course-nginx -o wide
El puerto llamado http documenta el puerto TCP 80 de cada contenedor. Las dos filas deberían mostrar el estado Running y distintas IP de Pod. Esas IP son reales, pero no constituyen direcciones duraderas para los clientes; en los siguientes pasos añadirás delante de ellas una identidad estable mediante un Service.
Conectar las etiquetas con la selección del Service
En este paso inspeccionarás los metadatos que conectan un Service con los Pods. Un Service no selecciona un Deployment por nombre; busca de forma independiente los Pods cuyas etiquetas coinciden con su selector.
Muestra las etiquetas de los Pods. -l app=course-nginx es un selector de etiquetas, mientras que --show-labels añade el conjunto completo de etiquetas como última columna:
kubectl get pods -l app=course-nginx --show-labels
Cada Pod tiene app=course-nginx y un pod-template-hash generado automáticamente. El Service debe seleccionar únicamente la etiqueta estable de la aplicación.
Compara nombres, etiquetas e IP en una vista compacta. -o custom-columns crea una tabla a partir de los campos de objeto seleccionados. Cada encabezado situado antes de : va seguido de la ruta del campo utilizada para esa columna; la barra invertida permite continuar un mismo comando en la línea siguiente:
kubectl get pods -l app=course-nginx \
-o custom-columns='NAME:.metadata.name,LABEL:.metadata.labels.app,IP:.status.podIP,READY:.status.containerStatuses[0].ready'
Los nombres e IP generados distinguen cada Pod; la etiqueta compartida describe su función. Este nivel de indirección permite que el Service siga funcionando cuando se reemplaza un Pod.
Confirma que el selector del Deployment y la etiqueta de la plantilla del Pod coinciden. -o jsonpath='...' extrae únicamente los campos solicitados. El texto situado fuera de las llaves se convierte en una etiqueta, las rutas de campo dentro de las llaves devuelven valores y {"\n"} inserta un salto de línea:
kubectl get deployment course-nginx \
-o jsonpath='Selector: {.spec.selector.matchLabels.app}{"\n"}Pod label: {.spec.template.metadata.labels.app}{"\n"}'
Ambos valores deberían ser course-nginx. Un selector que no coincida dejaría al controlador o al Service desconectado de los Pods previstos.
Crear un Service ClusterIP
En este paso crearás el tipo de Service predeterminado, ClusterIP. Proporciona una IP virtual y un nombre DNS accesibles para las cargas de trabajo que se ejecutan dentro del clúster.
Crea el manifiesto utilizando el mismo patrón de here-document presentado en este laboratorio:
cat <<'EOF' > course-nginx-service.yaml
apiVersion: v1
kind: Service
metadata:
name: course-nginx
spec:
type: ClusterIP
selector:
app: course-nginx
ports:
- name: http
port: 80
targetPort: http
protocol: TCP
EOF
Lee con atención la correspondencia entre puertos:
port: 80es el puerto que los clientes utilizan en el Service.targetPort: httphace referencia al puerto con nombre del contenedor en cada Pod seleccionado.- El selector elige los Pods de backend; no es una dirección de red.
Valida, aplica e inspecciona el Service. --dry-run=client analiza el archivo localmente sin crear nada; al eliminarlo se realiza la aplicación real; después, get service lee el objeto activo:
kubectl apply --dry-run=client -f course-nginx-service.yaml
kubectl apply -f course-nginx-service.yaml
kubectl get service course-nginx
El valor de CLUSTER-IP lo asigna Kubernetes. Permanece estable durante toda la vida útil de este Service, incluso cuando cambian sus Pods de backend.
Seguir el recorrido del Service hasta los EndpointSlices
En este paso seguirás el selector desde el Service hasta las direcciones reales de los backends. Kubernetes registra esas direcciones en objetos EndpointSlice.
Describe el Service. describe amplía un objeto concreto para mostrar su configuración, estado e información relacionada con los endpoints, por lo que resulta útil después de la tabla más resumida de get:
kubectl describe service course-nginx
Busca Selector: app=course-nginx y una línea Endpoints que contenga dos IP de Pod en el puerto 80.
Enumera el EndpointSlice seleccionado mediante la etiqueta del nombre del Service. Este selector -l utiliza la etiqueta añadida automáticamente kubernetes.io/service-name=course-nginx:
kubectl get endpointslices -l kubernetes.io/service-name=course-nginx
Inspecciona sus direcciones y su estado de preparación. Este JSONPath utiliza range para repetir la plantilla incluida por cada endpoint. Imprime la primera dirección, el texto literal ready=, el valor de preparación y un salto de línea:
kubectl get endpointslices -l kubernetes.io/service-name=course-nginx \
-o jsonpath='{range .items[*].endpoints[*]}{.addresses[0]}{" ready="}{.conditions.ready}{"\n"}{end}'
Deberías ver dos direcciones con ready=true. La cadena ya es concreta:
Service selector -> matching Pod labels -> EndpointSlice addresses -> ready Pods
Si existe un Service pero no tiene endpoints, compara primero su selector con las etiquetas y el estado de preparación de los Pods.
Acceder al Service mediante el DNS del clúster
En este paso actuarás como un cliente dentro del clúster. El DNS de Kubernetes permite que un Pod del mismo espacio de nombres utilice el nombre del Service course-nginx sin tener que recordar su IP virtual.
Inicia un Pod temporal de BusyBox, solicita la página de NGINX y elimínalo automáticamente cuando termine. Lee las opciones de arriba abajo:
--imageselecciona la imagen del contenedor y--image-pull-policy=IfNotPresentreutiliza la copia almacenada en caché.--restart=Nevercrea un Pod independiente en lugar de una carga de trabajo gestionada por un controlador.--rmelimina el Pod cuando termina su comando y-imantiene conectada la entrada y salida del comando.- El separador
--marca el final de las opciones de kubectl; todo lo que aparece después es el comando que se ejecutará dentro del contenedor. wget -qO-solicita la URL silenciosamente y escribe el cuerpo de la respuesta en el terminal.
kubectl run service-client \
--image=busybox:1.36 \
--image-pull-policy=IfNotPresent \
--restart=Never \
--rm -i \
-- wget -qO- http://course-nginx
La respuesta contiene la página de bienvenida de NGINX. El tráfico pasó por el Service, no directamente por una IP de Pod concreta.
Ejecuta una comprobación silenciosa del resultado. >/dev/null descarta el cuerpo HTML y && muestra el mensaje únicamente si el comando de solicitud termina correctamente:
kubectl run service-client-check \
--image=busybox:1.36 \
--image-pull-policy=IfNotPresent \
--restart=Never \
--rm -i \
-- wget -qO- http://course-nginx >/dev/null && echo "ClusterIP Service responded"
El mensaje de éxito demuestra tanto la resolución DNS como la accesibilidad HTTP. El Pod cliente es temporal; el Service y sus dos Pods de backend permanecen activos.
Añadir un Service NodePort
En este paso crearás un segundo Service para los mismos Pods utilizando el tipo NodePort. Un NodePort abre un puerto del intervalo predeterminado 30000–32767 en cada nodo y reenvía el tráfico a los backends del Service.
Kubernetes puede leer varios objetos desde un único archivo YAML con varios documentos. Cada objeto conserva su propio apiVersion, kind, metadata y spec; una línea que contiene --- separa un documento YAML del siguiente.
Crea un archivo reutilizable que contenga el Service ClusterIP existente y el nuevo Service NodePort. Repetir la definición de ClusterIP es seguro: aplicar el mismo estado deseado no modifica el objeto.
cat <<'EOF' > course-nginx-services.yaml
apiVersion: v1
kind: Service
metadata:
name: course-nginx
spec:
type: ClusterIP
selector:
app: course-nginx
ports:
- name: http
port: 80
targetPort: http
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: course-nginx-nodeport
spec:
type: NodePort
selector:
app: course-nginx
ports:
- name: http
port: 80
targetPort: http
nodePort: 30080
protocol: TCP
EOF
Valida ambos documentos YAML juntos antes de modificar el clúster. La salida debería mencionar service/course-nginx y service/course-nginx-nodeport, seguidos de (dry run):
kubectl apply --dry-run=client -f course-nginx-services.yaml
Aplícalos e inspecciónalos. El primer comando lee los dos documentos de un único archivo; el Service ClusterIP existente debería aparecer como unchanged, mientras que se crea el Service NodePort. El segundo comando lee el nuevo Service activo:
kubectl apply -f course-nginx-services.yaml
kubectl get service course-nginx-nodeport
La columna PORT(S) muestra 80:30080/TCP: el puerto 80 es el puerto del Service y 30080 es el puerto orientado al nodo. Ambos Services seleccionan los mismos Pods y, por tanto, pueden tener las mismas direcciones de backend.
Comparar los dos límites de acceso
En este paso probarás el NodePort y resumirás cuándo resulta apropiado cada tipo de Service.
Obtén la IP del nodo de Minikube. $(...) es una sustitución de comandos: el shell ejecuta minikube ip y guarda su salida en la variable NODE_IP. -p labex-v135 selecciona el perfil preparado y echo permite inspeccionar el valor almacenado:
NODE_IP=$(minikube ip -p labex-v135)
echo "$NODE_IP"
Solicita la aplicación a través del puerto orientado al nodo. Es posible que Kubernetes necesite unos segundos para configurar la nueva regla de red a nivel del nodo después de aceptar el Service. Las opciones de reintento hacen que curl espere durante ese breve periodo de convergencia en lugar de fallar ante la primera conexión rechazada:
curl -s --retry 5 --retry-connrefused --retry-delay 2 "http://${NODE_IP}:30080" | grep 'Welcome to nginx'
Aquí, -s oculta el indicador de progreso, --retry 5 permite hasta cinco reintentos, --retry-connrefused trata una conexión rechazada al principio como un error recuperable y --retry-delay 2 espera dos segundos entre intentos. Las comillas dobles permiten expandir ${NODE_IP} dentro de la URL. La tubería | pasa el HTML devuelto a grep, que imprime la línea de bienvenida coincidente como evidencia.
El título HTML coincidente demuestra que la solicitud llegó a un Pod de backend. Compara los dos recorridos que has creado:
in-cluster Pod -> course-nginx:80 -> ready backend Pod
VM/node client -> NODE_IP:30080 -> course-nginx-nodeport:80 -> ready backend Pod
Inspecciona ambos Services juntos:
kubectl get services course-nginx course-nginx-nodeport
Utiliza ClusterIP para la comunicación estable dentro del clúster; es el valor predeterminado y la base habitual de otros mecanismos de exposición. NodePort añade un punto de entrada a nivel de nodo y resulta útil para el aprendizaje, el desarrollo o la integración con balanceadores de carga externos. Ambos dependen de selectores correctos y de EndpointSlices preparados.
Aplicarás estas ideas de forma independiente en el siguiente desafío, donde expondrás más de una carga de trabajo web.
Resumen
Has creado una identidad de red estable delante de Pods reemplazables. Has conectado los selectores del Service con las etiquetas de los Pods, seguido los backends seleccionados mediante EndpointSlices, accedido a un Service ClusterIP a través del DNS del clúster y añadido un punto de entrada NodePort mediante el nodo.
El modelo mental clave es que un Service no es la aplicación ni contiene Pods. Representa continuamente un conjunto seleccionado de backends preparados. Cuando falle la conectividad, sigue la cadena en orden: puertos del Service, selector, etiquetas de los Pods, EndpointSlices, estado de preparación de los backends y, por último, el límite de acceso del cliente.


