今天聊聊RPM打包,对于很多内网环境,每次部署新软件都是一场灾难。要么依赖包找不到,要么版本冲突,要么就是在不同环境上跑不起来。后来学会了自己打RPM包,这些问题基本就解决了。

RPM包就像是给软件穿了一件标准化的外衣,不管在哪台CentOS或者RHEL机器上,都能优雅地安装和卸载。今天就跟大家分享一下,怎么从头开始制作一个RPM包,相信我,掌握了这个技能,你的运维生涯会轻松很多。

RPM包的基本概念

RPM全称是Red Hat Package Manager,虽然名字里有Red Hat,但现在几乎所有基于Red Hat的发行版都在用,包括CentOS、Fedora、SUSE等等。

一个RPM包其实就是一个压缩文件,里面包含了:

  • 要安装的文件
  • 安装脚本
  • 依赖关系信息
  • 软件描述信息

比如你安装nginx的时候,RPM包会告诉系统需要哪些依赖库,安装到哪些目录,需要创建什么用户,启动什么服务等等。

我记得以前手动部署一个Java应用,光是配置环境就要半天,现在打成RPM包,一条yum install命令就搞定了。这就是标准化的威力。

搭建RPM构建环境

想要制作RPM包,首先得搭建构建环境。我一般会专门准备一台虚拟机来做这个事情,避免污染生产环境。

安装必要工具

yum install -y rpm-build rpmdevtools rpmlint

这几个包的作用:

  • rpm-build:核心构建工具
  • rpmdevtools:提供一些便利脚本
  • rpmlint:检查RPM包质量的工具

创建构建目录结构

rpmdev-setuptree

这个命令会在你的家目录下创建rpmbuild目录,结构是这样的:

~/rpmbuild/
├── BUILD/      # 编译过程中的临时目录
├── RPMS/       # 生成的RPM包存放目录
├── SOURCES/    # 源码和补丁文件
├── SPECS/      # spec文件存放目录
└── SRPMS/      # 源码RPM包存放目录

这个目录结构是标准的,不要随意改动。我刚开始的时候总想按自己的习惯来组织,结果各种报错。

SPEC文件详解

SPEC文件是RPM包的核心,它告诉rpmbuild工具怎么构建包。我们来看一个实际的例子,假设要打包一个简单的Shell脚本工具。

基本信息段

Name:           my-backup-tool
Version:        1.0.0
Release:        1%{?dist}
Summary:        A simple backup tool for system administrators

License:        MIT
URL:            https://github.com/mycompany/backup-tool
Source0:        %{name}-%{version}.tar.gz

BuildArch:      noarch
Requires:       bash >= 4.0, rsync, tar

%description
This is a simple backup tool that helps system administrators
to backup important files and directories automatically.
It supports incremental backup and compression.

这里有几个要注意的点:

  • Name必须是小写,不能有空格
  • Version遵循语义化版本规范
  • Release通常从1开始,每次重新打包就递增
  • BuildArch设为noarch表示不依赖特定架构

准备阶段

%prep
%setup -q
# 这里可以应用补丁
# %patch0 -p1

%prep段主要是解压源码包,%setup -q会自动解压Source0指定的文件。

构建阶段

%build
# 对于脚本类软件,通常不需要编译
# 如果是C/C++程序,这里会有make命令
echo "Nothing to build for shell scripts"

安装阶段

%install
rm -rf $RPM_BUILD_ROOT
mkdir -p $RPM_BUILD_ROOT/usr/local/bin
mkdir -p $RPM_BUILD_ROOT/etc/backup-tool
mkdir -p $RPM_BUILD_ROOT/var/log/backup-tool

# 安装主程序
install -m 755 backup-tool.sh $RPM_BUILD_ROOT/usr/local/bin/backup-tool
# 安装配置文件
install -m 644 backup-tool.conf $RPM_BUILD_ROOT/etc/backup-tool/

这个阶段要把文件复制到$RPM_BUILD_ROOT目录下,这个目录模拟了真实的文件系统结构。

文件列表

%files
%defattr(-,root,root,-)
/usr/local/bin/backup-tool
%config(noreplace) /etc/backup-tool/backup-tool.conf
%dir /var/log/backup-tool

%files段列出了包中包含的所有文件。%config(noreplace)表示这是配置文件,升级时不会覆盖用户的修改。

安装前后脚本

%pre
# 安装前执行
getent group backup >/dev/null || groupadd -r backup
getent passwd backup >/dev/null || useradd -r -g backup -d /var/lib/backup -s /sbin/nologin backup

