Loading...

الحزم

كيف تستضيف مستودع APT وYUM موقَّعًا خاصًا بك على شبكة CDN

توزيع أداة لينكس كأرشيف مضغوط يعمل، لكن ما يريده الناس فعلًا هو `apt-get install أداتك` — فهذا ينقل التحديثات إلى الدورة التي يشغّلونها أصلًا. هذا شرح كامل ومجرَّب لبناء مستودعات APT وYUM موقَّعة، وتقديمها عبر شبكة CDN، ومصائد التوقيع والتخزين المؤقت التي ستعضّك. كل عطل مذكور هنا واجهناه بأنفسنا أثناء نشر واجهتنا الطرفية.

12 متوسط Updated

كيف تستضيف مستودع APT وYUM موقَّعًا خاصًا بك على شبكة CDN

لماذا نتكبّد العناء إن كان الأرشيف يفي بالغرض؟

التثبيت عبر `curl | tar` يعمل جيدًا في المرة الأولى. المشكلة في المرة الثانية. لا شيء يخبر مستخدميك بوجود إصدار جديد، ولا شيء يحدّثه مع بقية حزمهم، ولا شيء يزيله بنظافة. وينتهي الأمر بكل مستخدم يخترع طقس تحديث خاصًا به.

مستودع الحزم ينقل ذلك كله إلى آلية يمتلكها نظام التشغيل بالفعل. يلتقط `apt-get upgrade` إصدارك الجديد مع كل شيء آخر. والإزالة هي `apt-get remove`. أما المصدر فيثبته توقيع GPG يتحقق منه مدير الحزم تلقائيًا، بلا أن يفكر فيه أحد.

التكلفة أمسية واحدة للإعداد وقدر ضئيل من العمل مع كل إصدار يمكن أتمتته. وإليك الأمر كاملًا.

ما هو المستودع فعلًا

كلا الصيغتين مجرد ملفات ثابتة عبر HTTPS. لا خدمة تعمل في الخلفية ولا قاعدة بيانات — ولهذا بالضبط تقدّمها شبكات CDN بهذه الكفاءة.

يحتوي مستودع APT على `pool/` فيه ملفات `.deb`، وشجرة بيانات وصفية `dists/<suite>/`. يسرد `Packages` كل حزمة بحجمها وبصماتها ومسارها؛ ويسرد `Release` بصمات ملفات `Packages`؛ ويغطي التوقيع ملف `Release`. هذه سلسلة الثقة كاملة، وستهم لاحقًا.

مستودع YUM أبسط بنيةً: ملفات `.rpm` في مجلد بجوار `repodata/` الذي يضم `repomd.xml` والفهارس المضغوطة التي يشير إليها.

بنية المستودع

deb/
  dists/stable/
    Release  InRelease  Release.gpg
    main/binary-amd64/Packages{,.gz}
    main/binary-arm64/Packages{,.gz}
  pool/main/y/yourtool/yourtool_1.0.0_amd64.deb

rpm/
  yourtool-1.0.0-1.x86_64.rpm
  repodata/
    repomd.xml  repomd.xml.asc
    *-primary.xml.gz  *-filelists.xml.gz  *-other.xml.gz

أنشئ مفتاح التوقيع

المفتاح هو الجزء الوحيد الذي لا يمكنك إعادته باستخفاف: إن فقدته، على كل مستخدم استيراد مفتاح جديد. أنشئه مرة واحدة، وخذ نسخة احتياطية من المفتاح السري وشهادة الإبطال معًا، وأبقِ نصفه الخاص بعيدًا عن أي جهاز لا يحتاجه.

توقّع سكربتات الإصدار بلا تدخل بشري، ولذلك لا يكون لمفتاح توقيع المستودع عادةً عبارة مرور: يحميه أذونات الملفات ووجوده في مكان مقيَّد، تمامًا كمفتاح توقيع في أنظمة التكامل المستمر. امنحه تاريخ انتهاء حقيقيًا (خمس سنوات أمر شائع) وسجّل موعد التجديد في مكان ستراه فعلًا.

الإنشاء والتصدير
cat > key.batch <<'EOF'
%no-protection
Key-Type: RSA
Key-Length: 4096
Key-Usage: sign
Name-Real: example.com Package Signing
Name-Email: packages@example.com
Expire-Date: 5y
%commit
EOF
gpg --batch --gen-key key.batch && rm key.batch

# the public half — this is what users import
gpg --armor --export packages@example.com > example-com.gpg

# BACK THESE UP, offline:
#   gpg --export-secret-keys --armor packages@example.com
#   ~/.gnupg/openpgp-revocs.d/<fingerprint>.rev

