内网服务器访问不了公网 yum 源,打补丁、装软件就没了着落。解法是把 RPM 包集中到一台服务器,用 createrepo 生成仓库元数据,再用 OpenResty 以 HTTP 方式发布出去——客户端只需要一个 repo 文件指向这台服务器,dnf install 的体验和公网源完全一致。本文在 Rocky Linux 9.3 容器上从头到尾跑通这套流程,CentOS 7 时代的差异单独列出。

整体思路

整个方案分三步:

  1. 把 RPM 包集中到服务器的一个目录
  2. 用 createrepo_c 命令创建仓库
  3. 配置 OpenResty 发布 YUM 源

需要准备两个软件:

软件 用途 版本与来源
createrepo_c 制作仓库元数据 Rocky 9 AppStream 自带,dnf install 即得
OpenResty 以 HTTP 发布仓库 1.31.1.1,官方 yum 源安装

OpenResty 的官方下载页在这里,源码编译的安装步骤可以参考这篇文章;本篇直接用官方 yum 源安装,比编译省事得多。

发布层选 HTTP 而不是 FTP 或直接共享目录,理由有三个:dnf 原生支持 http baseurl,客户端零配置;HTTP 单向、无状态,防火墙上只需要开一个端口;OpenResty 顺手还能干别的——同一台 nginx 上再挂个内部静态站点毫无压力。仓库元数据本身只有几 MB,哪怕几百台客户端来拉,缓存命中率也足够高。

部署

准备 RPM 包

RPM 包有两个来源:

  1. 系统安装 ISO 镜像里自带的包,挂载后把 Packages 目录拷出来
  2. 同步公网镜像到本地

零散补包也可以在能上网的机器上用 dnf download --resolve 包名 直接拉取,--resolve 会连依赖一起下载。这个参数不能省——实测只 dnf download jq 不带 --resolve,包放进仓库后客户端安装直接报 nothing provides libonig.so.5()(64bit):仓库里只有 jq 自己,依赖 libonig 没跟进门。自建源最常见的坑就在这,收集包时务必连依赖一起收。

把收集到的包统一放进仓库目录(本文用 /data/yum-repo/)。ISO 里的包拷出来时记得用 mount -o loop CentOS-7-x86_64-DVD.iso /mnt/iso 这类方式挂载, Packages 目录整个拷贝即可;目录组织上建议按系统版本分开(比如 /data/yum-repo/9/、/data/yum-repo/7/ 各建各的 repodata),混在一起会出现 9 的客户端解析到 7 的包元数据的怪事。

安装 createrepo

Rocky 9 上 createrepo 已经由 C 重写的 createrepo_c 接替,AppStream 源直接装:

1
dnf install createrepo_c

装完系统里同时存在 createrepo 和 createrepo_c 两个命令,前者是指向后者的兼容软链,用法一致。el7 时代那串 deltarpm、python-deltarpm、libxml2-python 的 rpm 依赖链在 el9 上不需要了,一个包解决。

创建仓库

1
createrepo_c -d /data/yum-repo/

这条命令扫描 /data/yum-repo/ 下的所有 RPM 包,在目录里生成 repodata/ 元数据,客户端就是靠这份索引来查包的。-d 表示同时生成 sqlite 格式索引(el9 上默认也会生成)。

配图1

生成后的 repodata/ 里有 7 个文件:primary、filelists、other 三类信息各有 xml.gz 和 sqlite.bz2 两份,外加总入口 repomd.xml。和 CentOS 7 时代一眼能认出包名的 *-primary.xml.gz 不同,el9 的元数据文件改成了哈希命名,认文件只能看 repomd.xml 里的引用关系,但客户端解析不受影响。

安装并配置 OpenResty

添加 openresty 官方 yum 源再安装:

1
2
3
4
5
6
7
8
# /etc/yum.repos.d/openresty.repo
[openresty]
name=Official OpenResty Open Source Repository
baseurl=https://openresty.org/package/rocky/$releasever/$basearch
repo_gpgcheck=0
gpgcheck=0
enabled=1
gpgkey=https://openresty.org/package/pubkey.gpg
1
dnf install openresty

注:官方配置模板里 repo_gpgcheck=1,实测在容器里报 repomd.xml GPG signature verification error: Bad GPG signature,改 0 后正常。生产环境若要保留元数据验签,先确认系统时间准确、GPG 公钥已导入。

OpenResty 装好后配置文件在 /usr/local/openresty/nginx/conf/nginx.conf,在 server 块里加一段 location,把站点根路径映射到仓库目录:

1
2
3
4
location / {
root /data/yum-repo;
autoindex on;
}

autoindex on 让目录可浏览,方便人工核对包是否存在。启动服务并验证:

1
2
/usr/local/openresty/bin/openresty
curl -sI http://127.0.0.1:8080/repodata/repomd.xml

配图2

HTTP 200 且能返回 repomd.xml 内容,发布就通了。客户端可以按路径下载 rpm 和 repodata。

