فرض کنید می‌خواهیم ابزارهای شرکت را با dnf install نصب کنیم، نسخهٔ تازه را ابتدا روی ماشین آزمایش ببینیم و سپس همان مجموعه را برای سرورها منتشر کنیم. برای یک مخزن کوچک، RPMهای امضاشده، متادیتای تولیدشده با createrepo_c و یک وب‌سرور کافی‌اند؛ کنترل نسخه و تأیید انتشار را باید به این اجزا اضافه کنیم.

این راهنما روی AlmaLinux ۹ یا Rocky Linux ۹ با DNF 4 یک مخزن سازمانی می‌سازد. میزبان نمونه x86_64 است و دامنهٔ rpm.example.org آموزشی است. فرمان‌ها در جریان نگارش روی سرور واقعی اجرا نشده‌اند؛ ابتدا روی ماشین آزمایش اجرا کنید.

اجزای این معماری#

جزءنقش
rpmbuildساخت فایل RPM از SPEC
rpmsignامضای خود RPM
createrepo_cساخت متادیتای مخزن از بسته‌ها
GnuPGمدیریت کلید و امضای جداگانهٔ repomd.xml
Nginxارائهٔ فایل‌های عمومی با HTTPS
پوشهٔ نسخه‌دارنگهداری مجموعهٔ ثابت برای آزمایش و انتشار
SPEC / CI --> RPM --> package signature --> versioned directory
                                                  |
                                            createrepo_c
                                                  |
                                         metadata signature
                                                  |
                                            Nginx --> DNF

این پوشه‌ها snapshot داخلی یک ابزار مانند aptly نیستند؛ ثباتشان به سیاست ما وابسته است: بعد از تأیید، محتوای پوشهٔ نسخه را تغییر نمی‌دهیم. برای مجموعه‌های بزرگ، چند ناشر یا گردش‌کار APIمحور می‌توان مدیریت چرخه را به سامانه‌ای مانند Pulp سپرد؛ مثال حاضر یک انتشار ایستای کوچک است.

۱. آماده‌سازی میزبان#

با حساب مدیر و مخازن رسمی سالم:

sudo dnf install rpm-build rpm-sign createrepo_c gnupg2 nginx \
  policycoreutils-python-utils dnf-plugins-core curl
sudo useradd --create-home --shell /bin/bash rpmrepo
sudo install -d -o rpmrepo -g rpmrepo -m 0755 /srv/rpmrepo/public

اگر کاربر وجود دارد، useradd را تکرار نکنید. کلید خصوصی و محیط ساخت در home ناشر می‌مانند؛ وب‌سرور فقط public را می‌خواند. مخازن رسمی کلاینت را حذف نمی‌کنیم؛ مخزن کوچک شرکت جایگزین تمام سیستم‌عامل نیست.

وارد حساب ناشر شوید؛ تا پایان امضا و تولید متادیتا، فرمان‌ها در همین نشست‌اند:

sudo -iu rpmrepo
umask 022
mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
mkdir -p /srv/rpmrepo/public/keys
mkdir -p /srv/rpmrepo/public/el9/x86_64/releases/v1/Packages

برای معماری دیگری، هم مسیر انتشار و هم ماشین ساخت و آزمون مقصد را متناسب انتخاب کنید. noarch بودن بستهٔ نمونه، مجوز استفاده از هر RPM باینری روی هر معماری نیست.

۲. کلید امضای سازمان#

برای سازگاری این آزمایش با EL۹، یک کلید RSA می‌سازیم:

gpg --quick-generate-key 'Company RPM Repository <rpm@example.org>' rsa3072 sign 1y
gpg --list-secret-keys --keyid-format long --fingerprint

اثرانگشت کامل را از خروجی بردارید و جایگزین کنید:

RPM_SIGNING_FPR='REPLACE_WITH_FULL_FINGERPRINT'
gpg --armor --export "$RPM_SIGNING_FPR" \
  > /srv/rpmrepo/public/keys/RPM-GPG-KEY-company

