0%
Pods وServices وDeployments

Pods وServices

الموارد الثلاثة التي ستكتبها كل يوم.

15-20 دقيقة

Pods وServices وDeployments

لكل Pod عنوان IP. يتغيّر هذا العنوان عند كل إعادة إنشاء للـ Pod، وإعادة إنشاء الـ Pod أمر متكرر: عند كل عملية نشر، وعند كل انهيار، وكلما قرّر المجدوِل (scheduler) نقله. ولذلك لا يمكن لأي تطبيق أن يستدعي تطبيقًا آخر عبر عنوانه. هذه هي المشكلة التي يعالجها هذا الدرس، وهي أعمق مما تبدو عليه.

تشكّل الموارد الثلاثة التالية سلسلة لكل حلقة فيها سبب وجود محدد. يتولى الـ Pod التنفيذ. ويضمن الـ Deployment وجود العدد الصحيح من الـ Pods بالإصدار الصحيح، ويعرف كيف ينتقل من إصدار إلى آخر دون انقطاع. أما الـ Service فيوفّر اسمًا ثابتًا وعنوانًا ثابتًا أمام مجموعة من الـ Pods تتغيّر هويتها باستمرار.

ما يربط هذه الكائنات الثلاثة ليس مرجعًا مباشرًا، وهذه هي النقطة التي تربك في البداية: فهي لا تعرف بعضها باسمها، بل عبر الـ labels. لا يعرف الـ Service أي Pods يعرضها؛ إنه يعلن معيار اختيار، ويربط به النظام باستمرار كل الـ Pods المطابقة له. هذا الاقتران غير المباشر هو ما يسمح لأي Pod بالاختفاء وللـ Pod البديل عنه بأن يُخدَّم فورًا، دون أي إعادة ضبط.

إن فهم آلية الـ labels يعني فهم 80% من Kubernetes. فالمحدد المكتوب بشكل خاطئ ينتج أكثر الأعراض غموضًا لدى المبتدئ: Service يستجيب، لكن بخطأ في الاتصال، لأنه لا يشير إلى شيء.

استخدام Deployment بدلًا من Pod

لا يُنشأ Pod يدويًا إلا في اختبار عابر. فالـ Pod المنفرد لا يملك أي مرونة في التعافي: إن مات بقي ميتًا. نعلن بدلًا من ذلك عن Deployment يتحمل هذه المسؤولية.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  labels:
    app: web
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web            # doit correspondre aux labels du template ci-dessous
  template:               # le moule à partir duquel les Pods sont fabriqués
    metadata:
      labels:
        app: web          # les labels que porteront les Pods créés
    spec:
      containers:
        - name: nginx
          image: nginx:1.27-alpine
          ports:
            - containerPort: 80

في هذا الملف فخّان، وكلاهما مرتبط بالموضع نفسه.

الأول: يجب أن يتطابق spec.selector.matchLabels مع spec.template.metadata.labels. يقول المحدد (selector) «الـ Pods التي أتولى مسؤوليتها تحمل هذا الـ label»، ويقول القالب (template) «الـ Pods التي أصنعها تحمل هذا الـ label». وإذا اختلفا، يرفض API server الـ manifest، وهذا خبر جيد: فالخطأ يُكتشف لحظة الكتابة.

الثاني: spec.selector غير قابل للتعديل بعد الإنشاء. ولتعديله يجب حذف الـ Deployment وإعادة إنشائه. لذا اختر الـ labels بشيء من العناية.

kubectl apply -f deployment.yaml
kubectl get pods -l app=web -w   # observer la montée en charge en direct
kubectl get deployment web       # répliques prêtes sur répliques voulues

الوسيط ReplicaSet

لا يدير الـ Deployment الـ Pods مباشرة. إنه ينشئ ReplicaSet، وهذا الـ ReplicaSet هو الذي يحافظ على عدد الـ Pods. تبدو هذه الطبقة بلا فائدة إلى أن نفهم الغرض منها:

kubectl get replicasets
# NAME             DESIRED   CURRENT   READY   AGE
# web-7d4b8c9f5    3         3         3       2m

كل إصدار من القالب ينتج ReplicaSet مستقلًا. وعند التحديث، يُنقص الـ Deployment عدد الـ Pods في الـ ReplicaSet القديم ويزيدها في الجديد. ويُحتفظ بالقديم عند صفر نسخة — وهذا بالضبط ما يجعل التراجع ممكنًا وفوريًا: فالتعريف السابق ما زال موجودًا، ويكفي إعادة رفع عدد نسخه.

تحقّق من هذا التسلسل الهرمي: احذف Pod يدويًا، وسيعيد الـ ReplicaSet إنشاءه في ثوانٍ، لأنه لاحظ أن العدد لم يعد مطابقًا.

