如果你手头正好有一套基于 Spring Cloud 的微服务项目,正在为“测试环境手动打包上传,生产环境 SSH 上去改配置重启”这一套流程头疼,那你大概率迟早会走到这条路上:把 Jenkins、GitLab、Harbor、Kubernetes 串成一条自动化的部署流水线。这套组合在中小团队里几乎成了事实标准,因为它把代码托管、持续集成、镜像仓库、容器编排这四件事各自用最成熟的工具搞定,每个环节都能单独替换、单独扩展。这篇文章我基于自己实际搭建这套系统的经历,把从零到一的关键步骤、踩坑点和思考逻辑完整拆出来,希望能给正准备动手的你一个可参考的路线。
整个链路的工作方式可以一句话概括:开发把代码 push 到 GitLab,Jenkins 收到触发后拉代码、跑测试、打镜像,把镜像推到 Harbor,然后调用 Kubernetes API 更新服务。之后 K8s 集群从 Harbor 拉取新镜像完成滚动升级。核心价值在于,从代码提交到线上服务更新,全程不需要人登录任何一台服务器执行手工命令,而且每个环节都有日志、有记录、可回滚。
1. 为什么是这四个组件:需求拆解与方案选型
1.1 每个工具的定位与分工
先理清这套组合里每个角色到底在干什么,因为很多人一上来就问“Jenkins 和 GitLab CI 有什么区别”“Harbor 是不是必须的”,其实是没分清每个工具的边界。
GitLab 管的是代码:它承接开发者的 push、MR、分支管理,是整个流水线的源头。GitLab 也有自带的 CI/CD 能力(GitLab Runner),但在这套组合里,我们只把它当作代码仓库和触发器来源,流水线的执行交给 Jenkins。
Jenkins 管的是“构建编排”:它负责从 GitLab 拉代码、执行 Maven 编译、构建 Docker 镜像、推送到 Harbor,最后调用 K8s 完成部署。它是整个流水线的“大脑”,所有任务的调度和状态展示都在这里。
Harbor 管的是镜像:它是一个企业级的 Docker 镜像仓库,支持镜像漏洞扫描、签名、复制、回收策略。有人问“我用 Docker Hub 或者 registry:2 行不行”,如果你的项目对安全审计、镜像保留策略、私有化部署有要求,Harbor 是最省心的选择,尤其是它自带 Web UI 和 RBAC 权限控制。
Kubernetes 管的是运行:它负责接收 Jenkins 的部署请求,完成 Pod 的调度、副本扩缩、滚动升级、失败自愈。对于 Spring Cloud 这种有服务注册、配置中心、网关等组件的微服务架构,K8s 的 Service、Ingress、ConfigMap 机制刚好能把这些组件以标准化的方式管理起来。
1.2 为什么不用其他替代组合
这套组合不是唯一的正确答案,但在大多数场景下是性价比最高的。我见过有人用 GitLab CI 全包,省掉 Jenkins,这在纯 K8s 原生应用(比如 Deployment + Service)的场景下确实没问题,GitLab CI 的流程定义基于 YAML,仓库即流水线,也很好维护。但一旦你要做复杂的流水线逻辑——比如多环境并行发布、人工审批卡点、参数化构建、和已有的运维脚本深度集成,Jenkins 的成熟插件生态和 Groovy 脚本能力就明显更灵活。
还有人用阿里云镜像仓库或 Docker Hub 替代 Harbor,这在公网项目上没有问题,但内网部署或者对镜像安全有合规要求的场景,Harbor 的私有化部署和漏洞扫描能力还是更能打。选 Harbor 的另一个实际原因,是后面 Jenkins 推送镜像时需要一个稳定的 registry 地址,Harbor 对 push/pull 的权限控制做得比较细,项目团队多的时候不会出现互相覆盖镜像的情况。
K8s 这层基本没有争议,虽然 Rancher、KubeSphere 这类发行版提供了更友好的 UI,但底层还是 K8s。用原生 K8s 加命令行工具,虽然前期陡峭一点,但出了问题你更容易排查,也方便后续迁移到云厂商的托管集群。
2. 环境规划与前置准备
2.1 建议的服务器角色划分
我先说结论:这套系统最少需要 4 台服务器,如果资源紧张可以压缩到 3 台(把 GitLab 和 Jenkins 合在一起),但不建议把 Harbor 和 K8s 节点混布,因为镜像仓库的磁盘 IO 和带宽占用会影响集群稳定性。
| 角色 | 数量 | 最低配置 | 说明 |
|---|---|---|---|
| GitLab | 1 | 8C16G,200G SSD | 代码仓库,内存大户,建议独享 |
| Jenkins | 1 | 4C8G,100G SSD | 构建节点,磁盘要留足镜像构建缓存空间 |
| Harbor | 1 | 4C8G,500G SSD | 镜像仓库,存储建议独立挂载 |
| K8s Master | 1(或多节点) | 4C8G | 控制平面,生产环境建议 3 节点高可用 |
| K8s Worker | 2 起 | 8C16G | 运行微服务 Pod,数量按项目规模扩展 |
我实际遇到的情况是,测试环境只有三台物理机,最后把 GitLab 和 Harbor 拆开部署在 K8s 集群外部,Jenkins 也放在 K8s 外部,K8s 集群内部只跑 Spring Cloud 的各个微服务应用。这样做的原因是,GitLab、Jenkins、Harbor 都是有状态的,放在 K8s 里反而增加运维复杂度,不如独立部署来得清爽。
2.2 版本选型与兼容性
版本兼容是这套系统里最容易让人崩溃的问题,我踩过最深的坑就是 Jenkins 插件和 Kubernetes 客户端版本不匹配,导致调用 K8s API 时一直报 403。这里给出一个我实测比较稳的组合:
| 组件 | 版本建议 | 说明 |
|---|---|---|
| GitLab | 16.x | 新版对权限管理更友好 |
| Jenkins | 2.4xx(LTS) | 长期支持版,插件兼容性好 |
| Harbor | 2.9 或 2.10 | 稳定版,支持 OIDC 和机器人账号 |
| Kubernetes | 1.27 或 1.28 | 不要追最新,1.28 生态最稳 |
| JDK | 11/17 | 视 Spring Boot 版本而定 |
| Maven | 3.8+ | 配合私服或阿里云镜像源 |
| Docker | 20.10+ | 或 containerd 作为运行时 |
Spring Cloud 版本和 Spring Boot 版本要对应好,比如 Spring Cloud 2021.0.x 对应 Spring Boot 2.6.x/2.7.x,Spring Cloud 2022.0.x 对应 Spring Boot 3.0.x。这里尤其注意:如果你的服务要用 JDK17,那 Spring Boot 必须 3.x;如果还是 JDK8,Spring Boot 2.7.x 是最后的支持版本。提前确认这点,不然 Jenkins 里编译直接失败,还得回头改。
2.3 基础环境统一初始化
先把每台机器的基础环境做整齐,不然后面排查问题会疯掉。我在所有服务器上统一做了这几件事:
- 关闭防火墙或放行指定端口(内网环境直接关闭 iptables/firewalld,省事)
- 配置好主机名和 /etc/hosts,保证各节点之间用主机名互通,避免 IP 直连带来的维护混乱
- 统一时区为 Asia/Shanghai,同步配置 NTP 时间同步
- 所有节点安装 Docker(Harbor 和 Jenkins 构建要用),K8s 节点额外配置 cgroup 驱动为 systemd,这是 K8s 1.24 之后的硬性要求。
时间同步这个事容易被忽略,但非常重要。K8s 节点证书如果时间偏差太大会直接失效,Harbor 的 token 校验也依赖时间,Jenkins 和 GitLab 的 webhook 签名同样受时间影响。我们踩过一次所有 Pod 突然全部不可调度的故障,最后定位就是 master 节点时钟被 NTP 服务重置后跳变导致的。
3. 基础设施部署:GitLab、Harbor、K8s
3.1 GitLab 部署与代码托管配置
GitLab 我建议直接装 Omnibus 包,CentOS 上配置好 yum 源后一条命令就能装完。装完记住修改/etc/gitlab/gitlab.rb里的external_url,改成你的实际访问域名或 IP,然后执行gitlab-ctl reconfigure。
创建项目时要注意,Spring Cloud 微服务通常一个服务一个仓库,比如config-server、gateway、user-service、order-service。如果项目已经存在,直接 push 上来就行。这里有个小坑:如果仓库里已经有了 pom.xml 但结构不规范,Jenkins 构建时执行 Maven 命令会报错。我建议先花时间把每个服务的 pom 标准化,统一 parent、统一 module 结构,这能为后面流水线省很多事。
GitLab 这边还要准备一个访问令牌,Jenkins 拉代码要用。建议在 GitLab 里创建一个专门的机器人账号,只给它读权限,然后生成 Personal Access Token 配置到 Jenkins 的凭据里。千万别拿管理员账号给 Jenkins 用,万一令牌泄露范围太大,而且后续你没法单独撤销这个令牌的权限。
3.2 Harbor 部署与镜像仓库准备
Harbor 的部署很直接,下载离线安装包后改harbor.yml,核心是配置 harbor 的 hostname、端口、Harbor 管理员密码、数据目录。我把数据目录单独挂了一块盘,因为镜像文件增长速度快,系统盘很快就会被占满。
Harbor 里要建好项目空间,我按环境来建:dev、test、prod,或者按服务团队来建。这个要看你的管理粒度。我更推荐按环境建,因为流水线里推镜像的地址会写成harbor.example.com/dev/user-service:tag,这样每个环境的镜像天然隔离,不会被串掉。
另外,要在 Harbor 里开启机器人账号功能。Jenkins 推镜像和 K8s 拉镜像都需要凭据,Harbor 的机器人账号比普通用户账号更适合做这种自动化场景,因为机器人账号的权限可以精确限定到某个项目,而且可以单独轮转密钥,不影响人工登录账号。
有一个关键点:K8s 从 Harbor 拉取私有镜像时,需要先在集群里创建docker-registry类型的 Secret。这个 Secret 可以手动创建,也可以由 Jenkins 在部署时自动创建。我后面的流水线里是让 Jenkins 在部署前先调用 K8s API 确保 Secret 存在,这样新加节点或者重建命名空间时不用手工补一遍。
3.3 Kubernetes 集群准备
K8s 集群的安装方式很多,测试环境我用过 kubeadm,生产环境建议直接让你所在公司的运维平台或云厂商托管集群来提供。这里不展开安装过程,重点说和这套 CI/CD 链路相关的几件事:
第一是命名空间规划。我按环境建命名空间:dev、staging、prod,每个命名空间里的服务是同一套 Spring Cloud 应用的副本。这样隔离的好处是,测试环境的配置不会影响到生产环境的数据,而且 K8s 的 RBAC 可以精确到命名空间级别,给每个开发小组分配权限时非常清晰。
第二是 Ingress Controller。Spring Cloud 的网关服务(Gateway)需要暴露到集群外部,建议用 Ingress 而不是直接 NodePort。Ingress 的好处是可以按域名做路由,比如gateway.example.com路由到 gateway 服务,后续加新服务时不用改端口映射。我用的是 ingress-nginx,稳定性和性能都够用。
第三是 Metrics Server。K8s 的 HPA(水平自动伸缩)依赖它获取 Pod 的 CPU 内存指标,如果没有部署,kubectl top pod查不到数据,HPA 也没法工作。我一开始漏装了,后来发现 HPA 一直不生效才补上。
第四是存储类。如果你的 Spring Cloud 服务有状态(比如用到数据库、Redis、ES),在 K8s 里跑就要考虑持久化存储。测试环境可以考虑用 local-path-provisioner 这类本地存储方案,生产环境建议用云厂商的云盘或者自建存储系统。这个和 CI/CD 本身关系不大,但你编排 StatefulSet 或 Deployment 挂 PVC 时会用到。
4. Jenkins 流水线设计:从提交到上线的完整链路
4.1 Jenkins 与各系统的认证打通
这是整个搭建过程中最琐碎但最关键的一步。Jenkins 需要和四个系统建立信任关系:GitLab(拉代码)、Harbor(推镜像)、K8s(做部署)、以及你自己的私有 Maven 仓库(拉依赖)。
我的经验是把所有密码和令牌统一存到 Jenkins 的凭据管理里,并在命名上用统一前缀,比如gitlab-read-token、harbor-robot-cred、k8s-kubeconfig,这样在 Jenkinsfile 里引用时一眼就能看懂。千万不要把密码写在 Jenkinsfile 里,一是容易泄露,二是团队其他人维护时根本不知道这个密码是什么环境用的。
具体到每一项:
- GitLab:在 Jenkins 里添加“GitLab API Token”类型的凭据,用于 Jenkins 通过 API 拉取仓库信息和触发构建。
- Harbor:添加“用户名密码”类型的凭据,对应之前创建的机器人账号。
- K8s:添加一个 secret file 类型的凭据,内容是 kubeconfig 文件。这个 kubeconfig 要专门给部署用,权限建议限制在一个命名空间内,不要直接用集群管理员的 kubeconfig,那样风险太大。
4.2 流水线整体设计与 Jenkinsfile
我采用的流水线结构是经典的 Declarative Pipeline,阶段分为:检出、编译打包、构建镜像、推送镜像、更新 K8s 清单、滚动发布。下面是一份简化但可运行的 Jenkinsfile,你可以直接抄下来改改参数:
pipeline { agent any environment { // 这里改成你的实际仓库、项目地址 REGISTRY = 'harbor.example.com' PROJECT = 'demo' IMAGE_NAME = "${REGISTRY}/${PROJECT}/user-service" // 使用 Git 提交号作为镜像 tag,保证唯一性 IMAGE_TAG = "${GIT_COMMIT.take(8)}" K8S_NAMESPACE = 'dev' DEPLOYMENT_NAME = 'user-service' } stages { stage('检出代码') { steps { checkout scm } } stage('编译打包') { steps { script { // 如果模块化多,这里可以加 -pl 指定模块 sh 'mvn clean package -DskipTests -pl user-service -am' } } } stage('构建并推送镜像') { steps { script { // 先登录 Harbor,再构建和推送 withCredentials([usernamePassword( credentialsId: 'harbor-robot-cred', usernameVariable: 'HARBOR_USER', passwordVariable: 'HARBOR_PASSWORD' )]) { sh """ docker login ${REGISTRY} -u ${HARBOR_USER} -p ${HARBOR_PASSWORD} docker build -t ${IMAGE_NAME}:${IMAGE_TAG} user-service/ docker build -t ${IMAGE_NAME}:latest user-service/ docker push ${IMAGE_NAME}:${IMAGE_TAG} docker push ${IMAGE_NAME}:latest """ } } } } stage('更新 K8s 部署') { steps { script { withKubeConfig([credentialsId: 'k8s-deploy-kubeconfig']) { sh """ kubectl -n ${K8S_NAMESPACE} set image deployment/${DEPLOYMENT_NAME} \ ${DEPLOYMENT_NAME}=${IMAGE_NAME}:${IMAGE_TAG} """ } } } } } post { failure { // 失败时钉钉/邮件通知 dingtalk( robot: 'your-dingtalk-robot', type: 'ACTION_CARD', text: "构建失败: ${JOB_NAME} - ${BUILD_URL}" ) } } }这段脚本的运行逻辑我在实际项目里验证过,需要注意两个细节:
GIT_COMMIT.take(8)这个取前八位 commit 号的方式,能保证每次构建镜像 tag 唯一,回滚时也容易定位到具体代码版本。我不用时间戳的原因是,commit 号能直接和 git 历史对上,排查问题时效率高很多。
kubectl set image是原地更新 Deployment 的镜像路径,K8s 会自动触发滚动更新。如果你的 Deployment 里容器名和 Deployment 名不一致,这里的容器名参数要写对,否则命令会报错提示找不到容器。
4.3 Spring Cloud 项目特有的部署细节
如果说前面这些步骤是通用流程,那针对 Spring Cloud 项目有三件事需要单独处理。
第一是配置文件的外置化。Spring Cloud 项目通常有配置中心(Spring Cloud Config Server),或者至少用bootstrap.yml里配置外部配置源。在 K8s 环境下,我建议把不同环境的配置放到 K8s 的 ConfigMap 里,然后通过 Volume 挂载到 Pod 中,而不是打进镜像。这样修改配置时只需要kubectl edit configmap并重启服务,不用重新走一遍构建流程。实践下来,测试环境改配置的频率远高于改代码,外置配置能省下大量构建时间。
第二是服务注册与发现。Spring Cloud 的注册中心(比如 Nacos、Eureka)在 K8s 里地址需要特殊处理。如果你用 Nacos,每个服务在bootstrap.yml里的注册地址应该指向 Nacos 的 Service 名称,比如nacos:8848,而不是 IP。如果注册中心部署在 K8s 外部,则要确保 K8s 节点能访问到那个网络。一个常见坑是 Pod 里能 ping 通注册中心 IP,但服务注册失败,这时要检查是否走代理或者防火墙规则。
第三是网关路由。Spring Cloud Gateway 的路由配置通常依赖服务名,在 K8s 里服务名就是 Service 的名称。比如user-service在命名空间dev里注册到 Nacos 的地址是user-service.dev.svc.cluster.local。如果要在网关里做转发,得让网关服务能够解析这个内部域名。我遇到的问题是网关部署在prod命名空间,访问dev命名空间的服务时跨命名空间网络策略不放行,排查了半天才发现是 NetworkPolicy 的问题。
5. 常见问题与排查实录
我把自己实际遇到过的问题按频率排了个序,这些问题你如果自己摸大概率也会撞上,直接给结论和建议。
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| Jenkins 构建时无法从 GitLab 拉代码 | SSH key 未添加到 GitLab 或凭据配错 | 检查 Jenkins 节点的 ~/.ssh/known_hosts,确认关键凭据引用的是 git 协议还是 http 协议 |
| Harbor 登录失败 401 | 机器人账号权限不足或密码过期 | 到 Harbor 项目里检查机器人账号的权限是否包含“推送”和“拉取” |
| docker push 速度极慢 | 内网带宽限制 | 把 docker 的>
|