مشروع التخرج: منصة إنتاج بمستوى الشركات الكبرى

التسليم المستمر وGitOps

18 دقيقة الدرس 5 من 30

التسليم المستمر وGitOps

خط أنابيب التسليم هو المكان الذي تتحوّل فيه استثمارات المنصة إلى سرعة في الأعمال. في Google، يمكن لإيداع المطوّر الوحيد أن يصل إلى الإنتاج في أقل من ساعة — يُختبر تلقائيًا، يُفحص أمنيًا، يُبنى في حزمة أثرية غير قابلة للتغيير، يُرقَّى عبر البيئات، ويُطرح تدريجيًا على نسبة من حركة المرور قبل الإصدار الكامل. هذه النتيجة ليست مصادفة. إنها ثمرة هندسة متعمّدة لخط الأنابيب، وحلقة تحكّم GitOps، واستراتيجية تسليم تدريجي تتيح للفرق الشحن بثقة بمعدل 100+ نشر يوميًا.

تتتبّع هذه الدرس المسار الكامل من git push للمطوّر حتى الإصدار التدريجي في الإنتاج، مع استعراض القرارات الهندسية التي تُميّز نظام التسليم الاحترافي عن نص CI بسيط.

المرحلة الأولى — طلب السحب وبوابات CI

يبدأ خط الأنابيب قبل دمج أي سطر من الكود. يجب أن يُطبّق نظام CI بوابة جودة صارمة على طلب السحب نفسه. على نطاق واسع، يعني ذلك تشغيل كل شيء بالتوازي على بيئة معزولة وقصيرة الأمد — لا مشاركة بيئة تدريجية واحدة طويلة الأمد تتحوّل إلى عنق زجاجة.

تنجز وظيفة CI الجاهزة للإنتاج أربعة أشياء بترتيب صارم: التحقق من الأسلوب والتحليل الثابت، اختبارات الوحدة والتكامل مع تبعيات خدمية حقيقية (Testcontainers أو فضاءات أسماء مؤقتة داخل المجموعة)، المسح الأمني (SAST عبر Semgrep وSCA عبر Trivy أو Grype لثغرات التبعيات)، وأخيرًا بناء صورة الحاوية. فقط إذا اجتازت البوابات الأربع جميعها، تضع CI الصادقة على قابلية الدمج. تفرض قواعد حماية الفروع هذا الأمر — بلا تجاوز، ولا استثناء، حتى للمهندسين الكبار.

# .github/workflows/ci.yaml (GitHub Actions، يعمل على كل طلب سحب) name: CI Gate on: pull_request: branches: [main] jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run golangci-lint uses: golangci/golangci-lint-action@v6 with: version: v1.59 test: runs-on: ubuntu-latest services: postgres: image: postgres:16 env: POSTGRES_PASSWORD: test options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - uses: actions/checkout@v4 - name: Run tests with coverage run: go test -race -coverprofile=coverage.out ./... - name: Enforce 80% coverage floor run: | COVERAGE=$(go tool cover -func coverage.out | grep total | awk '{print $3}' | tr -d '%') if (( $(echo "$COVERAGE < 80" | bc -l) )); then echo "Coverage $COVERAGE% below 80%"; exit 1; fi scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: SAST (Semgrep) uses: semgrep/semgrep-action@v1 with: config: p/ci - name: SCA (Trivy filesystem scan) uses: aquasecurity/trivy-action@0.24.0 with: scan-type: fs severity: HIGH,CRITICAL exit-code: 1 build: needs: [lint, test, scan] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Build & push image (signed) uses: docker/build-push-action@v6 with: push: true tags: ${{ env.REGISTRY }}/myapp:${{ github.sha }} cache-from: type=gha cache-to: type=gha,mode=max - name: Sign image with Cosign run: cosign sign --yes ${{ env.REGISTRY }}/myapp:${{ github.sha }}
حزم أثرية غير قابلة للتغيير، موسومة بهاش الإيداع. كل صورة توسم بهاش Git الكامل — لا توسم أبدًا بـlatest ولا باسم الفرع. علامة الهاش حتمية وغير قابلة للتغيير: لن تشير أبدًا إلى ملف ثنائي مختلف. هذا شرط مسبق لترقية GitOps الموثوقة والتراجع ذي المعنى. استخدم التوقيع بدون مفتاح في Cosign (قائم على OIDC عبر GitHub Actions) حتى يتمكن أي نظام لاحقًا من التحقق من أي خط أنابيب أنتج الصورة.

المرحلة الثانية — ترقية GitOps عبر البيئات

