Missão: restabelecer as comunicações do cluster

20 min

Projeto 12 — Os Services Kubernetes · Nível intermédio → avançado · Duração estimada: 3 a 5 h

Todo o código da aplicação é-lhe fornecido em anexo neste documento. O seu trabalho: escrever, determinar e completar os Services que faltam — ou seja, fazer comunicar um sistema que, no estado atual, está totalmente mudo.


Índice


O contexto

Uma equipa implantou uma pequena plataforma de comércio eletrónico no Kubernetes. As imagens estão construídas, os Deployments e o StatefulSet estão a correr, todos os Pods estão Running.

E no entanto, nada funciona.

O portal mostra um painel inteiramente vermelho: não alcança nenhum componente. A base de dados é inatingível. A cache é invisível. Nenhuma página é acessível a partir do navegador.

A razão é simples: a pessoa que deveria escrever os Services partiu sem os entregar.

Lembrete fundamental que este projeto o vai fazer viver: Pods a correr não constituem uma aplicação. Sem Services, são ilhéus isolados, sem endereço estável nem nome, incapazes de se encontrarem uns aos outros.

A sua missão: restabelecer todas as comunicações, unicamente ao escrever os Services certos.


Conceitos essenciais antes de começar

Esta secção é um mini-manual autónomo: contém todo o vocabulário necessário às missões. Leia-a uma vez, volte a ela quando uma palavra lhe escapar.

1. O problema que um Service resolve

Um Pod é mortal: o Kubernetes pode apagá-lo, deslocá-lo, recriá-lo — e o seu novo IP será diferente. Por isso nunca se liga a um Pod pelo seu IP.

Um Service é um objeto estável — nome, IP, porta — que segue os Pods para onde forem. O mecanismo é simples:

O que faz a ligação entre um Service e «os seus» Pods é o seletor de labels:

yaml
spec:
  selector:
    app: api-produits          # tout Pod portant CE label est atteint

A lista dos Pods que correspondem forma o objeto Endpoints — é a sua radiografia do Service.

powershell
kubectl get endpoints api-produits
# api-produits   10.244.0.3:8000,10.244.0.4:8000,10.244.0.5:8000

Se Endpoints estiver vazio, o seletor não corresponde a nenhum Pod: é quase sempre um erro de digitação num label.


2. Os cinco tipos de Services (os únicos de que precisa)

TipoPara que serveVisto do exterior?Neste projeto
ClusterIPEndereço interno ao cluster, valor por omissãoNãoapi-produits, api-commandes, cache, notifications, metriques
NodePortAbre uma porta fixa (30000–32767) em cada nóSim, localhost:<nodePort>portail
LoadBalancerPede um IP público à cloud (AWS, GCP, Azure)Sim(missão bónus)
HeadlessUm ClusterIP sem IP virtual: devolve a lista dos IP dos Pods, mais um nome DNS por PodNãobd-interne
ExternalNameAlias DNS para um nome externo. Nenhum Pod, nenhum seletor.Nãopaiement-externe

Ponto importante: um Service que «não funciona» quase nunca é um problema de tipo. É quase sempre seletor ou porta.


3. DNS interno: as regras que dão a ilusão de magia

No cluster, o CoreDNS fabrica automaticamente nomes segundo regras fixas:

Você chamaO CoreDNS resolve para
api-produitso Service api-produits do mesmo namespace
api-produits.defaulto Service api-produits do namespace default
api-produits.default.svc.cluster.localforma longa e plenamente qualificada

Corolário capital: o nome do Service é o nome que a aplicação chama. Um Service chamado notification não responde a http://notifications. Esta armadilha está no coração da missão 6.


4. StatefulSet e Service headless: o tandem

Um Deployment trata as suas réplicas como gémeas intercambiáveis (web-abc123-x7k9, web-abc123-p2m1…). Perfeito para web sem estado.

Um StatefulSet produz, pelo contrário, Pods numerados e estáveis: bd-0, bd-1, bd-2. Cada Pod guarda a sua identidade através dos reinícios — indispensável para uma base de dados em que se deve designar precisamente a primária.

Mas um StatefulSet não basta sozinho: exige estar associado a um Service headless cujo nome indica no campo serviceName.

yaml
kind: StatefulSet
spec:
  serviceName: bd-interne         # <-- pointe vers un Service headless du même nom
  replicas: 3

Este Service headless dá então um nome DNS por Pod:

bd-0.bd-interne         -> IP du Pod bd-0 UNIQUEMENT
bd-1.bd-interne         -> IP du Pod bd-1 UNIQUEMENT
bd-interne              -> IPs des trois Pods (liste)

