Misión: restablecer las comunicaciones del cluster
20 min
Proyecto 12 — Los Services Kubernetes · Nivel intermedio → avanzado · Duración estimada: 3 a 5 h
Todo el código de las aplicaciones te lo dan en anexo de este documento. Tu trabajo: escribir, determinar y completar los Services que faltan — es decir, hacer comunicar un sistema que, tal como está, está totalmente mudo.
Un equipo ha desplegado una pequeña plataforma de comercio en línea en Kubernetes. Las imágenes están construidas, los Deployments y el StatefulSet corren, todos los Pods están Running.
Y sin embargo, nada funciona.
El portal muestra un tablero enteramente rojo: no alcanza ningún componente. La base de datos es inalcanzable. La caché es invisible. Ninguna página es accesible desde el navegador.
La razón es simple: la persona que debía escribir los Services se fue sin entregarlos.
Recordatorio fundamental que este proyecto te va a hacer vivir: unos Pods que corren no constituyen una aplicación. Sin Services, son islas aisladas, sin dirección estable ni nombre, incapaces de encontrarse unas a otras.
Tu misión: restablecer todas las comunicaciones, únicamente escribiendo los Services correctos.
Conceptos esenciales antes de empezar
Esta sección es un mini-manual autosuficiente: contiene todo el vocabulario necesario para las misiones. Léela una vez y vuelve a ella cuando se te escape una palabra.
1. El problema que resuelve un Service
Un Pod es mortal: Kubernetes puede eliminarlo, moverlo, recrearlo — y su nueva IP será distinta. Por tanto nunca se conecta uno a un Pod por su IP.
Un Service es un objeto estable — nombre, IP, puerto — que sigue a los Pods allá donde vayan. El mecanismo es simple:
Lo que hace el enlace entre un Service y «sus» Pods es el selector de labels:
yaml
spec: selector: app: api-produits # todo Pod con ESTE label es alcanzado
La lista de Pods que coinciden forma el objeto Endpoints — es tu radiografía del Service.
powershell
kubectl get endpoints api-produits# api-produits 10.244.0.3:8000,10.244.0.4:8000,10.244.0.5:8000
Si Endpoints está vacío, el selector no coincide con ningún Pod: casi siempre es una errata en un label.
2. Los cinco tipos de Services (los únicos que necesitas)
Un ClusterIP sin IP virtual: devuelve la lista de IP de los Pods, más un nombre DNS por Pod
No
bd-interne
ExternalName
Alias DNS hacia un nombre externo. Ningún Pod, ningún selector.
No
paiement-externe
Punto importante: un Service que «no funciona» casi nunca es un problema de tipo. Casi siempre es selector o puerto.
3. DNS interno: las reglas que dan la ilusión de magia
En el clúster, CoreDNS fabrica automáticamente nombres según reglas fijas:
Tú llamas
CoreDNS resuelve hacia
api-produits
el Service api-produits del mismo namespace
api-produits.default
el Service api-produits del namespace default
api-produits.default.svc.cluster.local
forma larga y plenamente cualificada
Corolario capital: el nombre del Service es el nombre que llama la aplicación. Un Service llamado notificationno responde a http://notifications. Esta trampa está en el corazón de la misión 6.
4. StatefulSet y Service headless: el tándem
Un Deployment trata sus réplicas como gemelas intercambiables (web-abc123-x7k9, web-abc123-p2m1…). Perfecto para web sin estado.
Un StatefulSet produce al contrario Pods numerados y estables: bd-0, bd-1, bd-2. Cada Pod conserva su identidad a través de los reinicios — imprescindible para una base de datos donde hay que designar precisamente la primaria.
Pero un StatefulSet no basta solo: exige estar asociado a un Service headless cuyo nombre indica en su campo serviceName.
yaml
kind: StatefulSetspec: serviceName: bd-interne # <-- apunta a un Service headless del mismo nombre replicas: 3
Ese Service headless da entonces un nombre DNS por Pod:
bd-0.bd-interne -> IP del Pod bd-0 ÚNICAMENTEbd-1.bd-interne -> IP del Pod bd-1 ÚNICAMENTEbd-interne -> IPs de los tres Pods (lista)
Sin la palabra None en spec.clusterIP, ninguno de esos nombres existe.
yaml
spec: clusterIP: None # transforma el Service en "headless"
Retén esto: para alcanzar bd-0.bd-interne, hacen falta dos condiciones simultáneas — un StatefulSet cuyo serviceName: bd-interne, y un Service headless llamado bd-interne. Si falta una, el nombre individual no existe.
5. Puertos nombrados y Services multi-puerto
Cuando un Service expone varios puertos, cada entrada se vuelve obligatoriamente nombrada:
Utilidad: tu código conserva el mismo nombre interno (paiement-externe) se trate de un service en el clúster, de un service SaaS externo o de un traslado de API. Se cambia el Service, no el código.
7. Las tres preguntas que desbloquean el 90 % de las averías
Cada vez que un Service no funciona, haz estas tres preguntas en este orden:
Es exactamente el método de la misión 6.
Lo que sabrás hacer al final
Distinguir los cinco tipos de Services y saber cuándo usar cada uno.
Usar kubectl get svc, kubectl describe svc, kubectl get endpoints como tres herramientas complementarias.
Acoplar correctamente un StatefulSet con un Service headless.
Escribir un Service multi-puerto limpio, con targetPort referenciado por nombre.
Diagnosticar las tres averías más frecuentes en empresa (label erróneo, puerto erróneo, nombre erróneo).
La arquitectura a poner en servicio
Siete componentes ya corren. Ninguno es alcanzable.
Componente
Puerto(s) del contenedor
Label de los Pods
Controlador
portail
5000
app: portail
Deployment (1 réplica)
api-produits
8000
app: api-produits
Deployment (3 réplicas)
api-commandes
8000
app: api-commandes
Deployment (2 réplicas)
cache
6379
app: cache
Deployment (1 réplica)
notifications
7000
app: notifications
Deployment (2 réplicas)
metriques
8080 (nombrado web) y 9090 (nombrado prom)
app: metriques
Deployment (2 réplicas)
bd
5432
app: bd
StatefulSet (3 réplicas)
Disposición de los archivos
Crea exactamente este árbol, copiando el contenido de los anexos. Cada anexo indica la ruta exacta del archivo a crear.
projet12-mission-services/├── 00-ENONCE.md <- este documento│├── apps/ <- EL CÓDIGO (ANEXO A) — NO MODIFICAR│ ├── micro/│ │ ├── app.py│ │ ├── requirements.txt│ │ └── Dockerfile│ ├── metriques/│ │ ├── app.py│ │ ├── requirements.txt│ │ └── Dockerfile│ └── portail/│ ├── app.py│ ├── requirements.txt│ └── Dockerfile│├── k8s/│ ├── 01-deployments.yaml <- SUMINISTRADO (ANEXO B) — NO MODIFICAR│ ├── 02-statefulset-bd.yaml <- SUMINISTRADO (ANEXO B) — NO MODIFICAR│ ││ └── services/ <- TE TOCA A TI│ ├── 01-api-produits.yaml <- esqueleto a completar (ANEXO C)│ ├── 02-portail.yaml <- esqueleto a completar (ANEXO C)│ ├── 03-bd-interne.yaml <- esqueleto a completar (ANEXO C)│ ├── 04-metriques.yaml <- esqueleto a completar (ANEXO C)│ ├── 05-paiement-externe.yaml <- esqueleto a completar (ANEXO C)│ ││ └── 06-casses/ <- SUMINISTRADOS pero DEFECTUOSOS (ANEXO D)│ ├── casse-1.yaml│ ├── casse-2.yaml│ └── casse-3.yaml│├── outils/│ └── valider.ps1 <- SUMINISTRADO (ANEXO E)│└── RAPPORT.md <- A REDACTAR por ti
Solo tres imágenes hacen falta: micro:1.0 sirve a cinco componentes distintos (el comportamiento cambia por variables de entorno), metriques:1.0 expone dos puertos, y portail:1.0 muestra el tablero.
El tablero: tu indicador de progreso
El portal interroga en continuo cada componente y muestra una baldosa por enlace, refrescada cada 3 segundos:
Baldosa
Significado
Dónde buscar el error
ROJO
El nombre DNS no existe
El Service no se ha creado, o su nombre es erróneo
NARANJA
El nombre se resuelve, pero nadie responde
El Service existe, pero su selector o su puerto es erróneo
VERDE
Comunicación establecida
Tu Service es correcto
Objetivo final: las 8 baldosas en verde, y el contador que muestra 8 / 8.
Esta distinción rojo/naranja no es decorativa: te dice de qué lado buscar. Rojo = el Service no existe (nada que depurar, hay que escribirlo). Naranja = el Service existe pero no encuentra sus Pods o pega en el puerto incorrecto.
Prohibición absoluta de modificar la carpeta apps/, así como 01-deployments.yaml y 02-statefulset-bd.yaml.
(Toda la dificultad consiste en adaptarse a lo existente: es exactamente la situación de un puesto de trabajo real.)
Solo creas y modificas archivos situados en k8s/services/.
Ninguna dirección IP a pelo. Todo debe apoyarse en los nombres DNS y los selectores de labels.
Debes determinar tú mismo el tipo de cada Service: nada te dice si es un ClusterIP, un NodePort, un LoadBalancer, un service headless o un ExternalName. Es el corazón de la evaluación.
Los nombres de los Services están impuestos: el código de las aplicaciones los llama tal cual. Un nombre erróneo da una baldosa roja.
Trabajas sobre el Kubernetes integrado en Docker Desktop (Settings → Kubernetes → Enable Kubernetes).
Preparación
Requisitos previos — a verificar una sola vez
Docker Desktop está arrancado (icono verde en la barra del sistema).
Kubernetes está activado en Docker Desktop: Settings → Kubernetes → Enable Kubernetes → Apply & Restart. Sin esa casilla marcada, ningún comando kubectl funcionará.
Docker Desktop dispone de al menos 4 GB de RAM: Settings → Resources → Memory ≥ 4 GB. Este proyecto lanza 14 Pods; con 2 GB la máquina se ahoga y algunos Pods se quedan en Pending.
Tienes Internet (para la misión 5 y para descargar la imagen busybox).
Secuencia de puesta en marcha
powershell
# 0) Situarse en el clúster correcto (imprescindible si minikube o kind ya se usaron)kubectl config use-context docker-desktopkubectl get nodes # debe mostrar docker-desktop Ready# 1) Construir las tres imágenesdocker build -t micro:1.0 ./apps/microdocker build -t metriques:1.0 ./apps/metriquesdocker build -t portail:1.0 ./apps/portail# 2) Desplegar la base suministrada (Pods, y NINGÚN Service)kubectl apply -f k8s/01-deployments.yamlkubectl apply -f k8s/02-statefulset-bd.yaml# 3) Esperar a que TODOS los Pods estén Ready (unos 30 s)kubectl wait --for=condition=ready pod --all --timeout=180s# 4) Constatar la situación de partidakubectl get pods # 14 Pods, todos Runningkubectl get svc # solo "kubernetes": ninguno de tus Services
En este punto: todos los Pods corren y nada comunica. Es el punto de partida normal.
Dos avisos técnicos que hay que conocer ya — no es un error tuyo:
Warning Endpoints is deprecated in v1.33+ — Kubernetes muestra sistemáticamente este mensaje en cada kubectl get endpoints. El comando sigue funcionando perfectamente, ignora el warning. La API nueva equivalente es kubectl get endpointslices, pero todos los comandos de este TP usan a propósito endpoints, más legible para aprender.
El tablero puede tardar 10 a 15 segundos en mostrarse la primera vez: el portal prueba 8 enlaces de red en cada visualización, cada uno con un tiempo de espera de 1,5 s. Cuando aún no funciona nada, espera cada plazo antes de mostrar rojo o naranja. Una vez los Services correctos, el tiempo de respuesta baja a unos cientos de milisegundos.
Pregunta que debes hacerte de inmediato: ¿cómo vas a ver siquiera el tablero, si aún no existe ninguna puerta de entrada?
Chaleco salvavidas:kubectl port-forward funciona sin ningún Service, directamente sobre un Pod.
powershell
kubectl port-forward deploy/portail 5000:5000
Luego abre http://localhost:5000. Verás el tablero todo rojo, con la baldosa «Acceso externo» en naranja (normal: no has pasado por el puerto 30500).
Las misiones
Misión 1 — Hacer hablar al portal con la API de productos (15 puntos)
El portal llama a http://api-produits en el puerto 80. Los Pods de la API escuchan en el puerto 8000 y llevan el label app: api-produits.
Archivo a completar:k8s/services/01-api-produits.yaml
A determinar: el tipo de Service, el selector, así como port y targetPort.
Validación:
powershell
kubectl apply -f k8s/services/01-api-produits.yamlkubectl get svc api-produitskubectl get endpoints api-produits # debe listar 3 direcciones IP
La baldosa API Productos pasa a verde.
Misión 2 — Abrir la puerta de entrada (15 puntos)
El tablero debe ser accesible desde tu navegador en la dirección exacta http://localhost:30500. Los Pods del portal escuchan en el puerto 5000.
Archivo a completar:k8s/services/02-portail.yaml
A determinar: ¿qué tipo de Service expone una aplicación fuera del clúster en un puerto fijo de la máquina? ¿Cuál es el rango de puertos permitido para ese campo?
Validación:
powershell
kubectl get svc portail # PORT(S) debe mostrar 80:30500/TCPstart http://localhost:30500
La baldosa Acceso externo pasa a verde.
Pregunta a tratar en el informe: otro tipo de Service también habría hecho el portal accesible desde el navegador en Docker Desktop. ¿Cuál? ¿Y qué diferencia haría en producción en la nube?
Misión 3 — Dar una identidad a cada base de datos (20 puntos)
El StatefulSet bd proporciona 3 réplicas. El portal debe alcanzar precisamente la primera (la primaria), en la dirección:
bd-0.bd-interne
Los Pods de la base llevan el label app: bd y escuchan en el puerto 5432.
Archivo a completar:k8s/services/03-bd-interne.yaml
A determinar: ¿qué tipo de Service da un nombre DNS individual a cada Pod, en vez de una única IP virtual? ¿Qué campo hay que escribir, y con qué valor particular?
Validación:
powershell
# 1) Desde un Pod utilitario, alcanzar directamente bd-0:kubectl run test --rm -i --restart=Never --image=busybox:1.36 -- wget -qO- http://bd-0.bd-interne:5432/ping# debe responder : {"pod":"bd-0","port":5432,"service":"base-de-donnees"}# 2) Verificar las entradas DNS con el nombre plenamente cualificado# (busybox nslookup NO aplica los search domains, hay que dar el FQDN):kubectl run test --rm -i --restart=Never --image=busybox:1.36 -- nslookup bd-0.bd-interne.default.svc.cluster.local# debe devolver UNA sola dirección (la del Pod bd-0)kubectl run test --rm -i --restart=Never --image=busybox:1.36 -- nslookup bd-interne.default.svc.cluster.local# debe devolver TRES direcciones (una por Pod del StatefulSet)
Trampa que no hay que perderse
examina el campo serviceName del StatefulSet suministrado. El nombre de tu Service debe coincidir exactamente, si no los nombres individuales de los Pods nunca se crearán.
Trampa técnica (busybox)
nslookup nom-court no funciona desde un Pod busybox porque su resolver no usa los search domains de /etc/resolv.conf. Desde un Pod de aplicación real (como portail), en cambio, bd-0.bd-interneresuelve perfectamente. Usa por tanto wget para probar la cadena de aplicación real, y el FQDN para despejar cualquier ambigüedad DNS.
Misión 4 — Exponer dos puertos en un mismo Service (15 puntos)
El componente metriques escucha en dos puertos:
Uso
Puerto del contenedor
Nombre del puerto declarado en el Deployment
Interfaz web
8080
web
Métricas
9090
prom
El portal llama a http://metriques (puerto 80) yhttp://metriques:9090/metrics.
Archivo a completar:k8s/services/04-metriques.yaml
A determinar: ¿cómo declarar varios puertos en un Service? ¿Qué restricción se vuelve entonces obligatoria para cada entrada? ¿Y cómo hacer que targetPort apunte a un puerto del contenedor por su nombre en vez de por su número, para que el Service siga válido aunque el número cambie?
Validación:
powershell
kubectl describe svc metriques # deben aparecer los DOS puertos
Misión 5 — Dar un nombre interno a un service externo (10 puntos)
El portal debe alcanzar un service de pago alojado fuera del clúster, pero el código llama a un nombre interno: paiement-externe. Ese nombre debe remitir a example.com.
Archivo a completar:k8s/services/05-paiement-externe.yaml
A determinar: ¿qué tipo de Service crea un simple alias DNS hacia un nombre externo, sin selector y sin ningún Pod?
Validación:
powershell
kubectl run test --rm -i --restart=Never --image=busybox:1.36 -- nslookup paiement-externe.default.svc.cluster.local# debe mostrar:# paiement-externe.default.svc.cluster.local canonical name = example.com# Name: example.com# Address: <IP publique>
Esta misión exige que el clúster pueda resolver nombres públicos. Si no tienes ningún acceso a Internet, sustituye el destino por api-produits.default.svc.cluster.local y señálalo en tu informe.
Misión 6 — La investigación: reparar tres Services defectuosos (20 puntos)
La carpeta k8s/services/06-casses/ contiene tres Services ya escritos… que no funcionan. Cada uno tiene un solo error, y son las tres faltas más frecuentes en empresa.
powershell
kubectl apply -f k8s/services/06-casses/
Archivo
Síntoma observado
casse-1.yaml
El Service existe, pero kubectl get endpoints devuelve <none>
casse-2.yaml
Los Endpoints están bien rellenos, pero toda conexión es rechazada
casse-3.yaml
El Service parece perfecto, pero el portal nunca lo alcanza
Para cada uno de los tres casos, tu informe debe contener:
el comando de diagnóstico que te puso en la pista;
la causa exacta de la avería;
el correctivo aplicado;
la prueba de que el enlace funciona (baldosa verde + salida de comando).
Método aconsejado: procede como un investigador. kubectl describe svc, kubectl get endpoints, kubectl get pods --show-labels, luego compara línea a línea el Service y los Pods. La diferencia entre «Endpoints vacíos» y «conexión rechazada» te indica ya de qué lado buscar.
Misión 7 — Bonus: el gran salto (5 puntos)
A elegir, uno solo basta:
a) Hacer que un mismo cliente sea siempre servido por el mismo Pod de la API de productos. (Pista: un campo del Service permite una «adherencia» basada en la IP del cliente.)
b) Crear un Service sin selector que apunte a una dirección IP externa fija, escribiendo tú mismo sus Endpoints.
c) Escribir un Service de tipo LoadBalancer para el portal y luego explicar qué se convierte EXTERNAL-IP en Docker Desktop, y qué se convertiría en AWS.
Validación automática
Un script te da tu puntuación en cualquier momento:
powershell
.\outils\valider.ps1
Si PowerShell se niega a ejecutar el script con un mensaje del tipo l'exécution de scripts est désactivée sur ce système, salta la restricción solo para este comando:
El script no da ninguna solución: solo indica lo que falla y dónde mirar.
Entregables
La carpeta k8s/services/ completa: tus 5 Services escritos y los 3 Services reparados.
Un RAPPORT.md que contenga:
para cada Service: el tipo elegido y una justificación en dos frases («por qué este y no otro»);
la investigación completa de la misión 6 (comando → causa → correctivo → prueba);
una captura del tablero que muestre 8 / 8;
una captura de kubectl get svc que muestre todos tus Services y sus tipos;
tus respuestas a las preguntas de reflexión.
La salida final de .\outils\valider.ps1.
Preguntas de reflexión
¿Por qué la aplicación no podía en absoluto funcionar sin Services, aunque todos los Pods estuvieran Running?
¿Cuál es la diferencia concreta entre una baldosa roja y una baldosa naranja? ¿Qué te enseña cada una sobre el lugar del error?
¿Por qué el Service de la base de datos debe ser headless, mientras que un Service ordinario basta para la API de productos?
¿Qué contiene exactamente la lista de Endpoints, y quién la actualiza? ¿Qué ocurre cuando un Pod pasa a NotReady?
Eliminas un Pod de la API de productos; Kubernetes crea otro con una dirección IP distinta. ¿Por qué el portal sigue funcionando sin la menor modificación?
En producción, ¿expondrías diez aplicaciones con diez Services de tipo LoadBalancer? Justifica y propone una alternativa.
Baremo
Elemento
Puntos
Misión 1 — Service interno y descubrimiento DNS
15
Misión 2 — Exposición externa en el puerto 30500
15
Misión 3 — Service headless e identidades estables
20
Misión 4 — Multi-puerto y puertos nombrados
15
Misión 5 — Alias hacia un service externo
10
Misión 6 — Diagnóstico y reparación (3 averías)
20
Calidad del informe y justificación de las elecciones
5
Bonus — Misión 7
+5
Total
100 (+5)
Penalizaciones: −10 por modificación de un archivo prohibido (apps/, 01-deployments.yaml, 02-statefulset-bd.yaml); −5 por dirección IP codificada a pelo.
Criterios de éxito
Criterio
Esperado
Tablero
8 / 8 baldosas verdes
Tipos de Services
Cada uno adaptado a su uso y justificado
DNS individual
bd-0.bd-interne resuelto hacia un solo Pod
Multi-puerto
Los dos puertos visibles, targetPort referenciado por nombre
Investigación
Las 3 averías identificadas, explicadas y corregidas
Resiliencia
Tras eliminar un Pod, el portal sigue funcionando
Ninguna IP a pelo
Únicamente nombres DNS y selectores de labels
Caja de herramientas
Ninguna solución aquí — solo pistas.
powershell
kubectl get svc # tipos, IP, puertoskubectl describe svc <nom> # detalles + Endpointskubectl get endpoints <nom> # ¿QUIÉN está detrás del Service?kubectl get pods --show-labels # los labels reales de los Podskubectl get pods -l app=<valeur> # probar un selectorkubectl port-forward deploy/portail 5000:5000 # acceder a un Pod SIN Servicekubectl run test --rm -i --restart=Never --image=busybox:1.36 -- wget -qO- http://<nom>/pingkubectl run test --rm -i --restart=Never --image=busybox:1.36 -- nslookup <nom>.default.svc.cluster.localkubectl logs -l app=portail --tail=30 # lo que el portal no consigue alcanzarkubectl delete svc <nom> # volver a cero en un Service
Trampa a conocer con kubectl run test: si encadenas varios comandos rápido, el Pod anterior no siempre se elimina a tiempo y obtendrás:
Error from server (AlreadyExists): pods "test" already exists
Dos soluciones: cambiar el nombre (test1, test2…) en cada comando, o limpiar antes:
powershell
kubectl delete pod test --ignore-not-found; kubectl run test --rm -i --restart=Never ...
Las tres preguntas que desbloquean el 90 % de las situaciones:
¿El Service existe, con el nombre correcto? (si no → baldosa roja: no hay nada que depurar, hay que escribirlo)
¿Los Endpoints están rellenos? (vacíos → el selector no coincide con ningún label de Pod)
¿El targetPort corresponde al puerto realmente escuchado por el contenedor? (si no → conexión rechazada)
ANEXO A — Las aplicaciones
No modifiques ninguno de estos archivos. Cópialos tal cual en las rutas indicadas.
A.1 — El microservicio genérico
Esta única aplicación sirve a cinco componentes (api-produits, api-commandes, cache, notifications, bd). Su nombre y su puerto los fijan variables de entorno.
Archivo: apps/micro/app.py
python
"""Microservicio genérico de demostración.La misma imagen sirve a varios componentes: el nombre y el puerto de escuchalos aportan variables de entorno (APP_NAME, PORT).Cada respuesta contiene el nombre del Pod, lo que hace visible elreparto de carga que realiza un Service."""import osimport socketfrom flask import Flask, jsonifyapp = Flask(__name__)NOM = os.environ.get("APP_NAME", "micro")PORT = int(os.environ.get("PORT", "8000"))@app.route("/")@app.route("/ping")def ping(): return jsonify(service=NOM, pod=socket.gethostname(), port=PORT)@app.route("/health")def health(): return "OK", 200if __name__ == "__main__": app.run(host="0.0.0.0", port=PORT)
# ---------------------------------------------------------------------------# LA BASE DE DATOS (3 réplicas) — SUMINISTRADO, NO MODIFICAR## ATENCIÓN: el campo serviceName de abajo impone el NOMBRE del Service que# deberás escribir para que bd-0, bd-1 y bd-2 obtengan cada uno un nombre DNS.# ---------------------------------------------------------------------------apiVersion: apps/v1kind: StatefulSetmetadata: name: bdspec: serviceName: bd-interne # <-- lee bien esta línea replicas: 3 selector: matchLabels: app: bd template: metadata: labels: app: bd spec: containers: - name: micro image: micro:1.0 imagePullPolicy: IfNotPresent env: - { name: APP_NAME, value: "base-de-donnees" } - { name: PORT, value: "5432" } ports: - containerPort: 5432 readinessProbe: httpGet: { path: /health, port: 5432 } initialDelaySeconds: 3 periodSeconds: 5
ANEXO C — Los esqueletos de Services a completar
Copia estos cinco archivos y luego sustituye cada TODO por el valor correcto.
Las líneas precedidas de # ? son preguntas a resolver: te toca decidir si hay que añadir, modificar o suprimir la línea correspondiente.
Archivo: k8s/services/01-api-produits.yaml
yaml
# MISIÓN 1 — Hacer que la API productos sea accesible desde el portail.## El portail llama a: http://api-produits (luego el puerto 80)# Los Pods escuchan en: 8000# Los Pods llevan el label: app: api-produits## ? ¿Qué tipo de Service para una comunicación INTERNA al clúster?apiVersion: v1kind: Servicemetadata: name: api-produits # nombre IMPUESTO: no cambiarspec: type: TODO selector: TODO: TODO ports: - port: TODO # el puerto por el que llaman los clientes targetPort: TODO # el puerto que realmente escucha el contenedor
Archivo: k8s/services/02-portail.yaml
yaml
# MISIÓN 2 — Hacer accesible el tablero desde el navegador,# en la dirección exacta: http://localhost:30500## Los Pods escuchan en: 5000# Los Pods llevan el label: app: portail## ? ¿Qué tipo de Service abre un puerto en la MÁQUINA?# ? ¿Cuál es el rango autorizado para ese puerto?# ? ¿Qué campo extra hay que añadir para imponer el puerto 30500?apiVersion: v1kind: Servicemetadata: name: portail # nombre IMPUESTO: no cambiarspec: type: TODO selector: TODO: TODO ports: - port: TODO targetPort: TODO # ? aquí falta una línea
Archivo: k8s/services/03-bd-interne.yaml
yaml
# MISIÓN 3 — Dar un nombre DNS INDIVIDUAL a cada réplica de la base,# para poder contactar exactamente: bd-0.bd-interne## Los Pods escuchan en: 5432# Los Pods llevan el label: app: bd## ? ¿Qué tipo de Service NO tiene una IP virtual única?# ? ¿Qué campo, con qué valor muy particular, produce ese efecto?# ? El nombre de abajo debe coincidir con qué campo del StatefulSet?apiVersion: v1kind: Servicemetadata: name: bd-interne # nombre IMPUESTO: no cambiarspec: # ? aquí falta una línea esencial selector: TODO: TODO ports: - port: TODO targetPort: TODO
Archivo: k8s/services/04-metriques.yaml
yaml
# MISIÓN 4 — Exponer DOS puertos en un solo y mismo Service.## El portail llama a: http://metriques (puerto 80)# y a: http://metriques:9090/metrics## Los Pods escuchan en: 8080 (puerto nombrado "web") y 9090 (puerto nombrado "prom")# Los Pods llevan el label: app: metriques## ? ¿Qué restricción se vuelve OBLIGATORIA en cuanto un Service expone varios puertos?# ? ¿Cómo hacer que targetPort apunte a un puerto del contenedor POR SU NOMBRE?apiVersion: v1kind: Servicemetadata: name: metriques # nombre IMPUESTO: no cambiarspec: type: TODO selector: TODO: TODO ports: - TODO: TODO # ? falta un campo obligatorio en cada entrada port: TODO targetPort: TODO - TODO: TODO port: TODO targetPort: TODO
Archivo: k8s/services/05-paiement-externe.yaml
yaml
# MISIÓN 5 — Hacer que un nombre INTERNO apunte a un servicio EXTERNO.## El portail usa el nombre: paiement-externe# Ese nombre debe resolver hacia: example.com## ? ¿Qué tipo de Service crea un simple alias DNS (CNAME)?# ? ¿Este tipo tiene un selector? ¿puertos? ¿Pods?apiVersion: v1kind: Servicemetadata: name: paiement-externe # nombre IMPUESTO: no cambiarspec: type: TODO TODO: TODO # ? el campo que indica el destino externo
ANEXO D — Los tres Services defectuosos
Copia estos tres archivos tal cual, aplícalos y luego diagnostica y corrige.
Cada uno contiene exactamente un error. No reescribas el archivo desde cero: encuentra la falta.
Archivo: k8s/services/06-casses/casse-1.yaml
yaml
# AVERÍA 1# Síntoma: el Service existe, pero "kubectl get endpoints api-commandes"# devuelve <none>. El portail muestra una baldosa ORANGE.apiVersion: v1kind: Servicemetadata: name: api-commandesspec: type: ClusterIP selector: app: api-commande ports: - port: 80 targetPort: 8000
Archivo: k8s/services/06-casses/casse-2.yaml
yaml
# AVERÍA 2# Síntoma: "kubectl get endpoints cache" muestra bien una dirección IP,# pero cualquier conexión falla. El portail muestra una baldosa ORANGE.apiVersion: v1kind: Servicemetadata: name: cachespec: type: ClusterIP selector: app: cache ports: - port: 80 targetPort: 6380
Archivo: k8s/services/06-casses/casse-3.yaml
yaml
# AVERÍA 3# Síntoma: este Service parece perfecto (tipo correcto, selector correcto,# Endpoints rellenos, puertos coherentes)... y sin embargo el portail# muestra una baldosa ROUGE y NUNCA lo alcanza.apiVersion: v1kind: Servicemetadata: name: notificationspec: type: ClusterIP selector: app: notifications ports: - port: 80 targetPort: 7000
ANEXO E — El script de validación
Archivo: outils/valider.ps1
powershell
# ---------------------------------------------------------------------------# Script de validación — da una puntuación, NUNCA la solución.# Uso: .\outils\valider.ps1# ---------------------------------------------------------------------------$total = 0function Existe($nom) { kubectl get svc $nom -o name 2>$null | Out-Null return $LASTEXITCODE -eq 0}function Afficher($libelle, $points, $max, $note) { $etat = if ($points -eq $max) { "[OK] " } else { "[ECHEC] " } $ligne = "{0} {1} {2}/{3}" -f $etat, $libelle.PadRight(34, '.'), $points, $max if ($note) { $ligne += " -> $note" } Write-Host $ligne}Write-Host ""Write-Host "=== VALIDATION — Mission : retablir les communications ===" -ForegroundColor CyanWrite-Host ""# --- Misión 1 : api-produits ---------------------------------------------$p = 0; $note = ""if (-not (Existe "api-produits")) { $note = "Service api-produits introuvable" }else { $eps = (kubectl get endpoints api-produits -o jsonpath="{.subsets[*].addresses[*].ip}" 2>$null) $tp = (kubectl get svc api-produits -o jsonpath="{.spec.ports[0].targetPort}" 2>$null) if (-not $eps) { $note = "Endpoints vides : le selecteur ne correspond a aucun Pod" } elseif ("$tp" -ne "8000") { $note = "targetPort ne correspond pas au port ecoute" } else { $p = 15 }}Afficher "Mission 1 - api-produits" $p 15 $note; $total += $p# --- Misión 2 : portail ---------------------------------------------------$p = 0; $note = ""if (-not (Existe "portail")) { $note = "Service portail introuvable" }else { $type = (kubectl get svc portail -o jsonpath="{.spec.type}" 2>$null) $np = (kubectl get svc portail -o jsonpath="{.spec.ports[0].nodePort}" 2>$null) if ("$np" -ne "30500") { $note = "le port expose sur la machine doit etre 30500 (actuel : '$np')" } elseif ($type -notin @("NodePort", "LoadBalancer")) { $note = "type inadapte a un acces externe" } else { $p = 15 }}Afficher "Mission 2 - portail" $p 15 $note; $total += $p# --- Misión 3 : bd-interne (headless) -------------------------------------$p = 0; $note = ""if (-not (Existe "bd-interne")) { $note = "Service bd-interne introuvable (verifiez serviceName du StatefulSet)" }else { $cip = (kubectl get svc bd-interne -o jsonpath="{.spec.clusterIP}" 2>$null) $eps = (kubectl get endpoints bd-interne -o jsonpath="{.subsets[*].addresses[*].ip}" 2>$null) if ("$cip" -ne "None") { $note = "ce Service ne doit PAS avoir d'IP virtuelle" } elseif (-not $eps) { $note = "Endpoints vides : verifiez le selecteur" } else { $p = 20 }}Afficher "Mission 3 - bd-interne" $p 20 $note; $total += $p# --- Misión 4 : metriques (multi-port) ------------------------------------$p = 0; $note = ""if (-not (Existe "metriques")) { $note = "Service metriques introuvable" }else { $ports = (kubectl get svc metriques -o jsonpath="{.spec.ports[*].port}" 2>$null) $noms = (kubectl get svc metriques -o jsonpath="{.spec.ports[*].name}" 2>$null) $cible = (kubectl get svc metriques -o jsonpath="{.spec.ports[*].targetPort}" 2>$null) $liste = ($ports -split '\s+') | Where-Object { $_ } if ($liste.Count -lt 2) { $note = "il manque un port : deux sont attendus (80 et 9090)" } elseif (-not $noms) { $note = "chaque port doit porter un nom lorsqu'il y en a plusieurs" } elseif ($cible -match '^\s*\d+(\s+\d+)*\s*$') { $note = "targetPort doit referencer les ports PAR LEUR NOM" } else { $p = 15 }}Afficher "Mission 4 - metriques" $p 15 $note; $total += $p# --- Misión 5 : paiement-externe -------------------------------------------$p = 0; $note = ""if (-not (Existe "paiement-externe")) { $note = "Service paiement-externe introuvable" }else { $type = (kubectl get svc paiement-externe -o jsonpath="{.spec.type}" 2>$null) $cible = (kubectl get svc paiement-externe -o jsonpath="{.spec.externalName}" 2>$null) if ("$type" -ne "ExternalName") { $note = "ce n'est pas le type attendu pour un alias DNS" } elseif (-not $cible) { $note = "la cible externe n'est pas renseignee" } else { $p = 10 }}Afficher "Mission 5 - paiement-externe" $p 10 $note; $total += $p# --- Misión 6 : las tres reparaciones --------------------------------------$p = 0; $notes = @()foreach ($cas in @( @{ nom = "api-commandes"; port = "8000" }, @{ nom = "cache"; port = "6379" }, @{ nom = "notifications"; port = "7000" })) { if (-not (Existe $cas.nom)) { $notes += "$($cas.nom) : Service introuvable"; continue } $eps = (kubectl get endpoints $cas.nom -o jsonpath="{.subsets[*].addresses[*].ip}" 2>$null) $tp = (kubectl get svc $cas.nom -o jsonpath="{.spec.ports[0].targetPort}" 2>$null) if (-not $eps) { $notes += "$($cas.nom) : Endpoints vides" } elseif ("$tp" -ne $cas.port) { $notes += "$($cas.nom) : aucun Pod ne repond sur ce port" } else { $p += 7 }}if ($p -gt 20) { $p = 20 }Afficher "Mission 6 - reparations" $p 20 ($notes -join " | "); $total += $pWrite-Host ""$couleur = if ($total -ge 90) { "Green" } elseif ($total -ge 50) { "Yellow" } else { "Red" }Write-Host ("SCORE AUTOMATIQUE : {0} / 95" -f $total) -ForegroundColor $couleurWrite-Host " (+5 pour la qualite du rapport, +5 de bonus : evalues manuellement)" -ForegroundColor DarkGrayWrite-Host ""Write-Host "Rappel : le tableau de bord doit afficher 8 / 8 sur http://localhost:30500" -ForegroundColor DarkGrayWrite-Host ""
Curso creado por el Dr. Haythem REHOUMA — Desarrollo y despliegue de soluciones de datos