%post
# 安装后执行
systemctl daemon-reload
systemctl enable backup-tool.service

%preun
# 卸载前执行
if [ $1 -eq 0 ]; then
    systemctl stop backup-tool.service
    systemctl disable backup-tool.service
fi

%postun
# 卸载后执行
systemctl daemon-reload
if [ $1 -eq 0 ]; then
    userdel backup 2>/dev/null || true
    groupdel backup 2>/dev/null || true
fi

这些脚本很重要,特别是涉及到系统服务的时候。我见过很多人忽略这些脚本,结果卸载包的时候留下一堆垃圾。

实战:打包一个Web应用

理论说完了,我们来个实际例子。假设要打包一个基于Node.js的Web应用。

准备源码包

首先要把源码打包成tar.gz格式:

cd /path/to/your/project
tar czf ~/rpmbuild/SOURCES/my-webapp-1.0.0.tar.gz .

编写SPEC文件

Name:           my-webapp
Version:        1.0.0
Release:        1%{?dist}
Summary:        My awesome web application

License:        MIT
URL:            https://github.com/mycompany/webapp
Source0:        %{name}-%{version}.tar.gz

Requires:       nodejs >= 12.0, npm
BuildRequires:  nodejs, npm

%description
This is my awesome web application built with Node.js.
It provides REST API services for business operations.

%prep
%setup -q

%build
npm install --production

%install
rm -rf $RPM_BUILD_ROOT
mkdir -p $RPM_BUILD_ROOT/opt/my-webapp
mkdir -p $RPM_BUILD_ROOT/etc/systemd/system
mkdir -p $RPM_BUILD_ROOT/var/log/my-webapp

# 复制应用文件
cp -r * $RPM_BUILD_ROOT/opt/my-webapp/

# 安装systemd服务文件
cat > $RPM_BUILD_ROOT/etc/systemd/system/my-webapp.service << 'EOF'
[Unit]
Description=My Web Application
After=network.target

[Service]
Type=simple
User=webapp
WorkingDirectory=/opt/my-webapp
ExecStart=/usr/bin/node app.js
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
EOF

%files
%defattr(-,webapp,webapp,-)
/opt/my-webapp
%attr(644,root,root) /etc/systemd/system/my-webapp.service
%dir %attr(755,webapp,webapp) /var/log/my-webapp

%pre
getent group webapp >/dev/null || groupadd -r webapp
getent passwd webapp >/dev/null || useradd -r -g webapp -d /opt/my-webapp -s /sbin/nologin webapp

%post
systemctl daemon-reload
systemctl enable my-webapp.service
systemctl start my-webapp.service

%preun
if [ $1 -eq 0 ]; then
    systemctl stop my-webapp.service
    systemctl disable my-webapp.service
fi

%postun
systemctl daemon-reload
if [ $1 -eq 0 ]; then
    userdel webapp 2>/dev/null || true
    groupdel webapp 2>/dev/null || true
fi

%changelog
* Wed Dec 13 2023 Your Name <your.email@company.com> - 1.0.0-1
- Initial package

构建RPM包

cd ~/rpmbuild
rpmbuild -ba SPECS/my-webapp.spec

如果一切顺利,你会在RPMS/noarch/目录下看到生成的RPM包。

常见问题和解决方案

在实际打包过程中,经常会遇到各种问题。我把常见的几个列出来:

依赖问题

最常见的就是依赖包找不到:

error: Failed dependencies:
    some-package >= 1.0 is needed by my-webapp-1.0.0-1.noarch

解决方法是检查Requires字段,确保依赖包名称正确。有时候包名和你想的不一样,可以用这个命令查找:

yum provides */some-file
rpm -qf /path/to/file

文件权限问题

经常会遇到这样的错误:

error: File not found: /builddir/build/BUILDROOT/my-webapp-1.0.0-1.el7.x86_64/some/file

这通常是%files段中列出的文件在%install阶段没有正确安装。检查路径是否正确,权限是否设置对了。

脚本执行失败

%pre、%post等脚本如果执行失败,会导致安装失败。我的建议是:

  • 所有命令都要考虑失败的情况
  • 使用条件判断,比如[ -f /some/file ] && do_something
  • 在脚本末尾加上exit 0确保返回成功

循环依赖

有时候会遇到包A依赖包B,包B又依赖包A的情况。这种问题比较复杂,通常需要重新设计包的结构,把公共部分提取出来。

高级技巧和最佳实践

使用宏定义

