K8s 运维系列 | 第 26 天:云原生 CI/CD——Jenkins 与 GitLab 镜像构建发布

引言

当 Kubernetes 集群在承载生产业务时,「怎么把新版本的镜像送上集群」这个动作会被每天执行几十甚至上百次。手工 kubectl apply、scp 推镜像、甚至本地 docker build 再手工导入——这些方式在小团队阶段或许还能维持,一旦业务进入多服务、多环境、多人协作的阶段,就会立刻暴露出三类问题:发布不可追溯、环境不一致、变更风险不可控。

云原生 CI/CD 的核心目标,是把「代码提交 → 镜像构建 → 制品归档 → 部署到集群 → 健康校验 → 回滚」这一整条链路自动化、可复现、可观测。本篇聚焦两大主流方案——Jenkins 与 GitLab CI,结合 Kubernetes 的 Deployments、Helm、镜像仓库(Harbor / registry)与 kubectl,搭建一条从源码到生产集群的完整流水线,并给出多环境晋级、镜像签名、构建缓存、并发控制等实战要点。

K8s 运维 第26天

云原生 CI/CD 的整体架构

一条面向 K8s 的生产流水线通常分为五个阶段,每个阶段都是一个可独立重试、可独立告警的原子步骤:

  1. Source(源码获取):拉取 Git 仓库,执行 git fetchcheckout,解析分支、Tag、Commit SHA。
  2. Build(构建与测试):编译代码、运行单元测试、静态扫描(如 SonarQube、Trivy 镜像漏洞扫描)。
  3. Package(镜像打包)docker buildkaniko build(在 Pod 内无需 Docker-in-Docker),推送镜像到私有仓库。
  4. Deploy(部署集群):调用 kubectl apply 或 Helm upgrade,将新镜像发布到目标命名空间。
  5. 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。同时把生产部署限定在 tagswhen: manual 触发,主干分支只到 staging,生产必须显式确认。

Q4:构建并发太多把 Runner 集群打挂怎么办?

Jenkins 侧配置 agent 数量上限与 concurrentBuilds 限制;GitLab 侧在 .gitlab-ci.ymlresource_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 声明式部署

微信二维码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片快捷回复

    暂无评论内容