CI/CD从Jenkins到Tekton的迁移复盘:云原生Pipeline架构的渐进式迁移与双轨并行策略
一、项目背景与业务挑战
我们团队管理着 85+ 微服务的 CI/CD Pipeline,全部运行在 Jenkins 上。Jenkins 作为 CI/CD 领域的"老兵",稳定运行了 5 年,但随着业务云原生化的深入,三个结构性矛盾日益突出:
- 架构耦合问题:Jenkins Master 是单点架构,85+ Pipeline 并发构建时 Master 节点 CPU 经常飙到 90%+,构建排队等待时间平均 8 分钟。Agent 通过 SSH 连接静态 EC2 节点,无法利用 K8s 的弹性调度能力。
- 配置碎片化:85+ 服务的 Jenkinsfile 分散在各项目仓库中,共享逻辑靠"copy-paste",一旦安全扫描步骤需要升级,需要逐个修改 85 个 Jenkinsfile,运维成本极高。
- K8s生态脱节:Jenkins Pipeline 在 K8s 外部运行,与 ArgoCD、Prometheus、K8s RBAC 等云原生工具链割裂,无法实现 GitOps 闭环。
Tekton 是 CNCF 的云原生 CI/CD 框架,Pipeline 以 K8s Pod 形式运行,天然具备弹性调度、RBAC 集成、GitOps 对齐等优势。经过 3 个月的评估和 POC,我们决定启动 Jenkins → Tekton 的迁移项目。
二、核心方案:渐进式迁移与双轨并行策略
2.1 Jenkins vs Tekton 架构对比
迁移的第一步是理解两个架构的核心差异:
核心差异总结:
| 维度 | Jenkins | Tekton |
|---|---|---|
| 调度模式 | Master集中调度 | K8s原生Pod调度 |
| 节点管理 | 预分配静态Agent | 按需动态创建Pod |
| Pipeline定义 | Jenkinsfile(Groovy) | Task/Pipeline(YAML) |
| 共享逻辑 | Shared Library(Groovy) | ClusterTask(YAML) |
| RBAC | Jenkins自有权限体系 | K8s RBAC |
| 资源利用 | Agent常驻,利用率低 | Pod按需创建销毁 |
2.2 双轨并行渐进式迁移策略
我们不能"一刀切"切换——85+ 服务全量迁移风险太大。我们设计了四阶段渐进式迁移策略:
from dataclasses import dataclass, field from datetime import datetime from enum import Enum from typing import Dict, List, Optional import logging logger = logging.getLogger(__name__) class PipelineStatus(Enum): """Pipeline迁移状态""" ON_JENKINS = "on_jenkins" # 仍在Jenkins运行 ON_TEKTON = "on_tekton" # 已迁移到Tekton DUAL_RUNNING = "dual_running" # 双轨并行运行中 MIGRATION_FAILED = "migration_failed" # 迁移失败,回退到Jenkins @dataclass class PipelineMigrationRecord: """Pipeline迁移记录""" service_name: str # 服务名称 current_status: PipelineStatus # 当前状态 jenkins_pipeline: Optional[str] = None # Jenkinsfile路径 tekton_pipeline: Optional[str] = None # Tekton Pipeline YAML路径 dual_start_time: Optional[datetime] = None # 双轨并行开始时间 switch_time: Optional[datetime] = None # 切换到Tekton时间 rollback_time: Optional[datetime] = None # 回退时间(如果迁移失败) migration_notes: str = "" # 迁移备注 class PipelineMigrationManager: """Pipeline迁移状态管理器""" # 双轨并行切换条件:连续7天Tekton构建成功率≥98% DUAL_RUNNING_DAYS = 7 SUCCESS_RATE_THRESHOLD = 0.98 def __init__(self): self.records: Dict[str, PipelineMigrationRecord] = {} def evaluate_switch_condition(self, service: str) -> bool: """评估双轨并行服务是否满足切换条件 Args: service: 服务名称 Returns: 是否可以正式切换到Tekton """ try: record = self.records.get(service) if not record or record.current_status != PipelineStatus.DUAL_RUNNING: logger.warning(f"服务 {service} 不在双轨并行状态") return False if not record.dual_start_time: return False # 检查双轨并行天数 days_running = (datetime.now() - record.dual_start_time).days if days_running < self.DUAL_RUNNING_DAYS: logger.info(f"服务 {service} 双轨并行仅 {days_running} 天,未满 {self.DUAL_RUNNING_DAYS} 天") return False # 检查Tekton构建成功率 tekton_success_rate = self._get_tekton_success_rate(service) if tekton_success_rate < self.SUCCESS_RATE_THRESHOLD: logger.warning( f"服务 {service} Tekton成功率 {tekton_success_rate:.2%} " f"低于阈值 {self.SUCCESS_RATE_THRESHOLD:.2%}" ) return False logger.info(f"服务 {service} 满足切换条件:并行 {days_running} 天,成功率 {tekton_success_rate:.2%}") return True except Exception as e: logger.error(f"切换条件评估异常: {e}", exc_info=True) return False def switch_to_tekton(self, service: str) -> bool: """将服务正式切换到Tekton Args: service: 服务名称 Returns: 切换是否成功 """ try: record = self.records.get(service) if not self.evaluate_switch_condition(service): logger.warning(f"服务 {service} 未满足切换条件,拒绝切换") return False # 更新状态 record.current_status = PipelineStatus.ON_TEKTON record.switch_time = datetime.now() record.migration_notes += f" | 正式切换到Tekton: {record.switch_time}" logger.info(f"服务 {service} 正式切换到Tekton") return True except Exception as e: logger.error(f"切换异常: {e}", exc_info=True) return False def rollback_to_jenkins(self, service: str, reason: str) -> bool: """回退到Jenkins Args: service: 服务名称 reason: 回退原因 Returns: 回退是否成功 """ try: record = self.records.get(service) if not record: logger.warning(f"服务 {service} 无迁移记录") return False record.current_status = PipelineStatus.MIGRATION_FAILED record.rollback_time = datetime.now() record.migration_notes += f" | 回退原因: {reason}" logger.warning(f"服务 {service} 回退到Jenkins,原因: {reason}") return True except Exception as e: logger.error(f"回退异常: {e}", exc_info=True) return False def _get_tekton_success_rate(self, service: str) -> float: """查询Tekton构建成功率(模拟调用监控系统)""" # 实际实现调用 Prometheus 查询 Tekton PipelineRun 成功率 return 0.99 # 模拟值2.3 Jenkinsfile 到 Tekton Pipeline 的转化工具
85+ 个 Jenkinsfile 手动转化为 Tekton YAML 不现实。我们开发了自动化转化工具:
import yaml import logging from typing import Dict, List logger = logging.getLogger(__name__) class JenkinsToTektonConverter: """Jenkinsfile → Tekton Pipeline 转化器""" # Jenkins步骤 → Tekton Task 映射表 STEP_MAPPING: Dict[str, dict] = { "git checkout": { "task_name": "git-clone", "clusterTask": "git-clone", # 使用ClusterTask共享任务 "params": {"url": "$(params.repo-url)", "revision": "$(params.revision)"} }, "mvn clean install": { "task_name": "maven-build", "clusterTask": "maven", "params": {"GOALS": "clean install", "MAVEN_PROJECT": "$(workspaces.source.path)"} }, "docker build": { "task_name": "build-and-push-image", "clusterTask": "kaniko", # 使用kaniko替代docker build "params": {"IMAGE": "$(params.image-url)", "DOCKERFILE": "$(workspaces.source.path)/Dockerfile"} }, "security scan": { "task_name": "security-scan", "clusterTask": "trivy-scan", "params": {"IMAGE": "$(params.image-url)"} }, "kubectl deploy": { "task_name": "deploy-to-k8s", "clusterTask": "kubectl-deploy", "params": {"manifest": "$(workspaces.source.path)/k8s/", "namespace": "$(params.namespace)"} }, } def convert(self, jenkinsfile_content: str, service_name: str) -> str: """将Jenkinsfile转化为Tekton Pipeline YAML Args: jenkinsfile_content: Jenkinsfile全文 service_name: 服务名称 Returns: Tekton Pipeline YAML字符串 """ try: # 第一步:解析Jenkinsfile中的步骤 steps = self._parse_jenkins_steps(jenkinsfile_content) logger.info(f"解析到 {len(steps)} 个构建步骤: {steps}") # 第二步:映射到Tekton Tasks tekton_tasks = [] for step in steps: mapping = self.STEP_MAPPING.get(step) if mapping: tekton_tasks.append(mapping) else: # 未映射的步骤:生成自定义Task tekton_tasks.append(self._generate_custom_task(step, service_name)) # 第三步:组装Tekton Pipeline YAML pipeline_yaml = self._assemble_pipeline_yaml(service_name, tekton_tasks) logger.info(f"Pipeline YAML生成完成: {service_name}") return pipeline_yaml except Exception as e: logger.error(f"Pipeline转化异常: {e}", exc_info=True) return "" def _parse_jenkins_steps(self, content: str) -> List[str]: """解析Jenkinsfile中的构建步骤名称""" steps = [] for line in content.split("\n"): line = line.strip() # 提取sh/git/mvn/docker等步骤 if line.startswith("sh '") or line.startswith("sh('"): cmd = line.split("'")[1] if "'" in line else "" for key in self.STEP_MAPPING: if key in cmd.lower(): steps.append(key) break elif "checkout" in line.lower(): steps.append("git checkout") elif "stage" in line.lower(): # 提取stage名称作为步骤参考 pass return steps def _assemble_pipeline_yaml(self, service_name: str, tasks: List[dict]) -> str: """组装Tekton Pipeline YAML""" pipeline = { "apiVersion": "tekton.dev/v1beta1", "kind": "Pipeline", "metadata": {"name": f"{service_name}-pipeline"}, "spec": { "params": [ {"name": "repo-url", "type": "string"}, {"name": "revision", "type": "string", "default": "main"}, {"name": "image-url", "type": "string"}, {"name": "namespace", "type": "string", "default": service_name}, ], "workspaces": [{"name": "source"}], "tasks": [] } } for idx, task in enumerate(tasks): task_def = { "name": task["task_name"], "taskRef": {"name": task.get("clusterTask", task["task_name"])}, "params": task.get("params", {}), "workspaces": [{"name": "source", "workspace": "source"}], } # 设置runAfter:依赖前序任务完成 if idx > 0: task_def["runAfter"] = [tasks[idx-1]["task_name"]] pipeline["spec"]["tasks"].append(task_def) return yaml.dump(pipeline, default_flow_style=False)2.4 Tekton workspace 与 Secret 配置
迁移过程中踩坑最多的地方是 workspace 和 Secret 的对接:
# Tekton PipelineRun 示例:包含workspace和Secret配置 apiVersion: tekton.dev/v1beta1 kind: PipelineRun metadata: name: user-service-pipeline-run-001 spec: pipelineRef: name: user-service-pipeline params: - name: repo-url value: https://gitlab.internal.com/devops/user-service.git - name: revision value: main - name: image-url value: registry.internal.com/devops/user-service:$(params.revision) - name: namespace value: user-service # workspace配置:使用PVC作为源码工作空间 workspaces: - name: source volumeClaimTemplate: spec: accessModes: - ReadWriteOnce resources: requests: storage: 1Gi storageClassName: fast-ssd # 使用SSD存储类提升构建速度 # Secret配置:镜像仓库凭证和Git凭证 # 注意:Tekton使用K8s原生Secret,而非Jenkins的Credentials Store taskSpecs: - taskName: build-and-push-image workspaces: - name: source workspace: source params: - name: IMAGE value: $(params.image-url) # 超时配置:防止Pod无限等待 timeouts: pipeline: 30m # 整个Pipeline最大30分钟 tasks: 10m # 每个Task最大10分钟 # 服务账号:绑定Secret访问权限 serviceAccountName: tekton-pipeline-sa三、实践落地:迁移过程与效果数据
3.1 四阶段迁移时间线
| 阶段 | 时间 | 服务数 | 状态 |
|---|---|---|---|
| POC验证 | 第1-2月 | 3个服务 | Tekton验证通过 |
| 双轨并行 | 第3-4月 | 15个服务 | Jenkins+Tekton同步构建 |
| 批量迁移 | 第5月 | 60个服务 | 逐个切换到Tekton |
| 全量切换 | 第6月 | 85个服务 | Jenkins下线 |
3.2 迁移过程中的踩坑记录
坑1:workspace 数据丢失
Jenkins 的 workspace 是 Agent 节点上的持久目录,多个 stage 自然共享。Tekton 的 workspace 是 Pod 级的,Task Pod 销毁后 workspace 数据随之丢失。
解决方案:使用volumeClaimTemplate为 PipelineRun 创建 PVC,整个 Pipeline 生命周期内 workspace 数据持久化。构建完成后 PVC 自动回收。
坑2:Secret 体系差异
Jenkins 使用 Credentials Store 管理 Git/Registry 凭证,Tekton 使用 K8s Secret。迁移初期,大量构建因凭证缺失而失败。
解决方案:开发 Secret 同步工具,从 Jenkins Credentials Store 自动导出为 K8s Secret,绑定到 Tekton ServiceAccount。
坑3:超时与资源限制
Jenkins Pipeline 没有硬超时限制,长时间构建只是排队等待。Tekton Pod 默认超时较短(60s),导致大型构建频繁超时失败。
解决方案:配置 PipelineRun 的timeouts字段,根据服务构建特征定制超时时间(10m-30m)。
3.3 迁移效果对比数据
| 指标 | Jenkins(迁移前) | Tekton(迁移后) | 变化 |
|---|---|---|---|
| 平均构建时长 | 12分钟 | 8.9分钟 | ↓26% |
| 构建成功率 | 94.2% | 96.6% | ↑2.4% |
| 构建排队等待 | 8分钟 | 0分钟 | ↓100% |
| Agent CPU利用率 | 28% | 62% | ↑119% |
| Jenkins Master负载 | CPU 90%+ | N/A | 已下线 |
| Pipeline配置复用率 | 15% | 78% | ↑63% |
| GitOps闭环支持 | 无 | ArgoCD+Tekton联动 | 全量 |
四、关键挑战与应对策略
4.1 转化工具的覆盖率问题
Jenkinsfile 是自由格式的 Groovy 脚本,85+ 服务的 Jenkinsfile 各有"方言"。转化工具初期的步骤映射覆盖率仅 60%,大量自定义脚本无法自动映射。
应对策略:
- 两轮转化:第一轮用工具转化标准步骤(覆盖率 60%),第二轮人工审查并补充自定义步骤
- 逐步收敛:将自定义脚本抽象为 ClusterTask,减少后续服务的转化工作量
4.2 双轨并行期间的成本翻倍
双轨并行期间,Jenkins + Tekton 两套 CI/CD 同时运行,构建资源消耗翻倍。
应对策略:
- 缩短并行窗口:从计划的 14 天缩短到 7 天(基于成功率快速评估)
- Tekton构建不触发部署:双轨期间 Tekton Pipeline 只做构建验证,不触发实际部署,减少资源浪费
4.3 团队技能转型
运维团队对 Jenkins 有 5 年经验,对 Tekton 完全陌生。迁移后需要快速掌握 Tekton 调试、YAML 编写、K8s RBAC 等新技能。
应对策略:
- Tekton 培训周:迁移前安排 2 周集中培训,覆盖 Pipeline 编写、调试、运维
- 双轨期间"跟跑学习":双轨期间 Tekton 构建结果同步发送给服务负责人,逐步熟悉新模式
4.4 Jenkins 下线的心理阻力
部分团队对 Jenkins 有长期依赖,"Jenkins 一直在,为什么非要换?" 这种心理阻力在迁移后期最明显。
应对策略:
- 数据驱动说服:用构建时长、成功率、资源利用率等硬数据证明 Tekton 的改进
- 保留回退窗口:Jenkins 环境在正式下线前保留 30 天冷备期,消除"无法回退"的焦虑
五、总结
从 Jenkins 到 Tekton 的迁移,本质是从"传统 CI/CD"向"云原生 Pipeline"的架构演进。双轨并行渐进式迁移策略让我们在不中断业务的前提下完成了 85+ 服务的全量切换。
三个关键经验:
- 渐进式而非一刀切:85+ 服务不能同时迁移。四阶段渐进策略(POC→双轨→批量→全量)将风险降到最小,每个阶段都有明确的评估标准和回退机制。
- 转化工具是加速器而非替代品:自动化转化工具能处理 60% 的标准步骤,但剩余 40% 的自定义逻辑仍需人工审查。工具加速了迁移进度,但不能完全替代人的判断。
- 技术迁移也是组织转型:团队技能转型、心理阻力、流程适配等非技术因素对迁移成败的影响不亚于技术本身。培训和数据驱动的说服是关键。
下一步计划:基于 Tekton Pipeline 构建更完整的 GitOps 闭环——Tekton 构建镜像 + ArgoCD 自动部署 + Prometheus 监控反馈,实现从代码提交到生产部署的全自动化流程。