أول نشر في خمس دقائق

11 دقيقة
الجمهور المستهدف
مبتدئ، الكتلة على Docker Desktop نشِطة، kubectl v1.34.1 مُتحقَّق منه (الدرس 03)
المدة
30 إلى 40 دقيقة
الوحدة
1/8
الكفاءة المستهدفة
نشر تطبيق بدون YAML باستخدام kubectl create deployment، عرضه على http://localhost:8080، تحويله إلى ثلاث نسخ متماثلة، ملاحظة توزيع الحمل والإصلاح الذاتي، ثم حذف كل شيء بنظافة

في صورة واحدة

تفتح كشكاً ببائع واحد ولافتة «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-pasNamespacekind: Namespace
kubectl create deployment api --image=… --port=80Deployment (+ ReplicaSet + Pod)kind: Deployment، spec.template.spec.containers[0].image
kubectl expose deployment api --type=LoadBalancer --port=8080 --target-port=80Service يستهدف app=apikind: Service، spec.type: LoadBalancer، ports[0].port: 8080، targetPort: 80
kubectl scale deployment api --replicas=3spec.replicas الخاص بالـ Deploymentتغيير replicas: 3 ثم kubectl apply
kubectl delete pod <nom>يحذف Pod؛ يستبدلها الـ ReplicaSetلا شيء لكتابته: إنه التوفيق (réconciliation)
العمودفي get deployفي get rsفي get pods
READYPods جاهزة / مرغوبة (3/3)Pods جاهزةحاويات جاهزة / المجموع (1/1)
UP-TO-DATEPods على آخر نموذج
DESIRED / CURRENTمرغوبة / موجودة
STATUSRunning، ContainerCreating، ImagePullBackOff

خطوة بخطوة

تحمل كل الأوامر -n premiers-pas. إذا كنت تستخدم اختصار k من الدرس 03، استبدل kubectl بـk.

  1. أنشئ namespace الوحدة. دُرج فارغ سيُخزَّن فيه كل ما يلي، وستحذفه الخطوة الأخيرة دفعة واحدة.

    bash
    kubectl create namespace premiers-pas

    المخرجات الحقيقية:

    text
    namespace/premiers-pas created

    ما يجب ملاحظته: created. إذا قرأت AlreadyExists، فقد احتفظت به من الدرس 03: لا مشكلة، تابع.

  2. أنشئ الـ Deployment باسم api. صورة عمومية، بوسم (tag) ثابت (v1.10، وأبداً latest: بدون وسم دقيق، يمكن لآلتين تحميل إصدارين مختلفين وتعود مشكلة «تعمل عندي»)، والمنفذ الذي تستمع إليه الحاوية.

    bash
    kubectl create deployment api --image=traefik/whoami:v1.10 --port=80 -n premiers-pas
    kubectl get deploy,rs,pods -n premiers-pas

    المخرجات الحقيقية (بعد ثماني ثوانٍ من الإنشاء):

    text
    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 ميغابايت)؛ أعد كتابة الأمر.

  3. انظر أين تعيش الـ Pod. يُضيف -o wide عنوان IP الخاص بالـ Pod والعقدة التي تستضيفها.

    bash
    kubectl get pods -n premiers-pas -o wide

    المخرجات الحقيقية:

    text
    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 في متصفّحك: لا شيء يستجيب، فهذا العنوان لا وجود له إلا داخل الآلة الافتراضية. هذا كامل موضوع الخطوتين التاليتين.

  4. افتح نفقاً باستخدام port-forward. تُحجَب الطرفية طالما بقي النفق مفتوحاً؛ هذا مقصود.

    bash
    kubectl port-forward deployment/api 8080:80 -n premiers-pas

    المخرجات الحقيقية:

    text
    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:

    text
    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).

  5. اعرضها باستخدام Service من نوع LoadBalancer. --port هو منفذ الـ Service (ما تكتبه في المتصفّح)، و--target-port هو منفذ الحاوية.

    bash
    kubectl expose deployment api --type=LoadBalancer --port=8080 --target-port=80 -n premiers-pas
    kubectl get svc -n premiers-pas

    المخرجات الحقيقية:

    text
    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.

  6. انتقل إلى ثلاث نسخ متماثلة وشاهد الحمل يتوزّع. يُغيّر الـ Deployment تعليمته؛ يُنشئ الـ ReplicaSet Pod-تين إضافيتين؛ ويضيفهما الـ Service إلى قائمته بمجرد جاهزيتهما.

    bash
    kubectl scale deployment api --replicas=3 -n premiers-pas
    kubectl get pods -n premiers-pas

    المخرجات الحقيقية:

    text
    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). ست مخرجات حقيقية متتالية:

    text
    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 في كل خطوة.

  7. احذف Pod وشاهد الاستبدال. تقتل بائعاً؛ يستدعي المدير آخر قبل أن تنتهي من القراءة.

    bash
    kubectl delete pod api-9bfb55fc6-jv2xw -n premiers-pas
    kubectl get pods -n premiers-pas

    المخرجات الحقيقية (الأمر الثاني يُنفَّذ فوراً بعد الأول):

    text
    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-تان الأخريان حركة المرور.

  8. اقرأ السجلات. يقبل kubectl logs Pod أو، وهو الأعملي، الـ Deployment (يختار واحدة منها).

    bash
    kubectl logs deployment/api -n premiers-pas

    المخرجات الحقيقية:

    text
    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. هنا ستقرأ، لاحقاً في الدورة، أخطاء تطبيق يتعطّل.

  9. أجرِ جرداً للدُرج. يُلخّص get all الأنواع الأربعة للكائنات التي أنشأتها.

    bash
    kubectl get all -n premiers-pas

    المخرجات الحقيقية:

    text
    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)؛ وأنشأت الكتلة الأربعة الأخرى.

  10. ألقِ نظرة على YAML الذي كُتب بدلاً عنك. كل كائن Kubernetes هو مستند YAML مُخزَّن في etcd؛ يُعيده لك -o yaml كما هو. إنها لمحة من الوحدة 2.

    bash
    kubectl get deployment api -n premiers-pas -o yaml

    المخرجات الحقيقية (حُذفت حقول metadata التتبعية وجزء من status):

    yaml
    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).

  11. احذف كل شيء دفعة واحدة وأثبت ذلك. يحذف حذف الـ namespace كل ما يحتويه؛ ويُحجَب الأمر حتى يتم ذلك (47 ثانية على آلة الدورة).

    bash
    kubectl delete namespace premiers-pas
    kubectl get all -n premiers-pas
    kubectl get namespace premiers-pas

    المخرجات الحقيقية:

    text
    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 من اليمين إلى اليسار.
  • لا يمكن الوصول إلى عنوان IP لـ 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 إلى آخر، بترتيب عشوائي.
  • حذف Pod تُديرها Deployment لا يفعل سوى استبدالها بـ Pod جديدة (لاحقة جديدة، 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/.