شبكة الخدمات: Istio وLinkerd

مشروع: تطبيق شبكة الخدمات على تطبيق مصغّر الخدمات

18 دقيقة الدرس 10 من 27

مشروع: تطبيق شبكة الخدمات على تطبيق مصغّر الخدمات

يجمع هذا المشروع الختامي كل مفاهيم الدورة في إطار عملي متكامل من البداية إلى النهاية: تثبيت Istio على مجموعة Kubernetes حقيقية، وإدخال تطبيق متعدد الخدمات إلى الشبكة، وتطبيق TLS المتبادل على جميع الاتصالات، ثم نشر إصدار canary بتقسيم دقيق للحركة، وإضافة سياسات المرونة، وتأكيد كل شيء عبر منظومة الرصد. الهدف هو إجراء جاهز للإنتاج يمكن تشغيله حرفيًا أو تكييفه مع بيئتك.

التطبيق المستهدف: Online Boutique

يُعدّ تطبيق Online Boutique (microservices-demo) من Google التطبيق المرجعي القياسي لاختبار شبكات الخدمات: أحد عشر خدمة بلغات برمجية متعددة (Go، Python، C#، Java، Node.js)، وحركة gRPC وHTTP واقعية، وواجهة أمامية تختبر كل مسار. سنربطه بالشبكة، ونطبق mTLS، ثم ننفذ نشرًا تدريجيًا لخدمة productcatalogservice من v1 إلى v2.

تفترض جميع الأوامر استخدام Istio 1.22 وKubernetes 1.29+، مع توفر istioctl في PATH. تكفي مجموعة k3s بعقدة واحدة (4 أنوية / 8 GB) لتنفيذ هذا الدرس. ملفات Online Boutique متاحة على github.com/GoogleCloudPlatform/microservices-demo.

الخطوة 1 — تثبيت Istio باستخدام IstioOperator جاهز للإنتاج

استخدم ملف تعريف default مع تعديل ثلاثة إعدادات حرجة في الإنتاج: سجلات وصول بتنسيق JSON منظم لتسهيل تحليل الأنابيب بدون تعبيرات منتظمة، وأخذ عينات 5% من التتبع (ارفعها إلى 100% خلال الحوادث)، وطلبات موارد صريحة لـ istiod حتى لا تُطرد تحت الضغط.

cat > istio-install.yaml <<EOF apiVersion: install.istio.io/v1alpha1 kind: IstioOperator metadata: name: production spec: profile: default meshConfig: accessLogFile: /dev/stdout accessLogFormat: | {"ts":"%START_TIME%","method":"%REQ(:METHOD)%", "path":"%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%", "status":"%RESPONSE_CODE%","ms":"%DURATION%", "upstream":"%UPSTREAM_CLUSTER%", "trace":"%REQ(X-B3-TRACEID)%"} defaultConfig: tracing: sampling: 5.0 components: pilot: k8s: resources: requests: {cpu: 200m, memory: 256Mi} limits: {cpu: "1", memory: 512Mi} EOF istioctl install -f istio-install.yaml -y istioctl verify-install kubectl get pods -n istio-system

الخطوة 2 — حقن الـ Sidecar: تسمية الـ Namespace

فعّل حقن الـ sidecar التلقائي على namespace التطبيق، ثم انشر Online Boutique. سيبدأ كل pod بحاويتين: التطبيق وproxy Envoy.

kubectl create namespace boutique kubectl label namespace boutique istio-injection=enabled kubectl apply -n boutique \ -f https://raw.githubusercontent.com/GoogleCloudPlatform/microservices-demo/main/release/kubernetes-manifests.yaml # تحقق من 2/2 READY لكل pod (التطبيق + istio-proxy) kubectl get pods -n boutique kubectl get pod -n boutique -l app=frontend -o jsonpath='{.items[0].spec.containers[*].name}' # الناتج المتوقع: server istio-proxy

الخطوة 3 — تطبيق mTLS الصارم على الـ Namespace

يعمل Istio افتراضيًا في وضع PERMISSIVE الذي يقبل الاتصالات النصية العادية واتصالات mTLS معًا. هذا مفيد أثناء الإعداد، لكن يجب تشديده قبل الادعاء بأن الـ namespace آمن. يحول مورد PeerAuthentication المحدد للـ namespace كل الخدمات إلى وضع STRICT: تُرفض الاتصالات النصية العادية عند الـ sidecar وليس عند التطبيق.

mTLS Strict Mode: mutual certificate exchange between sidecars Frontend Pod App Container Envoy Sidecar mTLS (SPIFFE certs) plain-text REJECTED Catalog Pod App Container Envoy Sidecar istiod cert authority (CA) PeerAuthentication: STRICT namespace: boutique
وضع mTLS الصارم: يقدم كلا الـ sidecar شهادات X.509 من نوع SPIFFE صادرة عن istiod؛ يُسقط proxy الاستقبال أي حركة نصية عادية.
# قفل الـ namespace بأكمله على وضع mTLS الصارم kubectl apply -n boutique -f - <<EOF apiVersion: security.istio.io/v1beta1 kind: PeerAuthentication metadata: name: default namespace: boutique spec: mtls: mode: STRICT EOF # تحقق: حاول الاتصال بخدمة بدون sidecar (يحاكي pod مارق) kubectl run test-no-mesh --image=curlimages/curl --restart=Never \ --command -- curl -s http://productcatalogservice.boutique:3550/ # متوقع: curl: (56) Recv failure: Connection reset by peer # تحقق من نشاط mTLS من داخل الشبكة kubectl exec -n boutique deploy/frontend -c istio-proxy -- \ curl -s http://productcatalogservice:3550/ -v 2>&1 | grep "TLS"
نفّذ istioctl authn tls-check <pod> productcatalogservice.boutique.svc.cluster.local لمعرفة ما إذا كان زوج عميل-خادم محدد يستخدم mTLS. يُظهر الناتج وضع PeerAuthentication، ووضع TLS في DestinationRule، وما إذا كانا متوافقَين — أسرع بكثير من قراءة إعداد Envoy الخام.

الخطوة 4 — تقسيم حركة Canary لخدمة productcatalogservice

ضع تسمية version: v1 على Deployment الحالي، ثم انشر Deployment جديدًا للإصدار v2. يعرّف DestinationRule مجموعتين فرعيتين. يبدأ VirtualService بنسبة 95/5 وتزيد وزن v2 تدريجيًا عبر مراحل النشر.

# 1. إضافة تسمية الإصدار إلى الـ deployment الحالي kubectl patch deployment productcatalogservice -n boutique \ --type=json \ -p='[{"op":"add","path":"/spec/template/metadata/labels/version","value":"v1"}]' # 2. نشر نسخة v2 kubectl apply -n boutique -f - <<EOF apiVersion: apps/v1 kind: Deployment metadata: name: productcatalogservice-v2 spec: replicas: 1 selector: matchLabels: app: productcatalogservice version: v2 template: metadata: labels: app: productcatalogservice version: v2 spec: serviceAccountName: productcatalogservice containers: - name: server image: gcr.io/google-samples/microservices-demo/productcatalogservice:v0.10.0 env: - name: NEW_FEATURES value: "true" ports: - containerPort: 3550 resources: requests: {cpu: 100m, memory: 64Mi} EOF # 3. DestinationRule — تعريف المجموعتين الفرعيتين kubectl apply -n boutique -f - <<EOF apiVersion: networking.istio.io/v1beta1 kind: DestinationRule metadata: name: productcatalogservice namespace: boutique spec: host: productcatalogservice trafficPolicy: connectionPool: http: http1MaxPendingRequests: 100 http2MaxRequests: 1000 outlierDetection: consecutive5xxErrors: 3 interval: 10s baseEjectionTime: 30s maxEjectionPercent: 50 subsets: - name: v1 labels: version: v1 - name: v2 labels: version: v2 EOF # 4. VirtualService — البداية بنسبة 95% v1 / 5% v2 kubectl apply -n boutique -f - <<EOF apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: productcatalogservice namespace: boutique spec: hosts: - productcatalogservice http: - route: - destination: host: productcatalogservice subset: v1 weight: 95 - destination: host: productcatalogservice subset: v2 weight: 5 timeout: 3s retries: attempts: 2 perTryTimeout: 1s retryOn: gateway-error,connect-failure,retriable-4xx EOF

الخطوة 5 — أتمتة تحويل الحركة والتراجع

في الإنتاج، تُدار تغييرات الأوزان عبر CI/CD (Argo Rollouts أو Flux مع تصحيح Kustomize) بدلًا من أوامر kubectl patch اليدوية. راقب معدل الخطأ والتأخر في Grafana أو Kiali بعد كل تحويل قبل المتابعة.

#!/usr/bin/env bash # canary-promote.sh — استعلام Prometheus؛ ترقية أو تراجع set -euo pipefail PROM="http://prometheus.istio-system:9090" SVC="productcatalogservice" NS="boutique" WEIGHTS=(5 20 50 80 100) error_rate() { curl -sG "${PROM}/api/v1/query" \ --data-urlencode "query=sum(rate(istio_requests_total{destination_service_name=\"${SVC}\",destination_version=\"v2\",response_code=~\"5..\"}[2m])) / sum(rate(istio_requests_total{destination_service_name=\"${SVC}\",destination_version=\"v2\"}[2m]))" \ | python3 -c "import sys,json; d=json.load(sys.stdin); print(float(d['data']['result'][0]['value'][1]) if d['data']['result'] else 0)" } rollback() { echo "معدل الخطأ مرتفع جداً — التراجع إلى v1" kubectl patch virtualservice productcatalogservice -n ${NS} \ --type=json \ -p='[{"op":"replace","path":"/spec/http/0/route/0/weight","value":100}, {"op":"replace","path":"/spec/http/0/route/1/weight","value":0}]' exit 1 } for WEIGHT in "${WEIGHTS[@]}"; do echo "تعيين وزن v2 إلى ${WEIGHT}%" kubectl patch virtualservice productcatalogservice -n ${NS} \ --type=json \ -p="[{\"op\":\"replace\",\"path\":\"/spec/http/0/route/0/weight\",\"value\":$((100-WEIGHT))}, {\"op\":\"replace\",\"path\":\"/spec/http/0/route/1/weight\",\"value\":${WEIGHT}}]" sleep 120 ERR=$(error_rate) echo "معدل خطأ v2: ${ERR}" python3 -c "import sys; sys.exit(1) if float('${ERR}') > 0.01 else sys.exit(0)" || rollback done echo "اكتمل النشر التدريجي — v2 يخدم 100% من الحركة"
لا تحذف Deployment الخاص بـ v1 حتى يعمل VirtualService بنسبة 100% على v2 لمدة نافذة SLO كاملة على الأقل (عادةً 30 دقيقة إلى ساعة). حذف v1 مبكرًا يعني أن التراجع يتطلب دفع صورة جديدة — مما يُلغي خاصية التراجع السريع التي تجعل نشرات canary آمنة.

الخطوة 6 — تطبيق سياسات المرونة

mTLS وتقسيم canary في مكانهما. طبّق الآن طبقة المرونة: قطع الدائرة عبر outlierDetection (موجود مسبقًا في DestinationRule)، وتحديد معدل للواجهة الأمامية باستخدام EnvoyFilter، وإضافة مهلة لـ checkoutservice.

# سياسة المهلة وإعادة المحاولة لـ checkoutservice kubectl apply -n boutique -f - <<EOF apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: checkoutservice namespace: boutique spec: hosts: - checkoutservice http: - route: - destination: host: checkoutservice timeout: 10s retries: attempts: 1 perTryTimeout: 8s retryOn: reset,connect-failure EOF # تحديد المعدل المحلي على الواجهة الأمامية (1000 طلب/دقيقة) kubectl apply -n boutique -f - <<EOF apiVersion: networking.istio.io/v1alpha3 kind: EnvoyFilter metadata: name: frontend-ratelimit namespace: boutique spec: workloadSelector: labels: app: frontend configPatches: - applyTo: HTTP_FILTER match: context: SIDECAR_INBOUND listener: filterChain: filter: name: envoy.filters.network.http_connection_manager subFilter: name: envoy.filters.http.router patch: operation: INSERT_BEFORE value: name: envoy.filters.http.local_ratelimit typed_config: "@type": type.googleapis.com/udpa.type.v1.TypedStruct value: stat_prefix: http_local_rate_limiter token_bucket: max_tokens: 1000 tokens_per_fill: 1000 fill_interval: 60s EOF
ثبّت لوحتي Kiali وGrafana في دليل الاستجابة للحوادث. خلال حادثة canary، اللوحة الأكثر قيمة هي Request Success Rate by Version — تعرض v1 وv2 جنبًا إلى جنب في الوقت الفعلي، مما يجعل نطاق التأثير مرئيًا فورًا دون الحاجة لتصفح السجلات.

الخطوة 7 — AuthorizationPolicy: ثقة صفرية في الاتصالات الداخلية

يُثبت mTLS الهوية لكن لا يقيّد أي هويات مسموح لها بالتواصل مع أي خدمات. أضف موارد AuthorizationPolicy لتطبيق مبدأ الصلاحية الأدنى: فقط الخدمات التي تحتاج فعليًا إلى استدعاء productcatalogservice مسموح لها بذلك، وكل المتصلين الآخرين يتلقون HTTP 403.

# السماح فقط للواجهة الأمامية وخدمة التوصية بالوصول إلى catalog kubectl apply -n boutique -f - <<EOF apiVersion: security.istio.io/v1beta1 kind: AuthorizationPolicy metadata: name: productcatalogservice-allow namespace: boutique spec: selector: matchLabels: app: productcatalogservice action: ALLOW rules: - from: - source: principals: - cluster.local/ns/boutique/sa/frontend - cluster.local/ns/boutique/sa/recommendationservice --- apiVersion: security.istio.io/v1beta1 kind: AuthorizationPolicy metadata: name: deny-all namespace: boutique spec: {} EOF # اختبار: الواجهة الأمامية تصل إلى catalog (يجب أن تحصل على 200) kubectl exec -n boutique deploy/frontend -c server -- \ wget -qO- http://productcatalogservice:3550/ | head -1 # اختبار: cartservice لا تستطيع الوصول إلى catalog (يجب أن تحصل على 403) kubectl exec -n boutique deploy/cartservice -c server -- \ wget -qO- http://productcatalogservice:3550/ 2>&1 | head -1 # متوقع: wget: server returned error: HTTP/1.1 403 Forbidden

دروس إنتاجية مستخلصة من هذا المشروع

يكشف تطبيق شبكة الخدمات على تطبيق حقيقي عن عدة دروس لا تظهر في الوثائق:

  • حقن الـ Sidecar أولًا ثم تشديد الأمان. حوّل الـ namespaces إلى PERMISSIVE أولًا، تحقق من تدفق الحركة مع الـ sidecars المحقونة، ثم انتقل إلى STRICT. التحويل إلى STRICT قبل اكتمال الحقن يكسر استدعاءات الخدمات بصمت.
  • يجب أن يسبق DestinationRule أي VirtualService يشير إليه. طبّق DestinationRule أولًا؛ وإلا فلن يجد Envoy مجموعة فرعية للتوجيه إليها ويُرجع 503.
  • يجب أن يكون التراجع عن canary عملية من أمر واحد. ينبغي أن يحتوي خط CI/CD على هدف make rollback يضبط أوزان VirtualService على 100/0 في أقل من 30 ثانية.
  • سياسة deny-all تكسر Jobs وInit Containers. طبّق deny-all للـ namespace تدريجيًا، وتحقق دائمًا من Kubernetes Jobs وCronJobs — غالبًا ما تستخدم service accounts غائبة عن قواعد السماح.
  • ميزانية موارد الـ Sidecar. على نطاق Google (آلاف الـ pods)، يستهلك كل Envoy sidecar 50-100 MB ذاكرة وصول عشوائي و0.1-0.2 نواة CPU في وضع الخمول. ادرج هذا في نموذج سعة المجموعة قبل تفعيل الحقن على مستوى الشبكة.
على هذا النطاق — آلاف الـ pods وعشرات الخدمات — يصبح سطح إعداد الشبكة مخاطرة موثوقية بحد ذاته. اجعل istioctl analyze خطوة إلزامية في CI على كل تغيير لإعداد الشبكة. يكتشف DestinationRules المفقودة، وVirtualServices المتعارضة، وAuthorizationPolicies غير الصالحة قبل وصولها إلى المجموعة.

لقد أكملت الآن دورة شبكة الخدمات بالكامل: من المبادئ الأساسية عبر المعمارية وإدارة الحركة وmTLS والمرونة والرصد وLinkerd والتشغيل وهذا المشروع الختامي. الأنماط هنا — حقن sidecar، والهوية المستندة إلى SPIFFE، وتوجيه الحركة التصريحي، والتفويض على مبدأ الثقة الصفرية — هي الخط الأساسي لأي منصة خدمات مصغّرة جادة في عام 2025 وما بعده.