المهمة: استعادة الاتصالات في cluster

16 دقيقة

المشروع 12 — خدمات Kubernetes · المستوى متوسط → متقدم · المدة المقدَّرة: 3 إلى 5 ساعات

كل الشفرة التطبيقية مقدَّمة في ملحق هذا المستند. عملك: كتابة الخدمات الناقصة وتحديدها وإكمالها — أي جعل نظام أخرس تماماً في حالته الحالية يتواصل.


جدول المحتويات


السياق

نشر فريق منصة تجارة إلكترونية صغيرة على Kubernetes. الصور مبنية، وعمليات النشر وStatefulSet تعمل، وكل الـ Pods بالحالة Running.

ومع ذلك، لا شيء يعمل.

تعرض البوابة لوحة حمراء بالكامل: لا تصل إلى أي مكوّن. قاعدة البيانات غير قابلة للوصول. الذاكرة المؤقتة غير مرئية. لا صفحة متاحة من المتصفح.

السبب بسيط: الشخص الذي كان عليه كتابة الخدمات غادر دون تسليمها.

تذكير أساسي سيعيشه هذا المشروع: Pods تعمل لا تشكّل تطبيقاً. بلا خدمات، هي جزر معزولة، بلا عنوان مستقر ولا اسم، عاجزة عن العثور بعضها على بعض.

مهمتك: استعادة كل الاتصالات، فقط بكتابة الخدمات الصحيحة.


مفاهيم أساسية قبل البدء

هذا القسم دليل أدنى مكتفٍ بذاته: يحتوي كل المفردات اللازمة للمهام. اقرأه مرة، وعد إليه عندما يفلت منك لفظ.

1. المشكلة التي تحلها الخدمة

الـ Pod فانٍ: يمكن لـ Kubernetes حذفه ونقله وإعادة إنشائه — وسيكون عنوان IP الجديد مختلفاً. إذن لا نتصل أبداً بـ Pod عبر IP.

الـ Service كائن مستقر — اسم، IP، منفذ — يتبع الـ Pods أينما ذهبت. الآلية بسيطة:

ما يصنع الرابط بين خدمة و«Podsها» هو مُنتقي الوسوم:

yaml
spec:
  selector:
    app: api-produits          # كل Pod يحمل هذا الوسم يُبلَغ

تشكّل قائمة الـ Pods المطابقة كائن Endpoints — هذه صورتك الشعاعية للخدمة.

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. أنواع الخدمات الخمسة (الوحيدة التي تحتاجها)

النوعفائدتهمرئي من الخارج؟في هذا المشروع
ClusterIPعنوان داخلي للعنقود، القيمة الافتراضيةلاapi-produits، api-commandes، cache، notifications، metriques
NodePortيفتح منفذاً ثابتاً (30000–32767) على كل عقدةنعم، localhost:<nodePort>portail
LoadBalancerيطلب عنوان IP عاماً من السحابة (AWS، GCP، Azure)نعم(مهمة مكافأة)
HeadlessClusterIP بلا عنوان IP افتراضي: يعيد قائمة عناوين IP للـ Pods، زائد اسم DNS لكل Podلاbd-interne
ExternalNameاسم مستعار DNS نحو اسم خارجي. لا Pod، لا مُنتقي.لاpaiement-externe

نقطة مهمة: خدمة «لا تعمل» نادراً ما تكون مشكلة نوع. هي تقريباً دائماً مُنتقي أو منفذ.


3. DNS الداخلي: القواعد التي تعطي وهم السحر

في العنقود، يصنع CoreDNS تلقائياً أسماء وفق قواعد ثابتة:

تستدعييحل CoreDNS نحو
api-produitsالخدمة api-produits في النطاق نفسه
api-produits.defaultالخدمة api-produits في النطاق default
api-produits.default.svc.cluster.localالصيغة الطويلة المؤهَّلة بالكامل

لازمة حاسمة: اسم الخدمة هو الاسم الذي يستدعيه التطبيق. خدمة باسم notification لا تجيب على http://notifications. هذا الفخ في قلب المهمة 6.


4. StatefulSet وخدمة headless: الثنائي

يعامل Deployment نسخه كتوائم قابلة للاستبدال (web-abc123-x7k9، web-abc123-p2m1…). مثالي لويب بلا حالة.

ينتج StatefulSet على العكس Pods مرقّمة ومستقرة: bd-0، bd-1، bd-2. يحافظ كل Pod على هويته عبر إعادة التشغيل — ضروري لقاعدة بيانات يجب فيها تعيين الأساسية بدقة.

لكن StatefulSet لا يكفي وحده: يتطلّب أن يكون مرتبطاً بخدمة headless يشير إلى اسمها في حقله serviceName.

yaml
kind: StatefulSet
spec:
  serviceName: bd-interne         # <-- يشير إلى خدمة headless بالاسم نفسه
  replicas: 3

