المشروع projet11-kubernetes-services · مستند مرجعي معمَّق.
يذهب هذا المستند أبعد بكثير من التصحيح: يفصّل كل أنواع الخدمات، والمفاهيم الداخلية (kube-proxy، Endpoints، EndpointSlices، DNS)، وحقول YAML المهمة، وسياسات الحركة، وتآلف الجلسة، وتعدد المنافذ، والمزالق الكلاسيكية وأفضل الممارسات.
الـ Pod زائل: يمكن إعادة إنشائه في أي لحظة، بـ عنوان IP جديد. إذن لا يمكن الاعتماد على IP لـ Pod للتواصل.
الـ Service تجريد مستقر يقوم بـ:
الفكرة الأساسية: الخدمة لا «تحتوي» الـ Pods. هي تجدها باستمرار بفضل مُنتقي الوسوم، وتصون قائمة عناوينها في Endpoints.
apiVersion: v1
kind: Service
metadata:
name: mon-service
labels:
app: demo
annotations: {} # بيانات وصفية (غالباً يستخدمها LoadBalancer السحابي)
spec:
type: ClusterIP # ClusterIP | NodePort | LoadBalancer | ExternalName
selector: # أي Pods تستهدفها هذه الخدمة (بالوسوم)
app: demo
ports:
- name: http # اسم المنفذ (مفيد إن وُجدت عدة منافذ)
protocol: TCP # TCP (افتراضي) | UDP | SCTP
port: 80 # منفذ الخدمة (ما يراه العملاء)
targetPort: 5000 # منفذ الحاوية (أو اسم منفذ الحاوية)
nodePort: 30080 # (NodePort/LoadBalancer) منفذ مفتوح على العقدة
clusterIP: 10.96.0.10 # (اختياري) IP ثابت؛ "None" = headless
sessionAffinity: None # None | ClientIP
externalTrafficPolicy: Cluster # Cluster | Local (NodePort/LoadBalancer)
internalTrafficPolicy: Cluster # Cluster | Local
ipFamilyPolicy: SingleStack # SingleStack | PreferDualStack | RequireDualStack
externalIPs: [] # عناوين IP خارجية موجَّهة نحو هذه الخدمة (متقدم)كل حقل مفصَّل أدناه. يمكن إنشاء خدمة دنيا في 8 أسطر؛ لبقية الحقول قيم افتراضية معقولة.
النوع الافتراضي. يخصّص عنوان IP افتراضياً داخلياً (في نطاق Service CIDR، مثلاً 10.96.0.0/12)، قابل للوصول من داخل العنقود فقط.
apiVersion: v1
kind: Service
metadata:
name: demo-clusterip
spec:
type: ClusterIP
selector:
app: demo-back
ports:
- port: 80
targetPort: 5000الخصائص:
EXTERNAL-IP).http://demo-clusterip (انظر §13).متى تستخدمه: لكل ما يبقى داخل العنقود. هذا النوع الأكثر شيوعاً.
يفعل كل ما يفعله ClusterIP (يحصل على IP داخلي)، وزيادة: يفتح منفذاً ثابتاً على كل عقدة في العنقود (النطاق الافتراضي 30000–32767).
apiVersion: v1
kind: Service
metadata:
name: demo-nodeport
spec:
type: NodePort
selector:
app: demo-back
ports:
- port: 80 # منفذ الخدمة (داخلي)
targetPort: 5000 # منفذ الحاوية
nodePort: 30082 # منفذ مفتوح على كل عقدةالوصول: http://<ip-de-n-importe-quel-noeud>:30082 (مع Docker Desktop: http://localhost:30082).
نقاط مهمة:
nodePort، يختار Kubernetes واحداً في النطاق.يفعل كل ما يفعله NodePort، وزيادة: يطلب من البنية التحتية (السحابة) توفير موزّع حمل خارجي بـ عنوان IP عام.
apiVersion: v1
kind: Service
metadata:
name: demo-lb
spec:
type: LoadBalancer
selector:
app: demo-back
ports:
- port: 8090
targetPort: 5000حسب البيئة:
| البيئة | السلوك |
|---|---|
| AWS / GCP / Azure | ينشئ LB مُداراً حقيقياً (ELB/NLB، GCP LB…) ويملأ EXTERNAL-IP |
| Docker Desktop | EXTERNAL-IP = localhost ← http://localhost:8090 |
| minikube | يوفّر minikube tunnel العنوان الخارجي |
| kind / bare-metal | يبقى <pending> بلا متحكّم مثل MetalLB |
السلسلة الكاملة: LoadBalancer → NodePort → ClusterIP → Endpoints → Pods.
التعليقات التوضيحية (خاصة بالمزوّد) تقود الـ LB، مثلاً على AWS:
metadata:
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
service.beta.kubernetes.io/aws-load-balancer-internal: "true"LoadBalancer لكل خدمة = مكلف في السحابة. في الإنتاج نفضّل غالباً نقطة دخول واحدة (Ingress/Gateway) أمام عدة خدمات (انظر §17).
حالة خاصة: لا مُنتقي، لا Pod، لا IP. ينشئ ببساطة اسماً مستعاراً DNS (سجل CNAME) نحو اسم خارجي.
apiVersion: v1
kind: Service
metadata:
name: base-externe
spec:
type: ExternalName
externalName: db.exemple.com # الـ Pods التي تستدعي "base-externe" تُوجَّه هناالاستخدام: توجيه اسم داخلي مستقر (base-externe) نحو خدمة خارج العنقود (قاعدة مُدارة، واجهة طرف ثالث). إن تغيّر العنوان، نعدّل مكاناً واحداً.
الحد: هذا DNS خالص، بلا توزيع حمل ولا تحكم في المنفذ. لا يناسب إن انتظرت الخدمة الخارجية
HostHTTP خاصاً.
بوضع clusterIP: None نحصل على خدمة بلا عنوان IP افتراضي. يعيد DNS إذن مباشرة عناوين IP لكل الـ Pods (قائمة سجلات A)، بدل عنوان واحد.
apiVersion: v1
kind: Service
metadata:
name: demo-headless
spec:
clusterIP: None # <-- headless
selector:
app: demo-back
ports:
- port: 80
targetPort: 5000فائدتها:
pod-0.demo-headless، pod-1.demo-headless…)، مفيد لقواعد البيانات المنسوخة (Cassandra، Kafka، إلخ).| ClusterIP عادي | Headless (clusterIP: None) | |
|---|---|---|
| عنوان IP افتراضي | نعم (واحد) | لا |
| استجابة DNS | عنوان واحد (عنوان الخدمة) | N عناوين (عناوين الـ Pods) |
| التوزيع | عبر kube-proxy | على عاتق العميل |
| حالة الاستخدام | ويب/واجهة بلا حالة | قواعد منسوخة، StatefulSet |
يمكن ألا يكون للخدمة مُنتقٍ. عندئذ لا يملأ Kubernetes الـ Endpoints وحده: أنت تعرّفها يدوياً. عملي لعرض مورد خارجي تحت عنوان IP داخلي مستقر.
apiVersion: v1
kind: Service
metadata:
name: api-legacy
spec:
ports:
- port: 80
targetPort: 8080
---
apiVersion: v1
kind: Endpoints # (أو EndpointSlice، أحدث)
metadata:
name: api-legacy # الاسم نفسه للخدمة
subsets:
- addresses:
- ip: 192.168.1.50 # خادم خارجي
ports:
- port: 8080الفرق مع ExternalName: هنا نوجّه عبر IP (مع توزيع حمل ممكن على عدة عناوين)، لا عبر CNAME DNS.
هذا مصدر اللبس. ثلاثة منافذ مختلفة، ثلاثة أدوار:
| الحقل | أين | المعنى |
|---|---|---|
port | على الخدمة | المنفذ الذي يستخدمه العملاء للوصول إلى الخدمة |
targetPort | على الحاوية | المنفذ الذي يستمع عليه فعلاً التطبيق في الـ Pod |
nodePort | على العقدة | (NodePort/LoadBalancer) المنفذ المفتوح على الآلة |
مثال يُقرأ بصوت عالٍ: «يضغط العملاء على المنفذ 80 للخدمة، التي تنقل نحو المنفذ 5000 للحاوية؛ في NodePort ندخل أيضاً عبر المنفذ 30082 للآلة».
يمكن أن يشير
targetPortإلى اسم منفذ معرَّف في الحاوية (انظر §10)، ما يتجنّب ترميز الرقم ثابتاً.
يمكن للخدمة عرض عدة منافذ (مثلاً HTTP + مقاييس). عندئذ يجب أن يكون لكل مدخل name.
spec:
selector:
app: demo
ports:
- name: http
port: 80
targetPort: web # يشير إلى منفذ مسمّى للحاوية
- name: metrics
port: 9090
targetPort: 9090من جهة الحاوية، نسمّي المنافذ:
containers:
- name: app
ports:
- name: web # <-- يعيد استخدامه targetPort: web
containerPort: 5000
- name: metrics
containerPort: 9090فائدة المنافذ المسمّاة: إن تغيّر منفذ الحاوية، لا شيء يُعدَّل في الخدمة.
الخدمة كائن مجرّد: ليست عملية تستقبل الحركة. تُنجَز المعجزة بواسطة kube-proxy، مكوّن موجود على كل عقدة، يبرمج قواعد الشبكة في النواة لإعادة توجيه «IP:port الخدمة» نحو «IP:port لـ Pod».
أوضاع kube-proxy:
| الوضع | المبدأ | ملاحظات |
|---|---|---|
| iptables (افتراضي) | قواعد iptables، اختيار عشوائي لـ Pod | بسيط، متين، شائع جداً |
| IPVS | جدول تجزئة في النواة، خوارزميات LB حقيقية (rr، lc، sh…) | أعلى أداء على العناقيد الكبيرة |
| nftables | خليفة iptables | أحدث |
آثار عملية:
يتجسّد الرابط خدمة ↔ Pods في كائنات:
IP:port للـ Pods الجاهزة.kubectl get endpoints demo-clusterip
kubectl get endpointslices -l kubernetes.io/service-name=demo-clusteripمن يحدّث القائمة؟ endpoint controller: ما إن يصير Pod Ready (readinessProbe سليم) ويطابق المُنتقي، يدخل عنوانه؛ إن سقط، يخرج.
يُسحب Pod غير Ready من Endpoints ← لا يتلقى حركة. لذلك readinessProbe أساسي: يتحكم في من هو «داخل» الخدمة. (استثناء:
publishNotReadyAddresses: trueينشر أيضاً الـ Pods غير الجاهزة — استخدام headless خاص.)
يشغّل Kubernetes CoreDNS. تتلقى كل خدمة اسماً DNS حتمياً:
<service> # النطاق نفسه
<service>.<namespace> # نطاق آخر
<service>.<namespace>.svc.cluster.local # الاسم الكامل FQDNمثال من Pod:
curl http://demo-clusterip # النطاق نفسه
curl http://demo-clusterip.default # صريح
curl http://demo-clusterip.default.svc.cluster.localالسجلات المنتَجة:
_http._tcp.demo-clusterip….تاريخياً، كان Kubernetes يحقن أيضاً متغيرات بيئة (
DEMO_CLUSTERIP_SERVICE_HOST،..._PORT) في الـ Pods المنشأة بعد الخدمة. يبقى DNS الطريقة الموصى بها (تعمل مهما كان ترتيب الإنشاء).
| القيمة | الأثر | المقايضة |
|---|---|---|
| Cluster (افتراضي) | يمكن إعادة توجيه الحركة نحو عقدة أخرى للوصول إلى Pod | توزيع جيد، لكن عنوان IP المصدر للعميل يُخفى (SNAT) وقفزة شبكة إضافية |
| Local | يخدم فقط الـ Pods في العقدة التي تستقبل الحزمة | يحافظ على IP المصدر للعميل، بلا قفزة؛ لكن اختلال إن وُزِّعت الـ Pods سيئاً |
| القيمة | الأثر |
|---|---|
| Cluster (افتراضي) | يوجّه نحو أي Pod للخدمة |
| Local | يوجّه فقط نحو الـ Pods في العقدة نفسها (مفيد للكمون / المحلية) |
externalTrafficPolicy: Localهو الإعداد الأساس عندما تحتاج إلى معرفة عنوان IP الحقيقي للعميل (سجلات، أمن، تحديد موقع).
افتراضياً، يمكن لكل طلب أن يذهب نحو أي Pod. لـ «لصق» عميل بـ Pod نفسه:
spec:
sessionAffinity: ClientIP
sessionAffinityConfig:
clientIP:
timeoutSeconds: 10800 # 3 ساعاتNone (افتراضي): توزيع في كل طلب.ClientIP: كل طلبات عنوان IP نفسه تذهب إلى الـ Pod نفسه (جلسات لاصقة أساسية L4).لجلسات HTTP أدق (حسب ملف تعريف الارتباط)، نستخدم Ingress (L7).
protocol: TCP (افتراضي)، UDP (DNS، ألعاب، بث)، SCTP (اتصالات).appProtocol (إرشادي) البروتوكول التطبيقي (http، https، grpc) للأدوات/الـ LB.ports:
- name: dns-udp
port: 53
protocol: UDP
targetPort: 53
- name: dns-tcp
port: 53
protocol: TCP
targetPort: 53تعمل الخدمة في L4 (IP/منفذ). لا تعرف التوجيه حسب URL أو اسم المضيف، ولا إدارة TLS. لذلك:
| الكائن | الطبقة | الدور |
|---|---|---|
| Service (ClusterIP/NodePort/LB) | L3/L4 | عنوان مستقر + LB بسيط نحو Pods |
| Ingress | L7 (HTTP/HTTPS) | توجيه حسب المضيف والمسار، TLS، نقطة دخول واحدة لـ عدة خدمات |
| Gateway API | L7 (خليفة Ingress) | أكثر تعبيراً، فصل الأدوار، متعدد البروتوكولات |
النموذج النموذجي في الإنتاج: LoadBalancer واحد → Ingress → عدة ClusterIP داخلية. نوفّر الـ LB المكلفة ونمركز TLS/التوجيه.
| النوع | IP داخلي | وصول خارجي | DNS | مُنتقي | حالة الاستخدام |
|---|---|---|---|---|---|
| ClusterIP | نعم | لا | سجل A واحد (ClusterIP) | نعم | تواصل داخلي (الأكثر شيوعاً) |
| NodePort | نعم | منفذ العقدة | سجل A واحد | نعم | تطوير/محلي، لبنة LB |
| LoadBalancer | نعم | IP عام | سجل A واحد | نعم | خدمة عامة في السحابة |
| ExternalName | لا | — (CNAME) | CNAME | لا | اسم مستعار نحو خدمة خارجية |
Headless (clusterIP: None) | لا | لا | N سجلات A (Pods) | نعم | StatefulSet، قواعد منسوخة |
| بلا مُنتقي | نعم | حسب النوع | سجل A واحد | لا | Endpoints يدوية (مورد خارجي) |
| العَرَض | السبب الشائع | الحل |
|---|---|---|
| الخدمة لا تجيب | المُنتقي ≠ وسوم الـ Pods | محاذاة spec.selector و labels لقالب الـ Pod |
| Endpoints فارغة | لا Pod Ready أو لا Pod مطابق | kubectl get endpoints <svc>؛ تحقق من readinessProbe والوسوم |
| اتصال مرفوض داخلياً | targetPort خاطئ | targetPort = المنفذ الفعلي للحاوية |
يبقى EXTERNAL-IP على <pending> | لا متحكّم LB (kind/bare-metal) | Docker Desktop سليم؛ وإلا MetalLB / port-forward |
| عنوان IP المصدر للعميل مخفي | externalTrafficPolicy: Cluster | الانتقال إلى Local |
| NodePort غير متاح | منفذ خارج النطاق / مشغول | استخدم 30000–32767، غيّر nodePort |
| DNS لا يحل | نطاق خاطئ / CoreDNS متعطل | اختبر FQDN؛ kubectl -n kube-system get pods (coredns) |
أوامر التشخيص:
kubectl get svc <nom> -o wide
kubectl describe svc <nom>
kubectl get endpoints <nom>
kubectl get endpointslices -l kubernetes.io/service-name=<nom>
kubectl run test --rm -it --image=busybox:1.36 -- sh # nslookup <svc>, wget -qO- http://<svc>targetPort بالاسم ← فك الارتباط).app، tier، version).externalTrafficPolicy: Local عندما يهم عنوان IP الحقيقي للعميل.اسحب type: NodePort (أو ضع ClusterIP)، أعد التطبيق، ثم أثبت أنه لم يعد قابلاً للوصول من المضيف لكنه قابل للوصول بالاسم من Pod (curl http://demo-nodeport... بعد إعادة التسمية). لاحظ kubectl get svc: لا عمود nodePort بعد.
غيّر مُنتقي الخدمة إلى app: inexistant، أعد التطبيق، ولاحظ kubectl get endpoints فارغاً + خدمة غير قابلة للوصول. أعد app: demo-back: تعود Endpoints.
أنشئ خدمة بـ clusterIP: None، ثم من Pod: nslookup demo-headless. يجب أن ترى عدة عناوين IP (واحد لكل Pod) بدل واحد.
أضف منفذاً metrics (9090) مسمّى للحاوية وللخدمة. تحقّق بـ kubectl describe svc أن المنفذين يظهران، وأن targetPort يشير فعلاً إلى اسم المنفذ.
أنشئ خدمة ExternalName نحو example.com. من Pod: يجب أن يعيد nslookup mon-alias CNAME نحو example.com.
العودة إلى تصحيح المشروع · المفاهيم الأساسية: 01-CONCEPTS-SERVICES.md · الأوامر: 02-COMMANDES.md.
دورة من إعداد د. هيثم رحومة — تطوير ونشر حلول البيانات