Mission: Restore Cluster Communications

19 min

Project 12 — Kubernetes Services · Level intermediate → advanced · Estimated duration: 3 to 5 h

All the application code is provided in the annexes of this document. Your work: write, determine, and complete the Services that are missing — that is, make a system communicate that, as it stands, is completely silent.


Table of contents


The context

A team deployed a small online-commerce platform on Kubernetes. The images are built, the Deployments and the StatefulSet are running, all Pods are Running.

And yet, nothing works.

The portal displays a dashboard that is entirely red: it cannot reach any component. The database is unreachable. The cache is invisible. No page is reachable from the browser.

The reason is simple: the person who was supposed to write the Services left without delivering them.

Fundamental reminder that this project will make you live: running Pods do not make an application. Without Services, they are isolated islands, with no stable address or name, unable to find one another.

Your mission: restore all communications, only by writing the right Services.


Essential concepts before you start

This section is a self-contained mini-manual: it contains all the vocabulary needed for the missions. Read it once, come back when a word escapes you.

1. The problem a Service solves

A Pod is mortal: Kubernetes can delete it, move it, recreate it — and its new IP will be different. You therefore never connect to a Pod by its IP.

A Service is a stable object — name, IP, port — that follows the Pods wherever they go. The mechanism is simple:

What links a Service to “its” Pods is the label selector:

yaml
spec:
  selector:
    app: api-produits          # every Pod carrying THIS label is reached

The list of matching Pods forms the Endpoints object — that is your X-ray of the Service.

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

If Endpoints is empty, the selector matches no Pod: that is almost always a typo in a label.


2. The five Service types (the only ones you need)

TypeWhat it is forSeen from outside?In this project
ClusterIPInternal cluster address, default valueNoapi-produits, api-commandes, cache, notifications, metriques
NodePortOpens a fixed port (30000–32767) on every nodeYes, localhost:<nodePort>portail
LoadBalancerAsks the cloud for a public IP (AWS, GCP, Azure)Yes(bonus mission)
HeadlessA ClusterIP without a virtual IP: returns the list of Pod IPs, plus a DNS name per PodNobd-interne
ExternalNameDNS alias to an external name. No Pod, no selector.Nopaiement-externe

Important point: a Service that “does not work” is almost never a type problem. It is almost always selector or port.


3. Internal DNS: the rules that look like magic

In the cluster, CoreDNS automatically builds names according to fixed rules:

You callCoreDNS resolves to
api-produitsthe api-produits Service of the same namespace
api-produits.defaultthe api-produits Service of the default namespace
api-produits.default.svc.cluster.locallong, fully qualified form

Capital corollary: the Service name is the name the application calls. A Service named notification does not answer http://notifications. That trap is at the heart of mission 6.


4. StatefulSet and headless Service: the pair

A Deployment treats its replicas as interchangeable twins (web-abc123-x7k9, web-abc123-p2m1…). Perfect for stateless web.

A StatefulSet instead produces numbered, stable Pods: bd-0, bd-1, bd-2. Each Pod keeps its identity across restarts — indispensable for a database where you must designate precisely the primary.

But a StatefulSet is not enough alone: it must be associated with a headless Service whose name it indicates in its serviceName field.

yaml
kind: StatefulSet
spec:
  serviceName: bd-interne         # <-- points to a headless Service of the same name
  replicas: 3

That headless Service then gives one DNS name per Pod:

bd-0.bd-interne         -> IP of Pod bd-0 ONLY
bd-1.bd-interne         -> IP of Pod bd-1 ONLY
bd-interne              -> IPs of the three Pods (list)

Without the word None in spec.clusterIP, none of these names exist.

yaml
spec:
  clusterIP: None                  # turns the Service into "headless"

Remember this: to reach bd-0.bd-interne, you need two simultaneous conditions — a StatefulSet whose serviceName: bd-interne, and a headless Service named bd-interne. If either is missing, the individual name does not exist.


5. Named ports and multi-port Services

When a Service exposes several ports, each entry necessarily becomes named:

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

