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

البنى القابلة للاستنساخ والمعزولة

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

البنى القابلة للاستنساخ والمعزولة

على نطاق الشركات الكبرى، لا تُقبل حجة "تعمل على جهازي" — فهي تقرير حادثة في انتظار أن يقع. البناء القابل للاستنساخ هو البناء الذي تُنتج فيه المدخلات نفسها مخرجات متطابقة بت-بت (أو متكافئة دلاليًا). أما البناء المعزول (Hermetic) فيذهب أبعد من ذلك: عملية البناء معزولة تمامًا عن العالم الخارجي — لا تنزيل من الإنترنت أثناء البناء، ولا متغيرات بيئية من المضيف، ولا تسرب من نظام الملفات المحيطي. معًا، يُشكّلان أساس سلاسل توريد برمجية آمنة وقابلة للمراجعة.

تُشغّل Google وMeta وUber أنظمة بناء معزولة (Bazel، Buck) تحديدًا لأن البناء غير المحدد على نطاقها يُمثّل مخاطر أمنية وعيوبًا في الموثوقية. يُرسّخ إطار SLSA (مستويات سلسلة التوريد لمُنتجات البرمجيات) هذه المتطلبات في طبقات امتثال متدرجة باتت الجهات التنظيمية والعملاء المؤسسيون يطالبون بها.

لماذا يصعب تحقيق الاستنساخية؟

معظم أنظمة البناء ليست قابلة للاستنساخ بشكل افتراضي. تشمل المصادر الشائعة لعدم الحتمية:

  • الطوابع الزمنية — أوقات تعديل الملفات، وماكروهات __DATE__/__TIME__ المُضمَّنة في الثنائيات.
  • ترتيب نظام الملفات — قوائم الدلائل غير مُرتَّبة في معظم أنظمة الملفات؛ وأدوات البناء التي تتكرر عليها تُنتج ترتيبات مختلفة لعناصر الأرشيف.
  • تباين الفاصلة العائمة والمعالج — يمكن أن يُنتج التحسين التلقائي للمُجمِّع شفرة آلية مختلفة عبر أجيال المعالجات.
  • معرّفات UUID أو ملح عشوائي مُضمَّن في مخرجات البناء (تفعل بعض أدوات الحزم هذا).
  • التبعيات غير المُثبَّتة — قد تجلب pip install requests اليوم الإصدار 2.31.0، وغدًا 2.32.0.
  • جلب الشبكة أثناء البناء — حل DNS في وقت البناء يُضيف اعتمادًا على حالة خادم بعيد.

تثبيت التبعيات وملفات القفل

أكثر الضوابط أثرًا هو تثبيت كل تبعية بنسخة دقيقة عبر ملف قفل. لكل بيئة برمجية كبرى آلية لملفات القفل. احرص على إيداع ملف القفل في git وعامل أي تباين كخطأ في CI.

# Node.js — npm npm ci # يثبّت فقط من package-lock.json؛ يفشل إن كان القفل قديمًا # لا تستخدم npm install في CI — قد يُحدِّث القفل بصمت # Python — pip مع pip-compile (pip-tools) pip-compile requirements.in # حل حتمي، يكتب requirements.txt مع هاشات pip install --require-hashes -r requirements.txt # يتحقق من كل هاش عند التثبيت # Go — go.sum هو قائمة الهاشات؛ go.mod يُثبِّت module@version go mod download # يجلب إلى ذاكرة التخزين؛ GONOSUMCHECK يجب أن يكون فارغًا في CI # Rust — Cargo.lock دقيق؛ احرص دومًا على إيداعه للتطبيقات الثنائية cargo build --locked # يفشل إن كان Cargo.lock قديمًا # Java/Maven — استخدم إضافة dependency:resolve مع المجاميع الاختبارية mvn dependency:resolve -Dclassifier=sources -Dmdep.useRepositoryLayout=true
لا تُضف ملف القفل إلى .gitignore. كثير من أدوات البنية التهيكلية تُضيف package-lock.json أو Pipfile.lock إلى .gitignore افتراضيًا. هذا صحيح للمكتبات القابلة لإعادة الاستخدام (حيث تريد الاختبار عبر نطاق من إصدارات التبعيات) لكنه خطأ للتطبيقات المُنشرة. كل تطبيق يعمل في الإنتاج يجب أن يكون لديه رسم بياني دقيق للتبعيات مُثبَّتًا ومُودعًا.

التنزيلات الموثّقة بالهاش

