Skip to content
Charles Shao
Go back

CI/CD 流水线 · Jenkins 构建镜像与自动部署到 K8s

views

前三篇我们把地基铺平了:容器是什么K8s 靠哪些对象编排怎么调度与弹性伸缩。但有一个问题一直没答:代码从开发者按下 git push,到真正跑在生产集群的 Pod 里,中间那一长串”编译、测试、打镜像、推仓库、部署、验证、放量”是谁在做、怎么做到又快又安全? 答案就是 CI/CD 流水线——这一篇专门深挖它,并以 Jenkins 为主线。

广告系统对发布这件事尤其苛刻:一天可能发几十次、镜像要小到能秒级拉取、出了问题要能秒回滚(收入太敏感)、流水线本身还得快(反馈环一长,工程效率就塌)。这些约束会把”能自动部署就行”的朴素流水线逼成一套需要精心设计的工程系统。我们就从概念辨析开始,一路搭到一条能扛住广告场景的生产级流水线。

本文是容器化与 K8s 部署系列的第 4 篇。 全系列 5 篇:

  1. 容器化(开篇)· Docker 原理与镜像分层
  2. Kubernetes 核心对象 · Pod/Deployment/Service/Ingress
  3. K8s 调度、资源与弹性伸缩
  4. CI/CD 流水线 · Jenkins 构建镜像与自动部署到 K8s(本篇)
  5. 广告服务上云实战 · 容器化部署与调优

一句话定位:CI/CD 是把”改一行代码到它安全上线”这条路自动化、标准化、可回退——CI 保证每次提交都能编译+测试通过,CD 把通过的产物做成不可变镜像、可靠地送到生产;Jenkins 用 Controller 编排 + 动态 Agent 构建,把这条路用 Jenkinsfile 写成代码。

TL;DR

Table of contents

Open Table of contents

1. CI / CD / 持续部署:先把三个概念掰开

这三个词天天混着说,但它们是递进的三层,边界很清楚:

概念全称做的事边界
CIContinuous Integration(持续集成)频繁把代码合进主干,每次提交都自动编译 + 跑测试,尽早发现集成冲突和回归产物停在”通过测试的构建”,不管发布
CDContinuous Delivery(持续交付)在 CI 之上,把通过的构建做成随时可发布的制品(镜像),一键就能上生产——但上生产前留一道人工审批能发,但”按不按发布键”由人决定
持续部署Continuous Deployment更进一步:通过所有自动关卡就直接上生产,无需人工点按钮全自动,从提交到生产无人工干预

一句话记忆链:CI 保证”代码是好的”→ 持续交付保证”随时能发”→ 持续部署保证”自动就发了”。两个 CD 常被混用,关键差别就是生产发布这一步有没有人工闸门:留了闸门是 Delivery,闸门也自动化了是 Deployment。

广告系统里,多数团队落在持续交付:CI 全自动、预发环境自动部署,但生产发布带一道 input 审批 + 金丝雀——因为竞价服务一旦发错就是真金白银的损失,愿意用一点人工确认换取安全边界。等流水线的自动化质量门(指标门禁、自动回滚)足够可信,才逐步向持续部署靠拢。

2. Jenkins 架构:Controller + Agent 的分布式构建

Jenkins 是老牌但依然主流的自动化服务器,核心是一套 Controller(旧称 master)+ Agent(node) 的分布式模型。