Sem a palavra None em spec.clusterIP, nenhum destes nomes existe.

yaml
spec:
  clusterIP: None                  # transforme le Service en "headless"

Retenha isto: para alcançar bd-0.bd-interne, são precisas duas condições simultâneas — um StatefulSet cujo serviceName: bd-interne, e um Service headless chamado bd-interne. Se uma faltar, o nome individual não existe.


5. Portas nomeadas e Services multiporta

Quando um Service expõe várias portas, cada entrada torna-se obrigatoriamente nomeada:

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

O nome serve pelo menos para duas coisas:

  1. O Kubernetes recusa sem nome assim que há várias entradas ;
  2. Permite referenciar uma porta do contentor pelo seu nome em vez de pelo seu número:
yaml
ports:
  - name: web
    port: 80
    targetPort: web              # renvoie au containerPort nommé "web"

Vantagem concreta: se amanhã o contentor passar de 8080 para 8081, altera-se um só sítio (o Pod). O Service permanece certo.


6. ExternalName: um alias DNS, nada mais

O tipo ExternalName não encaminha nada. Pede simplesmente ao CoreDNS que responda:

«O nome paiement-externe é example.com

Um ExternalName nunca tem seletor, Pods ou portas. Não é um proxy: é um alias.

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

Utilidade: o seu código guarda o mesmo nome interno (paiement-externe) quer se trate de um serviço no cluster, de um serviço SaaS externo, ou de uma mudança de API. Muda-se o Service, não o código.


7. As três perguntas que desbloqueiam 90 % das avarias

Cada vez que um Service não funciona, faça estas três perguntas nesta ordem:

É exatamente o método da missão 6.


O que saberá fazer no fim

  • Distinguir os cinco tipos de Services e saber quando usar cada um.
  • Usar kubectl get svc, kubectl describe svc, kubectl get endpoints como três ferramentas complementares.
  • Acoplar corretamente um StatefulSet com um Service headless.
  • Escrever um Service multiporta limpo, com targetPort referenciado por nome.
  • Diagnosticar as três avarias mais frequentes em empresa (label errado, porta errada, nome errado).

A arquitetura a pôr em serviço

Sete componentes já estão a correr. Nenhum é alcançável.

ComponentePorta(s) do contentorLabel dos 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 (nomeado web) e 9090 (nomeado prom)app: metriquesDeployment (2 réplicas)
bd5432app: bdStatefulSet (3 réplicas)

Organização dos ficheiros

Crie exatamente esta árvore, ao recopiar o conteúdo dos anexos. Cada anexo indica o caminho exato do ficheiro a criar.

projet12-mission-services/
├── 00-ENONCE.md                      <- este documento

├── apps/                             <- O CODIGO (ANEXO A) — NAO MODIFICAR
│   ├── micro/
│   │   ├── app.py
│   │   ├── requirements.txt
│   │   └── Dockerfile
│   ├── metriques/
│   │   ├── app.py
│   │   ├── requirements.txt
│   │   └── Dockerfile
│   └── portail/
│       ├── app.py
│       ├── requirements.txt
│       └── Dockerfile

├── k8s/
│   ├── 01-deployments.yaml           <- FORNECIDO (ANEXO B) — NAO MODIFICAR
│   ├── 02-statefulset-bd.yaml        <- FORNECIDO (ANEXO B) — NAO MODIFICAR
│   │
│   └── services/                     <- A SUA VEZ
│       ├── 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/                <- FORNECIDOS mas DEFEITUOSOS (ANEXO D)
│           ├── casse-1.yaml
│           ├── casse-2.yaml
│           └── casse-3.yaml

├── outils/
│   └── valider.ps1                   <- FORNECIDO (ANEXO E)

└── RAPPORT.md                        <- A REDIGIR por si

Apenas três imagens são necessárias: micro:1.0 serve cinco componentes diferentes (o comportamento muda por variáveis de ambiente), metriques:1.0 expõe duas portas, e portail:1.0 mostra o painel.


O painel: o seu indicador de progresso

O portal interroga em contínuo cada componente e mostra um cartão por ligação, atualizado a cada 3 segundos:

CartãoSignificadoOnde procurar o erro
VERMELHOO nome DNS não existeO Service não foi criado, ou o seu nome está errado
LARANJAO nome está resolvido, mas ninguém respondeO Service existe, mas o seu seletor ou a sua porta está errado
VERDEComunicação estabelecidaO seu Service está correto