The name serves at least two things:

  1. Kubernetes refuses it without a name as soon as there are several entries;
  2. It lets you reference a container port by its name rather than by its number:
yaml
ports:
  - name: web
    port: 80
    targetPort: web              # points to the containerPort named "web"

Concrete advantage: if tomorrow the container moves from 8080 to 8081, you change one place (the Pod). The Service stays correct.


6. ExternalName: a DNS alias, nothing more

The ExternalName type routes nothing. It simply asks CoreDNS to answer:

“The name paiement-externe is example.com.”

An ExternalName never has a selector, Pods, or ports. It is not a proxy: it is an alias.

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

Usefulness: your code keeps the same internal name (paiement-externe) whether it is a service in the cluster, an external SaaS, or an API move. You change the Service, not the code.


7. The three questions that unlock 90 % of outages

Every time a Service does not work, ask these three questions in this order:

That is exactly the method of mission 6.


What you will be able to do at the end

  • Distinguish the five Service types and know when to use each.
  • Use kubectl get svc, kubectl describe svc, kubectl get endpoints as three complementary tools.
  • Pair a StatefulSet correctly with a headless Service.
  • Write a clean multi-port Service, with targetPort referenced by name.
  • Diagnose the three most frequent outages in companies (wrong label, wrong port, wrong name).

The architecture to bring online

Seven components are already running. None is reachable.

ComponentContainer port(s)Pod labelsController
portail5000app: portailDeployment (1 replica)
api-produits8000app: api-produitsDeployment (3 replicas)
api-commandes8000app: api-commandesDeployment (2 replicas)
cache6379app: cacheDeployment (1 replica)
notifications7000app: notificationsDeployment (2 replicas)
metriques8080 (named web) and 9090 (named prom)app: metriquesDeployment (2 replicas)
bd5432app: bdStatefulSet (3 replicas)

File layout

Create exactly this tree, by copying the content of the annexes. Each annex indicates the exact path of the file to create.

projet12-mission-services/
├── 00-ENONCE.md                      <- this document

├── apps/                             <- THE CODE (ANNEX A) — DO NOT MODIFY
│   ├── micro/
│   │   ├── app.py
│   │   ├── requirements.txt
│   │   └── Dockerfile
│   ├── metriques/
│   │   ├── app.py
│   │   ├── requirements.txt
│   │   └── Dockerfile
│   └── portail/
│       ├── app.py
│       ├── requirements.txt
│       └── Dockerfile

├── k8s/
│   ├── 01-deployments.yaml           <- PROVIDED (ANNEX B) — DO NOT MODIFY
│   ├── 02-statefulset-bd.yaml        <- PROVIDED (ANNEX B) — DO NOT MODIFY
│   │
│   └── services/                     <- YOUR TURN
│       ├── 01-api-produits.yaml      <- skeleton to complete (ANNEX C)
│       ├── 02-portail.yaml           <- skeleton to complete (ANNEX C)
│       ├── 03-bd-interne.yaml        <- skeleton to complete (ANNEX C)
│       ├── 04-metriques.yaml         <- skeleton to complete (ANNEX C)
│       ├── 05-paiement-externe.yaml  <- skeleton to complete (ANNEX C)
│       │
│       └── 06-casses/                <- PROVIDED but DEFECTIVE (ANNEX D)
│           ├── casse-1.yaml
│           ├── casse-2.yaml
│           └── casse-3.yaml

├── outils/
│   └── valider.ps1                   <- PROVIDED (ANNEX E)

└── RAPPORT.md                        <- TO WRITE by you

Only three images are needed: micro:1.0 serves five different components (behavior changes via environment variables), metriques:1.0 exposes two ports, and portail:1.0 displays the dashboard.


The dashboard: your progress indicator

The portal continuously queries each component and displays a tile per link, refreshed every 3 seconds:

TileMeaningWhere to look for the error
REDThe DNS name does not existThe Service was not created, or its name is wrong
ORANGEThe name is resolved, but nobody answersThe Service exists, but its selector or its port is wrong
GREENCommunication establishedYour Service is correct