kubectl delete pod web-7d4b8c9f5-xk2mp
kubectl get pods -l app=web     # un nouveau Pod, avec un nouveau suffixe

الـ Labels والمحددات

الـ label زوج مفتاح-قيمة حر، يُوضع على أي كائن. وهو ليس توثيقًا: إنه آلية الربط في النظام.

metadata:
  labels:
    app: web
    tier: frontend
    version: v1
    environnement: production

يعرف kubectl كيف يستعلم بناءً على هذه الـ labels، وهذا يغني عن قراءة القوائم يدويًا:

kubectl get pods -l app=web                    # égalité
kubectl get pods -l app=web,tier=frontend      # conjonction
kubectl get pods -l 'app!=web'                 # négation
kubectl get pods -l 'environnement in (staging,production)'   # ensemble
kubectl get pods -l version                    # présence de la clé
kubectl get pods --show-labels                 # afficher les labels

أكثر النصائح تجنبًا للفوضى: خصّص labels المحدد لهوية التطبيق (app وtier)، وضع كل ما يتغيّر كثيرًا — رقم إصدار أو معرّف commit — في labels خارج المحدد أو في annotations. فالمحدد الذي يحتوي على version: v1 يقطع استمرارية الـ Service عند كل عملية تسليم.

الـ Service: عنوان لا يتغيّر

ينشئ الـ Service عنوان IP افتراضيًا واسم DNS ثابتين، ويوزّع الحركة على كل الـ Pods المطابقة لمحدده.

ClusterIP، الحالة الافتراضية

apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  type: ClusterIP         # valeur par défaut : joignable depuis l'intérieur du cluster
  selector:
    app: web              # tous les Pods portant app=web deviennent des cibles
  ports:
    - name: http
      port: 80            # port du Service
      targetPort: 80      # port écouté dans le conteneur
      protocol: TCP

التمييز بين port وtargetPort ثابت ويستحق الترسيخ: port هو ما يستدعيه العملاء، وtargetPort هو ما تستمع إليه الحاوية. لا شيء يُلزم بأن يكونا متساويين، وهما نادرًا ما يتساويان عمليًا.

يمكن الوصول إلى الـ Service باسمه من أي Pod في العنقود (cluster)، بفضل DNS الداخلي:

web                        # même namespace
web.atelier                # namespace explicite
web.atelier.svc.cluster.local   # nom complet

تحقّق من ذلك من Pod مؤقت، فهذا أسرع اختبار يزيل الشك:

kubectl run debug --image=busybox:1.36 --rm -it --restart=Never -- \
  wget -qO- http://web:80

التشخيص الأساسي: الـ endpoints

الـ Service الذي لا يجد أي Pod لا يُرجع خطأ صريحًا. إنه موجود، وله عنوان، ويرفض الاتصالات ببساطة. والسبب في الغالب محدد لا يطابق أي شيء.

kubectl get endpoints web   # les Pods réellement derrière le Service

# Liste vide ? Le sélecteur ne correspond à aucun Pod. Comparez les deux :
kubectl get service web -o jsonpath='{.spec.selector}'
kubectl get pods --show-labels

احتفظ بهذا المنعكس: عند رفض الاتصال على Service، انظر إلى الـ endpoints قبل أي شيء آخر. فهذا يحوّل نصف ساعة من التخبّط إلى ثلاثين ثانية.

NodePort

يفتح NodePort المنفذ نفسه على جميع عقد العنقود، ضمن النطاق 30000-32767.

apiVersion: v1
kind: Service
metadata:
  name: web-nodeport
spec:
  type: NodePort
  selector:
    app: web
  ports:
    - port: 80
      targetPort: 80
      nodePort: 30080     # facultatif ; sinon Kubernetes en attribue un
# minikube donne l'URL directement joignable
minikube service web-nodeport --url

هذا عملي في التطوير ويُفضَّل تجنبه في الإنتاج: فالمنفذ غير قياسي، ولا يوجد TLS ولا اسم نطاق، ويشمل العرض جميع العقد في وقت واحد. أما لحركة HTTP حقيقية، فالجواب الصحيح هو Ingress، الذي نراه في الدرس 6.

LoadBalancer، واعتماده على المزوّد

apiVersion: v1
kind: Service
metadata:
  name: web-lb
spec:
  type: LoadBalancer
  selector:
    app: web
  ports:
    - port: 80
      targetPort: 80

لنكن دقيقين، لأن هذه النقطة تضلّل كثيرين: Kubernetes لا يعرف كيف ينشئ موازن حمل (load balancer). إنه يصدر طلبًا وينتظر أن يلبّيه متحكم خارجي. على AWS أو Google Cloud أو Azure يوجد هذا المتحكم ويهيّئ مورًدا مدفوعًا. أما على عنقود محلي أو تثبيت دون تكامل مع السحابة، فلا أحد يستجيب، ويبقى الـ Service إلى ما لا نهاية بعنوان خارجي <pending>.

