أساسيات التكامل المستمر

مشروع: تصميم خط أنابيب CI

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

مشروع: تصميم خط أنابيب CI

أفضل طريقة لترسيخ كل ما تعلمته عن التكامل المستمر هي تصميم خط أنابيب كامل من الصفر — ليس مثالًا بسيطًا، بل مواصفة بمستوى إنتاجي لخدمة حقيقية. تأخذك هذه الدرس في تمرين التصميم هذا من البداية للنهاية: ستختار خدمة نموذجية، وتحدد وظائف خط الأنابيب، وتربطها معًا، وتضيف بوابات الأمان، وتعالج الأسرار، وتنتج مواصفة يعترف بها مهندس كبير في Google أو GitHub باعتبارها جاهزة للإنتاج.

الخدمة النموذجية

سنصمم خط أنابيب CI لـ OrderService، وهي خدمة مصغرة مكتوبة بـ Go تعرض REST API، وتكتب إلى PostgreSQL، وتنشر الأحداث على موضوع Kafka، وتُنشر كحاوية على Kubernetes. هذا المكدس تمثيلي للخدمات الخلفية التي ستواجهها على نطاق واسع. هيكل المستودع:

order-service/ ├── cmd/orderservice/main.go ├── internal/ │ ├── handler/ # HTTP handlers │ ├── store/ # Postgres queries │ └── event/ # Kafka publisher ├── migrations/ # SQL migration files ├── k8s/ # Kubernetes manifests ├── Dockerfile ├── docker-compose.yml # local dev + CI integration env ├── Makefile ├── go.mod └── go.sum

أهداف خط الأنابيب

قبل كتابة سطر YAML واحد، سجّل أهدافك. كل مرحلة تضيفها يجب أن تخدم هدفًا واحدًا على الأقل من هذه الأهداف:

  1. تغذية راجعة سريعة — يعلم المطورون في غضون 5 دقائق ما إذا كان تغييرهم صحيحًا.
  2. بيئة متسقة — البناء محكم الحدود؛ ينتج نفس المخرج بغض النظر عمن يُشغّله.
  3. بوابات الأمان — لا تسرب للأسرار؛ جميع التبعيات مفحوصة للثغرات؛ مصادقة SLSA مرفقة.
  4. مخرج قابل للنشر — المخرج النهائي لخط الأنابيب هو صورة OCI مرفوعة إلى السجل، موسومة بـ SHA الخاص بالـ commit، جاهزة للاستلام من قِبَل CD.
صمِّم قبل أن تُطبِّق. الفرق التي تقفز مباشرة إلى كتابة YAML تنتهي بخطوط أنابيب هشة وبطيئة ومتكررة. جلسة تصميم لمدة 30 دقيقة — للإجابة على "ماذا نحتاج أن نتحقق، وبأي ترتيب، وبأي سرعة؟" — ستوفر أسابيعًا من إطفاء حرائق خط الأنابيب لاحقًا.

تصميم خط الأنابيب مرحلة بمرحلة

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

OrderService CI pipeline dependency graph Validate lint · vet · fmt Build go build · binary Unit Test go test · coverage Security Scan govulncheck · trivy Integration Test Postgres · Kafka Publish docker push · SLSA ~45 ث ~60 ث ~90 ث ~80 ث ~2 د ~90 ث إجمالي وقت الجدار: ~5 د 30 ث (المسار الحرج عبر Integration Test)
خط أنابيب CI لـ OrderService: Validate وBuild مرحلتان متسلسلتان للتحقق؛ Unit Test وIntegration Test وSecurity Scan تعمل بالتوازي؛ Publish ينتظر الثلاثة جميعها.

المرحلة 1 — Validate (lint، vet، format)

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

# .github/workflows/ci.yml (أعلى الملف — المُشغِّلات والإعدادات الافتراضية) name: CI — OrderService on: push: branches: [main, 'release/**'] pull_request: branches: [main] defaults: run: shell: bash env: GO_VERSION: '1.23.4' # مثبَّت — لا تستخدم 'stable' أو '1.x' IMAGE: ghcr.io/${{ github.repository }} jobs: validate: name: Lint & Vet runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} cache: true - name: golangci-lint uses: golangci/golangci-lint-action@v6 with: version: v1.61.0 # مثبَّت؛ لا تستخدم 'latest' args: --timeout=5m --config=.golangci.yml - name: go vet run: go vet ./...

