从零搭建一个持续集成平台的实战总结:我踩过的那些坑
背景介绍:为何选择自己搭CI系统?

去年我在一家中型互联网公司接手了一个中后台服务重构项目。我们的团队人数在30人左右,主要使用Java和Node.js开发。随着业务的快速迭代,代码提交频率越来越高,但部署流程还是依赖人工操作:本地打包、测试、上传服务器、重启服务……一不小心就搞错了版本号,或者漏了某个步骤,出问题回滚也不方便。
当时我们使用的Jenkins已经有些年头了,配置混乱不堪,插件版本不兼容,经常出现执行失败但没有报警的情况。最严重的一次是上线时因为脚本执行顺序错误导致数据库表结构被破坏,整整花了一天时间才恢复。
所以我和架构组决定:重起炉灶,重新选型并搭建一套可持续演进、易于维护的持续集成平台。目标很明确:自动化构建、自动化部署、流程清晰、权限分明、可追溯性强。
遇到的挑战:理想与现实之间的差距


我们最初的设想很美好:用GitLab CI或者GitHub Actions作为核心引擎,配合Kubernetes做部署,再加个通知系统,完美!
但现实很快给了我们几个下马威:
- 多语言支持问题:公司内部既有Java又有Node.js还有Python,不同项目的构建方式差异大;
- 环境隔离问题:开发、测试、预发、生产都需要不同的构建策略和部署权限控制;
- 安全管控缺失:以前的Jenkins几乎所有人都有写权限,容易误操作;
- 日志追溯困难:出错后需要登录服务器查看日志,效率低下;
- 资源调度不合理:高峰期多个项目并发构建,导致构建节点CPU爆满。
这些问题让原本计划一周完成的迁移工作拖到了三周,中间甚至一度想放弃。
我们的解决方案:GitLab CI + Kubernetes + Harbor 组合拳