kubectl get service web-lb
# EXTERNAL-IP reste <pending> sans contrôleur : ce n'est pas un bug

# Avec minikube, une commande simule le comportement (à laisser tourner)
minikube tunnel
النوعالنطاقالاستخدام الواقعي
ClusterIPداخلي للعنقودالتواصل بين الخدمات: الحالة الشائعة
NodePortمنفذ مفتوح على كل عقدةالتطوير، والوصول لأغراض استكشاف الأعطال
LoadBalancerعنوان خارجي توفره السحابةنقطة دخول عامة، ويعتمد على المزوّد

التحديثات التدريجية

هذه هي الخدمة التي يؤديها Kubernetes على أفضل وجه، وهي شبه مجانية. غيّر صورة الـ Deployment: تُستبدل الـ Pods على دفعات، مع إبقاء الخدمة متاحة طوال فترة الانتقال.

# Fragment à insérer dans le spec du Deployment, à côté de replicas.
spec:
  replicas: 4
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1     # au plus 1 Pod indisponible pendant la bascule
      maxSurge: 1           # au plus 1 Pod en plus des 4 voulues

بهذه القيم يوجد دائمًا بين 3 و5 Pods أثناء الانتقال، منها 3 على الأقل متاحة. ويضمن maxUnavailable: 0 عدم فقدان أي قدرة، على حساب نشر أبطأ لأنه يجب الإنشاء أولًا قبل الحذف.

kubectl apply -f deployment.yaml    # après modification de l'image du manifeste

# Variante impérative, utile pour un correctif urgent
kubectl set image deployment/web nginx=nginx:1.27.1-alpine

kubectl rollout status deployment/web   # se termine quand la bascule est finie

التراجع

بما أن الـ ReplicaSets القديمة محفوظة، فإن العودة إلى الوراء لا تتطلب أي إعادة بناء:

kubectl rollout history deployment/web              # historique des révisions
kubectl rollout undo deployment/web                 # revenir à la précédente
kubectl rollout undo deployment/web --to-revision=2 # à une révision précise

# Suspendre puis reprendre une bascule en cours, si les erreurs augmentent
kubectl rollout pause deployment/web
kubectl rollout resume deployment/web

ملاحظة صريحة حول السجل: افتراضيًا لا يُظهر ما الذي تغيّر بين مراجعتين، بل رقمهما فقط. ولجعل السجل مقروءًا، أضف annotation عند كل تسليم:

metadata:
  name: web
  annotations:
    kubernetes.io/change-cause: "Passage à nginx 1.27.1 — correctif CVE"

تغيير حجم الانتشار

kubectl scale deployment/web --replicas=6   # impératif
kubectl apply -f deployment.yaml            # déclaratif, après édition du fichier

فضّل الأسلوب التصريحي. فأمر kubectl scale يُلغى عند أول apply تالٍ للـ manifest، مما ينتج تراجعًا صامتًا يصعب تفسيره.

المجسّات: liveness وreadiness

بدون مجسّات، يعدّ Kubernetes الحاوية سليمة بمجرد أن تعمل عمليتها الرئيسية. وهذا تقريب فجّ: فقد يكون التطبيق قد شغّل خادم HTTP دون أن يكون قادرًا على الرد — الاتصال بقاعدة البيانات لم يُنشأ، أو الذاكرة المؤقتة لم تُحمَّل. عندها سيرسل الـ Service الحركة إلى Pod سيرد بأخطاء.

يجيب مجسّان عن سؤالين مختلفين، والخلط بينهما له عواقب وخيمة.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
        - name: api
          image: mon-api:1.0.0
          ports:
            - containerPort: 8080

          # « Ce Pod peut-il recevoir du trafic maintenant ? »
          # En échec : retiré des endpoints du Service, mais NON redémarré.
          readinessProbe:
            httpGet:
              path: /ready
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 5
            failureThreshold: 3

          # « Ce processus est-il définitivement bloqué ? »
          # En échec : le conteneur est TUÉ et redémarré.
          livenessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 15
            periodSeconds: 20
            failureThreshold: 3

          # « Le démarrage est-il terminé ? »
          # Tant qu'elle échoue, la livenessProbe est suspendue.
          startupProbe:
            httpGet:
              path: /healthz
              port: 8080
            periodSeconds: 5
            failureThreshold: 30

