Αποστολή: αποκατάσταση επικοινωνιών cluster

20 λεπτά

Έργο 12 — Οι υπηρεσίες Kubernetes · Επίπεδο ενδιάμεσο → προχωρημένο · Εκτιμώμενη διάρκεια: 3 έως 5 ώρες

Όλος ο κώδικας εφαρμογής σας παρέχεται στο παράρτημα αυτού του εγγράφου. Η δουλειά σας: να γράψετε, να καθορίσετε και να συμπληρώσετε τα Services που λείπουν — δηλαδή να κάνετε να επικοινωνεί ένα σύστημα που, ως έχει, είναι εντελώς άφωνο.


Πίνακας περιεχομένων


Το πλαίσιο

Μια ομάδα ανέπτυξε μια μικρή πλατφόρμα ηλεκτρονικού εμπορίου στο Kubernetes. Οι εικόνες είναι κατασκευασμένες, τα Deployments και το StatefulSet τρέχουν, όλα τα Pods είναι Running.

Και όμως, τίποτα δεν λειτουργεί.

Το portail εμφανίζει έναν πίνακα ελέγχου εντελώς κόκκινο: δεν φτάνει κανένα στοιχείο. Η βάση δεδομένων είναι απρόσιτη. Η κρυφή μνήμη είναι αόρατη. Καμία σελίδα δεν είναι προσβάσιμη από τον περιηγητή.

Ο λόγος είναι απλός: το άτομο που έπρεπε να γράψει τα Services έφυγε χωρίς να τα παραδώσει.

Θεμελιώδης υπενθύμιση που αυτό το έργο θα σας κάνει να ζήσετε: Pods που τρέχουν δεν αποτελούν εφαρμογή. Χωρίς Services, είναι απομονωμένα νησιά, χωρίς σταθερή διεύθυνση ούτε όνομα, ανίκανα να βρουν το ένα το άλλο.

Η αποστολή σας: να αποκαταστήσετε όλες τις επικοινωνίες, μόνο γράφοντας τα σωστά Services.


Ουσιώδεις έννοιες πριν ξεκινήσετε

Αυτή η ενότητα είναι ένα αυτάρκες μίνι-εγχειρίδιο: περιέχει όλο το λεξιλόγιο που είναι απαραίτητο για τις αποστολές. Διαβάστε την μία φορά, επιστρέψτε όταν σας ξεφεύγει μια λέξη.

1. Το πρόβλημα που λύνει ένα Service

Ένα Pod είναι θνητό: το Kubernetes μπορεί να το διαγράψει, να το μετακινήσει, να το αναδημιουργήσει — και η νέα IP του θα είναι διαφορετική. Δεν συνδεόμαστε λοιπόν ποτέ σε ένα Pod μέσω της IP του.

Ένα Service είναι ένα σταθερό αντικείμενο — όνομα, IP, θύρα — που ακολουθεί τα Pods όπου κι αν πάνε. Ο μηχανισμός είναι απλός:

Αυτό που συνδέει ένα Service με «τα» Pods του είναι ο επιλογέας ετικετών:

yaml
spec:
  selector:
    app: api-produits          # κάθε Pod που φέρει ΑΥΤΗ την ετικέτα προσεγγίζεται

Η λίστα των Pods που αντιστοιχούν σχηματίζει το αντικείμενο Endpoints — είναι η ακτινογραφία σας του Service.

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

Αν τα Endpoints είναι κενά, ο επιλογέας δεν αντιστοιχεί σε κανένα Pod: σχεδόν πάντα είναι τυπογραφικό σε μια ετικέτα.


2. Οι πέντε τύποι Services (οι μόνοι που χρειάζεστε)

ΤύποςΣε τι χρησιμεύειΟρατό από έξω;Σε αυτό το έργο
ClusterIPΕσωτερική διεύθυνση στο σύμπλεγμα, προεπιλεγμένη τιμήΌχιapi-produits, api-commandes, cache, notifications, metriques
NodePortΑνοίγει σταθερή θύρα (30000–32767) σε κάθε κόμβοΝαι, localhost:<nodePort>portail
LoadBalancerΖητά δημόσια IP από το νέφος (AWS, GCP, Azure)Ναι(αποστολή μπόνους)
HeadlessΈνα ClusterIP χωρίς εικονική IP: επιστρέφει τη λίστα των IP των Pods, συν ένα όνομα DNS ανά PodΌχιbd-interne
ExternalNameΨευδώνυμο DNS προς εξωτερικό όνομα. Κανένα Pod, κανένας επιλογέας.Όχιpaiement-externe

Σημαντικό σημείο: ένα Service που «δεν λειτουργεί» σχεδόν ποτέ δεν είναι πρόβλημα τύπου. Είναι σχεδόν πάντα επιλογέας ή θύρα.


3. Εσωτερικό DNS: οι κανόνες που δίνουν την ψευδαίσθηση μαγείας

Στο σύμπλεγμα, το CoreDNS κατασκευάζει αυτόματα ονόματα σύμφωνα με σταθερούς κανόνες:

ΚαλείτεΤο CoreDNS επιλύει προς
api-produitsτο Service api-produits του ίδιου χώρου ονομάτων
api-produits.defaultτο Service api-produits του χώρου ονομάτων default
api-produits.default.svc.cluster.localμακρά και πλήρως προσδιορισμένη μορφή

Κεφαλαιώδες πόρισμα: το όνομα του Service είναι το όνομα που καλεί η εφαρμογή. Ένα Service με όνομα notification δεν απαντά στο http://notifications. Αυτή η παγίδα είναι στην καρδιά της αποστολής 6.


4. StatefulSet και Service headless: το δίδυμο

Ένα Deployment αντιμετωπίζει τα αντίγραφά του ως εναλλάξιμα δίδυμα (web-abc123-x7k9, web-abc123-p2m1…). Ιδανικό για web χωρίς κατάσταση.

Ένα StatefulSet παράγει αντίθετα αριθμημένα και σταθερά Pods: bd-0, bd-1, bd-2. Κάθε Pod κρατά την ταυτότητά του μέσα από τις επανεκκινήσεις — απαραίτητο για μια βάση δεδομένων όπου πρέπει να ορίσουμε ακριβώς την πρωτεύουσα.

Αλλά ένα StatefulSet δεν αρκεί μόνο του: απαιτεί να είναι συνδεδεμένο με ένα Service headless του οποίου δηλώνει το όνομα στο πεδίο serviceName.

yaml
kind: StatefulSet
spec:
  serviceName: bd-interne         # <-- δείχνει προς ένα Service headless του ίδιου ονόματος
  replicas: 3

