إدارة المخرجات وهندسة الإصدارات

التغليف عبر البيئات المختلفة

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

التغليف عبر البيئات المختلفة

يجب على مهندس DevOps أن يشحن الحزم بالتنسيق الذي تتطلبه بيئة المستهلك — خدمة Java إلى مستودع Maven، ومكتبة Python إلى PyPI، وواجهة سطر أوامر داخلية كحزمة Debian، وخدمة مصغّرة كصورة حاوية OCI. يحمل كل تنسيق اتفاقياته الخاصة للبيانات الوصفية، ونموذج تحليل التبعيات، وعقد التوقيع، وأنماط الفشل. فهم هذه الأنماط المشتركة — لا مجرد الأوامر — هو ما يميّز المهندس القادر على تشخيص إصدار معطوب في الساعة الثانية فجرًا عمّن لا يستطيع.

حزم JVM: ملفات JAR وWAR وإحداثيات Maven

تُنشر حزم Java إلى مستودعات متوافقة مع Maven (مثل Nexus وArtifactory وGitHub Packages وMaven Central) وتُعرَّف بثلاثة إحداثيات: groupId:artifactId:version (اختصارًا GAV). يرتّب المستودع الملفات بشكل حتمي على المسار groupId/artifactId/version/artifactId-version.jar، مما يتيح لأي أداة بناء جلب رسم بياني قابل للتكرار للتبعيات.

يبدو بناء Gradle الإنتاجي الذي ينشر إلى نسخة Artifactory داخلية كالتالي:

# build.gradle.kts — نشر مكتبة JAR إلى Artifactory plugins { `maven-publish` `java-library` } group = "com.acme.platform" version = providers.environmentVariable("RELEASE_VERSION").orElse("0.0.0-SNAPSHOT").get() publishing { publications { create<MavenPublication>("mavenJava") { from(components["java"]) pom { name.set("ACME Platform Core") description.set("Internal shared utilities") licenses { license { name.set("Apache-2.0") } } } } } repositories { maven { name = "artifactory" url = uri(System.getenv("ARTIFACTORY_URL") ?: "https://artifactory.internal/libs-release-local") credentials { username = System.getenv("ARTIFACTORY_USER") password = System.getenv("ARTIFACTORY_PASS") } } } } # النشر من CI ./gradlew publishMavenJavaPublicationToArtifactoryRepository \ -PRELEASE_VERSION=2.4.1
مستودعات SNAPSHOT ومستودعات الإصدار منفصلة بحسب التصميم. يطبّق Artifactory وNexus هذا المبدأ برفض تحميل SNAPSHOT إلى مستودعات الإصدار والعكس. لا تنشر أبدًا إصدارًا يحمل لاحقة -SNAPSHOT إلى مستودع إصدار — فالـ snapshots قابلة للتغيير بحكم تعريفها، مما يعني أن نفس الإحداثيات قد تُحلَّل إلى بايتات مختلفة في أيام مختلفة. يجب أن تكون حزم الإصدار غير قابلة للتغيير. الخلط بينهما ينتج بنيات غير قابلة للتكرار يكاد يستحيل تدقيقها بعد وقوع حادثة.

عجلات Python: لماذا تتفوق Wheels على توزيعات المصدر

يمتلك تغليف Python نوعَين من الحزم: توزيعات المصدر (.tar.gz أو sdist) والعجلات الثنائية (.whl). العجلة عبارة عن أرشيف ZIP يحمل اسمًا يشفّر إصدار Python والواجهة الثنائية والمنصة: mylib-1.2.0-cp311-cp311-manylinux_2_28_x86_64.whl. يثبّت pip العجلة بمجرد فك الضغط — لا خطوة تصريف، ولا سلسلة أدوات بناء مطلوبة عند التثبيت. هذا مهم جدًا لسرعة CI وللتكرارية في عمليات نشر الإنتاج.

نشر مكتبة داخلية إلى PyPI خاصة (Nexus أو Artifactory أو Google Artifact Registry) باستخدام twine:

