1. 为什么我把 CI/CD 的整套链路押在 Jenkins 加 GitLab 上
如果你正在带一个小团队做持续集成和持续交付,大概率绕不开一个经典组合:GitLab 管代码,Jenkins 管流水线。这套组合不是最新潮的,但它是目前落地成本最低、可控性最强、社区资料最厚的一套方案。我从几年前开始把团队的构建、测试、镜像打包、部署全部收拢到这条链路上,中间换过别的工具,最后还是回到这里。原因很简单:Jenkins 的插件生态几乎能对接任何你能想到的工具,GitLab 又能把代码托管、合并请求、Webhook 触发做得足够顺滑,两者拼在一起,就是一条从提交代码到服务上线的完整自动化通道。
这篇文章我想聊的不是“Jenkins 是什么”这种入门概念,而是把 Jenkins 和 GitLab 真正串起来之后,怎么设计流水线、怎么处理触发、怎么管凭据、怎么排查那些让人抓狂的构建失败。适合已经装过 Jenkins、也用过 GitLab,但还没把两者打通,或者打通了但总觉得不稳、不快的同学。我会把每一步背后的“为什么”讲清楚,包括参数怎么算、插件怎么选、坑在哪里。你照着做,基本能搭出一条能跑生产、能扛住日常迭代的流水线。
先说清楚这套方案解决的核心问题:代码提交之后,人工去构建、人工去部署,这件事在迭代频率低的时候还能忍,一旦一天要发好几次,人工操作就会变成瓶颈和事故源。Jenkins 加 GitLab 的价值,就是把“提交代码”这个动作,自动翻译成“构建、测试、部署”一连串动作,并且把结果反馈回 GitLab 的合并请求里。开发提交完代码,去喝杯水,回来就能看到构建结果和部署状态。这就是它最朴素也最核心的价值。
2. 整体链路设计与选型背后的取舍
2.1 为什么是 Jenkins 而不是别的
市面上做 CI/CD 的工具不少,有 SaaS 的,有自托管的,也有跟代码平台深度绑定的。我选 Jenkins 主要看三点。第一是可控性,Jenkins 可以完全部署在自己的机器上,构建环境、网络策略、依赖缓存都由自己说了算,这对有内网构建需求、或者构建产物不方便外传的场景非常关键。第二是插件生态,GitLab 的 Webhook 触发、凭据管理、构建后回写状态,这些都有成熟插件,不用自己造轮子。第三是流水线即代码,Jenkinsfile 可以跟代码一起进版本库,流水线的变更也能被 review,这一点对团队协作太重要了。
GitLab 这边,它自带的 CI 其实也能做流水线,但很多团队还是把构建交给 Jenkins,原因是 Jenkins 的构建节点管理更灵活,能挂多台不同环境的机器,做并行构建、做跨平台构建都更顺手。所以常见的分工是:GitLab 负责代码和触发,Jenkins 负责执行和反馈。
2.2 触发方式的选择:Webhook 还是轮询
触发方式有两种主流做法。一种是 Jenkins 定时轮询 GitLab 仓库,看有没有新提交;另一种是 GitLab 通过 Webhook 主动通知 Jenkins。轮询的缺点是延迟高、浪费资源,仓库多了之后 Jenkins 会被轮询任务拖垮。Webhook 是提交即触发,实时性好,资源消耗也低。所以只要网络允许,一律用 Webhook。
Webhook 的链路是这样的:GitLab 在收到 push 或 merge 事件后,向 Jenkins 暴露的一个固定地址发一个 HTTP POST 请求,Jenkins 收到后解析出仓库地址、分支、提交信息,然后启动对应的流水线任务。这里有个关键点,Jenkins 必须有一个能被 GitLab 访问到的地址,如果两者在同一内网,直接用内网地址即可;如果跨网络,就要保证这个回调地址可达。这是整套链路能不能跑通的第一道门槛。
2.3 凭据管理的设计原则
Jenkins 要拉 GitLab 的代码,就需要有访问权限。最忌讳的做法是把账号密码明文写在流水线脚本里。正确做法是用 Jenkins 的凭据管理功能,把 GitLab 的访问令牌存成凭据,在流水线里通过凭据 ID 引用。这样脚本里看不到明文,凭据也能集中轮换。访问令牌建议用项目访问令牌或者群组访问令牌,权限只给读代码库和回写提交状态这两项,遵循最小权限原则。这一点我在后面还会展开讲具体配置。
3. 环境准备与核心配置的实操细节
3.1 Jenkins 侧需要装哪些插件
Jenkins 装完之后,第一件事是补齐插件。跟 GitLab 集成相关的核心插件有这么几个:GitLab 插件负责处理 Webhook 和回写状态,Git 插件负责拉代码,Pipeline 插件负责流水线,Credentials Binding 插件负责在流水线里安全引用凭据。如果还要做镜像构建,再装 Docker Pipeline 插件。插件装完记得重启 Jenkins,让插件生效。
这里有个经验:插件不要贪多,装得越多,Jenkins 启动越慢,插件之间冲突的概率也越高。我见过一个 Jenkins 装了上百个插件,结果每次升级都提心吊胆。按需装,用不到的及时卸,这是长期维护的关键。
3.2 GitLab 侧生成访问令牌
在 GitLab 里进入用户设置或者项目设置,找到访问令牌的入口,创建一个令牌。权限范围勾选 read_repository 和 api,前者用来拉代码,后者用来回写提交状态。令牌生成后只显示一次,一定要立刻复制保存。这个令牌就是 Jenkins 访问 GitLab 的钥匙,丢了只能重新生成。
注意:令牌的权限范围要按最小必要原则勾选,不要图省事勾全权限。令牌一旦泄露,别人就能以你的身份操作仓库。
3.3 在 Jenkins 里创建凭据
进入 Jenkins 的凭据管理页面,新建一个凭据,类型选“用户名加密码”或者“Secret text”。如果用访问令牌,通常用 Secret text 类型,把令牌粘进去,给它起一个容易识别的 ID,比如 gitlab-token。这个 ID 后面会在流水线脚本里被引用。凭据的 ID 命名要有规范,团队里谁看到都知道它是干什么的,避免出现一堆 credential-1、credential-2 这种看不懂的名字。
3.4 配置 GitLab 连接
在 Jenkins 的系统配置里找到 GitLab 配置区域,填入 GitLab 的访问地址和刚才创建的凭据。这里可以配置多个 GitLab 实例,如果公司有多个代码平台,可以分别配置。配置完成后点测试连接,能通就说明 Jenkins 已经能访问 GitLab 了。这一步通了,后面的 Webhook 才有意义。
4. 打通 Webhook 触发与流水线编写
4.1 在 GitLab 项目里配置 Webhook
进入 GitLab 项目的 Webhook 设置页面,填入 Jenkins 的触发地址。这个地址的格式通常是 Jenkins 地址加上项目名加上 build 路径,具体格式跟 Jenkins 任务的类型有关。触发事件勾选 Push events 和 Merge request events,这样提交代码和合并请求都会触发构建。填完之后 GitLab 会发一个测试请求,如果 Jenkins 那边配置正确,会返回成功,Webhook 列表里会显示一个绿色的状态。
如果测试失败,先检查 Jenkins 地址是否可达,再检查 Jenkins 任务是否允许被远程触发。这两个是最常见的原因。
4.2 编写 Jenkinsfile 的核心结构
流水线用 Jenkinsfile 描述,放在代码库根目录。一个典型的 Jenkinsfile 包含几个阶段:拉代码、构建、测试、打包、部署。下面是一个结构示例,我把它拆开讲。
pipeline { agent any environment { GITLAB_TOKEN = credentials('gitlab-token') } stages { stage('Checkout') { steps { git branch: 'main', url: 'git@your-gitlab-host:group/project.git', credentialsId: 'gitlab-token' } } stage('Build') { steps { sh 'mvn clean package -DskipTests' } } stage('Test') { steps { sh 'mvn test' } } stage('Deploy') { steps { sh 'bash deploy.sh' } } } post { success { gitlabCommitStatus(name: 'pipeline', state: 'success') } failure { gitlabCommitStatus(name: 'pipeline', state: 'failed') } } }这段脚本里,agent any 表示用任意可用节点执行。environment 里引用了凭据,凭据会被注入成环境变量。stages 里每个 stage 是一个阶段,steps 里是具体命令。post 块负责在构建结束后把状态回写到 GitLab,这样在 GitLab 的合并请求页面就能看到构建结果。
4.3 回写状态到 GitLab 的意义
很多人配完触发就不管了,其实回写状态同样重要。开发在 GitLab 提了合并请求,如果看不到构建结果,就得切到 Jenkins 页面去查,体验很差。通过 gitlabCommitStatus 把状态回写,GitLab 的合并请求页面会直接显示构建通过还是失败,甚至能设置成构建不通过就不允许合并。这是把 CI 真正融入开发流程的关键一步。
回写状态需要 Jenkins 能访问 GitLab 的 API,所以前面配置的凭据和连接必须正确。如果回写不生效,先检查凭据权限里有没有 api 这一项。
5. 常见问题排查与避坑经验
5.1 Webhook 触发不生效怎么查
这是最高频的问题。排查顺序是这样的:先在 GitLab 的 Webhook 页面点测试,看返回码。如果是 404,说明 Jenkins 地址或路径写错了;如果是 403,说明 Jenkins 那边没开远程触发权限;如果是超时,说明网络不通。一步步缩小范围,基本都能定位。
还有一个隐蔽的坑:Jenkins 任务如果配置了参数化构建,Webhook 触发时可能因为缺少参数而失败。这种情况要么给参数设默认值,要么在 Webhook 里带上参数。
5.2 拉代码失败的原因
拉代码失败通常有三种原因。一是凭据不对,令牌过期或者权限不够;二是地址写错,SSH 地址和 HTTP 地址的写法不一样;三是 Jenkins 节点上没有配置对应的 SSH 密钥。如果是 SSH 方式拉代码,要在 Jenkins 节点上生成密钥对,把公钥加到 GitLab 的部署密钥里。HTTP 方式则用访问令牌即可,相对简单,推荐新手先用 HTTP 方式。
5.3 构建环境不一致导致的问题
本地能构建成功,Jenkins 上却失败,十有八九是环境不一致。比如本地 JDK 是 17,Jenkins 上是 8;本地有某个依赖缓存,Jenkins 上没有。解决办法是用容器化构建,把构建环境用 Docker 镜像固定下来,这样本地和 Jenkins 用的是同一个镜像,环境差异就消除了。这也是我强烈推荐的做法,虽然前期要多写一点 Dockerfile,但长期看省下的排查时间非常值。
5.4 凭据泄露的风险点
凭据泄露最常见的场景是流水线日志里打印了敏感信息。比如脚本里 echo 了令牌,或者构建工具把带密码的命令行参数打到了日志里。Jenkins 的凭据绑定插件会对注入的环境变量做掩码,但如果你手动把凭据拼进命令字符串,掩码可能失效。所以引用凭据一定要用插件提供的方式,不要自己拼接。
下面这张表是我整理的常见问题速查,遇到问题可以先对照排查。
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| Webhook 测试返回 404 | 地址或路径错误 | 核对 Jenkins 任务触发地址 |
| Webhook 测试返回 403 | 未开启远程触发 | 检查任务触发权限配置 |
| 拉代码报权限错误 | 令牌过期或权限不足 | 重新生成令牌并核对权限范围 |
| 构建成功但 GitLab 无状态 | 回写配置缺失 | 检查凭据是否含 api 权限 |
| 本地成功 Jenkins 失败 | 环境不一致 | 改用容器化构建统一环境 |
6. 流水线进阶优化与长期维护建议
6.1 并行构建缩短反馈时间
当项目变大,测试跑得越来越久,串行执行会让反馈周期变得很长。Jenkins 的 parallel 语法可以把互不依赖的阶段并行跑。比如单元测试和静态代码扫描可以同时进行,两个都跑完再进入打包阶段。并行能显著缩短流水线总时长,但要注意并行阶段之间不能有资源冲突,比如同时写同一个文件就会出问题。
6.2 构建缓存与依赖加速
每次构建都重新下载依赖,既慢又浪费带宽。可以在 Jenkins 节点上配置依赖缓存目录,或者用构建工具的本地仓库缓存。以 Maven 为例,把本地仓库目录挂载到节点上,第二次构建就能直接复用。容器化构建时,可以把缓存目录挂载进容器,这样既保持环境一致,又能享受缓存加速。
6.3 流水线的版本化管理
Jenkinsfile 一定要跟代码一起进版本库,不要直接在 Jenkins 页面上写脚本。页面上的脚本没法 review,改错了也没法回滚。放进版本库之后,流水线的每次变更都有记录,谁改的、改了什么、为什么改,一目了然。这是团队协作的基本要求。
6.4 定期清理构建历史
Jenkins 的构建历史会占用磁盘空间,时间长了磁盘会被撑满。要在任务配置里设置保留策略,比如只保留最近 30 次构建,或者只保留最近 7 天的构建。这个策略要按项目的重要程度来定,重要的项目多留一些,普通的项目少留一些。磁盘满了导致 Jenkins 挂掉,是运维事故里很常见的一种。
6.5 权限与安全加固
Jenkins 的权限要按角色划分,开发只能看和触发自己项目的构建,运维才有管理权限。GitLab 那边同理,访问令牌的权限要收紧。另外 Jenkins 的访问地址如果暴露在公网,一定要加访问控制,不能让任何人都能触发构建。构建环境里也不要存放跟构建无关的敏感数据,减少泄露面。
7. 我在实际落地中踩过的几个坑
第一个坑是 Webhook 的地址用了 localhost。当时 Jenkins 和 GitLab 不在同一台机器上,我在 Webhook 里填了 localhost,结果 GitLab 发请求发到了自己身上,自然失败。后来改成 Jenkins 的实际地址就好了。这个坑很基础,但确实容易犯,尤其是本地测试环境迁移到正式环境的时候。
第二个坑是凭据 ID 写错。流水线里引用的凭据 ID 跟实际创建的 ID 不一致,构建时报找不到凭据。这种错误信息有时候不够直观,排查起来要绕一圈。后来我养成了习惯,创建凭据时就把 ID 复制下来,写脚本时直接粘贴,避免手打出错。
第三个坑是构建节点磁盘满了。有一次流水线突然全部失败,查了半天发现是构建节点磁盘满了,日志都写不进去。从那以后我给所有节点加了磁盘监控,超过阈值就告警。这种问题不常发生,但一旦发生影响面很大。
第四个坑是并行阶段抢资源。我一开始把两个都要写同一个临时目录的阶段并行跑,结果两个阶段互相覆盖文件,构建结果时对时错,非常难查。后来把临时目录按阶段名区分开,问题就消失了。并行虽好,但一定要确认阶段之间没有共享的可写资源。
这套 Jenkins 加 GitLab 的链路,我从最初的手忙脚乱到现在能稳定支撑日常迭代,中间交了不少学费。核心体会就是:触发要实时,凭据要安全,环境要一致,日志要能回写。把这四件事做好,剩下的就是按项目需要慢慢加阶段、加优化。它不是什么高深的技术,但每一个细节都值得认真对待,因为流水线一旦不稳,整个团队的交付节奏都会受影响。