تعطي هذه الخدمة headless عندئذ اسماً DNS لكل Pod:

bd-0.bd-interne         -> عنوان IP للـ Pod bd-0 فقط
bd-1.bd-interne         -> عنوان IP للـ Pod bd-1 فقط
bd-interne              -> عناوين الـ Pods الثلاثة (قائمة)

بلا الكلمة None في spec.clusterIP، لا يوجد أي من هذه الأسماء.

yaml
spec:
  clusterIP: None                  # يحوّل الخدمة إلى "headless"

احفظ هذا: للوصول إلى bd-0.bd-interne يلزم شرطان متزامنان — StatefulSet بحقل serviceName: bd-interne، وخدمة headless باسم bd-interne. إن غاب أحدهما، لا يوجد الاسم الفردي.


5. منافذ مسمّاة وخدمات متعددة المنافذ

عندما تعرض خدمة عدة منافذ، يصير كل مدخل مسمّى إلزامياً:

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). تبقى الخدمة صحيحة.


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 خارجية أو نقل واجهة. نغيّر الخدمة، لا الشفرة.


7. الأسئلة الثلاثة التي تفك 90% من الأعطال

في كل مرة لا تعمل خدمة، اطرح هذه الأسئلة الثلاثة بهذا الترتيب:

هذه تماماً طريقة المهمة 6.


ما ستعرف فعله في النهاية

  • تمييز أنواع الخدمات الخمسة ومعرفة متى تستخدم كلاً.
  • استخدام kubectl get svc و kubectl describe svc و kubectl get endpoints كثلاث أدوات متكاملة.
  • ربط StatefulSet بشكل صحيح بـ خدمة headless.
  • كتابة خدمة متعددة المنافذ نظيفة، مع targetPort مشار إليه بالاسم.
  • تشخيص الأعطال الثلاثة الأكثر شيوعاً في المؤسسات (وسم خاطئ، منفذ خاطئ، اسم خاطئ).

المعمارية المطلوب تشغيلها

سبعة مكوّنات تعمل مسبقاً. لا أحد قابلاً للوصول.

المكوّنمنفذ(منافذ) الحاويةوسم الـ Podsالمتحكّم
portail5000app: portailDeployment (نسخة واحدة)
api-produits8000app: api-produitsDeployment (3 نسخ)
api-commandes8000app: api-commandesDeployment (نسختان)
cache6379app: cacheDeployment (نسخة واحدة)
notifications7000app: notificationsDeployment (نسختان)
metriques8080 (مسمّى web) و 9090 (مسمّى prom)app: metriquesDeployment (نسختان)
bd5432app: bdStatefulSet (3 نسخ)

توزيع الملفات

أنشئ هذه الشجرة بالضبط، بنسخ محتوى الملاحق. يشير كل ملحق إلى المسار الدقيق للملف المراد إنشاؤه.

projet12-mission-services/
├── 00-ENONCE.md                      <- هذا المستند

├── apps/                             <- الشفرة (الملحق أ) — لا تُعدَّل
│   ├── micro/
│   │   ├── app.py
│   │   ├── requirements.txt
│   │   └── Dockerfile
│   ├── metriques/
│   │   ├── app.py
│   │   ├── requirements.txt
│   │   └── Dockerfile
│   └── portail/
│       ├── app.py
│       ├── requirements.txt
│       └── Dockerfile

├── k8s/
│   ├── 01-deployments.yaml           <- مقدَّم (الملحق ب) — لا يُعدَّل
│   ├── 02-statefulset-bd.yaml        <- مقدَّم (الملحق ب) — لا يُعدَّل
│   │
│   └── services/                     <- دورك أنت
│       ├── 01-api-produits.yaml      <- هيكل للإكمال (الملحق ج)
│       ├── 02-portail.yaml           <- هيكل للإكمال (الملحق ج)
│       ├── 03-bd-interne.yaml        <- هيكل للإكمال (الملحق ج)
│       ├── 04-metriques.yaml         <- هيكل للإكمال (الملحق ج)
│       ├── 05-paiement-externe.yaml  <- هيكل للإكمال (الملحق ج)
│       │
│       └── 06-casses/                <- مقدَّمة لكنها معيبة (الملحق د)
│           ├── casse-1.yaml
│           ├── casse-2.yaml
│           └── casse-3.yaml

├── outils/
│   └── valider.ps1                   <- مقدَّم (الملحق هـ)

└── RAPPORT.md                        <- تكتبه أنت

ثلاث صور فقط لازمة: تخدم micro:1.0 خمسة مكوّنات مختلفة (يتغيّر السلوك بـ متغيرات بيئة)، وتعرض metriques:1.0 منفذين، وتعرض portail:1.0 اللوحة.