تثبيت الإصدار وحده لا يكفي — يمكن اختراق سجل الحزم، أو نقل وسم إصدار (الوسوم القابلة للتغيير ناقل هجوم حقيقي). الحل هو تثبيت هاشات المحتوى (SHA-256) إلى جانب سلاسل الإصدارات.

# pip: requirements.txt مع هاشات (مُولَّد بواسطة pip-compile --generate-hashes) requests==2.31.0 \ --hash=sha256:58cd2187423d21a6be1c2d4c12... \ --hash=sha256:c5b6e7a24c34e6fa7d9c29b... \ # Docker: اسحب دومًا بالـ digest في Dockerfiles للإنتاج FROM python:3.12.3-slim@sha256:9f7be3b0d1e0b9b2f5c49... AS base # تثبيت مزود Terraform مع SHA في .terraform.lock.hcl (يُدار تلقائيًا؛ أودعه) provider "registry.terraform.io/hashicorp/aws" { version = "5.50.0" constraints = "~> 5.0" hashes = [ "h1:xyz123...", "zh:abc456...", ] }
Renovate Bot / Dependabot يمكنهما فتح طلبات سحب تلقائيًا عند توفر إصدارات جديدة — بما فيها الهاشات المُحدَّثة. هذا يمنحك الحداثة (تصحيحات الأمان) والحتمية (الهاشات المُثبَّتة). هيّئه للعمل أسبوعيًا على نطاقات semver غير المُكسِّرة مع دمج تلقائي بعد نجاح CI.

بيانات وصف البناء والاستحقاق

تُجيب الاستنساخية على سؤال "هل يمكنني إعادة بناء هذا؟" بينما يُجيب الاستحقاق (Provenance) على "من بنى هذا، من أي مصدر، على أي جهاز، وفي أي وقت؟" كلاهما مطلوب لسلسلة توريد ناضجة.

بيانات وصف البناء هي معلومات منظمة تُطبع في الأدوات وقت البناء:

  • git.commit — SHA الدقيق للإيداع الذي أنتج هذا البناء.
  • git.branch / git.tag — الفرع أو وسم semver.
  • build.timestamp — طابع زمني RFC-3339 بتوقيت UTC (احفظه كبيانات وصف، لا مُضمَّنًا في الثنائي، للحفاظ على الاستنساخية).
  • build.runner — نظام CI، معرّف المُشغِّل، رابط خط الأنابيب.
  • build.builder_image — digest الصورة الدقيقة للـ Docker المستخدمة في التجميع.
# طبع ثنائيات Go عبر ldflags (بيانات وصف في الثنائي، لا تؤثر على الاستنساخية إن أُسقط الطابع الزمني) GIT_COMMIT=$(git rev-parse --short HEAD) GIT_TAG=$(git describe --tags --always --dirty) BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) go build -ldflags "\ -X main.Version=${GIT_TAG} \ -X main.GitCommit=${GIT_COMMIT} \ -X main.BuildDate=${BUILD_DATE}" \ -o bin/myapp ./cmd/myapp # للحصول على مخرجات قابلة للاستنساخ تمامًا، احذف BuildDate من ldflags # وبدلًا من ذلك خزّنه كتسمية صورة OCI: docker build \ --label org.opencontainers.image.revision="${GIT_COMMIT}" \ --label org.opencontainers.image.version="${GIT_TAG}" \ --label org.opencontainers.image.created="${BUILD_DATE}" \ --label org.opencontainers.image.source="https://github.com/myorg/myapp" \ -t myapp:${GIT_TAG} .

شهادات الاستحقاق SLSA

يُحدد SLSA (مستويات سلسلة التوريد لمُنتجات البرمجيات، وتُلفظ "سالسا") أربعة مستويات ثقة. مستوى SLSA 2 — وهو قابل للتحقيق لأي فريق يمتلك نظام CI حديث — يتطلب شهادة استحقاق موقّعة: وثيقة قابلة للتحقق آليًا تُبيّن ما هو المصدر الذي أنتج هذه الأداة، وعلى أي منصة بناء، وبأي مدخلات.

يُشحن GitHub Actions مع مُولِّد SLSA 3 من الدرجة الأولى. يتحقق المستهلكون من الشهادة قبل التثبيت.