Objetivo final: os 8 cartões a verde, e o contador a mostrar 8 / 8.

Esta distinção vermelho/laranja não é decorativa: diz-lhe de que lado procurar. Vermelho = o Service não existe (nada a depurar, é preciso escrevê-lo). Laranja = o Service existe mas não encontra os seus Pods ou bate na porta errada.

As 8 ligações verificadas:

#CartãoO que o portal testa
1Acesso externoQue consulta mesmo o portal via a porta 30500
2API Produtoshttp://api-produits/ping
3Base de dadoshttp://bd-0.bd-interne:5432/ping
4Métricashttp://metriques/ping e http://metriques:9090/metrics
5Pagamento externoResolução DNS do nome paiement-externe
6API Encomendashttp://api-commandes/ping
7Cachehttp://cache/ping
8Notificaçõeshttp://notifications/ping

As regras do jogo

  1. Proibição absoluta de modificar a pasta apps/, bem como 01-deployments.yaml e 02-statefulset-bd.yaml. (Toda a dificuldade consiste em adaptar-se ao existente: é exatamente a situação de um posto de trabalho real.)
  2. Só cria e só modifica ficheiros situados em k8s/services/.
  3. Nenhum endereço IP em concreto. Tudo deve assentar nos nomes DNS e nos seletores de labels.
  4. Deve determinar você mesmo o tipo de cada Service: nada lhe diz se se trata de um ClusterIP, de um NodePort, de um LoadBalancer, de um serviço headless ou de um ExternalName. É o coração da avaliação.
  5. Os nomes dos Services são impostos: o código da aplicação chama-os tal qual. Um nome errado dá um cartão vermelho.
  6. Trabalha no Kubernetes integrado no Docker Desktop (Settings → Kubernetes → Enable Kubernetes).

Preparação

Pré-requisitos — a verificar uma só vez

  1. O Docker Desktop está arrancado (ícone verde na barra do sistema).
  2. O Kubernetes está ativado no Docker Desktop: Settings → Kubernetes → Enable Kubernetes → Apply & Restart. Sem esta caixa marcada, nenhum comando kubectl funcionará.
  3. O Docker Desktop dispõe de pelo menos 4 Go de RAM: Settings → Resources → Memory ≥ 4 GB. Este projeto lança 14 Pods; com 2 Go a máquina sufoca e Pods ficam em Pending.
  4. Tem Internet (para a missão 5 e para descarregar a imagem busybox).

Sequência de arranque

powershell
# 0) Se placer sur le bon cluster (indispensable si minikube ou kind a déjà servi)
kubectl config use-context docker-desktop
kubectl get nodes                       # doit afficher docker-desktop   Ready

# 1) Construire les trois images
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) Déployer la base fournie (des Pods, et AUCUN Service)
kubectl apply -f k8s/01-deployments.yaml
kubectl apply -f k8s/02-statefulset-bd.yaml

# 3) Attendre que TOUS les Pods soient Ready (environ 30 s)
kubectl wait --for=condition=ready pod --all --timeout=180s

# 4) Constater la situation de départ
kubectl get pods                        # 14 Pods, tous Running
kubectl get svc                         # seulement "kubernetes" : aucun de vos Services

Neste estádio: todos os Pods correm e nada comunica. É o ponto de partida normal.

Dois avisos técnicos a conhecer desde já — não é um erro seu:

  1. Warning Endpoints is deprecated in v1.33+ — o Kubernetes mostra sistematicamente esta mensagem em cada kubectl get endpoints. O comando continua a funcionar perfeitamente, ignore o warning. A nova API equivalente é kubectl get endpointslices, mas todos os comandos deste TP usam de propósito endpoints, mais legível para aprender.

  2. O painel pode demorar 10 a 15 segundos a aparecer na primeira vez: o portal testa 8 ligações de rede em cada apresentação, cada uma com um tempo de espera de 1,5 s. Quando ainda nada funciona, espera cada prazo antes de mostrar vermelho ou laranja. Uma vez os Services corretos, o tempo de resposta cai para algumas centenas de milissegundos.

Pergunta a colocar a si mesmo imediatamente: como vai sequer ver o painel, já que nenhuma porta de entrada existe ainda?

Boia de salvamento: kubectl port-forward funciona sem nenhum Service, diretamente num Pod.

powershell
kubectl port-forward deploy/portail 5000:5000

Depois abra http://localhost:5000. Verá o painel todo vermelho, com o cartão «Acesso externo» a laranja (normal: não passou pela porta 30500).


As missões

Missão 1 — Fazer o portal falar com a API produtos (15 pontos)