بمجرد بناء الحزمة الأثرية ودفعها، تفتح وظيفة CI — لا إنسان — طلب سحب على مستودع إعدادات GitOps. هذه نقطة التسليم بين عالم فريق التطبيق (الكود) وعالم المنصة (الحالة المرغوبة). يحتوي مستودع الإعداد على طبقات Kustomize أو ملفات قيم Helm لكل بيئة: dev/، staging/، production/. يُحدّث طلب السحب علامة الصورة في الطبقة المعنية. يرصد Argo CD (أو Flux) الدمج ويزامن المجموعة.

CI/CD & GitOps Pipeline — PR to Production PR / Push app-repo CI Gates lint / test SAST / SCA build + sign Artifact OCI image sha256 tag Cosign sig GitOps PR bump tag in config-repo dev overlay Argo CD sync dev → staging → prod canary merge parallel jobs registry push auto-open PR reconcile loop dev staging prod (canary)
خط الأنابيب الكامل: من دمج طلب السحب عبر بوابات CI ومستودع الحزم الأثرية إلى الترقية التدريجية عبر البيئات والإصدار التجريبي في الإنتاج.

نموذج الترقية بين البيئات صريح وقابل للتدقيق. يتزامن dev تلقائيًا مع كل دمج. تُشغَّل ترقية staging إما تلقائيًا بعد نجاح اختبارات الدخان في dev (للخدمات منخفضة الخطر) أو عبر خطوة موافقة يدوية في خط أنابيب CI (للخدمات ذات SLO الصارم). ترقية الإنتاج مقيّدة دائمًا: يوافق مهندس على طلب سحب GitOps، يزامن Argo CD، وتتولى استراتيجية التسليم التدريجي لـArgo Rollouts من هناك.

# هيكل config-repo (Kustomize) k8s/ base/ deployment.yaml service.yaml overlays/ dev/ kustomization.yaml # image tag: sha-abc123 staging/ kustomization.yaml # image tag: sha-abc123 production/ kustomization.yaml # image tag: sha-def456 rollout.yaml # استراتيجية canary لـArgo Rollouts # وظيفة CI التي تفتح طلب السحب على config-repo بعد بناء الحزمة: - name: Bump dev image tag run: | git clone https://x-access-token:${{ secrets.CONFIG_REPO_TOKEN }}@github.com/org/config-repo cd config-repo yq e -i '.images[0].newTag = "${{ github.sha }}"' k8s/overlays/dev/kustomization.yaml git config user.email "ci-bot@org.com" git config user.name "ci-bot" git checkout -b bump-dev-${{ github.sha }} git commit -am "chore: bump dev image to ${{ github.sha }}" gh pr create --title "Deploy ${{ github.sha }} to dev" --body "Auto-promotion from CI" --base main

المرحلة الثالثة — التسليم التدريجي مع Argo Rollouts

نشر الإنتاج الذي يُحوّل 100% من حركة المرور فورًا ليس استراتيجية نشر — إنه رهان. يُغيّر التسليم التدريجي نموذج المخاطرة: تُعرّض الإصدار الجديد لنسبة صغيرة من حركة الإنتاج الفعلية، تتحقق من مؤشرات SLO الرئيسية (معدل الأخطاء، زمن الاستجابة p99، مقاييس الأعمال عبر قوالب التحليل)، ثم تتقدم أو تتراجع تلقائيًا. لا حاجة لأن يكون أي إنسان مستيقظًا الساعة 03:00 يراقب لوحات المعلومات.

# k8s/overlays/production/rollout.yaml apiVersion: argoproj.io/v1alpha1 kind: Rollout metadata: name: myapp spec: replicas: 40 strategy: canary: canaryService: myapp-canary stableService: myapp-stable trafficRouting: istio: virtualService: name: myapp-vsvc destinationRule: name: myapp-destrule canarySubsetName: canary stableSubsetName: stable steps: - setWeight: 5 - pause: {duration: 5m} - analysis: templates: - templateName: success-rate - setWeight: 25 - pause: {duration: 10m} - analysis: templates: - templateName: success-rate - templateName: latency-p99 - setWeight: 100 autoPromotionEnabled: false --- apiVersion: argoproj.io/v1alpha1 kind: AnalysisTemplate metadata: name: success-rate spec: metrics: - name: success-rate interval: 1m successCondition: result[0] >= 0.995 failureLimit: 2 provider: prometheus: address: http://prometheus.monitoring:9090 query: | sum(rate(http_requests_total{job="myapp",status!~"5.."}[2m])) / sum(rate(http_requests_total{job="myapp"}[2m]))
اختيار المقاييس لقوالب التحليل أهم من نسبة canary نفسها. معدل الأخطاء وحده إشارة متأخرة — بحلول وقت ارتفاعه، يكون المستخدمون قد تعرّضوا للأعطال بالفعل. أضف قالب تحليل للزمن (p99 > 200ms يُشغّل التراجع)، وللخدمات الحساسة للإيرادات أضف مقياسًا مخصصًا يتتبع الأحداث التجارية (الطلبات المُكتملة، عمليات الدفع) عبر عدّاد في تطبيقك تجمعه Prometheus. انخفاض 15% في معدل التحويل في حركة canary يعطيك إشارة أقوى من أي مقياس بنية تحتية.

