CI/CD流水线设计:从Jenkins到GitHub Actions
// 目录 · contents
前言
CI/CD(持续集成/持续交付)是现代软件开发的核心实践。一条设计良好的流水线能够显著提升交付速度和质量。本文将从流水线设计原则出发,深入对比Jenkins
Pipeline和GitHub Actions,并探讨常见的部署策略。
流水线设计原则
graph LR
subgraph CI["持续集成 (CI)"]
Code["代码提交"] --> Build["构建"]
Build --> UnitTest["单元测试"]
UnitTest --> Lint["代码检查"]
Lint --> SAST["安全扫描"]
SAST --> Artifact["制品打包"]
end
subgraph CD["持续交付 (CD)"]
Artifact --> DeployDev["部署Dev"]
DeployDev --> IntTest["集成测试"]
IntTest --> DeployStaging["部署Staging"]
DeployStaging --> E2E["E2E测试"]
E2E --> Approval["人工审批"]
Approval --> DeployProd["部署Production"]
end
核心设计原则:
- 快速反馈:构建和测试应在几分钟内完成
- 幂等性:同一提交多次构建结果一致
- 制品不可变:构建一次,部署到所有环境
- 最小权限:每个阶段只授予必要的权限
- 可观测:每个阶段都有日志、指标和通知
Jenkins Pipeline
Declarative Pipeline
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180
| pipeline { agent { kubernetes { yaml ''' apiVersion: v1 kind: Pod spec: containers: - name: maven image: maven:3.9-eclipse-temurin-21 command: ['sleep'] args: ['infinity'] - name: docker image: docker:24-dind securityContext: privileged: true env: - name: DOCKER_TLS_CERTDIR value: "" ''' } }
environment { REGISTRY = 'registry.example.com' IMAGE_NAME = 'myapp' SONAR_TOKEN = credentials('sonar-token') KUBECONFIG = credentials('kubeconfig-prod') }
options { timeout(time: 30, unit: 'MINUTES') retry(1) timestamps() disableConcurrentBuilds() }
stages { stage('Checkout') { steps { checkout scm script { env.GIT_COMMIT_SHORT = sh( script: 'git rev-parse --short HEAD', returnStdout: true ).trim() env.IMAGE_TAG = "${env.BRANCH_NAME}-${env.GIT_COMMIT_SHORT}" } } }
stage('Build & Test') { parallel { stage('Unit Test') { steps { container('maven') { sh 'mvn test -Dmaven.test.failure.ignore=false' } } post { always { junit 'target/surefire-reports/*.xml' jacoco(execPattern: 'target/jacoco.exec') } } }
stage('Code Analysis') { steps { container('maven') { sh """ mvn sonar:sonar \ -Dsonar.host.url=https://sonar.example.com \ -Dsonar.token=${SONAR_TOKEN} """ } } } } }
stage('Build Image') { steps { container('docker') { sh """ docker build \ --build-arg VERSION=${IMAGE_TAG} \ -t ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} \ -t ${REGISTRY}/${IMAGE_NAME}:latest \ . docker push ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} docker push ${REGISTRY}/${IMAGE_NAME}:latest """ } } }
stage('Security Scan') { steps { container('docker') { sh """ trivy image --exit-code 1 \ --severity HIGH,CRITICAL \ ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} """ } } }
stage('Deploy Staging') { when { branch 'develop' } steps { sh """ helm upgrade --install myapp ./charts/myapp \ --namespace staging \ --set image.tag=${IMAGE_TAG} \ -f charts/myapp/values-staging.yaml \ --wait --timeout 5m """ } }
stage('Integration Test') { when { branch 'develop' } steps { sh """ newman run tests/integration/postman_collection.json \ --environment tests/integration/staging.env.json \ --reporters cli,junit \ --reporter-junit-export results/integration.xml """ } }
stage('Deploy Production') { when { branch 'main' } input { message "部署到生产环境?" ok "确认部署" submitter "admin,deployer" } steps { sh """ helm upgrade --install myapp ./charts/myapp \ --namespace production \ --set image.tag=${IMAGE_TAG} \ -f charts/myapp/values-production.yaml \ --wait --timeout 10m """ } } }
post { success { slackSend( channel: '#deployments', color: 'good', message: "构建成功: ${env.JOB_NAME} #${env.BUILD_NUMBER} (${IMAGE_TAG})" ) } failure { slackSend( channel: '#deployments', color: 'danger', message: "构建失败: ${env.JOB_NAME} #${env.BUILD_NUMBER}" ) } always { cleanWs() } } }
|
Jenkins Shared Library
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| def call(Map config) { pipeline { agent any
stages { stage('Build') { steps { script { docker.build("${config.imageName}:${env.BUILD_NUMBER}") } } }
stage('Test') { steps { sh config.testCommand ?: 'make test' } }
stage('Deploy') { when { branch config.deployBranch ?: 'main' } steps { sh "helm upgrade --install ${config.releaseName} ${config.chartPath}" } } } } }
@Library('my-shared-lib') _ standardPipeline( imageName: 'myapp', releaseName: 'myapp', chartPath: './charts/myapp', testCommand: 'mvn test' )
|
GitHub Actions
完整工作流
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
| name: CI/CD Pipeline
on: push: branches: [main, develop] pull_request: branches: [main]
permissions: contents: read packages: write security-events: write
env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }}
jobs: test: name: Test runs-on: ubuntu-latest strategy: matrix: java-version: [17, 21] steps: - uses: actions/checkout@v4
- name: Setup Java uses: actions/setup-java@v4 with: java-version: ${{ matrix.java-version }} distribution: temurin cache: maven
- name: Run Tests run: mvn verify -B
- name: Upload Coverage if: matrix.java-version == 21 uses: codecov/codecov-action@v4 with: file: target/site/jacoco/jacoco.xml
- name: Upload Test Results if: always() uses: actions/upload-artifact@v4 with: name: test-results-java-${{ matrix.java-version }} path: target/surefire-reports/
lint: name: Code Quality runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0
- name: SonarCloud Scan uses: SonarSource/sonarcloud-github-action@master env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
security: name: Security Scan runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Run Snyk uses: snyk/actions/maven@master continue-on-error: true env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} with: args: --severity-threshold=high
- name: Upload SARIF uses: github/codeql-action/upload-sarif@v3 with: sarif_file: snyk.sarif
build: name: Build & Push Image needs: [test, lint, security] runs-on: ubuntu-latest outputs: image-tag: ${{ steps.meta.outputs.version }} steps: - uses: actions/checkout@v4
- name: Setup Docker Buildx uses: docker/setup-buildx-action@v3
- name: Login to Registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=sha,prefix= type=ref,event=branch type=semver,pattern={{version}}
- name: Build and Push uses: docker/build-push-action@v5 with: context: . push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max platforms: linux/amd64,linux/arm64
- name: Scan Image uses: aquasecurity/trivy-action@master with: image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} format: sarif output: trivy-results.sarif severity: CRITICAL,HIGH
deploy-staging: name: Deploy to Staging needs: build if: github.ref == 'refs/heads/develop' runs-on: ubuntu-latest environment: staging steps: - uses: actions/checkout@v4
- name: Setup Helm uses: azure/setup-helm@v4
- name: Setup kubectl uses: azure/setup-kubectl@v4
- name: Configure kubeconfig run: | mkdir -p $HOME/.kube echo "${{ secrets.KUBE_CONFIG_STAGING }}" | base64 -d > $HOME/.kube/config
- name: Deploy run: | helm upgrade --install myapp ./charts/myapp \ --namespace staging \ --set image.tag=${{ needs.build.outputs.image-tag }} \ -f charts/myapp/values-staging.yaml \ --wait --timeout 5m
deploy-production: name: Deploy to Production needs: [build, deploy-staging] if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest environment: name: production url: https://myapp.example.com steps: - uses: actions/checkout@v4
- name: Setup Helm uses: azure/setup-helm@v4
- name: Configure kubeconfig run: | mkdir -p $HOME/.kube echo "${{ secrets.KUBE_CONFIG_PROD }}" | base64 -d > $HOME/.kube/config
- name: Deploy with Canary run: | # 金丝雀发布:先部署10%流量 helm upgrade --install myapp ./charts/myapp \ --namespace production \ --set image.tag=${{ needs.build.outputs.image-tag }} \ --set canary.enabled=true \ --set canary.weight=10 \ -f charts/myapp/values-production.yaml \ --wait --timeout 10m
- name: Verify Canary run: | # 等待并检查错误率 sleep 120 ERROR_RATE=$(curl -s "https://prometheus.example.com/api/v1/query?query=rate(http_requests_total{status=~'5..'}[5m])/rate(http_requests_total[5m])*100" | jq '.data.result[0].value[1]' -r) if (( $(echo "$ERROR_RATE > 1" | bc -l) )); then echo "Error rate too high: ${ERROR_RATE}%" helm rollback myapp exit 1 fi
- name: Promote to Full Rollout run: | helm upgrade --install myapp ./charts/myapp \ --namespace production \ --set image.tag=${{ needs.build.outputs.image-tag }} \ --set canary.enabled=false \ -f charts/myapp/values-production.yaml \ --wait --timeout 10m
|
GitHub Actions工作流可视化
graph TB
Push["Push Event"] --> Test["test<br>(matrix: Java 17,21)"]
Push --> Lint["lint<br>(SonarCloud)"]
Push --> Security["security<br>(Snyk)"]
Test --> Build["build<br>(Docker Build & Push)"]
Lint --> Build
Security --> Build
Build --> |"develop分支"| Staging["deploy-staging"]
Build --> |"main分支"| Production["deploy-production"]
Staging --> Production
Staging -.-> |"environment: staging"| SApproval["自动部署"]
Production -.-> |"environment: production"| PApproval["需要审批"]
部署策略详解
蓝绿部署(Blue-Green)
graph TB
subgraph Before["部署前"]
LB1["Load Balancer"] --> Blue1["Blue (v1)<br>Active"]
Green1["Green (v2)<br>Idle"]
end
subgraph After["部署后"]
LB2["Load Balancer"] --> Green2["Green (v2)<br>Active"]
Blue2["Blue (v1)<br>Idle/Rollback"]
end
Before --> |"切换流量"| After
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| apiVersion: v1 kind: Service metadata: name: myapp spec: selector: app: myapp version: green ports: - port: 80 targetPort: 8080
--- apiVersion: apps/v1 kind: Deployment metadata: name: myapp-blue spec: replicas: 3 selector: matchLabels: app: myapp version: blue template: metadata: labels: app: myapp version: blue spec: containers: - name: app image: myapp:v1
--- apiVersion: apps/v1 kind: Deployment metadata: name: myapp-green spec: replicas: 3 selector: matchLabels: app: myapp version: green template: metadata: labels: app: myapp version: green spec: containers: - name: app image: myapp:v2
|
金丝雀部署(Canary)
graph TB
subgraph Step1["阶段1: 10%流量"]
LB1["Load Balancer"]
LB1 --> |"90%"| Stable1["Stable v1 (3副本)"]
LB1 --> |"10%"| Canary1["Canary v2 (1副本)"]
end
subgraph Step2["阶段2: 50%流量"]
LB2["Load Balancer"]
LB2 --> |"50%"| Stable2["Stable v1 (3副本)"]
LB2 --> |"50%"| Canary2["Canary v2 (3副本)"]
end
subgraph Step3["阶段3: 全量发布"]
LB3["Load Balancer"]
LB3 --> |"100%"| New["New v2 (3副本)"]
end
Step1 --> |"指标正常"| Step2
Step2 --> |"指标正常"| Step3
Secrets管理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| jobs: deploy: environment: production steps: - name: Deploy env: DB_PASSWORD: ${{ secrets.DB_PASSWORD }} API_KEY: ${{ secrets.API_KEY }} run: | # 通过Kubernetes Secret注入 kubectl create secret generic app-secrets \ --from-literal=db-password="$DB_PASSWORD" \ --from-literal=api-key="$API_KEY" \ --dry-run=client -o yaml | kubectl apply -f -
|
graph LR
subgraph SecretSources["Secret来源"]
GHS["GitHub Secrets"]
Vault["HashiCorp Vault"]
AWS["AWS Secrets Manager"]
SOPS["Mozilla SOPS"]
end
subgraph Pipeline["CI/CD Pipeline"]
Inject["注入环境变量"]
K8sSecret["创建K8s Secret"]
end
GHS --> Inject
Vault --> Inject
AWS --> Inject
SOPS --> Inject
Inject --> K8sSecret
K8sSecret --> Pod["Application Pod"]
Jenkins vs GitHub
Actions对比
| 部署方式 |
自托管 |
SaaS(也支持self-hosted runner) |
| 配置方式 |
Jenkinsfile (Groovy) |
YAML |
| 插件生态 |
1800+ 插件 |
Marketplace(数万Actions) |
| 并行执行 |
支持 (parallel) |
支持 (matrix/并行job) |
| 缓存 |
手动配置 |
内置actions/cache |
| Secrets |
Credentials Plugin |
内置Secrets管理 |
| 审批 |
input步骤 |
Environments Protection Rules |
| 成本 |
服务器成本 |
按分钟计费(公共仓库免费) |
| 适用场景 |
复杂企业级流水线 |
开源项目、中小团队 |
最佳实践总结
- 流水线即代码:将CI/CD配置纳入版本控制
- 并行化:独立的阶段并行执行,缩短反馈时间
- 缓存优化:缓存依赖下载、Docker层,加速构建
- 制品不可变:构建一次,推送到制品仓库,部署时拉取
- 渐进式发布:从canary到全量,出问题及时回滚
- 安全左移:在CI阶段集成SAST、依赖扫描、镜像扫描
- 环境一致性:使用容器化构建环境,避免”在我机器上能跑”
- 监控告警:部署后自动验证关键指标,异常自动回滚