ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

内网离线 Yum 源搭建:ISO、createrepo 与 HTTP 共享

内网离线 Yum 源搭建:ISO、createrepo 与 HTTP 共享 机房里有几台机器,网线插着,但只能跑内网,想装个 nginx、编译个工具链,抱着U盘来回拷 RPM 包,依赖一层套一层,一天下来啥也没干成——如果你也遇到过这种场面,那这篇就是我把自己在内网环境里搭 Linux 本地 yum 源、做离线 yum 仓库的全过程重新捋了一遍。整套方案不依赖任何外网,只用一张系统安装镜像,再配合 createrepo 和 httpd,就能把一台内网机器变成整片内网的软件仓库。这篇文章从场景判断、方案选型,到挂载、生成元数据、共享、客户端配置、增量维护、报错排查都会讲透,适合运维新手照着抄,也适合老手检查一下自己的做法有没有留坑。1. 先把场景说清楚:内网机器为什么需要一个本地 yum 源1.1 三种典型的断网现场我碰到的内网需求大致分三类,处理方式差别不小,所以先别急着敲命令,先对号入座。第一类是完全隔离的单机,机器插着网线但只能通到同网段的几台设备,连 DNS 都没有。这种场景最典型,装个gcc、vim-enhanced、telnet就卡住,yum install直接报Cannot find a valid baseurl for repo。第二类是一个小型内网集群,十几台到几十台机器,彼此能通,但没有出口。这种情况如果每台机器都挂一次 ISO,维护成本会随机器数量线性上涨,更合理的做法是选一台做仓库服务器,其他人走 HTTP。第三类是生产环境的灰度机器,出于稳妥考虑不允许装第三方源,只允许装发行版官方仓库里有的包,那就得把官方镜像整个搬进来,而不是随手放几个 RPM 包了事。这三类的共同点是:你手上一定有或者能拿到一张完整安装镜像(CentOS 7 的 DVD ISO、Rocky 9 的 DVD ISO 等)。这张镜像里已经包含了Packages目录和预生成的repodata,换句话说,一张 DVD 本身就等于一个能直接用的离线仓库,我们要做的只是把它挂起来、告诉 yum 去哪里找。很多人绕远路,是因为不知道这一层。1.2 四条技术路线怎么选在动手之前,我把能走的路列了一遍,你可以直接对照自己的场景挑。方案做法适合场景主要局限直接挂载 ISOmount -o loop后让 yum 指向挂载点单机、临时排查、救急重启后挂载丢失,只能读不能加包拷贝到硬盘自建仓库把 Packages 拷到磁盘,createrepo重建元数据需要往里加自定义 RPM占磁盘空间,元数据要手动刷新HTTP 共享在上一方案基础上用 httpd/nginx 暴露目录多机内网、长期使用要处理防火墙和 SELinux完整同步外网仓库用reposync拉全套再搬进来生产要求全量官方包数据量大,搬运麻烦单机救急我永远选第一条,五分钟搞定。但只要内网机器超过三台,我一定会做第三条,因为后面你会发现再加一个包这件事发生的频率远超预期,而挂载 ISO 的方案根本没法加包——ISO 是只读的,你往里塞不进任何东西。至于是不是要全量同步,我的经验是:除非合规要求包必须来自官方渠道且版本齐全,否则把 DVD 镜像里的内容拷出来,再按需往里补 RPM,性价比最高。1.3 yum 找包的底层逻辑(repodata 到底存了什么)要理解为什么目录拷贝完 yum 还是找不到包,得先搞清楚 yum 是怎么看见软件的。yum 不认识Packages目录里那些散落的.rpm文件。它只认仓库根目录下的repodata/文件夹,里面有repomd.xml、primary.xml.gz、filelists.xml.gz、other.xml.gz这几个文件。repomd.xml是索引里的索引,记录了其余每个文件的名字、校验和、时间戳;primary.xml.gz里存的是每个包的名字、版本、依赖关系、提供的能力(provides)。yum 每次操作,第一步就是下载repomd.xml,比对本地缓存里的校验和,发现变了就把其他几个 xml 拉下来解析成依赖树。这就解释了两件事。第一,光盘镜像为什么拿来就能用:官方在做镜像的时候已经把repodata生成好了,你挂上就能用。第二,为什么自己拷了 RPM 进去必须跑 createrepo:因为拷贝动作改变了仓库内容,但没人去更新那份索引,新包在 yum 眼里等于不存在。createrepo干的就是扫描目录、生成这套 xml 的活。顺带说一句,createrepo --update只增量更新变化的包,包多的时候能省不少时间,这个后面会细讲。提示:如果你拷贝完 RPM 直接yum clean all再试,依然会报No package xxx available,这不是缓存问题,是元数据没生成,别在缓存上来回折腾。2. 动手前的准备:镜像、目录与工具对齐2.1 镜像版本和系统版本必须对得上这一步吃过亏的人最多。我见过有人拿着 CentOS 7.6 的 ISO 给 CentOS 7.9 的机器做源,装基础包没问题,一旦涉及glibc、kernel、systemd这类和版本强绑定的包,就会出现依赖冲突或者干脆装不上。判断标准很简单:cat /etc/redhat-release看系统版本,lsblk或者问镜像提供方拿 ISO 的完整文件名,一般格式是CentOS-7-x86_64-DVD-2009.iso,其中2009对应 7.9。小版本号不一致时,优先找完全一致的;实在找不到,用同大版本里小版本不低于系统的镜像,并尽量只装应用层的包。还有一个容易忽略的点:一定要用DVD 版,不要用 Minimal 或者 Everything。Minimal 里包太少,连gcc都没有;Everything 虽然全但动辄十几个 G,内网搬运成本太高。对于 CentOS 8 之后的体系(Rocky 9、Alma 9、CentOS Stream 9),情况稍有不同:命令从yum换成了dnf,createrepo换成了createrepo_c,但思路完全一样,下面讲的方法把工具名替换掉就能用。2.2 目录规划:我习惯怎么摆目录怎么放,看着是小事,但真出问题的时候,清晰的目录结构能帮你省半小时定位时间。我的习惯是这样:# 镜像存放位置 /opt/iso/CentOS-7-x86_64-DVD-2009.iso # ISO 挂载点(临时只读) /mnt/iso # 自建仓库根目录,通过 HTTP 暴露 /data/repo/centos7//opt/iso存原始镜像,只读不动,做完备份就再也不碰。/mnt/iso是挂载点,专门给那些我只想快速装个包的临时场景用,重启会掉也不心疼。/data/repo/centos7是真正的仓库,内容从镜像里拷出来,可读可写,后面加自定义 RPM 就往这里丢。这样分层的好处是:临时方案和长期方案互不干扰,你哪天把/mnt/iso卸载了,HTTP 仓库一点事没有。如果磁盘紧张,/data/repo至少预留6 到 8 个 G。CentOS 7 的 DVD 完整内容拷贝出来大约 4.5G 左右,再算上你后续补进去的包和文件系统本身的开销,8G 是比较舒服的余量。2.3 createrepo 这类工具从哪儿来问题来了:createrepo本身也是个 RPM 包,内网装不了怎么办?这里有个很实用的顺序技巧——先用 ISO 把 createrepo 装上,再用 createrepo 建仓库。操作上分两步。先把 ISO 挂到/mnt/iso,写一个临时用的 repo 文件指向它:mount -o loop /opt/iso/CentOS-7-x86_64-DVD-2009.iso /mnt/iso cat /etc/yum.repos.d/tmp-iso.repo EOF [tmp-iso] nameTemporary ISO Repo baseurlfile:///mnt/iso enabled1 gpgcheck0 EOF然后关掉所有其他源,只从这个临时源装工具:yum --disablerepo* --enablerepotmp-iso install -y createrepo httpd这一步之所以要--disablerepo*,是因为内网机器上往往还残留着几个默认源(比如CentOS-Base.repo),它们指向的地址连不通,每次 yum 都要等超时,拖慢速度还可能直接报错。临时禁用它们,体验会清爽很多。用完之后这个tmp-iso.repo可以留着当救急源,但记得把enabled改成0,免得后面跟正式源打架。注意:如果你连yum都用不了(极端情况下 RPM 数据库损坏),可以直接用rpm硬装:rpm -ivh /mnt/iso/Packages/createrepo-*.rpm,缺依赖时把依赖包也一起列上。createrepo的依赖主要有python2-deltarpm、deltarpm、python2-libs等,从同一个 Packages 目录里都能找到。3. 三种落地形态的完整实操3.1 最省事:直接把 ISO 挂上做源这是救急首选,三步搞定,不占额外磁盘空间。# 1. 创建挂载点并挂载 mkdir -p /mnt/iso mount -o loop /opt/iso/CentOS-7-x86_64-DVD-2009.iso /mnt/iso # 2. 验证内容 ls /mnt/iso # 应该能看到 Packages、repodata、RPM-GPG-KEY-CentOS-7、media.repo 等看到repodata目录,就说明这个镜像自带索引,可以直接用了。接下来写正式的 repo 文件:cat /etc/yum.repos.d/local-iso.repo EOF [local-iso] nameCentOS 7 Local ISO Repository baseurlfile:///mnt/iso enabled1 gpgcheck1 gpgkeyfile:///mnt/iso/RPM-GPG-KEY-CentOS-7 EOF这里gpgcheck1是我推荐保留的。镜像里的RPM-GPG-KEY-CentOS-7就是官方公钥,gpgkey指向本地文件而不是 HTTP 地址,内网也能校验。校验能挡住ISO 下载不完整导致 RPM 损坏这种隐蔽问题,装到一半报rpm package signature verification failed的时候你会感谢自己开了它。不过这方案有两个硬伤:一是重启后挂载会丢,/etc/fstab里加一行可以让它自动挂:/opt/iso/CentOS-7-x86_64-DVD-2009.iso /mnt/iso iso9660 loop,defaults 0 0二是只读,没法加包。所以这个方案我只在单机救急时用,内网长期使用一定要往下走。3.2 最常用:拷贝到硬盘自建仓库把镜像内容搬到磁盘上,是内网仓库的正解。拷贝方式有两种,我倾向rsync,因为它可以断点续传,大文件传输中断了不用重来:mkdir -p /data/repo/centos7 rsync -av --progress /mnt/iso/Packages/ /data/repo/centos7/Packages/ cp -a /mnt/iso/RPM-GPG-KEY-CentOS-7 /data/repo/centos7/拷完之后目录里只有Packages和公钥,没有repodata——这是故意的,因为我们马上要自己生成一份,而且这份要覆盖官方那份里的路径信息。生成命令:createrepo -v /data/repo/centos7/-v是 verbose,包多的时候会刷屏,但第一次做的时候建议开着,能直观看到它在处理哪些包、有没有报错。执行完再ls /data/repo/centos7/,会多出一个repodata目录,里面正是前面说的那几个 xml。整个命令在 4000 多个包的情况下大概跑十几秒到几十秒,取决于磁盘性能。生成的 repo 文件变成这样:cat /etc/yum.repos.d/local-iso.repo EOF [local-iso] nameCentOS 7 Local Repository baseurlfile:///data/repo/centos7 enabled1 gpgcheck1 gpgkeyfile:///data/repo/centos7/RPM-GPG-KEY-CentOS-7 EOF对比 3.1 的方案,唯一变化就是baseurl从/mnt/iso变成了/data/repo/centos7。这个方案的核心优势就是可写:以后你从别的渠道拿到任何一个 RPM,往Packages里一丢,createrepo --update刷新一下,内网所有机器都能装上它。这才是离线仓库真正的价值。实操心得:拷贝的时候千万别用cp -r,用cp -a。-a会保留权限和时间戳,而 yum 在极少数情况下会参考文件时间戳判断是否需要重新校验,权限属性不一致也可能引发奇怪的读取失败。rsync -av同样保留这些属性,是更省心的选择。3.3 最省心:HTTP 共享给整个内网到了这一步,单机的仓库已经有了,接下来把它变成内网所有机器都能用。用 httpd 起一个静态文件服务就够,不需要什么复杂配置。# 让 httpd 指向我们的仓库根目录 ln -s /data/repo /var/www/html/repo # 启动并设置开机自启 systemctl enable --now httpd systemctl status httpd紧接着是内网部署最容易被卡住的两道门:防火墙和SELinux。防火墙放行 80 端口:firewall-cmd --permanent --add-servicehttp firewall-cmd --reloadSELinux 是更隐蔽的那个坑。默认策略下,httpd 进程只能读取被标记为httpd_sys_content_t的文件,你把目录软链到/var/www/html下,如果目标目录的标签不对,浏览器访问会直接返回403 Forbidden,而curl本地测试http://127.0.0.1/repo也照样 403,让人一脸懵。两种处理方式:# 方式一:临时放宽(不推荐用于长期) setenforce 0 # 方式二:正确打标签(推荐) semanage fcontext -a -t httpd_sys_content_t /data/repo(/.*)? restorecon -Rv /data/reposemanage命令来自policycoreutils-python包,如果没装可以先用方式一验证问题确实是 SELinux 引起的,再决定要不要装工具链。生产环境我强烈建议用方式二,直接关 SELinux 属于把门拆了防贼。配置完在另一台内网机器上验证:curl -I http://10.0.0.10/repo/centos7/repodata/repomd.xml # 期望返回 HTTP/1.1 200 OK返回 200 就说明服务通了,客户端可以接入了。3.4 客户端 .repo 文件怎么写客户端的配置和 3.2 几乎一样,只是baseurl换成 HTTP 地址:cat /etc/yum.repos.d/intranet.repo EOF [intranet-centos7] nameIntranet CentOS 7 Repository baseurlhttp://10.0.0.10/repo/centos7 enabled1 gpgcheck1 gpgkeyhttp://10.0.0.10/repo/centos7/RPM-GPG-KEY-CentOS-7 EOF有几个参数值得单独说。enabled1控制这个源默认是否启用,如果它只是备用源,平时想关掉,改成0,用的时候加--enablerepointranet-centos7临时开。gpgkey这里指向 HTTP 地址,意味着客户端每次校验前要下载一次公钥,内网环境下这个开销可以忽略,但如果你追求极致稳定,可以先把公钥文件拷到客户端本地,改成file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7。另外,机器上原有的外网源一定要处理掉。我通常的做法是把它们归档:mkdir -p /etc/yum.repos.d/backup mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/backup/只 mv 一个文件是不够的,CentOS 7 默认还有CentOS-Base.repo里的多个仓库段,以及可能存在的epel.repo。最稳的做法是把所有*.repo都移走,只留自己写的那个,确认没问题后再按需把个别源放回来并设成enabled0。3.5 验证与体检命令配完别急着装东西,先做一轮体检:# 清理旧缓存,这一步不能省 yum clean all # 重建缓存,顺便验证仓库能连通 yum makecache # 列出所有启用的仓库 yum repolist enabled # 看看仓库里到底有多少包 yum repolist -vyum repolist enabled的输出里,只要看到我们配的intranet-centos7出现在列表里,并且 package 数量是个合理的大数字(几千个),基本就稳了。如果数量是 0 或者仓库压根不出现,直接跳到第 5 章的排查表。最后做一次真实的安装测试,选一个体积小、依赖简单的包:yum install -y telnet成功装上,说明整条链路从repodata解析到 RPM 下载、签名校验全部正常。提示:测试包别用gcc这类依赖巨多的,一旦某一层依赖缺失,你会分不清是自己的仓库没配好,还是这个包本身就不在镜像里。先用telnet、tree、lrzsz这种单打独斗的包确认链路健康。4. 内网仓库的长期维护:补包、更新与签名4.1 往已有仓库里塞自定义 RPM仓库搭好只是开始,能不能加包决定了它能不能长期活下来。流程一共三步,顺序不能乱:# 1. 把新包丢进 Packages 目录 cp /tmp/nginx-1.20.2-1.el7.x86_64.rpm /data/repo/centos7/Packages/ # 2. 增量更新元数据 createrepo --update /data/repo/centos7/ # 3. 客户端刷新缓存 # 在客户端执行: yum clean metadata yum makecache--update是关键参数。它只处理新增和变化的包,不会把几千个包的元数据全部重算一遍,速度快很多。我第一次做的时候没加这个参数,每次加一个包要跑几十秒,加了之后基本一两秒完成。客户端那边,如果仓库配了metadata_expire,可以不用手动清缓存,但为了确定性,我习惯显式执行yum clean metadata yum makecache。有个细节要注意:客户端缓存的是元数据,不是包本身,所以你在服务器端换掉一个同名同版本的 RPM(内容不同),客户端的元数据校验和对得上,但包内容的校验和会对不上,装的时候报校验失败。这种同名不同内容的操作要尽量避免,真要替换就换个版本号。4.2 元数据更新与多版本共存内网仓库跑了一年半载,你会发现Packages目录里同一个包有好几个版本,比如python-2.7.5-89.el7和python-2.7.5-90.el7。这不是问题,createrepo支持同包多版本共存,yum 默认会选版本号最高的那个。但同包多版本会带来元数据体积膨胀,包上千个之后,primary.xml.gz可能到几十兆,客户端每次makecache都要下载解析,速度会明显变慢。我的处理原则是:保留最近两个小版本,老的定期清理,清理完必须重新跑一次完整的createrepo(不带--update,因为要重建索引,把已删除包的信息彻底抹掉),否则元数据里还留着已删包的记录,客户端会去下载一个不存在的文件,报 404。清理操作:# 建议先备份,别一上来就 rm mkdir -p /data/backup/old-rpms mv /data/repo/centos7/Packages/python-2.7.5-89.el7.x86_64.rpm /data/backup/old-rpms/ # 完整重建元数据 createrepo /data/repo/centos7/这个删除后必须完整重建的坑,踩过的人不多,但一旦踩上,报错信息(Failed to download ... 404)会让人以为是服务挂了,实际上只是索引没同步。4.3 gpgcheck 到底关不关关于gpgcheck,内网圈子里有两种流派。一种坚持开着,一种嫌麻烦直接gpgcheck0。我的态度是:自建仓库如果只放官方镜像里的包加自己编译的包,可以关;如果会从各种渠道收集第三方 RPM,建议开着。原因很简单。gpgcheck1校验的是 RPM 的签名,官方包都有签名,校验能挡住下载过程中镜像损坏和文件被替换两类问题。但你自己用rpmbuild编译出来的包通常没有签名,这时候要么给包签名(需要生成 GPG key 并配置%_gpg_name),要么在仓库层面关掉校验,或者更精细一点,用repo_gpgcheck0配合gpgcheck0只对这一个仓库放行。如果决定关掉,写法是:[local-custom] nameCustom Internal Repository baseurlfile:///data/repo/custom enabled1 gpgcheck0关掉校验意味着你信任这个目录里所有文件,所以这个目录的写权限要收好,chmod 755目录、chown root:root文件,别让普通用户有机会往里丢东西。5. 踩坑与排查实录5.1 高频报错速查表下面这些报错我几乎每个都碰过至少一次,整理成表,遇到时对着查比翻日志快得多。报错信息根因处理方式Cannot find a valid baseurl for repo挂载掉了 / HTTP 服务不通 / baseurl 写错df -h看挂载,curl -I测地址repomd.xml: [Errno 14] HTTP Error 404repodata没生成或路径不对去服务器上确认repodata/repomd.xml存在No package xxx available元数据没刷新 / 包名拼错yum clean all yum makecache,再用yum provides查403 ForbiddenSELinux 标签不对restorecon -Rv或临时setenforce 0验证Metadata file does not match checksum元数据被改动 / 时间戳不同步完整createrepo重建Public key for xxx.rpm is not installedgpgkey 路径错或公钥没导入检查gpgkey指向,或临时gpgcheck0定位问题Error: Nothing to do仓库里确实没这个包 / 源被禁用yum repolist确认源启用状态Downloading packages ... failed磁盘空间不足 / 权限问题df -h、ls -l检查repodata里 xml 全部为 0 字节createrepo 中途中断删除repodata重新执行5.2 几个容易被忽略的细节时间不同步引发的元数据异常。内网机器长期不联网,时钟漂移是常态。如果客户端时间比服务器时间早,repomd.xml里的时间戳会被判定为来自未来,yum 可能直接拒绝使用缓存。这个问题的表现很迷惑:服务器上文件明明在,curl也能下,但 yum 就是说元数据有问题。处理方式是统一时间,内网里至少保证仓库服务器和客户端时钟差在几分钟以内。软链和 SELinux 的组合拳。3.3 里我用了ln -s /data/repo /var/www/html/repo。软链本身没问题,但 httpd 默认配置里Options FollowSymLinks是开的,而 SELinux 检查的是最终目标目录的标签,不是软链所在的目录。所以你会发现给/var/www/html/repo打标签没用,必须给/data/repo打。这个小细节能省你半小时。多个仓库之间的优先级。当内网仓库和某个残留的外网源同时存在,同一个包两边都有,yum 会默认选版本高的,可能从外网源去拉——而内网根本拉不到,就会卡到超时。解决办法有两种:把外网源彻底 mv 走(最干净),或者装上yum-plugin-priorities插件,给内网源加一行priority1,数字越小优先级越高。ISO 挂载后不能写入这件事要反复强调。我见过有人把 RPM 直接cp到/mnt/iso/Packages/下,命令不报错(因为cp成功创建了文件?),实际上 ISO9660 是只读挂载,cp会直接失败。如果在挂载点上硬写,运气不好还会把挂载搞成只读状态,需要重新挂。5.3 我自己的避坑清单做了这么多次,我总结出几条经验,基本每次都会对照检查一遍。第一条,先在仓库服务器本地用curl自测,再上客户端。curl -I http://127.0.0.1/repo/centos7/repodata/repomd.xml返回 200,说明服务和文件都没问题,接下来如果客户端还是不通,那问题一定在防火墙或者网络层,排查范围立刻缩小一半。第二条,一次性把yum clean all和yum makecache连着跑。只跑前者不跑后者,下次 yum 操作时会重新下载全部元数据,速度慢得让人怀疑人生;只跑后者不跑前者,旧缓存的残留可能让你看到一个已经删掉的包还能搜到的诡异现象。第三条,把整个搭建过程写成一个脚本存起来。内网环境经常是今天搭好,三个月后换台机器重来一遍,有脚本在手,五分钟就能复现。脚本里至少包含:创建目录、挂载 ISO、rsync拷贝、createrepo、写 repo 文件、systemctl起服务、firewall-cmd放行这几步,幂等性做不做都行,反正每次都是干净环境。第四条,仓库服务器要留足日志空间。httpd 的access_log会记录每一次元数据下载,机器一多,日志涨得很快,把/var撑满之后 httpd 写不了日志会直接返回 500,表现成昨天还好好的,今天全内网都装不了包。我一般会给日志加个logrotate规则,或者干脆把CustomLog指到独立磁盘分区。第五条,也是最容易忘的:搭建完做一次完整的回归。找一台干净的、当前什么都没配的内网机器,按 3.4 的逻辑配一遍,装一个带依赖的包(比如yum install -y nginx),从头跑通一次。只有完整跑通,你才知道这个仓库到底是看着能用还是真的能用。这最后一步花的十分钟,能帮你避免后面很多次被叫去帮忙看看为什么装不上。内网离线 yum 仓库这件事,技术上没有多复杂,难点全在细节里——SELinux 的标签、元数据的刷新时机、版本对齐、时钟同步,每一个单拎出来都是小事,凑在一起就是半天排不出来的故障。把这套流程跑顺一次,以后再遇到纯内网的机器,基本就是复制粘贴的事了。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进