المرحلة 2 — Build

تنتج مرحلة البناء الملف الثنائي الذي تعتمد عليه كل وظيفة لاحقة. بالنسبة لـ Go، الملف الثنائي المرتبط بشكل ثابت مع SHA الخاص بالـ commit مدمجًا هو المخرج. ارفعه كـ artifact حتى لا تعيد وظيفة integration-test التجميع من المصدر — هذا يوفر الوقت ويضمن أن جميع المراحل تختبر نفس الملف الثنائي بالضبط.

build: name: Build Binary needs: validate runs-on: ubuntu-latest outputs: version: ${{ steps.ver.outputs.sha }} steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} cache: true - name: Compute version id: ver run: | SHA=$(echo "$GITHUB_SHA" | head -c8) echo "sha=${SHA}" >> "$GITHUB_OUTPUT" - name: Build static binary env: CGO_ENABLED: '0' # ثابت كليًا — لا اعتماد على glibc داخل الحاوية run: | go build \ -trimpath \ -ldflags="-s -w -X main.Version=${{ steps.ver.outputs.sha }}" \ -o dist/orderservice \ ./cmd/orderservice - name: Upload binary artifact uses: actions/upload-artifact@v4 with: name: orderservice-bin-${{ steps.ver.outputs.sha }} path: dist/orderservice retention-days: 3 # artifact وسيط؛ TTL قصيرة

المراحل 3 و4 و5 — البوابات المتوازية

بعد البناء، تعمل ثلاث وظائف بالتزامن. كل منها تُعبِّر عن needs: build — ستبدأ GitHub Actions جميعها الثلاث فور نجاح وظيفة البناء. هذا هو التوازي الرئيسي الذي يبقي خط الأنابيب تحت ست دقائق.

Unit Test — تُنزِّل artifact الملف الثنائي، ثم تُشغِّل go test -race -coverprofile=coverage.out ./.... علم -race يُفعِّل كاشف السباق في Go، الذي يكتشف سباقات البيانات بأداء عام قريب من الصفر. التغطية تُرفع كـ artifact CI وتُرسَل أيضًا إلى Codecov. يُطبَّق حدٌّ أدنى للتغطية بنسبة 80%: إذا أظهر go tool cover -func=coverage.out أقل من 80%، تفشل الوظيفة.

Integration Test — تُشغِّل PostgreSQL وKafka عبر Docker Compose باستخدام كتلة services. تُشغِّل ترحيلات SQL، ثم تُنفِّذ مجموعة اختبارات التكامل. هذه هي الوظيفة الوحيدة ذات التبعيات على الخدمات الخارجية — إبقاؤها معزولة في وظيفة واحدة يعني أن الوظيفتين الموازيتين الأخريين لا تتأخران بسبب وقت بدء الحاوية.

Security Scan — تُشغِّل أداتين: govulncheck (تفحص رسم الوحدات البرمجية مقابل قاعدة بيانات ثغرات Go — صفر إيجابيات كاذبة، فقط الثغرات في الكود الذي تستدعيه فعليًا) و trivy (تفحص Dockerfile للثغرات على مستوى نظام التشغيل في الصورة الأساسية). إذا وُجدت أي ثغرة CRITICAL أو HIGH، تفشل الوظيفة وتمنع مرحلة Publish.

