التوجيه العملي لـ Envoy Gateway HTTP: Host و Path و Header وتقسيم المرور

6 min read

المقالتان الأوليان منحتاك أساس "ما هذا الشيء". وهذه المقالة تدخل في الشيء الذي ستعدله فعليًا يومًا بعد يوم: HTTPRoute.

فكّر في HTTPRoute على أنه سكربت توجيه للمرور الوارد، لكن بدلًا من كتابة قواعد Nginx أو حفظ annotations الخاصة بالـ controllers، فأنت تستخدم موارد أصلية في Kubernetes.

شيء يجب توضيحه من البداية: هذه المقالة تغطي فقط HTTPRoute لأنه يتعامل مع دلالات HTTP/HTTPS. أما gRPC فالأرجح أنك ستتجه إلى GRPCRoute، وبالنسبة إلى TCP الخام فإلى TCPRoute، وبالنسبة إلى TLS passthrough فإلى TLSRoute. لا تُجبر كل شيء على الدخول في HTTPRoute لأن الـ YAML سيبدأ بإعطاء انطباع "نحن فقط نحاول الترقيع."

أكثر أنواع التوجيه شيوعًا: Host و Path

هذا هو المثال الأكثر شيوعًا:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: app-route
spec:
  parentRefs:
    - name: eg
  hostnames:
    - "app.example.com"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /api
      backendRefs:
        - name: api-service
          port: 8080

ومعناه:

  • فقط Host: app.example.com سيطابق هذا الـ route
  • يجب أن يبدأ الـ path بـ /api
  • سيتم تحويل المرور إلى api-service:8080

إذا حذفت hostnames، يصبح الـ route "معتمدًا على path فقط، ولأي domain."

ما الذي يمكن لـ matches أن يطابقه

matches أحد أكثر المفاهيم مركزية في HTTPRoute. فكّر فيه على أنه "ما الشروط التي تفعّل هذه القاعدة."

شروط المطابقة الشائعة:

  • path
  • headers
  • queryParams
  • method

مثال:

matches:
  - method: GET
    path:
      type: PathPrefix
      value: /api
    headers:
      - name: version
        value: v2

ومعناه:

  • فقط طلبات GET
  • الـ path يبدأ بـ /api
  • يجب أن يحتوي الطلب على header باسم version: v2

هذا النمط مثالي لتجارب نسخ الـ API أو الإطلاقات المرحلية لمستخدمين محددين.

Route واحد، وعدة Rules

يمكن لـ HTTPRoute واحد أن يوجّه paths مختلفة إلى خدمات مختلفة:

rules:
  - matches:
      - path:
          type: PathPrefix
          value: /api
    backendRefs:
      - name: api-service
        port: 8080
  - matches:
      - path:
          type: PathPrefix
          value: /admin
    backendRefs:
      - name: admin-service
        port: 8080

وهذا ممتاز لتقسيم المسارات الفرعية تحت domain واحد، مثلًا:

  • /api يذهب إلى خدمة API
  • /admin يذهب إلى backend الإدارة

منطق التوجيه هنا ظاهر فورًا. وليس مثل بعض ملفات الإعدادات القديمة التي تصل إلى منتصفها وتبدأ بالتشكيك في وجودك أصلًا.

استخدام Header و Method و Query كشروط

HTTPRoute لا يقتصر على المطابقة حسب path، بل يمكنه أيضًا المطابقة حسب request headers و methods و query params.

على سبيل المثال، وجّه فقط الطلبات التي تحمل version: v2 إلى الخدمة الجديدة:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: versioned-route
spec:
  parentRefs:
    - name: eg
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /api
          headers:
            - name: version
              value: v2
      backendRefs:
        - name: api-v2
          port: 8080
    - matches:
        - path:
            type: PathPrefix
            value: /api
      backendRefs:
        - name: api-v1
          port: 8080

هذا النمط مفيد جدًا في:

  • Canary releases
  • تمكين عملاء محددين من تجربة الإصدارات الجديدة مبكرًا
  • التحقق اليدوي من الميزات الجديدة

مقارنةً بالانتقال الكامل إلى domain جديد، فإن التوجيه المبني على header أدق وأسهل للتجربة.

filters: معالجة الطلبات قبل التوجيه

HTTPRoute لا يكتفي بـ "المطابقة ثم التوجيه"، بل يمكنه أيضًا تطبيق تحويلات قبل التوجيه أو بعده. ولهذا توجد filters.

من الاستخدامات الشائعة إضافة headers:

rules:
  - filters:
      - type: RequestHeaderModifier
        requestHeaderModifier:
          add:
            - name: x-env
              value: prod
    backendRefs:
      - name: api-service
        port: 8080

