1. 为什么要把 CI/CD 搬上 Kubernetes
过去几年我经手过不少部署方案,从最早的脚本打包、到后来基于虚拟机的 Jenkins 集群、再到容器化部署,每一步都是在解决同一个问题:环境不一致导致的上线事故。而把 CI/CD 这套流程本身跑在 Kubernetes 上,不是赶时髦,是解决这个问题的自然终点。
先说个最直观的痛点。传统 Jenkins 集群跑构建任务,一台机器上可能同时挂了四五个构建任务,某次构建把环境搞坏了,其他任务全跟着遭殃。或者构建高峰来了,机器资源不够,任务排队排了半小时。这些问题的根源在于,CI/CD 的底层资源和线上业务跑的是完全隔离的两套体系,没法弹性伸缩。
Kubernetes 天然适合干这件事。它把 CPU、内存当成池子,能自动调度、自动扩容,任务多就多跑几个 Pod,任务少就缩回去。同样的逻辑既能跑在线业务,就能跑构建任务和部署任务。一套体系从代码提交到线上发布完全打通,不用维护两套基础设施。
我的推荐对象很明确:正在做容器化转型但 CI/CD 还在用传统虚拟机方案的团队;天天被“我这个环境明明跑得好好的,怎么你一发布就挂了”这种撕扯折磨的团队;以及想搞多环境自动部署但还在用脚本硬扛的团队。如果你已经在 K8s 上跑业务了,那这套东西更是迟早要做的事。
这个框架要解决的核心问题有三个。第一,代码提交之后到镜像构建完成这个环节,怎么保证每次构建的环境是一致的。第二,测试、预发、生产多套环境的部署流程,怎么做到只改一个配置就能切换。第三,发版出问题的时候,怎么快速发现、快速回滚,而不是翻聊天记录找上一个版本是哪个镜像。
后面我会把所有环节拆开来讲:流水线怎么设计、工具怎么选、环境怎么管理、镜像怎么构建、发布策略怎么选、以及我踩过的那些坑。
2. 整体架构设计与工具选型逻辑
2.1 一个完整的流水线长什么样
先看架构全貌。我们假定一个典型的微服务项目,代码托管在 GitLab,整个流水线从代码提交开始,到生产环境完成滚动更新结束,中间经过五个阶段:
- 代码提交触发:开发者 push 代码到指定分支,触发流水线。
- 静态检查与单元测试:这个阶段和容器无关,跑的是代码仓库里定义的脚本。
- 镜像构建与推送:核心环节,把代码打包成 Docker 镜像,推到镜像仓库。
- 环境描述文件生成:根据目标环境(测试、预发、生产)生成不同的 Kubernetes 资源配置清单。
- 部署与验证:把资源清单应用到 K8s 集群,等待滚动更新完成,做健康检查。
这五个阶段里,阶段 3 和阶段 5 是整个框架的技术核心。阶段 3 关注“构建”,阶段 5 关注“发布”,中间用镜像仓库和配置文件解耦。
2.2 为什么选 GitLab CI 而不是别的引擎
流水线引擎的选择,市面上主流的无非那几种:GitLab CI、Jenkins、Drone 这类云原生 CI,还有云厂商自带的流水线产品。我在这套框架里选的是 GitLab CI,理由很实际:它和代码仓库是一体的,不需要单独维护一套触发系统。
用 GitLab CI 的时候,.gitlab-ci.yml文件直接放在代码仓库里,流水线的定义和代码本身一起做版本管理。改流水线就像改代码一样,有 MR、有评审、能回溯。流水线即代码这个理念,直接决定了这套框架的可持续性。
还一个重要因素是 Kubernetes 集成。GitLab Runner 支持以 Pod 的形式跑在 K8s 集群里,每次构建任务动态创建一个 Pod,任务结束自动销毁。构建环境永远是干净的,不会出现“上次跑完任务残留了什么脏数据影响这次构建”的情况。
再说下 Drone,也是云原生 CI 的代表,基于容器执行,启动速度快,但它的可观测性和权限模型没有 GitLab 那么成熟。Jenkins 的插件生态强大,但在 K8s 下的体验比较痛苦,Master 和 Agent 的关系、权限控制、配置管理,都比 GitLab CI 重。最终选型还是那句话:结合团队最熟悉、最容易维护的方案来定,没必要为了技术新鲜感增加维护量。
2.3 为什么要用动态 Runner 而不是固定节点
传统 Jenkins 的 Agent 是固定几台机器,装好 JDK、Maven、Docker 客户端,然后等任务来。这种模式在资源利用率和环境一致性上有天然缺陷。
GitLab Runner 调度到 K8s 集群后,每个构建任务对应一个独立的 Pod,任务结束 Pod 销毁,环境变量、构建缓存全部随之清空。这意味着:两台机器同时跑不同项目的构建,互不干扰;构建高峰期可以靠 HPA 自动扩展 Runner 的副本数量;同一时间多个项目的构建任务并发跑,资源按需分配。
这套方案还有一个容易被忽略的优势:团队里任何一台电脑都能操作整个 CI/CD 系统的扩缩容和调度,只要你有访问 K8s 集群的权限。你不用 SSH 登录到某台固定的构建服务器上去执行命令,所有操作都能通过 Kubernetes API 完成。
2.4 镜像仓库这里踩过的选型坑
镜像仓库是这套框架里的薄弱环节,很多队伍意识到它的定位。一开始我图省事,直接用了 Docker Hub 的私有仓库,结果就是构建时的拉取速度慢,而且依赖公网连接。后来切换到自建的 Harbor,能保存的镜像数量大幅上去了,清理策略也更可控。
Harbor 的好处不只是存储。它内置镜像扫描,能检测出基础镜像里的已知漏洞;有完整的镜像签名机制,部署时可以校验镜像是否被篡改;再加上 Webhook 通知,镜像构建完成后能自动触发下一阶段的流程。这些能力在 K8s 部署的场景下都是刚需。
仓库策略也不难。镜像的 tag 绝不使用latest,而是用分支名 + 提交号 + 构建时间,例如main-a1b2c3-202501151030。这样生产环境上部署的镜像和代码提交之间能精确对应,回滚时知道确切拉取的镜像,排查问题时也能知道是谁在什么时间提交的代码导致了这个版本的行为变化。
3. 核心环节解析:镜像构建与管理机制
3.1 构建镜像还是用 Kaniko 这样无特权的方案
在 K8s 里跑 Docker 构建,第一反应是用Docker-in-Docker,即挂载宿主机的/var/run/docker.sock让容器里跑 docker 命令。这个方法在开发环境没问题,但在生产集群里有严重隐患:容器能直接操作宿主机的 Docker daemon,等于给了完整的 root 权限。一旦某个构建任务被恶意代码注入,整个集群就沦陷了。
我的选择是Kaniko。它最大的特点是在用户空间完成镜像构建,不需要特权模式,不依赖 Docker daemon。构建任务以普通 Pod 的方式运行,配置上不会涉及敏感权限。这一点对于多租户共享 K8s 集群的场景特别重要。
Kaniko 的构建原理和 Docker Build 略有不同。它从一个基础镜像开始,执行每条指令时,把当前文件系统的状态快照下来,和上一步的状态做对比,差异部分就是这一层的内容。最后把所有层组装成一个完整镜像。每一层的构建结果都是幂等的。同一个 Dockerfile 在任何时间构建,得到的各层内容是一致的,这对缓存复用非常友好。
3.2 镜像层缓存与按需清理策略
Kaniko 有一个很重要的问题要处理:缓存。
如果把缓存存在 Pod 内,任务结束就没了,每次构建都要重新拉基础镜像。我的做法是挂载持久化存储作为缓存卷,常用基础镜像的层就存在里面。拉取基础镜像的时间从原来的每次几分钟缩到几秒。
下面是一段极简流水线定义示例,用 GitLab CI 的语法表达这个逻辑:
build-service-a: stage: build image: name: gcr.io/kaniko-project/executor:v1.23.2 entrypoint: [""] variables: KANIKO_CACHE: "true" KANIKO_CACHE_DIR: "/cache" script: - /kaniko/executor --context="${CI_PROJECT_DIR}" --dockerfile="${CI_PROJECT_DIR}/Dockerfile" --destination="registry.example.com/myapp/service-a:${CI_COMMIT_SHORT_SHA}" cache: key: kaniko-cache paths: - /cache tags: - k8s-runner镜像清理也是不可忽视的一环。Harbor 仓库的保留策略我设为:每个项目保留最近 30 个镜像,14 天前的非稳定 tag 全部清理。这个策略既避免了仓库无限膨胀,也保留了足够的时间窗口回滚到前几天能用的镜像版本。
镜像是 CI/CD 体系里承上启下的关键产物。构建阶段的产出质量直接决定了发布阶段的安全性。
4. 多环境管理与发布策略落地
4.1 用 Namespace 隔离环境比拆集群更省事
测试、预发、生产环境怎么分离,是另一个绕不开的问题。一开始很多团队直接拆三个集群,每个环境一套独立的 K8s。这个方案隔离性强,但资源浪费很大,而且配置管理复杂。除非预算实在充足,没必要这么做。
在同一套 K8s 集群里用 Namespace 隔离,配合 ResourceQuota 和 LimitRange 做资源限制,就能满足绝大多数团队的隔离需求。测试环境的 Namespace 配额设为 4 核 8G,预发环境和生产环境独立专用节点池跑,这样既控制了成本,又能保证核心环境的稳定。
命名规范也很重要。我常用的规则是{应用名}-{环境名},例如service-a-test、service-a-prod。所有资源都要打上统一的标签:app.kubernetes.io/name: service-a、app.kubernetes.io/env: prod。标签的价值在排查问题时特别明显,一条命令就能看到某个应用在某环境下的所有 Pod、Service、PVC、HPA。
4.2 用 Kustomize 解决多环境配置差异
多环境部署的一个老大难是同一套配置在不同环境下的差异如何管理。Kustomize 和 Helm 是目前主流的两种选择。我当时选的是 Kustomize,因为它在 GitLab CI 里的集成简单,而且少学一套模板语法。
Kustomize 的核心在于 overlay。基础目录里保存一份通用的 Deployment、Service 配置,然后每个环境一个子目录,里面只写自己环境特有的配置,例如测试环境里的配置副本数设为 1、镜像 tag 用test-tag,生产环境副本数设为 3、tag 用正式版本号。部署时通过kustomize build把基础配置和 overlay 配置合并成最终的 YAML。
举个例子。基础配置里定义容器镜像为service-a:2.4.0,生产 overlay 里指定 pod 副本数是3,资源 requests 设为1Gi。合并后生成的 YAML 只会在生产环境里出现这些改动,测试环境完全不受影响。这个方式下环境相关的配置全部由代码仓库管理。一个 MR 改了生产环境的副本数,会被其他同事看到并评审。
4.3 滚动更新之外,还有蓝绿和灰度
部署发布策略这里,有三种常见的选择,各自的出发点不同:
滚动更新:修改 Deployment 里的 image 字段,K8s 自动启动新 Pod,等其中一个新 Pod 就绪后杀掉一个老 Pod,逐步替换。这种方式零停机,默认踩过大量坑之后我才意识到就绪探针配置得不对,滚动更新形同虚设。如果探针没有真正检查到服务是否健康,新 Pod 之间切换就不是“优雅的”,而是“粗暴的”。
蓝绿发布:新版本完整部署为一个独立的 Deployment 或 Service,验证通过后再把流量切换过去。实现上可以用 Service 的 selector 从一个 label 值切换到另一个 label 值。切换很快,回滚也很快,缺点是资源占用是双份的。
灰度发布:通过调整不同版本副本的比例来逐渐引流。可以用 nginx ingress 按权重切分流量,也可以用直接调整 Deployment 副本数的比例。灰度发布是这三种策略中最细粒度、最灵活的,对自动化要求也更高。
实际项目中,我认为这个框架的部署策略应该落在滚动更新为主、手工触发蓝绿发布的节奏上。常规版本自动滚动更新;大版本或风险较高的版本,先部署到预发环境做全要素验证,然后手工或半自动触发蓝绿切换。
4.4 一个可复制的完整 CI/CD 部署流程示例
结合上面提到的所有设计,我整理一个完整的示例流程,方便直接借鉴。假设项目是service-a,分支是main:
- 代码 push 到 main 分支,触发 CI。
- 流水线跑单元测试用例。
- 测试全部通过后,Kaniko 构建 Docker 镜像并推到 Harbor 仓库。
- 流水线进入部署阶段,GitLab Runner 调用
kubectl工具,执行kustomize build生成最终的测试环境 YAML 文件。 - 执行
kubectl apply,K8s 开始滚动更新测试环境的 Deployment。 - 验证测试环境健康后,流水线触发预发布环境的部署。
- 预发布环境验证通过后,由人工确认是否部署到生产环境,确认后执行相同步骤。
这套流程看起来简单,背后对权限和机制的要求特别高。生产环境的部署动作绝不能交给普通开发人员的个人账号去执行。我在 GitLab 侧配置了受保护的分支、受保护的 tag,只有指定维护者才能触发到生产环境的 job。在 K8s 侧通过 RBAC 限制,流水线用到的 service account 只有对指定 Namespace 的apply权限。
5. 常见问题与排错实录
5.1 Pod 启动崩溃循环排查思路
这套框架上线后,最常见的故障是 Pod 启动即崩溃,典型表现是CrashLoopBackOff。排查方向就一条命令:kubectl logs <pod-name> --previous,先看上一次退出时容器日志里有没有报错。很多问题都是这一个命令就能定位的。
如果日志里没有明显报错,转向看kubectl describe pod里的Events部分。常见的原因包括:镜像拉取失败、配置中心连不上、健康检查探针配置得太严导致容器被杀、依赖的数据库没迁移。
健康检查这块经常遇到一个反直觉的情况:探针配置的initialDelaySeconds太短,容器还在初始化时就被杀掉。K8s 认为它不健康,就不断重启。遇到这种问题,把初始化慢的服务对应的探针延迟时间调大,比如从默认的 10 秒调到 60 秒,问题就能解决。
5.2 镜像拉取失败和认证配置
第二个常见问题是ImagePullBackOff。原因通常是镜像不存在、tag 写错、或者仓库需要登录认证但 Pod 里没有配置。
对于私有仓库的认证,不要在 Pod 的 YAML 里写死账号密码,应该用imagePullSecret,把认证信息放到 K8s 的 Secret 资源中。给默认的 service account 绑定这个 imagePullSecret,这样所有 Pod 默认就有权拉取,不用每个 Deployment 单独配一段重复的 YAML。
5.3 HPA 自动扩缩容不生效的排查
HPA 是这套框架解决资源弹性问题的关键部件。有时候出现 HPA 已经建好了,但副本数不变或不断伸缩的情况,优先级最高的是确认 metrics-server 是否正常工作。
kubectl get --raw /apis/metrics.k8s.io/v1beta1这条命令如果返回空数组或者错误,说明指标采集链路有问题,HPA 自然无指标可用。另一个原因是资源配额导致扩不了容,Pod 数量受限,HPA 没法创建新副本。这两个问题在排查时优先级靠前。
5.4 流水线超时的幕后真凶
流水线阻塞也是一个高频问题。GitLab Runner 执行一个 job,如果长时间没有输出,默认 20 分钟会被 GitLab 强制中断。这个“长时间没有输出”其实是指标错误,因为构建过程中的网络等待、依赖下载都可能静默超过 5 分钟。
解决办法很直接:给 CI 任务设置更高的超时时间,把timeout = 1h写进 Runner 的配置。同时排查阶段之间的依赖关系,比如后续任务在等待前一个任务生成的镜像,可前一阶段的镜像推进太慢,超时后流水线整体失败。
5.5 问题排查速查表
| 现象 | 可能原因 | 优先排查方式 |
|---|---|---|
CrashLoopBackOff | 启动初始化失败 / 探针配置过严 | kubectl logs --previous、调整initialDelaySeconds |
ImagePullBackOff | tag 写错 / 仓库认证失败 | kubectl describe pod查看事件、配置imagePullSecret |
| HPA 不生效 | metrics-server 异常 / 资源配额不足 | kubectl get --raw /apis/metrics.k8s.io/v1beta1 |
| 流水线超时 | 网络等待 / Runner 超时设置过短 | 调大timeout、查看 Runner 日志 |
| 滚动更新卡住 | 新 Pod 未就绪 / 旧 Pod 未终止 | kubectl rollout status、检查 Pod 事件 |
6. 安全加固与运维效率优化
6.1 敏感信息是 CI/CD 体系里的薄弱环节
很多事故的根因不在代码,而在密钥泄露:数据库密码、云厂商访问密钥、对外服务的 token,只要任何一个被打进镜像或写进了部署清单,那就等于把安全责任全都托付给代码仓库的权限边界了。
我的处理方式分三层:
第一层,不要在代码仓库里提交任何形式的密钥,包括.env文件。GitLab 的受保护变量可以存敏感值,动态注入到流水线任务的环境变量里。
第二层,K8s 里的 Secret 资源不建议 Base64 存储明文。Base64 不是加密,只是编码,任何能访问get secret的人都能直接解出来。可以通过外部密钥管理组件(比如 Vault 或云厂商的密钥管理服务)做动态注入,让应用运行时从外部拉取密钥而不是把密钥内置在环境变量里。
第三层,RBAC 权限必须收敛。给开发者的权限只到他负责的 Namespace,不要上来就给整个集群的cluster-admin。我给 CI 用的 service account 只授予了deployments/apply、services/apply这些必要权限,没有给secrets/get。这就等于是说:就算 CI 被入侵了,它也拿不到集群里的密钥信息。
6.2 加速构建的常用手段
CI 跑得慢,第一个原因通常是依赖安装的重复。Java 项目每次构建都重新下 Maven 依赖,Node 项目重新npm install,这些操作非常消耗时间。利用 Kaniko 的缓存机制,可以缓存基础镜像层,依赖层的重新生成频率就会显著降低。
第二个是构建架构的选择。编译任务跑在容器里,如果项目是 Java,考虑用多阶段构建:第一个阶段跑完整的编译工具链,第二个阶段只包含运行时的 JRE。这样最终的镜像体积能小一半以上,部署时拉取更快,Pod 启动也更快。
第三个是任务分解。把一个大项目拆成独立的子模块,利用流水线的并行能力,同时构建多个不依赖彼此的模块,整体流水线的耗时能明显缩短。
6.3 这个框架在运维工作流中的长期价值
架构搭完之后,这套框架带来的长期收益会在后面逐渐显现。最直观的是:部署时间从一个小时压缩到三分钟,而且是全流程可审计的。
每次发布在 GitLab 流水线界面上留了完整的记录,谁在什么时候触发、镜像的 sha256 是什么、部署到哪个环境、部署的结果如何。出问题不再用拉聊天记录靠猜,直接看流水线历史就能定位。
让我觉得真正有意义的,是它改变了团队协作的方式。开发者可以自主部署测试环境,不再需要运维手动介入。预发布环境的部署和回滚由流水线控制,运维不背“为什么这次地址没及时切过来”的锅,问题又回到了代码本身,团队之间的互相抱怨少了。
有一次生产环境因为某个老版本配置不兼容导致启动异常,我在 Kibana 日志里看到异常后,直接在流水线历史里找到上一个正常版本,点击回滚按钮,两分钟内服务恢复。整个过程中我连 SSH 都没用。这就是 CI/CD 自动化部署框架的稳定价值。
从更长远的角度看,这个框架还能继续扩展:把部署后的集成测试接进去、在预发布环节自动生成性能报告、跟踪每个版本对应的线上配置基线。总之,先把流水线跑通,让团队适应流程,再从过程中间不断调整,这比追求一步到位式的完美设计要现实得多。