ابنِ مستودع APT ووقّعه

يولّد `apt-ftparchive` (من حزمة `apt-utils`) ملفَّي البيانات الوصفية معًا. وهناك تفصيلان مهمّان.

شغّله من جذر المستودع، لأن مسارات `Filename:` التي يكتبها في `Packages` نسبية إلى المكان الذي استدعيته منه. إن أخطأت هنا، سيقرأ apt بياناتك الوصفية بلا مشكلة ثم يعطي 404 عند كل تنزيل.

ثم انشر التوقيع مرتين: `InRelease` هو الشكل الحديث الموقَّع داخليًا، و`Release.gpg` توقيع منفصل للعملاء الأقدم. إنتاجهما معًا لا يكلّف شيئًا ويجنّبك صنفًا كاملًا من أسئلة الدعم.

بناء بيانات APT الوصفية
cd deb
for arch in amd64 arm64; do
  mkdir -p "dists/stable/main/binary-$arch"
  apt-ftparchive --arch "$arch" packages pool \
    > "dists/stable/main/binary-$arch/Packages"
  gzip -9cn "dists/stable/main/binary-$arch/Packages" \
    > "dists/stable/main/binary-$arch/Packages.gz"
done

apt-ftparchive \
  -o APT::FTPArchive::Release::Origin=example.com \
  -o APT::FTPArchive::Release::Suite=stable \
  -o APT::FTPArchive::Release::Components=main \
  -o APT::FTPArchive::Release::Architectures="amd64 arm64" \
  release dists/stable > dists/stable/Release

gpg --batch --yes -abs -o dists/stable/Release.gpg dists/stable/Release
gpg --batch --yes --clearsign -o dists/stable/InRelease dists/stable/Release

RPM يحتاج توقيعَين لا واحدًا

هذه هي الخطوة التي يتعثر فيها الجميع تقريبًا، وقد تعثرنا فيها. يتحقق APT وYUM من الحزم بطريقتين مختلفتين جوهريًا.

APT سلسلة: يغطي التوقيع ملف `Release` الذي يحمل بصمات `Packages` الذي يحمل بصمات كل `.deb`. فبتوقيع البيانات الوصفية تصبح الحزم مغطّاة مجانًا — ولا تُوقَّع ملفات `.deb` منفردةً إطلاقًا.

أما RPM فله فحصان مستقلان. يتحقق `repo_gpgcheck=1` من `repomd.xml` عبر ملف `repomd.xml.asc` منفصل. لكن `gpgcheck=1` يتحقق من توقيع مضمَّن داخل كل حزمة، ولا يوفّره شيء آخر. فإن وقّعت البيانات الوصفية وحدها توقّف dnf برسالة `Package … is not signed / Error: GPG check FAILED`.

لذا شغّل `rpmsign` على كل `.rpm` — وقبل `createrepo_c`، لأن التوقيع يعيد كتابة الملفات فتصير أي بصمة سُجّلت قبله خاطئة فورًا.

