أفضل طريقة لترسيخ كل ما تعلمته عن التكامل المستمر هي تصميم خط أنابيب كامل من الصفر — ليس مثالًا بسيطًا، بل مواصفة بمستوى إنتاجي لخدمة حقيقية. تأخذك هذه الدرس في تمرين التصميم هذا من البداية للنهاية: ستختار خدمة نموذجية، وتحدد وظائف خط الأنابيب، وتربطها معًا، وتضيف بوابات الأمان، وتعالج الأسرار، وتنتج مواصفة يعترف بها مهندس كبير في Google أو GitHub باعتبارها جاهزة للإنتاج.
الخدمة النموذجية
سنصمم خط أنابيب CI لـ OrderService، وهي خدمة مصغرة مكتوبة بـ Go تعرض REST API، وتكتب إلى PostgreSQL، وتنشر الأحداث على موضوع Kafka، وتُنشر كحاوية على Kubernetes. هذا المكدس تمثيلي للخدمات الخلفية التي ستواجهها على نطاق واسع. هيكل المستودع:
قبل كتابة سطر YAML واحد، سجّل أهدافك. كل مرحلة تضيفها يجب أن تخدم هدفًا واحدًا على الأقل من هذه الأهداف:
تغذية راجعة سريعة — يعلم المطورون في غضون 5 دقائق ما إذا كان تغييرهم صحيحًا.
بيئة متسقة — البناء محكم الحدود؛ ينتج نفس المخرج بغض النظر عمن يُشغّله.
بوابات الأمان — لا تسرب للأسرار؛ جميع التبعيات مفحوصة للثغرات؛ مصادقة SLSA مرفقة.
مخرج قابل للنشر — المخرج النهائي لخط الأنابيب هو صورة OCI مرفوعة إلى السجل، موسومة بـ SHA الخاص بالـ commit، جاهزة للاستلام من قِبَل CD.
صمِّم قبل أن تُطبِّق. الفرق التي تقفز مباشرة إلى كتابة YAML تنتهي بخطوط أنابيب هشة وبطيئة ومتكررة. جلسة تصميم لمدة 30 دقيقة — للإجابة على "ماذا نحتاج أن نتحقق، وبأي ترتيب، وبأي سرعة؟" — ستوفر أسابيعًا من إطفاء حرائق خط الأنابيب لاحقًا.
تصميم خط الأنابيب مرحلة بمرحلة
يحتوي خط الأنابيب على ست وظائف. ليست كلها متسلسلة — التوازي هو مفتاح تحقيق هدف الخمس دقائق. فيما يلي رسم بياني كامل للتبعيات:
خط أنابيب 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 التجميع من المصدر — هذا يوفر الوقت ويضمن أن جميع المراحل تختبر نفس الملف الثنائي بالضبط.
بعد البناء، تعمل ثلاث وظائف بالتزامن. كل منها تُعبِّر عن 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 المستضافة.
تُحقَن الأسرار في بيئة بيئة التنفيذ على نطاق الوظيفة؛ رموز 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 لكل وظيفة لأن وظيفة واحدة تحتاج دفع وسم. خصِّص الأذونات بالحد الأدنى الذي تحتاجه كل وظيفة.
نستخدم ملفات تعريف الارتباط لتشغيل هذا الموقع وتحليل الزيارات وعرض إعلانات مخصّصة. يمكنك قبول كل ملفات تعريف الارتباط أو رفض غير الأساسية منها.
سياسة الخصوصية