一套搞定!基于 Docker + Jenkins + Harbor 的国产多系统自动化编译流水线实战全纪录

导语:为什么要折腾这套流水线?

三月份我的饭搭子球搭子离职了,雪上加霜的是他的工作一部分也分给了我。别的还好说,就是每次提版本时打的包实在是太多了。如果全靠手动开虚拟机、拉 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-toolsyum install squashfs-tools 安装):

Bash

复制代码
unsquashfs filesystem.squashfs

执行完后,当前目录下会生成一个名为 squashfs-root 的文件夹,这就是一个完整的、原汁原味的 Linux 根目录(里面有 binetclib 等)。

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. 制作完整编译环境镜像的两种姿势

拿到纯净版基础镜像后,需要安装 gcccmakesvn 等编译依赖(由于内网源的特殊性,这一步往往需要特别配置)。

方式 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

    sql 复制代码
    FROM 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)并解压后,最重要的一步就是修改配置文件。

  1. 复制模板文件:cp harbor.yml.tmpl harbor.yml

  2. 使用 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
  3. 运行准备脚本生成配置:./prepare

  4. 一键拉起所有服务:./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 容器执行脏活累活的脚本。 核心逻辑非常干脆:

  1. svn update 更新最新代码。
  2. 启动对应系统的纯净镜像:docker run --rm -v /data:/workspace 10.*.*.*:8001/library/kylin-build:v2 bash -c "cd /workspace && make"
  3. 将编译出的文件打包成 .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的重复使用,就没在文中赘述,希望这种思路可以帮到大家。

相关推荐
AskHarries9 小时前
把一个外部系统接成 MCP 工具
后端·程序员
threerocks10 小时前
AI编程的商业模式已经在互联网大厂跑通了
程序员·aigc·ai编程
用户5268356779010 小时前
云原生落地:如何配置 Alertmanager 插件,将 Prometheus 告警直接打通至硬件声光语音终端?
程序员
用户8524950718410 小时前
我跟 AI 说了名字它转头就忘,后来我手动给它加了个"记忆"
程序员
zzzzzz31010 小时前
当甲方说'logo放大的同时再缩小一点'时,我用 AI 把这个需求做出来了
javascript·css·程序员
Hilaku10 小时前
Node.js 还能再战十年?给你一个不换引擎的理由
前端·javascript·程序员
Hyyy1 天前
token是什么?为什么大模型会有上下文长度的限制
程序员·llm·ai编程
程序员cxuan1 天前
幽默,一个 Github 名字叫“马尾辫”,但是他给你省了 80% 的 token
人工智能·后端·程序员
kartjim1 天前
我用 AI 一小时写了一个世界杯数据可视化平台|前端 VibeCoding 初体验
前端·程序员·ai编程
曲幽1 天前
别再用网页翻译看源码了!你的私人翻译神器LibreTranslate,部署避坑指南来了
python·docker·web·pot·translate·libretranslate·arogstranslate