Jenkins والتكامل المستمر المؤسسي

المكتبات المشتركة

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

المكتبات المشتركة

كل مؤسسة هندسية تشغّل Jenkins على نطاق واسع تواجه في نهاية المطاف المشكلة ذاتها: عشرات الفرق تحتفظ كل منها بملف Jenkinsfile خاص بها، وتبدو هذه الملفات متشابهة بصورة لافتة. خطوة تسجيل الدخول إلى Docker مُنسوخة عبر أربعين مستودعاً. أي تغيير في سياسة فحص الثغرات الأمنية للشركة يستلزم تعديل ثلاثين pipeline يدوياً. هذا هو مشكلة تكرار الـ pipeline، وهي الدافع المباشر لاستخدام المكتبات المشتركة في Jenkins.

المكتبة المشتركة هي مستودع Git يحتوي على كود Groovy قابل لإعادة الاستخدام — دوال وكلاسات وقوالب pipeline كاملة — يمكن لأي pipeline على متحكم Jenkins استيرادها بتعليق @Library واحد. تُحمَّل المكتبة مرة واحدة عند وقت التشغيل، وتستهلكها بالطريقة ذاتها كل Jenkinsfile تُعلنها. أصبح الـ pipeline الآن تهيئة، ومعايير المؤسسة تعيش في كود.

المفهوم الجوهري: المكتبة المشتركة ليست اختصاراً للمهندسين الكسالى — إنها الآلية التي يفرض بها فريق المنصة معايير CI/CD على مستوى المؤسسة دون امتلاك كل pipeline بشكل فردي. تُنشَر تغييرات السياسة مرة واحدة على المكتبة، ويلتقطها كل pipeline مستهلك عند البناء التالي دون أي تعديل على Jenkinsfile.

هيكل المستودع

تفرض Jenkins اصطلاحاً صارماً لهيكل مستودعات المكتبات المشتركة. الانحراف عنه يسبب أخطاء تحميل صامتة، لذا استوعب البنية قبل كتابة سطر كود واحد.

my-shared-library/ # جذر مستودع Git ├── vars/ # متغيرات عامة — ملف واحد = خطوة pipeline واحدة │ ├── buildDockerImage.groovy │ ├── runSecurityScan.groovy │ └── deployToKubernetes.groovy ├── src/ # كلاسات Groovy — OOP كامل، تُصرَّف عند التحميل │ └── com/ │ └── acme/ │ ├── Docker.groovy │ └── Kubernetes.groovy ├── resources/ # ملفات غير Groovy: سكريبتات shell، قوالب JSON، مقتطفات YAML │ └── com/ │ └── acme/ │ └── deploy-template.yaml └── README.md

دليل vars/ هو المكان الذي تبدأ منه معظم الفرق. كل ملف .groovy في vars/ يصبح متغيراً عاماً متاحاً لكل pipeline. إذا عرّف الملف دالة call()، يصبح اسم المتغير نفسه خطوة قابلة للاستدعاء. دليل src/ يتبع اصطلاحات حزم Java/Groovy القياسية ويوفر تسلسلاً هرمياً كاملاً للكلاسات والوراثة والأساليب الثابتة للمنطق المعقد.

تسجيل المكتبة في Jenkins

قبل أن يتمكن أي pipeline من استخدام مكتبة، يجب على مسؤول Jenkins تسجيلها ضمن Manage Jenkins → System → Global Pipeline Libraries. إعدادات جوهرية يجب فهمها في الإنتاج:

  • الاسم — المعرّف المستخدم في @Library('my-lib'). استخدم اسماً مستقراً ومحدد النطاق (مثل acme-platform-lib). تغييره لاحقاً يستلزم تعديل كل Jenkinsfile مستهلك.
  • الإصدار الافتراضي — الفرع أو الوسم أو SHA تحميلها عندما يكتب pipeline @Library('acme-platform-lib') دون محدد إصدار. في الإنتاج، أشر إلى وسم إصدار أو فرع main محمي — ليس إلى فرع ميزة غير محمي.
  • التحميل الضمني — إذا فُعِّل، يحمل كل pipeline المكتبة دون تعليق. مفيد لفرض خطوات إلزامية (فحص الأمان، تسجيل التدقيق) التي لا يمكن لأي فريق تخطيها.
  • السماح بتجاوز الإصدار الافتراضي — يسمح للـ pipelines الفردية بتثبيت إصدار محدد عبر @Library('acme-platform-lib@v2.1.0'). ضروري لترقيات المكتبة — تختبر الفرق مقابل الإصدار الجديد قبل أن يصبح افتراضياً.