O portal chama http://api-produits na porta 80. Os Pods da API escutam na porta 8000 e levam o label app: api-produits.

Ficheiro a completar: k8s/services/01-api-produits.yaml

A determinar: o tipo de Service, o seletor, bem como port e targetPort.

Validação:

powershell
kubectl apply -f k8s/services/01-api-produits.yaml
kubectl get svc api-produits
kubectl get endpoints api-produits        # doit lister 3 adresses IP

O cartão API Produtos passa a verde.


Missão 2 — Abrir a porta de entrada (15 pontos)

O painel deve ser acessível a partir do seu navegador no endereço exato http://localhost:30500. Os Pods do portal escutam na porta 5000.

Ficheiro a completar: k8s/services/02-portail.yaml

A determinar: que tipo de Service expõe uma aplicação fora do cluster numa porta fixa da máquina? Qual é a gama de portas autorizada para este campo?

Validação:

powershell
kubectl get svc portail                   # PORT(S) doit afficher 80:30500/TCP
start http://localhost:30500

O cartão Acesso externo passa a verde.

Pergunta a tratar no relatório: outro tipo de Service também teria tornado o portal acessível a partir do navegador no Docker Desktop. Qual? E que diferença faria em produção na cloud?


Missão 3 — Dar uma identidade a cada base de dados (20 pontos)

O StatefulSet bd fornece 3 réplicas. O portal deve alcançar precisamente a primeira (a primária), no endereço:

bd-0.bd-interne

Os Pods da base levam o label app: bd e escutam na porta 5432.

Ficheiro a completar: k8s/services/03-bd-interne.yaml

A determinar: que tipo de Service dá um nome DNS individual a cada Pod, em vez de um único IP virtual? Que campo é preciso escrever, e com que valor particular?

Validação:

powershell
# 1) Depuis un Pod utilitaire, joindre directement bd-0 :
kubectl run test --rm -i --restart=Never --image=busybox:1.36 -- wget -qO- http://bd-0.bd-interne:5432/ping
# doit repondre : {"pod":"bd-0","port":5432,"service":"base-de-donnees"}

# 2) Verifier les entrees DNS avec le nom pleinement qualifie
#    (busybox nslookup n'applique PAS les search domains, il faut donner le FQDN) :
kubectl run test --rm -i --restart=Never --image=busybox:1.36 -- nslookup bd-0.bd-interne.default.svc.cluster.local
# doit renvoyer UNE seule adresse (celle du Pod bd-0)

kubectl run test --rm -i --restart=Never --image=busybox:1.36 -- nslookup bd-interne.default.svc.cluster.local
# doit renvoyer TROIS adresses (une par Pod du StatefulSet)
Armadilha a não falhar
examine o campo serviceName do StatefulSet fornecido. O nome do seu Service deve corresponder-lhe exatamente, senão os nomes individuais dos Pods nunca serão criados.
Armadilha técnica (busybox)
nslookup nom-court não funciona a partir de um Pod busybox porque o seu resolvedor não usa os search domains de /etc/resolv.conf. A partir de um Pod de aplicação verdadeiro (como portail), em contrapartida, bd-0.bd-interne resolve perfeitamente. Use portanto wget para testar a cadeia de aplicação real, e o FQDN para levantar qualquer ambiguidade DNS.

Missão 4 — Expor duas portas no mesmo Service (15 pontos)

O componente metriques escuta em duas portas:

UsoPorta do contentorNome da porta declarado no Deployment
Interface web8080web
Métricas9090prom

O portal chama http://metriques (porta 80) e http://metriques:9090/metrics.

Ficheiro a completar: k8s/services/04-metriques.yaml

A determinar: como declarar várias portas num Service? Que restrição se torna então obrigatória para cada entrada? E como fazer targetPort apontar para uma porta do contentor pelo seu nome em vez de pelo seu número, para que o Service permaneça válido mesmo se o número mudar?

Validação:

powershell
kubectl describe svc metriques            # les DEUX ports doivent apparaître

Missão 5 — Dar um nome interno a um serviço externo (10 pontos)

O portal deve alcançar um serviço de pagamento alojado fora do cluster, mas o código chama um nome interno: paiement-externe. Este nome deve apontar para example.com.

Ficheiro a completar: k8s/services/05-paiement-externe.yaml

A determinar: que tipo de Service cria um simples alias DNS para um nome externo, sem seletor e sem nenhum Pod?

Validação:

