首页
/
行业洞察
/
正文
INDUSTRY INSIGHT · 深度
从零搭建CI/CD流水线:工具选型、Kubernetes部署与踩坑实录
📅 2026/9/8 7:37:33
✍️ 爱科研究院
👁 阅读 3,247
1. 从零开始搭CI/CD先搞清楚你到底要解决什么问题CI/CD这个词这几年被提到烂但很多人对它的理解其实是模糊的。我见过不少团队以为上了Jenkins、写了几个流水线脚本、代码能自动构建就算“搞定CI/CD了”结果跑了一段时间才发现流水线只是把原来手动操作半自动化了该人工盯的还得人工盯该出的问题照样出。先说清楚CI和CD分别是什么。CI是持续集成核心动作是“频繁地把代码合并到主干并且每次合并都自动跑构建和测试”目的是早点发现问题别等代码攒了一个月再合并冲突和破功能全堆在一起。CD是持续交付/持续部署核心动作是“让每次通过验证的代码能随时、可重复地部署到目标环境”如果是持续部署那就干脆连部署的手动确认都省掉一键到底。CI负责“把代码变成可信的产物”CD负责“把可信的产物安全地送到线上”。在动手搭之前必须先做需求拆解。我见过太多团队一上来就装GitLab Runner、写.gitlab-ci.yml或者开一个GitHub Actions仓库跑了半个月才发现并发不够、制品管理混乱、测试环境部署要靠手动传参。所以在安装任何工具之前先回答这几个问题团队规模多大、并发构建量多大五个人和五十个人的方案完全不同。项目是单体还是微服务微服务意味着流水线模板化和多仓库管理策略要前置考虑。部署目标是虚拟机、容器还是Kubernetes这直接决定CD阶段用什么方式发布。测试策略是什么是纯单元测试还是需要集成测试、端到端测试环境谁有权限部署生产需要人工审批还是全自动把这些问题想透了再谈选型。我这次搭建的目标是一个标准的中小型微服务团队Java和Node项目为主Kubernetes部署代码托管在自建GitLab日常工作流是Git Flow简化版主干开发release分支发版要求构建稳定、构建速度快、测试环境自动部署、生产环境部署需要审批。2. 工具链选型CI源、制品仓、部署目标一个都不能少2.1 CI引擎选型别迷信“最火的”要选“最顺手的”CI引擎是整个流水线的发动机选型直接影响团队日常开发的体感。市面主流是这四类CI引擎适合场景上手成本维护成本备注GitLab CI代码在GitLab团队规模不大低低与MR深度集成.gitlab-ci.yml入仓库GitHub Actions代码在GitHub偏好SaaS低低生态最丰富marketplace很香Jenkins大型企业、复杂流水线高高插件多但要自己管Master/SlaveDrone / GoCD等容器化程度高的云原生团队中中灵活但社区相对小我的选择是GitLab CI原因是我们代码本来就在自建GitLab上MRMerge Request流程已经跑熟了GitLab CI和MR的集成是最顺滑的——可以在MR页面直接看到流水线结果、测试覆盖率、部署状态开发不用离开GitLab就能完成“提交代码→查看结果→合并”的闭环。相比之下Jenkins虽然功能强大但对小团队来说光是维护插件版本、处理Master节点可用性就觉得累。注意如果你们代码仓库是GitHub而偏要搭Jenkins也不是不行但每次提交都要靠Webhook触发Webhook偶尔丢事件的时候排查起来会想骂人。能用平台原生CI就别绕路。2.2 制品仓镜像仓库才是CD的命脉CI的产物如果只是编译出来的jar包那你CD部署就没法做到“环境无关”。所以我们的做法是CI阶段统一构建成Docker镜像推送到私有镜像仓库CD阶段从镜像仓库拉取指定tag的镜像进行部署。镜像仓库我们用的是Harbor。选它的核心原因是支持镜像漏洞扫描、支持镜像签名、有项目级别的权限隔离正好满足生产环境对制品安全性和可追溯性的要求。对小型团队Nexus或者云厂商的容器镜像服务也够用但Harbor开源免费、功能完整自建也不算重。给镜像打标签有一个必须遵守的原则镜像tag必须是不可变的、可追溯的。不要用latest不要用dev这种会变的tag。我们用的是${CI_COMMIT_SHORT_SHA}——Git提交的短SHA保证每个镜像对应唯一的代码版本。这样生产环境出了问题你能直接从镜像tag反查到是哪个commit再反查到是谁改的代码排查链路就完整了。2.3 部署目标Kubernetes让CD变得标准化关于部署方式我们选了Kubernetes原因实际很简单团队要部署的服务多test、staging、prod三套环境如果用传统的ansible脚本加虚拟机每个服务的部署脚本几乎都要单独写维护成本随着服务数量线性增长。而K8s里的Deployment Service描述方式极其统一一个服务一个目录差别只在镜像tag和副本数。当然K8s也引入了新的复杂度所以后文我会专门讲一下部署阶段的细节和坑。这里先明确一点CD到Kubernetes本质上就是把“部署”变成“声明式地更新集群里的资源描述”这比SSH上去拉代码、重启进程要可控得多。3. 流水线设计一个微服务从push到prod要经过哪些关卡3.1 我的流水线基础骨架四个阶段层层卡关我们仓库的流水线文件是.gitlab-ci.yml用YAML写核心结构非常简单就是四个阶段test、build、deploy-test、deploy-prod。这里我刻意没有加lint作为一个独立阶段而是并进test阶段里。原因很实际lint检查和单元测试在同一台Runner上跑能节省一次Job调度的开销。但当项目变大、测试变慢时我也会考虑把lint拆出去这样能并行跑。一个典型的微服务流水线配置长这样stages: - test - build - deploy-test - deploy-prod variables: IMAGE_REPO: harbor.example.com/backend/user-service IMAGE_TAG: $CI_COMMIT_SHORT_SHA cache: key: $CI_PROJECT_NAME-$CI_COMMIT_REF_SLUG paths: - .m2/repository/ - node_modules/ .test_template: test_template stage: test script: - echo Run tests test:unit: : *test_template image: maven:3.8-openjdk-11 script: - mvn -B test artifacts: reports: junit: target/surefire-reports/TEST-*.xml expire_in: 7 days only: - merge_requests - main - release/* build:image: stage: build image: docker:20.10.16 services: - docker:20.10.16-dind script: - docker build -t $IMAGE_REPO:$IMAGE_TAG . - docker login -u $HARBOR_USER -p $HARBOR_PASSWORD $HARBOR_URL - docker push $IMAGE_REPO:$IMAGE_TAG only: - main - release/* when: manual注意几个细节。第一cache配置了Maven和Node的依赖缓存否则每次构建都重新下载依赖会让流水线慢到怀疑人生。但缓存key的粒度有讲究我们是按项目名分支名来做避免不同分支之间脏缓存互相污染。第二build:image这个Job我特意设置成when: manual也就是手动触发原因下节讲。3.2 为什么build阶段要手动触发这是我踩过坑后总结出来的一个设计选择。如果main分支每次push都自动构建镜像那么开发中间的一次小改动也会触发构建推送镜像一天可能推几十个tag镜像仓库里堆满无人使用的镜像。而我们的流程是MR合并到main时自动跑test阶段保证主干永远是“可测试”的只有当需要发版本时人工点一下build:image的按钮构建出正式的版本镜像。这实际上是把“持续集成”和“持续交付”在动作上解耦CI频繁地跑、随时可验证CD按需地跑、保持稳重。当然如果你的团队已经做到了高度自动化每次合并到主干就自动构建镜像也没问题。但前提是你得有一套镜像清理策略定期清掉那些没有关联到任何Deployment的孤儿镜像否则Harbor的存储迟早被撑爆。3.3 test阶段为什么要收集JUnit报告.gitlab-ci.yml里那段artifacts.reports.junit很多人会忽略觉得反正是跑测试报告存不存无所谓。实际上这个配置的价值非常大GitLab会解析JUnit XML报告直接把测试用例通过/失败/跳过情况展示在MR页面上开发在MR页面上就能看到“改了这几行代码挂了两个测试用例”不用点进流水线的日志一层一层翻。同样重要的还有覆盖率。在Job里加上test:unit: script: - mvn -B test coverage: /Total.*?([0-9]{1,3})%/GitLab会正则匹配Maven输出的覆盖率然后在MR页面上显示覆盖率变化量。覆盖率少了MR合并不了——这是我们质量门禁的基础后面第5节会细讲。3.4 最终目标一条流水线描述清楚“谁在做、做什么、给谁用”很多团队的流水线配置文件越写越长几百行的YAML看着专业实际上谁能维护好我们当时的经验是保持单仓库流水线只描述“一个服务的生命周期”。公共的步骤比如构建镜像的通用逻辑、K8s部署的通用脚本尽量抽成公共模块用include引入子仓库只保留差异化的部分。GitLab CI的include:project支持跨仓库引入这非常适合微服务团队统一管理流水线模板。4. 部署实现用Kubernetes编排一套清晰可回滚的发布流程4.1 分支与环境的映射关系在聊部署细节前先明确环境与分支的关系。我们的策略分支自动部署到失败处理用途feature/*本地/开发者环境开发者自己处理日常开发develop主干test环境自动告警联调release/*staging环境手动确认发版前验证main生产分支prod环境强管控手动审批正式发布tagv*..prod环境强管控手动审批线上追踪这里有一个关键的认识不是所有环境都能自动部署。test环境可以全自动但staging和prod我坚持必须有人工确认。有人工确认的好处是发布动作有一个明确的“决策点”负责发布的同学需要确认当前MR合并的代码确实是要发布的版本同时在发布页面上填一句发布说明事后追溯起来非常清晰。4.2 Kubernetes滚动发布的核心多副本与Readiness探针我们的标准部署方式是Kubernetes的滚动更新RollingUpdate用一套Deployment模板来管。核心思想新版本Pod逐个替换旧版本Pod整个过程服务不中断。配置里有两个参数要小心处理strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 0 maxSurge: 1maxUnavailable: 0表示滚动更新期间不能有Pod宕机保证服务可用性maxSurge: 1表示允许临时多启动一个Pod加快替换速度。对小规模服务来说非常稳。生产环境我建议保持这两个值宁可部署慢一点也别冒着中断服务的风险把maxUnavailable调大。Readiness探针就绪探针是滚动发布能否“成功”的关键。如果新Pod的HTTP接口还没就绪K8s就不会把流量导过去如果就绪探针迟迟不通过滚动更新就卡在那里旧Pod照常服务不会发生“新Pod刚启动就被打流量然后502”的情况。我们的项目接口普遍是Spring Boot和Node就绪探针统一用HTTP GET路径是/actuator/health或/healthinitialDelaySeconds: 30给服务启动留足时间periodSeconds: 10每10秒探一次。4.3 部署脚本的演进从一条命令到一个可复用的Shell部署到K8s我早期写的是直接kubectl set image后来改成用一套模板文件加shell脚本生成最终YAML再交给kubectl apply。核心思路是这样的set -e DEPLOY_NAMESPACE${DEPLOY_NAMESPACE:-default} DEPLOYMENT_NAME${SERVICE_NAME:-user-service} IMAGE_WITH_TAG${IMAGE_REPO}:${IMAGE_TAG} cat EOF | kubectl apply -n $DEPLOY_NAMESPACE -f - apiVersion: apps/v1 kind: Deployment metadata: name: $DEPLOYMENT_NAME spec: replicas: 2 selector: matchLabels: app: $DEPLOYMENT_NAME template: metadata: labels: app: $DEPLOYMENT_NAME spec: containers: - name: app image: $IMAGE_WITH_TAG ports: - containerPort: 8080 readinessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 30 periodSeconds: 10 EOF kubectl rollout status deployment/$DEPLOYMENT_NAME -n $DEPLOY_NAMESPACE --timeout5m这段脚本很少但套路很完整先把镜像地址拼好然后生成一个Deployment定义apply到集群最后用kubectl rollout status阻塞流水线直到发布完成或超时失败。如果5分钟内新版本Pod还没有Ready脚本会以非0码退出流水线判失败同时自动把告警信息推到IM群里。在这个基础上我们的deploy-job里加了环境判断逻辑如果目标是prodJob会先暂停等待有权限的同事在GitLab界面上点“允许”然后再执行后面的脚本。这就是前面说的“人工审批”关卡。4.4 回滚发布失败后的最后一根救命稻草回滚能力一定要在搭建初期就想好不要等出了问题再临时写。K8s的优势在于Deployment的每次变更都对应一个kubectl rollout history记录回滚只需要一条命令kubectl rollout undo deployment/user-service -n production但注意kubectl rollout undo默认是回到上一个版本如果你连续发了多个版本直接undo可能回不到你想要的版本。所以我的习惯是在发布脚本里明确把镜像tag写入Deployment的annotations这样万一需要精准回滚可以直接把镜像tag改回去再apply一次比依赖rollout历史更可控。5. 质量门禁与反馈闭环让CI/CD成为研发质量的“守门员”5.1 质量门禁不是“跑个测试就完事”而是“不达标就合并不了”CI跑过了自动测试不代表代码质量合格。我们的做法是设了三个硬性门槛单元测试覆盖率不低于80%且新增代码行覆盖率不低于90%关键服务集成测试全部通过依赖漏洞扫描无高危以上问题。三项全绿MR才允许合入主干。覆盖率门槛有两个数据分别叫“全量覆盖率”和“增量覆盖率”。全量覆盖率管的是整体趋势防止代码越写越烂增量覆盖率管的是当前改动防止新写的代码没测试保护。一个老项目整体覆盖率可能只有40%但你要求本次MR的新代码覆盖率达到90%这实际上是允许团队“渐进式还债”老代码逐步补新代码必须达标。这是很多团队容易忽略的细节。5.2 测试金字塔别把大部分测试做成“端到端”关于测试策略最理想的还是经典的测试金字塔单元测试占70%集成测试占20%端到端测试占10%。但实际运行中端到端测试是最脆弱的——依赖外部接口、测试数据、网络环境稍有风吹草动就失败然后大家习以为常地“重跑一次”。重跑多了流水线的“绿灯”就没有公信力了。所以我在流水线里有两类Job一类是“必跑必过”的单元测试短集成测试任何一次MR都必须跑过另一类是“定时跑”的端到端测试挂在每天夜里失败只报警不阻塞合并。这样白天开发节奏不被打断夜里又有一只看门的“黑猫”在盯着主流程。5.3 反馈闭环流水线的“结果”要主动找上门不能等开发去翻CI/CD流程要真正产生价值反馈链条必须通畅。我们的做法流水线失败自动在MR里at对应的提交人和代码review人群里同步一条消息带上失败日志链接和失败Job名字测试覆盖率下降时GitLab自动在MR评论区发一条覆盖率对比报告。开发不用主动去任何一个平台找“我的构建为什么挂了”结果直接推到面前。这比“流水线扫出问题但没人看得见”强得多。6. GitOps的落地试验把部署状态也变成“代码”流水线跑顺之后我尝试引入GitOps的思路把Kubernetes的部署配置文件YAML也放进Git仓库统一管理通过CD工具我们试的是ArgoCD来自动同步Git仓库里的期望状态和集群的实际状态。GitOps最大的变化是把“CI/CD主动推送部署”变成“集群自己拉取期望状态并调整”。CI流水线只负责构建镜像并更新Git仓库中的部署配置比如把image tag换掉这个“更新配置的提交”会被ArgoCD识别然后由ArgoCD负责把集群里的Deployment收敛到期望状态。好处是整个部署过程完全可审阅、可回溯环境漂移有人手动改集群配置导致和Git仓库不一致会自动被检测出来并纠正。这个方案目前还处在试验阶段生产环境还是保留原有的人工审批脚本apply模式因为GitOps对团队的运维理念要求更高强行切换风险不小。我个人建议如果你的团队还没有建立很好的配置管理习惯先不要急着上GitOps把“所有部署配置都放进Git仓库”这条底线先守住就够了。7. 常见问题与排查实录直接抄作业版7.1 问题表现与处理思路症状可能原因排查方向 / 解决办法流水线一直处于pendingRunner并发不足查看Runner并发数K8s Runner的话看Pod资源是否饱和镜像构建极慢依赖未缓存 / 缓存key粒度太粗失效频繁检查Maven/npm缓存是否命中确认缓存key是否随分支变化kubectl apply后Pod一直没Ready镜像tag错误 / 探针配置问题kubectl describe pod看事件检查镜像是否存在、启动是否超过initialDelay测试用例偶发失败重跑就过测试之间有共享状态或依赖外部服务优先处理“不稳定测试”标记为flaky并单独修复别让它污染主干Redis/Mysql连接超时导致集成测试挂掉并发Job抢占同一个服务给每个Job独占一个服务实例或使用固定命名空间隔离MR合并后主干流水线直接挂了MR分支没跑过“构建镜像”步骤设置“唯一允许合并到主干的条件”部分Job只在main/release分支生效镜像仓库里有大量无用tag每push都触发构建调整构建触发条件为手动/版本tag加清理策略7.2 几个不常见但隐患很大的坑第一个坑是Runner节点上workspace残留文件导致构建结果不在预期状态。GitLab Runner每次拉代码默认是clean的但如果某些Job里有clone策略的怪异配置历史workspace会残留上一次构建的内容造成偶发性失败。我的做法是每个Job里GIT_STRATEGY: clone保证每次都是全量clone宁可慢一点也要干净。第二个坑是Docker in Dockerdind权限配置。很多人在GitLab CI里用dind构建镜像每次启动dind服务都要等待十几秒而且高并发下dind偶发网络问题。后来我逐步改成用Kaniko——它不需要docker daemon直接构建镜像并推送到仓库编译速度快很多也不容易出现daemon不稳定的情况。如果你还在用dind并且频繁踩“Cannot connect to the Docker daemon”强烈建议换Kaniko。第三个坑是敏感信息的存储。不要把你的HARBOR_USER、HARBOR_PASSWORD直接写在.gitlab-ci.yml里哪怕仓库是私有。GitLab CI项目里有独立的Settings - CI/CD - Variables要开启Masked和Protected选项。Masked保证日志里不会明文打印出变量值Protected保证只有受保护的分支比如main、release才能使用这些变量。这样一个普通MR的流水线里就算嵌套了很多脚本也没机会触碰到生产环境的密钥。7.3 我的排障心得先看日志原文再猜原因排查流水线问题最核心的原则就一条别猜先看原始日志。在GitLab里直接打开失败Job的日志搜ERROR、FAILURE、Exception、异常堆栈里最底部的cause部分90%的问题一眼就能发现。我见过很多同事排查了半天最后发现是YAML里缩进错了两个空格导致Job根本没按预期执行。所以CI配置文件本身就是代码一定要用严格的格式约束来管理——可以在流水线里加一个validate步骤用yamllint或者GitLab自带的CI Lint工具去检查配置文件的语法和逻辑。最后再分享一个小经验这次CI/CD流程搭建给我最大的体会是工具不重要流程设计才重要。GitLab CI也好、Jenkins也好、GitHub Actions也好本质上都是执行脚本的“机器人”真正决定流水线好不好用的是你给它设计的“行为规则”——什么时候跑、跑什么、卡在哪、谁负责。建议准备搭建的同学动手之前先拿一张白纸把你项目从提交到上线的路径画出来标清楚每个步骤的触发条件和负责人然后再去写YAML你会发现自己对流程的理解清晰很多后面少踩很多坑。
📌 标签:
工业官网
设计趋势
AI 建站
SEO
获取完整报告 →
RELATED ARTICLES
推荐阅读
2026/9/8 7:37:33
AI Agent项目降本实战:从五层技术栈到推理交付的全链路优化
2026/9/8 7:32:33
DMA原理详解与嵌入式实战:串口接收、ADC采样与疑难排障
2026/9/8 7:32:33
PyTorch逐算子性能基准测试:从profiler到独立benchmark的实践
2026/9/8 8:27:37
海南电信IPTV卫视4K频道全解析:技术参数与故障排查指南
2026/9/8 8:27:37
软件工程课程设计实战:空调设备管理系统开发与设计全解析
2026/9/8 8:27:37
微调模型部署实战:火山方舟托管推理全流程与成本解析
2026/9/8 8:27:37
从 LangChain 到 LangGraph:Agent 开发如何走向工程化与状态可控
2026/9/8 8:27:37
Vector CAN/LIN工具链实战:从DBC配置到采样点与调度表
2026/9/8 8:22:36
Agent软件底座开放之后,硬件如何接住这波时代机遇?
2026/9/8 0:02:01
中国车企再破谣言,GAC吉利零跑获欧盟安全五星
2026/9/8 0:02:01
Compose Hot Reload新增MCP服务器助AI智能体调试
2026/9/8 0:02:01
你熟悉的GoPro正在悄然改变
2026/9/8 0:43:11
超人会飞不算本事:系统稳定依赖清晰规则与边界设计
2026/9/8 1:13:27
超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论
2026/9/8 2:18:22
基于CNN的调制信号识别:MATLAB实现时频图分类实战