Αυτό το Service headless δίνει τότε ένα όνομα DNS ανά Pod:

bd-0.bd-interne         -> IP του Pod bd-0 ΜΟΝΟ
bd-1.bd-interne         -> IP του Pod bd-1 ΜΟΝΟ
bd-interne              -> IP των τριών Pods (λίστα)

Χωρίς τη λέξη None στο spec.clusterIP, κανένα από αυτά τα ονόματα δεν υπάρχει.

yaml
spec:
  clusterIP: None                  # μετατρέπει το Service σε "headless"

Κρατήστε αυτό: για να φτάσετε το bd-0.bd-interne, χρειάζονται δύο ταυτόχρονες συνθήκες — ένα StatefulSet με serviceName: bd-interne, και ένα headless Service με όνομα bd-interne. Αν λείπει μία, το ατομικό όνομα δεν υπάρχει.


5. Ονομασμένες θύρες και Services πολλαπλών θυρών

Όταν ένα Service εκθέτει πολλές θύρες, κάθε εγγραφή γίνεται υποχρεωτικά ονομασμένη:

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

Το όνομα χρησιμεύει τουλάχιστον σε δύο πράγματα:

  1. Το Kubernetes το αρνείται χωρίς όνομα μόλις υπάρχουν πολλές εγγραφές·
  2. Επιτρέπει την αναφορά σε θύρα του κοντέινερ με το όνομά της αντί με τον αριθμό της:
yaml
ports:
  - name: web
    port: 80
    targetPort: web              # παραπέμπει στο containerPort με όνομα "web"

Συγκεκριμένο πλεονέκτημα: αν αύριο το κοντέινερ περάσει από 8080 σε 8081, τροποποιούμε ένα μόνο σημείο (το Pod). Το Service μένει σωστό.


6. ExternalName: ένα ψευδώνυμο DNS, τίποτα περισσότερο

Ο τύπος ExternalName δεν δρομολογεί τίποτα. Απλώς ζητά από το CoreDNS να απαντήσει:

«Το όνομα paiement-externe είναι το example.com

Ένα ExternalName δεν έχει ποτέ επιλογέα, Pods ή θύρες. Δεν είναι διακομιστής μεσολάβησης: είναι ψευδώνυμο.

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

Χρησιμότητα: ο κώδικάς σας κρατά το ίδιο εσωτερικό όνομα (paiement-externe) είτε πρόκειται για υπηρεσία στο σύμπλεγμα, για εξωτερική υπηρεσία SaaS, είτε για μετακόμιση API. Αλλάζουμε το Service, όχι τον κώδικα.


7. Οι τρεις ερωτήσεις που ξεκλειδώνουν το 90 % των βλαβών

Κάθε φορά που ένα Service δεν λειτουργεί, θέστε αυτές τις τρεις ερωτήσεις με αυτή τη σειρά:

Αυτή είναι ακριβώς η μέθοδος της αποστολής 6.


Τι θα ξέρετε να κάνετε στο τέλος

  • Να διακρίνετε τους πέντε τύπους Services και να ξέρετε πότε να χρησιμοποιείτε καθέναν.
  • Να χρησιμοποιείτε τα kubectl get svc, kubectl describe svc, kubectl get endpoints ως τρία συμπληρωματικά εργαλεία.
  • Να συνδέετε σωστά ένα StatefulSet με ένα Service headless.
  • Να γράφετε ένα καθαρό Service πολλαπλών θυρών, με targetPort αναφερόμενο με όνομα.
  • Να διαγιγνώσκετε τις τρεις συχνότερες βλάβες στην επιχείρηση (εσφαλμένη ετικέτα, εσφαλμένη θύρα, εσφαλμένο όνομα).

Η αρχιτεκτονική προς θέση σε λειτουργία

Επτά στοιχεία τρέχουν ήδη. Κανένα δεν είναι προσβάσιμο.

ΣτοιχείοΘύρα(-ες) του κοντέινερΕτικέτα των PodsΕλεγκτής
portail5000app: portailDeployment (1 αντίγραφο)
api-produits8000app: api-produitsDeployment (3 αντίγραφα)
api-commandes8000app: api-commandesDeployment (2 αντίγραφα)
cache6379app: cacheDeployment (1 αντίγραφο)
notifications7000app: notificationsDeployment (2 αντίγραφα)
metriques8080 (ονομασμένο web) και 9090 (ονομασμένο prom)app: metriquesDeployment (2 αντίγραφα)
bd5432app: bdStatefulSet (3 αντίγραφα)

Διάταξη των αρχείων

Δημιουργήστε ακριβώς αυτή τη δενδροδομή, αντιγράφοντας το περιεχόμενο των παραρτημάτων. Κάθε παράρτημα δηλώνει την ακριβή διαδρομή του αρχείου προς δημιουργία.

projet12-mission-services/
├── 00-ENONCE.md                      <- αυτό το έγγραφο

├── apps/                             <- Ο ΚΩΔΙΚΑΣ (ΠΑΡΑΡΤΗΜΑ A) — ΝΑ ΜΗΝ ΤΡΟΠΟΠΟΙΗΘΕΙ
│   ├── micro/
│   │   ├── app.py
│   │   ├── requirements.txt
│   │   └── Dockerfile
│   ├── metriques/
│   │   ├── app.py
│   │   ├── requirements.txt
│   │   └── Dockerfile
│   └── portail/
│       ├── app.py
│       ├── requirements.txt
│       └── Dockerfile

├── k8s/
│   ├── 01-deployments.yaml           <- ΠΑΡΕΧΕΤΑΙ (ΠΑΡΑΡΤΗΜΑ B) — ΝΑ ΜΗΝ ΤΡΟΠΟΠΟΙΗΘΕΙ
│   ├── 02-statefulset-bd.yaml        <- ΠΑΡΕΧΕΤΑΙ (ΠΑΡΑΡΤΗΜΑ B) — ΝΑ ΜΗΝ ΤΡΟΠΟΠΟΙΗΘΕΙ
│   │
│   └── services/                     <- ΣΕΙΡΑ ΣΑΣ
│       ├── 01-api-produits.yaml      <- σκελετός προς συμπλήρωση (ΠΑΡΑΡΤΗΜΑ C)
│       ├── 02-portail.yaml           <- σκελετός προς συμπλήρωση (ΠΑΡΑΡΤΗΜΑ C)
│       ├── 03-bd-interne.yaml        <- σκελετός προς συμπλήρωση (ΠΑΡΑΡΤΗΜΑ C)
│       ├── 04-metriques.yaml         <- σκελετός προς συμπλήρωση (ΠΑΡΑΡΤΗΜΑ C)
│       ├── 05-paiement-externe.yaml  <- σκελετός προς συμπλήρωση (ΠΑΡΑΡΤΗΜΑ C)
│       │
│       └── 06-casses/                <- ΠΑΡΕΧΟΝΤΑΙ αλλά ΕΛΑΤΤΩΜΑΤΙΚΑ (ΠΑΡΑΡΤΗΜΑ D)
│           ├── casse-1.yaml
│           ├── casse-2.yaml
│           └── casse-3.yaml