powershell
kubectl run test --rm -i --restart=Never --image=busybox:1.36 -- nslookup paiement-externe.default.svc.cluster.local
# doit afficher :
#   paiement-externe.default.svc.cluster.local  canonical name = example.com
#   Name: example.com
#   Address: <IP publique>

Esta missão exige que o cluster possa resolver nomes públicos. Se não tiver nenhum acesso à Internet, substitua o alvo por api-produits.default.svc.cluster.local e assinale-o no seu relatório.


Missão 6 — O inquérito: reparar três Services defeituosos (20 pontos)

A pasta k8s/services/06-casses/ contém três Services já escritos… que não funcionam. Cada um comporta um só erro, e são as três faltas mais frequentes em empresa.

powershell
kubectl apply -f k8s/services/06-casses/
FicheiroSintoma observado
casse-1.yamlO Service existe, mas kubectl get endpoints devolve <none>
casse-2.yamlOs Endpoints estão bem preenchidos, mas qualquer ligação é recusada
casse-3.yamlO Service parece perfeito, mas o portal nunca o alcança

Para cada um dos três casos, o seu relatório deve conter:

  1. o comando de diagnóstico que o pôs na pista ;
  2. a causa exata da avaria ;
  3. a correção aplicada ;
  4. a prova de que a ligação funciona (cartão verde + saída de comando).

Método aconselhado: proceda como um investigador. kubectl describe svc, kubectl get endpoints, kubectl get pods --show-labels, e depois compare linha a linha o Service e os Pods. A diferença entre «Endpoints vazios» e «ligação recusada» já lhe indica de que lado procurar.


Missão 7 — Bónus: o grande salto (5 pontos)

À escolha, um só basta:

  • a) Fazer com que o mesmo cliente seja sempre servido pelo mesmo Pod da API produtos. (Indício: um campo do Service permite uma «aderência» fundada no IP do cliente.)
  • b) Criar um Service sem seletor a apontar para um endereço IP externo fixo, ao escrever você mesmo os seus Endpoints.
  • c) Escrever um Service do tipo LoadBalancer para o portal, e depois explicar o que se torna EXTERNAL-IP no Docker Desktop, e o que se tornaria na AWS.

Validação automática

Um script dá-lhe a pontuação a qualquer momento:

powershell
.\outils\valider.ps1

Se o PowerShell recusar executar o script com uma mensagem do tipo l'exécution de scripts est désactivée sur ce système, contorne a restrição só para este comando:

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

Outra solução duradoura (a fazer só uma vez para o seu utilizador):

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

O script não dá nenhuma solução: indica apenas o que falha e onde olhar.


Entregáveis

  1. A pasta k8s/services/ completa: os seus 5 Services escritos e os 3 Services reparados.
  2. Um RAPPORT.md contendo:
    • para cada Service: o tipo escolhido e uma justificação em duas frases («por que este e não outro») ;
    • o inquérito completo da missão 6 (comando → causa → correção → prova) ;
    • uma captura do painel a mostrar 8 / 8 ;
    • uma captura de kubectl get svc a mostrar todos os seus Services e os seus tipos ;
    • as suas respostas às questões de reflexão.
  3. A saída final de .\outils\valider.ps1.

Questões de reflexão

  1. Por que a aplicação absolutamente não podia funcionar sem Services, quando todos os Pods estavam Running?
  2. Qual é a diferença concreta entre um cartão vermelho e um cartão laranja? O que lhe ensina cada um sobre o sítio onde está o erro?
  3. Por que o Service da base de dados deve ser headless, quando um Service ordinário basta para a API produtos?
  4. O que contém exatamente a lista dos Endpoints, e quem a atualiza? O que acontece quando um Pod fica NotReady?
  5. Apaga um Pod da API produtos; o Kubernetes recria um com um endereço IP diferente. Por que o portal continua a funcionar sem a menor alteração?
  6. Em produção, exporia dez aplicações com dez Services do tipo LoadBalancer? Justifique, e proponha uma alternativa.

Cotação

ElementoPontos
Missão 1 — Service interno e descoberta DNS15
Missão 2 — Exposição externa na porta 3050015
Missão 3 — Service headless e identidades estáveis20
Missão 4 — Multiporta e portas nomeadas15
Missão 5 — Alias para um serviço externo10
Missão 6 — Diagnóstico e reparação (3 avarias)20
Qualidade do relatório e justificação das escolhas5
Bónus — Missão 7+5
Total100 (+5)

Penalizações: −10 por alteração de um ficheiro proibido (apps/, 01-deployments.yaml, 02-statefulset-bd.yaml) ; −5 por endereço IP escrito em concreto.


Critérios de sucesso