SPEC文件支持宏定义,可以让配置更灵活:

%define app_user webapp
%define app_dir /opt/%{name}

# 然后在其他地方使用
User=%{app_user}
WorkingDirectory=%{app_dir}

条件构建

有时候需要根据不同的发行版或架构进行条件构建:

%if 0%{?rhel} >= 7
Requires: systemd
%else
Requires: upstart
%endif

子包

一个SPEC文件可以生成多个RPM包:

%package devel
Summary: Development files for %{name}
Requires: %{name} = %{version}-%{release}

%description devel
This package contains development files for %{name}.

%files devel
/usr/include/%{name}/

版本管理

我建议在git仓库中管理SPEC文件,每次发布新版本时:

# 更新版本号
sed -i 's/Version:.*/Version: 1.0.1/' my-webapp.spec

# 更新changelog
cat >> my-webapp.spec << EOF
* $(date '+%a %b %d %Y') $(git config user.name) <$(git config user.email)> - 1.0.1-1
- Bug fixes and improvements
EOF

自动化构建

可以写个简单的脚本来自动化构建过程:

#!/bin/bash
# build-rpm.sh

SPEC_FILE="$1"
if [ -z "$SPEC_FILE" ]; then
    echo "Usage: $0 <spec-file>"
    exit 1
fi

# 检查SPEC文件语法
rpmlint "$SPEC_FILE"
if [ $? -ne 0 ]; then
    echo "SPEC file has errors"
    exit 1
fi

# 构建RPM包
rpmbuild -ba "$SPEC_FILE"

# 检查生成的RPM包
find ~/rpmbuild/RPMS -name "*.rpm" -exec rpmlint {} \;

测试和质量保证

打包完成后,一定要测试。我一般会在干净的虚拟机上测试安装:

基本安装测试

# 安装
yum localinstall my-webapp-1.0.0-1.noarch.rpm

# 检查服务状态
systemctl status my-webapp

# 检查文件是否正确安装
ls -la /opt/my-webapp
ls -la /var/log/my-webapp

# 卸载测试
yum remove my-webapp

# 检查是否有残留文件
find / -name "*webapp*" 2>/dev/null

升级测试

这个很重要,要确保从旧版本升级到新版本不会出问题:

# 安装旧版本
yum localinstall my-webapp-1.0.0-1.noarch.rpm

# 修改一些配置文件
echo "test config" >> /etc/my-webapp/config.conf

# 升级到新版本
yum localinstall my-webapp-1.0.1-1.noarch.rpm

# 检查配置文件是否保留
cat /etc/my-webapp/config.conf

依赖测试

在最小化安装的系统上测试,确保依赖关系正确:

# 在干净的CentOS minimal上
yum localinstall my-webapp-1.0.0-1.noarch.rpm

如果缺少依赖,yum会提示你安装。

发布和分发

RPM包制作完成后,就要考虑怎么分发了。

本地YUM仓库

可以搭建一个本地的YUM仓库:

# 创建仓库目录
mkdir -p /var/www/html/repo

# 复制RPM包
cp ~/rpmbuild/RPMS/noarch/*.rpm /var/www/html/repo/

# 创建仓库元数据
createrepo /var/www/html/repo/

# 配置nginx或apache提供HTTP服务

然后在客户端配置YUM源:

cat > /etc/yum.repos.d/local.repo << EOF
[local]
name=Local Repository
baseurl=http://your-server/repo/
enabled=1
gpgcheck=0
EOF

私有仓库

对于企业环境,我推荐使用Nexus或者Artifactory(下一期来讲讲他们两个!!!)这样的仓库管理工具。它们提供了更好的权限控制和版本管理功能。

签名验证

生产环境的RPM包最好要签名:

# 生成GPG密钥
gpg --gen-key

# 签名RPM包
rpm --addsign my-webapp-1.0.0-1.noarch.rpm

# 导出公钥
gpg --export -a "Your Name" > RPM-GPG-KEY-yourname

总结

RPM打包确实是个技术活,但掌握了之后真的能大大提高工作效率。从手动部署到一键安装,从混乱的依赖关系到标准化的包管理,这个转变是质的飞跃。

如果这篇文章对你有帮助,别忘了点赞转发支持一下!想了解更多运维实战经验和技术干货,记得关注微信公众号@运维躬行录,领取学习大礼包!!!我会持续分享更多接地气的运维知识和踩坑经验。让我们一起在运维这条路上互相学习,共同进步!

公众号:运维躬行录

个人博客:躬行笔记

标签: none