وهذا مفيد في:

  • حقن بيانات tracing قبل تمرير الطلب إلى backend
  • إضافة headers ثابتة على طبقة ingress
  • تحويلات الطلب/الاستجابة

ومع ذلك، فـ filters قوية لكنها ليست تصريحًا مجانيًا لتحويل طبقة ingress إلى blob ضخم من middleware. تكديس منطق كثير على الـ routes سيجعل debugging يبدو وكأنه يحتاج إلى جلسة علاج نفسي خاصة به.

تقسيم المرور بالأوزان: أكثر أنماط Canary شيوعًا

لإرسال 90% من المرور إلى النسخة القديمة و10% إلى النسخة الجديدة، استخدم weight:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: canary-route
spec:
  parentRefs:
    - name: eg
  hostnames:
    - "app.example.com"
  rules:
    - backendRefs:
        - name: api-v1
          port: 8080
          weight: 90
        - name: api-v2
          port: 8080
          weight: 10

هذا هو الشكل الأساسي للـ canary / traffic splitting.

أنت لا تحتاج إلى service mesh كامل فقط كي تنفذ تجربة على المرور. بالنسبة إلى كثير من خدمات الـ API، فهذا أكثر من كافٍ، كما أن ضبطه أنظف بكثير.

💡 weight قيمة نسبية، ولا يشترط أن يكون مجموعها 100. فـ 9 و 1 تعبّران عن النسبة نفسها التي تعبّر عنها 90 و 10.

timeouts: لا تجعل الطلبات معلّقة للأبد

يدعم HTTPRoute إعدادات timeout على مستوى rule. وهذا مهم، فإذا لم تكن هناك حدود في طبقة ingress، فستبقى الطلبات السيئة معلقة بلا نهاية مثل عميل وقح يرفض المغادرة.

rules:
  - matches:
      - path:
          type: PathPrefix
          value: /reports
    timeouts:
      request: 10s
      backendRequest: 2s
    backendRefs:
      - name: report-service
        port: 8080

فهم الحقلين:

  • request: الحد الأقصى للانتظار الكلي لطلب العميل
  • backendRequest: الحد الأقصى لانتظار طلب واحد إلى الـ backend

لاحظ أن backendRequest لا ينبغي أن يتجاوز request، وإلا يصبح المنطق شبيهًا بقولك: "الرحلة كلها 10 دقائق، لكن انتظار حافلة واحدة قد يستغرق 20 دقيقة." وهنا يبدأ الكون نفسه بالارتباك.

نقطتان شائعتان ودقيقتان

1. يمكن لـ parentRefs أن يستهدف Listener محددًا

إذا كان الـ Gateway يملك عدة listeners، فيمكنك الارتباط بواحد فقط:

parentRefs:
  - name: eg
    sectionName: http

وهذا يعني أن الـ route يرتبط فقط بالـ listener المسمى http. وفي البنى متعددة الـ listeners، يكون هذا أكثر قابلية للتوقع بكثير من الارتباط بكل شيء.

2. عدم وجود matches يعني مطابقة كل شيء

إذا كانت الـ rule لا تحتوي على matches، فهي افتراضيًا تطابق /، أي أنها catch-all عمليًا.

وهذا مريح، لكنه قد يبتلع بصمت ما كنت تظنه rules أكثر تحديدًا. فلا تتجاوزه بدافع الكسل ثم تقضي ساعة تتساءل لماذا قواعدك الدقيقة لا تعمل.

التحقق من أن التوجيه يعمل

بعد ضبط الـ routes، تحقّق باستخدام curl:

curl -H "Host: app.example.com" http://$GATEWAY_HOST/api/users
curl -H "Host: app.example.com" -H "version: v2" http://$GATEWAY_HOST/api/users

ولفحص حالة HTTPRoute:

kubectl get httproute app-route -o yaml

ركّز بشكل خاص على:

  • status.parents
  • حالة Accepted

أحيانًا يبدو الـ YAML صحيحًا، لكن الـ Gateway رفضه. وهذا يشبه أن ترسل طلب توظيف وتفترض أنك قُبلت لمجرد أن لا أحد رد عليك.

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

جوهر HTTPRoute هو: "طابِق أولًا، ثم وجّه." وبمجرد أن تتقن host و path و header و weight، ستتمكن من تغطية نحو 80% من متطلبات HTTP ingress اليومية.

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

المقالة التالية تتناول شيئًا ستواجهه في كل بيئة إنتاج تقريبًا: HTTPS والشهادات و TLS termination والأمان: 👉 TLS والأمان