CritérioEsperado
Painel8 / 8 cartões verdes
Tipos de ServicesCada um adaptado ao seu uso e justificado
DNS individualbd-0.bd-interne resolvido para um só Pod
MultiportaAs duas portas visíveis, targetPort referenciado por nome
InquéritoAs 3 avarias identificadas, explicadas e corrigidas
ResiliênciaDepois de apagar um Pod, o portal continua a funcionar
Nenhum IP em concretoUnicamente nomes DNS e seletores de labels

Caixa de ferramentas

Nenhuma solução aqui — apenas pistas.

powershell
kubectl get svc                              # types, IP, ports
kubectl describe svc <nom>                   # détails + Endpoints
kubectl get endpoints <nom>                  # QUI se trouve derrière le Service ?
kubectl get pods --show-labels               # les labels réels des Pods
kubectl get pods -l app=<valeur>             # tester un sélecteur
kubectl port-forward deploy/portail 5000:5000    # accéder à un Pod SANS 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        # ce que le portail n'arrive pas à joindre
kubectl delete svc <nom>                     # repartir de zéro sur un Service

Armadilha a conhecer com kubectl run test: se encadear vários comandos depressa, o Pod anterior nem sempre é apagado a tempo e obterá:

Error from server (AlreadyExists): pods "test" already exists

Duas soluções: mudar o nome (test1, test2…) em cada comando, ou limpar antes:

powershell
kubectl delete pod test --ignore-not-found; kubectl run test --rm -i --restart=Never ...

As três perguntas que desbloqueiam 90 % das situações:

  1. O Service existe, com o nome certo? (senão → cartão vermelho: não há nada a depurar, é preciso escrevê-lo)
  2. Os Endpoints estão preenchidos? (vazios → o seletor não corresponde a nenhum label de Pod)
  3. O targetPort corresponde à porta realmente escutada pelo contentor? (senão → ligação recusada)


ANEXO A — As aplicações

Não modifique nenhum destes ficheiros. Recopie-os tal qual nos caminhos indicados.

A.1 — O microserviço genérico

Esta única aplicação serve cinco componentes (api-produits, api-commandes, cache, notifications, bd). O seu nome e a sua porta são fixados por variáveis de ambiente.

Ficheiro: apps/micro/app.py

python
"""Micro-servico generico de demonstracao.

A mesma imagem serve varios componentes: o nome e a porta de escuta sao
fornecidos por variaveis de ambiente (APP_NAME, PORT).
Cada resposta contem o nome do Pod, o que torna visivel a distribuicao
de carga realizada por um 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)

Ficheiro: apps/micro/requirements.txt

text
flask==3.0.3

Ficheiro: 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 — O componente «metriques» (duas portas)

Esta aplicação escuta em simultâneo em duas portas: 8080 (interface web) e 9090 (métricas). É ela que torna a missão 4 possível.

Ficheiro: apps/metriques/app.py

python
"""Componente que expoe DUAS portas em simultaneo.

  - 8080 : interface web         (rota /ping)
  - 9090 : metricas Prometheus   (rota /metrics)

Dois servidores Flask correm em duas threads distintas.
"""

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)

Ficheiro: apps/metriques/requirements.txt

text
flask==3.0.3

Ficheiro: 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 — O portal (painel)

É ele que mostra os 8 cartões e a sua pontuação em direto.

Ficheiro: apps/portail/app.py

python
"""Painel das ligacoes do cluster.

Para cada ligacao, o portal distingue TRES situacoes :
  - VERMELHO : o nome DNS nao existe        -> o Service nao foi criado
  - LARANJA  : o nome resolve, sem resposta -> seletor ou porta errados
  - VERDE    : a comunicacao funciona       -> o Service esta correto