Final goal: the 8 tiles in green, and the counter showing 8 / 8.

This red/orange distinction is not decorative: it tells you which side to search. Red = the Service does not exist (nothing to debug, you must write it). Orange = the Service exists but does not find its Pods or hits the wrong port.

The 8 links checked:

#TileWhat the portal tests
1External accessThat you really consult the portal via port 30500
2Products APIhttp://api-produits/ping
3Databasehttp://bd-0.bd-interne:5432/ping
4Metricshttp://metriques/ping and http://metriques:9090/metrics
5External paymentDNS resolution of the name paiement-externe
6Orders APIhttp://api-commandes/ping
7Cachehttp://cache/ping
8Notificationshttp://notifications/ping

The rules of the game

  1. Absolute ban on modifying the apps/ folder, as well as 01-deployments.yaml and 02-statefulset-bd.yaml. (The whole difficulty is adapting to what already exists: that is exactly the situation of a real job.)
  2. You create and modify only files located in k8s/services/.
  3. No hard-coded IP address. Everything must rest on DNS names and label selectors.
  4. You must determine the type yourself of each Service: nothing tells you whether it is a ClusterIP, a NodePort, a LoadBalancer, a headless service, or an ExternalName. That is the heart of the evaluation.
  5. The Service names are imposed: the application code calls them as-is. A wrong name gives a red tile.
  6. You work on the Kubernetes built into Docker Desktop (Settings → Kubernetes → Enable Kubernetes).

Preparation

Prerequisites — to check once only

  1. Docker Desktop is started (green icon in the system tray).
  2. Kubernetes is enabled in Docker Desktop: Settings → Kubernetes → Enable Kubernetes → Apply & Restart. Without that box checked, no kubectl command will work.
  3. Docker Desktop has at least 4 GB of RAM: Settings → Resources → Memory ≥ 4 GB. This project starts 14 Pods; on 2 GB the machine chokes and Pods stay Pending.
  4. You have Internet (for mission 5 and to download the busybox image).

Startup sequence

powershell
# 0) Switch to the right cluster (required if minikube or kind was already used)
kubectl config use-context docker-desktop
kubectl get nodes                       # must show docker-desktop   Ready

# 1) Build the three 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) Deploy the provided base (Pods, and NO Service)
kubectl apply -f k8s/01-deployments.yaml
kubectl apply -f k8s/02-statefulset-bd.yaml

# 3) Wait until ALL Pods are Ready (about 30 s)
kubectl wait --for=condition=ready pod --all --timeout=180s

# 4) Observe the starting situation
kubectl get pods                        # 14 Pods, all Running
kubectl get svc                         # only "kubernetes": none of your Services

At this stage: all Pods are running and nothing communicates. That is the normal starting point.

Two technical warnings to know from now — this is not a mistake on your part:

  1. Warning Endpoints is deprecated in v1.33+ — Kubernetes systematically shows this message on every kubectl get endpoints. The command still works perfectly, ignore the warning. The equivalent new API is kubectl get endpointslices, but all commands in this lab deliberately use endpoints, more readable to learn.

  2. The dashboard can take 10 to 15 seconds to display the first time: the portal tests 8 network links on each display, each with a 1.5 s timeout. When nothing works yet, it waits each timeout before showing red or orange. Once the Services are correct, the response time drops back to a few hundred milliseconds.

Question to ask yourself immediately: how will you even see the dashboard, since no entry door exists yet?

Lifeline: kubectl port-forward works without any Service, directly on a Pod.

powershell
kubectl port-forward deploy/portail 5000:5000

Then open http://localhost:5000. You will see the dashboard all red, with the “External access” tile in orange (expected: you did not go through port 30500).


The missions

Mission 1 — Make the portal talk to the products API (15 points)

The portal calls http://api-produits on port 80. The API Pods listen on port 8000 and carry the label app: api-produits.

File to complete: k8s/services/01-api-produits.yaml

To determine: the Service type, the selector, as well as port and targetPort.

Validation:

