CI/CD流水线:从手动部署到一键上线
一、手动部署的血泪史
2019年某团队的部署流程:
开发提交代码 → 打包war → FTP上传到服务器 → SSH登录 → 停Tomcat → 替换war → 启Tomcat → 手动验证
平均部署时间:40分钟
部署频率:每周一次(因为太痛苦)
回滚方式:再上传旧war重启
故障率:30%(配置遗漏、文件覆盖不完整、步骤遗漏)
有一次,运维在3台服务器上部署,第2台漏了停Tomcat的步骤,旧版本和新版本同时运行了10分钟------产生了200条脏数据。
更惨的是,一次紧急hotfix:凌晨2点手动部署到10台服务器,第7台配置文件路径写错,服务启动失败,凌晨4点才恢复------2小时故障时间。
CI/CD的目标:把40分钟手动部署变成5分钟一键上线,把30%故障率降到1%以下。
二、CI/CD流水线架构
2.1 CI/CD核心概念
CI(持续集成):代码提交后自动触发构建+测试
CD(持续交付):构建产物自动部署到预发布环境
CD(持续部署):预发布验证通过后自动部署到生产环境
完整流水线:
代码提交 → CI(构建+测试) → CD(交付到Staging) → CD(部署到Prod)
┌─────────────────────────────────────────────────────┐
│ CI/CD流水线架构 │
│ │
│ ┌──CI阶段──┐ ┌──CD阶段──┐ ┌──CD阶段──┐ │
│ │代码检出 │ │部署Staging│ │部署Prod │ │
│ │编译构建 │ → │集成测试 │ → │人工审批 │ │
│ │单元测试 │ │性能测试 │ │灰度发布 │ │
│ │代码扫描 │ │验收测试 │ │全量发布 │ │
│ │镜像构建 │ │ │ │健康检查 │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ Git → Jenkins → Docker Registry → K8s集群 │
└─────────────────────────────────────────────────────┘
2.2 CI/CD工具选型
| 工具 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| Jenkins | 插件丰富、灵活 | 维护成本高、UI丑 | 大型团队、复杂流程 |
| GitLab CI | 与GitLab深度集成 | 需GitLab生态 | 使用GitLab的团队 |
| GitHub Actions | 与GitHub集成、云原生 | 免费额度有限 | 开源项目、小团队 |
| ArgoCD | GitOps原生、K8s专精 | 仅K8s场景 | K8s团队、GitOps |
实战选型:
- 已有GitLab → GitLab CI(最自然的选择)
- K8s团队追求GitOps → ArgoCD
- 需要高度自定义 → Jenkins
- 小团队/开源 → GitHub Actions
三、Jenkins流水线实战
3.1 Jenkins Pipeline配置
groovy
// Jenkinsfile(声明式Pipeline)
pipeline {
agent any
environment {
APP_NAME = 'order-service'
DOCKER_REGISTRY = 'registry.example.com'
K8S_NAMESPACE = 'production'
VERSION = "${env.BUILD_NUMBER}"
}
stages {
// Stage 1: 代码检出
stage('Checkout') {
steps {
git branch: 'main', url: 'https://git.example.com/order-service.git'
}
}
// Stage 2: 编译构建
stage('Build') {
steps {
sh 'mvn clean package -DskipTests'
}
}
// Stage 3: 单元测试
stage('Unit Test') {
steps {
sh 'mvn test'
}
post {
always {
junit 'target/surefire-reports/*.xml'
}
}
}
// Stage 4: 代码扫描
stage('Code Scan') {
steps {
sh 'sonar-scanner'
}
}
// Stage 5: Docker镜像构建
stage('Docker Build') {
steps {
script {
sh """
docker build -t ${DOCKER_REGISTRY}/${APP_NAME}:${VERSION} .
docker push ${DOCKER_REGISTRY}/${APP_NAME}:${VERSION}
"""
}
}
}
// Stage 6: 部署Staging
stage('Deploy Staging') {
steps {
sh """
kubectl set image deployment/${APP_NAME} \
${APP_NAME}=${DOCKER_REGISTRY}/${APP_NAME}:${VERSION} \
-n staging
"""
}
}
// Stage 7: 集成测试
stage('Integration Test') {
steps {
sh 'mvn verify -Pintegration-test'
}
}
// Stage 8: 人工审批
stage('Approval') {
steps {
input message: '确认部署到生产环境?', ok: '确认部署'
}
}
// Stage 9: 灰度部署Prod
stage('Deploy Prod Canary') {
steps {
sh """
kubectl apply -f k8s/canary-deployment.yaml
"""
// 等待健康检查
sh 'kubectl rollout status deployment/${APP_NAME}-canary -n production'
}
}
// Stage 10: 全量发布
stage('Deploy Prod Full') {
steps {
sh """
kubectl set image deployment/${APP_NAME} \
${APP_NAME}=${DOCKER_REGISTRY}/${APP_NAME}:${VERSION} \
-n production
"""
sh 'kubectl rollout status deployment/${APP_NAME} -n production'
}
}
}
post {
failure {
// 部署失败 → 自动回滚
sh """
kubectl rollout undo deployment/${APP_NAME} -n production
"""
mail to: 'dev-team@example.com',
subject: "部署失败: ${APP_NAME} #${VERSION}",
body: "流水线失败,请检查: ${env.BUILD_URL}"
}
success {
mail to: 'dev-team@example.com',
subject: "部署成功: ${APP_NAME} #${VERSION}",
body: "流水线成功完成"
}
}
}
3.2 Docker镜像构建优化
dockerfile
# 多阶段构建:减小镜像体积
# 阶段1:构建(Maven + JDK17)
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline # 先下载依赖(缓存层)
COPY src ./src
RUN mvn package -DskipTests
# 阶段2:运行(仅JRE,体积小)
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=builder /app/target/app.jar .
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "-jar", "app.jar"]
# 镜像体积:从800MB降到150MB
bash
# Docker构建缓存优化
# .dockerignore(排除不需要的文件)
.git
node_modules
target
*.md
.env
# 构建时利用缓存
docker build --cache-from ${DOCKER_REGISTRY}/${APP_NAME}:latest \
-t ${DOCKER_REGISTRY}/${APP_NAME}:${VERSION} .
四、GitLab CI实战
4.1 GitLab CI Pipeline
yaml
# .gitlab-ci.yml
stages:
- build
- test
- scan
- deploy-staging
- deploy-prod
variables:
APP_NAME: order-service
DOCKER_REGISTRY: registry.example.com
# 构建阶段
build:
stage: build
image: maven:3.9-eclipse-temurin-17
script:
- mvn clean package -DskipTests
artifacts:
paths:
- target/app.jar
expire_in: 1 day
# 单元测试
unit-test:
stage: test
image: maven:3.9-eclipse-temurin-17
script:
- mvn test
artifacts:
reports:
junit: target/surefire-reports/*.xml
# Docker镜像构建
docker-build:
stage: build
image: docker:24
services:
- docker:24-dind
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $DOCKER_REGISTRY
- docker build -t $DOCKER_REGISTRY/$APP_NAME:$CI_PIPELINE_ID .
- docker push $DOCKER_REGISTRY/$APP_NAME:$CI_PIPELINE_ID
# 部署Staging
deploy-staging:
stage: deploy-staging
image: bitnami/kubectl
script:
- kubectl config use-context staging
- kubectl set image deployment/$APP_NAME
$APP_NAME=$DOCKER_REGISTRY/$APP_NAME:$CI_PIPELINE_ID
-n staging
- kubectl rollout status deployment/$APP_NAME -n staging
environment:
name: staging
# 部署Prod(需人工审批)
deploy-prod:
stage: deploy-prod
image: bitnami/kubectl
script:
- kubectl config use-context production
- kubectl set image deployment/$APP_NAME
$APP_NAME=$DOCKER_REGISTRY/$APP_NAME:$CI_PIPELINE_ID
-n production
- kubectl rollout status deployment/$APP_NAME -n production
environment:
name: production
when: manual # 人工触发
only:
- main
五、灰度发布与回滚
5.1 灰度发布策略
灰度发布流程:
1. Canary部署:新版本部署到1-2台Pod
┌──────────────────────────────┐
│ Pod1(v1) Pod2(v1) Pod3(v2)│ ← 1/3流量走新版本
└──────────────────────────────┘
2. 观察5分钟:监控错误率、延迟、业务指标
3. 逐步扩量:5% → 20% → 50% → 100%
4. 全量发布:所有Pod切到v2
yaml
# K8s Canary Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service-canary
spec:
replicas: 1 # 先1个Pod灰度
selector:
matchLabels:
app: order-service
track: canary
template:
metadata:
labels:
app: order-service
track: canary
spec:
containers:
- name: order-service
image: registry.example.com/order-service:v2
readinessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
# Istio流量分割(更精细的灰度控制)
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: order-service
spec:
hosts:
- order-service
http:
- route:
- destination:
host: order-service
subset: stable
weight: 90 # 90%流量走稳定版本
- destination:
host: order-service
subset: canary
weight: 10 # 10%流量走灰度版本
5.2 自动回滚
groovy
// Jenkins自动回滚逻辑
stage('Deploy Prod Canary') {
steps {
sh 'kubectl apply -f k8s/canary-deployment.yaml'
// 等待5分钟观察
sh 'sleep 300'
// 检查健康指标
script {
def errorRate = sh(
script: "curl -s 'http://prometheus:9090/api/v1/query?query=error_rate{app=\"${APP_NAME}\"}' | jq '.data.result[0].value[1]'",
returnStdout: true).trim()
if (Float.parseFloat(errorRate) > 0.01) {
// 错误率>1% → 自动回滚
sh 'kubectl rollout undo deployment/${APP_NAME}-canary -n production'
error "灰度发布错误率超标(${errorRate}),已自动回滚"
}
}
}
}
六、流水线监控与质量门禁
6.1 质量门禁配置
流水线质量门禁(每个阶段必须通过才能继续):
代码质量门禁:
- 单元测试覆盖率 > 80%
- SonarQube Quality Gate通过
- 无高危安全漏洞
镜像质量门禁:
- 镜像扫描无高危漏洞
- 镜像体积 < 200MB
- 健康检查端点可访问
部署质量门禁:
- Staging集成测试全部通过
- 性能测试P99延迟 < 200ms
- 错误率 < 0.5%
groovy
// 质量门禁检查
stage('Quality Gate') {
steps {
script {
// 1. 测试覆盖率检查
def coverage = sh(
script: "mvn jacoco:report && cat target/site/jacoco/jacoco.csv | grep 'order-service' | cut -d',' -f4",
returnStdout: true).trim()
if (Float.parseFloat(coverage) < 80) {
error "测试覆盖率${coverage}% < 80%,流水线终止"
}
// 2. SonarQube质量门禁
def sonarStatus = sh(
script: "curl -s 'http://sonar:9000/api/qualitygates/project_status?projectKey=${APP_NAME}' | jq '.projectStatus.status'",
returnStdout: true).trim()
if (sonarStatus != 'OK') {
error "SonarQube质量门禁未通过,流水线终止"
}
}
}
}
6.2 流水线指标监控
CI/CD关键指标:
1. 构建成功率(Build Success Rate):目标 > 95%
2. 构建时长(Build Duration):目标 < 10分钟
3. 部署频率(Deployment Frequency):目标 > 每天一次
4. 变更前置时间(Lead Time):从提交到上线 < 1小时
5. 变更失败率(Change Failure Rate):目标 < 5%
6. MTTR(平均恢复时间):目标 < 30分钟
这些是DORA(DevOps Research and Assessment)定义的核心指标,
衡量团队DevOps成熟度。
七、踩坑总结
坑点1:流水线环境不一致
问题:CI环境用Maven 3.8,本地用Maven 3.9,构建结果不一致。
解决:使用Docker镜像统一构建环境,本地和CI用同一镜像。
坑点2:部署后不验证
问题:部署成功但服务启动失败,流水线显示成功但用户看到502。
解决 :部署后必须做健康检查,K8s readinessProbe + 流水线 rollout status。
坑点3:密钥泄露在Pipeline配置中
问题:数据库密码、API密钥写在Jenkinsfile或.gitlab-ci.yml中。
解决:使用CI工具的Secret管理功能(Jenkins Credentials / GitLab Variables),密钥不出现在代码中。
坑点4:全量部署无灰度
问题:一次性部署到所有节点,出问题全量故障。
解决:必须灰度发布:1台→5%→20%→50%→100%,每步验证后继续。
坑点5:回滚脚本不测试
问题:回滚脚本从未执行过,真正需要回滚时发现脚本有bug。
解决:定期演练回滚(每月一次),回滚脚本纳入CI测试。
八、总结
CI/CD不是工具问题,是流程问题。工具只是实现手段,核心是建立从代码提交到生产部署的自动化流水线。
核心要点:
- CI/CD三阶段:构建+测试 → 部署Staging → 部署Prod,每步有质量门禁
- Docker多阶段构建减小镜像体积,利用缓存加速构建
- 灰度发布必做:1台→5%→20%→100%,每步验证健康指标
- 自动回滚:部署后监控错误率,超标自动回滚
- 质量门禁:覆盖率>80%、SonarQube通过、安全扫描无高危
- 密钥管理:密码不出现流水线配置中,使用Secret管理功能
一句话总结:手动部署是运维的噩梦,一键上线是DevOps的起点------先把流程自动化,再追求速度和可靠性。
作者:架构实战团队
日期:2026-07-19
标签:#CI/CD #DevOps #Jenkins #GitLabCI #灰度发布