اكتشاف الخدمات والإعداد والبوّابة

السجل والإعدادات والبوابة معًا

18 دقيقة الدرس 9 من 12

السجل والإعدادات والبوابة معًا

لقد درست Eureka (سجل الخدمات) وSpring Cloud Config Server (الإعداد المركزي) وSpring Cloud Gateway (توجيه الحافة) كأجزاء منفصلة. في الإنتاج، تعمل الثلاثة في آنٍ واحد وتتفاعل مع بعضها بطرق لا تظهر عند دراسة كل أداة بمعزل عن الأخرى. يربط هذا الدرس الأجزاء الثلاثة في نموذج ذهني متماسك واحد — وفي كود يعمل فعلًا — حتى تتمكن من التفكير في المنظومة ككل وتشخيص الأعطال العابرة للطبقات وتطبيق ضوابط الأمان التي تمتد عبر الطبقات الثلاث جميعها.

كيف ترتبط خدمات البنية التحتية الثلاث

فكّر في الخدمات الثلاث باعتبارها حلقات متداخلة من المسؤوليات:

  • خادم الإعدادات (Config Server) هو الحلقة الداخلية. يبدأ أولًا ويزوّد كل خدمة أخرى بالإعدادات اللازمة، بما فيها Eureka والبوابة ذاتها.
  • خادم Eureka هو الحلقة التالية. يبدأ بعد استلام إعداداته، ثم يعمل دليلًا هاتفيًا تستشيره كل خدمة — بما فيها البوابة — لمعرفة عناوين الحالات.
  • بوابة API هي الحلقة الخارجية. تبدأ أخيرًا، وتسحب جدول مساراتها من خادم الإعدادات، وتكتشف عناوين الحالات الحقيقية من Eureka، ثم تقبل حركة المرور الخارجية.

تقع الخدمات المصغّرة للأعمال خلف الحلقات الثلاث جميعها: تسحب الإعدادات من خادم الإعدادات، وتسجّل نفسها في Eureka، وتستقبل الطلبات الموجَّهة من البوابة.

ترتيب البدء مهم. إذا بدأت البوابة قبل أن تكون Eureka جاهزة، فلن تتمكن من تحليل عناوين URI ذات الصيغة lb:// وستُعيد 503 لكل مسار. أدوات التنسيق مثل Kubernetes تحل هذه المشكلة بفحوصات الجاهزية (readiness probes)؛ وفي Docker Compose، استخدم depends_on مع condition: service_healthy.

خادم الإعدادات: تزويد خدمات البنية التحتية أيضًا

خطأ شائع هو التعامل مع خادم الإعدادات باعتباره خدمةً للخدمات المصغّرة الخاصة بالأعمال فقط. فـ Eureka والبوابة هما تطبيقا Spring Boot بدورهما ويجب أن يشتقّا إعداداتهما من خادم الإعدادات أيضًا. تكون خصائص التمهيد لكل منهما في حدّها الأدنى — بما يكفي للعثور على خادم الإعدادات فحسب — ويأتي الباقي عبر الشبكة.

# eureka-server/src/main/resources/application.yml spring: application: name: eureka-server config: import: "configserver:http://config-server:8888" # gateway/src/main/resources/application.yml spring: application: name: api-gateway config: import: "configserver:http://config-server:8888"

يقدّم خادم الإعدادات بعد ذلك ملفات باسم eureka-server.yml وapi-gateway.yml (إضافةً إلى ملف application.yml مشترك للخصائص العامة) من مستودع Git الخاص به. يمكن لجميع عناوين URL لنظراء Eureka وتعريفات مسارات البوابة وإعدادات TLS أن تقطن في ذلك المستودع بشكل منظّم وقابل للتدقيق.

تدفق تسجيل Eureka لكل خدمة

عند بدء تشغيل خدمة مصغّرة خاصة بالأعمال — لنقل order-service — تجري التسلسل التالي تلقائيًا إذا أضفت التبعية spring-cloud-starter-netflix-eureka-client:

  1. تسحب الخدمة إعداداتها الكاملة من خادم الإعدادات (بما فيها عنوان URL الخاص بـ Eureka).
  2. ترسل نبضة تسجيل POST /eureka/apps/ORDER-SERVICE إلى Eureka تتضمن عنوان IP والمنفذ وعنوان URL لفحص الصحة وخريطة البيانات الوصفية.
  3. تخزّن Eureka الحالة في سجلّها وتبثّ التغيير إلى نظراء Eureka الآخرين (في حال التقوس).
  4. تلتقط بوابة API — التي تستطلع Eureka كل 30 ثانية افتراضيًا — الحالة الجديدة وتضيفها إلى تجمّع موازن الحمل الخاص بمسار lb://order-service.