لوحة التحكم: مؤشر تقدّمك

تستجوب البوابة باستمرار كل مكوّن وتعرض مربّعاً لكل اتصال، يُحدَّث كل 3 ثوانٍ:

المربّعالمعنىأين تبحث عن الخطأ
أحمراسم DNS غير موجودالخدمة لم تُنشأ، أو اسمها خاطئ
برتقاليحُلّ الاسم، لكن لا أحد يجيبالخدمة موجودة، لكن مُنتقيها أو منفذها خاطئ
أخضراتصال قائمخدمتك صحيحة

الهدف النهائي: المربّعات الثمانية خضراء، والعدّاد يعرض 8 / 8.

هذا التمييز أحمر/برتقالي ليس تجميلياً: يخبرك من أي جهة تبحث. أحمر = الخدمة غير موجودة (لا شيء للتصحيح، يلزم كتابتها). برتقالي = الخدمة موجودة لكنها لا تجد Podsها أو تضرب المنفذ الخطأ.

الاتصالات الثمانية المتحقَّق منها:

#المربّعما تختبره البوابة
1وصول خارجيأنك تستشير البوابة فعلاً عبر المنفذ 30500
2واجهة المنتجاتhttp://api-produits/ping
3قاعدة البياناتhttp://bd-0.bd-interne:5432/ping
4المقاييسhttp://metriques/ping و http://metriques:9090/metrics
5دفع خارجيحل DNS للاسم paiement-externe
6واجهة الطلبات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. يجب أن تحدّد بنفسك نوع كل خدمة: لا شيء يخبرك إن كان ClusterIP أو NodePort أو LoadBalancer أو خدمة headless أو ExternalName. هذا قلب التقييم.
  5. أسماء الخدمات مفروضة: تستدعيها الشفرة التطبيقية كما هي. اسم خاطئ يعطي مربّعاً أحمر.
  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 غيغابايت RAM على الأقل: Settings → Resources → Memory ≥ 4 GB. يشغّل هذا المشروع 14 Pod؛ على 2 غيغابايت تختنق الآلة وتبقى 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، ولا أي خدمة)
kubectl apply -f k8s/01-deployments.yaml
kubectl apply -f k8s/02-statefulset-bd.yaml

# 3) انتظار أن تكون كل الـ Pods Ready (حوالي 30 ث)
kubectl wait --for=condition=ready pod --all --timeout=180s

# 4) ملاحظة وضع الانطلاق
kubectl get pods                        # 14 Pod، كلها Running
kubectl get svc                         # "kubernetes" فقط: لا خدمة من خدماتك

في هذه المرحلة: كل الـ Pods تعمل ولا شيء يتواصل. هذه نقطة الانطلاق الطبيعية.

تحذيران تقنيان يُعرفان من الآن — ليسا خطأ منك:

  1. تحذير Endpoints is deprecated in v1.33+ — يعرض Kubernetes هذه الرسالة منهجياً عند كل kubectl get endpoints. الأمر يعمل دائماً بشكل كامل، تجاهل التحذير. الواجهة الجديدة المعادلة هي kubectl get endpointslices، لكن كل أوامر هذا التدريب تستخدم عمداً endpoints، أوضح للتعلّم.

  2. قد تستغرق اللوحة 10 إلى 15 ثانية للظهور أول مرة: تختبر البوابة 8 اتصالات شبكة في كل عرض، لكل منها مهلة انتظار 1.5 ث. عندما لا يعمل شيء بعد، تنتظر كل مهلة قبل عرض أحمر أو برتقالي. بعد صحة الخدمات، ينخفض زمن الاستجابة إلى بضع مئات من الميلي ثانية.

سؤال تطرحه فوراً: كيف سترى اللوحة أصلاً، بينما لا باب دخول موجود بعد؟

عوّامة نجاة: يعمل kubectl port-forward بلا أي خدمة، مباشرة على Pod.

powershell
kubectl port-forward deploy/portail 5000:5000

ثم افتح http://localhost:5000. سترى اللوحة حمراء بالكامل، بمربّع «وصول خارجي» برتقالي (طبيعي: لم تمر عبر المنفذ 30500).


المهام

المهمة 1 — جعل البوابة تتحدث إلى واجهة المنتجات (15 نقطة)

تستدعي البوابة http://api-produits على المنفذ 80. تستمع Pods الواجهة على المنفذ 8000 وتحمل الوسم app: api-produits.

الملف المطلوب إكماله: k8s/services/01-api-produits.yaml

المطلوب تحديده: نوع الخدمة، والمُنتقي، وكذلك port و targetPort.

التحقق:

powershell
kubectl apply -f k8s/services/01-api-produits.yaml
kubectl get svc api-produits
kubectl get endpoints api-produits        # يجب أن يسرد 3 عناوين IP

يصير مربّع واجهة المنتجات أخضر.