# بناء توزيع المصدر والعجلة python -m build # ينتج dist/mylib-1.2.0.tar.gz و dist/mylib-1.2.0-py3-none-any.whl # pyproject.toml (PEP 517/518 — المعيار الحديث) [build-system] requires = ["setuptools>=68", "wheel"] build-backend = "setuptools.backends.legacy:build" [project] name = "mylib" version = "1.2.0" requires-python = ">=3.11" dependencies = [ "httpx>=0.27", "pydantic>=2.7", ] [project.optional-dependencies] dev = ["pytest", "mypy", "ruff"] # الرفع إلى الفهرس الخاص (TWINE_PASSWORD هو رمز API) TWINE_USERNAME=__token__ \ TWINE_PASSWORD="$PYPI_API_TOKEN" \ twine upload \ --repository-url https://artifact-registry.internal/simple/python/ \ dist/* # الاستهلاك من الفهرس الخاص في requirements.txt --extra-index-url https://__token__:${PYPI_API_TOKEN}@artifact-registry.internal/simple/ mylib==1.2.0
استخدم عجلات manylinux للإضافات المُصرَّفة. إذا كانت مكتبتك تُغلّف امتداد C، فابنِها داخل صورة Docker الرسمية manylinux_2_28 واستخدم auditwheel repair لتضمين المكتبات المشتركة الدقيقة في العجلة. ينتج عن ذلك حزمة مكتفية بذاتها قابلة للتثبيت على أي Linux متوافق مع glibc دون أن يحتاج المستخدم إلى مكتبات نظام مطابقة — وهو نفس مبدأ الربط الثابت لثنائيات Go.

حزم npm: Shrinkwrap وProvenanceونظافة النطاق

تُنشر حزم npm إلى سجل (npmjs.com أو Verdaccio/Artifactory داخليًا) على شكل أرشيفات tar مع بيان package.json. الهاجس التشغيلي الرئيسي في الإنتاج هو سلامة سلسلة التوريد — كان سجل npm العام ناقلًا لهجمات التشابه في الأسماء وهجمات الارتباك في التبعيات. تحديد نطاق جميع الحزم الداخلية تحت مساحة اسم خاصة وتوجيه تلك المساحة إلى سجلك الداخلي يُغلق سطح هجوم الارتباك في التبعيات بالكامل.

# .npmrc — توجيه حزم @acme/* إلى السجل الداخلي؛ الحزم العامة إلى npmjs @acme:registry=https://registry.internal.acme.com/ //registry.internal.acme.com/:_authToken=${NPM_INTERNAL_TOKEN} registry=https://registry.npmjs.org/ # package.json لمكتبة مشتركة داخلية { "name": "@acme/design-tokens", "version": "3.0.0", "main": "dist/index.js", "types": "dist/index.d.ts", "files": ["dist/"], "scripts": { "build": "tsc --project tsconfig.build.json", "prepublishOnly": "npm run build" }, "publishConfig": { "registry": "https://registry.internal.acme.com/", "access": "restricted" } } # النشر من CI (الرمز محدود الصلاحية للنشر فقط) npm publish --provenance # يُرفق شهادة SLSA (npm >= 9.5) # قفل الإصدارات الدقيقة لعمليات نشر الإنتاج npm ci # التثبيت من package-lock.json بدقة؛ يفشل إذا كان الملف قديمًا

حزم Debian: deb للحزم على مستوى النظام

بالنسبة للوكلاء والخدمات والواجهات السطرية وكل ما يُثبَّت على VM مجردة أو حاوية مبنية على Debian، فإن حزم .deb هي المعيار الذهبي. تتكامل مع apt لتحليل التبعيات، وتدعم خطافات التثبيت قبل وبعده (preinst وpostinst) لإنشاء المستخدمين وتسجيل الخدمات، وتتعامل مع إدارة ملفات الإعداد (conffiles) حتى لا تُطغى تعديلات المشغّل عند الترقية. بناء ملفات deb باستخدام nfpm شائع في مشاريع Go وRust.

# nfpm.yaml — إعداد الحزمة لخدمة Go name: "myagent" arch: "amd64" platform: "linux" version: "${RELEASE_VERSION}" maintainer: "Platform Team <platform@acme.com>" description: "ACME monitoring agent" section: "utils" priority: "optional" contents: - src: ./dist/myagent-linux-amd64 dst: /usr/bin/myagent file_info: mode: 0755 - src: ./configs/myagent.yaml dst: /etc/myagent/myagent.yaml type: config|noreplace # يحافظ على تعديلات المشغّل عند الترقية - src: ./systemd/myagent.service dst: /lib/systemd/system/myagent.service scripts: postinstall: ./scripts/postinstall.sh # systemctl daemon-reload && systemctl enable myagent preremove: ./scripts/preremove.sh # systemctl stop && systemctl disable myagent # بناء جميع التنسيقات دفعة واحدة nfpm package --config nfpm.yaml --packager deb --target dist/ nfpm package --config nfpm.yaml --packager rpm --target dist/ # التوقيع بـ GPG والنشر في مستودع APT (Aptly أو Pulp) dpkg-sig --sign builder dist/myagent_1.0.0_amd64.deb aptly repo add stable dist/myagent_1.0.0_amd64.deb aptly publish update stable