کلید را با passphrase محافظت کنید. رمز یا کلید خصوصی را در SPEC، تنظیم وب‌سرور یا Git قرار ندهید. در این نمونه یک کلید برای بسته و متادیتا داریم؛ در سازمان می‌توان نقش‌ها را جدا کرد. زمان انقضا و فرایند تعویض کلید باید پیش از استفادهٔ دائمی مشخص باشند. مدیریت کلید OpenPGP در GnuPG

۳. ساخت یک RPM ساده#

بستهٔ ما فقط یک فایل متنی نصب می‌کند. فایل ~/rpmbuild/SPECS/company-hello.spec را بسازید:

Name:           company-hello
Version:        1.0
Release:        1%{?dist}
Summary:        Internal RPM repository demonstration
License:        MIT
BuildArch:      noarch

%description
A small data-only package for an internal repository lab.

%prep

%build

%install
mkdir -p %{buildroot}%{_datadir}/%{name}
printf '%s\n' 'Hello from the internal RPM repository.' \
  > %{buildroot}%{_datadir}/%{name}/message.txt

%files
%dir %{_datadir}/%{name}
%{_datadir}/%{name}/message.txt

این SPEC آموزشی است و فایل منبع خارجی یا scriptlet ندارد. برای نرم‌افزار واقعی، source، مجوز واقعی، وابستگی‌ها و مراحل build/test را تعریف کنید؛ مقدار MIT را برای پروژهٔ دیگری بدون بررسی کپی نکنید.

RPM_BUILD_DIR=~/rpmbuild
rpmbuild --define "_topdir $RPM_BUILD_DIR" \
  -bb ~/rpmbuild/SPECS/company-hello.spec

متغیر RPM_BUILD_DIR مسیر محیط ساخت را مستقل از پوشهٔ جاری تعیین می‌کند. خروجی مورد انتظار روی EL۹:

~/rpmbuild/RPMS/noarch/company-hello-1.0-1.el9.noarch.rpm

مسیر اعلام‌شده در خروجی rpmbuild مرجع است؛ macro مربوط به dist ممکن است در محیط متفاوت، پسوند دیگری بدهد. ساخت RPM و محیط rpmbuild

۴. ابتدا بسته، سپس متادیتا را امضا کنیم#

نام واقعی خروجی را در متغیر بگذارید:

RPM_FILE=~/rpmbuild/RPMS/noarch/company-hello-1.0-1.el9.noarch.rpm
rpmsign --define "_gpg_name $RPM_SIGNING_FPR" --addsign "$RPM_FILE"

در EL۹، _gpg_name برای انتخاب کلید ابزار امضای RPM استفاده می‌شود. اگر محیط headless برای pinentry خطا داد، ترمینال فعلی را به GPG معرفی کنید و دوباره امضا را امتحان کنید:

export GPG_TTY="$(tty)"

برای بررسی محلی، از یک rpmdb آزمایشی در home ناشر استفاده می‌کنیم تا نیازی به تغییر پایگاه بسته‌های میزبان نباشد:

RPM_CHECK_DB=~/rpm-signature-check
mkdir -p "$RPM_CHECK_DB"
rpm --dbpath "$RPM_CHECK_DB" --initdb
rpmkeys --dbpath "$RPM_CHECK_DB" \
  --import /srv/rpmrepo/public/keys/RPM-GPG-KEY-company
rpmkeys --dbpath "$RPM_CHECK_DB" --checksig --verbose "$RPM_FILE"

خروجی signature باید با کلید موردنظر تأیید شود. سپس:

REPO_V1=/srv/rpmrepo/public/el9/x86_64/releases/v1
cp "$RPM_FILE" "$REPO_V1/Packages/"
createrepo_c "$REPO_V1"
gpg --armor --detach-sign --local-user "$RPM_SIGNING_FPR" \
  --output "$REPO_V1/repodata/repomd.xml.asc" \
  "$REPO_V1/repodata/repomd.xml"
gpg --verify "$REPO_V1/repodata/repomd.xml.asc" \
  "$REPO_V1/repodata/repomd.xml"

ترتیب مهم است: امضای RPM بایت‌های فایل را تغییر می‌دهد؛ اگر پس از تولید متادیتا بسته را امضا کنید، هش ثبت‌شده برای فایل قدیمی خواهد بود. بعد از هر بازتولید repomd.xml نیز باید امضای متادیتا را دوباره بسازید. createrepo_c خود به‌تنهایی RPMها را امضا نمی‌کند. ابزار createrepo_c