├── outils/
│   └── valider.ps1                   <- ΠΑΡΕΧΕΤΑΙ (ΠΑΡΑΡΤΗΜΑ E)

└── RAPPORT.md                        <- ΝΑ ΣΥΝΤΑΧΘΕΙ από εσάς

Τρεις εικόνες μόνο είναι απαραίτητες: το micro:1.0 χρησιμεύει σε πέντε διαφορετικά στοιχεία (η συμπεριφορά αλλάζει μέσω μεταβλητών περιβάλλοντος), το metriques:1.0 εκθέτει δύο θύρες, και το portail:1.0 εμφανίζει τον πίνακα ελέγχου.


Ο πίνακας ελέγχου: ο δείκτης προόδου σας

Το portail ανακρίνει συνεχώς κάθε στοιχείο και εμφανίζει ένα πλακίδιο ανά σύνδεση, ανανεωμένο κάθε 3 δευτερόλεπτα:

ΠλακίδιοΣημασίαΠού να αναζητήσετε το σφάλμα
ΚΟΚΚΙΝΟΤο όνομα DNS δεν υπάρχειΤο Service δεν δημιουργήθηκε, ή το όνομά του είναι εσφαλμένο
ΠΟΡΤΟΚΑΛΙΤο όνομα επιλύεται, αλλά κανείς δεν απαντάΤο Service υπάρχει, αλλά ο επιλογέας ή η θύρα του είναι εσφαλμένα
ΠΡΑΣΙΝΟΗ επικοινωνία αποκαταστάθηκεΤο Service σας είναι σωστό

Τελικός στόχος: τα 8 πλακίδια πράσινα, και ο μετρητής να εμφανίζει 8 / 8.

Αυτή η διάκριση κόκκινο/πορτοκαλί δεν είναι διακοσμητική: σας λέει από ποια πλευρά να αναζητήσετε. Κόκκινο = το Service δεν υπάρχει (τίποτα προς αποσφαλμάτωση, πρέπει να γραφτεί). Πορτοκαλί = το Service υπάρχει αλλά δεν βρίσκει τα Pods του ή χτυπά στη λάθος θύρα.

Οι 8 επαληθευμένες συνδέσεις:

#ΠλακίδιοΤι δοκιμάζει το portail
1Εξωτερική πρόσβασηΌτι συμβουλεύεστε όντως το portail μέσω της θύρας 30500
2API Προϊόντωνhttp://api-produits/ping
3Βάση δεδομένωνhttp://bd-0.bd-interne:5432/ping
4Μετρικέςhttp://metriques/ping και http://metriques:9090/metrics
5Εξωτερική πληρωμήΕπίλυση DNS του ονόματος paiement-externe
6API Παραγγελιώνhttp://api-commandes/ping
7Κρυφή μνήμηhttp://cache/ping
8Ειδοποιήσειςhttp://notifications/ping

Οι κανόνες του παιχνιδιού

  1. Απόλυτη απαγόρευση τροποποίησης του φακέλου apps/, καθώς και των 01-deployments.yaml και 02-statefulset-bd.yaml. (Όλη η δυσκολία συνίσταται στην προσαρμογή στο υπάρχον: αυτή είναι ακριβώς η κατάσταση μιας πραγματικής θέσης εργασίας.)
  2. Δημιουργείτε και τροποποιείτε μόνο αρχεία που βρίσκονται στο k8s/services/.
  3. Καμία διεύθυνση IP στα σκληρά. Όλα πρέπει να βασίζονται στα ονόματα DNS και τους επιλογείς ετικετών.
  4. Πρέπει να καθορίσετε μόνοι σας τον τύπο κάθε Service: τίποτα δεν σας λέει αν πρόκειται για ClusterIP, NodePort, LoadBalancer, υπηρεσία headless ή ExternalName. Αυτό είναι η καρδιά της αξιολόγησης.
  5. Τα ονόματα των Services είναι επιβεβλημένα: ο κώδικας εφαρμογής τα καλεί ως έχουν. Ένα εσφαλμένο όνομα δίνει κόκκινο πλακίδιο.
  6. Εργάζεστε στο ενσωματωμένο Kubernetes του Docker Desktop (Settings → Kubernetes → Enable Kubernetes).

Προετοιμασία

Προαπαιτούμενα — να επαληθευτούν μία μόνο φορά

  1. Το Docker Desktop είναι εκκινημένο (πράσινο εικονίδιο στη γραμμή συστήματος).
  2. Το Kubernetes είναι ενεργό στο Docker Desktop: Settings → Kubernetes → Enable Kubernetes → Apply & Restart. Χωρίς αυτό το πλαίσιο τσεκαρισμένο, καμία εντολή kubectl δεν θα λειτουργήσει.
  3. Το Docker Desktop διαθέτει τουλάχιστον 4 GB RAM: Settings → Resources → Memory ≥ 4 GB. Αυτό το έργο εκκινεί 14 Pods· με 2 GB το μηχάνημα πνίγεται και Pods μένουν σε Pending.
  4. Έχετε Διαδίκτυο (για την αποστολή 5 και για τη λήψη της εικόνας busybox).

Ακολουθία εκκίνησης

powershell
# 0) Τοποθέτηση στο σωστό σύμπλεγμα (απαραίτητο αν έχουν ήδη χρησιμοποιηθεί minikube ή kind)
kubectl config use-context docker-desktop
kubectl get nodes                       # πρέπει να εμφανίζει docker-desktop   Ready

# 1) Κατασκευή των τριών εικόνων
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) Ανάπτυξη της παρεχόμενης βάσης (Pods, και ΚΑΝΕΝΑ Service)
kubectl apply -f k8s/01-deployments.yaml
kubectl apply -f k8s/02-statefulset-bd.yaml

# 3) Αναμονή να είναι Ready ΟΛΑ τα Pods (περίπου 30 s)
kubectl wait --for=condition=ready pod --all --timeout=180s

# 4) Διαπίστωση της αρχικής κατάστασης
kubectl get pods                        # 14 Pods, όλα Running
kubectl get svc                         # μόνο "kubernetes" : κανένα από τα Services σας