powershell
kubectl apply -f k8s/services/01-api-produits.yaml
kubectl get svc api-produits
kubectl get endpoints api-produits        # must list 3 IP addresses

The API Produits tile turns green.


Mission 2 — Open the entry door (15 points)

The dashboard must be reachable from your browser at the exact address http://localhost:30500. The portal Pods listen on port 5000.

File to complete: k8s/services/02-portail.yaml

To determine: which Service type exposes an application outside the cluster on a fixed machine port? What is the allowed port range for that field?

Validation:

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

The External access tile turns green.

Question to answer in the report: another Service type would also have made the portal reachable from the browser on Docker Desktop. Which one? And what difference would that make in cloud production?


Mission 3 — Give an identity to each database (20 points)

The bd StatefulSet provides 3 replicas. The portal must reach precisely the first one (the primary), at the address:

bd-0.bd-interne

The database Pods carry the label app: bd and listen on port 5432.

File to complete: k8s/services/03-bd-interne.yaml

To determine: which Service type gives an individual DNS name to each Pod, instead of a single virtual IP? Which field must you write, and with which particular value?

Validation:

powershell
# 1) From a utility Pod, reach bd-0 directly:
kubectl run test --rm -i --restart=Never --image=busybox:1.36 -- wget -qO- http://bd-0.bd-interne:5432/ping
# must answer: {"pod":"bd-0","port":5432,"service":"base-de-donnees"}

# 2) Check the DNS entries with the fully qualified name
#    (busybox nslookup does NOT apply search domains, you must give the FQDN):
kubectl run test --rm -i --restart=Never --image=busybox:1.36 -- nslookup bd-0.bd-interne.default.svc.cluster.local
# must return ONE address only (that of Pod bd-0)

kubectl run test --rm -i --restart=Never --image=busybox:1.36 -- nslookup bd-interne.default.svc.cluster.local
# must return THREE addresses (one per StatefulSet Pod)
Trap not to miss
examine the serviceName field of the provided StatefulSet. The name of your Service must match it exactly, otherwise the individual Pod names will never be created.
Technical trap (busybox)
nslookup short-name does not work from a busybox Pod because its resolver does not use the search domains of /etc/resolv.conf. From a real application Pod (such as portail), however, bd-0.bd-interne resolves perfectly. So use wget to test the real application chain, and the FQDN to remove any DNS ambiguity.

Mission 4 — Expose two ports on the same Service (15 points)

The metriques component listens on two ports:

UsageContainer portPort name declared in the Deployment
Web interface8080web
Metrics9090prom

The portal calls http://metriques (port 80) and http://metriques:9090/metrics.

File to complete: k8s/services/04-metriques.yaml

To determine: how do you declare several ports on a Service? Which constraint then becomes mandatory for each entry? And how do you point targetPort to a container port by its name rather than by its number, so the Service stays valid even if the number changes?

Validation:

powershell
kubectl describe svc metriques            # BOTH ports must appear

Mission 5 — Give an internal name to an external service (10 points)

The portal must reach a payment service hosted outside the cluster, but the code calls an internal name: paiement-externe. That name must resolve to example.com.

File to complete: k8s/services/05-paiement-externe.yaml

To determine: which Service type creates a simple DNS alias to an external name, with no selector and no Pod?

Validation:

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

This mission requires the cluster to resolve public names. If you have no Internet access, replace the target with api-produits.default.svc.cluster.local and mention it in your report.


Mission 6 — The investigation: repair three defective Services (20 points)

The folder k8s/services/06-casses/ contains three Services already written… that do not work. Each has a single error, and they are the three most frequent mistakes in companies.

powershell
kubectl apply -f k8s/services/06-casses/
FileObserved symptom
casse-1.yamlThe Service exists, but kubectl get endpoints returns <none>
casse-2.yamlThe Endpoints are filled, but every connection is refused
casse-3.yamlThe Service looks perfect, but the portal never reaches it

For each of the three cases, your report must contain:

  1. the diagnostic command that put you on the trail;
  2. the exact cause of the outage;
  3. the fix applied;
  4. the proof that the link works (green tile + command output).