۵. Nginx، HTTPS و SELinux#

با exit به حساب مدیر برگردید. پیش‌نیاز این بخش DNS صحیح و گواهی معتبر است؛ گواهی‌های مسیر زیر باید واقعاً وجود داشته باشند. فایل /etc/nginx/conf.d/rpm-repository.conf:

server {
    listen 443 ssl;
    server_name rpm.example.org;

    ssl_certificate /etc/pki/tls/certs/rpm.example.org.fullchain.pem;
    ssl_certificate_key /etc/pki/tls/private/rpm.example.org.key;

    root /srv/rpmrepo/public;
    autoindex off;

    location / {
        try_files $uri =404;
    }
}

در چیدمان رایج EL، از conf.d استفاده می‌کنیم. root را روی خروجی عمومی بگذارید؛ home ناشر، rpmdb آزمایشی و کلید خصوصی نباید ارائه شوند. راهنمای root و try_files در Nginx

برای خواندن محتوای ایستا با SELinux:

sudo semanage fcontext -a -t httpd_sys_content_t '/srv/rpmrepo/public(/.*)?'
sudo restorecon -Rv /srv/rpmrepo/public
sudo nginx -t
sudo systemctl enable --now nginx
sudo systemctl reload nginx

اگر همان قاعده از قبل تعریف شده، با semanage fcontext -l بررسی و در صورت نیاز با -m اصلاح کنید. فقط پس از موفقیت nginx -t سرویس را راه‌اندازی یا reload کنید. برای این سرویس ایستا نیازی به خاموش‌کردن SELinux یا فعال‌کردن دسترسی شبکهٔ خروجی Nginx نیست.

اگر firewalld فعال است، HTTPS را در zone متصل به رابط سرویس مجاز کنید؛ این مثال از zone پیش‌فرض استفاده می‌کند:

sudo firewall-cmd --add-service=https --permanent
sudo firewall-cmd --reload

اگر zone اختصاصی دارید، --zone را صریح تعیین کنید. هنگام 403، علاوه بر مجوزهای فایل و عبور از پوشه‌های والد، context و پیام‌های AVC را بررسی کنید.

۶. راه‌اندازی اعتماد کلاینت#

روی کلاینت EL۹ آزمایشی با curl و GnuPG نصب‌شده:

curl --fail --show-error --location \
  https://rpm.example.org/keys/RPM-GPG-KEY-company \
  --output /tmp/RPM-GPG-KEY-company
gpg --show-keys --with-fingerprint /tmp/RPM-GPG-KEY-company

اثرانگشت کامل را با مقدار تحویل‌شده از کانال مورداعتماد سازمان مقایسه کنید. سپس:

sudo install -m 0644 /tmp/RPM-GPG-KEY-company \
  /etc/pki/rpm-gpg/RPM-GPG-KEY-company
sudo rpmkeys --import /etc/pki/rpm-gpg/RPM-GPG-KEY-company

فایل /etc/yum.repos.d/company-tools.repo:

[company-tools]
name=Company tools for EL9
baseurl=https://rpm.example.org/el9/$basearch/releases/v1/
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-company
includepkgs=company-*
sslverify=1

اگر CA داخلی دارید، ابتدا آن را از فرایند معتبر در trust store سیستم قرار دهید. sslverify=0 جای نصب CA نیست. DNF ممکن است برای ورود کلید به keyring جداگانهٔ متادیتا نیز تأیید بخواهد؛ اثرانگشت را دوباره بخوانید. gpgkey را معادل محدودسازی سخت امضای هر بسته به یک repo فرض نکنید. کلید و بررسی امضا در DNF

sudo dnf --refresh --disablerepo='*' --enablerepo=company-tools makecache
dnf list --showduplicates company-hello
sudo dnf --assumeno install company-hello
sudo dnf install company-hello
cat /usr/share/company-hello/message.txt
rpm -qi company-hello