الأخطاء الكلاسيكية، التي ينبغي معرفتها قبل ارتكابها:

  • توجيه الـ liveness إلى endpoint يفحص قاعدة البيانات. إذا تباطأت القاعدة، تفشل جميع الـ Pods في المجسّ، وتُقتل في الوقت نفسه، فيتحول بطء عابر إلى عطل كامل. يجب ألا تختبر الـ liveness سوى العملية نفسها؛ أما التبعيات الخارجية فمن اختصاص الـ readiness.
  • قيمة initialDelaySeconds قصيرة جدًا في الـ liveness. يُقتل التطبيق قبل أن ينتهي من الإقلاع، في حلقة متكررة: وهذا هو CrashLoopBackOff الغامض. والـ startupProbe هي الحل السليم لحالات الإقلاع البطيء.
  • عدم تعريف أي readiness. تصل الحركة إلى Pod غير جاهز، وتتسبب كل عملية نشر في موجة من أخطاء 502.

توجد ثلاثة أشكال للمجسّات بحسب ما يعرضه التطبيق:

# HTTP : tout code de 200 à 399 est un succès
readinessProbe:
  httpGet: { path: /ready, port: 8080 }
---
# TCP : la connexion s'ouvre-t-elle ?
readinessProbe:
  tcpSocket: { port: 5432 }
---
# Commande : code de sortie 0 = succès
livenessProbe:
  exec: { command: ["sh", "-c", "test -f /tmp/sain"] }

تشخيص Pod

الأوامر التي ينبغي حفظها عن ظهر قلب، بالترتيب الذي تُستخدم به:

# 1. Vue d'ensemble : STATUS et RESTARTS racontent l'essentiel
kubectl get pods -o wide

# 2. La section Events en fin de sortie donne la cause réelle
kubectl describe pod web-7d4b8c9f5-xk2mp

# 3. Les logs applicatifs
kubectl logs web-7d4b8c9f5-xk2mp
kubectl logs -f deployment/web              # suivi en direct
kubectl logs web-7d4b8c9f5-xk2mp --previous # logs du conteneur AVANT son crash

# 4. Entrer dans le conteneur
kubectl exec -it web-7d4b8c9f5-xk2mp -- sh

# 5. Atteindre le Pod depuis le poste, sans l'exposer
kubectl port-forward pod/web-7d4b8c9f5-xk2mp 8080:80

خيار --previous هو الخيار الذي يكتشفه المرء متأخرًا جدًا. ففي Pod بحالة CrashLoopBackOff، يعرض kubectl logs الحاوية الحالية، وغالبًا ما تكون سجلاتها فارغة لأنها أُعيد تشغيلها للتو. أما --previous فيعرض النسخة التي تعطّلت فعلًا، وبالتالي رسالة الخطأ.

أكثر الحالات شيوعًا ومعناها:

الحالةالسبب المعتاد
Pendingلا يجد المجدوِل (scheduler) عقدة مناسبة: موارد غير كافية أو وحدة تخزين غير مرتبطة
ImagePullBackOffالصورة غير موجودة، أو الوسم (tag) خاطئ، أو سجل خاص دون بيانات اعتماد
CrashLoopBackOffتبدأ الحاوية ثم تتوقف في حلقة متكررة: راجع logs --previous
Running مع 0/1 READYفشل فحص readinessProbe
OOMKilledتجاوز حد الذاكرة (راجع الدرس 6)

ما يجب تذكّره

  • نصرّح عن Deployment ولا ننشئ Pod يدويًا أبدًا. فهو ينشئ ReplicaSet لكل نسخة من القالب، والاحتفاظ بـ ReplicaSets القديمة هو ما يجعل kubectl rollout undo فوريًا.
  • ترتبط الكائنات ببعضها عبر التسميات (labels) لا عبر الأسماء. حافظوا على ثبات تسميات المحدِّد (app، tier)، وأخرجوا من المحدِّد كل ما يتغير كثيرًا — كالإصدارات والـ commits — فهو غير قابل للتعديل على أي حال.
  • يُشخَّص تعذّر الوصول إلى Service عبر kubectl get endpoints: القائمة الفارغة تعني أن المحدِّد لا يطابق أي Pod.
  • يُستخدم ClusterIP للاتصال الداخلي، وNodePort للتطوير، وLoadBalancer فقط حيث يوجد متحكّم سحابي — وإلا يبقى العنوان الخارجي <pending> إلى أجل غير مسمى.
  • يسحب readiness الـ Pod من حركة المرور دون إعادة تشغيله، بينما تقتل liveness الحاوية وتعيد تشغيلها. لا تضعوا أبدًا اعتمادًا خارجيًا في liveness: فبطء قاعدة البيانات سيتحول إلى عطل عام.

نصيحة: شغّلوا kubectl get pods -w في طرفية، ثم أطلقوا تحديثًا من طرفية أخرى. إن رؤية الـ Pods تظهر وتختفي على دفعات ترسّخ الآلية أفضل من أي شرح.

التعليقات

التعليقات مدعومة عبر GitHub Discussions

سجّل الدخول باستخدام GitHub للمشاركة في النقاش

تم نسخ الرابط!