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.


Tabla de contenidos


El contexto

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)

TipoPara qué sirve¿Visto desde fuera?En este proyecto
ClusterIPDirección interna al clúster, valor por defectoNoapi-produits, api-commandes, cache, notifications, metriques
NodePortAbre un puerto fijo (30000–32767) en cada nodoSí, localhost:<nodePort>portail
LoadBalancerPide una IP pública a la nube (AWS, GCP, Azure)(misión bonus)
HeadlessUn ClusterIP sin IP virtual: devuelve la lista de IP de los Pods, más un nombre DNS por PodNobd-interne
ExternalNameAlias DNS hacia un nombre externo. Ningún Pod, ningún selector.Nopaiement-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ú llamasCoreDNS resuelve hacia
api-produitsel Service api-produits del mismo namespace
api-produits.defaultel Service api-produits del namespace default
api-produits.default.svc.cluster.localforma larga y plenamente cualificada

Corolario capital: el nombre del Service es el nombre que llama la aplicación. Un Service llamado notification no 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: StatefulSet
spec:
  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 ÚNICAMENTE
bd-1.bd-interne         -> IP del Pod bd-1 ÚNICAMENTE
bd-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:

yaml
ports:
  - name: web
    port: 80
    targetPort: 8080
  - name: prom
    port: 9090
    targetPort: 9090

El nombre sirve al menos para dos cosas:

  1. Kubernetes lo rechaza sin nombre en cuanto hay varias entradas;
  2. Permite referenciar un puerto del contenedor por su nombre en vez de por su número:
yaml
ports:
  - name: web
    port: 80
    targetPort: web              # remite al containerPort nombrado "web"

Ventaja concreta: si mañana el contenedor pasa de 8080 a 8081, se modifica un solo sitio (el Pod). El Service sigue siendo correcto.


6. ExternalName: un alias DNS, nada más

El tipo ExternalName no enruta nada. Simplemente pide a CoreDNS que responda:

«El nombre paiement-externe es example.com

Un ExternalName nunca tiene selector, Pods ni puertos. No es un proxy: es un alias.

yaml
apiVersion: v1
kind: Service
metadata:
  name: paiement-externe
spec:
  type: ExternalName
  externalName: example.com

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.

ComponentePuerto(s) del contenedorLabel de los PodsControlador
portail5000app: portailDeployment (1 réplica)
api-produits8000app: api-produitsDeployment (3 réplicas)
api-commandes8000app: api-commandesDeployment (2 réplicas)
cache6379app: cacheDeployment (1 réplica)
notifications7000app: notificationsDeployment (2 réplicas)
metriques8080 (nombrado web) y 9090 (nombrado prom)app: metriquesDeployment (2 réplicas)
bd5432app: bdStatefulSet (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:

BaldosaSignificadoDónde buscar el error
ROJOEl nombre DNS no existeEl Service no se ha creado, o su nombre es erróneo
NARANJAEl nombre se resuelve, pero nadie respondeEl Service existe, pero su selector o su puerto es erróneo
VERDEComunicación establecidaTu 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.

Los 8 enlaces verificados:

#BaldosaLo que prueba el portal
1Acceso externoQue consultas el portal vía el puerto 30500
2API Productoshttp://api-produits/ping
3Base de datoshttp://bd-0.bd-interne:5432/ping
4Métricashttp://metriques/ping y http://metriques:9090/metrics
5Pago externoResolución DNS del nombre paiement-externe
6API Pedidoshttp://api-commandes/ping
7Cachéhttp://cache/ping
8Notificacioneshttp://notifications/ping

Las reglas del juego

  1. 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.)
  2. Solo creas y modificas archivos situados en k8s/services/.
  3. Ninguna dirección IP a pelo. Todo debe apoyarse en los nombres DNS y los selectores de labels.
  4. 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.
  5. 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.
  6. Trabajas sobre el Kubernetes integrado en Docker Desktop (Settings → Kubernetes → Enable Kubernetes).

Preparación

Requisitos previos — a verificar una sola vez

  1. Docker Desktop está arrancado (icono verde en la barra del sistema).
  2. Kubernetes está activado en Docker Desktop: Settings → Kubernetes → Enable Kubernetes → Apply & Restart. Sin esa casilla marcada, ningún comando kubectl funcionará.
  3. 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.
  4. 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-desktop
kubectl get nodes                       # debe mostrar docker-desktop   Ready

# 1) Construir las tres imágenes
docker build -t micro:1.0      ./apps/micro
docker build -t metriques:1.0  ./apps/metriques
docker build -t portail:1.0    ./apps/portail