صور الحاويات: معيار OCI والطبقات والشهادات

تُعدّ صور الحاويات OCI تنسيق التغليف العالمي لأعباء العمل من جانب الخادم. على نطاق الشركات الكبرى، الانضباط ليس "بناء Dockerfile" — بل هو تقليل سطح الهجوم ووقت البناء وإبطال ذاكرة التخزين المؤقت للطبقات في آنٍ واحد.

Container image layer model and registry flow Build Layers FROM base (cached) RUN apt-get (cached) COPY deps (cached) COPY app (new) RUN compile (new) ENTRYPOINT push OCI Registry Image Manifest (sha256 digest) Layer Blobs (content-addressed) SBOM + Attestation (cosign / sigstore) pull Kubernetes Pod / Deployment ECS / Cloud Run Task / Service Cache miss — rebuilt Cache hit — reused
طبقات صورة الحاوية: تُعاد بناء الطبقات المتغيّرة فقط؛ طبقات التبعيات الثابتة تُخزَّن مؤقتًا وتُعاد استخدامها عبر عمليات البناء.

الانضباط الجوهري لصور الحاويات الإنتاجية هو ترتيب الطبقات والبنيات متعددة المراحل. ضع الطبقات الأكثر استقرارًا (نظام التشغيل الأساسي، الحزم، بيئة تشغيل اللغة) في أعلى Dockerfile حتى تُخزَّن مؤقتًا عبر عمليات البناء. انسخ ملفات البيان وثبّت التبعيات قبل نسخ كود التطبيق. هذا يعني أن تغييرًا في الكود فقط يتخطى جميع خطوات تثبيت الحزم البطيئة. استخدم البنيات متعددة المراحل حتى تحتوي الصورة النهائية على الحزمة التشغيلية فقط، لا المُصرِّف أو أدوات الاختبار أو ذاكرة التخزين المؤقت للبناء.

قيّد بالملخص لا بالوسم. وسم مثل node:20-alpine قابل للتغيير — يمكن لصاحب السجل دفع صورة مختلفة إلى نفس الوسم في أي وقت. في CI الإنتاجي، قيّد الصور الأساسية بملخص SHA-256 غير القابل للتغيير: FROM node:20-alpine@sha256:a1b2c3.... هذا يضمن أن بناءاتك محكمة: نفس Dockerfile ينتج دائمًا نفس الطبقات الوسيطة، مما يجعل التدقيقات الأمنية ذات معنى والتراجعات قابلة للتنبؤ.

النمط المشترك: مُعنوَن بالمحتوى، موقَّع، ومُصادَق عليه

على الرغم من الاختلافات السطحية بين .jar و.whl وحزمة npm و.deb وصورة OCI، يتقارب كل نظام بيئي حديث نحو نفس الضمانات الثلاث على مستوى الحزمة:

  1. التخزين المُعنوَن بالمحتوى — تُعرَّف الحزمة بتجزئة تشفيرية SHA-256 لمحتواها، لا باسم قابل للتغيير. إذا اشتركت حزمتان في نفس التجزئة، فهما متطابقتان على مستوى البايت.
  2. التوقيع — يوقّع مفتاح خاص الحزمة أو بيانها؛ يُوزَّع المفتاح العام خارج النطاق. يتحقق المستهلكون من التوقيع قبل الوثوق بالحزمة. تستخدم OCI أداة cosign؛ وMaven أداة GPG؛ ومستودعات deb بنية مفاتيح APT؛ وnpm شهادات provenance المستندة إلى OIDC عبر Sigstore.
  3. SBOM (قائمة مكوّنات البرمجيات) — بيان قابل للقراءة آليًا لكل تبعية مُضمَّنة في الحزمة. مطلوب للتدقيق في سلسلة التوريد وفحص CVE والامتثال لمستوى SLSA 3 وما فوق. الأدوات: syft لصور الحاويات، وcyclonedx-maven-plugin لملفات JAR، وcyclonedx-bom لـ Python.
سجل واحد يحكمها جميعًا. على نطاق واسع، شغّل مستودعًا واحدًا بنمط proxy + host + virtual في Artifactory أو Nexus يجمع Maven Central وPyPI وnpmjs وDocker Hub ومستوداتك الداخلية خلف عنوان URL واحد. كل بناء يسحب عبر هذا الوكيل الذي يخزّن الحزم الخارجية مؤقتًا. إذا تعطّل سجل خارجي (حدود معدل Docker Hub، أو انقطاع npm)، تستمر بناءاتك من الذاكرة المؤقتة. هذا التغيير الواحد يُزيل فئة كاملة من إخفاقات CI وهو أحد أول ما تضعه فرق هندسة المنصات.