客户端配置

写一个自定义 repo 文件:

1
2
3
4
5
6
# /etc/yum.repos.d/local.repo
[local-yum]
name=Local YUM Repo
baseurl=http://IP:PORT
gpgcheck=0
enabled=1

其中 IP:PORT 为 YUM 服务器的地址和对外提供的端口。如果服务器端按系统版本分了目录,baseurl 里可以直接用 dnf 的内置变量拼路径,例如 baseurl=http://IP:PORT/$releasever/$basearch,客户端各自解析成自己的版本和架构,一份 repo 文件全网通用。然后在客户端重建缓存并做一次真实安装——全程禁用其他源,只留 local-yum,装得下来才算通:

1
2
3
dnf clean all
dnf --disablerepo="*" --enablerepo="local-yum" makecache
dnf --disablerepo="*" --enablerepo="local-yum" install tree

配图3

Installed 与 Complete! 出现,说明元数据解析、依赖判断、包下载整条链路都在自建源上闭环了。

日常维护

更新自建仓库

往目录里追加了新包之后,不用全量重建,增量更新即可:

1
createrepo_c --update /data/yum-repo/

--update 只把新增或变化的包合并进现有索引,仓库大时比全量重建快得多。更新完在客户端 dnf clean all && dnf makecache 一次,新包可见。缓存过期这件事客户端自己也会处理——repomd.xml 带 metadata_expire(Rocky 官方源默认 6 小时),自建源不写这个字段时 dnf 按默认策略刷新,急用的包手动 clean 再 makecache 最稳妥。

下载失败排查

客户端安装时报这个错:

1
2
3
Error: Error downloading packages:
wget-1.21.1-11.el9_8.aarch64: Cannot download, all mirrors were already
tried without success

含义是 repodata 索引里记录的包文件在源上已经不存在了——包被移动或删除后索引没有跟着更新,客户端拿着旧索引去找文件,404 一路试到放弃。yum 时代同类故障的报错是 [Errno 2] Local file does not exist,dnf 的提示换成上面这句,本质是同一个病。

修复路径实测有效:把误删的包补回目录(或确认该包确实该下架),然后删除 repodata/ 重新 createrepo_c -d 重建,或者直接 createrepo_c --update 增量合并,最后客户端清缓存重试。

排查这类问题有个自检三板斧:先在服务器本机 curl -sI http://127.0.0.1:8080/repodata/repomd.xml 确认发布层返回 200;再在客户端 dnf repolist 确认源可见;最后 dnf makecache 看元数据拉取有无报错。三层分别对应发布、寻址、索引,哪层先断,问题就在哪层。

配图4

历史版本差异(CentOS 7)

老读者从 CentOS 7 迁过来,主要注意这四处:

  • 包名:createrepo(Python 实现)换成 createrepo_c,el7 那套 deltarpm 依赖链不用再手工 rpm -ivh;
  • 命令:客户端 yum 换成 dnf,yum makecache 对应 dnf makecache,yum 在 el9 里只是 dnf 的别名;
  • 元数据命名:repodata 从可读文件名变成哈希命名,排查看 repomd.xml;
  • 收集包:yumdownloader --resolve 换成 dnf download --resolve,不解析依赖的坑两代系统一模一样。
  • 装 OpenResty:el7 年代基本靠源码编译,如今官方直接提供 Rocky/RHEL 9 的 yum 源,dnf install openresty 一条命令,升级维护都省心。

注意事项

  • 依赖必须齐:仓库里只有主包没有依赖,客户端装的时候报 nothing provides;收集包一律 --resolve,必要时把对应版本的整个基础仓库同步进来。
  • 仓库目录和 repodata 必须保持同步:包有增删就要 createrepo_c --update,客户端再执行 dnf clean all、dnf makecache,否则就是上面那个 Cannot download。
  • gpgcheck=0 的边界:跳过签名校验,内网自用没问题,不要把这种源暴露到公网;对安全性有要求的场景给自建源配好自己的签名(rpmsign + 导入公钥)。
  • ISO 只覆盖系统自带的包:EPEL 这类第三方源的包要另行收集后再入库,别指望一个 ISO 通吃。
  • 防火墙放行:服务器记得放行发布端口(如 8080),firewall-cmd --add-port=8080/tcp --permanent && firewall-cmd --reload,不然客户端连不上还以为源坏了。

一套局域网 YUM 源,服务端两个软件、客户端一个 repo 文件,成本几乎为零。真正决定它好不好用的,是包收集时有没有带上依赖、增删包之后索引有没有跟着走——这两件事做扎实,内网机器的 dnf install 就和公网一样顺手。等哪天要扩到多机房,把这台 OpenResty 换成上游,再在各个机房用反向代理缓存一份,这套架构还能继续长。

本文由作者 2020-2024 年间的 CSDN 博客文章重构而来,原发布于 CSDN。