unit-test: name: Unit Test needs: build runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} cache: true - name: Run unit tests with race detector run: | go test -race -coverprofile=coverage.out -covermode=atomic ./... COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | tr -d '%') echo "Total coverage: ${COVERAGE}%" if (( $(echo "$COVERAGE < 80" | bc -l) )); then echo "::error::Coverage ${COVERAGE}% is below the 80% gate" exit 1 fi - name: Upload coverage report uses: actions/upload-artifact@v4 with: name: coverage-report path: coverage.out retention-days: 7 integration-test: name: Integration Test needs: build runs-on: ubuntu-latest services: postgres: image: postgres:16-alpine env: POSTGRES_USER: orders POSTGRES_PASSWORD: orders POSTGRES_DB: orders_test options: >- --health-cmd pg_isready --health-interval 5s --health-timeout 5s --health-retries 10 kafka: image: bitnami/kafka:3.7 env: KAFKA_CFG_NODE_ID: '0' KAFKA_CFG_PROCESS_ROLES: controller,broker KAFKA_CFG_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093 KAFKA_CFG_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092 KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 0@localhost:9093 KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} cache: true - name: Run DB migrations env: DATABASE_URL: postgres://orders:orders@localhost:5432/orders_test?sslmode=disable run: go run ./cmd/migrate up - name: Run integration tests env: DATABASE_URL: postgres://orders:orders@localhost:5432/orders_test?sslmode=disable KAFKA_BROKERS: localhost:9092 INTEGRATION: 'true' run: go test -tags=integration -timeout=3m ./... security-scan: name: Security Scan needs: build runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} cache: true - name: govulncheck — Go module vulnerabilities run: | go install golang.org/x/vuln/cmd/govulncheck@latest govulncheck ./... - name: trivy — Dockerfile & OS CVE scan uses: aquasecurity/trivy-action@master with: scan-type: fs scan-ref: . severity: HIGH,CRITICAL exit-code: '1' ignore-unfixed: true

المرحلة 6 — Publish

تعمل وظيفة Publish فقط حين تنجح البوابات الثلاث الموازية جميعها. تبني صورة OCI بـ Docker BuildKit، وتضع عليها ثلاثة وسوم (SHA القصيرة، واسم الفرع، وsemver إذا أُطلِق بوسم)، وترفعها إلى GitHub Container Registry، وترفق مصادقة SLSA. تُسجِّل المصادقة تشغيل سير العمل والـ commit والمدخلات بالضبط — مستوفيةً SLSA المستوى 2 تلقائيًا مع مشغِّلات GitHub المستضافة.

publish: name: Publish Image needs: [unit-test, integration-test, security-scan] runs-on: ubuntu-latest permissions: contents: read packages: write id-token: write # مطلوب لمصادقة SLSA attestations: write steps: - uses: actions/checkout@v4 - name: Log in to GHCR uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract Docker metadata id: meta uses: docker/metadata-action@v5 with: images: ${{ env.IMAGE }} tags: | type=sha,prefix=,format=short # abc1234f — وسم النشر الأساسي type=ref,event=branch # main type=semver,pattern={{version}} # 2.4.1 عند دفع وسم git - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build and push image id: push uses: docker/build-push-action@v6 with: context: . push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max build-args: | VERSION=${{ needs.build.outputs.version }} - name: Attest build provenance (SLSA) uses: actions/attest-build-provenance@v1 with: subject-name: ${{ env.IMAGE }} subject-digest: ${{ steps.push.outputs.digest }} push-to-registry: true - name: Output deploy tag run: | echo "### Artifact ready for CD" >> "$GITHUB_STEP_SUMMARY" echo "Image: \`${{ env.IMAGE }}@${{ steps.push.outputs.digest }}\`" >> "$GITHUB_STEP_SUMMARY"
Secrets and permissions flow in the OrderService pipeline GitHub Secrets GITHUB_TOKEN (auto) SONAR_TOKEN (manual) OIDC Token id-token: write Runner (publish job) env: GITHUB_TOKEN scoped per job SLSA attestation signed with OIDC GHCR Registry ghcr.io / org / image Sigstore / TUF provenance stored تدفق الأسرار بأدنى امتياز — الأسرار لا تظهر أبدًا في السجلات
تُحقَن الأسرار في بيئة بيئة التنفيذ على نطاق الوظيفة؛ رموز OIDC قصيرة العمر ولا تُخزَّن أبدًا؛ مصادقة SLSA تُدفع مباشرةً إلى جذر الثقة في السجل.

تصميم الأسرار