المهمة 2 — فتح باب الدخول (15 نقطة)

يجب أن تكون اللوحة متاحة من متصفحك بالعنوان الدقيق http://localhost:30500. تستمع Pods البوابة على المنفذ 5000.

الملف المطلوب إكماله: k8s/services/02-portail.yaml

المطلوب تحديده: أي نوع خدمة يعرض تطبيقاً خارج العنقود على منفذ ثابت للآلة؟ ما نطاق المنافذ المسموح لهذا الحقل؟

التحقق:

powershell
kubectl get svc portail                   # يجب أن يعرض PORT(S) القيمة 80:30500/TCP
start http://localhost:30500

يصير مربّع الوصول الخارجي أخضر.

سؤال يُعالَج في التقرير: نوع خدمة آخر كان سيجعل البوابة متاحة من المتصفح أيضاً على Docker Desktop. أي نوع؟ وما الفرق الذي سيحدثه ذلك في الإنتاج في السحابة؟


المهمة 3 — إعطاء هوية لكل قاعدة بيانات (20 نقطة)

يوفّر StatefulSet bd 3 نسخ. يجب أن تصل البوابة بدقة إلى الأولى (الأساسية)، بالعنوان:

bd-0.bd-interne

تحمل Pods القاعدة الوسم app: bd وتستمع على المنفذ 5432.

الملف المطلوب إكماله: k8s/services/03-bd-interne.yaml

المطلوب تحديده: أي نوع خدمة يعطي اسماً 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 نطاقات البحث، يلزم إعطاء 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 المقدَّم. يجب أن يطابق اسم خدمتك ذلك تماماً، وإلا لن تُنشأ الأسماء الفردية للـ Pods أبداً.
فخ تقني (busybox)
لا يعمل nslookup nom-court من Pod busybox لأن محلّله لا يستخدم search domains في /etc/resolv.conf. من Pod تطبيقي حقيقي (مثل portail)، على العكس، يحل bd-0.bd-interne بشكل كامل. استخدم إذن wget لاختبار السلسلة التطبيقية الحقيقية، و FQDN لرفع أي لبس DNS.

المهمة 4 — عرض منفذين على الخدمة نفسها (15 نقطة)

يستمع المكوّن metriques على منفذين:

الاستخداممنفذ الحاويةاسم المنفذ المصرَّح في الـ Deployment
واجهة ويب8080web
مقاييس9090prom

تستدعي البوابة http://metriques (المنفذ 80) و http://metriques:9090/metrics.

الملف المطلوب إكماله: k8s/services/04-metriques.yaml

المطلوب تحديده: كيف تصرّح بـ عدة منافذ على خدمة؟ أي قيد يصير عندئذ إلزامياً لكل مدخل؟ وكيف تجعل targetPort يشير إلى منفذ الحاوية باسمه بدل رقمه، لكي تبقى الخدمة صالحة حتى إن تغيّر الرقم؟

التحقق:

powershell
kubectl describe svc metriques            # يجب أن يظهر المنفذان

المهمة 5 — إعطاء اسم داخلي لخدمة خارجية (10 نقاط)

يجب أن تصل البوابة إلى خدمة دفع مستضافة خارج العنقود، لكن الشفرة تستدعي اسماً داخلياً: paiement-externe. يجب أن يحيل هذا الاسم نحو example.com.

الملف المطلوب إكماله: k8s/services/05-paiement-externe.yaml

المطلوب تحديده: أي نوع خدمة ينشئ مجرد اسم مستعار 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 — التحقيق: إصلاح ثلاث خدمات معيبة (20 نقطة)

يحتوي المجلد k8s/services/06-casses/ على ثلاث خدمات مكتوبة مسبقاًلا تعمل. لكل منها خطأ واحد، وهذه الأخطاء الثلاثة الأكثر شيوعاً في المؤسسات.

powershell
kubectl apply -f k8s/services/06-casses/
الملفالعَرَض الملحوظ
casse-1.yamlالخدمة موجودة، لكن يعيد kubectl get endpoints القيمة <none>
casse-2.yamlEndpoints ممتلئة فعلاً، لكن كل اتصال مرفوض
casse-3.yamlتبدو الخدمة مثالية، لكن البوابة لا تصل إليها أبداً

لكل من الحالات الثلاث، يجب أن يحتوي تقريرك:

  1. أمر التشخيص الذي وضعك على الأثر؛
  2. السبب الدقيق للعطل؛
  3. التصحيح المطبَّق؛
  4. الدليل أن الاتصال يعمل (مربّع أخضر + مخرجات أمر).

طريقة موصى بها: تصرّف كمحقق. kubectl describe svc، kubectl get endpoints، kubectl get pods --show-labels، ثم قارن سطراً سطراً الخدمة والـ Pods. الفرق بين «Endpoints فارغة» و «اتصال مرفوض» يخبرك سلفاً من أي جهة تبحث.


