مقدمة
لماذا ننسّق الحاويات، وما الذي لا يستطيع Docker Compose فعله.
مقدمة إلى Kubernetes
أنت تعرف كيف تبني صورة (image) وتشغّل حاوية. على جهازك، يشغّل الأمر docker compose up واجهة API وقاعدة بيانات وذاكرة تخزين مؤقت خلال ثوانٍ، ويعمل كل شيء. ثم يأتي السؤال الذي يغيّر كل شيء: ماذا يحدث عندما يجب أن يعمل هذا التطبيق باستمرار، على عدة أجهزة، دون أن يضطر أحد إلى النهوض ليلًا لإعادة تشغيله؟
يصف Compose حالة مطلوبة، لكن لا شيء يحافظ عليها مع مرور الوقت. إذا تعطلت العملية، يعيد restart: always تشغيلها. وإذا سقط الجهاز، لا شيء يعيد تشغيل أي شيء. وإذا تضاعف الضغط ثلاث مرات، تبدأ النسخ المتماثلة يدويًا، وتتغير عناوينها، ويجب إعادة ضبط الوكيل العكسي (reverse proxy). وإذا نشرت إصدارًا جديدًا، يحدث انقطاع بين إيقاف القديم وبدء الجديد. لكل مشكلة من هذه المشاكل حل يدوي بديهي، لكن تراكمها هو ما يصبح غير محتمل.
جواب Kubernetes ليس «مزيدًا من الأوامر». إنه انعكاس في المسؤولية: تصف الحالة التي تريدها بصورة دائمة، وتعمل مجموعة من حلقات التحكم باستمرار لكي يطابق الواقع هذا الوصف. هذا هو المفهوم المؤسِّس الوحيد حقًا في النظام: كل ما عداه — Pods وDeployments وServices والأقراص (volumes) — ليس سوى تطبيق لهذه الفكرة على كائن بعينه.
يعرض هذا الدرس هذا النموذج، ثم يصف معمارية العنقود (cluster). ليس ذلك حبًّا في المصطلحات، بل لأن معرفة أي مكوّن يقرر ماذا هي ما سيمكّنك من التشخيص. فالـ Pod العالق لأن المجدوِل (scheduler) لا يجد أي عقدة متاحة، والـ Pod العالق لأن kubelet لا يستطيع تنزيل الصورة، عطلان لا يتشابهان في السبب ولا في المكان الذي ينبغي النظر فيه.
الأمري مقابل التصريحي
مع Docker، أنت تعطي أوامر. كل أمر يصف إجراءً يُنفَّذ الآن:
# أمري: أصف الإجراءات واحدًا تلو الآخر، بالترتيب.
docker build -t mon-api:1.2.0 .
docker run -d --name api-1 -p 8080:8080 mon-api:1.2.0
docker run -d --name api-2 -p 8081:8080 mon-api:1.2.0
docker stop api-1 # للتحديث: انقطاع مقبول
docker rm api-1
docker run -d --name api-1 -p 8080:8080 mon-api:1.3.0
ينجح هذا السكربت مرة واحدة. لكنه لا يقول شيئًا عمّا يجب أن يبقى صحيحًا. لا أحد يعرف، عند قراءته، هل ينبغي أن توجد نسختان متماثلتان أم ثلاث، ولا أي إصدار يُفترض أن يعمل.
مع Kubernetes، أنت تصف هدفًا. الملف أدناه لا يقول «شغّل ثلاث حاويات»، بل يقول «يجب أن توجد ثلاث نسخ متماثلة من هذا الإصدار، بصورة دائمة»:
apiVersion: apps/v1
kind: Deployment
metadata:
name: mon-api
spec:
replicas: 3 # الحالة المطلوبة، وليست إجراءً لمرة واحدة
selector:
matchLabels:
app: mon-api
template:
metadata:
labels:
app: mon-api
spec:
containers:
- name: api
image: mon-api:1.3.0
ports:
- containerPort: 8080
الفرق العملي فوري. أوقف إحدى الحاويات الثلاث: ستولد حاوية رابعة بعد ثوانٍ قليلة، لأن العدد لم يعد يطابق الهدف. أطفئ عقدة: تُعاد إنشاء النسخ التي كانت تحملها في مكان آخر. لم يأمر أحد بشيء، فقد لاحظ مُتحكِّم (controller) وجود فرق فحسب.
حلقة المطابقة
هذه هي الآلية المركزية، وهي بسيطة:
- يقرأ مُتحكِّم الحالة المطلوبة (ما صرّحت به).
- يراقب الحالة الفعلية للعنقود.
- إذا اختلفت الحالتان، يتصرف لتقليص الفرق.
- ثم يعيد الكرّة. إلى ما لا نهاية.
يوجد مُتحكِّم لكل نوع من المسؤوليات، وهم قصيرو النظر عن قصد: فالذي يدير ReplicaSets لا يعرف شيئًا عن الشبكة، والذي يدير Services لا يعرف شيئًا عن الأقراص. كل واحد يراقب فرقه الخاص. والتنسيق الشامل ينبثق من هذه الحلقات المستقلة، ولا يقوده قائد أوركسترا واحد.
لهذه المعمارية نتيجة ينبغي قبولها مبكرًا: Kubernetes غير متزامن. عندما يرد عليك kubectl apply، فإنه لم ينشر شيئًا، بل سجّل نيتك فقط. العمل الفعلي يتم بعد ذلك، وقد يفشل بعد ذلك. لهذا لا نتحقق أبدًا من عملية نشر بناءً على ما يعيده apply، بل دائمًا بناءً على الحالة الملاحَظة:
# apply يسجّل النية: ما يعيده لا يثبت شيئًا عن التنفيذ.
kubectl apply -f deployment.yaml
# التحقق يتم على الحالة الفعلية، بعد ذلك.
kubectl get deployment mon-api
kubectl get pods -l app=mon-api
# وإذا لم يبدأ Pod، فالسبب موجود في الأحداث في نهاية الوصف.
kubectl describe pod mon-api-7d4b8c9f5-xk2mp
معمارية العنقود
ينقسم العنقود إلى دورين: مستوى التحكم (control plane) الذي يقرر، وعُقد العمل (worker nodes) التي تنفّذ.
مستوى التحكم
| المكوّن | الدور | ما يعنيه عند تعطله |
|---|---|---|
kube-apiserver | بوابة الدخول الوحيدة. يتحقق من كل طلب ويحفظه. | لا يمكن إجراء أي تعديل؛ أما الأحمال التي بدأت فعلًا فتواصل العمل. |
etcd | قاعدة بيانات مفتاح-قيمة تخزّن الحالة المطلوبة والحالة الملاحَظة. | فقدان etcd دون نسخة احتياطية = فقدان العنقود المنطقي. |
kube-scheduler | يختار العقدة التي توضع عليها كل Pod جديدة. | تبقى الـ Pods الجديدة في حالة Pending. |
kube-controller-manager | ينفّذ حلقات المطابقة. | لا إصلاح ذاتي: الـ Pods الميتة لا تُستبدل. |
النقطة اللافتة: kubectl لا يتحدث إلا مع API server. فهو لا يفتح أي اتصال بأي عقدة، ولا يتحكم في أي حاوية. وبذلك يمر كل الأمان وكل التتبّع عبر نقطة واحدة.
عُقد العمل
على كل عقدة تعمل ثلاثة عناصر:
- kubelet: الوكيل الذي يتلقى قائمة الـ Pods المطلوب تشغيلها على عقدته هو، ويطلب من بيئة التشغيل (runtime) بدء تشغيلها، وينفّذ فحوص السلامة، ويرفع الحالة.
- بيئة تشغيل الحاويات (container runtime): غالبًا
containerdأوCRI-O. هي التي تنزّل الصور وتطلق العمليات. تعمل صور Docker لديك دون أي تعديل: فهي تلتزم بصيغة OCI التي تعرف هذه البيئات قراءتها. - kube-proxy: يبرمج قواعد الشبكة على العقدة بحيث يصل العنوان الثابت لـ Service إلى الـ Pods الصحيحة.
# عرض العقد وحالتها كما يراها مستوى التحكم
kubectl get nodes
# تفاصيل عقدة: السعة، والموارد المحجوزة مسبقًا، والشروط
kubectl describe node minikube
# مكوّنات مستوى التحكم هي نفسها Pods، داخل kube-system
kubectl get pods -n kube-system
هذا الأمر الأخير يستحق وقفة: فمستوى التحكم تديره Kubernetes نفسها. وليس ذلك ترفًا، بل هو ما يجعل مكوّناته قابلة للمراقبة والإصلاح بالأدوات نفسها التي تستخدمها مع تطبيقاتك.
الـ Pod، ولماذا ليس حاوية
Kubernetes لا تنشر حاويات. إنها تنشر Pods. الـ Pod مجموعة من حاوية واحدة أو أكثر تتشارك فضاء الشبكة نفسه ويمكنها مشاركة الأقراص. تتصل حاويات الـ Pod الواحدة ببعضها عبر localhost، وتوضع دائمًا على العقدة نفسها.
apiVersion: v1
kind: Pod
metadata:
name: demo-pod
labels:
app: demo
spec:
containers:
- name: web
image: nginx:1.27-alpine
ports:
- containerPort: 80
لماذا هذه الطبقة الوسيطة؟ لأن بعض الاحتياجات لا تنفصل عن العملية الرئيسية: مجمّع سجلات (logs) يقرأ المجلد نفسه، أو وكيل (proxy) ينهي TLS قبل وصول الطلب إلى التطبيق. ومعاملتها كحاويات مستقلة تفرض ضمان تواجدها معًا بوسائل ملتوية. الـ Pod يجعل هذا الضمان صريحًا.
النقطة الثانية، وهي الأهم عمليًا: الـ Pod قابل للتخلص منه. ليس له اسم ثابت، ويتغير عنوان IP الخاص به عند كل إعادة إنشاء، ولا شيء ينجو من اختفائه. لذلك لا ننشئ Pod مباشرة إلا نادرًا — فالملف أعلاه مثال تعليمي وليس نموذجًا للإنتاج. ننشئ Deployment، الذي ينشئ الـ Pods ويستبدلها نيابة عنك. يفصّل الدرس 3 هذا التسلسل.
ما تكسبه وما تدفعه
توفر Kubernetes، دون أن تضطر إلى كتابته:
- الإصلاح الذاتي: استبدال الـ Pods والعقد المعطوبة تلقائيًا؛
- اكتشاف الخدمات: اسم DNS ثابت أمام مجموعة من الـ Pods المتغيرة؛
- التحديثات التدريجية مع إمكانية التراجع، دون انقطاع؛
- التوسيع اليدوي أو التلقائي بحسب الحمل المقيس؛
- التوزيع تحت القيود: يضع المجدوِل الـ Pods بحسب المعالج والذاكرة المتاحين فعلًا.
يمكن التحقق من الإصلاح الذاتي بعملية واحدة، وهو أبلغ عرض للنموذج التصريحي:
# نحذف نسخة متماثلة يدويًا…
kubectl delete pod mon-api-7d4b8c9f5-xk2mp
# …وقد أُعيد العدد المطلوب بالفعل، مع Pod بلاحقة مختلفة.
kubectl get pods -l app=mon-api
الكلفة حقيقية، ويجب تسميتها. تضيف Kubernetes طبقة تجريد لها تعقيدها الخاص: مفاهيم أكثر ينبغي معرفتها، وإعدادات YAML أكثر، وعنقود ينبغي صيانته أو دفع ثمنه، ومجموعة أدوات تشخيص ينبغي تعلمها. بالنسبة إلى موقع تعريفي على جهاز واحد، يبقى docker compose الخيار الصحيح، وادعاء عكس ذلك سيكون غير أمين. تصبح Kubernetes مجدية عندما تكون لديك عدة خدمات، أو متطلب توافر عالٍ، أو عدة فرق تنشر على البنية التحتية نفسها.
المفردات المشتركة بين جميع الملفات التوصيفية
كل كائن في Kubernetes، أيًّا كان نوعه، يُكتب بالحقول الأربعة نفسها من المستوى الأول. ومعرفتها تجعل أي ملف توصيفي (manifest) مقروءًا:
apiVersion: apps/v1 # مجموعة وإصدار واجهة API التي تعرّف هذا النوع
kind: Deployment # نوع الكائن
metadata: # الهوية: الاسم، وnamespace، وlabels، وannotations
name: mon-api
labels:
app: mon-api
spec: # الحالة المطلوبة؛ ويتوقف محتواها على kind
replicas: 3
قاعدة واحدة تخص apiVersion ينبغي حفظها في هذه الدورة، وهي تجنّبك أكثر أخطاء المبتدئين شيوعًا:
v1لكل من Pod وService وConfigMap وSecret وPersistentVolume وPersistentVolumeClaim؛apps/v1لكل من Deployment وReplicaSet وStatefulSet وDaemonSet.
يرفض API server قيمة apiVersion: v1 على Deployment برسالة غير واضحة. أما القائمة الدقيقة لنوع معيّن فتُقرأ مباشرة من العنقود:
# أي apiVersion لهذا النوع؟ الجواب يأتي من العنقود، لا من الذاكرة.
kubectl api-resources | grep -i deployment
# وثائق حقل ما، دون مغادرة الطرفية
kubectl explain deployment.spec.strategy
kubectl explain هو أقل أدوات المنظومة استخدامًا قياسًا بقيمته. فهو يصف حقول إصدار واجهة API المثبَّت فعليًا على عنقودك أنت، وهو ما لا يستطيع أي مقال في مدونة ضمانه.
ما تغطيه بقية الدورة
- التثبيت والإعداد — عنقود محلي باستخدام minikube أو kind، و
kubectl، والسياقات (contexts) وفضاءات الأسماء (namespaces). - Pods وServices وDeployments — موارد الاستخدام اليومي، ومحددات labels، والتحديثات التدريجية، والفحوص (probes).
- ConfigMaps وSecrets — إخراج الإعدادات من الصور، وما الذي لا تعنيه كلمة «Secret».
- التخزين الدائم — الأقراص، والمطالبات، وStorageClass وStatefulSet.
- نشر التطبيقات المعقدة — تعدد الخدمات، وIngress، والموارد، والتوسيع التلقائي، ولمحة عن Helm.
كل هذه الكائنات معروفة أصلًا لعنقودك المستقبلي. يسردها هذا الأمر مع أسمائها المختصرة، تلك التي ستكتبها يوميًا:
kubectl api-resources | grep -E 'deployments|services|configmaps|secrets|persistentvolumeclaims'
# deployments deploy apps/v1 true Deployment
# services svc v1 true Service
# configmaps cm v1 true ConfigMap
الخلاصة
- Kubernetes تصريحي: تصف حالة مطلوبة، وتحافظ عليها حلقات المطابقة. يسجّل
applyنية ولا يضمن نتيجة — ويتم التحقق دائمًا على الحالة الملاحَظة. - مستوى التحكم يقرر (API server وetcd وscheduler وcontroller-manager)، والعقد تنفّذ (kubelet وruntime وkube-proxy). معرفة من يفعل ماذا هي أساس التشخيص.
- الوحدة المنشورة هي الـ Pod وليست الحاوية: مجموعة متجاورة تتشارك الشبكة والأقراص، وهي قبل كل شيء قابلة للتخلص منها. تُنشأ عبر مُتحكِّم، وليس يدويًا في الإنتاج.
- تتشارك جميع الملفات التوصيفية
apiVersionوkindوmetadataوspec. احفظv1لـ Pod/Service/ConfigMap/Secret/PVC وapps/v1لـ Deployment/StatefulSet. - للتنسيق كلفة تعقيد مقبولة. ويبرره عدد الخدمات ومتطلب التوافر وعدد الفرق، لا الموضة.
مرجع: الوثائق الرسمية متوفرة بالفرنسية لجزء كبير من المفاهيم. أبقِ kubernetes.io/fr/docs/concepts/ مفتوحًا طوال هذه الدورة.
التعليقات
التعليقات مدعومة عبر GitHub Discussions
سجّل الدخول باستخدام GitHub للمشاركة في النقاش