يمكن أن تصدر المكتبة من أي SCM تدعمه Jenkins: GitHub أو GitLab أو Bitbucket أو رابط Git عادي. في المؤسسات الكبيرة، عادةً ما يمتلك مستودع المكتبة قواعد حماية الفروع ومراجعة الكود الإلزامية وpaipeline CI خاصاً به يشغّل اختبارات الوحدة باستخدام إطار jenkins-pipeline-unit.

كتابة خطوة vars/

النمط الأكثر شيوعاً هو خطوة vars/ تلف إجراء متعدد الخطوات كانت الفرق ستكرره. هذا مثال بجودة إنتاجية: خطوة بناء ودفع Docker تفرض اصطلاح تسمية سجل المؤسسة، وتضخ بيانات الاعتماد من مخزن بيانات اعتماد Jenkins، وتُعلِّم بـ SHA الـ commit وعلامة إصدار دلالية.

// vars/buildAndPushImage.groovy def call(Map config = [:]) { // اشترط المعاملات الإلزامية — افشل بسرعة برسالة واضحة if (!config.imageName) { error "buildAndPushImage: 'imageName' is required" } def registry = config.registry ?: 'registry.acme.internal' def imageTag = config.imageTag ?: env.GIT_COMMIT.take(8) def credId = config.credId ?: 'docker-registry-creds' def dockerfile = config.dockerfile ?: 'Dockerfile' def buildArgs = config.buildArgs ?: [:] def fullImage = "${registry}/${config.imageName}:${imageTag}" def buildArgStr = buildArgs.collect { k, v -> "--build-arg ${k}=${v}" }.join(' ') docker.withRegistry("https://${registry}", credId) { def img = docker.build(fullImage, "${buildArgStr} -f ${dockerfile} .") img.push() img.push('latest') echo "Pushed ${fullImage}" return fullImage } }

يمكن الآن لأي pipeline في أي مستودع استهلاك هذا بأقل قدر من الكود النمطي. اصطلاح السجل وبيانات الاعتماد والتعليم للمؤسسة مُطبَّق في مكان واحد — المكتبة — وليس مبعثراً عبر أربعين Jenkinsfile.

// Jenkinsfile في مستودع تطبيق @Library('acme-platform-lib@v3.2.1') _ pipeline { agent { label 'docker-builder' } stages { stage('Build & Push') { steps { buildAndPushImage( imageName: 'payments-service', buildArgs: [APP_VERSION: env.BUILD_NUMBER] ) } } stage('Deploy') { steps { deployToKubernetes( cluster: 'prod-us-east-1', namespace: 'payments', image: "registry.acme.internal/payments-service:${env.GIT_COMMIT.take(8)}" ) } } } }

قوالب الـ Pipeline: إعادة استخدام Pipeline بالكامل

الاستخدام الأقوى للمكتبات المشتركة ليس الخطوات الفردية بل قوالب pipeline كاملة. يعرّف فريق المنصة pipeline كاملاً ومُوجَّهاً لنوع مشروع (مثل "خدمة Java مصغّرة"، "تطبيق React") داخل المكتبة، وتستدعيه فرق التطبيق كدالة واحدة. يتقلص Jenkinsfile للتطبيق إلى ثلاثة أسطر.