وقّع الحزم ثم ابنِ البيانات الوصفية
# rpmsign ships in the 'rpm' package on Debian/Ubuntu.
# rpm hardcodes %__gpg to /usr/bin/gpg2, which Debian/Ubuntu do not ship:
rpmsign --define "_gpg_name packages@example.com" \
        --define "__gpg $(command -v gpg)" \
        --addsign rpm/*.rpm

# fail loudly rather than publish something unsigned
for r in rpm/*.rpm; do
  rpm -qpi "$r" | grep -qi '^Signature *: *(none)' \
    && { echo "UNSIGNED: $r"; exit 1; }
done

# only now build the indexes, then sign repomd.xml
createrepo_c --quiet rpm/
gpg --batch --yes --detach-sign --armor \
    -o rpm/repodata/repomd.xml.asc rpm/repodata/repomd.xml

تقديمه عبر شبكة CDN — والمصيدة

مستودع الحزم قريب من الحِمل المثالي لشبكة CDN: ملفات ثابتة تُنزَّل أكثر بكثير مما تتغيّر، ومن مستخدمين حول العالم. وجّه الحافة إلى خادمك الأصلي وتختفي مشكلة عرض النطاق.

المصيدة أن البيانات الوصفية للمستودع متسقة داخليًا. كل ملف يشير إلى بصمات ملفات أخرى. والتخزين المؤقت الذي يعطيك `Release` جديدًا و`Packages` قديمًا لم يعطك مستودعًا متأخرًا قليلًا — بل أعطاك مستودعًا مكسورًا، ولن تذكر رسالة الخطأ التخزين المؤقت إطلاقًا.

هذه الأعطال الثلاثة واجهناها في عملية نشر واحدة:

• `InRelease` قديم مع `Packages` جديد — يبلّغ apt عن عدم تطابق في البصمة.

• `.rpm` قديم — يبلّغ dnf بأن `Package … is not signed`، لأن التوقيع أعاد كتابة الملف والنسخة المخزَّنة هي القديمة غير الموقَّعة.

• `repomd.xml` جديد مع `repomd.xml.asc` قديم — يبلّغ dnf عن `Bad PGP signature`. تغيّر الملفان وفُرّغ واحد فقط.

القاعدة المستخلَصة: التفريغ الجزئي أسوأ من عدم التفريغ. فرّغ كل ملف أعادت عملية النشر كتابته — نقاط الدخول، وملفات الفهارس ذات البصمات تحت `repodata/`، والحزم نفسها. أو امنح البيانات الوصفية مدة صلاحية قصيرة جدًا ودع الحزم تُخزَّن طويلًا، فأسماؤها تتغيّر مع كل إصدار.

فرّغ كل ما لمسته
# entry points
for p in /deb/dists/stable/InRelease \
         /deb/dists/stable/Release \
         /deb/dists/stable/Release.gpg \
         /deb/dists/stable/main/binary-amd64/Packages \
         /deb/dists/stable/main/binary-amd64/Packages.gz \
         /rpm/repodata/repomd.xml \
         /rpm/repodata/repomd.xml.asc; do
  cdnctl purge --account "$ACC" --path "$p" --type exact
done

# the hashed indexes and the packages changed too
for f in rpm/repodata/* rpm/*.rpm; do
  cdnctl purge --account "$ACC" \
    --path "/rpm/$(basename "$f")" --type exact
done

النشر: كيف تصل الملفات إلى الـCDN

المستودع ليس إلا ملفات ثابتة، لذا فـ«النشر» يعني وضع شجرة المجلدات تلك حيث تستطيع الشبكة تقديمها. وهناك شكلان، وأيهما تريد يعتمد على ما إذا كنت تشغّل خادم ويب أصلًا.

**إن كان لديك خادم أصلي** — أي جهاز يقدّم HTTPS — فضع الشبكة أمامه وانشر بنسخ الملفات إليه. يكفي `rsync` عبر SSH؛ تجلب الحافة من الأصل عند أول طلب ثم تخزّن بعدها. هذا ما نفعله في cdn.com.tr، لأن خادم التنزيلات هو نفسه الذي يقدّم الموقع.

تحذير من واقع التجربة: زامن المجلدات الفرعية لا المجلد الأب. فأمر `rsync --delete` الموجَّه إلى المجلد الذي يضم تنزيلاتك الأخرى سيحذفها بلا تردد.

**وإن كنت لا تريد تشغيل خادم أصلي إطلاقًا**، فادفع الملفات مباشرة إلى تخزين الشبكة. مع cdn.com.tr ترفع الشجرة فتُقدَّم من نطاق الـCDN الخاص بك — بلا خادم ويب تثبّته أو ترقّعه أو تبقيه حيًّا. ويرفع `cdnctl cp -r` مجلدًا كاملًا، وهو تمامًا شكل المستودع.

في الحالتين، الرابط الأساسي الذي يضعه مستخدموك في `sources.list` أو في ملف `.repo` هو ببساطة نطاق الـCDN لديك مضافًا إليه المسار الذي نشرت فيه. وفي الحالتين، اختم بخطوة التفريغ من القسم السابق — فهي الجزء الذي يُنسى.

طريقتان لنشر الشجرة نفسها
# A) you have an origin: sync the subdirectories, never the parent
rsync -a --delete dist/repo/deb/ user@origin:/var/www/downloads/deb/
rsync -a --delete dist/repo/rpm/ user@origin:/var/www/downloads/rpm/
rsync -a          dist/repo/example-com.gpg user@origin:/var/www/downloads/

# B) no origin: push straight into CDN storage
cdnctl cp -r dist/repo/deb <account_uuid>:/downloads/deb
cdnctl cp -r dist/repo/rpm <account_uuid>:/downloads/rpm
cdnctl files put --file dist/repo/example-com.gpg \
                 --target-path /downloads/example-com.gpg

# the base URL is simply your CDN hostname + that path:
#   deb  -> https://cdn.example.com/downloads/deb
#   rpm  -> https://cdn.example.com/downloads/rpm

تحقّق داخل حاوية نظيفة

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

أبقِ التحقق من التوقيع مفعَّلًا أثناء الاختبار. فتعطيل `gpgcheck` لترى «إن كان الباقي يعمل» هو الطريق المعتاد لنشر مستودع غير موقَّع.

نصيحة لجانب فيدورا: مرّر `--disablerepo="*" --enablerepo="مستودعك"`. وإلا فسيقضي dnf وقته في تنزيل بيانات فيدورا الوصفية، ولن يخبرك تشغيل بطيء أو فاشل بشيء عن مستودعك.

حاويتان، تثبيت حقيقي
# Debian / Ubuntu
docker run --rm debian:12 sh -c '
  apt-get update -qq && apt-get install -y -qq curl gnupg ca-certificates
  curl -fsSL https://example.com/example-com.gpg |
    gpg --dearmor -o /usr/share/keyrings/example-com.gpg
  echo "deb [signed-by=/usr/share/keyrings/example-com.gpg] \
        https://example.com/deb stable main" \
    > /etc/apt/sources.list.d/example.list
  apt-get update && apt-get install -y yourtool && yourtool --version'

# Fedora / RHEL — only your repo, so the test is about your repo
docker run --rm fedora:40 sh -c '
  printf "[example]\nbaseurl=https://example.com/rpm\nenabled=1\ngpgcheck=1\nrepo_gpgcheck=1\ngpgkey=https://example.com/example-com.gpg\n" \
    > /etc/yum.repos.d/example.repo
  dnf --disablerepo="*" --enablerepo="example" -y install yourtool
  rpm -qi yourtool | grep Signature'

أمر أخير: عطّل التحديث الذاتي في الحزم

إن كانت أداتك قادرة على تحديث نفسها، فهذه الميزة تتعارض الآن مع مدير الحزم. فالتحديث الذاتي يكتب فوق ملف يملكه `dpkg` أو `rpm`، وتظل قاعدة بياناتهما تشير إلى إصدار لم يعد موجودًا على القرص — فيفعل التحديث التالي شيئًا مفاجئًا.

الإصلاح بسيط: اطبع على البناء الطريقة التي وُزِّع بها، واجعل محدّث الأداة يتنحّى حين يكون مدير حزم قد ثبّتها. أما التنزيلات المباشرة فتواصل تحديث نفسها كما كانت.

ختم وقت البناء (مثال بلغة Go)
// installChannel is overridden at build time for packaged builds.
var installChannel = "direct"

func update() error {
    if installChannel != "direct" {
        return fmt.Errorf(
            "installed via %s — upgrade it with that instead",
            installChannel)
    }
    // ... normal self-update
}

// packaged builds:
//   go build -ldflags "-X main.installChannel=deb"

أسئلة شائعة

هل يجب أن أوقّع المستودع أصلًا؟

تقنيًا لا — يمكن إخبار apt وdnf بتخطي التحقق. عمليًا نعم. إذ سيضطر المستخدمون إلى تعطيل فحص أمني لتثبيت برنامجك، وهو طلب سيئ وعادة أسوأ أن تعلّمها. التوقيع يكلّف مفتاحًا واحدًا وأمرَين.

وقّعت البيانات الوصفية، فلماذا يقول dnf إن الحزمة غير موقَّعة؟

لأن RPM يفحص أمرين منفصلين. `repo_gpgcheck` يغطي `repomd.xml`؛ و`gpgcheck` يغطي توقيعًا مضمَّنًا داخل كل `.rpm`. تحتاج أيضًا إلى `rpmsign --addsign` على الحزم — ويجب تشغيله قبل `createrepo_c` لأن التوقيع يغيّر الملفات.

لماذا يبلّغ apt عن عدم تطابق البصمة فور النشر؟

في الغالب بسبب طبقة تخزين مؤقت تقدّم خليطًا من بيانات وصفية قديمة وجديدة. بيانات المستودع الوصفية متسقة داخليًا، فالتحديث الجزئي يعني مستودعًا مكسورًا. فرّغ كل ملف أعادت عملية النشر كتابته، أو امنح البيانات الوصفية مدة صلاحية قصيرة.

هل يمكن لمستودع واحد أن يخدم amd64 وarm64؟

نعم. في APT، ولّد ملف `Packages` لكل معمارية ضمن الـsuite نفسها واذكر الاثنتين في `Architectures`. وفي YUM، ضع الحزمتين في المجلد نفسه — إذ يسجّل `createrepo_c` معمارية كل حزمة.

هل توضع ملفات المستودع في git؟

عادةً لا. فمجمّعات الحزم تكبر مع كل إصدار، وgit يحتفظ بكل نسخة إلى الأبد. ابنِ الشجرة من مخرجات إصدارك وزامنها إلى الخادم الأصلي، مبقيًا في git السكربتات والمفتاح العام فقط. واجعل البناء قابلًا لإعادة الإنتاج كي يمكن إنشاء المستودع من الصفر في أي وقت.