GitHub Actions بعمق

مشروع: سير عمل CI/CD متكامل

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

مشروع: سير عمل CI/CD متكامل

كان كل درس في هذه السلسلة لبنةً بناء منفردة. الآن حان وقت تجميعها في خط أنابيب CI/CD جاهز للإنتاج — من النوع الذي تستخدمه شركات مثل Shopify وStripe وGitHub نفسها. يأخذك هذا الدرس خطوةً بخطوة في تصميم وكتابة وتشغيل سير عمل متكامل يبني ويختبر ويعبّئ وينشر تطبيق Node.js في حاوية إلى بيئة سحابية، مع حماية كل مرحلة بفحوصات جودة آلية وضوابط موافقة.

البنية المستهدفة

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

Complete CI/CD pipeline stages Lint & Test Unit tests Build Image docker build Scan & Push Trivy + GHCR Deploy Staging smoke-test gate Approve Manual gate Deploy Prod kubectl rollout Push / PR Trigger on:release
خط أنابيب CI/CD الشامل: كل مرحلة عبارة عن وظيفة ذات حماية؛ والنشر الإنتاجي يستلزم موافقة يدوية.

ملف سير العمل الكامل

تعيش المراحل الخمس جميعها في ملف سير عمل واحد. يفرض المفتاح needs سلسلة التبعيات؛ وتضيف كتلة environment: production بوابة الموافقة. لاحظ أن IMAGE_TAG مشتق من SHA الخاص بـ Git — ثابت غير قابل للتغيير، قابل للتتبع، ولا يمكن الكتابة فوقه بالخطأ.

# .github/workflows/cicd.yml name: CI/CD Pipeline on: push: branches: [main] pull_request: branches: [main] release: types: [published] env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: # ── 1. LINT & TEST ────────────────────────────────────────────── test: name: Lint & Unit Tests runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: "20" cache: "npm" - run: npm ci - name: Lint run: npm run lint - name: Unit tests with coverage run: npm test -- --coverage --coverageThreshold='{"global":{"lines":80}}' - uses: actions/upload-artifact@v4 if: always() with: name: coverage-report path: coverage/ retention-days: 7 # ── 2. BUILD IMAGE ──────────────────────────────────────────────── build-image: name: Build Docker Image runs-on: ubuntu-24.04 needs: test outputs: image-digest: ${{ steps.build.outputs.digest }} image-tag: ${{ steps.meta.outputs.tags }} permissions: contents: read packages: write steps: - uses: actions/checkout@v4 - uses: docker/setup-buildx-action@v3 - uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=sha,prefix=sha-,format=short type=ref,event=branch type=semver,pattern={{version}} - name: Build & push (cache-optimised) id: build uses: docker/build-push-action@v5 with: context: . push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max provenance: true sbom: true # ── 3. SCAN & SIGN ────────────────────────────────────────────── scan: name: CVE Scan runs-on: ubuntu-24.04 needs: build-image permissions: contents: read packages: read security-events: write steps: - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@master with: image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ needs.build-image.outputs.image-digest }} format: sarif output: trivy-results.sarif severity: CRITICAL,HIGH exit-code: "1" - uses: github/codeql-action/upload-sarif@v3 if: always() with: sarif_file: trivy-results.sarif # ── 4. DEPLOY STAGING ──────────────────────────────────────────── deploy-staging: name: Deploy to Staging runs-on: ubuntu-24.04 needs: scan environment: staging if: github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v4 - uses: azure/setup-kubectl@v4 with: version: "v1.30.0" - name: Authenticate to cluster run: | mkdir -p ~/.kube echo "${{ secrets.STAGING_KUBECONFIG }}" | base64 -d > ~/.kube/config chmod 600 ~/.kube/config - name: Rolling update run: | IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ needs.build-image.outputs.image-digest }}" kubectl set image deployment/api api="$IMAGE" -n staging kubectl rollout status deployment/api -n staging --timeout=120s - name: Smoke test run: | STAGING_URL="https://staging.api.example.com" for i in 1 2 3 4 5; do STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$STAGING_URL/healthz") [ "$STATUS" = "200" ] && echo "Smoke test passed" && exit 0 echo "Attempt $i: got $STATUS, retrying in 10s..." sleep 10 done echo "Smoke test failed after 5 attempts" && exit 1 # ── 5. DEPLOY PRODUCTION ───────────────────────────────────────── deploy-production: name: Deploy to Production runs-on: ubuntu-24.04 needs: deploy-staging environment: production if: github.event_name == 'release' steps: - uses: actions/checkout@v4 - uses: azure/setup-kubectl@v4 with: version: "v1.30.0" - name: Authenticate to production cluster run: | mkdir -p ~/.kube echo "${{ secrets.PROD_KUBECONFIG }}" | base64 -d > ~/.kube/config chmod 600 ~/.kube/config - name: Blue/green cutover run: | IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ needs.build-image.outputs.image-digest }}" kubectl set image deployment/api api="$IMAGE" -n production kubectl rollout status deployment/api -n production --timeout=300s - name: Tag deployment in Datadog env: DD_API_KEY: ${{ secrets.DATADOG_API_KEY }} run: | curl -s -X POST "https://api.datadoghq.com/api/v1/events" \ -H "DD-API-KEY: $DD_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"title\":\"Deployed ${{ github.ref_name }}\",\"text\":\"SHA ${{ github.sha }}\",\"tags\":[\"env:production\"]}"

