前三篇我们把地基铺平了:容器是什么、K8s 靠哪些对象编排、怎么调度与弹性伸缩。但有一个问题一直没答:代码从开发者按下 git push,到真正跑在生产集群的 Pod 里,中间那一长串”编译、测试、打镜像、推仓库、部署、验证、放量”是谁在做、怎么做到又快又安全? 答案就是 CI/CD 流水线——这一篇专门深挖它,并以 Jenkins 为主线。
广告系统对发布这件事尤其苛刻:一天可能发几十次、镜像要小到能秒级拉取、出了问题要能秒回滚(收入太敏感)、流水线本身还得快(反馈环一长,工程效率就塌)。这些约束会把”能自动部署就行”的朴素流水线逼成一套需要精心设计的工程系统。我们就从概念辨析开始,一路搭到一条能扛住广告场景的生产级流水线。
本文是容器化与 K8s 部署系列的第 4 篇。 全系列 5 篇:
- 容器化(开篇)· Docker 原理与镜像分层
- Kubernetes 核心对象 · Pod/Deployment/Service/Ingress
- K8s 调度、资源与弹性伸缩
- CI/CD 流水线 · Jenkins 构建镜像与自动部署到 K8s(本篇)
- 广告服务上云实战 · 容器化部署与调优
一句话定位:CI/CD 是把”改一行代码到它安全上线”这条路自动化、标准化、可回退——CI 保证每次提交都能编译+测试通过,CD 把通过的产物做成不可变镜像、可靠地送到生产;Jenkins 用 Controller 编排 + 动态 Agent 构建,把这条路用
Jenkinsfile写成代码。
TL;DR
- CI / CD / 持续部署是三层递进:持续集成(CI) 是频繁把代码合进主干、每次都自动编译+测试,尽早暴露冲突;持续交付(Continuous Delivery) 是在 CI 之上,让每次通过的构建随时可一键发布(到生产前留一道人工审批);持续部署(Continuous Deployment) 更进一步——通过所有关卡就自动上生产,无需人工点按钮。
- Jenkins 是 Controller + Agent 架构:Controller(master)只做编排、调度、UI、凭据管理,不干重活;真正的编译/打镜像跑在 Agent(node/executor)上。分布式构建让你横向加 Agent 就能并行更多构建。
- Agent 用 K8s 动态 Pod 最香:kubernetes plugin 按每次构建动态起一个 Agent Pod、构建完就销毁——环境干净、天然隔离、资源即用即还,比常驻固定构建机弹性得多。
- Pipeline as Code:用声明式
Jenkinsfile(pipeline { agent / stages / stage / steps / post })把流水线写进仓库、随代码版本化;配合多分支流水线(每个分支/PR 自动建流水线)、共享库(复用公共步骤)、凭据管理(密钥进 credentials,绝不硬编码进脚本)。 - 一条广告服务流水线:拉代码 → 编译+单测 → 构建镜像(多阶段) → 推镜像仓库(tag 用 git sha,别用
latest)→ 部署到 K8s(kubectl set image/apply或 Helm)→ 冒烟+健康检查 → 金丝雀放量。 - 部署策略 + 安全门禁:滚动/蓝绿/金丝雀择机而用;
input加人工审批门禁;post { failure { ... } }失败自动回滚;镜像不可变 + 版本可追溯是这一切的地基。 - Push vs Pull 两种范式:Jenkins 属 push 式(流水线跑完直接
kubectl apply推给集群);GitOps(Argo CD/Flux) 是 pull 式(集群内控制器监听 Git、持续把真实状态收敛到声明),可审计、防漂移、回滚即git revert。 - 广告场景取舍:高频发布 + 镜像要小(拉取快、扩容才快)+ 秒级可回滚(收入敏感)+ 流水线本身要快(反馈环短)。
- AdTech 血泪:一条用
latesttag、且没有回滚门禁的流水线,一次把带 bug 的竞价镜像推上去,K8s 滚动更新自动铺满全网,回滚时因latest已被覆盖找不到上一个可用版本,收入长时间流血。教训全在”不可变 tag + 金丝雀 + 一键回滚”。
Table of contents
Open Table of contents
1. CI / CD / 持续部署:先把三个概念掰开
这三个词天天混着说,但它们是递进的三层,边界很清楚:
| 概念 | 全称 | 做的事 | 边界 |
|---|---|---|---|
| CI | Continuous Integration(持续集成) | 频繁把代码合进主干,每次提交都自动编译 + 跑测试,尽早发现集成冲突和回归 | 产物停在”通过测试的构建”,不管发布 |
| CD | Continuous Delivery(持续交付) | 在 CI 之上,把通过的构建做成随时可发布的制品(镜像),一键就能上生产——但上生产前留一道人工审批 | 能发,但”按不按发布键”由人决定 |
| 持续部署 | Continuous Deployment | 更进一步:通过所有自动关卡就直接上生产,无需人工点按钮 | 全自动,从提交到生产无人工干预 |
一句话记忆链:CI 保证”代码是好的”→ 持续交付保证”随时能发”→ 持续部署保证”自动就发了”。两个 CD 常被混用,关键差别就是生产发布这一步有没有人工闸门:留了闸门是 Delivery,闸门也自动化了是 Deployment。
广告系统里,多数团队落在持续交付:CI 全自动、预发环境自动部署,但生产发布带一道
input审批 + 金丝雀——因为竞价服务一旦发错就是真金白银的损失,愿意用一点人工确认换取安全边界。等流水线的自动化质量门(指标门禁、自动回滚)足够可信,才逐步向持续部署靠拢。
2. Jenkins 架构:Controller + Agent 的分布式构建
Jenkins 是老牌但依然主流的自动化服务器,核心是一套 Controller(旧称 master)+ Agent(node) 的分布式模型。
Jenkins 架构:Controller 只负责编排/调度/UI/凭据,Agent 干真正的编译打镜像;用 kubernetes plugin 让每次构建动态起一个临时 Agent Pod、构建完销毁。
2.1 Controller 与 Agent 各干什么
- Controller(master · 大脑):解析
Jenkinsfile、编排 stage 的执行顺序、调度任务、提供 Web UI、管理凭据与插件。它本身不该跑重活(编译、打镜像),否则一台机器很快被吃满、还成了单点。 - Agent(node / executor):真正干活的执行节点。一个 Agent 上有若干 executor(执行槽),每个 executor 同一时间跑一个构建。Controller 把 stage 里的
steps派发给某个 Agent 的 executor 去执行。
2.2 为什么要分布式构建
如果所有构建都挤在 Controller 一台机器上:并发一高就排队(executor 不够)、环境互相污染(这个构建装的依赖影响那个)、Controller 一挂全线瘫痪。分布式构建把”编排”和”执行”分开:
- 横向扩展:构建任务多了,加 Agent 就行,并行度线性提升。
- 环境隔离/异构:不同 Agent 可以有不同环境(JDK 版本、需要 GPU、特定 OS),按标签(label)把构建调度到合适的 Agent。
- 保护 Controller:Controller 只做轻量编排,稳定性和可用性更好保障。
2.3 用 K8s 动态 Agent Pod(kubernetes plugin)
固定的 Agent 机器有老毛病:平时闲置烧钱、构建高峰又不够用、跑久了环境越来越脏。Jenkins 的 kubernetes plugin 把 Agent 做成 K8s 里的动态 Pod:
- 按需创建:一个构建来了,就在集群里起一个 Agent Pod(通常含一个
jnlp容器连回 Controller,加一个或多个 build 容器如maven、kaniko)。 - 构建完销毁:构建结束,Pod 立即删掉——不常驻、不占用固定 executor、下次构建是全新干净环境。
- 弹性 + 隔离:构建量大就自动起更多 Pod(受集群容量约束,配合集群弹性伸缩),每个构建天然隔离在自己的 Pod 里。
这正是”把 CI 也云原生化”——构建资源和业务 Pod 一样即用即还,尤其适合广告这种构建高峰明显(合版本、大促前密集发布)的场景。
3. Pipeline as Code:声明式 Jenkinsfile
现代 Jenkins 的核心实践是 Pipeline as Code:把流水线定义写成一个 Jenkinsfile、跟着代码一起提交进仓库、一起做 code review、一起版本化——而不是在 Jenkins UI 上点点点配出来(点出来的配置没法 review、没法回溯、换个 job 就得重配)。
3.1 声明式流水线的骨架
推荐用声明式(declarative) 语法,结构清晰、约束性强:
pipeline {
agent { kubernetes { ... } } // 在哪跑:用 K8s 动态 Agent Pod
stages {
stage('Build') { // 一个阶段
steps { sh 'mvn -B clean package' } // 阶段里的具体步骤
}
stage('Test') {
steps { sh 'mvn -B test' }
}
}
post { // 收尾:无论成败都跑
success { echo '流水线成功' }
failure { echo '失败 → 触发告警/回滚' }
always { junit '**/target/surefire-reports/*.xml' } // 归档测试报告
}
}
agent:这条流水线(或某个 stage)在哪个 Agent 上跑——这里就接上了 §2.3 的 K8s 动态 Pod。stages/stage/steps:stages是所有阶段的容器,每个stage是一个逻辑阶段(Build/Test/Deploy…),steps是阶段里的实际命令。post:收尾块,按结果(success/failure/always/unstable)执行——失败自动回滚、成功打 tag、总是归档报告都写在这。
3.2 多分支流水线与共享库
- 多分支流水线(Multibranch Pipeline):Jenkins 自动扫描仓库的每个分支和 PR,只要该分支里有
Jenkinsfile就自动为它建一条流水线。这样每个 feature 分支、每个 PR 都能自动跑 CI,合并前就知道好不好。 - 共享库(Shared Library):把多个项目通用的流水线逻辑(构建镜像、部署到 K8s、发通知)抽成一个公共 Groovy 库,各项目的
Jenkinsfile一行@Library('...')引用即可。避免几十个服务各自 copy-paste 一份几百行的流水线,改一处、全线生效。
3.3 凭据管理:密钥绝不进脚本
流水线要用一堆敏感信息:镜像仓库账号、K8s 集群 kubeconfig、云厂商 API key。这些绝不能明文写进 Jenkinsfile(进了仓库 = 泄密)。Jenkins 的 credentials 机制统一托管这些密钥,流水线里用 ID 引用、运行时注入为环境变量或临时文件:
steps {
withCredentials([usernamePassword(
credentialsId: 'registry-cred', // 只引用 ID,密钥不出现在脚本里
usernameVariable: 'REG_USER',
passwordVariable: 'REG_PASS')]) {
sh 'echo "$REG_PASS" | docker login -u "$REG_USER" --password-stdin registry.example.com'
}
}
原则:代码库里只有”用哪个凭据”(ID),没有”凭据是什么”(值)。密钥集中在 credentials(或对接 Vault/KMS)里管理、可轮换、可审计。这条纪律和镜像最佳实践里”别把密钥 COPY 进镜像” 是一脉相承的。
4. 一条广告服务的完整流水线
把前面的零件拼起来,看一条真实的广告服务流水线长什么样——从开发者 push 代码,到金丝雀放量。
一条广告服务流水线:git push 触发 → 拉代码/编译测试/构建推镜像 → 部署到 K8s → 冒烟健康检查 → 金丝雀放量;配 input 审批门禁与 post failure 自动回滚。
4.1 逐个阶段拆解
- 拉代码(checkout):多分支流水线被 webhook 触发,
checkout scm拉对应分支/commit 的代码。 - 编译 + 单测(mvn verify):编译打包 + 跑单元/集成测试。这一步失败就该立刻断流水线——不让坏代码往下走,这是 CI 的核心价值。
- 构建镜像:用多阶段构建打出瘦镜像(build 阶段编译、runtime 阶段只留产物)。在 K8s 里没有 Docker daemon,常用 kaniko、buildkit 这类无 daemon 构建工具在 Agent Pod 里打镜像。
- 推镜像仓库(打不可变 tag):把镜像推到镜像仓库,tag 必须用
git sha或语义版本(如v1.8.3),绝不用latest——这样每个镜像和一次确定的提交一一对应,能追溯、能精确回滚(这条是 §7 war story 的核心)。 - 部署到 K8s:更新 Deployment 的镜像。最简单是
kubectl set image deploy/bid-service app=registry/bid:$GIT_SHA,更规范是kubectl apply一份渲染好的 manifest 或helm upgrade(用 Helm 管理模板 + values)。 - 冒烟 + 健康检查:部署后别急着宣告成功——跑一组冒烟测试、确认新 Pod 的 readiness 探针通过、关键接口返回正常。不健康就走 §5 的失败回滚。
- 金丝雀放量:核心变更不一把全量,先切一小撮流量(1~5%)到新版本,盯核心指标(竞价成功率、p99、错误率、收入)稳了再逐步放大——按权重分流可交给 Ingress / Service Mesh(金丝雀/蓝绿/回滚的体系化编排见发布与变更安全)。
4.2 一个精简的 Jenkinsfile
把上面串成代码(省略号处为简化):
pipeline {
agent {
kubernetes { // 每次构建起一个临时 Agent Pod
yaml '''
spec:
containers:
- name: build
image: maven:3.9-eclipse-temurin-17
- name: kaniko # 无 daemon 打镜像
image: gcr.io/kaniko-project/executor:debug
'''
}
}
environment { IMG = "registry.example.com/bid-service:${GIT_COMMIT}" } // 用 git sha 做 tag
stages {
stage('Build & Test') {
steps { container('build') { sh 'mvn -B clean verify' } }
}
stage('Build & Push Image') {
steps {
container('kaniko') {
sh '/kaniko/executor --dockerfile=Dockerfile --destination=$IMG' // 打并推镜像
}
}
}
stage('Approve') { // 生产发布前的人工门禁(持续交付)
steps { input message: '确认发布到生产?', ok: 'Deploy' }
}
stage('Deploy to K8s') {
steps {
withKubeConfig([credentialsId: 'kubeconfig-prod']) { // 凭据注入,不硬编码
sh 'kubectl set image deploy/bid-service app=$IMG --record'
sh 'kubectl rollout status deploy/bid-service --timeout=120s' // 等就绪
}
}
}
stage('Smoke & Canary') {
steps { sh './scripts/smoke.sh && ./scripts/canary.sh 5' } // 冒烟 + 放 5% 金丝雀
}
}
post {
failure { // 任何阶段失败 → 自动回滚上一个可用版本
withKubeConfig([credentialsId: 'kubeconfig-prod']) {
sh 'kubectl rollout undo deploy/bid-service'
}
}
}
}
这条流水线把本篇的关键点都落地了:动态 Agent、多阶段构建、git-sha 不可变 tag、凭据注入、审批门禁、部署等就绪、冒烟+金丝雀、失败自动回滚。
5. 部署策略与发布安全
流水线跑到”部署”这步,怎么把新版本铺上去、出问题怎么退,直接决定发布是”平滑”还是”事故”。
5.1 三种部署策略
| 策略 | 做法 | 优点 | 代价 |
|---|---|---|---|
| 滚动更新(Rolling) | 一批批用新 Pod 替换旧 Pod(K8s Deployment 默认) | 不额外占太多资源、平滑 | 新旧版本短暂共存;回滚要再滚一轮 |
| 蓝绿(Blue-Green) | 起一整套新版本(绿),验证 OK 后流量整体切过去,旧的(蓝)留着兜底 | 切换瞬间完成、回滚极快(切回蓝) | 要双倍资源 |
| 金丝雀(Canary) | 先放一小撮新版本、按权重逐步放量,盯指标 | 风险最小、能用真流量验证 | 需要按权重分流的能力 + 指标门禁 |
广告服务常用金丝雀 + 滚动:核心变更走金丝雀(先 1~5% 看收入/成功率),确认无损再滚动全量;成本敏感时也可对整套竞价链路做蓝绿,换取秒级切回。
5.2 门禁与自动回滚
- 审批门禁(
input):生产发布前插一个input步骤,等人点确认——这是”持续交付”和”持续部署”的分界(§1)。 - 失败自动回滚(
post { failure }):任何阶段失败(编译挂、冒烟不过、金丝雀指标崩),post { failure }里kubectl rollout undo自动退回上一个可用版本,不留残局。 - 指标门禁:金丝雀阶段接可观测/SLO 指标——错误率/延迟/收入越线就自动判定失败、触发回滚,把”人肉盯盘”变成”指标说了算”。
5.3 地基:镜像不可变 + 版本可追溯
上面所有回滚能力,都建立在一个前提上:镜像是不可变的、且版本可追溯。
- 不可变:一个 tag(
bid:9f3a1c)一旦推上去,内容永不改变——你任何时候拉它,拿到的都是同一个二进制。 - 可追溯:tag = git sha,意味着从线上运行的镜像能反查到确切的代码提交;回滚就是”把 Deployment 的镜像换回上一个 git sha”,精确、确定。
反过来,用 latest 就把这两条全毁了——同一个 latest 今天和昨天可能是完全不同的内容,回滚时你根本不知道”上一个可用版本”对应哪个镜像。这正是下面 war story 的病根。
6. Push 式 CI/CD vs Pull 式 GitOps
前面 Jenkins 的部署方式(流水线里直接 kubectl apply)属于 push 式。业界还有另一种主流范式 GitOps(pull 式),值得知道两者的取舍。
Push 式(Jenkins)流水线跑完直接把变更推给集群;Pull 式(GitOps)由集群内控制器持续监听 Git、把真实状态收敛到声明。
- Push 式(Jenkins 等 CI 工具):流水线跑完,主动用
kubectl/helm把变更推到集群。简单直接、和 CI 天然连贯,但CI 必须持有集群写凭据、且集群真实状态可能被人kubectl edit后和仓库悄悄漂移、变更审计也偏弱。 - Pull 式(GitOps,Argo CD / Flux):Git 仓库是唯一可信源(期望状态),集群里跑一个控制器(Argo CD/Flux)持续监听 Git,一旦发现集群实际状态和 Git 声明不一致,就自动 reconcile 收敛回去。开发者/CI 只往 Git 提交 manifest(走 PR 审计),不直接碰集群——凭据不外泄、状态防漂移、回滚就是
git revert。
关键认知:Jenkins 属于 push 式,GitOps 是另一种范式,不是”谁取代谁”。很常见的组合是——Jenkins 负责 CI(编译、测试、打镜像)并把新镜像 tag 写回 Git 仓库的 manifest,然后交给 Argo CD 完成 pull 式部署,两者各取所长。本篇聚焦 Jenkins 这条 push 式主线,GitOps 只做范式对比、点到为止。
参考
- Jenkins. Pipeline as Code & Jenkinsfile(声明式流水线语法):
pipeline/agent/stages/stage/steps/post的一手权威说明。 - Jenkins. Kubernetes plugin(动态 Agent Pod):用 K8s Pod 做按需 Agent、构建完销毁的官方文档。
- Jenkins. Using credentials(凭据管理):如何安全地在流水线中引用密钥而不硬编码。
- Kubernetes. Rolling Update Deployment & Rollback(rollout undo):滚动更新与回滚到历史版本的机制。
- Argo CD. What is GitOps / Argo CD Declarative GitOps:pull 式 GitOps 的代表实现与 reconcile 心智。
- Martin Fowler. Continuous Integration / Continuous Delivery:CI、持续交付、持续部署概念区分的经典阐述。