导语:为什么要折腾这套流水线?
三月份我的饭搭子球搭子离职了,雪上加霜的是他的工作一部分也分给了我。别的还好说,就是每次提版本时打的包实在是太多了。如果全靠手动开虚拟机、拉 SVN 代码、敲 make 编译再打包,不仅耗时耗力,而且极易出错。
为了彻底解决这个"体力活",一劳永逸地提升交付效率,我决定从 0 到 1 搭建一套基于 Docker 容器化环境隔离 + Jenkins 并行调度 + Harbor 私有镜像仓 的完全自动化 CI/CD 流水线。
本文将复盘整个搭建全过程,并附上核心脚本与踩坑记录,希望能帮助到团队后续的维护同学,以及正在折腾构建系统的你。
核心架构概览
在这套流水线中,各个组件各司其职:
- 基础环境层 (Docker) :利用 Docker 容器的轻量级和隔离性,为每个目标系统制作专属的纯净编译环境镜像,实现一台物理机同时跑多个不同的操作系统。
- 存储分发层 (Harbor) :搭建企业内网私有仓库。统一存储和分发上述做好的基础镜像,方便团队复用(毕竟这些镜像的制作和配置还是比较费时间的)。
- 调度控制层 (Jenkins) :作为流水线的"大脑",通过 SSH 连入编译节点,提供可视化的按需参数化构建,并控制多个系统的容器并发启动 ,最后统一收集编译产物(
.deb/.rpm/run包)。
一、 基石构建:从 ISO 系统镜像到 Docker 编译镜像
要用 Docker 编译国产系统代码,首先得有对应的基础镜像。由于公有云仓库往往缺乏特定版本(尤其是内网特定 SP 版本)的国产 OS 镜像,我们需要手动从光盘 ISO 镜像中"榨"出系统底包。
1. 手把手教你从 ISO 提取 Rootfs (根文件系统)
这部分网上的教程往往语焉不详,其实核心原理就是把 ISO 里自带的 Live 系统文件提取出来打包给 Docker。
Step 1:挂载 ISO 镜像 找一台 Linux 服务器,把下载好的系统镜像(比如麒麟 V10 的 ISO)放进去,创建一个挂载点并挂载:
Bash
bash
mkdir -p /mnt/iso
mount -o loop Kylin-Desktop-V10-SP1.iso /mnt/iso
Step 2:寻找并解压 Squashfs 文件 进入挂载目录,你需要找到一个体积最大的核心系统压缩文件,通常叫 squashfs.img 或者 filesystem.squashfs(一般藏在 casper/ 或 LiveOS/ 目录下):
Bash
bash
cd /mnt/iso/casper/ # 具体目录根据不同系统 ISO 可能不同
接下来,我们需要用 unsquashfs 工具把它解压出来(如果提示没有该命令,先执行 apt install squashfs-tools 或 yum install squashfs-tools 安装):
Bash
unsquashfs filesystem.squashfs
执行完后,当前目录下会生成一个名为 squashfs-root 的文件夹,这就是一个完整的、原汁原味的 Linux 根目录(里面有 bin、etc、lib 等)。
Step 3:打包并导入 Docker 进入解压出来的根目录,将其打包并直接通过管道"喂"给 Docker:
Bash
arduino
# 注意命令最后的点号 '.' 代表当前目录下的所有文件
tar -C squashfs-root -c . | docker import - my-kylin-base:v1
测试一下是否成功:docker run -it my-kylin-base:v1 /bin/bash,如果顺利进入终端,恭喜你,基础底包制作成功!清理战场:umount /mnt/iso。
2. 制作完整编译环境镜像的两种姿势
拿到纯净版基础镜像后,需要安装 gcc、cmake、svn 等编译依赖(由于内网源的特殊性,这一步往往需要特别配置)。
方式 A:交互式构建 (docker run + commit) ------ 适合前期摸索(快速但不规范-我是用的这种-因为还要进去配源)
- 操作 :直接拉起容器进终端
docker run -it my-kylin-base:v1 /bin/bash。 - 配置 :在里面手动敲
apt update、安装依赖、配置环境变量(感谢之前同事提供的内网源配置)。 - 保存 :退出容器后,找到刚退出的容器 ID,执行
docker commit <容器ID> 10.x.x.x:8001/library/kylin-build:v1保存为新镜像。 - 评价:操作直观,所见即所得;但过程不可复现,如果未来换源或者升级依赖,还得从头再来,俗称"黑盒镜像"。
方式 B:声明式构建 (Dockerfile) -推荐
-
操作 :找个空目录,编写
Dockerfile脚本。将内网源配置文件提前放在同级目录,通过COPY拷入。Dockerfile
sqlFROM my-kylin-base:v1 # 替换内网源(假设当前目录有 sources.list) COPY sources.list /etc/apt/sources.list RUN apt-get update && apt-get install -y gcc g++ cmake subversion ENV LANG=zh_CN.UTF-8 CMD ["/bin/bash"] -
构建 :
docker build -t 10.x.x.x:8001/library/kylin-build:v2 . -
评价:属于理想很丰满现实很骨感那种,前期写起来需要多次调试比较麻烦,不过一旦摸索出完美的配置,后边可以复用起来。
二、 弹药库:Harbor 仓库的搭建与使用
为了让内网的多台编译机都能快速拉取镜像,搭建私有 Harbor 仓库是必经之路。
1. 详细搭建与 harbor.yml 配置
从 Harbor 官方下载离线安装包(offline installer)并解压后,最重要的一步就是修改配置文件。
-
复制模板文件:
cp harbor.yml.tmpl harbor.yml -
使用 vim 编辑
harbor.yml,重点修改以下几个地方:YAML
yaml# 1. 主机名:必须改成你服务器的真实内网 IP,千万不要用 localhost 或 127.0.0.1 hostname: 10.*.*.* # 2. HTTP 端口:默认 80,如果被占用了可以改,比如改成 8001 http: port: 8001 # 3. HTTPS 配置:【关键避坑】因为我们是纯内网环境,没有搞 SSL 证书, # 所以必须把 https: 及其下面的 port, certificate, private_key 这几行全部注释掉或删掉! # https: # port: 443 # certificate: /your/certificate/path # private_key: /your/private/key/path # 4. 管理员密码:设置一个你能记住的密码 harbor_admin_password: YourStrongPassword # 5. 数据存放路径:一定要改到一个磁盘空间足够大的目录 data_volume: /data/harbor_data -
运行准备脚本生成配置:
./prepare -
一键拉起所有服务:
./install.sh
2. 镜像的上传与拉取
-
致命踩坑 :因为我们用的是无证书的 HTTP 部署,必须在所有需要连接 Harbor 的宿主机(包括你的编译机)上的
/etc/docker/daemon.json中配置信任该仓库:JSON
json{ "insecure-registries": ["10.*.*.*:8001"] }配置完别忘了重启 Docker 服务:
systemctl restart docker。 -
上传流程:
Bash
markdown# 1. 登录 Harbor (输入你配置的 admin 账号密码) docker login 10.*.*.*:8001 # 2. 按规范打标签 (镜像名必须以你的仓库地址开头) docker tag kylin-build:v2 10.*.*.*:8001/library/kylin-build:v2 # 3. 推送镜像到仓库 docker push 10.*.*.*:8001/library/kylin-build:v2
三、 大脑中枢:Jenkins 的离线部署与节点接入
1. 外网制作 Jenkins 离线镜像 (包含核心插件)
因为目标服务器断网,如果直接在内网装原生 Jenkins,你会被无数个安装失败的插件逼疯。最优雅的方式是在有网的电脑上,把 Jenkins 连同我们需要的插件,一起打包成离线 Docker 镜像。
在有网的电脑上操作: 新建一个 Dockerfile,填入以下内容:
Dockerfile
bash
FROM jenkins/jenkins:lts
# 切换为 jenkins 用户下载插件
USER jenkins
# 提前下载这几个核心流水线、SSH 节点和 SVN 密码管理必备的插件
RUN jenkins-plugin-cli --plugins "workflow-aggregator ssh-slaves blueocean subversion credentials-binding"
执行编译并导出镜像:
Bash
perl
docker build -t my-jenkins-offline:v1 .
docker save -o jenkins_offline.tar my-jenkins-offline:v1
在内网服务器上操作: 把 jenkins_offline.tar 传进内网并导入:
Bash
css
docker load -i jenkins_offline.tar
# 启动时务必注意宿主机端口冲突(比如常见的 8080 被占用,换成 8088)
docker run -d --name jenkins --restart=always -p 8088:8080 -v /data/jenkins_home:/var/jenkins_home my-jenkins-offline:v1
打开网页后,直接选择"跳过插件安装" (因为我们已经提前打在镜像里了),创建管理员账号即可进入主页。
2. 接入编译节点 (SSH Node)
Jenkins 自身不干重活,需要通过 SSH 将任务下发给真实的飞腾/x86 编译物理机。
- 核心配置 :在节点管理中,选择
Launch agents via SSH,填入编译机 IP 和普通账号(如zhongfu)凭据。一定要将Host Key Verification Strategy设置为Non verifying以免因为找不到 SSH 指纹报错。 - 高频踩坑 1 (Java 版本冲突) :Jenkins 需要向节点推送
remoting.jar通讯程序,较新的 Jenkins 要求节点机器必须有 Java 17 。老机器自带的 Java 8 会直接导致连接失败。解决方法是下载免安装版的 JDK17 离线包解压到编译机,并在 Jenkins 节点设置的Advanced -> JavaPath中精准指定新版 Java 路径(例如/usr/local/jdk-17/bin/java)。 - 高频踩坑 2 (Docker 权限) :使用普通账号连接节点跑代码时,必须将该账号加入 docker 组 (
sudo usermod -aG docker zhongfu),否则 Jenkins 一调 Docker 就会报permission denied惨遭拒绝。
四、 核心双引擎:双脚本解析
整个自动化的灵魂在于"内外两层"脚本的完美配合。
1. 外层控制权:Jenkinsfile (声明式流水线)
这是运行在 Jenkins 上的 Groovy 脚本,负责可视化按需调度、并发控制和 SVN 密码保护。
Groovy
php
pipeline {
agent { label 'build-server' } // 精准空降到你配置好的编译机节点
parameters {
// 提供 Web 界面上的可视化勾选框和输入框,想编哪个勾哪个
string(name: 'B_VERSION', defaultValue: 'V0.*.*.*', description: '打包版本号')
booleanParam(name: 'BUILD_KYLIN', defaultValue: true, description: '编译: debian')
booleanParam(name: 'BUILD_UOS', defaultValue: true, description: '编译: UOS')
}
environment {
// 安全获取 SVN 密码,通过 Jenkins 的 Credentials 管理,绝不把明文密码写在脚本里
SVN_CREDS = credentials('svn-account')
}
stages {
stage('多系统并行编译') {
parallel { // 并行语法,瞬间拉满机器 CPU,极大缩短总时间
stage('debian') {
when { expression { params.BUILD_KYLIN } } // 读取界面参数,决定是否执行
steps {
// 强烈建议加上 #!/bin/bash,强制使用完整 Bash 环境
sh """#!/bin/bash
cd /data/build/kylin
# 使用 sed 将 Jenkins 变量动态注入配置文件
# 注意:由于 Groovy 和 Bash 变量语法的冲突,这里 sed 最外层使用单引号最稳妥
sed -i 's|^VERSION=.*|VERSION="${params.B_VERSION}"|g' config.ini
bash auto_build.sh
"""
}
}
// ... 平行复制其他系统 stage
}
}
stage('产物归档') {
steps {
// 自动收集各系统生成的安装包,方便在网页端一键下载
archiveArtifacts artifacts: '**/out/*.deb', allowEmptyArchive: true
}
}
}
}
2. 内层苦力工:auto_build.sh (底层打包脚本)
这是真正跑在宿主机目录里,负责拉起 Docker 容器执行脏活累活的脚本。 核心逻辑非常干脆:
svn update更新最新代码。- 启动对应系统的纯净镜像:
docker run --rm -v /data:/workspace 10.*.*.*:8001/library/kylin-build:v2 bash -c "cd /workspace && make" - 将编译出的文件打包成
.deb、.rpm或最终压制成自解压的run包。
五、 血泪总结:实战中的高频避坑指南
在打通这整条自动化链路的过程中,我踩过了几个极具代表性的坑,这里全盘托出,帮后人避雷:
坑一:Docker TTY 限制导致的"秒挂假成功"
- 现象:将编译脚本接入 Jenkins 并行执行时,任务 0.1 秒就结束并亮起绿灯提示成功,但去目录一看根本没生成包。
- 原因 :脚本里写了
docker run -it。后台自动化执行时没有分配真实的物理终端(TTY),Docker 一看没终端直接崩溃。又因为脚本里写了管道符| tee log,管道吞掉了 Docker 崩溃的错误码,导致外层判定成功。 - 解法 :后台自动化跑 Docker 绝对不能带
-it参数 ,直接用docker run --rm;并且在执行脚本开头加上set -o pipefail,确保管道中任何一步报错都能直接炸出来,杜绝"假成功"。
坑二:底层 Bash 的 Bad substitution 报错
- 现象 :Jenkins 执行
sh """ ... """代码块时,明明 Bash 语法没问题,却报Bad substitution错误。 - 原因 :Jenkins 的
sh步骤默认调用的是目标系统极其简陋的/bin/sh(通常是指向 dash 的软链接),它根本不支持${数组[@]}等高级字符串替换语法。 - 解法 :在
sh """代码块内的第一行,紧贴着写上#!/bin/bash作为文件头,强制 Jenkins 使用功能完整的 Bash 解释器。
坑三:著名的"Root 幽灵" (文件权限冲突)
- 现象 :Jenkins 跑着跑着,在执行
chmod或者清理旧文件时,爆出红色的不允许的操作 (Operation not permitted)。 - 原因 :前期手动测试时你可能用了
root账号去svn checkout,或者 Docker 容器内部(默认是 root 身份)编译产生的文件落盘到了挂载的宿主机目录。这导致文件归属权全变成了 root。等正式跑的时候,Jenkins 以普通用户(zhongfu)身份进来,想改 root 老大的东西直接被系统保安拦死。 - 解法 :如果权限乱了,最彻底的解决办法是用
root账号在宿主机终端执行一波终极大法:chown -R zhongfu:zhongfu /data/你的工作目录,把文件的控制权统统抢回给 Jenkins 的运行账号。
结语
从过去手动挨个进虚拟机敲命令,到如今只需在 Jenkins 页面上"勾选想要的系统,填入版本号,点下运行,喝口茶的功夫(需要四十分钟 哈哈哈 不过要快六倍 还不会出错),安装包就全自动吐出来了",这种工业化流水的效率飞跃是极其震撼的。
目前实现了一台机器可以打6个系统的子系统包,另一台机器打所有最终的run包,由于都是jenkins的重复使用,就没在文中赘述,希望这种思路可以帮到大家。