Dockerfile الذي يجعل كل شيء يعمل

خط الأنابيب لا يكون أفضل من الـ Dockerfile الذي يبنيه. استخدم البناء متعدد المراحل للحفاظ على صغر حجم الصورة النهائية، ولا تشغّل التطبيق بصلاحيات root في الإنتاج. إن تعليمات --chown وUSER node غير قابلة للتفاوض في شركات التقنية الكبرى.

# Dockerfile FROM node:20-alpine AS deps WORKDIR /app COPY package*.json ./ RUN npm ci --omit=dev FROM node:20-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN npm run build FROM node:20-alpine AS runner WORKDIR /app RUN addgroup --system --gid 1001 nodejs \ && adduser --system --uid 1001 nodeuser COPY --from=builder --chown=nodeuser:nodejs /app/dist ./dist COPY --from=deps --chown=nodeuser:nodejs /app/node_modules ./node_modules USER nodeuser EXPOSE 3000 HEALTHCHECK --interval=15s --timeout=5s --start-period=10s --retries=3 \ CMD wget -qO- http://localhost:3000/healthz || exit 1 CMD ["node", "dist/server.js"]

القرارات التصميمية الرئيسية موضّحةً

هوية الصورة عبر الـ digest لا الوسم

تكشف وظيفة build-image عن digest الصورة (وهو hash SHA-256 لمحتواها) كمخرج. كل وظيفة تالية تشير إلى الصورة عبر هذا الـ digest لا عبر وسم قابل للتغيير كـ :latest. يضمن ذلك أن البايناري الذي نشرته في التجربة هو بالضبط ما يصل إلى الإنتاج — لا سباقات كتابة فوق الوسم، ولا انحراف بين البيئات.

فحص الثغرات بوابةً صارمة

يعمل Trivy مع exit-code: 1، أي أن أي ثغرة CRITICAL أو HIGH ستُفشل وظيفة scan وتمنع انطلاق نشر التجربة والإنتاج كليهما. تُرفع نتائج SARIF إلى تبويب Security في GitHub ليتمكن المهندسون من المراجعة دون مغادرة GitHub.

بيئة الموافقة

تعليمة environment: production تربط الوظيفة ببيئة GitHub مهيأة بـ Required Reviewers. عند بلوغ سير العمل تلك الوظيفة يتوقف وتُرسل GitHub إشعاراً للمراجعين المحددين. لا تعديل في الكود لإضافة المراجعين أو إزالتهم — يُدار الأمر كله من واجهة إعدادات المستودع ومحفوظ بالكامل في سجل التدقيق.

