运维实战

Jenkins on Kubernetes CI/CD:临时 Agent、镜像构建、自动发布与安全回滚

使用 Jenkins Kubernetes 插件与 rootless BuildKit 构建可追溯镜像,通过最小 RBAC 自动部署、验收并在失败时安全回滚。

TY
Tycho
技术博主
• 2026-09-24 • 47 分钟阅读 • 3 次浏览
Jenkins on Kubernetes CI/CD:临时 Agent、镜像构建、自动发布与安全回滚

本文在 Kubernetes 内部署 Jenkins Controller,并由 Kubernetes 插件为每次构建创建一次性 Agent Pod。流水线完成代码检出、测试、rootless BuildKit 构建镜像、推送仓库、更新 Deployment、健康验证和失败回滚。所有关键凭据都通过 Jenkins Credentials 或 Kubernetes Secret 注入,不写进 Jenkinsfile。

一、最终架构与安全边界

Git Webhook -> Jenkins Controller(PVC, 无构建负载)
                    |
                    +-> Ephemeral Agent Pod
                        |- test container
                        |- rootless BuildKit container -> Image Registry
                        `- kubectl container -> staging namespace
                                                   |- Deployment rollout
                                                   `- Service smoke test
  • Controller 只负责编排与保存配置,构建在临时 Pod 中执行。
  • 不用宿主机 Docker Socket,避免构建任务获得节点 root 等价权限。
  • 部署 ServiceAccount 只允许操作目标 namespace 中约定的 Deployment、Service 与 Pod 日志。
  • 生产发布与测试发布使用不同凭据;生产阶段增加人工批准与变更窗口。

二、准备命名空间、存储与 Helm 版本

kubectl create namespace jenkins --dry-run=client -o yaml | kubectl apply -f -
kubectl create namespace staging --dry-run=client -o yaml | kubectl apply -f -
helm repo add jenkinsci https://charts.jenkins.io
helm repo update
helm search repo jenkinsci/jenkins -l | head
export JENKINS_CHART_VERSION='<经验证的版本>'
kubectl get storageclass

先确认 Jenkins chart 对应的 Jenkins LTS 与插件最低版本。插件版本需要与 Controller 核心版本兼容,不要在生产启动时自动拉取未审核的最新插件。

三、部署 Jenkins Controller

3.1 最小可运营 values

controller:
  image:
    tag: "<固定的-jenkins-lts-jdk21-镜像标签>"
  installPlugins:
    - kubernetes:<固定版本>
    - workflow-aggregator:<固定版本>
    - git:<固定版本>
    - credentials-binding:<固定版本>
    - configuration-as-code:<固定版本>
  resources:
    requests: {cpu: 500m, memory: 2Gi}
    limits: {cpu: "2", memory: 4Gi}
  serviceType: ClusterIP
  ingress:
    enabled: false
  JCasC:
    defaultConfig: true
  probes:
    startupProbe:
      failureThreshold: 30
      periodSeconds: 10
persistence:
  enabled: true
  storageClass: fast-rwo
  size: 50Gi
serviceAccount:
  create: true
  name: jenkins
agent:
  enabled: true
  podRetention: never
  idleMinutes: 0
  workspaceVolume:
    emptyDirWorkspaceVolume:
      memory: false
helm upgrade --install jenkins jenkinsci/jenkins \
  -n jenkins --version "$JENKINS_CHART_VERSION" \
  -f jenkins-values.yaml --atomic --timeout 20m
kubectl -n jenkins get pods,pvc,svc
kubectl -n jenkins rollout status statefulset/jenkins --timeout=10m
kubectl -n jenkins port-forward svc/jenkins 8080:8080

正式环境应通过 TLS Ingress 和企业 SSO 访问,限制管理入口来源,并把 JCasC、插件锁定清单和 job 定义纳入备份。JENKINS_HOME 的 PVC 不是唯一备份:还要导出凭据加密所需的 secrets、插件清单和恢复步骤。

四、创建最小权限部署身份

apiVersion: v1
kind: ServiceAccount
metadata:
  name: jenkins-deployer
  namespace: staging
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: jenkins-deployer
  namespace: staging