Recommended method: proceed like an investigator. kubectl describe svc, kubectl get endpoints, kubectl get pods --show-labels, then compare line by line the Service and the Pods. The difference between “empty Endpoints” and “connection refused” already tells you which side to search.


Mission 7 — Bonus: the stretch (5 points)

Choose one only:

  • a) Make the same client always served by the same Pod of the products API. (Hint: a Service field allows “stickiness” based on the client IP.)
  • b) Create a Service without a selector pointing to a fixed external IP address, by writing yourself its Endpoints.
  • c) Write a LoadBalancer Service for the portal, then explain what EXTERNAL-IP becomes on Docker Desktop, and what it would become on AWS.

Automatic validation

A script gives you your score at any time:

powershell
.\outils\valider.ps1

If PowerShell refuses to run the script with a message such as script execution is disabled on this system, bypass the restriction for this command only:

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

Another lasting solution (to do only once for your user):

powershell
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned
[OK]     Mission 1 - api-produits ............. 15/15
[OK]     Mission 2 - portail .................. 15/15
[FAIL]   Mission 3 - bd-interne ...............  0/20   -> clusterIP must be None
[OK]     Mission 4 - metriques ................ 15/15
[FAIL]   Mission 5 - paiement-externe .........  0/10   -> Service not found
[FAIL]   Mission 6 - repairs ..................  7/20   -> cache: no Pod answers

SCORE : 52 / 100

The script gives no solution: it only indicates what fails and where to look.


Deliverables

  1. The complete k8s/services/ folder: your 5 written Services and the 3 repaired Services.
  2. A RAPPORT.md containing:
    • for each Service: the chosen type and a two-sentence justification (“why this one and not another”);
    • the full investigation of mission 6 (command → cause → fix → proof);
    • a screenshot of the dashboard showing 8 / 8;
    • a screenshot of kubectl get svc showing all your Services and their types;
    • your answers to the reflection questions.
  3. The final output of .\outils\valider.ps1.

Reflection questions

  1. Why could the application absolutely not work without Services, even though all Pods were Running?
  2. What is the concrete difference between a red tile and an orange tile? What does each tell you about where the error is?
  3. Why must the database Service be headless, while an ordinary Service is enough for the products API?
  4. What exactly does the Endpoints list contain, and who updates it? What happens when a Pod becomes NotReady?
  5. You delete a products API Pod; Kubernetes recreates one with a different IP address. Why does the portal keep working with no modification at all?
  6. In production, would you expose ten applications with ten LoadBalancer Services? Justify, and propose an alternative.

Grading

ItemPoints
Mission 1 — Internal Service and DNS discovery15
Mission 2 — External exposure on port 3050015
Mission 3 — Headless Service and stable identities20
Mission 4 — Multi-port and named ports15
Mission 5 — Alias to an external service10
Mission 6 — Diagnosis and repair (3 outages)20
Report quality and justification of choices5
Bonus — Mission 7+5
Total100 (+5)

Penalties: −10 per modification of a forbidden file (apps/, 01-deployments.yaml, 02-statefulset-bd.yaml); −5 per hard-coded IP address.


Success criteria

CriterionExpected
Dashboard8 / 8 green tiles
Service typesEach one adapted to its use and justified
Individual DNSbd-0.bd-interne resolved to a single Pod
Multi-portBoth ports visible, targetPort referenced by name
InvestigationThe 3 outages identified, explained, and fixed
ResilienceAfter deleting a Pod, the portal keeps working
No hard-coded IPOnly DNS names and label selectors

Toolbox

No solution here — only leads.

powershell
kubectl get svc                              # types, IP, ports
kubectl describe svc <nom>                   # details + Endpoints
kubectl get endpoints <nom>                  # WHO is behind the Service?
kubectl get pods --show-labels               # the real Pod labels
kubectl get pods -l app=<valeur>             # test a selector
kubectl port-forward deploy/portail 5000:5000    # reach a Pod WITHOUT a 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        # what the portal cannot reach
kubectl delete svc <nom>                     # start over on a Service