المهمة 7 — مكافأة: القفزة الكبيرة (5 نقاط)

حسب اختيارك، واحد فقط يكفي:

  • أ) جعل العميل نفسه يُخدَم دائماً من الـ Pod نفسه لواجهة المنتجات. (تلميح: حقل في الخدمة يتيح «التصاقاً» قائماً على عنوان IP للعميل.)
  • ب) إنشاء خدمة بلا مُنتقي تشير إلى عنوان IP خارجي ثابت، بكتابة أنت Endpointsها.
  • ج) كتابة خدمة من النوع LoadBalancer للبوابة، ثم شرح ما يصير إليه 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/ كاملاً: خدماتك الخمس المكتوبة والخدمات الثلاث المصلحة.
  2. ملف RAPPORT.md يحتوي:
    • لـ كل خدمة: النوع المختار وتبرير بجملتين («لماذا هذا وليس آخر»)؛
    • التحقيق الكامل للمهمة 6 (أمر → سبب → تصحيح → دليل)؛
    • لقطة للوحة تعرض 8 / 8؛
    • لقطة من kubectl get svc تُظهر كل خدماتك وأنواعها؛
    • إجاباتك على أسئلة التفكير.
  3. المخرجات النهائية لـ .\outils\valider.ps1.

أسئلة للتفكير

  1. لماذا لم يكن ممكناً مطلقاً أن يعمل التطبيق بلا خدمات، بينما كانت كل الـ Pods بالحالة Running؟
  2. ما الفرق الملموس بين مربّع أحمر ومربّع برتقالي؟ ماذا يعلّمك كل منهما عن مكان الخطأ؟
  3. لماذا يجب أن تكون خدمة قاعدة البيانات headless، بينما تكفي خدمة عادية لواجهة المنتجات؟
  4. ماذا تحتوي قائمة Endpoints بالضبط، ومن يحدّثها؟ ماذا يحدث عندما يصير Pod بالحالة NotReady؟
  5. تحذف Podاً من واجهة المنتجات؛ يعيد Kubernetes إنشاء واحد بـ عنوان IP مختلف. لماذا تستمر البوابة في العمل بلا أي تعديل؟
  6. في الإنتاج، هل تعرض عشرة تطبيقات بعشر خدمات من النوع LoadBalancer؟ برّر، واقترح بديلاً.

السلم

العنصرالنقاط
المهمة 1 — خدمة داخلية واكتشاف DNS15
المهمة 2 — عرض خارجي على المنفذ 3050015
المهمة 3 — خدمة 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 مربّعات خضراء
أنواع الخدماتكل منها ملائم لاستخدامه ومبرَّر
DNS فردييُحل bd-0.bd-interne نحو Pod واحد
تعدد المنافذالمنفذان ظاهران، targetPort مشار إليه بالاسم
التحقيقالأعطال الثلاثة مُحدَّدة ومشروحة ومصحَّحة
المرونةبعد حذف Pod، تستمر البوابة في العمل
لا IP ثابتاًأسماء DNS ومُنتقيات وسوم فقط

صندوق الأدوات

لا حلول هنا — مسارات فقط.

powershell
kubectl get svc                              # أنواع، IP، منافذ
kubectl describe svc <nom>                   # تفاصيل + Endpoints
kubectl get endpoints <nom>                  # من يوجد خلف الخدمة؟
kubectl get pods --show-labels               # الوسوم الفعلية للـ Pods
kubectl get pods -l app=<valeur>             # اختبار مُنتقٍ
kubectl port-forward deploy/portail 5000:5000    # الوصول إلى Pod بلا خدمة
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        # ما تفشل البوابة في الوصول إليه
kubectl delete svc <nom>                     # البدء من الصفر على خدمة

فخ يُعرَف مع 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. هل الخدمة موجودة، بالاسم الصحيح؟ (وإلا ← مربّع أحمر: لا شيء للتصحيح، يلزم كتابتها)
  2. هل Endpoints ممتلئة؟ (فارغة ← المُنتقي لا يطابق أي وسم Pod)
  3. هل يطابق targetPort المنفذ الذي تستمع عليه الحاوية فعلاً؟ (وإلا ← اتصال مرفوض)


الملحق A — التطبيقات

لا تعدّل أيًا من هذه الملفات. انسخها كما هي إلى المسارات المشار إليها.

A.1 — الخدمة المصغّرة العامة

هذا التطبيق الوحيد يخدم خمسة مكوّنات (api-produits وapi-commandes وcache وnotifications وbd). يُحدَّد اسمه ومنفذه عبر متغيرات بيئة.

الملف : apps/micro/app.py