rules:
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "list", "watch", "patch", "update"]
  - apiGroups: [""]
    resources: ["pods", "pods/log", "services", "endpoints"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: jenkins-deployer
  namespace: staging
subjects:
  - kind: ServiceAccount
    name: jenkins-deployer
    namespace: staging
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: jenkins-deployer
kubectl apply -f staging-jenkins-rbac.yaml
kubectl auth can-i patch deployments -n staging \
  --as system:serviceaccount:staging:jenkins-deployer
kubectl auth can-i delete secrets -n staging \
  --as system:serviceaccount:staging:jenkins-deployer
# 第二条必须输出 no

Agent Pod 若运行在 jenkins namespace,不能直接挂载另一个 namespace 的 ServiceAccount。推荐由 Jenkins Kubernetes Credentials Provider/短期 kubeconfig 或受控部署 Job 取得 staging 身份;无论采用哪种方式,都先用 kubectl auth can-i 验证允许和拒绝两组权限。

五、准备仓库凭据、镜像仓库与部署清单

  1. 在 Jenkins Credentials 中创建 Git 只读凭据,ID 为 git-readonly。
  2. 创建镜像仓库机器人账号,ID 为 registry-robot,仅允许推送项目路径。
  3. 创建 staging kubeconfig Secret Text,ID 为 staging-kubeconfig,只绑定前述最小权限身份。
  4. 在目标 namespace 创建只读 imagePullSecret,Deployment 只负责拉取。
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo-api
  namespace: staging
spec:
  replicas: 3
  strategy:
    rollingUpdate: {maxUnavailable: 0, maxSurge: 1}
  selector:
    matchLabels: {app: demo-api}
  template:
    metadata:
      labels: {app: demo-api}
    spec:
      imagePullSecrets: [{name: registry-pull}]
      containers:
        - name: app
          image: registry.example.com/team/demo-api:bootstrap
          ports: [{name: http, containerPort: 8080}]
          readinessProbe:
            httpGet: {path: /ready, port: http}
            periodSeconds: 5
          livenessProbe:
            httpGet: {path: /live, port: http}
            initialDelaySeconds: 20
          resources:
            requests: {cpu: 100m, memory: 128Mi}
            limits: {cpu: "1", memory: 512Mi}

六、编写可审计 Jenkinsfile

pipeline {
  agent {
    kubernetes {
      retries 2
      yaml '''
apiVersion: v1
kind: Pod
spec:
  securityContext:
    runAsNonRoot: true
  containers:
    - name: test
      image: maven:3.9.9-eclipse-temurin-21
      command: ["cat"]
      tty: true
      resources:
        requests: {cpu: 500m, memory: 1Gi}
    - name: buildkit
      image: moby/buildkit:rootless
      command: ["sh", "-c", "sleep 99d"]
      securityContext:
        runAsUser: 1000
        runAsGroup: 1000
        allowPrivilegeEscalation: false
      resources:
        requests: {cpu: "1", memory: 2Gi}
    - name: kubectl
      image: bitnami/kubectl:<固定版本>
      command: ["sh", "-c", "sleep 99d"]
      securityContext:
        runAsNonRoot: true
'''
    }
  }
  options {
    timestamps()
    disableConcurrentBuilds(abortPrevious: false)
    timeout(time: 30, unit: 'MINUTES')
  }
  environment {
    REGISTRY = 'registry.example.com'
    IMAGE_REPO = 'team/demo-api'
  }
  stages {
    stage('Checkout') {
      steps { checkout scm }
    }
    stage('Test') {
      steps {
        container('test') { sh 'mvn -B clean verify' }
      }
      post { always { junit allowEmptyResults: false, testResults: '**/surefire-reports/*.xml' } }
    }
    stage('Build and Push') {
      steps {
        container('buildkit') {
          withCredentials([usernamePassword(credentialsId: 'registry-robot', usernameVariable: 'REG_USER', passwordVariable: 'REG_PASS')]) {
            sh '''#!/bin/sh
set -eu
mkdir -p "$HOME/.docker"
AUTH=$(printf '%s:%s' "$REG_USER" "$REG_PASS" | base64 | tr -d '\n')
printf '{"auths":{"%s":{"auth":"%s"}}}' "$REGISTRY" "$AUTH" > "$HOME/.docker/config.json"
export IMAGE_TAG="${GIT_COMMIT}-${BUILD_NUMBER}"
buildctl-daemonless.sh build \
  --frontend dockerfile.v0 \
  --local context=. --local dockerfile=. \
  --output "type=image,name=${REGISTRY}/${IMAGE_REPO}:${IMAGE_TAG},push=true"
rm -f "$HOME/.docker/config.json"
printf '%s' "$IMAGE_TAG" > image-tag.txt
'''
          }
        }
      }
    }
    stage('Deploy staging') {
      steps {
        container('kubectl') {
          withCredentials([file(credentialsId: 'staging-kubeconfig', variable: 'KUBECONFIG')]) {
            sh '''#!/bin/sh
set -eu
IMAGE_TAG=$(cat image-tag.txt)
kubectl -n staging set image deployment/demo-api \
  app="${REGISTRY}/${IMAGE_REPO}:${IMAGE_TAG}"
kubectl -n staging rollout status deployment/demo-api --timeout=5m
kubectl -n staging get deployment demo-api -o jsonpath='{.status.updatedReplicas}/{.status.availableReplicas}{"\n"}'
'''
          }
        }
      }
    }
  }
  post {
    unsuccessful {
      container('kubectl') {
        withCredentials([file(credentialsId: 'staging-kubeconfig', variable: 'KUBECONFIG')]) {
          sh 'kubectl -n staging rollout undo deployment/demo-api || true'
        }
      }
    }
    always { deleteDir() }
  }
}

示例把镜像标签绑定到 Git SHA 和构建号,确保可追溯且不覆盖旧镜像。真正生产流水线还应生成 SBOM、执行依赖/镜像扫描、签名镜像,并用镜像 digest 部署。rollout undo 只是紧急兜底:数据库迁移若不向后兼容,回滚旧镜像仍可能失败。

七、验证、门禁与回滚演练

kubectl -n staging rollout status deployment/demo-api --timeout=5m
kubectl -n staging get rs,pods -l app=demo-api -o wide
kubectl -n staging describe deployment demo-api
kubectl -n staging logs deployment/demo-api --tail=100
kubectl -n staging rollout history deployment/demo-api

# 集群内冒烟测试
kubectl -n staging run smoke --rm -i --restart=Never \
  --image=curlimages/curl:<固定版本> -- \
  curl -fsS --retry 10 --retry-delay 2 http://demo-api/ready

成功标准包括:测试报告已归档、镜像 digest 可查询、Deployment 三个副本全部 Available、readiness 通过、冒烟测试返回预期内容、监控无新增错误率。任何一项失败都不能用“构建绿色”替代。

八、生产优化与常见故障

问题证据常见原因修复
Agent PendingPod Events资源、污点、配额设置 requests/limits 与专用节点策略
构建推送 401BuildKit 日志凭据范围或 registry 地址错误核对机器人权限与 auth 文件
rollout 超时Deployment/Pod Events镜像拉取、探针、资源不足先修新 ReplicaSet,再决定回滚
偶发并发覆盖流水线时间线同环境并行发布disableConcurrentBuilds 或环境锁
Controller 卡顿队列、GC、CPU构建跑在 controller迁移到临时 Agent 并限制队列
  • Controller、Agent 和部署权限分离;不同团队使用不同 Jenkins folder 与 cloud 限制。
  • 所有镜像和插件固定版本,建立定期升级窗口,不在启动时漂移。
  • 对队列长度、构建耗时、失败率、Agent 启动耗时、Controller PVC 与 JVM 建立监控。
  • 发布采用向后兼容数据库迁移:先扩展 schema,再发布新代码,最后清理旧字段。
  • 生产阶段使用人工批准、变更单和 digest 白名单;紧急回滚定期演练。

九、验收清单

  • Webhook 触发一次构建,Agent Pod 自动创建并在结束后删除。
  • 测试失败不会构建或部署;凭据不会出现在控制台日志和工作区归档。
  • 镜像以不可变标签或 digest 推送,部署记录可关联 Git commit、构建号与审批人。
  • staging ServiceAccount 的越权检查返回 no,生产权限与 staging 完全分离。
  • 探针失败会阻止 rollout;回滚演练恢复到上一版本并通过冒烟测试。
  • JENKINS_HOME、JCasC、插件锁定清单和 credentials secrets 完成恢复演练。

十、总结

Kubernetes 上的 Jenkins 不是把传统构建机搬进 Pod,而是让控制器保持轻量、让构建环境一次一建、让权限按发布目标收敛,并让每次部署都有可验证和可回滚证据。流水线只有同时满足可复现、最小权限、不可变制品和自动验收,才具备生产价值。

十一、官方资料

TY

Tycho

热爱分享技术知识,帮助开发者成长。

评论 (0)

评论功能当前已关闭
暂无评论,快来抢沙发吧!