# .github/workflows/release.yml — استحقاق SLSA 3 لثنائي Go jobs: build: permissions: id-token: write # مطلوب لتوقيع الاستحقاق contents: read uses: slsa-framework/slsa-github-generator/.github/workflows/builder_go_slsa3.yml@v2.0.0 with: go-version: "1.22" # التحقق من شهادة استحقاق صورة حاوية (cosign من Sigstore) cosign verify-attestation \ --type slsaprovenance \ --certificate-identity-regexp "^https://github.com/myorg/myapp" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ myregistry.io/myapp:v1.4.2
Hermetic build pipeline with provenance attestation Git Commit SHA pinned HERMETIC BUILD SANDBOX Locked dependencies (hash-verified) Pinned builder image (digest SHA) No network egress Artifact OCI image / binary SLSA Provenance JSON (Sigstore signed) Consumer cosign verify attests
خط أنابيب البناء المعزول يُنتج أداةً وشهادة استحقاق SLSA موقّعة؛ يتحقق المستهلكون من كليهما قبل النشر.

البناء المعزول في التطبيق العملي

العزل الحقيقي يعني أن صندوق رمل البناء لا يملك أي وصول صادر للشبكة ويستخدم فقط مدخلات مُجلَبة مسبقًا وموثّقة بالهاش. يُفرض Bazel هذا عبر الصندوق الرملي. للفرق التي لا تستخدم Bazel، يمكن تقريب ذلك:

  • جلب جميع التبعيات مسبقًا إلى مرايا السجلات أو دلائل vendor قبل بدء خطوة البناء.
  • استخدام --network=none في مراحل Docker التي تُجمِّع الكود (مرحلة جلب منفصلة تُنزِّل التبعيات).
  • تشغيل البناءات داخل صورة مُنشئ مُثبَّتة (مثل golang:1.22.3@sha256:...) لتثبيت إصدار المُجمِّع.
  • تعيين SOURCE_DATE_EPOCH على طابع زمني إيداع git لإزالة عدم حتمية الطابع الزمني في أدوات الأرشيف.
# Dockerfile متعدد المراحل: مرحلة الجلب لها شبكة؛ مرحلة التجميع ليس لها # المرحلة 1 — الجلب (الشبكة مسموح بها) FROM golang:1.22.3-alpine@sha256:b4f5d3... AS fetch WORKDIR /app COPY go.mod go.sum ./ RUN go mod download -x # جميع التبعيات مُجلَبة إلى ذاكرة التخزين # المرحلة 2 — التجميع (لا شبكة مطلوبة؛ يستخدم الوحدات المُخزَّنة) FROM fetch AS build COPY . . ENV CGO_ENABLED=0 \ SOURCE_DATE_EPOCH=0 # عيّنه على epoch git للاستنساخية RUN --network=none go build -trimpath -ldflags "-s -w" -o /bin/app ./cmd/app # المرحلة 3 — صورة تشغيل دنيا FROM gcr.io/distroless/static-debian12@sha256:a7f4... AS runtime COPY --from=build /bin/app /app ENTRYPOINT ["/app"]
علامة -trimpath (Go) تُجرِّد مسارات نظام الملفات المطلقة للمضيف من الثنائي، وتُزيل مصدرًا رئيسيًا لعدم الحتمية بين المطورين على أجهزة مختلفة. لمعظم اللغات ما يعادلها — Rust تستخدم --remap-path-prefix؛ بناءات Python wheel تقبل SOURCE_DATE_EPOCH؛ بعض أدوات الحزم npm تُعيّن reproducible: true.

التحقق من الاستنساخية

للتأكد من أن البناء قابل للاستنساخ حقًا، أعد البناء من المدخلات نفسها على جهاز مختلف وقارن هاشات الأدوات. يُنشر مشروع Reproducible Builds أدوات لهذا:

# إعادة البناء ومقارنة digests صور Docker docker build --no-cache -t myapp:v1.4.2-rebuild . docker inspect --format='{{.Id}}' myapp:v1.4.2 myapp:v1.4.2-rebuild # يجب أن يتطابق كلا المعرّفَين # diffoscope: مقارنة عميقة لثنائيَّين للعثور على مصادر عدم الحتمية diffoscope ./bin/app-build1 ./bin/app-build2 # لصور OCI: قارن digests الطبقات crane digest myregistry.io/myapp:v1.4.2 # خزّن هذا الـ digest في ملاحظات الإصدار وSBOM

البناءات القابلة للاستنساخ والمعزولة ليست كماليات اختيارية على نطاق الإنتاج — إنها الأساس الذي تُبنى عليه أمان سلسلة توريد البرمجيات، وشفافية الثنائيات، والتخزين المؤقت الفعّال. يظهر امتثال SLSA بشكل متزايد في متطلبات المشتريات المؤسسية والتفويضات الحكومية (NIST SP 800-218، EO 14028). الفرق التي تستثمر في هذه الممارسات مبكرًا تتجنب إعادة الهيكلة المؤلمة حين يصل المدققون.