python
"""Micro-service generique de demonstration.

La meme image sert a plusieurs composants : le nom et le port d'ecoute sont
fournis par des variables d'environnement (APP_NAME, PORT).
Chaque reponse contient le nom du Pod, ce qui rend visible la repartition
de charge realisee par un Service.
"""

import os
import socket

from flask import Flask, jsonify

app = Flask(__name__)

NOM = os.environ.get("APP_NAME", "micro")
PORT = int(os.environ.get("PORT", "8000"))


@app.route("/")
@app.route("/ping")
def ping():
    return jsonify(service=NOM, pod=socket.gethostname(), port=PORT)


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


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

الملف : 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 — مكوّن « المقاييس » (منفذان)

يستمع هذا التطبيق في الوقت نفسه على منفذين: 8080 (واجهة ويب) و9090 (مقاييس). وهو ما يجعل المهمة 4 ممكنة.

الملف : apps/metriques/app.py

python
"""Composant exposant DEUX ports simultanement.

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

Deux serveurs Flask tournent dans deux fils d'execution distincts.
"""

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 — البوابة (لوحة التحكم)

هي التي تعرض 8 بلاطات ودرجتك مباشرة.

الملف : apps/portail/app.py

python
"""Tableau de bord des liaisons du cluster.

Pour chaque liaison, le portail distingue TROIS situations :
  - ROUGE  : le nom DNS n'existe pas       -> le Service n'a pas ete cree
  - ORANGE : le nom resout, aucune reponse -> selecteur ou port errone
  - VERT   : la communication fonctionne   -> le Service est correct
"""

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

from flask import Flask, request

app = Flask(__name__)

DELAI = 1.5          # secondes
PORT_ATTENDU = 30500  # port par lequel le portail doit etre consulte

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 — البيانات المقدَّمة

لا تعدّل أيًا من هذين الملفين. هما الموجود الذي يجب أن تتكيّف معه خدماتك.

الملف : k8s/01-deployments.yaml

yaml
# ---------------------------------------------------------------------------
# LES PODS DE LA PLATEFORME — FOURNI, NE PAS MODIFIER
# Observez attentivement : les LABELS et les PORTS declares ici sont les seules
# informations dont vous disposez pour ecrire vos 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            # <-- port NOMME
              containerPort: 8080
            - name: prom           # <-- port NOMME
              containerPort: 9090
          readinessProbe:
            httpGet: { path: /health, port: 8080 }
            initialDelaySeconds: 3
            periodSeconds: 5

الملف : k8s/02-statefulset-bd.yaml

yaml
# ---------------------------------------------------------------------------
# LA BASE DE DONNEES (3 repliques) — FOURNI, NE PAS MODIFIER
#
# ATTENTION : le champ serviceName ci-dessous impose le NOM du Service que
# vous devrez ecrire pour que bd-0, bd-1 et bd-2 obtiennent chacun un nom DNS.
# ---------------------------------------------------------------------------
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: bd
spec:
  serviceName: bd-interne          # <-- lisez bien cette ligne
  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 — هياكل الخدمات المطلوب إكمالها

انسخ هذه الملفات الخمسة، ثم استبدل كل TODO بالقيمة الصحيحة. الأسطر المسبوقة بـ # ? هي أسئلة يجب حسمها: عليك أن تقرّر إن كان يلزم إضافة السطر أو تعديله أو حذفه.

الملف : k8s/services/01-api-produits.yaml

yaml
# MISSION 1 — Rendre l'API produits joignable depuis le portail.
#
# Le portail appelle :  http://api-produits        (donc le port 80)
# Les Pods ecoutent sur : 8000
# Les Pods portent le label : app: api-produits
#
# ? Quel type de Service pour une communication INTERNE au cluster ?
apiVersion: v1
kind: Service
metadata:
  name: api-produits          # nom IMPOSE : ne pas changer
spec:
  type: TODO
  selector:
    TODO: TODO
  ports:
    - port: TODO              # le port par lequel les clients appellent
      targetPort: TODO        # le port reellement ecoute par le conteneur

الملف : k8s/services/02-portail.yaml

yaml
# MISSION 2 — Rendre le tableau de bord accessible depuis le navigateur,
#             a l'adresse exacte : http://localhost:30500
#
# Les Pods ecoutent sur : 5000
# Les Pods portent le label : app: portail
#
# ? Quel type de Service ouvre un port sur la MACHINE ?
# ? Quelle est la plage autorisee pour ce port ?
# ? Quel champ supplementaire faut-il ajouter pour imposer le port 30500 ?
apiVersion: v1
kind: Service
metadata:
  name: portail               # nom IMPOSE : ne pas changer
spec:
  type: TODO
  selector:
    TODO: TODO
  ports:
    - port: TODO
      targetPort: TODO
      # ? une ligne manque ici

الملف : k8s/services/03-bd-interne.yaml