أنماط الأعطال في الإنتاج التي ستواجهها فعلًا

على نطاق واسع، تتكرر هذه الأنماط بما يكفي لتبرير التصميم ضدها من اليوم الأول:

  • تعارض دمج طلبات سحب config-repo. خدمتان مُرقَّيان في وقت واحد تعدّلان ملف الطبقة ذاتها. يحدث تعارض Git عند الدمج التلقائي ويحجب كلا النشرَين. الحل: أسند لكل خدمة مسار Kustomize خاص بها؛ استخدم yq لاستهداف حقل محدد، لا أسلوب sed القائم على السطور.
  • عاصفة مزامنة Argo CD. تعديل على دليل Kustomize الأساسي يُشغّل مزامنة لجميع الطبقات في 50 خدمة دفعة واحدة. تبدأ 50 عملية نشر متزامنة، تُرهق طاقة جدولة الـpods. الحل: ابدأ بـsyncPolicy.automated.prune: false؛ قيّد تغييرات القاعدة الجماعية خلف بيان طرح مرحلي.
  • تأخر سحب الصورة يحجب الطرح. صورة Java بحجم 1.2 GB تستغرق 4 دقائق للسحب على عقدة باردة، مما يجعل pods الطرح تتجاوز مهلة جاهزية readiness probe وتُشغّل تراجعًا رغم سلامة التطبيق. الحل: طبّق حدًا أقصى لحجم الصورة أقل من 200 MB في CI؛ استخدم multi-stage builds بقوة؛ فعّل containerd image streaming على فئة عقدتك للصور الكبيرة.
  • إيجابية زائفة في قوالب التحليل بسبب حركة canary الشحيحة. حركة المرور منخفضة جدًا في canary (5% من 40 pod = 2 pods) لدرجة أن الضوضاء الإحصائية تُفشل عتبة 99.5%. الحل: استخدم حارسًا بحد أدنى من الطلبات في PromQL — لا تُقيّم المقياس حتى تُلاحَظ على الأقل 100 طلب في الفترة الزمنية.
لا توجّه حركة canary استنادًا إلى نسبة الـpods وحدها في بيئات Istio/Envoy. بدون وزن VirtualService صريح، يوزّع round-robin الافتراضي في Kubernetes حركة المرور بنسبة تعكس عدد نقاط النهاية الجاهزة، لا الوزن المقصود. إذا كان Deployment الثابت لديك يحتوي على 38 pod والـcanary على 2، ستحصل تقريبًا على 5% — لكن انهيار pod واحد في أي مجموعة يُغيّر النسبة بشكل ملحوظ. اربط دائمًا Argo Rollouts بـIstio VirtualService أو بمجموعة هدف ذات وزن في AWS ALB.

اعتبارات الحجم وإنتاجية خط الأنابيب

فريق من 10 مهندسين قد ينفّذ 15 نشرًا يوميًا. منصة تخدم 300 فريق خدمات ستحتاج إلى 500+ نشر يوميًا. الفوارق المعمارية جوهرية. عند 500 نشر يومي، تصبح طاقة منفّذي CI مصدر قلق من الدرجة الأولى من حيث التكلفة والزمن. منفّذو GitHub Actions المستضافون لديهم زمن بدء بارد يتراوح بين 30 و60 ثانية لكل وظيفة — اضرب ذلك في الوظائف المتوازية على نطاق واسع وسيهيمن وقت الانتظار على مدة خط الأنابيب. تشغّل الشركات الكبرى أساطيل منفّذين ذاتيَّي الاستضافة (Actions Runner Controller على Kubernetes، أو منفّذون AWS CodeBuild) للقضاء على البدء البارد والتحكم في فئة المنفّذ.

يتطلب Argo CD على نطاق واسع تصميمًا دقيقًا لـApplicationSet وApp-of-Apps. يمكن لمثيل Argo CD واحد إدارة ما يصل إلى ~2,000 تطبيق قبل أن يصبح استهلاك ذاكرة المتحكم وحمل مراقبة خادم API مشكلة. وراء ذلك، قسّم متحكمات Argo CD باستخدام وضع التشظية ARGOCD_CONTROLLER_REPLICAS، أو انقسم إلى مثيلات Argo CD متعددة مُفَدرَلة عبر ApplicationSets مع مولّد مجموعة.