Jenkins 分布式构建架构图。左侧蓝色面板为 Jenkins Controller(master · 大脑,不干重活),内部纵向堆四个职责芯片:解析 Jenkinsfile / 编排 stages、调度构建并把任务分发给 Agent、Web UI / 凭据(credentials)管理、插件生态 / 多分支流水线 / 共享库,下方小字说明 Controller 只做编排与调度、真正的编译构建甩给 Agent。右侧绿色面板为 Kubernetes 集群(kubernetes plugin 动态 Agent),顶部一条绿色芯片写"kubernetes plugin:每次构建按需拉起 Agent Pod",下面并排两个临时 Agent Pod(Agent Pod #1、#2),每个 Pod 内含 jnlp 容器(连回 Controller)与 build 容器(maven/kaniko),再下方一条红色芯片写"构建结束 → Agent Pod 立即销毁(不常驻、不占固定 executor)"。Controller 与集群之间有两条箭头:绿色实线标注"申请 Agent",灰色虚线标注"回传结果"。底部琥珀色注释说明分布式构建的意义与用 K8s 动态 Agent 的好处。

Jenkins 架构:Controller 只负责编排/调度/UI/凭据,Agent 干真正的编译打镜像;用 kubernetes plugin 让每次构建动态起一个临时 Agent Pod、构建完销毁。

2.1 Controller 与 Agent 各干什么

2.2 为什么要分布式构建

如果所有构建都挤在 Controller 一台机器上:并发一高就排队(executor 不够)、环境互相污染(这个构建装的依赖影响那个)、Controller 一挂全线瘫痪。分布式构建把”编排”和”执行”分开:

2.3 用 K8s 动态 Agent Pod(kubernetes plugin)

固定的 Agent 机器有老毛病:平时闲置烧钱、构建高峰又不够用、跑久了环境越来越脏。Jenkins 的 kubernetes plugin 把 Agent 做成 K8s 里的动态 Pod:

这正是”把 CI 也云原生化”——构建资源和业务 Pod 一样即用即还,尤其适合广告这种构建高峰明显(合版本、大促前密集发布)的场景。

3. Pipeline as Code:声明式 Jenkinsfile

现代 Jenkins 的核心实践是 Pipeline as Code:把流水线定义写成一个 Jenkinsfile跟着代码一起提交进仓库、一起做 code review、一起版本化——而不是在 Jenkins UI 上点点点配出来(点出来的配置没法 review、没法回溯、换个 job 就得重配)。

3.1 声明式流水线的骨架

推荐用声明式(declarative) 语法,结构清晰、约束性强:

pipeline {
  agent { kubernetes { ... } }          // 在哪跑:用 K8s 动态 Agent Pod
  stages {
    stage('Build') {                    // 一个阶段
      steps { sh 'mvn -B clean package' }   // 阶段里的具体步骤
    }
    stage('Test') {
      steps { sh 'mvn -B test' }
    }
  }
  post {                                // 收尾:无论成败都跑
    success { echo '流水线成功' }
    failure { echo '失败 → 触发告警/回滚' }
    always  { junit '**/target/surefire-reports/*.xml' }  // 归档测试报告
  }
}

3.2 多分支流水线与共享库

3.3 凭据管理:密钥绝不进脚本

流水线要用一堆敏感信息:镜像仓库账号、K8s 集群 kubeconfig、云厂商 API key。这些绝不能明文写进 Jenkinsfile(进了仓库 = 泄密)。Jenkins 的 credentials 机制统一托管这些密钥,流水线里用 ID 引用、运行时注入为环境变量或临时文件:

steps {
  withCredentials([usernamePassword(
      credentialsId: 'registry-cred',      // 只引用 ID,密钥不出现在脚本里
      usernameVariable: 'REG_USER',
      passwordVariable: 'REG_PASS')]) {
    sh 'echo "$REG_PASS" | docker login -u "$REG_USER" --password-stdin registry.example.com'
  }
}

原则:代码库里只有”用哪个凭据”(ID),没有”凭据是什么”(值)。密钥集中在 credentials(或对接 Vault/KMS)里管理、可轮换、可审计。这条纪律和镜像最佳实践里”别把密钥 COPY 进镜像” 是一脉相承的。

4. 一条广告服务的完整流水线

把前面的零件拼起来,看一条真实的广告服务流水线长什么样——从开发者 push 代码,到金丝雀放量。

广告服务 CI/CD 流水线全景图。顶部一个灰色芯片"git push / PR → webhook 触发"引出整条流水线。紫色大面板"声明式 Jenkinsfile · stages(多分支流水线自动触发)"内含两行阶段芯片:第一行四个(① 拉代码 checkout scm、② 编译+单测 mvn verify、③ 构建镜像 多阶段·kaniko、④ 推镜像仓库 tag=git sha)用紫色箭头顺序相连;一条绿色折线标注"镜像就绪 → 部署"从第一行末尾绕回第二行开头;第二行三个阶段芯片(⑤ 部署 K8s set image/Helm、⑥ 冒烟+健康检查 readiness/smoke、⑦ 金丝雀放量 按权重分流)用绿色箭头相连;⑤ 上方有一个琥珀色药丸"input 审批门禁";右下角一个红色芯片"post { failure } 自动回滚上一版",一条红色虚线从⑥/⑦ 折回,标注"指标不达标 / 冒烟失败"。底部琥珀色注释三条:镜像 tag 用 git sha 别用 latest、input 门禁与 post failure 自动回滚、构建/部署/放量分别呼应 Docker、K8s、发布安全与 Service Mesh。

一条广告服务流水线:git push 触发 → 拉代码/编译测试/构建推镜像 → 部署到 K8s → 冒烟健康检查 → 金丝雀放量;配 input 审批门禁与 post failure 自动回滚。

4.1 逐个阶段拆解

  1. 拉代码(checkout):多分支流水线被 webhook 触发,checkout scm 拉对应分支/commit 的代码。
  2. 编译 + 单测(mvn verify):编译打包 + 跑单元/集成测试。这一步失败就该立刻断流水线——不让坏代码往下走,这是 CI 的核心价值。
  3. 构建镜像:用多阶段构建打出瘦镜像(build 阶段编译、runtime 阶段只留产物)。在 K8s 里没有 Docker daemon,常用 kanikobuildkit 这类无 daemon 构建工具在 Agent Pod 里打镜像。
  4. 推镜像仓库(打不可变 tag):把镜像推到镜像仓库,tag 必须用 git sha 或语义版本(如 v1.8.3),绝不用 latest——这样每个镜像和一次确定的提交一一对应,能追溯、能精确回滚(这条是 §7 war story 的核心)。
  5. 部署到 K8s:更新 Deployment 的镜像。最简单是 kubectl set image deploy/bid-service app=registry/bid:$GIT_SHA,更规范是 kubectl apply 一份渲染好的 manifest 或 helm upgrade(用 Helm 管理模板 + values)。
  6. 冒烟 + 健康检查:部署后别急着宣告成功——跑一组冒烟测试、确认新 Pod 的 readiness 探针通过、关键接口返回正常。不健康就走 §5 的失败回滚。
  7. 金丝雀放量:核心变更不一把全量,先切一小撮流量(1~5%)到新版本,盯核心指标(竞价成功率、p99、错误率、收入)稳了再逐步放大——按权重分流可交给 Ingress / Service Mesh(金丝雀/蓝绿/回滚的体系化编排见发布与变更安全)。

4.2 一个精简的 Jenkinsfile

把上面串成代码(省略号处为简化):

pipeline {
  agent {
    kubernetes {                          // 每次构建起一个临时 Agent Pod
      yaml '''
        spec:
          containers:
          - name: build
            image: maven:3.9-eclipse-temurin-17
          - name: kaniko                  # 无 daemon 打镜像
            image: gcr.io/kaniko-project/executor:debug
      '''
    }
  }
  environment { IMG = "registry.example.com/bid-service:${GIT_COMMIT}" }  // 用 git sha 做 tag
  stages {
    stage('Build & Test') {
      steps { container('build') { sh 'mvn -B clean verify' } }
    }
    stage('Build & Push Image') {
      steps {
        container('kaniko') {
          sh '/kaniko/executor --dockerfile=Dockerfile --destination=$IMG'  // 打并推镜像
        }
      }
    }
    stage('Approve') {                    // 生产发布前的人工门禁(持续交付)
      steps { input message: '确认发布到生产?', ok: 'Deploy' }
    }
    stage('Deploy to K8s') {
      steps {
        withKubeConfig([credentialsId: 'kubeconfig-prod']) {   // 凭据注入,不硬编码
          sh 'kubectl set image deploy/bid-service app=$IMG --record'
          sh 'kubectl rollout status deploy/bid-service --timeout=120s'   // 等就绪
        }
      }
    }
    stage('Smoke & Canary') {
      steps { sh './scripts/smoke.sh && ./scripts/canary.sh 5' }   // 冒烟 + 放 5% 金丝雀
    }
  }
  post {
    failure {                             // 任何阶段失败 → 自动回滚上一个可用版本
      withKubeConfig([credentialsId: 'kubeconfig-prod']) {
        sh 'kubectl rollout undo deploy/bid-service'
      }
    }
  }
}

这条流水线把本篇的关键点都落地了:动态 Agent、多阶段构建、git-sha 不可变 tag、凭据注入、审批门禁、部署等就绪、冒烟+金丝雀、失败自动回滚

5. 部署策略与发布安全

流水线跑到”部署”这步,怎么把新版本铺上去、出问题怎么退,直接决定发布是”平滑”还是”事故”。

5.1 三种部署策略

策略做法优点代价
滚动更新(Rolling)一批批用新 Pod 替换旧 Pod(K8s Deployment 默认)不额外占太多资源、平滑新旧版本短暂共存;回滚要再滚一轮
蓝绿(Blue-Green)起一整套新版本(绿),验证 OK 后流量整体切过去,旧的(蓝)留着兜底切换瞬间完成、回滚极快(切回蓝)要双倍资源
金丝雀(Canary)先放一小撮新版本、按权重逐步放量,盯指标风险最小、能用真流量验证需要按权重分流的能力 + 指标门禁

广告服务常用金丝雀 + 滚动:核心变更走金丝雀(先 1~5% 看收入/成功率),确认无损再滚动全量;成本敏感时也可对整套竞价链路做蓝绿,换取秒级切回。

5.2 门禁与自动回滚

5.3 地基:镜像不可变 + 版本可追溯

上面所有回滚能力,都建立在一个前提上:镜像是不可变的、且版本可追溯

反过来,用 latest 就把这两条全毁了——同一个 latest 今天和昨天可能是完全不同的内容,回滚时你根本不知道”上一个可用版本”对应哪个镜像。这正是下面 war story 的病根。

6. Push 式 CI/CD vs Pull 式 GitOps

前面 Jenkins 的部署方式(流水线里直接 kubectl apply)属于 push 式。业界还有另一种主流范式 GitOps(pull 式),值得知道两者的取舍。

Push 式 CI/CD 与 Pull 式 GitOps 对比图。左侧蓝色面板"Push 式:Jenkins 主动推到集群":上方紫色芯片"Jenkins 流水线(持有集群凭据)",一条蓝色箭头向下标注"kubectl apply / set image、helm upgrade",指向绿色芯片"K8s API Server → 集群实际状态";下方小字说明 CI 拥有集群写权限、凭据外置在 CI、简单直接,但集群真实状态与 Git 可能悄悄漂移、审计弱。右侧绿色面板"Pull 式:集群内控制器主动拉 Git":顶部琥珀色芯片"Git 仓库(期望状态 = manifests / Helm)",绿色箭头向下标注"watch / 定期拉取 diff"指向紫色芯片"Argo CD / Flux(集群内控制器)",再向下箭头标注"reconcile 收敛真实状态"指向绿色芯片"K8s 实际状态 = Git 声明(防漂移)";下方小字说明 Dev/CI 只提交 Git(PR 审计)、控制器负责收敛、回滚=git revert。底部琥珀色注释对比两种范式的取舍并指出两者可组合。

Push 式(Jenkins)流水线跑完直接把变更推给集群;Pull 式(GitOps)由集群内控制器持续监听 Git、把真实状态收敛到声明。

关键认知:Jenkins 属于 push 式,GitOps 是另一种范式,不是”谁取代谁”。很常见的组合是——Jenkins 负责 CI(编译、测试、打镜像)并把新镜像 tag 写回 Git 仓库的 manifest,然后交给 Argo CD 完成 pull 式部署,两者各取所长。本篇聚焦 Jenkins 这条 push 式主线,GitOps 只做范式对比、点到为止。

参考


views
Share this post on:

Previous Post
广告服务上云实战 · 容器化部署与调优
Next Post
K8s 调度、资源与弹性伸缩