yaml
# MISSION 3 — Donner un nom DNS INDIVIDUEL a chaque replique de la base,
#             afin de pouvoir joindre precisement : bd-0.bd-interne
#
# Les Pods ecoutent sur : 5432
# Les Pods portent le label : app: bd
#
# ? Quel type de Service ne possede PAS d'IP virtuelle unique ?
# ? Quel champ, avec quelle valeur tres particuliere, produit cet effet ?
# ? Le nom ci-dessous doit correspondre a quel champ du StatefulSet ?
apiVersion: v1
kind: Service
metadata:
  name: bd-interne            # nom IMPOSE : ne pas changer
spec:
  # ? une ligne essentielle manque ici
  selector:
    TODO: TODO
  ports:
    - port: TODO
      targetPort: TODO

الملف : k8s/services/04-metriques.yaml

yaml
# MISSION 4 — Exposer DEUX ports sur un seul et meme Service.
#
# Le portail appelle :  http://metriques         (port 80)
#                  et : http://metriques:9090/metrics
#
# Les Pods ecoutent sur : 8080 (port nomme "web") et 9090 (port nomme "prom")
# Les Pods portent le label : app: metriques
#
# ? Quelle contrainte devient OBLIGATOIRE des qu'un Service expose plusieurs ports ?
# ? Comment faire pointer targetPort vers un port du conteneur PAR SON NOM ?
apiVersion: v1
kind: Service
metadata:
  name: metriques             # nom IMPOSE : ne pas changer
spec:
  type: TODO
  selector:
    TODO: TODO
  ports:
    - TODO: TODO              # ? un champ obligatoire manque sur chaque entree
      port: TODO
      targetPort: TODO
    - TODO: TODO
      port: TODO
      targetPort: TODO

الملف : k8s/services/05-paiement-externe.yaml

yaml
# MISSION 5 — Faire pointer un nom INTERNE vers un service EXTERNE.
#
# Le portail utilise le nom : paiement-externe
# Ce nom doit renvoyer vers : example.com
#
# ? Quel type de Service cree un simple alias DNS (CNAME) ?
# ? Ce type possede-t-il un selecteur ? des ports ? des Pods ?
apiVersion: v1
kind: Service
metadata:
  name: paiement-externe      # nom IMPOSE : ne pas changer
spec:
  type: TODO
  TODO: TODO                  # ? le champ qui indique la cible externe


الملحق D — الخدمات الثلاث المعطّلة

انسخ هذه الملفات الثلاثة كما هي، طبّقها، ثم شخّص وصحّح. كل ملف يحتوي على خطأ واحد بالضبط. لا تعِد كتابة الملف من الصفر: اعثر على العيب.

الملف : k8s/services/06-casses/casse-1.yaml

yaml
# PANNE 1
# Symptome : le Service existe, mais "kubectl get endpoints api-commandes"
#            renvoie <none>. Le portail affiche une tuile ORANGE.
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
# PANNE 2
# Symptome : "kubectl get endpoints cache" affiche bien une adresse IP,
#            mais toute connexion echoue. Le portail affiche une tuile ORANGE.
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
# PANNE 3
# Symptome : ce Service semble parfait (type correct, selecteur correct,
#            Endpoints remplis, ports coherents)... et pourtant le portail
#            affiche une tuile ROUGE et ne le joint JAMAIS.
apiVersion: v1
kind: Service
metadata:
  name: notification
spec:
  type: ClusterIP
  selector:
    app: notifications
  ports:
    - port: 80
      targetPort: 7000


الملحق E — سكربت التحقق

الملف : outils/valider.ps1

powershell
# ---------------------------------------------------------------------------
# Script de validation — donne un score, JAMAIS la solution.
# Utilisation :  .\outils\valider.ps1
# ---------------------------------------------------------------------------

$total = 0

function Existe($nom) {
    kubectl get svc $nom -o name 2>$null | Out-Null
    return $LASTEXITCODE -eq 0
}

function Afficher($libelle, $points, $max, $note) {
    $etat = if ($points -eq $max) { "[OK]    " } else { "[ECHEC] " }
    $ligne = "{0} {1} {2}/{3}" -f $etat, $libelle.PadRight(34, '.'), $points, $max
    if ($note) { $ligne += "   -> $note" }
    Write-Host $ligne
}

Write-Host ""
Write-Host "=== VALIDATION — Mission : retablir les communications ===" -ForegroundColor Cyan
Write-Host ""

# --- Mission 1 : api-produits ---------------------------------------------
$p = 0; $note = ""
if (-not (Existe "api-produits")) { $note = "Service api-produits introuvable" }
else {
    $eps = (kubectl get endpoints api-produits -o jsonpath="{.subsets[*].addresses[*].ip}" 2>$null)
    $tp  = (kubectl get svc api-produits -o jsonpath="{.spec.ports[0].targetPort}" 2>$null)
    if (-not $eps) { $note = "Endpoints vides : le selecteur ne correspond a aucun Pod" }
    elseif ("$tp" -ne "8000") { $note = "targetPort ne correspond pas au port ecoute" }
    else { $p = 15 }
}
Afficher "Mission 1 - api-produits" $p 15 $note; $total += $p

