CI/CD 端到端落地实录:从 Git Push 到 K8s 发布的完整流水线
- 约 21 分钟读完 - 全文 8069 字CI/CD 端到端落地实录:从 Git Push 到 K8s 发布的完整流水线
目标读者:会写 Jenkinsfile、会用 kubectl,但还没在生产环境把「提交代码 → 自动发布 → 失败回滚」整条链路跑通的同学。 文中例子取自我主导的真实项目 videocloud(视频云转码服务,Spring Boot + Nacos + K8s),与站内《Kubernetes 学习总结与避坑指南》(kubernetes-authoritative-guide-notes.md)互为补充。
CI/CD 的 Demo 半小时能搭出来,生产可用要踩半年坑。kubectl apply 返回 0 不代表发布成功,Pod Ready 不代表配置生效。本文按真实落地顺序拆解五个阶段,每个阶段给出可抄的配置和踩过的坑。
一、全景:一条流水线要干多少事
graph LR
Dev[git push / merge] --> WH[Webhook 触发 Jenkins]
subgraph S1["阶段一:拉取与编译"]
A1[检出 commit] --> A2[恢复依赖缓存]
A2 --> A3[单测 + 打包]
end
subgraph S2["阶段二:推送产物"]
B1[构建镜像] --> B2[推 ACR<br/>tag = git commit]
B3[渲染部署模板<br/>快照留档]
end
subgraph S3["阶段三:执行变更单"]
C1[DB changelog<br/>Liquibase] --> C2[Nacos changelog<br/>OpenAPI 发布]
end
subgraph S4["阶段四:部署与验证"]
D1[更新 ConfigMap<br/>+ 滚动更新] --> D2[rollout status 阻塞]
D2 --> D3[冒烟测试]
D3 -->|成功| OK[发布完成]
D3 -->|失败| RB[rollout undo<br/>自动回滚 + 告警]
end
WH --> S1 --> S2 --> S3 --> S4
阶段顺序有一条铁律:先备好「环境」,再动「代码」——数据库先于应用升级(新代码依赖新表结构),配置先于 Pod 重启(新 Pod 不能拿旧配置)。顺序反了,就是半夜被叫醒的那种故障。
二、阶段一:Jenkins 拉取与编译
2.1 触发:Webhook,不是轮询
Git 平台配 Webhook,push/merge 时推给 Jenkins。不要用 SCM 轮询——几十个仓库每分钟扫一遍,Jenkins 自己先爆了。分支与环境对应:feature/* 只编译+单测,develop 自动部署到 dev,master/tag 走完整流水线 + 人工审批发 prod。
2.2 编译提速:缓存 + 并行
stage('拉取与编译') {
steps {
checkout scm
sh '''
mvn -q -T 1C test # 单测先跑,挂了直接终止
mvn -q -T 1C -DskipTests package # -T 1C 按核数并行
'''
}
}- 依赖缓存是性价比最高的优化:
~/.m2挂共享 PVC,构建从 5 分钟压到 1 分钟。 - 单测只跑变更相关模块(
mvn -pl -am),全量留给 nightly。门禁是拦问题的,不是表演严格的。
2.3 Jenkins 自身怎么部署才不拖后腿
流水线慢,一半原因在 Jenkins 自己的形态。我们的规范五条:
- Controller 只调度不构建(executors 设为 0),否则构建打满 Controller,UI 卡死、webhook 丢失。
- agent 用 Kubernetes 插件动态拉起:按语言预定义 Pod 模板(maven/node/go),构建完即销毁,并发上限就是集群容量:
# agent Pod 模板要点
spec:
nodeSelector: { node-pool: ci-build } # 调度到 CI 专用节点池
containers:
- name: maven
image: registry.../ci/maven:3.9-jdk17 # 自维护构建镜像,工具一次装好
resources:
requests: { cpu: "4", memory: 8Gi }
volumeMounts:
- { name: m2-cache, mountPath: /root/.m2 }
volumes:
- name: m2-cache
persistentVolumeClaim: { claimName: ci-m2-cache }- CI 独立节点池:SSD 磁盘(编译是 IO 密集型)、taint 防业务混部、闲时自动缩容。
- 构建镜像自己维护:JDK、Maven 私服配置、docker/kubectl/cosign 全打进去,agent 起来就能干活。
- 部署阶段并行化:无依赖的服务用
parallel并发部署。我们 8 个服务的流水线从 25 分钟降到 9 分钟。
三、阶段二:推镜像、推配置
3.1 镜像 tag:回滚能力的地基
environment {
IMAGE = "registry.cn-hangzhou.aliyuncs.com/mycompany/videomart-videocloud"
TAG = "${env.GIT_COMMIT.take(8)}" // git commit 短哈希:唯一、可追溯、可回滚
}三条规矩:
- 部署 tag 必须唯一可追溯,杜绝
latest——回滚时说不清「上一个 latest」是谁。 - 生产仓库开 tag 不可变,禁止覆盖推送,否则同 tag 不同代码,回滚失效。
- 推送后 cosign 签名,K8s 侧用 Kyverno 校验,未签名拒绝部署——镜像仓库凭证泄露时,这是最后一道闸。
3.2 部署配置:仓库里只存一份参数化模板
原则:代码仓库不按环境拆分配置(没有 prod/、dev/ 目录)。同一份配置写三遍,改一处忘两处是必然结局。videocloud 的做法——部署模板随应用仓库,全部 ${占位符}:
videocloud/
├── k8s_deploy/ # 部署模板:环境无关,只有一份
│ ├── k8s-deployment-videomart-videocloud.yaml # 头部注释列出全部参数
│ ├── k8s-service-videomart-videocloud.yaml
│ └── k8s-persistentvolumeclaim-videomart-videocloud.yaml
├── changelog/ # 数据库/Nacos 变更单(第四章)
└── src/main/resources/bootstrap.properties # 服务读 Nacos 的方式(第五章)### 参数: APP_NAME, APP_PORT, NAMESPACE, VERSION
kind: Service
apiVersion: v1
metadata:
name: videomart-videocloud
namespace: ${NAMESPACE} # 发布到哪个环境,由 CD 注入
spec:
ports:
- { name: http-0, port: 9577, targetPort: 9577 }流水线在这一步把渲染结果(含镜像 tag、namespace、副本数的完整快照)提交到发布记录库留档。模板只有一份保证「写法一致」,快照留档保证「可审计、可回滚」,两件事分开解决。
四、阶段三:变更单驱动——数据库与 Nacos
发布事故的头号根源不是工具,而是变更没落盘:开发改了字段口头通知运维,配置加在 Nacos 控制台、测试环境忘了加。规范只有一条:一次迭代对「环境」的所有变更——表结构、Nacos 配置——必须作为 changelog 文件随代码提交、随代码评审。
videocloud/changelog/
├── database/ # Liquibase 格式
│ ├── videomart-videocloud-changelog-db-v6.0.4.0-01.yaml
│ └── ...-02.yaml ...
└── nacos/
└── videomart-videocloud-changelog-nacos-v6.0.4.0-01.yaml命名 {服务}-changelog-{db|nacos}-{版本}-{序号}.yaml,序号定执行顺序。CD 的职责是按序、幂等地执行:
graph LR
A[扫描 changelog<br/>按版本+序号排序] --> B{changeSet id<br/>已执行?}
B -->|是| C[跳过,幂等]
B -->|否| D["渲染占位符<br/>${VAR} ← Jenkins 凭据"]
D --> E[执行<br/>DB: liquibase update<br/>Nacos: OpenAPI]
E --> F[记录 id<br/>失败即终止流水线]
4.1 数据库变更单(Liquibase)
# changelog/database/videomart-videocloud-changelog-db-v6.0.4.0-01.yaml(节选)
databaseChangeLog:
- changeSet:
id: videomart-videocloud-changelog-db-v6.0.3.0-05
author: cline
comment: Add stream task table for transcoding
changes:
- sql:
sql: |-
CREATE TABLE `GTT_VIDEOMART_VIDEOCLOUD_STREAM_TASK` ( ... );CD 用官方镜像执行,已执行的 changeSet 记在 DATABASECHANGELOG 表里,天然幂等:
sh '''
docker run --rm -v $WORKSPACE/changelog/database:/liquibase/changelog \
liquibase/liquibase:4.29 \
--url=jdbc:mysql://prod-mysql:3306/videomart \
--username=$DBU --password=$DBP \
--changelog-file=videomart-videocloud-changelog-db-${VERSION}.yaml \
update
'''纪律三条:
- 只增不改、向后兼容:滚动更新期间新旧 Pod 同时在线,新代码要的表结构先就位,旧代码不能被新结构弄挂。
- 破坏性变更拆两次发布:先发「代码不再读写该字段」,观察一周,再发「删字段」。代码能回滚,删掉的列回不来。
- 执行过的 changeSet 绝不许改(checksum 变了 Liquibase 直接拒绝),要改就写新变更单。
4.2 Nacos 变更单
自定义格式,与 DB 同一套约定(videocloud 真实文件):
# changelog/nacos/videomart-videocloud-changelog-nacos-v6.0.4.0-01.yaml
nacosChangeLog:
- changeSet:
id: videomart-videocloud-changelog-nacos-v6.0.4.0-01
comment: videocloud drm configuration
changes:
- namespace: ${NAMESPACE}
group: videocloud
dataId: gtt-videomart-videocloud.properties
format: properties
patchContent: |-
gtt.videomart.videocloud.db.username = ${VIDEOMART_VIDEOCLOUD_DATABASE_USERNAME}
gtt.videomart.videocloud.aws.s3.accessKeyId = ${FUSIONSTORAGE_ACCESS_KEY}
gtt.videomart.videocloud.aws.s3.secretAccessKey = ${FUSIONSTORAGE_SECRET_KEY}注意:变更单里没有一个真实密钥,全是 ${占位符}——「key 叫什么、引哪份凭据」可以进 Git,「值是什么」不可以。
patchContent 是增量。CD 脚本逻辑:GET 现有配置 → envsubst 渲染增量 → 合并(增量覆盖同名 key)→ 整体发布 → 读回校验 → 记录 id。发布用的是 Nacos OpenAPI(POST /nacos/v1/cs/configs),Nacos 客户端长轮询秒级生效,不需要重启 Pod。生产 Nacos 必须开鉴权。
五、凭证权限划分:密钥只有 CD 管理员可见
占位符背后的真实值在哪、谁能碰?分三层:
graph TB
Dev[开发者] -->|只能看到| Git[Git 仓库<br/>changelog 占位符 / 模板]
CD[CD 管理员] -->|独占维护| Cred[Jenkins 凭据<br/>DB 账号 / S3 密钥]
CD -->|独占维护| Sec[K8s Secret<br/>videomart-videocloud-secret]
Pipe[CD 流水线] -->|"withCredentials 临时注入"| Cred
Pod[运行中的 Pod] -->|"envFrom / secretRef"| Sec
四条规矩:
1. Jenkins 分角色。 Role-Based Strategy 插件:开发者能触发构建、看日志;只有 CD 管理员能管凭据、改生产流水线。凭据按 Folder 隔离,prod 的 DB 账号对开发者连「存在」都不可见。流水线里 withCredentials 临时注入,用完即销毁。
2. 服务只引用配置,绝不内嵌。 videocloud 的 bootstrap.properties 节选:
spring.cloud.nacos.config.server-addr=${NACOS_SERVER_ENDPOINT:localhost:8848}
spring.cloud.nacos.config.namespace=${NACOS_NAMESPACE:videomart}
spring.cloud.nacos.config.group=videocloud
spring.cloud.nacos.config.username=${NACOS_USERNAME}
spring.cloud.nacos.config.password=${NACOS_PASSWORD}
spring.cloud.nacos.config.refresh-enabled=true
spring.config.import=optional:nacos:gtt-videomart-videocloud.properties服务只声明「连哪个 Nacos、读哪个 dataId」,账号全占位,生产由 K8s Secret 注入。默认值只给本地开发用,宁可留空让缺参时启动直接报错,也不要把内网地址当真默认值写进仓库。
3. 注入靠 Secret 引用。 看 k8s_deploy 的 Deployment 模板:
envFrom:
- configMapRef: { name: gtt-common-config } # 公共非敏感配置
- secretRef: { name: videomart-videocloud-secret } # 本服务的 Nacos/DB 账号
- secretRef: { name: gtt-common-secret } # 公共敏感配置Secret 由 CD 管理员提前建好,开发者不需要、也不应该知道里面的值。
4. 轮换只动一处。 换 DB 密码 = 更新 Jenkins 凭据 + K8s Secret + 触发一次流水线,代码和模板都不用动。这就是「引用配置」相对「携带配置」的收益。
六、阶段四:更新 ConfigMap
《Kubernetes 学习总结与避坑指南》第七章讲透了两个坑,这里给结论:
- 环境变量注入的 ConfigMap,改了不生效,必须重启 Pod;文件挂载方式 kubelet 分钟级同步,但应用要自己支持 reload。
subPath挂载单文件的 ConfigMap 永远不自动更新。
流水线的稳妥做法:kubectl apply 更新 ConfigMap 后无脑跟一次滚动更新——反正接下来要换镜像,合成一次滚动零成本。「要不要重启」交给脚本做死,比交给值班同学半夜判断可靠。
七、阶段五:部署与验证
7.1 部署配置长什么样
发布完成后集群里的对象关系:
graph TB
GW[Gateway<br/>order.example.com] --> RT[HTTPRoute<br/>/api/order/**]
RT --> SVC[Service videomart-videocloud<br/>ClusterIP 9577]
SVC --> P1[Pod ✅] & P2[Pod ✅] & P3[Pod ✅]
DEP[Deployment<br/>replicas: 3<br/>RollingUpdate] -.管理.-> P1
CM[ConfigMap] -.挂载.-> P1
NC[(Nacos)] -.长轮询推送.-> P1
NC -.长轮询推送.-> P2
NC -.长轮询推送.-> P3
P1 --> DB[(MySQL)]
videocloud 的 Deployment 模板里有几处直接抄的规范:podAntiAffinity 强制同服务 Pod 分散到不同节点、topologySpreadConstraints 跨可用区打散、revisionHistoryLimit: 10 留回滚版本、探针端口与容器启动端口写死一致。对外暴露用 Gateway API(已 GA,Ingress 停止演进),把「入口」(Gateway,运维管)和「路由」(HTTPRoute,开发管)拆开。
7.2 部署与验证:流水线里真正干活的命令
stage('部署与验证') {
steps {
sh '''
# 1. 渲染模板并部署
envsubst < k8s_deploy/k8s-deployment-videomart-videocloud.yaml \
| kubectl --kubeconfig=$KUBECONFIG apply -f -
# 2. 阻塞等待滚动更新完成,超时/失败非零退出
kubectl --kubeconfig=$KUBECONFIG -n $NAMESPACE \
rollout status deployment/videomart-videocloud --timeout=300s
# 3. 冒烟测试:健康检查 + 核心业务接口
curl -fsS --retry 5 http://videomart-videocloud:9577/healthz || exit 1
'''
}
post {
failure {
sh "kubectl --kubeconfig=$KUBECONFIG -n $NAMESPACE \
rollout undo deployment/videomart-videocloud || true"
// + 钉钉/邮件告警
}
}
}再强调一遍:apply 成功只代表 apiserver 收下了 YAML。rollout status 阻塞 → 冒烟测试 → 失败自动 undo 三件套缺了,你的流水线只是「自动提交 YAML 的机器」。
7.3 部署凭证:别把 admin kubeconfig 给 Jenkins
给 Jenkins 一个只能操作特定 namespace、只能 patch Deployment 的 ServiceAccount(Role 限定 verbs get/list/watch/patch/update,完整 RBAC 示例见笔记 12.3 节)。凭证泄露的爆炸半径锁死在一个 namespace,且随时可 delete rolebinding 吊销。prod 部署再叠一个 input 人工审批。
7.4 弹性伸缩:HPA 不是配上就完事
前提:requests 必须配准(按 P95 实际用量)——HPA 的利用率是 实际用量 / requests,requests 瞎填,扩缩时机全错。
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: { name: videomart-videocloud }
spec:
scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: videomart-videocloud }
minReplicas: 3 # 生产最小副本 ≥ 2,单副本没资格谈弹性
maxReplicas: 20 # 按下游容量倒推(DB 连接数 / 每 Pod 连接数)
metrics:
- type: Resource
resource: { name: cpu, target: { type: Utilization, averageUtilization: 60 } }
behavior: # 快扩慢缩
scaleUp:
stabilizationWindowSeconds: 0
policies: [{ type: Percent, value: 100, periodSeconds: 60 }] # 每分钟最多翻倍
scaleDown:
stabilizationWindowSeconds: 300 # 缩容观察 5 分钟
policies: [{ type: Pods, value: 1, periodSeconds: 120 }]配套三条:HPA 必搭 PDB(防节点维护+缩容+发布叠加把服务缩没);GitOps 场景 Deployment YAML 别写死 replicas(否则 ArgoCD 每次同步把 HPA 扩的副本压回去);HPA 管 Pod、Cluster Autoscaler 管节点,两层都要配,流量可预期的业务用 KEDA 定时伸缩提前备容量。
7.5 灰度发布:现有流程加两个 stage 就能支持
前面几节的设计纪律已经埋好了灰度所需的前提:模板里 Deployment 命名带版本(videomart-videocloud-${VERSION})、Pod 打 app + version 双标签,而 Service 的 selector 只选 app——新旧两个 Deployment 可以并行存在,Pod 同时挂在同一个 Service 下。在这个基础上,灰度有两级做法:
第一级:副本级灰度(零新组件)。 渲染同一份模板,只换 VERSION 和 REPLICAS,拉起 1 个新版本副本与旧版并存——流量比 ≈ 副本比(3 旧 + 1 新 ≈ 25% 流量进新版):
VERSION=v6.0.5.0 REPLICAS=1 IMAGE=$IMAGE:$TAG \
envsubst < k8s_deploy/k8s-deployment-videomart-videocloud.yaml \
| kubectl --kubeconfig=$KUBECONFIG apply -f -第二级:权重级灰度(精确比例)。 新旧版本各起一个 Service(selector 分别锁定各自 version),用 Gateway API 的 HTTPRoute backendRefs 配 weight: 95 / 5,或上 Istio VirtualService 按权重/请求头分流。要「只让内部员工进新版」这类按用户维度的灰度,只能走这一级。
流水线形态——在「变更单执行」和「全量部署」之间插入灰度段:
graph LR
A[变更单执行] --> B[部署 canary<br/>1 副本或 5% 权重]
B --> C[观察期<br/>指标对比 / 人工 input]
C -->|正常| D[全量滚动更新<br/>删除 canary]
C -->|异常| E[删除 canary<br/>流量即刻回稳]
要点四条:
- 灰度期新旧版本同时在线,DB/Nacos 变更必须向后兼容——changelog「只增不改」的纪律正是为此准备的;不兼容的变更没有资格灰度。
- 观察期看指标不看感觉:canary 的错误率、P99 延迟与 stable 对比,Prometheus 按
version标签分组就能出数——这就是 Pod 要打 version 标签的另一个理由。观察期可以是定时(如 10 分钟)加 Jenkinsinput人工确认。 - 回滚动作极轻:删掉 canary Deployment,Service selector 自动不再选中它,秒级完成,不用动旧版本一根毫毛。
- Nacos 配置也能灰度:配置支持按 IP/标签灰度下发,配置类变更先推给 canary 实例验证,再全量。
7.6 实操:给新版本分 20% 流量的完整跑法
以「线上 v6.0.4.0 共 4 副本,给 v6.0.5.0 分 20% 流量」为例,两种做法选一。
做法 A:副本比例近似(4 旧 + 1 新 ≈ 20%)。改动最小,适合服务间(东西向)流量或没有网关层权重能力的场景:
# 1. 变更单已执行完毕(DB/Nacos,向后兼容)
# 2. 起 1 个新版本副本(与旧版并存,同一个 Service 下)
VERSION=v6.0.5.0 REPLICAS=1 IMAGE=$IMAGE:$NEW_TAG \
envsubst < k8s_deploy/k8s-deployment-videomart-videocloud.yaml | kubectl apply -f -
kubectl rollout status deployment/videomart-videocloud-v6.0.5.0 --timeout=300s
# 3. 观察:Prometheus 按 version 分组对比错误率 / P99
# 4a. 正常 → 全量:新版本扩到满配,再删旧版本
VERSION=v6.0.5.0 REPLICAS=4 IMAGE=$IMAGE:$NEW_TAG \
envsubst < k8s_deploy/k8s-deployment-videomart-videocloud.yaml | kubectl apply -f -
kubectl rollout status deployment/videomart-videocloud-v6.0.5.0 --timeout=300s
kubectl delete deployment videomart-videocloud-v6.0.4.0
# 4b. 异常 → 回滚:删掉 canary 即可,旧版全程没动过
kubectl delete deployment videomart-videocloud-v6.0.5.0做法 B:HTTPRoute 权重精确 20%。适合入口(南北向)HTTP 流量。需要两个按 version 锁定 selector 的 Service:
# stable 和 canary 各一个 Service,selector 分别锁死版本
# videomart-videocloud-stable: selector { app: videomart-videocloud, version: v6.0.4.0 }
# videomart-videocloud-canary: selector { app: videomart-videocloud, version: v6.0.5.0 }
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata: { name: videocloud-route }
spec:
parentRefs: [{ name: prod-gateway }]
hostnames: ["videocloud.example.com"]
rules:
- backendRefs:
- { name: videomart-videocloud-stable, port: 9577, weight: 80 }
- { name: videomart-videocloud-canary, port: 9577, weight: 20 }调比例就是改 weight(20% → 50% → 100%),每次 kubectl apply 即时生效。全量后把 stable Service 指向新版本、删掉 canary,路由权重归位。
做法 B 有一个必踩的坑:原来那个 selector 只选 app 的 Service,灰度期间会同时选中新旧 Pod——HTTPRoute 里 20% 的权重之外,走老 Service 的内部调用方还在按副本比随机进新版,等于流量叠加失控。所以权重灰度前,必须把老 Service 的 selector 也加上 version 锁到旧版,或者干脆只保留 stable/canary 两个 Service。
对应的 Jenkinsfile 就是在「变更单执行」后加两个 stage:
stage('灰度 20%') {
steps { sh 'deploy-canary.sh $NEW_TAG 20' } // 上面做法 A/B 的脚本化
}
stage('灰度观察') {
steps {
sh 'check-canary-metrics.sh' // 错误率/P99 对比,超阈值非零退出
input message: '灰度指标正常,是否全量?' // 人工确认,prod 必备
}
}
stage('全量') { steps { sh 'promote-canary.sh $NEW_TAG' } }
post { failure { sh 'cleanup-canary.sh || true' } } // 任何失败自动清理 canary八、配置治理:文档化与三环境一致性
8.1 配置文档化:阈值不再是「口口相传」
症状:限流阈值为什么是 500 不是 1000,只有离职的那个人知道。解法:每个服务在仓库里维护 CONFIG.md,强制四列——配置项 / 含义 / 当前值 / 取值依据与影响面:
| 配置项 | 含义 | 当前值 | 取值依据与影响面 |
|---|---|---|---|
upload.retryCount |
上传重试次数 | 5 | 对象存储偶发抖动 P99 在 3 次内恢复;超过 5 次基本是故障,应快速失败 |
upload.chunkSize |
分片大小 | 5MB | 内网带宽实测拐点;调大加剧内存压力 |
纪律三条:配置项三处一致(Git 里的值、CONFIG.md、代码注释,评审时卡点检查);阈值必须写取值依据(拍脑袋的值两个版本后就成了没人敢动的屎山);开关必须有退役计划(全量后一个迭代内删除)。进阶:用 JSON Schema 在推送 Nacos 前校验类型和取值范围,低级错误 CI 阶段拦住。
8.2 三环境一致性:写法一致,取值注入
「在测试环境是好的」的根源:各环境配置是各自维护的副本。解法前面已经埋下——仓库里只有一份参数化模板和变更单,差异压缩成发布时注入的参数:
| 差异项 | 注入方式 | 维护者 |
|---|---|---|
| namespace、集群地址 | CD 环境参数 | CD 管理员 |
| 副本数、资源 requests/limits | CD 环境参数 | CD 管理员 |
| DB 账号、密钥、合作方地址 | Jenkins 凭据 → envsubst | CD 管理员(开发者不可见) |
| 业务阈值、特性开关 | changelog 变更单,三环境执行同一份 | 开发者(评审把关) |
「写法」必须三环境一致,「取值」本来就该不同。再加两道保险:发 prod 前用各环境参数分别渲染同一份模板并 diff(源头只有一份,diff 干干净净只有几行);每周漂移检测,发现有人手工改过线上配置就告警(上 ArgoCD 后免费获得)。
九、行业动态:2025–2026 值得关注的变化
- GitOps 成默认答案:Jenkins 只构建推镜像,集群内 ArgoCD 从 Git 同步部署——CI 不再需要生产集群凭证,安全性质变。
- Gateway API 取代 Ingress:后者已停止演进,新项目别再用它起头。
- 供应链安全成必修:cosign 签名、SBOM、SLSA 框架已进入金融政企招标要求。
- CI 引擎分化:Tekton、Dagger 各有拥趸,GitHub Actions 吞噬中小团队;Jenkins 份额缓降,但存量企业和内网仍是主力。
- AI 进流水线:CI 失败日志自动归因、pipeline 脚本生成评审已经实用;「AI 自动决定发布」不建议——爆炸半径需要人负责。
十、思考与感悟
1. 流水线的本质是「把运气变成流程」。 手工发布的质量取决于值班同学当晚的状态。流水线的价值不是快,是把「老司机会记得检查、新人一定会漏」的 20 个检查点固化成代码——先消灭方差,其次才是耗时。
2. 最贵的知识是顺序和兼容。 DB 先于应用、配置先于重启、破坏性变更拆两次——背后是同一个认知:发布中系统处于新旧混合的中间态,所有设计都要保证中间态可用。蓝绿、金丝雀、滚动更新都只是它的具体实现。
3. 验证深度决定发布频次。 没有可靠自动验证 → 发布靠人肉兜底 → 怕 → 少发 → 单次变更量大 → 更怕,恶性循环。打破循环的入口就是把验证做深:rollout 阻塞、冒烟、自动回滚。验证越可信,发布越敢小而频。
4. 配置有权限边界。 「引用关系」进 Git,「真实值」进凭据系统,且只有 CD 管理员能碰。这条边界一开始不划,等密钥躺在几百人可见的仓库里再补救就晚了。
十一、避坑清单(速查)
- Webhook 触发,不用轮询;Controller 不跑构建;agent 用一次性 Pod + 依赖缓存 PVC + CI 专用 SSD 节点池;无依赖的部署步骤并行。
- 镜像 tag = git commit 短哈希,杜绝
latest;仓库开 tag 不可变;cosign 签名。 - DB 与 Nacos 变更一律走 changelog 变更单,随代码评审,CD 按序幂等执行;控制台手工改属违规。
- 数据库变更只增不改;破坏性变更拆两次发布;执行过的 changeSet 绝不许改。
- 变更单和模板只写
${占位符};真实密钥只在 Jenkins 凭据和 K8s Secret,只有 CD 管理员可见;服务一律envFrom/secretRef引用。 - 每个服务维护 CONFIG.md 四列(配置项/含义/当前值/取值依据);开关有退役计划。
- 仓库只存一份环境无关模板,环境差异由 CD 注入;发 prod 前渲染结果 diff;每周漂移检测。
- ConfigMap 更新后跟一次滚动更新(env 注入和 subPath 都不自动生效)。
- Deployment 必配 readinessProbe +
maxUnavailable: 0+ preStop 延迟;revisionHistoryLimit别设 0。 - 验证三件套:rollout status → 冒烟 → 失败自动 undo + 告警。
- HPA:requests 按 P95 配准;minReplicas ≥ 2、maxReplicas 按下游容量倒推;快扩慢缩 + PDB;GitOps 场景别写死
replicas。 - 灰度发布:Service selector 只选 app、Deployment 按版本并行,先副本级灰度,要精确比例再上 HTTPRoute 权重;观察期看 version 分组的指标;回滚 = 删 canary。
- Jenkins 用最小权限 ServiceAccount,prod 加
input审批;新系统对外暴露用 Gateway API;渲染快照进发布记录库,为 GitOps 铺路。
参考资料:
- 《Kubernetes 学习总结与避坑指南》(本站 kubernetes-authoritative-guide-notes.md):ConfigMap 注入机制、RBAC 最小权限、rollout 验证的完整细节
- Jenkins 官方文档 / Liquibase / Nacos OpenAPI
- Kubernetes Gateway API / ArgoCD / SLSA