# 2) Desplegar la base suministrada (Pods, y NINGÚN Service)
kubectl apply -f k8s/01-deployments.yaml
kubectl 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 partida
kubectl get pods                        # 14 Pods, todos Running
kubectl 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:

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

  2. 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.yaml
kubectl get svc api-produits
kubectl 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/TCP
start 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-interne resuelve 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:

UsoPuerto del contenedorNombre del puerto declarado en el Deployment
Interfaz web8080web
Métricas9090prom

El portal llama a http://metriques (puerto 80) y http://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/
ArchivoSíntoma observado
casse-1.yamlEl Service existe, pero kubectl get endpoints devuelve <none>
casse-2.yamlLos Endpoints están bien rellenos, pero toda conexión es rechazada
casse-3.yamlEl Service parece perfecto, pero el portal nunca lo alcanza

Para cada uno de los tres casos, tu informe debe contener:

  1. el comando de diagnóstico que te puso en la pista;
  2. la causa exacta de la avería;
  3. el correctivo aplicado;
  4. 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:

powershell
powershell -ExecutionPolicy Bypass -File .\outils\valider.ps1

Otra solución duradera (a hacer solo una vez para tu usuario):

powershell
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned
[OK]     Mission 1 - api-produits ............. 15/15
[OK]     Mission 2 - portail .................. 15/15
[ECHEC]  Mission 3 - bd-interne ...............  0/20   -> clusterIP doit valoir None
[OK]     Mission 4 - metriques ................ 15/15
[ECHEC]  Mission 5 - paiement-externe .........  0/10   -> Service introuvable
[ECHEC]  Mission 6 - reparations ..............  7/20   -> cache : aucun Pod ne repond

SCORE : 52 / 100

El script no da ninguna solución: solo indica lo que falla y dónde mirar.


Entregables

  1. La carpeta k8s/services/ completa: tus 5 Services escritos y los 3 Services reparados.
  2. 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.
  3. La salida final de .\outils\valider.ps1.

Preguntas de reflexión

  1. ¿Por qué la aplicación no podía en absoluto funcionar sin Services, aunque todos los Pods estuvieran Running?
  2. ¿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?
  3. ¿Por qué el Service de la base de datos debe ser headless, mientras que un Service ordinario basta para la API de productos?
  4. ¿Qué contiene exactamente la lista de Endpoints, y quién la actualiza? ¿Qué ocurre cuando un Pod pasa a NotReady?
  5. 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?
  6. En producción, ¿expondrías diez aplicaciones con diez Services de tipo LoadBalancer? Justifica y propone una alternativa.

Baremo