Σε αυτό το στάδιο: όλα τα Pods τρέχουν και τίποτα δεν επικοινωνεί. Αυτό είναι το κανονικό σημείο εκκίνησης.

Δύο τεχνικές προειδοποιήσεις που πρέπει να γνωρίζετε από τώρα — δεν είναι σφάλμα από μέρους σας:

  1. Warning Endpoints is deprecated in v1.33+ — το Kubernetes εμφανίζει συστηματικά αυτό το μήνυμα σε κάθε kubectl get endpoints. Η εντολή λειτουργεί πάντα τέλεια, αγνοήστε την προειδοποίηση. Το νέο ισοδύναμο API είναι kubectl get endpointslices, αλλά όλες οι εντολές αυτού του εργαστηρίου χρησιμοποιούν ηθελημένα τα endpoints, πιο αναγνώσιμα για μάθηση.

  2. Ο πίνακας ελέγχου μπορεί να χρειαστεί 10 έως 15 δευτερόλεπτα για να εμφανιστεί την πρώτη φορά: το portail δοκιμάζει 8 συνδέσεις δικτύου σε κάθε εμφάνιση, καθεμία με καθυστέρηση αναμονής 1,5 s. Όταν τίποτα δεν λειτουργεί ακόμη, περιμένει κάθε καθυστέρηση πριν εμφανίσει κόκκινο ή πορτοκαλί. Μόλις τα Services είναι σωστά, ο χρόνος απόκρισης πέφτει σε μερικές εκατοντάδες χιλιοστά του δευτερολέπτου.

Ερώτηση να θέσετε στον εαυτό σας αμέσως: πώς θα δείτε έστω τον πίνακα ελέγχου, αφού καμία πόρτα εισόδου δεν υπάρχει ακόμη;

Σωσίβιο: το kubectl port-forward λειτουργεί χωρίς κανένα Service, απευθείας σε ένα Pod.

powershell
kubectl port-forward deploy/portail 5000:5000

Έπειτα ανοίξτε το http://localhost:5000. Θα δείτε τον πίνακα ελέγχου εντελώς κόκκινο, με το πλακίδιο « Εξωτερική πρόσβαση » σε πορτοκαλί (κανονικό: δεν περάσατε από τη θύρα 30500).


Οι αποστολές

Αποστολή 1 — Να κάνετε το portail να μιλήσει στην API προϊόντων (15 βαθμοί)

Το portail καλεί το http://api-produits στη θύρα 80. Τα Pods της API ακούν στη θύρα 8000 και φέρουν την ετικέτα app: api-produits.

Αρχείο προς συμπλήρωση: k8s/services/01-api-produits.yaml

Προς καθορισμό: ο τύπος του Service, ο επιλογέας, καθώς και port και targetPort.

Επικύρωση:

powershell
kubectl apply -f k8s/services/01-api-produits.yaml
kubectl get svc api-produits
kubectl get endpoints api-produits        # πρέπει να απαριθμεί 3 διευθύνσεις IP

Το πλακίδιο API Προϊόντων γίνεται πράσινο.


Αποστολή 2 — Άνοιγμα της πόρτας εισόδου (15 βαθμοί)

Ο πίνακας ελέγχου πρέπει να είναι προσβάσιμος από τον περιηγητή σας στην ακριβή διεύθυνση http://localhost:30500. Τα Pods του portail ακούν στη θύρα 5000.

Αρχείο προς συμπλήρωση: k8s/services/02-portail.yaml

Προς καθορισμό: ποιος τύπος Service εκθέτει μια εφαρμογή εκτός του συμπλέγματος σε σταθερή θύρα του μηχανήματος; Ποιο είναι το επιτρεπόμενο εύρος θυρών για αυτό το πεδίο;

Επικύρωση:

powershell
kubectl get svc portail                   # PORT(S) πρέπει να εμφανίζει 80:30500/TCP
start http://localhost:30500

Το πλακίδιο Εξωτερική πρόσβαση γίνεται πράσινο.

Ερώτηση προς επεξεργασία στην έκθεση: ένας άλλος τύπος Service θα καθιστούσε επίσης το portail προσβάσιμο από τον περιηγητή στο Docker Desktop. Ποιος; Και ποια διαφορά θα έκανε αυτό στην παραγωγή στο νέφος;


Αποστολή 3 — Να δώσετε ταυτότητα σε κάθε βάση δεδομένων (20 βαθμοί)

Το StatefulSet bd παρέχει 3 αντίγραφα. Το portail πρέπει να φτάσει ακριβώς την πρώτη (την πρωτεύουσα), στη διεύθυνση:

bd-0.bd-interne

Τα Pods της βάσης φέρουν την ετικέτα app: bd και ακούν στη θύρα 5432.

Αρχείο προς συμπλήρωση: k8s/services/03-bd-interne.yaml

Προς καθορισμό: ποιος τύπος Service δίνει ένα ατομικό όνομα DNS σε κάθε Pod, αντί για μία μοναδική εικονική IP; Ποιο πεδίο πρέπει να γραφτεί, και με ποια ιδιαίτερη τιμή;

Επικύρωση:

powershell
# 1) Από ένα βοηθητικό Pod, σύνδεση απευθείας στο bd-0 :
kubectl run test --rm -i --restart=Never --image=busybox:1.36 -- wget -qO- http://bd-0.bd-interne:5432/ping
# πρέπει να απαντήσει : {"pod":"bd-0","port":5432,"service":"base-de-donnees"}

# 2) Επαλήθευση των εγγραφών DNS με το πλήρως προσδιορισμένο όνομα
#    (το busybox nslookup ΔΕΝ εφαρμόζει τα search domains, πρέπει να δοθεί το FQDN) :
kubectl run test --rm -i --restart=Never --image=busybox:1.36 -- nslookup bd-0.bd-interne.default.svc.cluster.local
# πρέπει να επιστρέψει ΜΙΑ μόνο διεύθυνση (αυτή του Pod bd-0)

kubectl run test --rm -i --restart=Never --image=busybox:1.36 -- nslookup bd-interne.default.svc.cluster.local
# πρέπει να επιστρέψει ΤΡΕΙΣ διευθύνσεις (μία ανά Pod του StatefulSet)
Παγίδα να μην χάσετε
εξετάστε το πεδίο serviceName του παρεχόμενου StatefulSet. Το όνομα του Service σας πρέπει να του αντιστοιχεί ακριβώς, αλλιώς τα ατομικά ονόματα των Pods δεν θα δημιουργηθούν ποτέ.
Τεχνική παγίδα (busybox)
το nslookup nom-court δεν λειτουργεί από ένα Pod busybox επειδή ο επιλύτης του δεν χρησιμοποιεί τα search domains του /etc/resolv.conf. Από ένα πραγματικό Pod εφαρμογής (όπως το portail), αντίθετα, το bd-0.bd-interne επιλύεται τέλεια. Χρησιμοποιήστε λοιπόν το wget για να δοκιμάσετε την πραγματική αλυσίδα εφαρμογής, και το FQDN για να άρετε κάθε ασάφεια DNS.

