تعرف منظّم الحرارة (thermostat) في صالة منزلك: تضبطه على 21 درجة مئوية ولا تعود تفكّر فيه. فهو يقيس درجة الحرارة، يقارنها بما ضبطته، يُشغّل التدفئة أو يوقفها، وإذا فتح أحدهم النافذة فإنه يستدرك الأمر دون أن تتحرّك أنت. يفعل Kubernetes الأمر نفسه تماماً مع حاوياتك (conteneurs). تُعلن «أريد ثلاث نسخ من واجهتي البرمجية (API)، متاحة دائماً على المنفذ 8080»؛ فيقارن باستمرار ما تريده بما يعمل فعلياً، ويصحّح: تموت حاوية في الساعة الثالثة صباحاً، فيُعيد تشغيل واحدة أخرى؛ تتعطّل آلة، فيُعيد وضع الحاويات في مكان آخر؛ تُغيّر إصدار الصورة، فيستبدل النسخ واحدة تلو الأخرى دون قطع الخدمة. مع docker run، أنت منظّم الحرارة: أنت من يراقب ويُعيد التشغيل. في هذا الدرس، سترى الفرق بعينيك: تقتل حاوية Docker وتلاحظ أنها تبقى ميتة، ثم تنظر إلى «قطع» Kubernetes التي تعمل بالفعل على جهازك، جاهزة لأداء هذا العمل بدلاً منك.
ثلاثة مواقف يعيشها الجميع في النهاية مع Docker وحده. الأول: تعمل واجهتك البرمجية داخل حاوية على خادم؛ في إحدى الأمسيات تتعطّل (تسريب ذاكرة، اعتمادية خارجية معطّلة) ولا يلاحظ أحد ذلك حتى الصباح. يُعيد --restart=always تشغيل العملية، لكنه لا يُخبرك بشيء، ولا يتحقق من أن التطبيق يستجيب فعلاً، ولا يفعل شيئاً إذا انطفأ الخادم كلياً. الثاني: لديك عشر حاويات لنفس الواجهة البرمجية خلف وكيل عكسي (reverse proxy) وعليك تسليم الإصدار 2 دون أن يرى المستخدمون أي خطأ؛ يدوياً، يعني ذلك إيقاف وإعادة تشغيل وإزالة كل حاوية من الوكيل بالترتيب الصحيح، عشر مرات، دون خطأ. الثالث: لم يعد حركة المرور تتّسع على آلة واحدة؛ عليك إضافة اثنتين أخريين، وتقرير أي حاوية تذهب إلى أين، وجعل الحاويات تجد بعضها بعضاً رغم أن عناوين IP الخاصة بها تتغيّر مع كل إعادة تشغيل.
هذه المشاكل الثلاث هي مهنة المُنسِّق (orchestrateur). يقترح Docker حلَّين جزئيَّين: يصف Compose عدة حاويات في ملف واحد لكنه يبقى على آلة واحدة ولا يراقب شيئاً؛ ويضيف Swarm تعدد الآلات والنسخ المتماثلة لكن منظومته بقيت صغيرة. أصبح Kubernetes (يُختصر K8s: «K»، ثم ثماني حروف، ثم «s»)، الذي وُلد لدى Google سنة 2014 وتديره اليوم مؤسسة CNCF، هو المعيار السائد: ستجده لدى AWS (باسم EKS)، وGoogle (باسم GKE)، وAzure (باسم AKS)، وOVH، وScaleway، وعلى خوادم معظم الشركات.
| المعيار | Docker Compose | Docker Swarm | Kubernetes |
|---|---|---|---|
| النطاق | آلة واحدة | عدة آلات | عدة آلات (بالآلاف) |
| إعادة التشغيل التلقائي | restart: فقط | نعم | نعم، مع مسابر صحة (readiness، liveness) |
| التحديث دون انقطاع | لا | نعم، بشكل بسيط | نعم، بشكل تدريجي، مع تراجع بأمر واحد |
| توزيع الحمل المدمج | لا | نعم | نعم (Services)، إضافة إلى Ingress وDNS داخلي |
| التحجيم التلقائي | لا | لا | نعم (HPA، بناءً على المعالج أو الذاكرة أو مقاييس الأعمال) |
| المنظومة (Helm، المشغّلات (operators)، السحابة) | — | صغيرة | ضخمة |
| منحنى التعلّم | سهل | متوسط | حاد، ومن هنا هذه الدورة |
تتكوّن كتلة Kubernetes من جزأين. control plane يقرّر؛ العقد (nœuds) تنفّذ. على Docker Desktop، يعيش الجزآن على نفس الآلة الافتراضية، لكن الأدوار تبقى نفسها كما في بيئة الإنتاج.
| المكوّن | أين | الدور في جملة واحدة |
|---|---|---|
kube-apiserver | control plane | يستقبل كل الأوامر (kubectl، المتحكّمات، kubelet) عبر HTTPS على المنفذ 6443؛ لا يحدث شيء دون المرور به. |
etcd | control plane | قاعدة بيانات مفتاح-قيمة تخزّن الحالة المرغوبة والحالة الملاحظة لكامل الكتلة؛ فقدانها بدون نسخة احتياطية يعني فقدان الكتلة. |
kube-scheduler | control plane | يختار على أي عقدة يضع كل Pod جديدة (الموارد المتاحة، القيود). لا يُشغّل شيئاً بنفسه. |
kube-controller-manager | control plane | يجمع حلقات التوفيق: «تنقص نسخة متماثلة؟ أُنشئ واحدة»، «عقدة توقّفت عن الاستجابة؟ أُعلّم الـ Pods الخاصة بها لإعادة وضعها». |
kubelet | كل عقدة | الوكيل الذي يقرأ من الـ API الـ Pods المُسندة إليه، ويطلب من runtime تشغيل الحاويات، ويرفع حالتها. |
kube-proxy | كل عقدة | يبرمج قواعد الشبكة كي يصل عنوان الـ Service إلى الـ Pod الصحيحة. |
| runtime | كل عقدة | البرنامج الذي يُشغّل الحاويات فعلياً: containerd أو CRI-O عموماً، وdocker://29.3.1 على Docker Desktop. |
جوهر النموذج هو إعلان الحالة المرغوبة. أنت لا تأمر بـ«شغّل حاوية»؛ بل تكتب (أو تُولّد) كائناً يقول «أريد Deployment باسم api، بصورة traefik/whoami:v1.10، بثلاث نسخ متماثلة». يُسجّل الـ API ذلك في etcd. تقارن المتحكّمات (controllers) بعد ذلك، في حلقة دائمة ومستمرة، هذه الحالة المرغوبة بـالحالة الملاحظة التي ترفعها الـ kubelets؛ وكل انحراف يُطلق إجراءً. لهذا السبب تُبعث Pod محذوفة من جديد: لم يُعِد أحد «تشغيلها»، بل لاحظ المتحكّم ببساطة 2 بدلاً من 3 وصحّح الأمر. سترى الحلقة تعمل في الدرس 04.
لماذا التعلّم على Docker Desktop بدلاً من minikube أو kind أو كتلة سحابية؟ لأنك على الأرجح تملكه بالفعل، ولأن نقرة واحدة تُفعّل كتلة كاملة، ولأن الصور التي تبنيها بأمر docker build مرئية مباشرة من طرف Kubernetes (نفس مخزن الصور، لا حاجة لتثبيت سجلّ صور)، ولأن Services من نوع LoadBalancer تحصل على عنوان localhost: تفتح متصفّحك ويستجيب، تماماً كما في الإنتاج خلف موازن حمل حقيقي. حدوده واضحة أيضاً: عقدة واحدة فقط (لا وجود لـ«آلة تتعطّل» لمحاكاتها)، ولا NetworkPolicy مطبَّقة افتراضياً (يتجاهلها CNI المدمج)، ولا تخزين شبكي ولا عنوان IP عمومي. كل ما ستكتبه هنا سيُطبَّق كما هو على كتلة حقيقية؛ فقط طريقة الوصول إليها ستتغيّر.
| الخيار | التثبيت | العقد | LoadBalancer على localhost | صور محلية بدون سجلّ | لمن |
|---|---|---|---|---|---|
| Docker Desktop (هذه الدورة) | نقرة واحدة في التطبيق | 1 (kubeadm) أو أكثر (kind، 4.38+) | نعم | نعم | التعلّم والتطوير على جهازك |
| minikube | ملف تنفيذي + برنامج تشغيل (Docker، VM) | 1، تعدد ممكن | عبر minikube tunnel | عبر minikube image load | التعلّم، إضافات جاهزة |
| kind | ملف تنفيذي، العقد = حاويات Docker | عدة عقد | لا (port-forward) | عبر kind load docker-image | الاختبارات الآلية، CI |
| السحابة (EKS، GKE، AKS…) | حساب، فوترة، شبكة | بقدر ما تشاء | عنوان IP عمومي حقيقي | لا، يلزم سجلّ صور | الإنتاج |
تُكتب كل الأوامر في PowerShell (على Windows) أو في طرفية (على macOS وLinux)؛ وهي متطابقة. لا شيء مما يلي يُعدّل كتلتك: أنت تُلاحظ فقط.
شغّل حاوية باستخدام Docker، كما تعرف فعله بالفعل. traefik/whoami تطبيق ويب صغير يعرض اسم الآلة التي تستجيب: إنها الصورة الخيط الناظم للوحدات الثلاث الأولى.
docker run -d --name essai-whoami -p 8089:80 traefik/whoami:v1.10
docker ps --filter name=essai-whoami --format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}"المخرجات الحقيقية:
a088124875835ed375ddb25a4e4246faf62416a9772751ec7eedecc48bb758f8
NAMES IMAGE STATUS PORTS
essai-whoami traefik/whoami:v1.10 Up Less than a second 0.0.0.0:8089->80/tcp, [::]:8089->80/tcpما يجب ملاحظته: سطر واحد، Up، والمنفذ 8089 من جهازك موجَّه نحو المنفذ 80 داخل الحاوية.
اسأله. المعرّف Hostname المُعاد هو معرّف الحاوية: احتفظ بهذا التفصيل، فسيصبح اسم Pod في الدرس 04.
curl -s http://localhost:8089على Windows، اكتب curl.exe (فـcurl بدون امتداد هو اسم مستعار في PowerShell لـInvoke-WebRequest). المخرجات الحقيقية (أول سطرين):
Hostname: a08812487583
IP: 127.0.0.1انتبه جيداً لما سيحدث: قتل الحاوية. إنه عطل الساعة الثالثة صباحاً، بشكل مُسرَّع.
docker rm -f essai-whoami
docker ps --filter name=essai-whoami --format "table {{.Names}}\t{{.Status}}"المخرجات الحقيقية:
essai-whoami
NAMES STATUSما يجب ملاحظته: الجدول فارغ. لن يُعيد أحد تشغيل هذه الحاوية؛ يفشل الآن curl http://localhost:8089 (curl: (52) Empty reply from server، أو (56) Recv failure، أو Connection refused حسب اللحظة التي يُحرّر فيها Docker Desktop المنفذ). فعل Docker ما طُلب منه، لا أكثر ولا أقل. احتفظ بهذه الصورة الذهنية: في الدرس 04، ستفعل نفس الشيء بـ Pod وستعود قبل أن تنتهي من إعادة قراءة السطر.
تحقّق من وجود Kubernetes. إذا لم تُفعّل بعد Kubernetes في Docker Desktop، انتقل إلى الدرس 02 ثم عُد؛ وإلا، انظر إلى كتلتك.
kubectl get nodesالمخرجات الحقيقية:
NAME STATUS ROLES AGE VERSION
docker-desktop Ready control-plane 18d v1.34.1ما يجب ملاحظته: عقدة واحدة، Ready، بدور control-plane. على Docker Desktop، الآلة التي تقرّر وتلك التي تنفّذ هما نفس الآلة. AGE هو عمر كتلتك، وVERSION إصدار Kubernetes (1.34 هنا).
المس control plane بإصبعك. المكوّنات الأربعة «التي تقرّر» تعمل هي نفسها كـ Pods، في namespace محجوز باسم kube-system، بعلامة (label) tier=control-plane.
kubectl get pods -n kube-system -l tier=control-planeالمخرجات الحقيقية:
NAME READY STATUS RESTARTS AGE
etcd-docker-desktop 1/1 Running 0 18d
kube-apiserver-docker-desktop 1/1 Running 0 18d
kube-controller-manager-docker-desktop 1/1 Running 0 18d
kube-scheduler-docker-desktop 1/1 Running 0 18dما يجب ملاحظته: etcd، وواجهة الـ API، ومدير المتحكّمات، والمُجدوِل، كل واحد 1/1 Running، بدون أي إعادة تشغيل (0). ينتهي اسم كل منها باسم العقدة التي تستضيفه. قارن مع جدول «كيف يعمل الأمر»: أصبح لديك الآن وجه لكل دور.
اطّلع على بقية kube-system. بدون المرشّح (filter)، تكتشف مكوّنات العقدة والإضافات الخاصة بـ Docker Desktop.
kubectl get pods -n kube-systemالمخرجات الحقيقية على آلة الدورة (سطر metrics-server يأتي من مكوّن سنُثبّته في الوحدة 6: من الطبيعي ألا يكون لديك بعد):
NAME READY STATUS RESTARTS AGE
coredns-66bc5c9577-6gwhl 1/1 Running 0 18d
coredns-66bc5c9577-9p89d 1/1 Running 0 18d
etcd-docker-desktop 1/1 Running 0 18d
kube-apiserver-docker-desktop 1/1 Running 0 18d
kube-controller-manager-docker-desktop 1/1 Running 0 18d
kube-proxy-2ddzv 1/1 Running 0 18d
kube-scheduler-docker-desktop 1/1 Running 0 18d
metrics-server-7c5fdf4664-6882s 1/1 Running 0 26m
storage-provisioner 1/1 Running 0 18d
vpnkit-controller 1/1 Running 0 18dما يجب ملاحظته: kube-proxy (قواعد شبكة العقدة)، وcoredns مرّتين (DNS الداخلي الذي سيسمح لـfrontend باستدعاء api باسمه)، وقطعتان خاصتان بـ Docker Desktop: storage-provisioner (يُنشئ أحجام (volumes) من نوع hostpath عند الطلب، الوحدة 5) وvpnkit-controller (وهو من يمنح localhost لـ Services من نوع LoadBalancer، الدرس 04). أما الـ kubelet، فلا يظهر: فهو ليس Pod بل خدمة نظام على مستوى العقدة.
اختياري: شاهد أن Kubernetes ما هو «إلا» حاويات. إذا كان خيار «Show system containers» مفعّلاً في Docker Desktop (الدرس 02)، يُظهر لك Docker الحاويات التي تحمل control plane. إنه نفس Docker الذي في الخطوة 1.
docker ps --filter "name=k8s_kube-apiserver_" --filter "name=k8s_etcd_" --filter "name=k8s_kube-scheduler_" --filter "name=k8s_kube-controller-manager_" --format "table {{.Names}}\t{{.Status}}"المخرجات الحقيقية:
NAMES STATUS
k8s_etcd_etcd-docker-desktop_kube-system_0b753cb7812d40a401f3a8f63b18f779_0 Up 44 minutes
k8s_kube-apiserver_kube-apiserver-docker-desktop_kube-system_647244f1c75810d936baf4253b7903ef_0 Up 44 minutes
k8s_kube-scheduler_kube-scheduler-docker-desktop_kube-system_b44739859c757a4712b786569a89a1f3_0 Up 44 minutes
k8s_kube-controller-manager_kube-controller-manager-docker-desktop_kube-system_10b0d524eef4b9a12d5827ba17a36f4f_0 Up 44 minutesما يجب ملاحظته: اسم Docker مبنيّ على الصيغة k8s_<حاوية>_<pod>_<namespace>_<uid>_<n>. إذا لم يكن الخيار مفعّلاً، يكون الجدول فارغاً: لا شيء خطير، إنه مجرد إعداد عرض. لا يتناقض AGE البالغ 18 يوماً في Kubernetes مع Up 44 minutes في Docker: فالكتلة موجودة منذ 18 يوماً، وأُعيد تشغيل Docker Desktop قبل 44 دقيقة وأعاد إنشاء حاوياته دون فقدان الحالة، المخزّنة في etcd.
أين نتحدّث؟ أمر أخير لتحديد بوابة الدخول التي ستمرّ منها كل الأوامر الأخرى.
kubectl cluster-infoالمخرجات الحقيقية:
Kubernetes control plane is running at https://kubernetes.docker.internal:6443
CoreDNS is running at https://kubernetes.docker.internal:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.ما يجب ملاحظته: تستمع واجهة الـ API عبر HTTPS على المنفذ 6443 من kubernetes.docker.internal، وهو اسم يجعله Docker Desktop يُشير إلى جهازك. عندما يكون Docker Desktop متوقفاً، فإن هذا المنفذ هو من «يرفض الاتصال» (الدرس 02، «إذا واجهت مشكلة»). لا شيء لتنظيفه: حُذفت الحاوية essai-whoami في الخطوة 3 ولم تُنشئ شيئاً في Kubernetes.
docker: Error response from daemon: failed to set up container networking: … Bind for 0.0.0.0:8089 failed: port is already allocated ← حاوية أخرى تنشر المنفذ 8089 بالفعل (رسالة تظهر عند تشغيل الخطوة 1 مرّتين؛ إذا كان برنامج خارج Docker يشغل المنفذ، تقول الرسالة ports are not available). غيّر المنفذ على جهازك (-p 8090:80) أو حدّد من يشغله: Get-NetTCPConnection -LocalPort 8089 (PowerShell) / lsof -i :8089 (macOS، Linux).kubectl : Le terme 'kubectl' n'est pas reconnu (أو kubectl: command not found) ← Kubernetes غير مفعّل، أو أن الطرفية فُتحت قبل التفعيل ولم تُعِد تحميل PATH. الدرس 02 لتفعيله؛ ثم أغلق الطرفية وأعد فتحها.Unable to connect to the server: dial tcp 127.0.0.1:6443: connectex: No connection could be made because the target machine actively refused it. ← لا أحد يستمع على المنفذ 6443: Docker Desktop متوقف أو Kubernetes معطّل. شغّل Docker Desktop، انتظر ظهور «Kubernetes is up and running» في واجهة Kubernetes، ثم أعد المحاولة.kubectl get pods -n kube-system -l tier=control-plane يُعيد No resources found ← من المحتمل أنك على كتلة أخرى (سياق minikube، kind-…) حيث لا يحمل control plane نفس العلامة أو غير مرئي. يجب أن يجيب kubectl config current-context بـdocker-desktop؛ يشرح الدرس 03 كيفية التصحيح.docker ps --filter "name=k8s_…" لا يُظهر شيئاً رغم أن Kubernetes يعمل ← خيار «Show system containers (advanced)» غير مفعّل في إعدادات Kubernetes الخاصة بـ Docker Desktop. ليس عطلاً؛ الخطوة 7 اختيارية.kube-apiserver (بوابة الدخول، المنفذ 6443)، وetcd (ذاكرة الكتلة)، وkube-scheduler (يختار العقدة)، وkube-controller-manager (حلقات التوفيق).kubelet (الوكيل)، وkube-proxy (شبكة الـ Services)، وruntime (docker://29.3.1 على Docker Desktop)؛ يُظهر kubectl get pods -n kube-system -l tier=control-plane الأربعة الأولى وهي تعمل.docker-desktop بإصدار v1.34.1، صور محلية مشتركة مع Kubernetes، LoadBalancer على localhost؛ الحدود: عقدة واحدة فقط، ولا NetworkPolicy مطبَّقة، ولا سحابة حقيقية.docker-desktop.تُخفي العقدة الوحيدة في Docker Desktop فرقاً مهماً عن كتلة حقيقية: في الإنتاج، يعيش control plane على آلات مخصّصة (غالباً ثلاث، كي تحافظ etcd على النصاب القانوني (quorum))، ولا يستضيف أي Pod تطبيقي، بفضل «taint» باسم node-role.kubernetes.io/control-plane:NoSchedule. هنا، يُظهر kubectl describe node docker-desktop القيمة Taints: <none>: كل شيء يعمل في المكان نفسه. تُفصّل صفحة Kubernetes Components من التوثيق الرسمي كل مكوّن رأيناه في هذا الدرس، مع الاحتمالات الممكنة (cloud-controller-manager، runtimes أخرى).