引言
当 Kubernetes 集群在承载生产业务时,「怎么把新版本的镜像送上集群」这个动作会被每天执行几十甚至上百次。手工 kubectl apply、scp 推镜像、甚至本地 docker build 再手工导入——这些方式在小团队阶段或许还能维持,一旦业务进入多服务、多环境、多人协作的阶段,就会立刻暴露出三类问题:发布不可追溯、环境不一致、变更风险不可控。
云原生 CI/CD 的核心目标,是把「代码提交 → 镜像构建 → 制品归档 → 部署到集群 → 健康校验 → 回滚」这一整条链路自动化、可复现、可观测。本篇聚焦两大主流方案——Jenkins 与 GitLab CI,结合 Kubernetes 的 Deployments、Helm、镜像仓库(Harbor / registry)与 kubectl,搭建一条从源码到生产集群的完整流水线,并给出多环境晋级、镜像签名、构建缓存、并发控制等实战要点。

云原生 CI/CD 的整体架构
一条面向 K8s 的生产流水线通常分为五个阶段,每个阶段都是一个可独立重试、可独立告警的原子步骤:
- Source(源码获取):拉取 Git 仓库,执行
git fetch与checkout,解析分支、Tag、Commit SHA。 - Build(构建与测试):编译代码、运行单元测试、静态扫描(如 SonarQube、Trivy 镜像漏洞扫描)。
- Package(镜像打包):
docker build或kaniko build(在 Pod 内无需 Docker-in-Docker),推送镜像到私有仓库。 - Deploy(部署集群):调用
kubectl apply或 Helm upgrade,将新镜像发布到目标命名空间。 - Verify(发布校验):等待 Deployment 滚动完成、检查 Pod 就绪状态、探针通过、错误率指标无异常,失败则自动回滚。
关键设计原则
- 不可变制品:镜像一经构建不再修改,环境间只切换 Tag,不重新构建。
- 环境晋级:dev → staging → production,逐级放行,降低直接推生产的风险。
- 配置与代码分离:镜像只携带二进制,环境差异(配置、密钥、副本数)由 ConfigMap、Secret、Helm values 或 Kustomize 提供。
- 最小权限:CI 使用的 kubeconfig 只授予特定 namespace 的部署权限,使用 ServiceAccount + RBAC 而非 cluster-admin。
- 可回滚:每次发布都保留历史版本与镜像 Tag,支持一键 rollback。
核心概念详解
CI 与 CD 的职责边界
CI(Continuous Integration)负责快速反馈:任何代码变更都应触发构建、测试与镜像打包,目标是把缺陷拦在合并前。CD(Continuous Deployment/Delivery)负责安全交付:把通过 CI 的制品部署到目标环境,并通过健康检查决定放行或回滚。
Jenkins 与 GitLab CI 的能力对比
| 维度 | Jenkins | GitLab CI/CD |
|---|---|---|
| 形态 | 独立服务器/Agent 架构 | 与 GitLab 深度集成 |
| 配置方式 | Jenkinsfile(Groovy 语法) | .gitlab-ci.yml(YAML) |
| 插件生态 | 极丰富,覆盖各类工具 | 官方 Runner + Container 内置 |
| 学习曲线 | 较陡,需了解 Pipeline DSL | 平缓,YAML 直观 |
| 适用场景 | 多语言、多仓库、复杂编排 | 单体仓库、DevOps 一体化 |
| 与 K8s 集成 | Kubernetes 插件,Pod 模板调度 | K8s Runner,Executor 原生支持 |
两者并非二选一,实际生产常并存:GitLab CI 跑轻量构建与测试,Jenkins 承担复杂的多环境编排、人工审批与灰度发布。
Kubernetes 中的 CI/CD 运行位置
流水线本身可以运行在三种位置:物理机、虚拟机、K8s 集群。推荐把 Runner 调度到 K8s 集群内(Jenkins Agent Plugin 或 GitLab K8s Executor),原因有三:弹性伸缩(构建并发高峰时自动扩容 Pod)、隔离(每次构建一个独立 Pod,避免污染)、复用(Runner 集群与业务集群共享节点池,节省成本)。
镜像仓库与制品管理
私有仓库(Harbor、Nexus、Docker Registry)承担两个职责:存储构建产物、提供漏洞扫描与镜像签名。生产镜像建议带三重标识:v1.2.3(语义化版本)、sha256:<digest>(内容寻址)、commit-<git_sha>(源码关联),三者组合保证「知道镜像来自哪、知道哪份代码、知道是否被篡改」。
实战步骤
步骤一:在 K8s 集群中部署 Jenkins
以官方 Helm Chart 为例,在集群内创建 Jenkins,并配置 Docker-in-Docker(或 Kaniko)构建能力。
# 创建 Jenkins 专用命名空间与 ServiceAccount
kubectl create ns ci-cd
kubectl create sa jenkins -n ci-cd
# 绑定只读集群权限 + ci-cd 命名空间全量权限
kubectl create clusterrolebinding jenkins-crb
--clusterrole=edit --serviceaccount=ci-cd:jenkins
# jenkins-values.yaml(Helm values 覆盖)
controller:
JENKINS_OPTS: "-Dhudson.slaves.NodeProvisioner.MS_TO_SLEEP=15000"
installPlugins:
- kubernetes:4277.vc01f084b_1b_7a
- workflow-aggregator:573.v6e0b_b_9d7a_7b_e
- docker-workflow:570.vb_95809007f99
- config-file-provider:948.v144671d02583
agent:
pullSecret: harbor-pullsecret
persistence:
enabled: true
size: 20Gi
# 安装 Jenkins
helm repo add jenkins https://charts.jenkins.io
helm repo update
helm install jenkins jenkins/jenkins
-n ci-cd -f jenkins-values.yaml --wait --timeout 10m
步骤二:配置 Jenkinsfile 与 Kubernetes Agent
Jenkinsfile 使用 agent { k8s } 语法,把每次构建调度为一个临时 Pod,容器内自带 docker、kubectl、helm、kaniko 等工具。
pipeline {
agent {
k8s {
yaml '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: jnlp
image: jenkins/inbound-agent:latest
args: ['$(JENKINS_URL)', '${computer.jnlpmacro}']
- name: docker
image: docker:dind
env:
- name: DOCKER_TLS_CERTDIR
value: /certs
volumeMounts:
- name: dockersock
mountPath: /certs/client
- name: kubectl
image: bitnami/kubectl:1.29
env:
- name: KUBECONFIG
value: /workspace/.kube/config
volumeMounts:
- name: kubeconfig
mountPath: /workspace/.kube
- name: harbor
image: registry.gitlab.com/gitlab-org/release-tools:latest
env:
- name: REGISTRY_USER
valueFrom: {secretKeyRef: {name: harbor, key: user}}
- name: REGISTRY_PASS
valueFrom: {secretKeyRef: {name: harbor, key: pass}}
volumes:
- name: dockersock
secret: {secretName: docker-tls}
- name: kubeconfig
secret: {secretName: k8s-deploy}
'''
}
}
options {
timeout(time: 30, unit: 'MINUTES')
timestamps()
disableConcurrentBuilds()
}
stages {
stage('Checkout') {
steps {
checkout scm
sh 'echo "Building commit ${GIT_COMMIT}"'
}
}
stage('Test') {
steps {
sh './run-tests.sh'
}
}
stage('Build & Push') {
steps {
sh '''
VERSION=$(date +%Y%m%d-%H%M%S)
docker build -t harbor.stellardata.top/app/myapp:${VERSION} .
docker tag harbor.stellardata.top/app/myapp:${VERSION} harbor.stellardata.top/app/myapp:latest
docker push harbor.stellardata.top/app/myapp:${VERSION}
docker push harbor.stellardata.top/app/myapp:latest
echo ${VERSION} > .build_version
'''
}
}
stage('Deploy Staging') {
steps {
sh '''
VERSION=$(cat .build_version)
kubectl -n staging set image deployment/myapp
myapp=harbor.stellardata.top/app/myapp:${VERSION}
kubectl -n staging rollout status deployment/myapp --timeout=300s
'''
}
}
stage('Deploy Production') {
input message: '确认发布到生产环境?', submitter: 'release-approvers'
steps {
sh '''
VERSION=$(cat .build_version)
kubectl -n production set image deployment/myapp
myapp=harbor.stellardata.top/app/myapp:${VERSION}
kubectl -n production rollout status deployment/myapp --timeout=300s
'''
}
}
stage('Verify') {
steps {
sh 'curl -f https://myapp.stellardata.top/healthz || kubectl -n production rollout undo deployment/myapp'
}
}
}
}
步骤三:使用 GitLab CI 实现等价流水线
GitLab CI 通过 .gitlab-ci.yml 定义流水线,Runner 使用 kubernetes executor 调度到 K8s 集群。
# .gitlab-ci.yml
stages:
- test
- build
- deploy-staging
- deploy-prod
image: docker:24-dind
services:
- docker:24-dind
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: "/certs"
REGISTRY: harbor.stellardata.top/app
test-job:
stage: test
script:
- ./run-tests.sh
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
- if: '$CI_COMMIT_TAG'
build-job:
stage: build
script:
- docker login -u "$REG_USER" -p "$REG_PASS" $REGISTRY
- docker build -t $REGISTRY/myapp:$CI_COMMIT_SHA .
- docker build -t $REGISTRY/myapp:latest .
- docker push $REGISTRY/myapp:$CI_COMMIT_SHA
- docker push $REGISTRY/myapp:latest
only:
- main
- tags
artifacts:
reports:
dotnet: test-results.xml
deploy-staging:
stage: deploy-staging
image: bitnami/kubectl:1.29
script:
- kubectl -n staging set image deployment/myapp myapp=$REGISTRY/myapp:$CI_COMMIT_SHA
- kubectl -n staging rollout status deployment/myapp --timeout=300s
- kubectl -n staging get pods -l app=myapp
environment:
name: staging
url: https://staging.myapp.stellardata.top
only:
- main
deploy-prod:
stage: deploy-prod
image: bitnami/kubectl:1.29
script:
- kubectl -n production set image deployment/myapp myapp=$REGISTRY/myapp:$CI_COMMIT_SHA
- kubectl -n production rollout status deployment/myapp --timeout=300s
environment:
name: production
when: manual
only:
- tags
步骤四:用 Helm 替代 kubectl,实现参数化部署
当应用复杂、参数多变时,直接 kubectl set image 不够灵活。推荐把部署模板化:
# 使用 Helm upgrade 部署新镜像
helm upgrade --install myapp ./chart/myapp
-n production
--set image.repository=harbor.stellardata.top/app/myapp
--set image.tag=$CI_COMMIT_SHA
--set replicas=3
--set resources.requests.cpu=500m
--set resources.limits.memory=1Gi
--set ingress.host=myapp.stellardata.top
--wait --atomic --timeout 5m
{
"version": "1.0.0",
"build": {
"image": "harbor.stellardata.top/app/myapp:abcdef1234567",
"dockerfile": "Dockerfile",
"context": ".",
"cache_from": ["harbor.stellardata.top/app/myapp:latest"]
},
"deploy": {
"strategy": "rolling",
"replicas": 3,
"maxUnavailable": 0,
"maxSurge": 1
},
"rollback_on_fail": true,
"verify": {
"endpoint": "https://myapp.stellardata.top/healthz",
"timeout": 300,
"interval": 10
}
}
步骤五:构建缓存与镜像瘦身优化
构建速度直接影响流水线反馈效率,推荐三个优化点:
# Dockerfile 使用多阶段构建 + 缓存友好排序
FROM golang:1.22 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /out/app ./cmd
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /out/app /app
USER 65532:65532
EXPOSE 8080
ENTRYPOINT ["/app"]
- 依赖层前置:把
go.mod/requirements.txt/package.json单独 COPY,使依赖变更才触发重新下载。 - 多阶段构建:构建镜像与运行镜像分离,运行镜像仅包含二进制与运行时依赖,最终镜像控制在 20MB 以内。
- Registry 层缓存:使用
--cache-from复用上次构建的层,Harbor 可开启缓存策略。
常见问题 FAQ
Q1:Jenkins Agent 调度失败,Pod 一直 Pending 怎么办?
先看 kubectl describe pod -n ci-cd 中的 Events,常见原因是资源不足(request 过大)、污点未容忍(Runner 节点打了 dedicated=ci 污点)、镜像拉取失败(Harbor 凭据 Secret 未挂载)。排查三件套:节点 Allocatable 资源、ServiceAccount 绑定关系、imagePullSecret 是否存在于命名空间。
Q2:CI 里 kubectl 一直报 forbidden,如何最小授权?
避免给 Runner 使用 cluster-admin。正确做法是在集群内创建专用 ServiceAccount,绑定只授予目标 namespace 的 RBAC 权限:
kubectl create role deploy-role -n production --verbs=get,list,watch,patch,update,create,delete
--resources=pods,services,deployments,replicasets,configmaps,secrets
kubectl create rolebinding deploy-rb -n production
--role=deploy-role --serviceaccount=ci-cd:jenkins
Q3:多环境如何保证不串号、不推错?
强制使用 environment 关键字绑定:Jenkins 用 input 步骤 + 审批人组;GitLab 用 environment.name 并开启 Approval Rules。同时把生产部署限定在 tags 或 when: manual 触发,主干分支只到 staging,生产必须显式确认。
Q4:构建并发太多把 Runner 集群打挂怎么办?
Jenkins 侧配置 agent 数量上限与 concurrentBuilds 限制;GitLab 侧在 .gitlab-ci.yml 加 resource_group 串行化同项目,或在 Runner 配置 limit: N。同时给 Runner Pod 设置合理的 request/limit,避免抢占业务节点资源。
Q5:如何验证发布是否真的成功?
不要只依赖 rollout status。三层校验:应用层(HTTP healthz 端点返回 200)、指标层(Prometheus 错误率、延迟未上升)、日志层(新 Pod 无 FATAL/ERROR 激增)。失败时流水线自动 kubectl rollout undo 回滚,并触发告警通知。
总结
云原生 CI/CD 是把 Kubernetes 集群「从手工操作」推向「自动化治理」的关键一环。本篇给出 Jenkins 与 GitLab CI 两套完整方案,覆盖从 Pod 化 Runner、K8s 权限、镜像构建、Helm 部署到健康校验的全链路,并附带构建缓存与最小权限两个常被忽略的优化点。
实践要点:Jenkins 灵活强大、适合复杂编排与多环境审批;GitLab CI 轻量直观、适合 DevOps 一体化仓库。两者可并存,按团队习惯与技术栈选型。核心思路是「不可变制品 + 环境晋级 + 最小权限 + 自动回滚」,把这四条做好,就能在 K8s 集群上稳定运行每天上百次的安全发布。
下期预告
K8s 运维系列 | 第 27 天:GitOps 实践——Argo CD 声明式部署


















暂无评论内容