Αποστολή 4 — Έκθεση δύο θυρών στο ίδιο Service (15 βαθμοί)

Το στοιχείο metriques ακούει σε δύο θύρες:

ΧρήσηΘύρα του κοντέινερΌνομα θύρας δηλωμένο στο Deployment
Διεπαφή web8080web
Μετρικές9090prom

Το portail καλεί το http://metriques (θύρα 80) και το http://metriques:9090/metrics.

Αρχείο προς συμπλήρωση: k8s/services/04-metriques.yaml

Προς καθορισμό: πώς δηλώνουμε πολλές θύρες σε ένα Service; Ποιος περιορισμός γίνεται τότε υποχρεωτικός για κάθε εγγραφή; Και πώς κάνουμε το targetPort να δείχνει σε θύρα του κοντέινερ με το όνομά της αντί με τον αριθμό της, ώστε το Service να μένει έγκυρο ακόμη κι αν αλλάξει ο αριθμός;

Επικύρωση:

powershell
kubectl describe svc metriques            # οι ΔΥΟ θύρες πρέπει να εμφανίζονται

Αποστολή 5 — Να δώσετε εσωτερικό όνομα σε εξωτερική υπηρεσία (10 βαθμοί)

Το portail πρέπει να φτάσει μια υπηρεσία πληρωμής φιλοξενημένη εκτός του συμπλέγματος, αλλά ο κώδικας καλεί ένα εσωτερικό όνομα: paiement-externe. Αυτό το όνομα πρέπει να παραπέμπει στο example.com.

Αρχείο προς συμπλήρωση: k8s/services/05-paiement-externe.yaml

Προς καθορισμό: ποιος τύπος Service δημιουργεί ένα απλό ψευδώνυμο DNS προς εξωτερικό όνομα, χωρίς επιλογέα και χωρίς κανένα Pod;

Επικύρωση:

powershell
kubectl run test --rm -i --restart=Never --image=busybox:1.36 -- nslookup paiement-externe.default.svc.cluster.local
# πρέπει να εμφανίζει :
#   paiement-externe.default.svc.cluster.local  canonical name = example.com
#   Name: example.com
#   Address: <IP publique>

Αυτή η αποστολή απαιτεί το σύμπλεγμα να μπορεί να επιλύει δημόσια ονόματα. Αν δεν έχετε καμία πρόσβαση στο Διαδίκτυο, αντικαταστήστε τον στόχο με api-produits.default.svc.cluster.local και σημειώστε το στην έκθεσή σας.


Αποστολή 6 — Η έρευνα: επισκευή τριών ελαττωματικών Services (20 βαθμοί)

Ο φάκελος k8s/services/06-casses/ περιέχει τρία ήδη γραμμένα Services… που δεν λειτουργούν. Καθένα περιέχει ένα μόνο σφάλμα, και είναι τα τρία συχνότερα σφάλματα στην επιχείρηση.

powershell
kubectl apply -f k8s/services/06-casses/
ΑρχείοΠαρατηρούμενο σύμπτωμα
casse-1.yamlΤο Service υπάρχει, αλλά το kubectl get endpoints επιστρέφει <none>
casse-2.yamlΤα Endpoints είναι όντως γεμάτα, αλλά κάθε σύνδεση απορρίπτεται
casse-3.yamlΤο Service φαίνεται τέλειο, αλλά το portail δεν το φτάνει ποτέ

Για καθεμία από τις τρεις περιπτώσεις, η έκθεσή σας πρέπει να περιέχει:

  1. την εντολή διάγνωσης που σας έβαλε στο ίχνος·
  2. την ακριβή αιτία της βλάβης·
  3. τη διόρθωση που εφαρμόστηκε·
  4. την απόδειξη ότι η σύνδεση λειτουργεί (πράσινο πλακίδιο + έξοδος εντολής).

Συνιστώμενη μέθοδος: προχωρήστε ως ερευνητής. kubectl describe svc, kubectl get endpoints, kubectl get pods --show-labels, έπειτα συγκρίνετε γραμμή προς γραμμή το Service και τα Pods. Η διαφορά μεταξύ «κενά Endpoints» και «άρνηση σύνδεσης» σας δείχνει ήδη από ποια πλευρά να αναζητήσετε.


Αποστολή 7 — Μπόνους: το μεγάλο άλμα (5 βαθμοί)

Κατ' επιλογή, ένα μόνο αρκεί:

  • a) Να κάνετε ώστε ο ίδιος πελάτης να εξυπηρετείται πάντα από το ίδιο Pod της API προϊόντων. (Υπόδειξη: ένα πεδίο του Service επιτρέπει «προσκόλληση» βασισμένη στην IP του πελάτη.)
  • b) Να δημιουργήσετε ένα Service χωρίς επιλογέα που δείχνει σε σταθερή εξωτερική διεύθυνση IP, γράφοντας εσείς οι ίδιοι τα Endpoints του.
  • c) Να γράψετε ένα Service τύπου LoadBalancer για το portail, έπειτα να εξηγήσετε τι γίνεται το EXTERNAL-IP στο Docker Desktop, και τι θα γινόταν στο AWS.

Αυτόματη επικύρωση

Ένα σενάριο σας δίνει τη βαθμολογία σας ανά πάσα στιγμή:

powershell
.\outils\valider.ps1

Αν το PowerShell αρνηθεί να εκτελέσει το σενάριο με μήνυμα του τύπου l'exécution de scripts est désactivée sur ce système, παρακάμψτε τον περιορισμό για αυτή τη μόνη εντολή:

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

Άλλη διαρκής λύση (να γίνει μόνο μία φορά για τον χρήστη σας):

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

Το σενάριο δεν δίνει καμία λύση: δηλώνει μόνο τι αποτυγχάνει και πού να κοιτάξετε.


