دليل تصميم Listener في Envoy Gateway: أنماط Multi-Port و Multi-Hostname و Multi-Ingress

4 min read

كثير من الناس الذين يتعلمون Gateway API يتعاملون مع Listener وكأنه مجرد "آه، هذا فقط حقل port: 80." لكن في النشر الحقيقي، لا يكون Listener مجرد دور ثانوي، بل أقرب إلى مركز التحكم في المرور لطبقة ingress كاملة.

الـ listener لا يفتح منفذًا فقط، بل يعرّف:

  • أي بروتوكول يقبل
  • أي hostname يقبل
  • هل سيجري TLS أم لا
  • ما أنواع الـ routes المسموح لها بالارتباط

وبمعنى آخر، الـ listener هو عقد ingress. اكتب عقودًا نظيفة، وستصبح الـ routes اللاحقة وملكية الفرق وتعقيد debugging أنظف تلقائيًا.

ابنِ النموذج الذهني الصحيح: ‏Listener واحد = فتحة Ingress واحدة

خذ هذا الـ Gateway:

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: eg
spec:
  gatewayClassName: eg
  listeners:
    - name: http
      protocol: HTTP
      port: 80
    - name: https
      protocol: HTTPS
      port: 443
      hostname: app.example.com
      tls:
        mode: Terminate
        certificateRefs:
          - name: app-cert
    - name: grpc
      protocol: HTTPS
      port: 8443
      hostname: grpc.example.com
      tls:
        mode: Terminate
        certificateRefs:
          - name: grpc-cert

هذا ليس مجرد "Gateway واحد فيه ثلاث فتحات." بل فكّر فيه هكذا:

  • listener http: من أجل HTTP غير المشفر
  • listener https: من أجل app.example.com
  • listener grpc: فتحة ingress مخصصة لخدمات gRPC

بمجرد أن تبدأ بالتفكير بهذه الطريقة، ستتضح كثير من قرارات التصميم فورًا. أنت لا تركّب YAML فحسب، بل تصمّم حدود ingress.

ثلاثة أنماط شائعة للتقسيم

النمط 1: التقسيم حسب البروتوكول

الأسلوب الأكثر بداهة:

  • HTTP على listener واحد
  • HTTPS على listener واحد
  • gRPC على listener واحد
  • و TCP/TLS passthrough منفصلان وحدهما

هذا يعطي وضوحًا في المسؤولية. حتى لو كان gRPC و web APIs العامة يعملان كلاهما فوق HTTP/2، فإن نماذج الخدمة لديهما مختلفة. والفصل بينهما يمنع تداخل الـ routes بعضها مع بعض.

النمط 2: التقسيم حسب Hostname

إذا كان لديك عدة domains، فهناك نمط شائع يقوم على listener واحد لكل hostname رئيسي:

listeners:
  - name: app
    hostname: app.example.com
    protocol: HTTPS
    port: 443
  - name: admin
    hostname: admin.example.com
    protocol: HTTPS
    port: 443

وهذا مناسب جدًا لـ:

  • فصل حوكمة العام عن الإداري
  • hostnames المختلفة التي تحتاج شهادات مختلفة
  • إبقاء حدود ارتباط الـ routes نظيفة

النمط 3: التقسيم حسب مسؤولية الفريق أو البيئة

في الإعدادات متعددة الفرق، يمكن للـ listeners أن تكون حدودًا للحوكمة:

  • public-api من أجل مرور المنتج الخارجي
  • internal-api من أجل الأنظمة الداخلية
  • partner-api من أجل تكاملات الشركاء

هنا تتجاوز قيمة الـ listener كونه إعدادًا تقنيًا فقط، ليصبح حدًا للصلاحيات والمسؤوليات. وهذا النمط يصنع فرقًا حقيقيًا في الفرق المتوسطة والكبيرة، لأنه يقلل كارثة "Ingress واحد يمكن للجميع أن يربطوا به أي شيء يريدونه."

sectionName و allowedRoutes: حقلان حاسمان

يرتبط الـ route بـ listener محدد عبر parentRefs.sectionName:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: app-route
spec:
  parentRefs:
    - name: eg
      sectionName: https
  hostnames:
    - "app.example.com"
  rules:
    - backendRefs:
        - name: app-service
          port: 8080

هذا الـ route سيبحث فقط عن listener اسمه https. ومن دون تحديد sectionName، يطبّق النظام قواعد للعثور على listeners القابلة للارتباط، لكن في البيئات متعددة الـ listeners يكون التصريح الواضح أكثر أمانًا تقريبًا دائمًا.

أما الحقل الحاسم الآخر فهو allowedRoutes:

listeners:
  - name: public
    protocol: HTTPS
    port: 443
    allowedRoutes:
      namespaces:
        from: Same

وهذا يعني أن الـ routes الموجودة في نفس namespace فقط مع Gateway يمكنها الارتباط. القيم الشائعة:

  • Same: نفس الـ namespace فقط
  • All: أي namespace
  • Selector: فقط namespaces التي تطابق label selector

إذا كنت تريد أن تكون مقصودًا في حدود المنصة، فهذا الحقل قوي جدًا. فهو يقرر "من يمكنه الارتباط بهذا ingress" على مستوى الـ listener نفسه.

نصيحة عملية: كيف تنظّم Listeners من دون ندم

هناك مبدأ مفيد جدًا:

كل listener يحمل مسؤولية واحدة محددة بوضوح.

على سبيل المثال:

  • Listener واحد يخدم مجموعة hostname رئيسية واحدة
  • Listener واحد يتعامل مع نوع بروتوكول رئيسي واحد
  • Listener واحد مفتوح لنوع Route واحد أو لمجموعة ثابتة من namespaces

لا تحشر كل المرور في listener واحد شامل لكل شيء ثم تأمل أن تنظّم الـ routes نفسها في الأسفل. هذا النمط يبدو فعالًا في البداية، لكن بعد ثلاثة أشهر سيصبح أشبه بدرج الكابلات الغامض الموجود في كل بيت: كل شيء بداخله، ولا شيء يمكن العثور عليه.

اقتراحات محددة:

  • يجب أن تكون أسماء الـ listeners دلالية: https-public و grpc-internal
  • في البيئات متعددة الـ listeners، اربط دائمًا ذلك مع sectionName
  • لا تخلط hostnames المهمة داخل listener واحد
  • اجعل تقييد allowedRoutes هو الافتراضي أولًا، ولا تبدأ بـ from: All

الخلاصة في سطر واحد

تعامل مع Listener على أنه عقد ingress، لا مجرد إعداد منفذ. فالحدود النظيفة للـ listeners تعني أن الـ routes سترتبط بشكل صحيح، والفرق ستمتلك مناطقها بشكل طبيعي، وسيصبح debugging أقل شبهًا بمطاردة قطة داخل متاهة.

الخطوة التالية

بعد حسم تصميم الـ listeners، يأتي السؤال الشائع التالي: عبر namespaces وفرق مختلفة، من يمكنه إرفاق routes، ومن يمكنه الإشارة إلى ماذا؟

المقالة التالية تغطي الأنماط عبر namespaces المختلفة ونموذج الملكية: 👉 تعدد الـ Namespace وملكية الفرق