"""

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  # porta pela qual o portal deve ser consultado

CIBLES = [
    {"cle": "externe", "titre": "Acesso externo", "mode": "externe",
     "aide": "O portal deve ser consultado via http://localhost:30500"},
    {"cle": "produits", "titre": "API Produtos", "mode": "http",
     "url": "http://api-produits/ping"},
    {"cle": "bd", "titre": "Base de dados (bd-0)", "mode": "http",
     "url": "http://bd-0.bd-interne:5432/ping"},
    {"cle": "metriques", "titre": "Metricas (2 portas)", "mode": "http2",
     "url": "http://metriques/ping", "url2": "http://metriques:9090/metrics"},
    {"cle": "paiement", "titre": "Pagamento externo", "mode": "dns",
     "hote": "paiement-externe"},
    {"cle": "commandes", "titre": "API Encomendas", "mode": "http",
     "url": "http://api-commandes/ping"},
    {"cle": "cache", "titre": "Cache", "mode": "http",
     "url": "http://cache/ping"},
    {"cle": "notifications", "titre": "Notificacoes", "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", "nome DNS nao encontrado : o Service nao existe"
    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", "resposta HTTP %s" % err.code
    except Exception as err:
        return "orange", "nome resolvido mas nenhuma resposta (%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", "consultado via a porta %s" % PORT_ATTENDU
        return "orange", "consultado via a porta %s : escreva o Service do portal" % port

    if mode == "dns":
        if resout(cible["hote"]):
            return "vert", "o nome %s esta resolvido" % cible["hote"]
        return "rouge", "o nome %s nao esta resolvido" % 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", "as duas portas respondem"
        if etat1 == "rouge" or etat2 == "rouge":
            return "rouge", "porta 80 : %s | porta 9090 : %s" % (det1, det2)
        return "orange", "porta 80 : %s | porta 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="pt">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="refresh" content="3">
  <title>Missao : restabelecer as comunicacoes</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>Missao : restabelecer as comunicacoes do cluster</h1>
  <div class="sous">Servido pelo pod <strong>{pod}</strong> &middot; atualizacao automatica a cada 3 s</div>
  <div class="score">{score} / {total}</div>
  <div class="grille">{tuiles}</div>
  <div class="pied">VERMELHO : o Service nao existe &middot; LARANJA : seletor ou porta errados &middot; VERDE : ligacao estabelecida</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)

Ficheiro: apps/portail/requirements.txt

text
flask==3.0.3

Ficheiro: 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 — Os manifestos fornecidos

Não modifique nenhum destes dois ficheiros. Constituem o existente ao qual os seus Services devem adaptar-se.

Ficheiro: k8s/01-deployments.yaml

yaml
# ---------------------------------------------------------------------------
# OS PODS DA PLATAFORMA — FORNECIDO, NAO MODIFICAR
# Observe com atencao: os LABELS e as PORTAS declarados aqui sao as unicas
# informacoes de que dispõe para escrever os seus 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            # <-- porta NOMEADA
              containerPort: 8080
            - name: prom           # <-- porta NOMEADA
              containerPort: 9090
          readinessProbe:
            httpGet: { path: /health, port: 8080 }
            initialDelaySeconds: 3
            periodSeconds: 5

Ficheiro: k8s/02-statefulset-bd.yaml

yaml
# ---------------------------------------------------------------------------
# A BASE DE DADOS (3 replicas) — FORNECIDO, NAO MODIFICAR
#
# ATENCAO: o campo serviceName abaixo impõe o NOME do Service que
# tera de escrever para que bd-0, bd-1 e bd-2 obtenham cada um um nome DNS.
# ---------------------------------------------------------------------------
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: bd
spec:
  serviceName: bd-interne          # <-- leia bem esta linha
  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 — Os esqueletos de Services a completar

Recopie estes cinco ficheiros e depois substitua cada TODO pelo valor certo. As linhas precedidas de # ? são perguntas a decidir: cabe-lhe a si decidir se deve acrescentar, modificar ou apagar a linha em causa.

Ficheiro: k8s/services/01-api-produits.yaml

yaml
# MISSION 1 — Tornar a API produtos alcancavel a partir do portal.
#
# O portal chama :  http://api-produits        (logo a porta 80)
# Os Pods escutam em : 8000
# Os Pods levam o label : app: api-produits
#
# ? Que tipo de Service para uma comunicacao INTERNA ao cluster ?
apiVersion: v1
kind: Service
metadata:
  name: api-produits          # nome IMPOSTO : nao mudar
spec:
  type: TODO
  selector:
    TODO: TODO
  ports:
    - port: TODO              # a porta pela qual os clientes chamam
      targetPort: TODO        # a porta realmente escutada pelo contentor

Ficheiro: k8s/services/02-portail.yaml

yaml
# MISSION 2 — Tornar o painel acessivel a partir do navegador,
#             no endereco exato : http://localhost:30500
#
# Os Pods escutam em : 5000
# Os Pods levam o label : app: portail
#
# ? Que tipo de Service abre uma porta na MAQUINA ?
# ? Qual e a gama autorizada para esta porta ?
# ? Que campo suplementar e preciso acrescentar para impor a porta 30500 ?
apiVersion: v1
kind: Service
metadata:
  name: portail               # nome IMPOSTO : nao mudar
spec:
  type: TODO
  selector:
    TODO: TODO
  ports:
    - port: TODO
      targetPort: TODO
      # ? falta aqui uma linha

Ficheiro: k8s/services/03-bd-interne.yaml

yaml
# MISSION 3 — Dar um nome DNS INDIVIDUAL a cada replica da base,
#             para poder alcancar precisamente : bd-0.bd-interne
#
# Os Pods escutam em : 5432
# Os Pods levam o label : app: bd
#
# ? Que tipo de Service NAO possui IP virtual unico ?
# ? Que campo, com que valor muito particular, produz este efeito ?
# ? O nome abaixo deve corresponder a que campo do StatefulSet ?
apiVersion: v1
kind: Service
metadata:
  name: bd-interne            # nome IMPOSTO : nao mudar
spec:
  # ? falta aqui uma linha essencial
  selector:
    TODO: TODO
  ports:
    - port: TODO
      targetPort: TODO

Ficheiro: k8s/services/04-metriques.yaml

yaml
# MISSION 4 — Expor DUAS portas num unico e mesmo Service.
#
# O portal chama :  http://metriques         (porta 80)
#              e : http://metriques:9090/metrics
#
# Os Pods escutam em : 8080 (porta nomeada "web") e 9090 (porta nomeada "prom")
# Os Pods levam o label : app: metriques
#
# ? Que restricao se torna OBRIGATORIA assim que um Service expoe varias portas ?
# ? Como fazer targetPort apontar para uma porta do contentor PELO SEU NOME ?
apiVersion: v1
kind: Service
metadata:
  name: metriques             # nome IMPOSTO : nao mudar
spec:
  type: TODO
  selector:
    TODO: TODO
  ports:
    - TODO: TODO              # ? falta um campo obrigatorio em cada entrada
      port: TODO
      targetPort: TODO
    - TODO: TODO
      port: TODO
      targetPort: TODO

Ficheiro: k8s/services/05-paiement-externe.yaml

yaml
# MISSION 5 — Fazer um nome INTERNO apontar para um servico EXTERNO.
#
# O portal usa o nome : paiement-externe
# Este nome deve apontar para : example.com
#
# ? Que tipo de Service cria um simples alias DNS (CNAME) ?
# ? Este tipo possui seletor ? portas ? Pods ?
apiVersion: v1
kind: Service
metadata:
  name: paiement-externe      # nome IMPOSTO : nao mudar
spec:
  type: TODO
  TODO: TODO                  # ? o campo que indica o alvo externo


ANEXO D — Os três Services defeituosos

Recopie estes três ficheiros tal qual, aplique-os e depois diagnostique e corrija. Cada um contém exatamente um erro. Não reescreva o ficheiro do zero: encontre a falta.

Ficheiro: k8s/services/06-casses/casse-1.yaml

yaml
# PANNE 1
# Sintoma : o Service existe, mas "kubectl get endpoints api-commandes"
#            devolve <none>. O portal mostra um cartao LARANJA.
apiVersion: v1
kind: Service
metadata:
  name: api-commandes
spec:
  type: ClusterIP
  selector:
    app: api-commande
  ports:
    - port: 80
      targetPort: 8000

Ficheiro: k8s/services/06-casses/casse-2.yaml

yaml
# PANNE 2
# Sintoma : "kubectl get endpoints cache" mostra mesmo um endereco IP,
#            mas qualquer ligacao falha. O portal mostra um cartao LARANJA.
apiVersion: v1
kind: Service
metadata:
  name: cache
spec:
  type: ClusterIP
  selector:
    app: cache
  ports:
    - port: 80
      targetPort: 6380

Ficheiro: k8s/services/06-casses/casse-3.yaml

yaml
# PANNE 3
# Sintoma : este Service parece perfeito (tipo correto, seletor correto,
#            Endpoints preenchidos, portas coerentes)... e no entanto o portal
#            mostra um cartao VERMELHO e NUNCA o alcanca.
apiVersion: v1
kind: Service
metadata:
  name: notification
spec:
  type: ClusterIP
  selector:
    app: notifications
  ports:
    - port: 80
      targetPort: 7000


ANEXO E — O script de validação

Ficheiro: outils/valider.ps1

powershell
# ---------------------------------------------------------------------------
# Script de validacao — da uma pontuacao, NUNCA a solucao.
# Utilizacao :  .\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 ""

# --- Mission 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

# --- Mission 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

# --- Mission 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

# --- Mission 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

# --- Mission 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

# --- Mission 6 : les trois reparations --------------------------------------
$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 criado pelo Dr. Haythem REHOUMA — Desenvolvimento e implantação de soluções de dados