
在Rocky Linux 8.5上部署Jenkins这件事听起来不算新鲜但真要把流水线做到能支撑跨平台CI/CD、还能把容器化构建跑得又快又稳很多人是踩了坑才回头补课的。我这套方案是在某制造企业的研发基础设施改造中一步步磨出来的前后在虚拟机环境里折腾了三个星期最后才沉淀成一套可以直接复用的流程。这次就把从零开始部署、调优到流水线落地的完整过程拆开讲包含Jenkins服务的安装配置、多节点跨平台调度、Docker容器构建加速、以及我实际踩过的那些坑。如果你正准备为团队搭建一套可靠、可扩展的持续集成平台这篇文章应该能帮你少绕不少弯路。1. 为什么选Rocky Linux 8.5做CI/CD底座——聊聊选型逻辑1.1 从CentOS停更说起8.5这个版本节点的重要性很多团队在2021年底遇到一个现实问题CentOS 8要停止维护了。我们原来那套CI环境跑在CentOS 7.9上稳定是稳定但Docker版本旧、内核特性跟不上尤其后面要上BuildKit和Kaniko明显感觉力不从心。后来在CentOS 8.5和Rocky Linux 8.5之间对比了一圈最终选了Rocky。原因很朴素它跟Red Hat Enterprise Linux 8的兼容性极佳而且8.5版本正好处于生命周期内能拿到后续的补丁更新。这个选择在工程师圈子里有过争议有人说直接用Ubuntu Server不香吗apt装东西多方便。但我们的环境里有大量内网的RPM包依赖、以及旧有的运维脚本全部基于RHEL兼容系切换到Debian系意味着要做一轮全面的兼容性测试。Rocky Linux最核心的价值是它不折腾——系统库路径、SELinux策略、systemd单元文件、yum/dnf生态全部熟悉迁移成本几乎是零。说白了运维老哥那套shell脚本改个包管理器别名就能继续用。1.2 跨平台CI/CD的需求拆解多架构构建的现实诉求再说说为什么要刻意强调跨平台三个字。我们的产品需要同时交付x86_64和ARM64两种架构的二进制包早期只跑在x86_64服务器上每次出ARM版本都得手动交叉编译偶尔一编译就是几小时还经常因为环境差异出问题。后来引入了多台ARM64的构建机就面临一个调度问题哪种任务该跑在哪种机器上这其实就是CI/CD平台从单机迈向集群的第一个分水岭。Jenkins本身支持多节点Agent架构我们可以通过标签Label把不同架构的节点分组。设计流水线时把需要交叉编译的步骤按标签调度这样x86节点只跑x86任务ARM节点只跑ARM任务各司其职。之后又加入了Windows和macOS的构建节点用来产出跨平台安装包这才算真正意义上的跨平台流水线。所以在部署Jenkins之前你得先想清楚一件事你的流水线未来需要在哪些操作系统、哪些CPU架构上运行这个决定了节点规划、工具链选型和网络拓扑。千万别等Jenkins跑起来了才开始考虑要不要加节点。2. 环境准备与Jenkins部署的完整过程2.1 初始化系统从裸机到可用状态我习惯先把服务器系统层面的坑填掉再装应用。这里有一段最基础的初始化脚本覆盖了时区、字符集、国内镜像源、常用工具这几项# 更新系统并安装基础依赖 sudo dnf update -y sudo dnf install -y vim wget curl tar unzip git java-11-openjdk-devel # 设置时区与时间同步 sudo timedatectl set-timezone Asia/Shanghai sudo systemctl enable --now chronyd # 优化文件描述符限制 echo -e jenkins soft nofile 65536\njenkins hard nofile 65536 | sudo tee /etc/security/limits.d/jenkins.conf把文件描述符调高到65536很关键。流水线并发一上来Jenkins主节点要同时维持大量Agent的通信连接默认的1024很容易出现Too many open files报错。另外Java版本我选了OpenJDK 11这个版本和当前主流Jenkins LTS版本适配度最好既不需要为JDK 17的新特性折腾权限模块也不会因为JDK 8太老而缺失一些并发工具。SELinux是一个隐藏的坑。Rocky Linux默认是Enforcing状态如果你不理会它后面配置nginx反向代理、让Jenkins读取Docker Socket时会遇到各种权限错误。我的做法是先把SELinux设置为Permissive确认一切正常后再为特定端口和进程单独加策略而不是直接永久关闭。这么做的理由很简单在真实生产环境里安全审计会要求SELinux处于开启状态我们后来给Jenkins的/var/lib/jenkins目录和8080端口单独写了一条allow规则既有安全性又不影响功能。2.2 安装Jenkins传统方式与容器化方式的抉择Jenkins官方在安装文档里给出了RPM包和Docker镜像两种主流方案。我在最初部署时有点纠结因为我们的目标是容器化构建有人顺水推舟说那干脆Jenkins本身也用容器跑。但仔细想过之后我放弃了容器化部署Jenkins主节点理由如下第一主节点需要管理大量凭据、SSH密钥和本地工具链容器化之后每次升级、重启都要处理数据卷的挂载和权限映射复杂度不低第二主节点如果跑在Docker里而构建任务又要调用Docker命令做容器化构建就产生了Docker-in-Docker的问题这在这个社区是被反复讨论过的反模式第三我们已经有了一套RPM管理的统一规范所有服务器都通过Ansible安装软件用RPM方式对运维自动化更友好。所以主机节点用以下方式安装# 添加Jenkins官方软件源 sudo wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io-2023.key # 安装最新LTS版本 sudo dnf install -y jenkins # 启动服务并设置开机自启 sudo systemctl enable --now jenkins安装完成后Jenkins默认端口是8080。初次访问会要求输入一个随机管理密码位置在/var/lib/jenkins/secrets/initialAdminPassword。这里要注意如果使用了自动化的配置管理工具Ansible、Puppet等你应该先读取这个密码用它完成第一次登录后立刻创建自己的管理员账号并禁用初始用户。2.3 反向代理与HTTPS配置安全访问的细节默认8080端口直接暴露给内网其实问题不大但如果Jenkins要对外开放、或者和公司的单点登录系统对接必须用Nginx做一层反向代理统一入口并加TLS。我们的场景是在内网通过域名访问配了一张内部签发的证书。Nginx配置样例server { listen 443 ssl http2; server_name ci.internal.example.com; ssl_certificate /etc/nginx/certs/ci.crt; ssl_certificate_key /etc/nginx/certs/ci.key; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_max_temp_file_size 0; proxy_connect_timeout 150; proxy_send_timeout 100; proxy_read_timeout 100; proxy_buffer_size 8k; proxy_buffers 4 32k; proxy_busy_buffers_size 64k; proxy_temp_file_write_size 64k; } }抄完这段配置别忘了修改Jenkins的系统管理地址。进入系统设置找到Jenkins URL把它改成https://ci.internal.example.com。如果不改通过Nginx访问时很多页面会生成错误的相对路径导致CSS样式丢失、构建日志打不开。还有一招WebSocket代理如果是新版Jenkins2.300以上已经默认支持WebSocket连接AgentNginx配置里需要加上proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade;否则Agent会断连。这个细节在高并发连接时尤其重要。3. 流水线脚本设计思路与关键配置3.1 从自由风格任务到Declarative Pipeline一次彻底的革命最早团队里用的是自由风格任务界面上点一点就能配置源码地址、构建命令、归档路径好处是零门槛坏处是一旦项目多了每个任务的配置都是黑盒。别人的任务是怎么构建出来的无人知晓。所以我顶着压力把所有构建迁移到了Declarative Pipeline把完整的构建流程以Jenkinsfile的形式和项目源代码存放在一起。这么做的收益立竿见影代码评审时能一并评审CI配置换新机器部署时不需要人工复制任务配置Git分支策略直接映射到流水线行为。一个最小可用的流水线长这样pipeline { agent { label linux-x64 } options { timestamps() timeout(time: 30, unit: MINUTES) buildDiscarder(logRotator(numToKeepStr: 20)) } stages { stage(Checkout) { steps { checkout scm } } stage(Build) { steps { sh make build } } stage(Test) { steps { sh make test } } stage(Archive) { steps { archiveArtifacts artifacts: dist/**/*.tar.gz } } } post { failure { emailext ( to: dev-teamexample.com, subject: 构建失败: ${env.JOB_NAME} - ${env.BUILD_NUMBER}, body: 请查看 ${env.BUILD_URL} 获取详细日志。 ) } } }看起来简单但几个点值得解释。timestamps()会给每行日志加上时间戳排查近几个小时的执行缓慢问题时非常有用。timeout保证任何异常卡住的任务都能在30分钟后自动结束避免占用Agent资源。buildDiscarder限制构建记录最多保留20份防止磁盘被一堆无用历史包塞满。3.2 多节点与标签调度让不同架构的任务各回各家跨平台流水线的核心是标签调度。我们规划了三类Agent标签标签操作系统架构用途linux-x64Rocky Linux 8.5x86_64主构建、单元测试linux-arm64Ubuntu 20.04ARM64交叉编译、ARM容器镜像windows-x64Windows Server 2022x86_64Windows安装包制作在Jenkins的节点管理里每个Agent都要勾选对应的标签。流水线里通过agent { label linux-x64 }硬性指定。更灵活的做法是给参数化构建加一个选择器pipeline { agent { label ${params.BUILD_ARCH arm ? linux-arm64 : linux-x64} } parameters { choice(name: BUILD_ARCH, choices: [x64, arm], description: 选择构建架构) } // 其余省略 }这样开发者在页面上就能自由选择构建目标架构。但我个人其实更推荐自动化判断比如根据Git分支名或代码里某个配置文件自动决定目标架构而不是靠人工选择——人工选择这件事在深夜出紧急修复的时候是最容易出错的。跨平台构建还有一个隐蔽问题不同平台的Shell语法不通。sh make build在Linux上没问题但Windows Agent上要改成bat make build或powershell ...。所以在流水线里可以用bat和sh配合isUnix()函数区分steps { script { if (isUnix()) { sh make build } else { bat make build } } }3.3 凭据管理与参数化构建把密钥从代码里赶出去很多初学者把Git密码或镜像仓库密码直接写在Jenkinsfile里这是最糟糕的实践。一旦代码仓库泄露凭据就全完了。Jenkins提供了凭据绑定插件Credentials Binding Plugin和文件夹级别的凭据管理。我的习惯是在Jenkins中预先添加Username with password类型的凭据然后在流水线里引用withCredentials([usernamePassword(credentialsId: gitlab-account, usernameVariable: GIT_USER, passwordVariable: GIT_PASSWORD)]) { sh git clone http://${GIT_USER}:${GIT_PASSWORD}git.internal.example.com/group/repo.git }参数化构建则是把需要频繁变动的版本号、构建模式、是否发布等做成参数让构建触发方式更灵活。要注意的是参数值会被记录到构建日志中如果其中包含敏感信息比如密码记得在系统设置中打开隐藏已保存的密文选项否则会出现信息泄露的隐患。4. 容器化构建的落地方法与优化技巧4.1 在Jenkins中使用Docker进行容器化构建的三种姿势容器化构建并不是简单地在流水线里加一行docker build就完事。我整理了下主流方案有三种第一种是直接在Agent节点上安装Docker流水线里通过sh调用宿主的Docker命令。这种方案最直接但会产生一个严重问题因为同一个Jenkins主节点上可能有多个并发构建同时操作Docker彼此之间缺乏隔离特别是在构建自定义镜像时两条不相关的流水线可能会互相污染。第二种是使用Jenkins官方Docker插件Cloud中的Docker Agent。它会在需要构建时动态启动一个Docker容器作为临时Agent构建完再销毁。这种方式隔离性好但需要预先准备好包含完整工具链的镜像也要求网络足够快能随时拉取镜像。第三种是使用Kaniko在容器内构建容器镜像不需要宿主的Docker守护进程。这个方案在Kubernetes集群环境里近乎是标准做法因为容器内没有再嵌套Docker的权限。但Kaniko在构建复杂镜像时偶尔会遇到莫名其妙的缓存问题需要额外调参。我们最终采取的是第二种和第三种结合的方案常规Java、Node.js项目直接用Docker Agent动态构建需要打包成Docker镜像的项目用Kaniko在临时Agent里做镜像构建和推送。这样可以彻底规避Docker-in-Docker的安全漏洞同时让构建环境保持干净。4.2 Docker构建加速镜像缓存与并发控制容器化构建最痛的痛点之一是慢。一个包含编译、打包、推镜像全流程的任务如果每次都从零开始下载基础镜像和依赖耗时轻松翻两三倍。我们的优化手段可以总结为三招第一招启用BuildKit和缓存挂载。在Dockerfile中把包管理器缓存目录设置为RUN --mounttypecache这样多次构建之间可以复用已下载的依赖包。以Maven为例FROM maven:3.8-eclipse-temurin-11 AS builder RUN --mounttypecache,target/root/.m2 \ mvn clean package -DskipTests第二招在Jenkins中使用docker cache-from参数。对于有固定基础镜像的项目可以先把上一次构建生成的镜像拉下来作为缓存来源docker pull registry.internal/backend:latest docker build --cache-from registry.internal/backend:latest -t registry.internal/backend:${BUILD_TAG} .第三招合理控制并发构建数。不控制的话两个大型构建同时跑磁盘IO和内存会被瞬间打爆。我在Jenkins的系统设置中限制了主节点最多2个并发任务每个Docker Agent节点额外设置Timeout和instanceCap保证资源始终在可控范围内。4.3 不搞坏宿主机的Docker安全策略容器化构建容易招来安全审计的麻烦因为构建过程通常需要向守护进程传递Socket权限。我们的做法是在Agent节点上创建一个独立的jenkins-docker组把Jenkins用户加入这个组但只允许其访问特定的容器运行时API而不是开放整个宿主Docker。另外非常重要的一点是永远不要在生产流水线里使用一个以root身份运行、并挂载了/var/run/docker.sock的容器。一旦代码仓库被恶意提交攻击者可以直接接管宿主机。如果实在需要Docker-in-Docker可以改用Docker的--privileged加上dind镜像但必须在网络层面做严格限制并且只在专用构建机上使用。5. 跨平台CI/CD的经验避坑与优化实录5.1 多平台构建成功但产物却无法运行检查这三处我们曾经遇到一个诡异的问题在Linux x86_64上编译出的静态二进制文件运行在ARM64机器上直接报Exec format error。表面上是架构不匹配但深挖下去发现是因为跨平台工具链的配置有问题。这里有三处极容易踩坑编译器前缀。用aarch64-linux-gnu-gcc交叉编译后检查产物格式应该使用file命令确保输出显示ELF 64-bit LSB executable, ARM aarch64。如果显示的是x86说明Makefile里的CC变量没有被正确赋值。链接器与系统库。交叉编译的动态链接程序需要指向目标平台的系统库目录否则链接时可能错误地抓取宿主机的libc.so导致产物一运行就崩溃。Go语言的特殊性。如果你用Go写后端跨平台编译根本不需要交叉工具链只需设置GOOSlinux GOARCHarm64。不知道这一点就会白白多搭一套复杂的编译环境。我还专门写过一个流水线小片段来做产物自动校验stage(Verify Artifact) { steps { sh file dist/app readelf -h dist/app | grep Machine } }这样每次构建完都能立刻发现架构是否出错省得等到部署阶段才暴露问题。5.2 Agent节点连接异常一个低配机器引发的蝴蝶效应有一段时间ARM64 Agent时不时就掉线。排查了很久直到看了jenkins-slave的日志才发现是Agent节点上JVM的堆内存开得太小。默认情况下Agent启动脚本只分配了较小的内存一旦构建任务稍微重一点GC就会频繁触发最终导致Agent进程假死。解决方案是在Agent节点的启动参数中显式指定java -Xmx2048m -Xms1024m -jar agent.jar -jnlpUrl http://ci.internal.example.com/computer/arm64/slave-agent.jnlp -secret secret-file另外如果使用JNLP连接即Agent主动连入Master的方式必须保证主节点和Agent之间的防火墙能建立WebSocket连接。Nginx反向代理的场景下记得在server块中升级Upgrade头。我一开始漏配了这个结果Agent每隔十来分钟就掉一次线。5.3 容器化构建资源清理与磁盘空间管理Jenkins跑久了磁盘被占满几乎是必然事件。最典型的是Docker镜像堆积和Workspace残留。我们做了三层防护第一层天级清理每天凌晨执行一次docker system prune -af和jenkins的工作空间清理只保留最近3天的构建产物。第二层构建级清理在流水线的post部分根据分支决定是否删除该次构建的Workspace。比如feature/*分支构建结束后自动删除工作空间main分支则保留以缓存依赖。第三层记录级清理通过buildDiscarder限制每个任务保留最近20次构建记录同时定期清理/var/lib/jenkins/logs下的运行日志。有一次就是因为新加的一个任务把所有构建历史都保留着每个历史构建都有几百MB的制品两个月不到就吃了900GB。后来我补了一张磁盘占用监控告警指标一超过80%就给运维发消息从此再也没有半夜被磁盘写满的告警惊醒。5.4 权限设计不能让所有人都能改流水线跨团队协作时Jenkins的权限管理要提前规划好。我们按照项目-环境-角色三维模型来控制权限角色权限范围开发工程师在项目文件夹内触发构建、查看日志编译管理员修改Jenkinsfile、调整构建参数、管理凭据平台管理员管理节点、全局配置、安装插件具体操作是在Jenkins系统设置中开启基于项目的矩阵授权策略把不同用户组映射到对应文件夹。同时启用文件夹Folder插件把同一产品的多个流水线放在一个文件夹下权限可以继承避免每个任务重复配置。这套设计的好处是开发人员再也不用担心自己误改了全局设置把整个平台搞崩。6. 一些实操阶段沉淀下来的心得折腾这套Jenkins平台的过程中我踩过最大的坑其实不是技术本身而是盲目追求面面俱到。最开始听说Kubernetes能提供动态Agent我非要在一台4核8GB的机器上跑一套K3s结果资源严重不足流水线反而变得又慢又不稳定。后来老老实实回到最简单的虚拟机多节点架构3台专用构建机一台跑Master两台分别跑x86和ARM的Agent反而是最稳定、最容易维护的形态。还有一个心得是关于流水线脚本的版本控制。一定不要把人写的流水线脚本放在Jenkins内部编辑应当提交到git仓库由Jenkinsfile从仓库读取。这样流水线演进能被记录、审阅出了事故也能通过回滚代码来恢复。我们还加了一个约定任何人修改CI配置必须附带一条强制注释说明原因这个简单的习惯大大降低了团队的沟通成本。再分享一个小技巧给每个构建增加一个描述性的显示名称。比如在流水线里加一行stage(Set build name) { steps { buildName #${BUILD_NUMBER}-${params.BUILD_TARGET}-${params.BUILD_ARCH} } }有了这个构建历史页面上一眼就能看出哪条任务是哪个分支、哪个架构、哪个目标再也不用点进去看具体参数排查问题的时候省下大量时间。如果你刚开始搭建这套平台我的建议是先做一份现状盘点列出你们所有需要构建的项目类型、目标平台、工具链、触发方式。然后再考虑怎么部署、怎么写流水线。技术永远为现实的工程需求服务想明白这个后面每一步都会走得特别顺畅。