技术选型考量
最终我们选择了以下技术栈:
CI引擎:GitLab CI
- 优点:原生集成GitLab,适合私有化部署,YAML配置灵活
- 缺点:学习曲线陡峭,文档不够友好
构建执行器:Kubernetes Runner
- 使用gitlab-runner+K8s Pod模式,动态伸缩构建节点
镜像仓库:Harbor
- 自建私有镜像仓库,支持权限分级、漏洞扫描等企业级功能
通知中心:企业微信机器人 + 邮件通知
- 关键事件自动提醒,提高反馈速度
日志聚合:ELK Stack + GitLab内置日志
- 所有流水线日志统一收集分析,便于排查问题
架构图概览
[Developer] → [Push to GitLab]
↓
[.gitlab-ci.yml触发构建]
↓
[Kubernetes Runner 动态Pod运行]
↓
[Maven/NPM/PyInstaller 构建产物]
↓
[Docker Build & Push Harbor]
↓
[Helm/Kubectl 部署至K8s]
这套体系上线后,我们终于实现了“代码提交即构建”、“分支变化自动部署”的能力。
实践落地:关键代码与配置说明
下面是一段实际使用的.gitlab-ci.yml片段(经过简化):
stages:
- build
- test
- deploy
variables:
DOCKER_TLS_CERTDIR: ""
IMAGE_NAME: registry.example.com/myapp
build-java:
image: maven:3.8.6-openjdk-11
stage: build
script:
- mvn clean package
- docker build -t $IMAGE_NAME:latest .
- docker login registry.example.com -u admin -p $HARBOR_PASSWORD
- docker push $IMAGE_NAME:latest
test:
stage: test
image: node:18-alpine
script:
- npm install
- npm run test
deploy-prod:
stage: deploy
when: manual
environment:
name: production
url: https://prod.example.com
script:
- |
cat <<EOF > helm-values.yaml
replicaCount: 3
image:
repository: $IMAGE_NAME
tag: latest
EOF
- helm upgrade --install myapp ./mychart -f helm-values.yaml
这段配置做了几件事:
- 定义了构建阶段
build、test、deploy - Java应用用Maven构建,并推送到私有Harbor仓库
- Node.js前端跑单元测试
- 生产部署手动触发,使用Helm进行滚动更新
其中 $HARBOR_PASSWORD 是我们在GitLab项目设置里配置的密钥变量,避免暴露敏感信息。
另外,在搭建Kubernetes Runner时,我们也经历了不少波折。下面是我们最终使用的Runner Helm Chart配置的一部分:
gitlabUrl: https://gitlab.example.com
runnerRegistrationToken: "xxxxxx" # 替换为你自己的token
runners:
tags: "k8s"
executor: kubernetes
namespace: gitlab-runners
privileged: true
cache:
type: s3
path: "runner_cache"
s3:
serverAddress: minio.example.com
accessKey: XXXX
secretKey: XXXX
这个配置让Runner以Pod形式动态创建,结合MinIO实现缓存共享,提高了构建效率。
踩坑经验:那些让我彻夜难眠的问题
🐞 问题一:Docker in Docker 失败
刚开始我们使用docker-in-docker来构建镜像,结果发现每次都会提示:
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
尝试了很多办法都不行,最后换成了Kubernetes Pod直接挂载宿主机的 /var/run/docker.sock 才解决问题。
小贴士:如果你用K8s runner做Docker构建,建议直接挂载docker socket而不是用dinD。
🧩 问题二:Runner启动慢得离谱
我们一开始没配置合适的Pod模板,每次构建都要新建Pod,动辄要十几秒,严重影响体验。
后来优化了Runner使用的PodTemplateSpec,指定固定的基础镜像,并减少初始化容器数量,性能提升了70%以上。
🔒 问题三:权限失控,谁都能部署生产?
最初我们对所有分支都开放了部署权限,导致一次测试人员误点部署按钮,把测试镜像部署到了预发环境……
之后我们做了严格的限制:只有主分支(main)合并后才能触发生产部署任务,且必须由负责人手动确认。
deploy-prod:
...
rules:
- if: $CI_COMMIT_BRANCH == "main"
这样保证了只有特定分支可以进入生产流程。
最终效果:效率提升与信心增强
整个平台稳定运行半年后,我们进行了一个小结:
| 指标 | 改造前 | 改造后 |
|---|---|---|
| 平均部署耗时 | 30分钟 | 8分钟 |
| 回滚成本 | 约1小时 | <5分钟 |
| 故障发生率 | 每月2~3次 | 每月0~1次 |
| 日志可追溯性 | 差 | 强 |
| 权限控制 | 基本无 | 按角色控制 |
更难得的是,大家开始习惯看流水线状态来判断是否构建通过,而不再是问“你push了吗?”。
有一次,有个实习生提了个PR,流水线跑完才发现他改错了SQL语句,幸好没合入master——这要是放在以前,可能就已经线上报错了。
经验总结:给你的几点建议
如果你也在考虑搭建或优化CI平台,这里是我的一些真实经验:
✅ 1. 优先解决高频痛点
别想着一步到位,先解决你们团队最头疼的几个问题,比如部署流程混乱、回滚困难等等。我们就是从“一键部署”和“自动回滚”做起,先把收益拿回来。
✅ 2. 选择能长期维护的技术栈
我们之前纠结过要不要用GitHub Actions,后来考虑到公司代码不能外泄,还是坚持用了GitLab CI。现在回头看,这是明智的选择。
✅ 3. 给每条Pipeline加清晰的命名规则
比如 build_java_11、test_frontend 这样的命名,比默认的 job1, job2 友好多了,尤其当流程变复杂的时候。
✅ 4. 提前规划好权限模型
不同环境的部署权限一定要区分开,特别是生产环境只能由少数人触发,并且要有二次确认机制。
✅ 5. 别忽略监控和告警
我们初期忽略了流水线异常的通知机制,导致几次构建失败没能及时发现。后来接入企业微信机器人和Prometheus监控,问题第一时间就能感知。
写在最后:工具只是手段,协作才是目的
CI系统的价值远远不止是一个自动化工具,它本质上是一种工程文化的体现。当我们从手工部署走向标准化流程时,团队的协作也变得更顺畅、更有安全感。
有时候我会想,如果我们早点建立这样的机制,很多低级错误其实是可以避免的。而现在,我可以安心地说:“只要流水线绿了,就可以放心合入。”
希望这篇文章能给你一些启发,也欢迎留言交流你在搭建CI平台过程中遇到的有趣问题或小技巧!

评论 0