معیار موفقیت: دریافت متادیتا بدون خطای امضا، مشاهدهٔ نسخه از company-tools، تأیید برنامهٔ نصب و وجود فایل متنی. این بسته وابستگی کاربردی ندارد؛ بسته‌های واقعی ممکن است همچنان به BaseOS و AppStream نیاز داشته باشند.

۷. دریافت مخازن رسمی با reposync#

برای میرور، از میزبان دریافت‌کنندهٔ همان توزیع، major release و معماری مقصد استفاده کنید. AlmaLinux را از مخازن AlmaLinux و Rocky را از مخازن Rocky بگیرید. شناسه‌ها را ابتدا ببینید:

dnf repolist --enabled
sudo install -d -m 0755 /srv/rpmrepo/incoming
sudo dnf reposync --repoid=baseos --repoid=appstream \
  --download-path=/srv/rpmrepo/incoming \
  --download-metadata --gpgcheck

اینجا baseos و appstream شناسه‌های رایج‌اند؛ خروجی میزبان خودتان مرجع است. reposync پوشهٔ جدا برای هر repo می‌سازد. برای بررسی امضا، gpgcheck=1 باید در تنظیم مخزن ورودی نیز فعال باشد؛ گزینهٔ --gpgcheck آن را در مخزنی که صریحاً بررسی را خاموش کرده الزاماً جایگزین نمی‌کند. رفتار reposync و گزینهٔ gpgcheck

این فرمان ممکن است حجم زیادی دریافت کند. پوشهٔ incoming عمداً خارج از public است تا مجموعهٔ درحال دریافت منتشر نشود. خروج موفق فرمان کافی نیست؛ لاگ، فضای دیسک، فایل‌های حذف‌شده به علت امضای نامعتبر و نصب واقعی روی ماشین تازه را بررسی کنید.

--download-metadata برای حفظ متادیتای اصلی مهم است، از جمله module metadata و advisoryهایی که بالادست ارائه می‌کند. روی کپی کامل، بی‌دلیل createrepo_c اجرا نکنید. اگر امضای جداگانهٔ متادیتای بالادست وجود دارد، انتقال و اعتبار آن را هم بررسی کنید؛ از وجود گزینهٔ download-metadata نتیجه نگیرید همهٔ فایل‌های جانبی امضا حتماً دریافت شده‌اند.

--newest-only را با متادیتای کامل بدون بررسی ترکیب نکنید. فهرست ممکن است نسخه‌هایی را معرفی کند که دانلود نکرده‌اید. فیلتر معماری هم می‌تواند همین ناسازگاری را ایجاد کند. برای آزمایش گزینشی یک برنامه، dnf download --resolve --alldeps PACKAGE مفید است، اما تضمین میرور کامل یا حفظ module/updateinfo نیست. محدودیت فیلترها در reposync ، گزینه‌های download

برای استفادهٔ آفلاین، علاوه بر BaseOS و AppStream، CRB، Extras یا EPEL موردنیاز، کلیدهای همان منابع، وابستگی‌های نصب اولیه و سیاست دریافت sourceها را پوشش دهید. کپی همین دو repo به معنی پوشش همهٔ نیازهای سازمان نیست.

۸. نسخهٔ تازه را در مسیر مستقل بسازیم#

دوباره وارد ناشر شوید و اثرانگشت را در نشست جدید تنظیم کنید:

sudo -iu rpmrepo
RPM_SIGNING_FPR='REPLACE_WITH_FULL_FINGERPRINT'

در SPEC، Version را به 1.1 تغییر دهید و بسته را دوباره بسازید. فایل واقعی خروجی CI نیز می‌تواند جای این build را بگیرد؛ هر تغییر محتوا باید شناسهٔ نسخهٔ تازه داشته باشد.

RPM_BUILD_DIR=~/rpmbuild
rpmbuild --define "_topdir $RPM_BUILD_DIR" \
  -bb ~/rpmbuild/SPECS/company-hello.spec
RPM_CHECK_DB=~/rpm-signature-check
RPM_FILE_V2=~/rpmbuild/RPMS/noarch/company-hello-1.1-1.el9.noarch.rpm
rpmsign --define "_gpg_name $RPM_SIGNING_FPR" --addsign "$RPM_FILE_V2"
rpmkeys --dbpath "$RPM_CHECK_DB" --checksig --verbose "$RPM_FILE_V2"

