النظرية وحدها لا تكفي. في هذا الدرس الأخير نجمع كل ما غطيناه عبر الدروس — صياغة Declarative، والـ agents، والمكتبات المشتركة، وبيانات الاعتماد، والمشغلات المعتمدة على الفروع — في ملف Jenkinsfile واحد جاهز للإنتاج يمكنك إدراجه في أي مستودع microservice حقيقي اليوم. سنستعرض أيضًا القرارات الكامنة وراء كل مقطع حتى تفهم لماذا كُتب بهذه الطريقة، لا فقط ما الذي يفعله.
معمارية الهدف
خدمتنا المثالية هي Java Spring Boot API. على الـ Pipeline أن:
تشغّل اختبارات الوحدة والتكامل بالتوازي عند كل commit.
تبني Docker image موسومة بـ Git commit SHA.
ترسل الصورة إلى سجل حاويات داخلي (AWS ECR في مثالنا).
تنشر تلقائيًا على مجموعة Kubernetes للاختبار عند وصول commits إلى main.
تشترط بوابة موافقة يدوية قبل الترقية إلى الإنتاج.
ترسل إشعار Slack عند النجاح والفشل.
تسحب جميع بيانات الاعتماد من Jenkins Credentials Store — صفر أسرار في الـ Jenkinsfile.
الـ Pipeline المؤسسية: اختبارات متوازية، بناء صورة ومسح أماني، نشر مقيَّد بالفرع لبيئة الاختبار، بوابة موافقة يدوية قبل الإنتاج.
إعداد المكتبة المشتركة
قبل كتابة الـ Jenkinsfile، نُهيئ المكتبة المشتركة (تناولناها في الدرس 6) التي توفر ثلاث دوال قابلة لإعادة الاستخدام: dockerBuild()، وhelmDeploy()، وnotifySlack(). تقع هذه الدوال في مستودع jenkins-shared-lib الخاص بالمؤسسة وتُسجَّل عالميًا في Manage Jenkins → Configure System → Global Pipeline Libraries تحت الاسم acme-shared.
لماذا مكتبة مشتركة لثلاث دوال فقط؟ في مؤسسة حقيقية تضم عشرات الـ microservices، كل فريق سينسخ ويلصق نفس أوامر تسجيل دخول Docker ورفع Helm وإرسال Slack. عندما يتغير مسار Slack API أو أمر ECR، تُصلحه في مكان واحد ويلتقطه كل pipeline في التشغيل التالي — دون لمس 40 ملف Jenkinsfile.
الـ Jenkinsfile المؤسسي الكامل
@Library('acme-shared@main') _ // استيراد المكتبة المشتركة، تثبيت على main
pipeline {
agent none // لا agent عالمي — كل stage يُعلن عن agent خاص به لتوفير الـ executors
options {
buildDiscarder(logRotator(numToKeepStr: '30')) // احتفظ بـ 30 build من التاريخ
timestamps() // أضف طابعًا زمنيًا لكل سطر
timeout(time: 45, unit: 'MINUTES') // اقتل الـ builds المتعثرة
disableConcurrentBuilds(abortPrevious: true) // build واحد لكل فرع في الوقت ذاته
}
environment {
APP_NAME = 'payments-service'
ECR_REGISTRY = '123456789012.dkr.ecr.us-east-1.amazonaws.com'
IMAGE_NAME = "${ECR_REGISTRY}/${APP_NAME}"
IMAGE_TAG = "${GIT_COMMIT[0..7]}" // أول 8 أحرف من commit SHA
K8S_NS_STG = 'staging'
K8S_NS_PROD = 'production'
}
stages {
// ── 1. CHECKOUT ────────────────────────────────────────────────────────
stage('Checkout') {
agent { label 'builder' }
steps {
checkout scm
script {
env.GIT_COMMIT_FULL = sh(script: 'git rev-parse HEAD', returnStdout: true).trim()
env.GIT_BRANCH_SAFE = env.BRANCH_NAME.replaceAll('[^a-zA-Z0-9_.-]', '-')
}
stash name: 'source', includes: '**' // تخزين الكود ليصله agents أخرى
}
}
// ── 2. PARALLEL TESTS ──────────────────────────────────────────────────
stage('Tests') {
parallel {
stage('Unit Tests') {
agent { label 'builder' }
steps {
unstash 'source'
sh './mvnw test -Dgroups=unit -T 4' // 4 خيوط، مجموعة unit فقط
}
post {
always {
junit 'target/surefire-reports/**/*.xml'
publishCoverage adapters: [jacocoAdapter('target/site/jacoco/jacoco.xml')]
}
}
}
stage('Integration Tests') {
agent { label 'builder' }
steps {
unstash 'source'
sh './mvnw verify -Dgroups=integration -DskipUnitTests'
}
post {
always { junit 'target/failsafe-reports/**/*.xml' }
}
}
stage('Static Analysis') {
agent { label 'builder' }
steps {
unstash 'source'
sh './mvnw checkstyle:check spotbugs:check pmd:check'
}
}
}
}
// ── 3. BUILD DOCKER IMAGE ──────────────────────────────────────────────
stage('Build & Scan Image') {
agent { label 'docker' } // agent مخصص يحمل Docker daemon
steps {
unstash 'source'
script {
// استدعاء المكتبة المشتركة — تتولى تسجيل الدخول إلى ECR داخليًا
dockerBuild(
image: env.IMAGE_NAME,
tag: env.IMAGE_TAG,
args: "--build-arg APP_VERSION=${env.IMAGE_TAG} --cache-from ${env.IMAGE_NAME}:latest"
)
}
// مسح Trivy للثغرات — يُفشل البناء عند وجود HIGH أو CRITICAL
sh """
trivy image \\
--exit-code 1 \\
--severity HIGH,CRITICAL \\
--ignore-unfixed \\
${IMAGE_NAME}:${IMAGE_TAG}
"""
}
}
// ── 4. PUSH TO ECR ────────────────────────────────────────────────────
stage('Push Image') {
agent { label 'docker' }
steps {
withCredentials([[$class: 'AmazonWebServicesCredentialsBinding',
credentialsId: 'aws-ecr-push']]) {
sh """
aws ecr get-login-password --region us-east-1 \\
| docker login --username AWS --password-stdin ${ECR_REGISTRY}
docker push ${IMAGE_NAME}:${IMAGE_TAG}
docker tag ${IMAGE_NAME}:${IMAGE_TAG} ${IMAGE_NAME}:latest
docker push ${IMAGE_NAME}:latest
"""
}
}
}
// ── 5. DEPLOY TO STAGING (main branch only) ───────────────────────────
stage('Deploy → Staging') {
when {
branch 'main'
not { changeRequest() }
}
agent { label 'k8s-deployer' }
steps {
script {
helmDeploy(
release: env.APP_NAME,
chart: 'charts/payments-service',
namespace: env.K8S_NS_STG,
values: "deploy/values-staging.yaml",
set: "image.tag=${env.IMAGE_TAG}"
)
}
sh "curl --retry 5 --retry-delay 5 --fail https://staging.acme.internal/payments/health"
}
}
// ── 6. MANUAL GATE → PRODUCTION ───────────────────────────────────────
stage('Approve Production') {
when { branch 'main' }
steps {
timeout(time: 24, unit: 'HOURS') {
input message: "Deploy ${env.APP_NAME}:${env.IMAGE_TAG} to PRODUCTION?",
ok: 'Ship it',
submitter: 'release-team' // أعضاء مجموعة release-team فقط
}
}
}
// ── 7. DEPLOY TO PRODUCTION ───────────────────────────────────────────
stage('Deploy → Production') {
when { branch 'main' }
agent { label 'k8s-deployer' }
steps {
script {
helmDeploy(
release: env.APP_NAME,
chart: 'charts/payments-service',
namespace: env.K8S_NS_PROD,
values: "deploy/values-production.yaml",
set: "image.tag=${env.IMAGE_TAG}",
atomic: true, // rollback تلقائي عند الفشل
timeout: '5m'
)
}
}
}
}
// ── POST: NOTIFICATIONS ───────────────────────────────────────────────────
post {
success {
script {
notifySlack(
channel: '#deployments',
color: 'good',
message: ":white_check_mark: *${env.APP_NAME}* `${env.IMAGE_TAG}` deployed successfully\n<${env.BUILD_URL}|Build #${env.BUILD_NUMBER}>"
)
}
}
failure {
script {
notifySlack(
channel: '#deployments',
color: 'danger',
message: ":x: *${env.APP_NAME}* `${env.IMAGE_TAG}` FAILED on `${env.BRANCH_NAME}`\n<${env.BUILD_URL}console|View logs>"
)
}
}
always {
cleanWs() // تنظيف مساحة العمل لمنع تسرب الـ artifacts بين البناءات
}
}
}
شرح قرارات التصميم الرئيسية
agent none على المستوى العام. عدم تعريف agent عالمي يُجبر كل stage على طلب executor فقط أثناء تشغيله. مع agent عالمي، يحتجز Jenkins executor طوال البناء — بما يشمل انتظار input الذي قد يمتد لساعات. سيُنهك ذلك مجموعة الـ executors لديك. الـ agents على مستوى الـ stage تحرر الـ slot فور انتهاء الـ stage.
disableConcurrentBuilds(abortPrevious: true). في فرع نشط، قد يدفع مطور commit-ين متتاليين. دون هذا الخيار، يعمل كلا البناءين في آن واحد، وقد ينتهي الأقدم آخرًا فينشر كودًا قديمًا. abortPrevious: true يلغي البناء الجاري فور وضع بناء أحدث في قائمة الانتظار لنفس الفرع.
Stash / unstash للكود المصدري. بما أننا نستخدم agent none، قد يعمل كل stage على خادم agent مختلف. يؤرشف stash مساحة العمل إلى Jenkins controller بعد الـ checkout؛ وكل stage يحتاج الكود يستدعي unstash لاستعادته. للمستودعات الكبيرة، ادمج stash مع NFS مشترك أو استخدم Artifact Caching Plugin لتجنب إرسال غيغابايتات عبر الشبكة في كل stage.
Trivy داخل الـ pipeline لا كـ pre-commit hook. الـ pre-commit hooks تعمل على أجهزة المطورين ويمكن تجاوزها. تشغيل Trivy داخل Jenkins يعني أن كل صورة تصل إلى السجل قد مُسحت بقاعدة بيانات CVE الحالية — لا قاعدة بيانات مؤرشفة منذ ستة أشهر على جهاز شخص ما.
atomic: true في نشر Helm للإنتاج. هذا العلم يطلب من Helm التراجع عن النشر تلقائيًا إذا لم تصل الـ pods الجديدة إلى حالة Running ضمن نافذة المهلة. دونه، تترك صورة معطوبة الـ release في حالة deployed يعتبرها Helm ناجحة رغم أن pods-ك تعيد التشغيل باستمرار.
سمِّ الصور بـ Git SHA الكامل، لا باسم الفرع أو latest. أسماء الفروع وlatest قابلة للتغيير — سحب نفس الوسم غدًا قد يعطيك صورة مختلفة. الـ SHA ثابت ويربط كل حاوية جارية بسطر محدد في تاريخ الكود. مهندسو الإنتاج المناوبون والمحققون في الحوادث سيشكرونك على ذلك.
نظيرات المكتبة المشتركة
تقع استدعاءات المكتبة الثلاثة أعلاه في vars/dockerBuild.groovy وvars/helmDeploy.groovy وvars/notifySlack.groovy في مستودع jenkins-shared-lib. ملف helmDeploy.groovy المبسَّط يبدو هكذا:
خطوة input تحجز executor إذا لم تستخدم agent none. نافذة موافقة مدتها 24 ساعة مع agent { label 'builder' } عالمي ستحتجز builder slot كاملًا ليوم. هذه إحدى أكثر حالات استنزاف موارد Jenkins شيوعًا في البيئات المؤسسية. دائمًا أعلن agent none على مستوى الـ pipeline وخصص agents فقط على مستوى الـ stage.
التشغيل لأول مرة
ضع هذا الـ Jenkinsfile في جذر المستودع، ثم أنشئ Multibranch Pipeline يشير إلى المستودع (تناولناه في الدرس 8). يكتشف Jenkins كل فرع يحتوي الملف ويُنشئ له job ويشغّل الـ pipeline. أول تشغيل على main سيصطدم ببوابة input — انقر Approve Production في واجهة البناء أو اتركها تنتهي بمهلة 24 ساعة آمنة. كل push تالٍ على أي فرع يشغّل الاختبارات ويبني صورة؛ فقط الـ pushes إلى main تتجاوز إلى مراحل أبعد.
يمثل هذا الـ Jenkinsfile المعيار الإنتاجي المعتمد في معظم شركات التقنية الكبرى. قد تختلف الأدوات المحددة — ECR مقابل GCR مقابل Harbor، وHelm مقابل kubectl المباشر، وSlack مقابل Teams — من مؤسسة لأخرى، لكن الأنماط البنيوية عالمية: اختبارات متوازية، وسوم صور ثابتة، إدارة بيانات اعتماد مركزية، نشر مقيَّد بالفرع، بوابات موافقة يدوية، وتنظيف عند كل خروج. أتقن هذا الهيكل وستتمكن من تكييفه مع أي تقنية في أي شركة.
نستخدم ملفات تعريف الارتباط لتشغيل هذا الموقع وتحليل الزيارات وعرض إعلانات مخصّصة. يمكنك قبول كل ملفات تعريف الارتباط أو رفض غير الأساسية منها.
سياسة الخصوصية