الشرط if على وظيفة deploy-production بالغ الأهمية. بدون if: github.event_name == 'release'، كل عملية دمج في main ستضع نشراً إنتاجياً في طابور الانتظار. يجب أن يفتح باب الإنتاج إصدارُ GitHub Release فحسب. النشر في التجربة يتم عند كل دمج في main؛ أما النشر الإنتاجي فيتم عند حدث الإصدار فقط.

استراتيجية التراجع

كل صورة مرفوعة ثابتة وموسومة بـ SHA. التراجع يكون بأمر واحد: ابحث في واجهة Actions عن آخر تشغيل ناجح، انسخ الـ digest الخاص به وأعد التشغيل، أو ببساطة:

# التراجع: إعادة صورة الـ deployment إلى الـ digest السابع المعروف بأنه سليم kubectl set image deployment/api \ api=ghcr.io/your-org/your-repo@sha256:<previous-digest> \ -n production kubectl rollout status deployment/api -n production --timeout=300s # التحقق kubectl get pods -n production -l app=api -o wide
ثبّت إجراءات GitHub على commit SHA في خطوط الأنابيب الإنتاجية. استخدام actions/checkout@v4 مريح لكن actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 ثابت غير قابل للتغيير. يمكن لمهاجم اختراق وسم إجراء وتحديث محتواه دون تغيير الرقم؛ أما الـ SHA المثبت فلا يمكن تغييره. أدوات مثل Dependabot وRenovate تحافظ على تحديث هذه التثبيتات آلياً.

أنماط الفشل الشائعة في الإنتاج

  • انتهاء مهلة النشر قبل أن تصبح Pods في وضع صحي — مسبار الجاهزية يفشل؛ تحقق من سجلات التطبيق بـ kubectl logs -n staging -l app=api --since=2m قبل إلقاء اللوم على خط الأنابيب.
  • تذبذب اختبار الدخان — حلقة إعادة المحاولة في المثال تعالج بدء تشغيل موازن الحمل المتأخر؛ اضبط مدة الانتظار وفق وقت الإقلاع في بنيتك التحتية.
  • يوقف Trivy بسبب نتيجة إيجابية خاطئة — استخدم .trivyignore لتجاهل معرفات CVE محددة مع تعليق يوضح القرار وتاريخ المراجعة.
  • GITHUB_TOKEN يفتقر إلى packages: write — يجب تعريف الصلاحية على مستوى الوظيفة، لا مجرد افتراضها. لهذا أُضيفت صراحةً في build-image.
  • انتهاء صلاحية kubeconfig — جدّد أسرار STAGING_KUBECONFIG وPROD_KUBECONFIG عند انتهاء رموز حسابات الخدمة. ضع تذكيراً في التقويم أو استخدم اتحاد OIDC بدلاً من ذلك (تغطى في الدرس الثامن).
لا تضع بيانات اعتماد المجموعة في متغيرات بيئة سير العمل المرئية في السجلات. ثبّت دائماً بفك تشفير من سر base64 مباشرةً إلى ملف (echo "$SECRET" | base64 -d > ~/.kube/config) وعيّن صلاحيات صارمة (chmod 600). تخفي GitHub قيم الأسرار في السجلات، لكن echo $SECRET الصريح قد يسرّب جزءاً من القيمة في بعض البيئات. النمط في هذا الدرس هو الأسلوب الآمن.

ما يمكن إضافته لاحقاً

هذا خط الأنابيب أساس متين. في قاعدة كود شركة حقيقية ستضيف: اختبارات التكامل والنهاية إلى النهاية كوظائف متوازية بين test وbuild-image؛ وظائف ترحيل قاعدة البيانات مع بوابة تشغيل جاف؛ إشعارات Slack / PagerDuty عند الفشل باستخدام خطوات if: failure()؛ وإرسال مقاييس DORA (تكرار النشر، وقت الاستيفاء) إلى منصة الرصد والمراقبة. تتوسع هذه البنية لأن كل اهتمام وظيفته الخاصة — إضافة بوابة جديدة مسألة إدراج وظيفة بسلسلة needs الصحيحة.