# --- Mission 2 : portail ---------------------------------------------------
$p = 0; $note = ""
if (-not (Existe "portail")) { $note = "Service portail introuvable" }
else {
    $type = (kubectl get svc portail -o jsonpath="{.spec.type}" 2>$null)
    $np   = (kubectl get svc portail -o jsonpath="{.spec.ports[0].nodePort}" 2>$null)
    if ("$np" -ne "30500") { $note = "le port expose sur la machine doit etre 30500 (actuel : '$np')" }
    elseif ($type -notin @("NodePort", "LoadBalancer")) { $note = "type inadapte a un acces externe" }
    else { $p = 15 }
}
Afficher "Mission 2 - portail" $p 15 $note; $total += $p

# --- Mission 3 : bd-interne (headless) -------------------------------------
$p = 0; $note = ""
if (-not (Existe "bd-interne")) { $note = "Service bd-interne introuvable (verifiez serviceName du StatefulSet)" }
else {
    $cip = (kubectl get svc bd-interne -o jsonpath="{.spec.clusterIP}" 2>$null)
    $eps = (kubectl get endpoints bd-interne -o jsonpath="{.subsets[*].addresses[*].ip}" 2>$null)
    if ("$cip" -ne "None") { $note = "ce Service ne doit PAS avoir d'IP virtuelle" }
    elseif (-not $eps) { $note = "Endpoints vides : verifiez le selecteur" }
    else { $p = 20 }
}
Afficher "Mission 3 - bd-interne" $p 20 $note; $total += $p

# --- Mission 4 : metriques (multi-port) ------------------------------------
$p = 0; $note = ""
if (-not (Existe "metriques")) { $note = "Service metriques introuvable" }
else {
    $ports = (kubectl get svc metriques -o jsonpath="{.spec.ports[*].port}" 2>$null)
    $noms  = (kubectl get svc metriques -o jsonpath="{.spec.ports[*].name}" 2>$null)
    $cible = (kubectl get svc metriques -o jsonpath="{.spec.ports[*].targetPort}" 2>$null)
    $liste = ($ports -split '\s+') | Where-Object { $_ }
    if ($liste.Count -lt 2) { $note = "il manque un port : deux sont attendus (80 et 9090)" }
    elseif (-not $noms) { $note = "chaque port doit porter un nom lorsqu'il y en a plusieurs" }
    elseif ($cible -match '^\s*\d+(\s+\d+)*\s*$') { $note = "targetPort doit referencer les ports PAR LEUR NOM" }
    else { $p = 15 }
}
Afficher "Mission 4 - metriques" $p 15 $note; $total += $p

# --- Mission 5 : paiement-externe -------------------------------------------
$p = 0; $note = ""
if (-not (Existe "paiement-externe")) { $note = "Service paiement-externe introuvable" }
else {
    $type = (kubectl get svc paiement-externe -o jsonpath="{.spec.type}" 2>$null)
    $cible = (kubectl get svc paiement-externe -o jsonpath="{.spec.externalName}" 2>$null)
    if ("$type" -ne "ExternalName") { $note = "ce n'est pas le type attendu pour un alias DNS" }
    elseif (-not $cible) { $note = "la cible externe n'est pas renseignee" }
    else { $p = 10 }
}
Afficher "Mission 5 - paiement-externe" $p 10 $note; $total += $p

# --- Mission 6 : les trois reparations --------------------------------------
$p = 0; $notes = @()
foreach ($cas in @(
    @{ nom = "api-commandes"; port = "8000" },
    @{ nom = "cache";         port = "6379" },
    @{ nom = "notifications"; port = "7000" })) {

    if (-not (Existe $cas.nom)) { $notes += "$($cas.nom) : Service introuvable"; continue }
    $eps = (kubectl get endpoints $cas.nom -o jsonpath="{.subsets[*].addresses[*].ip}" 2>$null)
    $tp  = (kubectl get svc $cas.nom -o jsonpath="{.spec.ports[0].targetPort}" 2>$null)
    if (-not $eps) { $notes += "$($cas.nom) : Endpoints vides" }
    elseif ("$tp" -ne $cas.port) { $notes += "$($cas.nom) : aucun Pod ne repond sur ce port" }
    else { $p += 7 }
}
if ($p -gt 20) { $p = 20 }
Afficher "Mission 6 - reparations" $p 20 ($notes -join " | "); $total += $p

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

دورة من إعداد د. هيثم رحومة — تطوير ونشر حلول البيانات