Trap to know with kubectl run test: if you chain several commands quickly, the previous Pod is not always deleted in time and you will get:

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

Two solutions: change the name (test1, test2…) on each command, or clean up first:

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

The three questions that unlock 90 % of situations:

  1. Does the Service exist, with the right name? (if not → red tile: there is nothing to debug, you must write it)
  2. Are the Endpoints filled? (empty → the selector matches no Pod label)
  3. Does targetPort match the port actually listened on by the container? (if not → connection refused)


ANNEX A — The applications

Do not modify any of these files. Copy them as-is to the indicated paths.

A.1 — The generic micro-service

This single application serves five components (api-produits, api-commandes, cache, notifications, bd). Its name and port are set by environment variables.

File: apps/micro/app.py

python
"""Generic demonstration micro-service.

The same image serves several components: the name and listen port are
provided by environment variables (APP_NAME, PORT).
Each response contains the Pod name, which makes the load balancing
performed by a Service visible.
"""

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)

File: apps/micro/requirements.txt

text
flask==3.0.3

File: 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 — The “metriques” component (two ports)

This application listens simultaneously on two ports: 8080 (web interface) and 9090 (metrics). It is what makes mission 4 possible.

File: apps/metriques/app.py

python
"""Component exposing TWO ports at the same time.

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

Two Flask servers run in two distinct threads.
"""

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)

File: apps/metriques/requirements.txt

text
flask==3.0.3

File: 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 — The portal (dashboard)

This is what displays the 8 tiles and your live score.

File: apps/portail/app.py

python
"""Dashboard of the cluster links.

For each link, the portal distinguishes THREE situations:
  - RED    : the DNS name does not exist     -> the Service was not created
  - ORANGE : the name resolves, no answer    -> wrong selector or port
  - GREEN  : communication works             -> the Service is correct
"""

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

from flask import Flask, request

app = Flask(__name__)

DELAI = 1.5          # seconds
PORT_ATTENDU = 30500  # port through which the portal must be consulted