// vars/javaMicroservicePipeline.groovy (قالب pipeline في المكتبة) def call(Map config = [:]) { pipeline { agent { label 'jdk-21' } options { timeout(time: 30, unit: 'MINUTES') buildDiscarder(logRotator(numToKeepStr: '20')) disableConcurrentBuilds() } environment { MAVEN_OPTS = '-Xmx1g -Xms256m' SONAR_URL = 'https://sonar.acme.internal' } stages { stage('Compile') { steps { sh 'mvn -B compile' } } stage('Test') { steps { sh 'mvn -B verify' } post { always { junit 'target/surefire-reports/*.xml' } } } stage('SonarQube Analysis') { when { branch 'main' } steps { withSonarQubeEnv('acme-sonar') { sh 'mvn sonar:sonar' } timeout(time: 5, unit: 'MINUTES') { waitForQualityGate abortPipeline: true } } } stage('Build & Push Image') { steps { buildAndPushImage(imageName: config.serviceName) } } stage('Deploy to Staging') { when { branch 'main' } steps { deployToKubernetes( cluster: 'staging', namespace: config.namespace ?: config.serviceName, image: "registry.acme.internal/${config.serviceName}:${env.GIT_COMMIT.take(8)}" ) } } } post { failure { slackSend channel: config.slackChannel ?: '#ci-alerts', message: "FAILED: ${env.JOB_NAME} #${env.BUILD_NUMBER}" } } } }

Jenkinsfile الكامل لفريق التطبيق أصبح الآن:

@Library('acme-platform-lib@v3.2.1') _ javaMicroservicePipeline(serviceName: 'payments-service', slackChannel: '#payments-team')

المعمارية: كيف تُحمَّل المكتبة

يوضح الرسم البياني أدناه العلاقة في وقت التشغيل بين Jenkinsfile ومستودع المكتبة المشتركة ومتحكم Jenkins.

Jenkins Shared Library Load Flow App Repo Jenkinsfile @Library('acme-lib@v3') 1. Checkout Jenkins Controller Parse @Library annotation Fetch library @ version tag Compile src/ Load vars/ Execute pipeline with loaded steps Library Repo vars/buildAndPushImage vars/deployToKubernetes src/com/acme/*.groovy 2. Clone 3. Classes + steps Build Agent Runs shell steps 4. Dispatch
يحمّل Jenkins المكتبة المشتركة ويصرّفها من مستودع Git الخاص بها قبل تنفيذ أي خطوة في الـ pipeline.

أخطاء الإنتاج الشائعة وأفضل الممارسات

خطأ إنتاجي — تسلسل CPS في Groovy: تعمل pipelines Jenkins في مترجم بأسلوب Continuation Passing Style (CPS)، وليس JVM قياسياً. يجب أن يكون كل متغير pipeline قابلاً للتسلسل إلى القرص (للإيقاف المؤقت والاستئناف). إذا استدعيت نوع Java غير قابل للتسلسل (مثل java.io.File) في خطوة vars/ دون لفّه في دالة @NonCPS، يفشل البناء بخطأ NotSerializableException غير واضح. القاعدة: أي دالة تتعامل مع كائنات غير قابلة للتسلسل يجب أن تُعلَّم بـ @NonCPS، لكنها حينئذٍ لا تستطيع استدعاء خطوات pipeline المحوّلة بـ CPS. احتفظ بحد فاصل صارم بين دوال @NonCPS ودوال الخطوات المدركة لـ CPS.
ممارسة احترافية — استراتيجية تثبيت الإصدار: يجب أن يكون الإصدار الافتراضي للمكتبة في تهيئة Jenkins العامة دائماً وسم semver مُطلَق (مثل v3.2.1)، وليس main أو HEAD. تُثبّت الفرق إصداراً محدداً في تعليق @Library خلال اختبار الترقية، ثم تحذف التثبيت بمجرد تعيين الافتراضي الجديد. هذا يمنحك مساراً تدريجياً للترقية بدون ترحيل إجباري مفاجئ. عوامل وسمَك إصدارات المكتبة من CI — مكتبة بدون اختبارات أو وسوم هي التزام وليست أصلاً.
المفهوم الجوهري — الصندوق الرملي: يشغّل Jenkins كود المكتبة في صندوق رملي Groovy افتراضياً. يحجب الصندوق الرملي بعض استدعاءات الأساليب (I/O للملفات، الاستبطان، HTTP الخارجي) ما لم يوافق عليها المسؤول صراحةً في Manage Jenkins → Script Approval. يمكن تعيين المكتبات من مصادر SCM موثوقة كـ Trusted في إعدادات Global Pipeline Libraries، مما يتجاوز الصندوق الرملي. قم بتعيين المكتبات كموثوقة فقط إذا كانت مملوكة لفريق المنصة ولديها مراجعة كود إلزامية — مكتبة موثوقة تعمل بامتيازات متحكم Jenkins الكاملة.