تفتح كشكاً ببائع واحد ولافتة «API». يحمل البائع شارة باسمه، ولكل زبون يقول «مرحباً، أنا jv2xw». عندما يطول الطابور، تقول للمدير «أريد ثلاثة باعة»، فتظهر شارتان جديدتان. يتوجّه الزبائن إلى نفس الشباك، لكن تارة يستجيب 4pl9z وتارة nvshh: يتوزّع الحمل. إذا غادر أحد الباعة، يكون المدير قد استدعى آخر قبل أن تلاحظ ذلك، لأن تعليمته هي «ثلاثة باعة»، وليست «هؤلاء الثلاثة بالذات». هذا بالضبط ما ستراه: traefik/whoami بائع يعرض اسمه (اسم الـ Pod)، والـ Deployment هو المدير، والـ Service هو الشباك، وlocalhost:8080 هو باب الكشك.
عندما تكتب kubectl create deployment api --image=traefik/whoami:v1.10، لا يُنشأ Pod واحد، بل ثلاثة كائنات متداخلة. يحمل الـ Deployment باسم api نيتك («هذه الصورة، هذا العدد من النسخ المتماثلة») ويعرف كيف يُدير التحديثات. فيُنشئ ReplicaSet باسم api-9bfb55fc6، مهمته الوحيدة هي الحفاظ على N نسخة متطابقة بالضبط من الـ Pods؛ اللاحقة 9bfb55fc6 هي بصمة (condensé) لنموذج الـ Pod (الصورة، المنافذ، التسميات)، فصورة جديدة ستُعطي ReplicaSet جديداً. ينشئ الـ ReplicaSet أخيراً الـ Pods: api-9bfb55fc6-jv2xw، و-4pl9z… بلاحقة عشوائية من خمسة أحرف. تحصل كل Pod على عنوان IP داخلي (10.1.0.210) لا يمكن الوصول إليه من متصفّحك.
للوصول إليها، طريقان. يفتح kubectl port-forward نفقاً مؤقتاً من طرفيتك نحو Pod واحدة: مثالي للتحقق، لكنه غير قابل للاستخدام لتوزيع الحمل، ويموت مع Ctrl+C. أما الـ Service فهو الكائن الدائم: عنوان IP ثابت داخل الكتلة (CLUSTER-IP)، واسم DNS (api.premiers-pas.svc)، وقائمة عناوين مستهدَفة يُحدّثها متحكِّم (contrôleur) اعتماداً على التسميات (app=api). ومن نوع LoadBalancer، يطلب إضافة إلى ذلك عنواناً خارجياً؛ وعلى Docker Desktop، يُجيب المكوّن vpnkit-controller بـ«localhost» وينشر منفذ الـ Service على جهازك. هذا ما يجعل http://localhost:8080 ممكناً دون نفق.
يعمل هذا الدرس بأسلوب أمري (impératif): يقول كل أمر للكتلة ما عليها فعله، فوراً. إنها الوسيلة الجيدة لتحقيق انتصار سريع ورؤية الكائنات تظهر. ابتداءً من الوحدة القادمة، ستكتب نفس الكائنات بصيغة YAML وستُطبّقها بأمر kubectl apply: إنه الأسلوب التصريحي (déclaratif)، القابل للإعادة والتخزين في نظام التحكم بالإصدارات، وهو أسلوب الحياة الحقيقية. ينتج الأسلوبان نفس الكائنات تماماً؛ ويُظهر لك kubectl get deployment api -o yaml بالفعل ما كتبه create deployment بدلاً عنك.
| الأمر الأمري | ما ينشئه أو يغيّره | المعادل التصريحي (الوحدة القادمة) |
|---|---|---|
kubectl create namespace premiers-pas | Namespace | kind: Namespace |
kubectl create deployment api --image=… --port=80 | Deployment (+ ReplicaSet + Pod) | kind: Deployment، spec.template.spec.containers[0].image |
kubectl expose deployment api --type=LoadBalancer --port=8080 --target-port=80 | Service يستهدف app=api | kind: Service، spec.type: LoadBalancer، ports[0].port: 8080، targetPort: 80 |
kubectl scale deployment api --replicas=3 | spec.replicas الخاص بالـ Deployment | تغيير replicas: 3 ثم kubectl apply |
kubectl delete pod <nom> | يحذف Pod؛ يستبدلها الـ ReplicaSet | لا شيء لكتابته: إنه التوفيق (réconciliation) |
| العمود | في get deploy | في get rs | في get pods |
|---|---|---|---|
READY | Pods جاهزة / مرغوبة (3/3) | Pods جاهزة | حاويات جاهزة / المجموع (1/1) |
UP-TO-DATE | Pods على آخر نموذج | — | — |
DESIRED / CURRENT | — | مرغوبة / موجودة | — |
STATUS | — | — | Running، ContainerCreating، ImagePullBackOff… |
تحمل كل الأوامر -n premiers-pas. إذا كنت تستخدم اختصار k من الدرس 03، استبدل kubectl بـk.
أنشئ namespace الوحدة. دُرج فارغ سيُخزَّن فيه كل ما يلي، وستحذفه الخطوة الأخيرة دفعة واحدة.
kubectl create namespace premiers-pasالمخرجات الحقيقية:
namespace/premiers-pas createdما يجب ملاحظته: created. إذا قرأت AlreadyExists، فقد احتفظت به من الدرس 03: لا مشكلة، تابع.
أنشئ الـ Deployment باسم api. صورة عمومية، بوسم (tag) ثابت (v1.10، وأبداً latest: بدون وسم دقيق، يمكن لآلتين تحميل إصدارين مختلفين وتعود مشكلة «تعمل عندي»)، والمنفذ الذي تستمع إليه الحاوية.
kubectl create deployment api --image=traefik/whoami:v1.10 --port=80 -n premiers-pas
kubectl get deploy,rs,pods -n premiers-pasالمخرجات الحقيقية (بعد ثماني ثوانٍ من الإنشاء):
deployment.apps/api created
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/api 1/1 1 1 8s
NAME DESIRED CURRENT READY AGE
replicaset.apps/api-9bfb55fc6 1 1 1 8s
NAME READY STATUS RESTARTS AGE
pod/api-9bfb55fc6-jv2xw 1/1 Running 0 8sما يجب ملاحظته: ثلاثة كائنات مقابل أمر واحد. اقرأ اسم الـ Pod من اليمين إلى اليسار: jv2xw = هذه الـ Pod بالذات (عشوائي)، و9bfb55fc6 = الـ ReplicaSet، أي نموذج الـ Pod، وapi = الـ Deployment. إذا قالت STATUS عبارة ContainerCreating، فإن الصورة قيد التحميل (بضع ثوانٍ لحجمها 10.6 ميغابايت)؛ أعد كتابة الأمر.
انظر أين تعيش الـ Pod. يُضيف -o wide عنوان IP الخاص بالـ Pod والعقدة التي تستضيفها.
kubectl get pods -n premiers-pas -o wideالمخرجات الحقيقية:
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
api-9bfb55fc6-jv2xw 1/1 Running 0 8s 10.1.0.210 docker-desktop <none> <none>ما يجب ملاحظته: عنوان IP من نطاق 10.1.0.x، شبكة داخلية للكتلة، والعقدة docker-desktop. افتح http://10.1.0.210 في متصفّحك: لا شيء يستجيب، فهذا العنوان لا وجود له إلا داخل الآلة الافتراضية. هذا كامل موضوع الخطوتين التاليتين.
افتح نفقاً باستخدام port-forward. تُحجَب الطرفية طالما بقي النفق مفتوحاً؛ هذا مقصود.
kubectl port-forward deployment/api 8080:80 -n premiers-pasالمخرجات الحقيقية:
Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80
Handling connection for 8080افتح http://localhost:8080 في المتصفح (يظهر السطر Handling connection عند كل طلب). الصفحة المعروضة، كما يُظهرها curl -s http://localhost:8080:
Hostname: api-9bfb55fc6-jv2xw
IP: 127.0.0.1
IP: ::1
IP: 10.1.0.210
IP: fe80::48b:d4ff:fe47:6adb
RemoteAddr: 127.0.0.1:43450
GET / HTTP/1.1
Host: localhost:8080
User-Agent: curl/8.21.0
Accept: */*ما يجب ملاحظته: Hostname: api-9bfb55fc6-jv2xw، اسم الـ Pod، وعنوانها 10.1.0.210. عُد إلى الطرفية واكتب Ctrl+C: يُغلَق النفق، ولا يعود http://localhost:8080 يستجيب. النفق ليس عرضاً (exposition).
اعرضها باستخدام Service من نوع LoadBalancer. --port هو منفذ الـ Service (ما تكتبه في المتصفّح)، و--target-port هو منفذ الحاوية.
kubectl expose deployment api --type=LoadBalancer --port=8080 --target-port=80 -n premiers-pas
kubectl get svc -n premiers-pasالمخرجات الحقيقية:
service/api exposed
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
api LoadBalancer 10.101.121.221 localhost 8080:30153/TCP 3sما يجب ملاحظته: تساوي EXTERNAL-IP القيمة localhost، وهذا توقيع Docker Desktop (على سحابة ستقرأ عنواناً عمومياً بعد دقيقة؛ وعلى kind، <pending> إلى الأبد). تقول PORT(S) بـ8080:30153: 8080 هو المنفذ المنشور على جهازك، و30153 منفذ عقدة مُسنَد تلقائياً لا تحتاجه هنا. أعد تحميل http://localhost:8080 دون أي port-forward: تعود صفحة whoami.
انتقل إلى ثلاث نسخ متماثلة وشاهد الحمل يتوزّع. يُغيّر الـ Deployment تعليمته؛ يُنشئ الـ ReplicaSet Pod-تين إضافيتين؛ ويضيفهما الـ Service إلى قائمته بمجرد جاهزيتهما.
kubectl scale deployment api --replicas=3 -n premiers-pas
kubectl get pods -n premiers-pasالمخرجات الحقيقية:
deployment.apps/api scaled
NAME READY STATUS RESTARTS AGE
api-9bfb55fc6-4pl9z 1/1 Running 0 7s
api-9bfb55fc6-jv2xw 1/1 Running 0 82s
api-9bfb55fc6-nvshh 1/1 Running 0 7sأعد تحميل http://localhost:8080 ست مرات (أو، في طرفية ثانية، curl -s http://localhost:8080 | findstr Hostname على Windows، | grep Hostname على bash). ست مخرجات حقيقية متتالية:
Hostname: api-9bfb55fc6-nvshh
Hostname: api-9bfb55fc6-jv2xw
Hostname: api-9bfb55fc6-4pl9z
Hostname: api-9bfb55fc6-4pl9z
Hostname: api-9bfb55fc6-4pl9z
Hostname: api-9bfb55fc6-nvshhما يجب ملاحظته: ثلاثة أسماء مختلفة، بترتيب ليس تناوباً صارماً (التوزيع عشوائي، وليس دائرياً). أما المتصفح، فهو يُبقي أحياناً نفس الاتصال مفتوحاً ويُظهر لك نفس الاسم ثلاث مرات: أغلق التبويب وأعد فتحه، أو استخدم curl. لرؤية الـ Pods وهي تُولَد مباشرة بدلاً من رؤيتها بعد وقوعها، احتفظ بطرفية ثانية مع kubectl get pods -n premiers-pas -w (-w من watch، Ctrl+C للخروج): تستخدمها العملية الموجهة 05 في كل خطوة.
احذف Pod وشاهد الاستبدال. تقتل بائعاً؛ يستدعي المدير آخر قبل أن تنتهي من القراءة.
kubectl delete pod api-9bfb55fc6-jv2xw -n premiers-pas
kubectl get pods -n premiers-pasالمخرجات الحقيقية (الأمر الثاني يُنفَّذ فوراً بعد الأول):
pod "api-9bfb55fc6-jv2xw" deleted from premiers-pas namespace
NAME READY STATUS RESTARTS AGE
api-9bfb55fc6-4pl9z 1/1 Running 0 66s
api-9bfb55fc6-nvshh 1/1 Running 0 66s
api-9bfb55fc6-xg8qt 1/1 Running 0 3sما يجب ملاحظته: اختفت jv2xw، وعمر xg8qt هو 3s: لاحظ الـ ReplicaSet «2 Pods، أريد 3» وأنشأ واحدة، باسم جديد. يبقى RESTARTS عند 0: ليست إعادة تشغيل لنفس الـ Pod، بل Pod جديدة تماماً. لم يتوقّف الموقع عن الاستجابة أبداً، فقد استوعبت الـ Pod-تان الأخريان حركة المرور.
اقرأ السجلات. يقبل kubectl logs Pod أو، وهو الأعملي، الـ Deployment (يختار واحدة منها).
kubectl logs deployment/api -n premiers-pasالمخرجات الحقيقية:
Found 3 pods, using pod/api-9bfb55fc6-4pl9z
2026/09/11 01:41:49 Starting up on port 80ما يجب ملاحظته: whoami صامت، سطر واحد عند الإقلاع وهذا كل شيء. لـ Pod محدد: kubectl logs api-9bfb55fc6-4pl9z -n premiers-pas؛ للمتابعة المباشرة: -f. هنا ستقرأ، لاحقاً في الدورة، أخطاء تطبيق يتعطّل.
أجرِ جرداً للدُرج. يُلخّص get all الأنواع الأربعة للكائنات التي أنشأتها.
kubectl get all -n premiers-pasالمخرجات الحقيقية:
NAME READY STATUS RESTARTS AGE
pod/api-9bfb55fc6-4pl9z 1/1 Running 0 72s
pod/api-9bfb55fc6-nvshh 1/1 Running 0 72s
pod/api-9bfb55fc6-xg8qt 1/1 Running 0 9s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/api LoadBalancer 10.101.121.221 localhost 8080:30153/TCP 75s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/api 3/3 3 3 2m27s
NAME DESIRED CURRENT READY AGE
replicaset.apps/api-9bfb55fc6 3 3 3 2m27sما يجب ملاحظته: 3/3 في كل مكان، Service واحد، ReplicaSet واحد. أنشأت كائنين يدوياً (Deployment، Service)؛ وأنشأت الكتلة الأربعة الأخرى.
ألقِ نظرة على YAML الذي كُتب بدلاً عنك. كل كائن Kubernetes هو مستند YAML مُخزَّن في etcd؛ يُعيده لك -o yaml كما هو. إنها لمحة من الوحدة 2.
kubectl get deployment api -n premiers-pas -o yamlالمخرجات الحقيقية (حُذفت حقول metadata التتبعية وجزء من status):
apiVersion: apps/v1
kind: Deployment
metadata:
generation: 2
labels:
app: api
name: api
namespace: premiers-pas
spec:
replicas: 3
selector:
matchLabels:
app: api
template:
spec:
containers:
- image: traefik/whoami:v1.10
imagePullPolicy: IfNotPresent
name: whoami
ports:
- containerPort: 80
status:
availableReplicas: 3
readyReplicas: 3
replicas: 3ما يجب ملاحظته: spec هو ما طلبته (replicas: 3 منذ scale الخاص بك، ومن هنا generation: 2)، وstatus هو ما تُلاحظه الكتلة؛ لا تفعل حلقة التوفيق من الدرس 01 سوى تقريب الثاني من الأول. القيم التي لم تكتبها أبداً (imagePullPolicy: IfNotPresent، وقسم strategy: RollingUpdate تم حذفه هنا) هي قيم افتراضية ستتعلّم الوحدة 3 كيفية ضبطها. نفس الـ Deployment والـ Service الخاص به، مكتوبَين يدوياً بصيغة YAML كاملة، موجودان في حزمة الدورة: 01-installer-kubernetes-avec-docker-desktop/04-api-declaratif.yaml (يُعيد kubectl apply -f على هذا الملف إنشاء الكائنين؛ تم التحقق: deployment.apps/api created، service/api created).
احذف كل شيء دفعة واحدة وأثبت ذلك. يحذف حذف الـ namespace كل ما يحتويه؛ ويُحجَب الأمر حتى يتم ذلك (47 ثانية على آلة الدورة).
kubectl delete namespace premiers-pas
kubectl get all -n premiers-pas
kubectl get namespace premiers-pasالمخرجات الحقيقية:
namespace "premiers-pas" deleted
No resources found in premiers-pas namespace.
Error from server (NotFound): namespaces "premiers-pas" not foundما يجب ملاحظته: لم يعد هناك شيء، ولا يعود http://localhost:8080 يستجيب (يعرض المتصفح «هذا الموقع غير قابل للوصول»، ويُجيب curl بـConnection refused). أُعيد المنفذ 8080 إلى جهازك: تُعيد استخدامه العملية الموجهة 05 والوحدات التالية.
يُظهر kubectl get pods -n premiers-pas الحالة api-… 0/1 ImagePullBackOff (أو ErrImagePull) ← وسم الصورة غير موجود أو يرفض Docker Hub تقديم الصورة. يُعطي kubectl describe pod <nom> -n premiers-pas، في قسم Events، السبب؛ على آلة الدورة، بوسم غير موجود traefik/whoami:v9.99: Failed to pull image "traefik/whoami:v9.99": Error response from daemon: failed to resolve reference "docker.io/traefik/whoami:v9.99": docker.io/traefik/whoami:v9.99: not found. رسالة أخرى ممكنة، 429 Too Many Requests، تُشير إلى أن Docker Hub يُقيّد التحميلات المجهولة: انتظر أو سجّل الدخول بـdocker login. صحّح الوسم بـkubectl set image deployment/api whoami=traefik/whoami:v1.10 -n premiers-pas (تفعل العملية الموجهة 05 ذلك خطوة بخطوة).
يعرض kubectl get svc القيمة EXTERNAL-IP <pending> بدلاً من localhost ← على Docker Desktop، يعني هذا في الغالب أن المنفذ المطلوب منشور بالفعل بواسطة Service آخر من نوع LoadBalancer (رسالة حصلنا عليها بعرض Service ثانٍ على المنفذ 8080: يبقى <pending>، ويحتفظ الأول بالمنفذ). يُظهر kubectl get svc -A من يشغل المنفذ؛ اختر واحداً آخر (--port=8081) أو احذف التكرار. إذا لم تكن على Docker Desktop (kind، minikube)، فإن <pending> هي الحالة الطبيعية: استخدم port-forward.
يُجيب kubectl port-forward بـUnable to listen on port 8080 (متبوعاً، على Windows، بـbind: Only one usage of each socket address … is normally permitted) ← المنفذ المحلي 8080 مستخدَم بالفعل من تطبيق آخر. لم نُعِد إنتاج ذلك على آلة الدورة، حيث تعايش LoadBalancer على المنفذ 8080 مع port-forward على المنفذ 8080 دون خطأ (يقبل Windows استماعاً على 0.0.0.0 وآخر على 127.0.0.1)، مما يجعل النتيجة غامضة. في كل الحالات، غيّر المنفذ المحلي: kubectl port-forward deployment/api 9090:80 -n premiers-pas، ثم http://localhost:9090.
يُظهر المتصفح دائماً نفس Hostname بعد scale --replicas=3 ← HTTP keep-alive: يُعيد المتصفح استخدام اتصاله نحو نفس الـ Pod. ليس عيباً في التوزيع. أغلق التبويب، أعد فتحه، أو قارن باستخدام curl، الذي يفتح اتصالاً جديداً في كل استدعاء.
Error from server (NotFound): deployments.apps "api" not found ← نسيان -n premiers-pas؛ بحث kubectl في default. تحمل كل أوامر هذه الدورة -n.
kubectl create deployment api --image=traefik/whoami:v1.10 --port=80 -n premiers-pas ثلاثة كائنات: Deployment باسم api ← ReplicaSet باسم api-9bfb55fc6 ← Pod باسم api-9bfb55fc6-jv2xw؛ يُقرَأ اسم Pod من اليمين إلى اليسار.10.1.0.x) إلا داخل الكتلة؛ يُعدّ kubectl port-forward نفقاً مؤقتاً نحو Pod واحدة، بينما يُشكّل Service الوصول الدائم والموزَّع.kubectl expose deployment api --type=LoadBalancer --port=8080 --target-port=80: على Docker Desktop، تساوي EXTERNAL-IP القيمة localhost ويستجيب http://localhost:8080 دون نفق.kubectl scale deployment api --replicas=3: ثلاث Pods، ويتغيّر Hostname من استدعاء curl إلى آخر، بترتيب عشوائي.RESTARTS 0): إنه التوفيق، وليس إعادة تشغيل.kubectl delete namespace premiers-pas كل محتوى الدُرج بأمر واحد؛ ويجب أن يُجيب kubectl get namespace premiers-pas بعدها بـNotFound.kubectl get <objet> -o yaml المستند الذي كتبه create وexpose بدلاً عنك: spec = ما طُلب، status = ما لُوحظ.labo.ps1 / labo.sh)، وتُعيد هذا النشر، وتكسره مرتين، وتُصلحه.على Docker Desktop، لا يُنشَر على localhost سوى منفذ الـ LoadBalancer (8080)؛ أما منفذ العقدة المرافق له (30153 في 8080:30153) فلا يستجيب من جهازك (تم التحقق: رفض الاتصال)، بينما يستجيب Service من نوع NodePort مُنشأ بمفرده بشكل جيد على localhost:<port>. على كتلة سحابية، العكس هو المهم: يتسبّب LoadBalancer في إنشاء موازن حمل يُفوتَر لدى المزوّد (ELB، Cloud Load Balancing، Azure LB) وتصبح EXTERNAL-IP عنواناً عمومياً بعد دقيقة أو دقيقتين؛ لهذا السبب لا يُعرَض كل تطبيق بهذه الطريقة بل خلف Ingress واحد، الذي تُقدّمه الدورة لاحقاً. أنواع الـ Service الثلاثة ونطاق كل واحد منها بدقة: kubernetes.io/docs/concepts/services-networking/service/.