CIBLES = [
    {"cle": "externe", "titre": "External access", "mode": "externe",
     "aide": "The portal must be consulted via http://localhost:30500"},
    {"cle": "produits", "titre": "Products API", "mode": "http",
     "url": "http://api-produits/ping"},
    {"cle": "bd", "titre": "Database (bd-0)", "mode": "http",
     "url": "http://bd-0.bd-interne:5432/ping"},
    {"cle": "metriques", "titre": "Metrics (2 ports)", "mode": "http2",
     "url": "http://metriques/ping", "url2": "http://metriques:9090/metrics"},
    {"cle": "paiement", "titre": "External payment", "mode": "dns",
     "hote": "paiement-externe"},
    {"cle": "commandes", "titre": "Orders API", "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", "DNS name not found: the Service does not exist"
    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", "HTTP response %s" % err.code
    except Exception as err:
        return "orange", "name resolved but no answer (%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", "consulted via port %s" % PORT_ATTENDU
        return "orange", "consulted via port %s: write the portal Service" % port

    if mode == "dns":
        if resout(cible["hote"]):
            return "vert", "the name %s is resolved" % cible["hote"]
        return "rouge", "the name %s is not resolved" % 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", "both ports answer"
        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="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="refresh" content="3">
  <title>Mission: restore the 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: restore the cluster communications</h1>
  <div class="sous">Served by pod <strong>{pod}</strong> &middot; automatic refresh every 3 s</div>
  <div class="score">{score} / {total}</div>
  <div class="grille">{tuiles}</div>
  <div class="pied">RED: the Service does not exist &middot; ORANGE: wrong selector or port &middot; GREEN: link established</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)

File: apps/portail/requirements.txt

text
flask==3.0.3

File: 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"]


ANNEX B — The provided manifests

Do not modify either of these two files. They are the existing base that your Services must adapt to.

File: k8s/01-deployments.yaml

yaml
# ---------------------------------------------------------------------------
# THE PLATFORM PODS — PROVIDED, DO NOT MODIFY
# Observe carefully: the LABELS and PORTS declared here are the only
# information you have to write your 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            # <-- NAMED port
              containerPort: 8080
            - name: prom           # <-- NAMED port
              containerPort: 9090
          readinessProbe:
            httpGet: { path: /health, port: 8080 }
            initialDelaySeconds: 3
            periodSeconds: 5

File: k8s/02-statefulset-bd.yaml

yaml
# ---------------------------------------------------------------------------
# THE DATABASE (3 replicas) — PROVIDED, DO NOT MODIFY
#
# WARNING: the serviceName field below imposes the NAME of the Service that
# you will have to write so that bd-0, bd-1 and bd-2 each get a DNS name.
# ---------------------------------------------------------------------------
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: bd
spec:
  serviceName: bd-interne          # <-- read this line carefully
  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


ANNEX C — The Service skeletons to complete

Copy these five files, then replace each TODO with the right value. Lines starting with # ? are questions to decide: you decide whether to add, change, or delete the line concerned.

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

yaml
# MISSION 1 — Make the products API reachable from the portal.
#
# The portal calls:  http://api-produits        (so port 80)
# The Pods listen on: 8000
# The Pods carry the label: app: api-produits
#
# ? Which Service type for INTERNAL cluster communication?
apiVersion: v1
kind: Service
metadata:
  name: api-produits          # IMPOSED name: do not change
spec:
  type: TODO
  selector:
    TODO: TODO
  ports:
    - port: TODO              # the port clients call
      targetPort: TODO        # the port actually listened on by the container

File: k8s/services/02-portail.yaml

yaml
# MISSION 2 — Make the dashboard reachable from the browser,
#             at the exact address: http://localhost:30500
#
# The Pods listen on: 5000
# The Pods carry the label: app: portail
#
# ? Which Service type opens a port on the MACHINE?
# ? What is the allowed range for this port?
# ? Which extra field must you add to impose port 30500?
apiVersion: v1
kind: Service
metadata:
  name: portail               # IMPOSED name: do not change
spec:
  type: TODO
  selector:
    TODO: TODO
  ports:
    - port: TODO
      targetPort: TODO
      # ? a line is missing here

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

yaml
# MISSION 3 — Give an INDIVIDUAL DNS name to each database replica,
#             so you can reach precisely: bd-0.bd-interne
#
# The Pods listen on: 5432
# The Pods carry the label: app: bd
#
# ? Which Service type does NOT have a single virtual IP?
# ? Which field, with which very particular value, produces this effect?
# ? The name below must match which StatefulSet field?
apiVersion: v1
kind: Service
metadata:
  name: bd-interne            # IMPOSED name: do not change
spec:
  # ? an essential line is missing here
  selector:
    TODO: TODO
  ports:
    - port: TODO
      targetPort: TODO

File: k8s/services/04-metriques.yaml

yaml
# MISSION 4 — Expose TWO ports on one and the same Service.
#
# The portal calls:  http://metriques         (port 80)
#                  and: http://metriques:9090/metrics
#
# The Pods listen on: 8080 (port named "web") and 9090 (port named "prom")
# The Pods carry the label: app: metriques
#
# ? Which constraint becomes MANDATORY as soon as a Service exposes several ports?
# ? How do you point targetPort to a container port BY ITS NAME?
apiVersion: v1
kind: Service
metadata:
  name: metriques             # IMPOSED name: do not change
spec:
  type: TODO
  selector:
    TODO: TODO
  ports:
    - TODO: TODO              # ? a mandatory field is missing on each entry
      port: TODO
      targetPort: TODO
    - TODO: TODO
      port: TODO
      targetPort: TODO

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

yaml
# MISSION 5 — Point an INTERNAL name to an EXTERNAL service.
#
# The portal uses the name: paiement-externe
# That name must resolve to: example.com
#
# ? Which Service type creates a simple DNS alias (CNAME)?
# ? Does this type have a selector? ports? Pods?
apiVersion: v1
kind: Service
metadata:
  name: paiement-externe      # IMPOSED name: do not change
spec:
  type: TODO
  TODO: TODO                  # ? the field that indicates the external target


ANNEX D — The three defective Services

Copy these three files as-is, apply them, then diagnose and fix. Each contains exactly one error. Do not rewrite the file from scratch: find the fault.

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

yaml
# OUTAGE 1
# Symptom: the Service exists, but "kubectl get endpoints api-commandes"
#          returns <none>. The portal shows an ORANGE tile.
apiVersion: v1
kind: Service
metadata:
  name: api-commandes
spec:
  type: ClusterIP
  selector:
    app: api-commande
  ports:
    - port: 80
      targetPort: 8000

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

yaml
# OUTAGE 2
# Symptom: "kubectl get endpoints cache" does show an IP address,
#          but every connection fails. The portal shows an ORANGE tile.
apiVersion: v1
kind: Service
metadata:
  name: cache
spec:
  type: ClusterIP
  selector:
    app: cache
  ports:
    - port: 80
      targetPort: 6380

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

yaml
# OUTAGE 3
# Symptom: this Service looks perfect (correct type, correct selector,
#          Endpoints filled, coherent ports)... and yet the portal
#          shows a RED tile and NEVER reaches it.
apiVersion: v1
kind: Service
metadata:
  name: notification
spec:
  type: ClusterIP
  selector:
    app: notifications
  ports:
    - port: 80
      targetPort: 7000


ANNEX E — The validation script

File: outils/valider.ps1

powershell
# ---------------------------------------------------------------------------
# Validation script — gives a score, NEVER the solution.
# Usage:  .\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 { "[FAIL]  " }
    $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: restore the communications ===" -ForegroundColor Cyan
Write-Host ""

# --- Mission 1 : api-produits ---------------------------------------------
$p = 0; $note = ""
if (-not (Existe "api-produits")) { $note = "Service api-produits not found" }
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 = "Empty Endpoints: the selector matches no Pod" }
    elseif ("$tp" -ne "8000") { $note = "targetPort does not match the listened port" }
    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 not found" }
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 = "the port exposed on the machine must be 30500 (current: '$np')" }
    elseif ($type -notin @("NodePort", "LoadBalancer")) { $note = "type unsuitable for external access" }
    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 not found (check the StatefulSet serviceName)" }
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 = "this Service must NOT have a virtual IP" }
    elseif (-not $eps) { $note = "Empty Endpoints: check the selector" }
    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 not found" }
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 = "a port is missing: two are expected (80 and 9090)" }
    elseif (-not $noms) { $note = "each port must have a name when there are several" }
    elseif ($cible -match '^\s*\d+(\s+\d+)*\s*$') { $note = "targetPort must reference the ports BY THEIR NAME" }
    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 not found" }
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 = "this is not the expected type for a DNS alias" }
    elseif (-not $cible) { $note = "the external target is not set" }
    else { $p = 10 }
}
Afficher "Mission 5 - paiement-externe" $p 10 $note; $total += $p

# --- Mission 6 : the three repairs ------------------------------------------
$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 not found"; 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) : empty Endpoints" }
    elseif ("$tp" -ne $cas.port) { $notes += "$($cas.nom) : no Pod answers on this port" }
    else { $p += 7 }
}
if ($p -gt 20) { $p = 20 }
Afficher "Mission 6 - repairs" $p 20 ($notes -join " | "); $total += $p

Write-Host ""
$couleur = if ($total -ge 90) { "Green" } elseif ($total -ge 50) { "Yellow" } else { "Red" }
Write-Host ("AUTOMATIC SCORE : {0} / 95" -f $total) -ForegroundColor $couleur
Write-Host "   (+5 for report quality, +5 bonus: evaluated manually)" -ForegroundColor DarkGray
Write-Host ""
Write-Host "Reminder: the dashboard must show 8 / 8 at http://localhost:30500" -ForegroundColor DarkGray
Write-Host ""

Course created by Dr. Haythem REHOUMA — Development and Deployment of Data Solutions