كل سر في خط الأنابيب هذا يتبع مبدأ الامتياز الأدنى. القواعد المطبقة هنا هي نفس القواعد التي تستخدمها Google وGitHub لخطوط أناببيها الداخلية:

  • يُتاح GITHUB_TOKEN تلقائيًا لكل وظيفة وينتهي حين تنتهي الوظيفة. نطاقه مقيد بالأذونات المُعلَنة في كتلة permissions — وظيفة Publish تطلب packages: write و id-token: write؛ الوظائف السابقة لها فقط contents: read.
  • لا يُمرَّر أي سر خارجي (رمز Sonar، webhook Slack، إلخ) إلى وظيفة لا تحتاجه. أعلن الأسرار على مستوى الوظيفة، لا على مستوى سير العمل.
  • لا تُعاد طباعة الأسرار أبدًا، ولا تُستوفَى في عناوين URL، ولا تُخزَّن في متغيرات بيئة قد تظهر في قائمة العمليات. استخدم --password-stdin لتسجيل الدخول إلى docker، لا --password $SECRET.
  • جميع الإجراءات الخارجية مثبَّتة بـ SHA (actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af68)، لا بوسم متغير. يمنع هذا ناشرًا خارجيًا مُخترَقًا من حقن كود خبيث في خط الأنابيب.
لا تستخدم أبدًا pull_request_target مع أذونات الكتابة. هذا الحدث يعمل بأسرار الفرع الأساسي — مما يعني أن PR خبيثًا يمكنه تسريب كل سر في مستودعك إذا منحته أذونات الكتابة. للمساهمين الخارجيين، استخدم pull_request (أسرار للقراءة فقط) واشترط موافقة المشرف لتشغيل الوظائف ذات الامتياز.

ملخص بوابات الجودة

خط الأنابيب المُصمَّم جيدًا له بوابات جودة صريحة وموثَّقة يفهمها الجميع في الفريق. فيما يلي بطاقة البوابات لـ OrderService:

  • lint — صفر تحذيرات golangci-lint (الإعداد في .golangci.yml، مُطبَّق على كل PR)
  • تغطية الوحدات — 80% حد أدنى لإجمالي تغطية الأسطر
  • كاشف السباق — صفر سباقات بيانات (مُطبَّق بعلم -race)
  • التكامل — جميع اختبارات التكامل تنجح مقابل Postgres 16 وKafka 3.7 الحقيقيين
  • الثغرات — صفر CVEs بدرجة HIGH أو CRITICAL في وحدات Go أو الصورة الأساسية
  • قابلية تكرار البناءCGO_ENABLED=0، -trimpath، إصدار Go مثبَّت، digest صورة أساسية مثبَّتة في Dockerfile
  • سلامة المخرج — مصادقة SLSA المستوى 2 مرفقة بكل صورة مرفوعة إلى main
اكتب بوابات الجودة في PIPELINE.md في جذر المستودع. حين تفشل بوابة ويسأل مطور "لماذا يفشل خط الأنابيب عند 78% تغطية؟"، سياسة البوابة الموثَّقة تنهي الجدال فورًا. وثِّق المنطق، لا فقط الرقم: "80% هو الحد الأدنى المطلوب للكشف عن الانحدار في طبقة store، التي لا تحتوي على اختبارات عقد."

أنماط الفشل الشائعة في تصميم خط الأنابيب

  • غياب سلاسل needs — تعمل وظيفة Publish حتى حين فشل security-scan لأن المؤلف نسي إدراجه في needs. أدرج دائمًا كل وظيفة بوابة صراحةً في مصفوفة needs للمرحلة الأخيرة.
  • بيانات اعتماد مضمَّنة في YAML — خطأ شائع حين يُعمَل بسرعة. افحص كل سير عمل جديد بحثًا عن أسرار حرفية قبل الدمج.
  • اختبارات تكامل غير مستقرة — اختبار يعتمد على توقيت Kafka ويفشل أحيانًا سيُقوِّض الثقة في CI حتى يبدأ المطورون بإعادة تشغيل الوظائف دون تحقيق. أصلح الاختبارات غير المستقرة فورًا؛ عاملها كأخطاء برمجية، لا مجرد إزعاج.
  • لا استراتيجية تخزين مؤقت — إعادة تنزيل وحدات Go وإعادة بناء طبقات Docker من الصفر في كل تشغيل قد تضيف 3-4 دقائق من وقت الانتظار. استخدم cache: true في setup-go و cache-from: type=gha في BuildKit.
  • أذونات واسعة النطاق — منح contents: write لكل وظيفة لأن وظيفة واحدة تحتاج دفع وسم. خصِّص الأذونات بالحد الأدنى الذي تحتاجه كل وظيفة.