# order-service/src/main/resources/application.yml # (تُسلَّم من خادم الإعدادات، غير مخزّنة محليًا) server: port: 0 # منفذ عشوائي — تتتبع Eureka المنفذ الحقيقي تلقائيًا eureka: client: service-url: defaultZone: http://eureka-server:8761/eureka/ instance: prefer-ip-address: true lease-renewal-interval-in-seconds: 10 lease-expiration-duration-in-seconds: 30 metadata-map: version: "@project.version@" # يُحقن وقت البناء من pom.xml

يتيح ضبط server.port=0 لنظام التشغيل تخصيص منفذ متاح لكل حالة. وبما أن Eureka تسجّل المنفذ الفعلي عند التسجيل، يمكنك تشغيل ثلاث حالات من order-service على المضيف ذاته دون تعارض في المنافذ — لكل منها إدخال فريد في السجل.

اكتشاف مسارات البوابة عبر Eureka

يستخدم أبسط إعداد للبوابة الاكتشاف التلقائي للمسارات: تحصل كل تطبيق مسجّل في Eureka تلقائيًا على مسار من خلال /{service-id}/**. هذا مريح للتطوير لكنه يكشف الخدمات الداخلية التي لا ينبغي أن تكون عامة. في الإنتاج، حدّد المسارات بشكل صريح.

# api-gateway.yml (يُقدَّم من مستودع Git الخاص بخادم الإعدادات) spring: cloud: gateway: discovery: locator: enabled: false # تعطيل الاكتشاف التلقائي؛ المسارات صريحة routes: - id: order-service uri: lb://order-service # يخبر lb:// البوابة باستخدام Eureka + LoadBalancer predicates: - Path=/api/orders/** filters: - StripPrefix=1 - name: CircuitBreaker args: name: orderServiceCB fallbackUri: forward:/fallback/orders - id: inventory-service uri: lb://inventory-service predicates: - Path=/api/inventory/** filters: - StripPrefix=1

المخطط lb:// هو العقد بين البوابة وطبقة تجريد Spring Cloud LoadBalancer. عند تلقّي الطلب، تطلب البوابة من LoadBalancer تحليل order-service؛ يستعلم LoadBalancer ذاكرة التخزين المؤقت لـ Eureka ويُعيد عنوان حالة صحية واحدة باستخدام توزيع دائري (أو استراتيجية مخصصة). تعيد البوابة بعد ذلك كتابة URI الطلب إلى http://<resolved-ip>:<resolved-port>/orders/... وتُحوّله.

استخدم prefer-ip-address: true في عملاء Eureka. في البيئات المُحاوَية، نادرًا ما تكون أسماء المضيفين قابلة للتحليل عبر الخدمات، لكن عناوين IP دائمًا قابلة للتحليل. بدون هذه الإشارة، تسجّل Eureka اسم المضيف الخاص بالحاوية (مثل a3f9c1b2) الذي لا تستطيع البوابة تحليله من شبكة حاوية مختلفة.

الأمان عبر الطبقات الثلاث

لكل من السجل وخادم الإعدادات والبوابة سطحه الأمني الخاص. عند تشغيل الثلاثة معًا، تتداخل السطوح وتحتاج إلى سياسة متسقة:

  • خادم الإعدادات: احمِ نقطتَي /encrypt و/decrypt بمصادقة HTTP Basic أو OAuth2. شفِّر جميع الأسرار (كلمات المرور ومفاتيح التوقيع) المخزّنة في مستودع Git باستخدام البادئة {cipher}. يحتفظ خادم الإعدادات وحده بالمفتاح الخاص؛ يستقبل العملاء القيم مفكوكة التشفير عبر قناة TLS داخلية فقط.
  • خادم Eureka: فعِّل مصادقة HTTP Basic. يجب على جميع العملاء تضمين بيانات الاعتماد في عنوان URL الخاص بـ defaultZone: http://user:password@eureka-server:8761/eureka/. بدون مصادقة، يمكن لأي عملية على الشبكة ذاتها تسجيل حالة مزيّفة واعتراض حركة المرور.
  • بوابة API: هي المكوّن الوحيد الذي يقبل حركة المرور الخارجية. طبّق التحقق من JWT وإعداد خادم موارد OAuth2 وسياسات CORS وتحديد معدل الطلبات هنا — لا في الخدمات المصغّرة الفردية. يجب أن تحمل الاستدعاءات الداخلية بين الخدمات التي تصل عبر موازن الحمل (لا عبر البوابة) JWT مُحالًا أو رمزًا من آلة إلى آلة حتى لا تمنح اختراقات طبقة الشبكة وصولًا داخليًا غير مقيّد.
// GatewaySecurityConfig.java (في خدمة api-gateway) @Configuration @EnableWebFluxSecurity public class GatewaySecurityConfig { @Bean public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { http .csrf(ServerHttpSecurity.CsrfSpec::disable) .authorizeExchange(ex -> ex .pathMatchers("/api/auth/**", "/actuator/health").permitAll() .anyExchange().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt.jwtAuthenticationConverter(grantedAuthoritiesConverter())) ); return http.build(); } @Bean ReactiveJwtAuthenticationConverterAdapter grantedAuthoritiesConverter() { JwtGrantedAuthoritiesConverter delegate = new JwtGrantedAuthoritiesConverter(); delegate.setAuthoritiesClaimName("roles"); delegate.setAuthorityPrefix("ROLE_"); ReactiveJwtAuthenticationConverter converter = new ReactiveJwtAuthenticationConverter(); converter.setJwtGrantedAuthoritiesConverter( jwt -> Flux.fromIterable(delegate.convert(jwt)) ); return new ReactiveJwtAuthenticationConverterAdapter(converter); } }

فحوصات الصحة والجاهزية في منظومة الثلاث طبقات

يكشف Spring Boot Actuator نقطة /actuator/health في كل خدمة. عند وجود خدمات البنية التحتية الثلاث، تصبح نقطة صحة البوابة مؤشرًا تجميعيًا مفيدًا: فهي تعتمد على قابلية الوصول إلى Eureka، التي تعتمد بدورها على أن يكون خادم الإعدادات قد اكتمل تمهيده بنجاح. هيّئ كل خدمة لإفصاح عن تبعياتها المنبعية:

# application.yml (مشترك، يُقدَّم من خادم الإعدادات) management: endpoints: web: exposure: include: health, info, refresh, metrics endpoint: health: show-details: when-authorized probes: enabled: true # يكشف /actuator/health/liveness و /actuator/health/readiness health: eureka: enabled: true # يُسهم عميل Eureka بمؤشر صحته الخاص
لا تكشف /actuator عبر مسار البوابة العامة. تكشف نقاط نهاية Actuator عن الطوبولوجيا والمقاييس — وإذا أُدرجت /refresh أو /busrefresh فإنها تتيح للمتصلين تعديل الإعداد الجاري تشغيله. إما احجب مسارات Actuator على مستوى البوابة باستخدام محدّد مسار (route predicate)، أو نشر Actuator على منفذ إدارة منفصل (management.server.port) لا يمكن الوصول إليه من الإنترنت العام.

تتبع الطلب من طرف إلى طرف

لمّاً جمعنا كل شيء، إليك ما يحدث عندما يستدعي عميل على الهاتف المحمول POST https://api.example.com/api/orders:

  1. تستقبل البوابة الطلب. يتحقق مرشّح JWT من رمز Bearer مقابل JWKS URI (مُهيَّأ عبر خادم الإعدادات). إذا كان غير صالح، يُعيد 401 فورًا — ولا يصل الطلب أبدًا إلى الخدمة المصغّرة.
  2. مطابقة المسار. يطابق المحدّد Path=/api/orders/**؛ يعيد مرشّح StripPrefix=1 كتابة المسار إلى /orders.
  3. تحليل Eureka. يستعلم LoadBalancer ذاكرة التخزين المؤقت لـ Eureka عن order-service ويختار حالة واحدة (مثلًا 10.0.1.42:52381).
  4. التحويل. تُحوّل البوابة POST http://10.0.1.42:52381/orders مُضيفةً رأس نشر (X-Forwarded-For ومعرّف ارتباط اختياريًا من مرشح AddRequestHeader).
  5. الاستجابة. يُعيد order-service 201؛ ترحّلها البوابة مع تطبيق أي مرشّحات استجابة (مثل إزالة رؤوس داخلية كـ X-Application-Context).

كل قيمة إعداد مُستخدَمة في الخطوات 1–5 — JWKS URI وجدول المسارات وعنوان Eureka URL — قدّمها خادم الإعدادات من مستودع Git، لذا فإن تغيير أي منها يعني إجراء التزام Git يليه استدعاء /actuator/busrefresh، دون الحاجة إلى إعادة نشر.

الخلاصة

يشكّل خادم الإعدادات وEureka وبوابة API ثلاثيًا متعاضدًا: يوزّع الإعداد المعلومات على الاثنين الآخرين؛ تحتفظ Eureka بالخريطة الحية للحالات الجارية؛ وتستخدم البوابة الاثنين لقبول حركة المرور الخارجية والتحقق منها وتوجيهها بذكاء. إنّ تأمين الثلاثة باتساق — بأسرار مشفّرة وسجل بمصادقة وحافة تُتحقق من JWT — يُغلق محيط الأمان دون دفع هذا التعقيد إلى كل خدمة مصغّرة على حدة. في الدرس الأخير من هذا الدرس التعليمي ستطبّق كل شيء على مشروع صغير لكن واقعي من الخدمات المصغّرة القابلة للاكتشاف.