ElementoPuntos
Misión 1 — Service interno y descubrimiento DNS15
Misión 2 — Exposición externa en el puerto 3050015
Misión 3 — Service headless e identidades estables20
Misión 4 — Multi-puerto y puertos nombrados15
Misión 5 — Alias hacia un service externo10
Misión 6 — Diagnóstico y reparación (3 averías)20
Calidad del informe y justificación de las elecciones5
Bonus — Misión 7+5
Total100 (+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

CriterioEsperado
Tablero8 / 8 baldosas verdes
Tipos de ServicesCada uno adaptado a su uso y justificado
DNS individualbd-0.bd-interne resuelto hacia un solo Pod
Multi-puertoLos dos puertos visibles, targetPort referenciado por nombre
InvestigaciónLas 3 averías identificadas, explicadas y corregidas
ResilienciaTras 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, puertos
kubectl describe svc <nom>                   # detalles + Endpoints
kubectl get endpoints <nom>                  # ¿QUIÉN está detrás del Service?
kubectl get pods --show-labels               # los labels reales de los Pods
kubectl get pods -l app=<valeur>             # probar un selector
kubectl port-forward deploy/portail 5000:5000    # acceder a un Pod SIN Service
kubectl run test --rm -i --restart=Never --image=busybox:1.36 -- wget -qO- http://<nom>/ping
kubectl run test --rm -i --restart=Never --image=busybox:1.36 -- nslookup <nom>.default.svc.cluster.local
kubectl logs -l app=portail --tail=30        # lo que el portal no consigue alcanzar
kubectl 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:

  1. ¿El Service existe, con el nombre correcto? (si no → baldosa roja: no hay nada que depurar, hay que escribirlo)
  2. ¿Los Endpoints están rellenos? (vacíos → el selector no coincide con ningún label de Pod)
  3. ¿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 escucha
los aportan variables de entorno (APP_NAME, PORT).
Cada respuesta contiene el nombre del Pod, lo que hace visible el
reparto de carga que realiza un Service.
"""

import os
import socket

from flask import Flask, jsonify

app = 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", 200


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=PORT)

Archivo: apps/micro/requirements.txt

text
flask==3.0.3

Archivo: apps/micro/Dockerfile

dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
CMD ["python", "app.py"]

A.2 — El componente «metriques» (dos puertos)

Esta aplicación escucha simultáneamente en dos puertos: 8080 (interfaz web) y 9090 (métricas). Es la que hace posible la misión 4.

Archivo: apps/metriques/app.py

python
"""Componente que expone DOS puertos a la vez.

  - 8080 : interfaz web          (ruta /ping)
  - 9090 : métricas Prometheus   (ruta /metrics)

Dos servidores Flask corren en dos hilos de ejecución distintos.
"""

import socket
import threading

from flask import Flask, jsonify

web = Flask("web")
prom = Flask("prom")


@web.route("/")
@web.route("/ping")
def ping():
    return jsonify(service="metriques", pod=socket.gethostname(), port=8080)


@web.route("/health")
def health_web():
    return "OK", 200


@prom.route("/metrics")
def metrics():
    pod = socket.gethostname()
    corps = (
        "# HELP demo_requetes_total Nombre total de requetes\n"
        "# TYPE demo_requetes_total counter\n"
        'demo_requetes_total{pod="%s"} 42\n' % pod
    )
    return corps, 200, {"Content-Type": "text/plain; charset=utf-8"}


@prom.route("/health")
def health_prom():
    return "OK", 200


def demarrer(application, port):
    application.run(host="0.0.0.0", port=port)


if __name__ == "__main__":
    threading.Thread(target=demarrer, args=(prom, 9090), daemon=True).start()
    demarrer(web, 8080)

Archivo: apps/metriques/requirements.txt

text
flask==3.0.3

Archivo: apps/metriques/Dockerfile

dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
CMD ["python", "app.py"]

A.3 — El portal (tablero)

Es el que muestra las 8 baldosas y tu puntuación en directo.

Archivo: apps/portail/app.py

python
"""Tablero de las conexiones del clúster.

Para cada conexión, el portail distingue TRES situaciones:
  - ROUGE  : el nombre DNS no existe       -> el Service no se ha creado
  - ORANGE : el nombre resuelve, sin respuesta -> selector o puerto erróneo
  - VERT   : la comunicación funciona      -> el Service es correcto
"""

import socket
import urllib.error
import urllib.parse
import urllib.request

from flask import Flask, request

app = Flask(__name__)

DELAI = 1.5          # segundos
PORT_ATTENDU = 30500  # puerto por el que debe consultarse el portail

CIBLES = [
    {"cle": "externe", "titre": "Acces externe", "mode": "externe",
     "aide": "Le portail doit etre consulte via http://localhost:30500"},
    {"cle": "produits", "titre": "API Produits", "mode": "http",
     "url": "http://api-produits/ping"},
    {"cle": "bd", "titre": "Base de donnees (bd-0)", "mode": "http",
     "url": "http://bd-0.bd-interne:5432/ping"},
    {"cle": "metriques", "titre": "Metriques (2 ports)", "mode": "http2",
     "url": "http://metriques/ping", "url2": "http://metriques:9090/metrics"},
    {"cle": "paiement", "titre": "Paiement externe", "mode": "dns",
     "hote": "paiement-externe"},
    {"cle": "commandes", "titre": "API Commandes", "mode": "http",
     "url": "http://api-commandes/ping"},
    {"cle": "cache", "titre": "Cache", "mode": "http",
     "url": "http://cache/ping"},
    {"cle": "notifications", "titre": "Notifications", "mode": "http",
     "url": "http://notifications/ping"},
]


def resout(hote):
    try:
        socket.getaddrinfo(hote, None)
        return True
    except socket.gaierror:
        return False


def tester_http(url):
    hote = urllib.parse.urlparse(url).hostname
    if not resout(hote):
        return "rouge", "nom DNS introuvable : le Service n'existe pas"
    try:
        with urllib.request.urlopen(url, timeout=DELAI) as reponse:
            corps = reponse.read(160).decode("utf-8", "ignore")
        return "vert", corps.strip()
    except urllib.error.HTTPError as err:
        return "orange", "reponse HTTP %s" % err.code
    except Exception as err:
        return "orange", "nom resolu mais aucune reponse (%s)" % type(err).__name__


def evaluer(cible):
    mode = cible["mode"]

    if mode == "externe":
        port = (request.host.split(":") + ["80"])[1]
        if str(port) == str(PORT_ATTENDU):
            return "vert", "consulte via le port %s" % PORT_ATTENDU
        return "orange", "consulte via le port %s : ecrivez le Service du portail" % port

    if mode == "dns":
        if resout(cible["hote"]):
            return "vert", "le nom %s est resolu" % cible["hote"]
        return "rouge", "le nom %s n'est pas resolu" % cible["hote"]

    if mode == "http2":
        etat1, det1 = tester_http(cible["url"])
        etat2, det2 = tester_http(cible["url2"])
        if etat1 == "vert" and etat2 == "vert":
            return "vert", "les deux ports repondent"
        if etat1 == "rouge" or etat2 == "rouge":
            return "rouge", "port 80 : %s | port 9090 : %s" % (det1, det2)
        return "orange", "port 80 : %s | port 9090 : %s" % (det1, det2)

    return tester_http(cible["url"])


COULEURS = {"vert": "#16a34a", "orange": "#ea580c", "rouge": "#b91c1c"}


@app.route("/health")
def health():
    return "OK", 200


@app.route("/")
def accueil():
    resultats = []
    for cible in CIBLES:
        etat, detail = evaluer(cible)
        resultats.append((cible["titre"], etat, detail))

    score = sum(1 for _, etat, _ in resultats if etat == "vert")
    total = len(resultats)

    tuiles = ""
    for titre, etat, detail in resultats:
        tuiles += """
        <div class="tuile" style="border-left:10px solid {couleur}">
          <div class="t">{titre}</div>
          <div class="e" style="color:{couleur}">{etat}</div>
          <div class="d">{detail}</div>
        </div>""".format(couleur=COULEURS[etat], titre=titre,
                         etat=etat.upper(), detail=detail)

    couleur_score = "#16a34a" if score == total else "#ea580c"

    return """<!DOCTYPE html>
<html lang="fr">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="refresh" content="3">
  <title>Mission : retablir les communications</title>
  <style>
    body {{ font-family: system-ui, sans-serif; background:#0f172a; color:#e2e8f0;
            margin:0; padding:32px; }}
    h1 {{ margin:0 0 4px; }}
    .sous {{ color:#94a3b8; margin-bottom:24px; }}
    .score {{ font-size:2.4rem; font-weight:800; color:{couleur_score}; margin-bottom:24px; }}
    .grille {{ display:grid; grid-template-columns:repeat(auto-fill,minmax(320px,1fr)); gap:16px; }}
    .tuile {{ background:#1e293b; border-radius:12px; padding:16px 20px;
              box-shadow:0 6px 20px rgba(0,0,0,.35); }}
    .t {{ font-weight:700; font-size:1.05rem; }}
    .e {{ font-weight:800; font-size:.8rem; letter-spacing:2px; margin:6px 0; }}
    .d {{ color:#94a3b8; font-size:.85rem; word-break:break-word; }}
    .pied {{ margin-top:28px; color:#64748b; font-size:.85rem; }}
  </style>
</head>
<body>
  <h1>Mission : retablir les communications du cluster</h1>
  <div class="sous">Servi par le pod <strong>{pod}</strong> &middot; rafraichissement automatique toutes les 3 s</div>
  <div class="score">{score} / {total}</div>
  <div class="grille">{tuiles}</div>
  <div class="pied">ROUGE : le Service n'existe pas &middot; ORANGE : selecteur ou port errone &middot; VERT : liaison etablie</div>
</body>
</html>""".format(pod=socket.gethostname(), score=score, total=total,
                  tuiles=tuiles, couleur_score=couleur_score)


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

Archivo: apps/portail/requirements.txt

text
flask==3.0.3

Archivo: apps/portail/Dockerfile

dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
CMD ["python", "app.py"]


ANEXO B — Los manifiestos suministrados

No modifiques ninguno de estos dos archivos. Constituyen lo existente a lo que tus Services deben adaptarse.

Archivo: k8s/01-deployments.yaml

yaml
# ---------------------------------------------------------------------------
# LOS PODS DE LA PLATAFORMA — SUMINISTRADO, NO MODIFICAR
# Observa con atención: los LABELS y los PUERTOS declarados aquí son la única
# información de la que dispones para escribir tus Services.
# ---------------------------------------------------------------------------
apiVersion: apps/v1
kind: Deployment
metadata:
  name: portail
spec:
  replicas: 1
  selector:
    matchLabels:
      app: portail
  template:
    metadata:
      labels:
        app: portail
    spec:
      containers:
        - name: portail
          image: portail:1.0
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 5000
          readinessProbe:
            httpGet: { path: /health, port: 5000 }
            initialDelaySeconds: 3
            periodSeconds: 5
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-produits
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api-produits
  template:
    metadata:
      labels:
        app: api-produits
    spec:
      containers:
        - name: micro
          image: micro:1.0
          imagePullPolicy: IfNotPresent
          env:
            - { name: APP_NAME, value: "api-produits" }
            - { name: PORT,     value: "8000" }
          ports:
            - containerPort: 8000
          readinessProbe:
            httpGet: { path: /health, port: 8000 }
            initialDelaySeconds: 3
            periodSeconds: 5
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-commandes
spec:
  replicas: 2
  selector:
    matchLabels:
      app: api-commandes
  template:
    metadata:
      labels:
        app: api-commandes
    spec:
      containers:
        - name: micro
          image: micro:1.0
          imagePullPolicy: IfNotPresent
          env:
            - { name: APP_NAME, value: "api-commandes" }
            - { name: PORT,     value: "8000" }
          ports:
            - containerPort: 8000
          readinessProbe:
            httpGet: { path: /health, port: 8000 }
            initialDelaySeconds: 3
            periodSeconds: 5
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: cache
spec:
  replicas: 1
  selector:
    matchLabels:
      app: cache
  template:
    metadata:
      labels:
        app: cache
    spec:
      containers:
        - name: micro
          image: micro:1.0
          imagePullPolicy: IfNotPresent
          env:
            - { name: APP_NAME, value: "cache" }
            - { name: PORT,     value: "6379" }
          ports:
            - containerPort: 6379
          readinessProbe:
            httpGet: { path: /health, port: 6379 }
            initialDelaySeconds: 3
            periodSeconds: 5
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: notifications
spec:
  replicas: 2
  selector:
    matchLabels:
      app: notifications
  template:
    metadata:
      labels:
        app: notifications
    spec:
      containers:
        - name: micro
          image: micro:1.0
          imagePullPolicy: IfNotPresent
          env:
            - { name: APP_NAME, value: "notifications" }
            - { name: PORT,     value: "7000" }
          ports:
            - containerPort: 7000
          readinessProbe:
            httpGet: { path: /health, port: 7000 }
            initialDelaySeconds: 3
            periodSeconds: 5
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: metriques
spec:
  replicas: 2
  selector:
    matchLabels:
      app: metriques
  template:
    metadata:
      labels:
        app: metriques
    spec:
      containers:
        - name: metriques
          image: metriques:1.0
          imagePullPolicy: IfNotPresent
          ports:
            - name: web            # <-- puerto NOMBRADO
              containerPort: 8080
            - name: prom           # <-- puerto NOMBRADO
              containerPort: 9090
          readinessProbe:
            httpGet: { path: /health, port: 8080 }
            initialDelaySeconds: 3
            periodSeconds: 5

Archivo: k8s/02-statefulset-bd.yaml

yaml
# ---------------------------------------------------------------------------
# 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/v1
kind: StatefulSet
metadata:
  name: bd
spec:
  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: v1
kind: Service
metadata:
  name: api-produits          # nombre IMPUESTO: no cambiar
spec:
  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: v1
kind: Service
metadata:
  name: portail               # nombre IMPUESTO: no cambiar
spec:
  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: v1
kind: Service
metadata:
  name: bd-interne            # nombre IMPUESTO: no cambiar
spec:
  # ? 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: v1
kind: Service
metadata:
  name: metriques             # nombre IMPUESTO: no cambiar
spec:
  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: v1
kind: Service
metadata:
  name: paiement-externe      # nombre IMPUESTO: no cambiar
spec:
  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: v1
kind: Service
metadata:
  name: api-commandes
spec:
  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: v1
kind: Service
metadata:
  name: cache
spec:
  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: v1
kind: Service
metadata:
  name: notification
spec:
  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 = 0

function 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 Cyan
Write-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 += $p

Write-Host ""
$couleur = if ($total -ge 90) { "Green" } elseif ($total -ge 50) { "Yellow" } else { "Red" }
Write-Host ("SCORE AUTOMATIQUE : {0} / 95" -f $total) -ForegroundColor $couleur
Write-Host "   (+5 pour la qualite du rapport, +5 de bonus : evalues manuellement)" -ForegroundColor DarkGray
Write-Host ""
Write-Host "Rappel : le tableau de bord doit afficher 8 / 8 sur http://localhost:30500" -ForegroundColor DarkGray
Write-Host ""

Curso creado por el Dr. Haythem REHOUMA — Desarrollo y despliegue de soluciones de datos