REPO_V2=/srv/rpmrepo/public/el9/x86_64/releases/v2
mkdir -p "$REPO_V2/Packages"
cp "$RPM_FILE_V2" "$REPO_V2/Packages/"
createrepo_c "$REPO_V2"
gpg --armor --detach-sign --local-user "$RPM_SIGNING_FPR" \
  --output "$REPO_V2/repodata/repomd.xml.asc" \
  "$REPO_V2/repodata/repomd.xml"
gpg --verify "$REPO_V2/repodata/repomd.xml.asc" \
  "$REPO_V2/repodata/repomd.xml"

پس از پایان تولید، مدیر سیستم باید context فایل‌های جدید را با restorecon -Rv /srv/rpmrepo/public اعمال کند. مسیر v1 را دست‌نخورده نگه دارید. کلاینت staging را به releases/v2/ متصل کنید و نصب تازه و ارتقای 1.0 به 1.1 را بررسی کنید.

۹. promotion و بازگشت مخزن#

در این معماری، promotion یعنی تغییر baseurl کلاینت‌های تأییدشده به همان مسیر ثابت v2، مثلاً با مدیریت پیکربندی. برای production دوباره متادیتا نسازید؛ همان بایت‌هایی را ارائه کنید که آزمایش شده‌اند.

مزیت URL نسخه‌دار این است که در میانهٔ چند درخواست HTTP، مسیر از مجموعهٔ اول به دوم نمی‌پرد. هزینه‌اش تغییر کنترل‌شدهٔ تنظیم کلاینت‌هاست. اگر بعداً یک alias مثل current اضافه کردید، فقط عوض‌کردن symlink برای سازگاری تمام درخواست‌های هم‌زمان کافی نیست؛ نگهداری فایل‌ها و سیاست کش را هم طراحی کنید.

برای بازگرداندن نمای مخزن، baseurl را به releases/v1/ برگردانید و متادیتا را تازه کنید. ماشینی که 1.1 نصب کرده با این کار خودکار 1.0 نمی‌شود. برنامهٔ downgrade را جدا ببینید:

sudo dnf --refresh makecache
sudo dnf --assumeno downgrade company-hello

فقط پس از بررسی نسخه و سازگاری برنامه می‌توان downgrade واقعی را اجرا کرد. در برنامهٔ دارای پایگاه داده، downgrade فایل اجرایی ممکن است با schema تازه ناسازگار باشد؛ بازگشت داده و تنظیمات فرایند جدا دارد. downgrade و تاریخچهٔ DNF

۱۰. نگهداری مخزن داخلی#

دریافت، آزمایش و انتشار را سه مرحلهٔ مستقل نگه دارید. دریافت موفق نباید خودکار به معنی تأیید production باشد. برای هر نسخه، فهرست RPMها و هش آن‌ها، توزیع و معماری مقصد، زمان دریافت و نتیجهٔ آزمون را ثبت کنید.

از RPMهای اصلی، متادیتا، فایل تنظیم انتشار و کلید خصوصی محافظت‌شده backup بگیرید. کلید عمومی قابل‌بازتولید است، اما از دست‌دادن کلید خصوصی بدون طرح چرخش کلید می‌تواند انتشار بعدی را متوقف کند. کلید خصوصی را با فایل‌های وب در یک backup عمومی قرار ندهید.

نسخهٔ قدیمی را تا پایان دورهٔ بازگشت و به‌روزرسانی همهٔ کلاینت‌ها نگه دارید. مصرف دیسک، شکست sync، انقضای کلید و TLS، زمان آخرین اصلاحیه و دسترسی واقعی به repomd.xml و یک RPM را پایش کنید. پاسخ ۲۰۰ از صفحهٔ اصلی وب‌سرور به‌تنهایی سلامت مخزن را نشان نمی‌دهد.

این سرویس بعد از انتشار نیازی به اجرای دائمی createrepo_c یا rpmbuild ندارد. ابزارها هنگام تغییر محتوا فایل تولید می‌کنند و Nginx خروجی تأییدشده را به DNF می‌دهد؛ مسئولیت انتخاب و تازگی مجموعه همچنان با ناشر داخلی است.