Παραδοτέα

  1. Ο πλήρης φάκελος k8s/services/: τα 5 γραμμένα Services σας και τα 3 επισκευασμένα Services.
  2. Ένα RAPPORT.md που περιέχει:
    • για κάθε Service: τον επιλεγμένο τύπο και μια αιτιολόγηση σε δύο προτάσεις («γιατί αυτός και όχι άλλος»)·
    • την πλήρη έρευνα της αποστολής 6 (εντολή → αιτία → διόρθωση → απόδειξη)·
    • ένα στιγμιότυπο του πίνακα ελέγχου που εμφανίζει 8 / 8·
    • ένα στιγμιότυπο του kubectl get svc που δείχνει όλα τα Services σας και τους τύπους τους·
    • τις απαντήσεις σας στις ερωτήσεις προβληματισμού.
  3. Την τελική έξοδο του .\outils\valider.ps1.

Ερωτήσεις προβληματισμού

  1. Γιατί η εφαρμογή δεν μπορούσε απολύτως να λειτουργήσει χωρίς Services, ενώ όλα τα Pods ήταν Running;
  2. Ποια είναι η συγκεκριμένη διαφορά μεταξύ ενός κόκκινου πλακιδίου και ενός πορτοκαλί; Τι σας μαθαίνει καθένα για το πού βρίσκεται το σφάλμα;
  3. Γιατί το Service της βάσης δεδομένων πρέπει να είναι headless, ενώ ένα συνηθισμένο Service αρκεί για την API προϊόντων;
  4. Τι περιέχει ακριβώς η λίστα των Endpoints, και ποιος την ενημερώνει; Τι συμβαίνει όταν ένα Pod γίνεται NotReady;
  5. Διαγράφετε ένα Pod της API προϊόντων· το Kubernetes αναδημιουργεί ένα με διαφορετική διεύθυνση IP. Γιατί το portail συνεχίζει να λειτουργεί χωρίς την παραμικρή τροποποίηση;
  6. Στην παραγωγή, θα εκθέτατε δέκα εφαρμογές με δέκα Services τύπου LoadBalancer; Αιτιολογήστε, και προτείνετε εναλλακτική.

Βαθμολόγιο

ΣτοιχείοΒαθμοί
Αποστολή 1 — Εσωτερικό Service και ανακάλυψη DNS15
Αποστολή 2 — Εξωτερική έκθεση στη θύρα 3050015
Αποστολή 3 — Service headless και σταθερές ταυτότητες20
Αποστολή 4 — Πολλαπλές θύρες και ονομασμένες θύρες15
Αποστολή 5 — Ψευδώνυμο προς εξωτερική υπηρεσία10
Αποστολή 6 — Διάγνωση και επισκευή (3 βλάβες)20
Ποιότητα της έκθεσης και αιτιολόγηση των επιλογών5
Μπόνους — Αποστολή 7+5
Σύνολο100 (+5)

Ποινές: −10 ανά τροποποίηση απαγορευμένου αρχείου (apps/, 01-deployments.yaml, 02-statefulset-bd.yaml)· −5 ανά διεύθυνση IP κωδικοποιημένη στα σκληρά.


Κριτήρια επιτυχίας

ΚριτήριοΑναμενόμενο
Πίνακας ελέγχου8 / 8 πράσινα πλακίδια
Τύποι ServicesΚαθένας προσαρμοσμένος στη χρήση του και αιτιολογημένος
Ατομικό DNSΤο bd-0.bd-interne επιλύεται προς ένα μόνο Pod
Πολλαπλές θύρεςΟι δύο θύρες ορατές, targetPort αναφερόμενο με όνομα
ΈρευναΟι 3 βλάβες εντοπισμένες, εξηγημένες και διορθωμένες
ΑνθεκτικότηταΜετά τη διαγραφή ενός Pod, το portail συνεχίζει να λειτουργεί
Καμία IP στα σκληράΜόνο ονόματα DNS και επιλογείς ετικετών

Εργαλειοθήκη

Καμία λύση εδώ — μόνο ίχνη.

powershell
kubectl get svc                              # τύποι, IP, θύρες
kubectl describe svc <nom>                   # λεπτομέρειες + Endpoints
kubectl get endpoints <nom>                  # ΠΟΙΟΣ βρίσκεται πίσω από το Service ;
kubectl get pods --show-labels               # οι πραγματικές ετικέτες των Pods
kubectl get pods -l app=<valeur>             # δοκιμή ενός επιλογέα
kubectl port-forward deploy/portail 5000:5000    # πρόσβαση σε Pod ΧΩΡΙΣ 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        # αυτό που το portail δεν καταφέρνει να φτάσει
kubectl delete svc <nom>                     # επανεκκίνηση από το μηδέν σε ένα Service

Παγίδα να γνωρίζετε με το kubectl run test: αν αλυσιδώσετε πολλές εντολές γρήγορα, το προηγούμενο Pod δεν διαγράφεται πάντα εγκαίρως και θα λάβετε:

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

Δύο λύσεις: αλλάξτε το όνομα (test1, test2…) σε κάθε εντολή, ή καθαρίστε πριν:

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

Οι τρεις ερωτήσεις που ξεκλειδώνουν το 90 % των καταστάσεων:

  1. Το Service υπάρχει, με το σωστό όνομα; (αλλιώς → κόκκινο πλακίδιο: δεν υπάρχει τίποτα προς αποσφαλμάτωση, πρέπει να γραφτεί)
  2. Τα Endpoints είναι γεμάτα; (κενά → ο επιλογέας δεν αντιστοιχεί σε καμία ετικέτα Pod)
  3. Το targetPort αντιστοιχεί στη θύρα που ακούει πραγματικά το κοντέινερ; (αλλιώς → άρνηση σύνδεσης)


ΠΑΡΑΡΤΗΜΑ A — Οι εφαρμογές

Μην τροποποιήσετε κανένα από αυτά τα αρχεία. Αντιγράψτε τα ως έχουν στις υποδεικνυόμενες διαδρομές.

A.1 — Η γενική μικροϋπηρεσία

Αυτή η μοναδική εφαρμογή χρησιμεύει σε πέντε στοιχεία (api-produits, api-commandes, cache, notifications, bd). Το όνομα και η θύρα της καθορίζονται από μεταβλητές περιβάλλοντος.

Αρχείο: apps/micro/app.py

python
"""Γενική μικροϋπηρεσία επίδειξης.

Η ίδια εικόνα χρησιμεύει σε πολλά στοιχεία : το όνομα και η θύρα ακρόασης
παρέχονται από μεταβλητές περιβάλλοντος (APP_NAME, PORT).
Κάθε απάντηση περιέχει το όνομα του Pod, κάτι που καθιστά ορατή την κατανομή
φορτίου που πραγματοποιεί ένα 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)

Αρχείο: apps/micro/requirements.txt

text
flask==3.0.3

Αρχείο: 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 — Το στοιχείο « metriques » (δύο θύρες)

Αυτή η εφαρμογή ακούει ταυτόχρονα σε δύο θύρες: 8080 (διεπαφή web) και 9090 (μετρικές). Αυτή καθιστά δυνατή την αποστολή 4.

Αρχείο: apps/metriques/app.py

python
"""Στοιχείο που εκθέτει ΔΥΟ θύρες ταυτόχρονα.

  - 8080 : διεπαφή web         (διαδρομή /ping)
  - 9090 : μετρικές Prometheus  (διαδρομή /metrics)

Δύο διακομιστές Flask τρέχουν σε δύο διακριτά νήματα εκτέλεσης.
"""

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)

Αρχείο: apps/metriques/requirements.txt

text
flask==3.0.3

Αρχείο: 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 — Το portail (πίνακας ελέγχου)

Αυτό εμφανίζει τα 8 πλακίδια και τη βαθμολογία σας ζωντανά.

Αρχείο: apps/portail/app.py

python
"""Πίνακας ελέγχου των συνδέσεων του συμπλέγματος.

Για κάθε σύνδεση, το portail διακρίνει ΤΡΕΙΣ καταστάσεις :
  - ΚΟΚΚΙΝΟ  : το όνομα DNS δεν υπάρχει       -> το Service δεν δημιουργήθηκε
  - ΠΟΡΤΟΚΑΛΙ : το όνομα επιλύεται, καμία απάντηση -> εσφαλμένος επιλογέας ή θύρα
  - ΠΡΑΣΙΝΟ   : η επικοινωνία λειτουργεί   -> το Service είναι σωστό
"""

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

from flask import Flask, request

app = Flask(__name__)

DELAI = 1.5          # δευτερόλεπτα
PORT_ATTENDU = 30500  # θύρα μέσω της οποίας πρέπει να συμβουλεύεται το portail

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


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


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


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

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

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

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

    return tester_http(cible["url"])


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


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


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

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

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

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

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


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

Αρχείο: apps/portail/requirements.txt

text
flask==3.0.3

Αρχείο: 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"]


ΠΑΡΑΡΤΗΜΑ B — Τα παρεχόμενα μανιφέστα

Μην τροποποιήσετε κανένα από αυτά τα δύο αρχεία. Αποτελούν το υπάρχον στο οποίο πρέπει να προσαρμοστούν τα Services σας.

Αρχείο: k8s/01-deployments.yaml

yaml
# ---------------------------------------------------------------------------
# ΤΑ PODS ΤΗΣ ΠΛΑΤΦΟΡΜΑΣ — ΠΑΡΕΧΕΤΑΙ, ΝΑ ΜΗΝ ΤΡΟΠΟΠΟΙΗΘΕΙ
# Παρατηρήστε προσεκτικά : οι ΕΤΙΚΕΤΕΣ και οι ΘΥΡΕΣ που δηλώνονται εδώ είναι οι μόνες
# πληροφορίες που διαθέτετε για να γράψετε τα 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            # <-- ΟΝΟΜΑΣΜΕΝΗ θύρα
              containerPort: 8080
            - name: prom           # <-- ΟΝΟΜΑΣΜΕΝΗ θύρα
              containerPort: 9090
          readinessProbe:
            httpGet: { path: /health, port: 8080 }
            initialDelaySeconds: 3
            periodSeconds: 5

Αρχείο: k8s/02-statefulset-bd.yaml

yaml
# ---------------------------------------------------------------------------
# Η ΒΑΣΗ ΔΕΔΟΜΕΝΩΝ (3 αντίγραφα) — ΠΑΡΕΧΕΤΑΙ, ΝΑ ΜΗΝ ΤΡΟΠΟΠΟΙΗΘΕΙ
#
# ΠΡΟΣΟΧΗ : το πεδίο serviceName παρακάτω επιβάλλει το ΟΝΟΜΑ του Service που
# θα πρέπει να γράψετε ώστε τα bd-0, bd-1 και bd-2 να λάβει καθένα ένα όνομα DNS.
# ---------------------------------------------------------------------------
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: bd
spec:
  serviceName: bd-interne          # <-- διαβάστε καλά αυτή τη γραμμή
  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


ΠΑΡΑΡΤΗΜΑ C — Οι σκελετοί Services προς συμπλήρωση

Αντιγράψτε αυτά τα πέντε αρχεία, έπειτα αντικαταστήστε κάθε TODO με τη σωστή τιμή. Οι γραμμές που προηγούνται από # ? είναι ερωτήσεις προς απόφαση: σε εσάς εναπόκειται να αποφασίσετε αν πρέπει να προσθέσετε, να τροποποιήσετε ή να διαγράψετε την εν λόγω γραμμή.

Αρχείο: k8s/services/01-api-produits.yaml

yaml
# ΑΠΟΣΤΟΛΗ 1 — Να καταστήσετε την API προϊόντων προσβάσιμη από το portail.
#
# Το portail καλεί :  http://api-produits        (άρα τη θύρα 80)
# Τα Pods ακούν στο : 8000
# Τα Pods φέρουν την ετικέτα : app: api-produits
#
# ? Ποιος τύπος Service για ΕΣΩΤΕΡΙΚΗ επικοινωνία στο σύμπλεγμα ;
apiVersion: v1
kind: Service
metadata:
  name: api-produits          # ΕΠΙΒΕΒΛΗΜΕΝΟ όνομα : να μην αλλάξει
spec:
  type: TODO
  selector:
    TODO: TODO
  ports:
    - port: TODO              # η θύρα μέσω της οποίας καλούν οι πελάτες
      targetPort: TODO        # η θύρα που ακούει πραγματικά το κοντέινερ

Αρχείο: k8s/services/02-portail.yaml

yaml
# ΑΠΟΣΤΟΛΗ 2 — Να καταστήσετε τον πίνακα ελέγχου προσβάσιμο από τον περιηγητή,
#             στην ακριβή διεύθυνση : http://localhost:30500
#
# Τα Pods ακούν στο : 5000
# Τα Pods φέρουν την ετικέτα : app: portail
#
# ? Ποιος τύπος Service ανοίγει μια θύρα στο ΜΗΧΑΝΗΜΑ ;
# ? Ποιο είναι το επιτρεπόμενο εύρος για αυτή τη θύρα ;
# ? Ποιο συμπληρωματικό πεδίο πρέπει να προστεθεί για να επιβληθεί η θύρα 30500 ;
apiVersion: v1
kind: Service
metadata:
  name: portail               # ΕΠΙΒΕΒΛΗΜΕΝΟ όνομα : να μην αλλάξει
spec:
  type: TODO
  selector:
    TODO: TODO
  ports:
    - port: TODO
      targetPort: TODO
      # ? μια γραμμή λείπει εδώ

Αρχείο: k8s/services/03-bd-interne.yaml

yaml
# ΑΠΟΣΤΟΛΗ 3 — Να δώσετε ΑΤΟΜΙΚΟ όνομα DNS σε κάθε αντίγραφο της βάσης,
#             ώστε να μπορείτε να φτάσετε ακριβώς : bd-0.bd-interne
#
# Τα Pods ακούν στο : 5432
# Τα Pods φέρουν την ετικέτα : app: bd
#
# ? Ποιος τύπος Service ΔΕΝ διαθέτει μοναδική εικονική IP ;
# ? Ποιο πεδίο, με ποια πολύ ιδιαίτερη τιμή, παράγει αυτό το αποτέλεσμα ;
# ? Το όνομα παρακάτω πρέπει να αντιστοιχεί σε ποιο πεδίο του StatefulSet ;
apiVersion: v1
kind: Service
metadata:
  name: bd-interne            # ΕΠΙΒΕΒΛΗΜΕΝΟ όνομα : να μην αλλάξει
spec:
  # ? μια ουσιώδης γραμμή λείπει εδώ
  selector:
    TODO: TODO
  ports:
    - port: TODO
      targetPort: TODO

Αρχείο: k8s/services/04-metriques.yaml

yaml
# ΑΠΟΣΤΟΛΗ 4 — Έκθεση ΔΥΟ θυρών στο ίδιο Service.
#
# Το portail καλεί :  http://metriques         (θύρα 80)
#                  και : http://metriques:9090/metrics
#
# Τα Pods ακούν στο : 8080 (θύρα με όνομα "web") και 9090 (θύρα με όνομα "prom")
# Τα Pods φέρουν την ετικέτα : app: metriques
#
# ? Ποιος περιορισμός γίνεται ΥΠΟΧΡΕΩΤΙΚΟΣ μόλις ένα Service εκθέτει πολλές θύρες ;
# ? Πώς κάνουμε το targetPort να δείχνει σε θύρα του κοντέινερ ΜΕ ΤΟ ΟΝΟΜΑ ΤΗΣ ;
apiVersion: v1
kind: Service
metadata:
  name: metriques             # ΕΠΙΒΕΒΛΗΜΕΝΟ όνομα : να μην αλλάξει
spec:
  type: TODO
  selector:
    TODO: TODO
  ports:
    - TODO: TODO              # ? ένα υποχρεωτικό πεδίο λείπει σε κάθε εγγραφή
      port: TODO
      targetPort: TODO
    - TODO: TODO
      port: TODO
      targetPort: TODO

Αρχείο: k8s/services/05-paiement-externe.yaml

yaml
# ΑΠΟΣΤΟΛΗ 5 — Να δείξει ένα ΕΣΩΤΕΡΙΚΟ όνομα προς ΕΞΩΤΕΡΙΚΗ υπηρεσία.
#
# Το portail χρησιμοποιεί το όνομα : paiement-externe
# Αυτό το όνομα πρέπει να παραπέμπει στο : example.com
#
# ? Ποιος τύπος Service δημιουργεί ένα απλό ψευδώνυμο DNS (CNAME) ;
# ? Αυτός ο τύπος διαθέτει επιλογέα ; θύρες ; Pods ;
apiVersion: v1
kind: Service
metadata:
  name: paiement-externe      # ΕΠΙΒΕΒΛΗΜΕΝΟ όνομα : να μην αλλάξει
spec:
  type: TODO
  TODO: TODO                  # ? το πεδίο που δηλώνει τον εξωτερικό στόχο


ΠΑΡΑΡΤΗΜΑ D — Τα τρία ελαττωματικά Services

Αντιγράψτε αυτά τα τρία αρχεία ως έχουν, εφαρμόστε τα, έπειτα διαγνώστε και διορθώστε. Καθένα περιέχει ακριβώς ένα σφάλμα. Μην ξαναγράψετε το αρχείο από το μηδέν: βρείτε το λάθος.

Αρχείο: k8s/services/06-casses/casse-1.yaml

yaml
# ΒΛΑΒΗ 1
# Σύμπτωμα : το Service υπάρχει, αλλά το "kubectl get endpoints api-commandes"
#            επιστρέφει <none>. Το portail εμφανίζει ΠΟΡΤΟΚΑΛΙ πλακίδιο.
apiVersion: v1
kind: Service
metadata:
  name: api-commandes
spec:
  type: ClusterIP
  selector:
    app: api-commande
  ports:
    - port: 80
      targetPort: 8000

Αρχείο: k8s/services/06-casses/casse-2.yaml

yaml
# ΒΛΑΒΗ 2
# Σύμπτωμα : το "kubectl get endpoints cache" εμφανίζει όντως μια διεύθυνση IP,
#            αλλά κάθε σύνδεση αποτυγχάνει. Το portail εμφανίζει ΠΟΡΤΟΚΑΛΙ πλακίδιο.
apiVersion: v1
kind: Service
metadata:
  name: cache
spec:
  type: ClusterIP
  selector:
    app: cache
  ports:
    - port: 80
      targetPort: 6380

Αρχείο: k8s/services/06-casses/casse-3.yaml

yaml
# ΒΛΑΒΗ 3
# Σύμπτωμα : αυτό το Service φαίνεται τέλειο (σωστός τύπος, σωστός επιλογέας,
#            γεμάτα Endpoints, συνεπείς θύρες)... και όμως το portail
#            εμφανίζει ΚΟΚΚΙΝΟ πλακίδιο και δεν το φτάνει ΠΟΤΕ.
apiVersion: v1
kind: Service
metadata:
  name: notification
spec:
  type: ClusterIP
  selector:
    app: notifications
  ports:
    - port: 80
      targetPort: 7000


ΠΑΡΑΡΤΗΜΑ E — Το σενάριο επικύρωσης

Αρχείο: outils/valider.ps1

powershell
# ---------------------------------------------------------------------------
# Σενάριο επικύρωσης — δίνει βαθμολογία, ΠΟΤΕ τη λύση.
# Χρήση :  .\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 ""

# --- Αποστολή 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

# --- Αποστολή 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

# --- Αποστολή 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

# --- Αποστολή 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

# --- Αποστολή 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

# --- Αποστολή 6 : οι τρεις επισκευές --------------------------------------
$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 ""

Μάθημα δημιουργημένο από τον Dr. Haythem REHOUMA — Ανάπτυξη και ανάπτυξη λύσεων δεδομένων