第四章:持续集成与持续部署 (CI/CD)
最后更新: 2024-01-01
作者: DevOps Team
页面目录
第四章:持续集成与持续部署 (CI/CD)
CI/CD 是 DevOps 的核心实践,通过自动化构建、测试和部署流程,实现快速、可靠的软件交付。本章将深入讲解 CI/CD 的概念、工具和最佳实践。
4.1 CI/CD 核心概念
4.1.1 什么是 CI/CD
┌─────────────────────────────────────────────────────────────────┐
│ │
│ 持续集成 (Continuous Integration) │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 编写 │───→│ 提交 │───→│ 构建 │───→│ 测试 │ │
│ │ 代码 │ │ 代码 │ │ 项目 │ │ 自动化 │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ 持续交付 (Continuous Delivery) │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 构建 │───→│ 部署 │───→│ 预生产 │───→│ 人工 │ │
│ │ 验证 │ │ 测试 │ │ 环境 │ │ 审批 │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ 持续部署 (Continuous Deployment) │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 构建 │───→│ 测试 │───→│ 生产 │───→│ 监控 │ │
│ │ 验证 │ │ 环境 │ │ 环境 │ │ 反馈 │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
4.1.2 CI/CD 的价值
| 价值 | 说明 | 量化指标 |
|---|---|---|
| 快速交付 | 缩短从代码提交到生产部署的时间 | 部署频率提升 30x |
| 高质量 | 自动化测试减少人为错误 | 缺陷率降低 50% |
| 低风险 | 小步快跑,降低每次变更的风险 | 故障恢复时间缩短 50% |
| 透明 | 每个人都能看到构建和部署状态 | 团队协作效率提升 |
4.2 Jenkins 实战
4.2.1 Jenkinsfile 语法
Jenkins Pipeline 使用 Groovy DSL 定义流水线:
// Jenkinsfile
pipeline {
agent any
environment {
DOCKER_IMAGE = 'myapp'
DOCKER_TAG = "${env.BUILD_NUMBER}"
}
options {
timestamps()
timeout(time: 30, unit: 'MINUTES')
buildDiscarder(logRotator(numToKeepStr: '10'))
}
stages {
stage('Checkout') {
steps {
echo 'Checking out source code...'
checkout scm
}
}
stage('Build') {
steps {
echo 'Building application...'
sh '''
npm install
npm run build
'''
}
}
stage('Test') {
steps {
echo 'Running tests...'
sh 'npm test'
junit '**/test-results/*.xml'
cobertura coberturaReportFile: 'coverage/cobertura-coverage.xml'
}
}
stage('Security Scan') {
steps {
echo 'Running security scans...'
sh '''
npm audit --audit-level=moderate
docker run --rm -v $(pwd):/src aquasec/trivy:latest /src
'''
}
}
stage('Docker Build & Push') {
when {
branch 'main'
}
steps {
echo 'Building Docker image...'
sh '''
docker build -t ${DOCKER_IMAGE}:${DOCKER_TAG} .
docker tag ${DOCKER_IMAGE}:${DOCKER_TAG} ${DOCKER_IMAGE}:latest
docker login -u ${DOCKER_USER} -p ${DOCKER_PASS}
docker push ${DOCKER_IMAGE}:${DOCKER_TAG}
docker push ${DOCKER_IMAGE}:latest
'''
}
}
stage('Deploy to Staging') {
when {
branch 'main'
}
steps {
echo 'Deploying to staging...'
sh '''
kubectl set image deployment/myapp \
app=${DOCKER_IMAGE}:${DOCKER_TAG} \
--namespace=staging
kubectl rollout status deployment/myapp \
--namespace=staging
'''
}
}
stage('Deploy to Production') {
when {
branch 'main'
}
steps {
input message: 'Deploy to production?',
ok: 'Deploy'
echo 'Deploying to production...'
sh '''
kubectl set image deployment/myapp \
app=${DOCKER_IMAGE}:${DOCKER_TAG} \
--namespace=production
kubectl rollout status deployment/myapp \
--namespace=production
'''
}
}
}
post {
always {
echo 'Cleaning up...'
cleanWs()
}
success {
echo 'Pipeline succeeded!'
slackSend channel: '#devops',
message: "Build #${env.BUILD_NUMBER} succeeded!"
}
failure {
echo 'Pipeline failed!'
slackSend channel: '#devops',
message: "Build #${env.BUILD_NUMBER} failed!",
color: 'danger'
}
}
}
4.2.2 共享库 (Shared Libraries)
// vars/buildDockerImage.groovy
def call(Map config) {
def imageName = config.imageName ?: env.DOCKER_IMAGE
def tag = config.tag ?: env.BUILD_NUMBER
def registry = config.registry ?: 'docker.io'
pipeline {
stages {
stage('Build Docker Image') {
steps {
script {
def fullImageName = "${registry}/${imageName}:${tag}"
sh """
docker build -t ${fullImageName} .
docker push ${fullImageName}
"""
}
}
}
}
}
}
// 使用共享库
@Library('my-shared-library') _
buildDockerImage imageName: 'myapp', tag: 'v1.0.0'
4.3 GitLab CI 实战
4.3.1 .gitlab-ci.yml 配置
# .gitlab-ci.yml
stages:
- build
- test
- security
- deploy
variables:
DOCKER_IMAGE: registry.gitlab.com/username/myapp
DOCKER_DRIVER: overlay2
# 缓存配置
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
- .npm/
# Docker 镜像
docker:
stage: build
image: docker:latest
services:
- docker:dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $DOCKER_IMAGE:$CI_COMMIT_SHA .
- docker push $DOCKER_IMAGE:$CI_COMMIT_SHA
only:
- main
- develop
# 测试阶段
unit-test:
stage: test
image: node:18-alpine
script:
- npm ci
- npm run test:unit
- npm run test:e2e
coverage: '/Coverage: \d+\.\d+%/'
artifacts:
reports:
junit: junit.xml
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
lint:
stage: test
image: node:18-alpine
script:
- npm ci
- npm run lint
- npm run type-check
# 安全扫描
security-scan:
stage: security
image: aquasec/trivy:latest
script:
- trivy image --exit-code 1 --severity HIGH $DOCKER_IMAGE:$CI_COMMIT_SHA
allow_failure: true
only:
- main
# 部署到开发环境
deploy:dev:
stage: deploy
image: bitnami/kubectl:latest
script:
- kubectl config set-cluster dev --server=${K8S_DEV_SERVER} --insecure-skip-tls-verify=true
- kubectl config set-credentials dev --token=${K8S_DEV_TOKEN}
- kubectl config set-context dev --cluster=dev --user=dev
- kubectl config use-context dev
- kubectl set image deployment/myapp app=$DOCKER_IMAGE:$CI_COMMIT_SHA -n default
- kubectl rollout status deployment/myapp -n default
environment:
name: development
url: https://dev.example.com
only:
- develop
# 部署到生产环境
deploy:prod:
stage: deploy
image: bitnami/kubectl:latest
script:
- kubectl config set-cluster prod --server=${K8S_PROD_SERVER}
- kubectl config set-credentials prod --token=${K8S_PROD_TOKEN}
- kubectl config set-context prod --cluster=prod --user=prod
- kubectl config use-context prod
- kubectl set image deployment/myapp app=$DOCKER_IMAGE:$CI_COMMIT_SHA -n production
- kubectl rollout status deployment/myapp -n production
environment:
name: production
url: https://example.com
when: manual
only:
- main
4.3.2 GitLab CI 高级特性
# 矩阵式构建
build:matrix:
stage: build
parallel:
matrix:
- OS: ubuntu
VERSION: [18.04, 20.04, 22.04]
- OS: alpine
VERSION: [3.15, 3.16, 3.17]
script:
- echo "Building for $OS $VERSION"
# DAG 调度
build:
stage: build
script: echo "Building..."
test:unit:
stage: test
script: echo "Unit tests..."
needs: [build]
test:integration:
stage: test
script: echo "Integration tests..."
needs: [build]
deploy:
stage: deploy
script: echo "Deploying..."
needs:
- test:unit
- test:integration
4.4 GitHub Actions 实战
4.4.1 工作流配置
# .github/workflows/ci.yml
name: CI Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
outputs:
image-tag: ${{ steps.meta.outputs.tags }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container 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=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=sha,prefix=
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max
test:
runs-on: ubuntu-latest
needs: build
services:
postgres:
image: postgres:15
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run type check
run: npm run type-check
- name: Run unit tests
run: npm run test:unit -- --coverage
- name: Run integration tests
env:
DATABASE_URL: postgresql://test:test@localhost:5432/test
REDIS_URL: redis://localhost:6379
run: npm run test:integration
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
fail_ci_if_error: true
security:
runs-on: ubuntu-latest
needs: build
permissions:
security-events: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'
- name: Run npm audit
run: npm audit --audit-level=moderate
deploy:
runs-on: ubuntu-latest
needs: [build, test, security]
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Deploy to Kubernetes
uses: azure/k8s-deploy@v4
with:
manifests: |
manifests/deployment.yaml
manifests/service.yaml
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
namespace: production
4.4.2 自定义 Action
# action.yml
name: 'Deploy to Kubernetes'
description: 'Deploy application to Kubernetes cluster'
inputs:
namespace:
description: 'Kubernetes namespace'
required: true
default: 'default'
image:
description: 'Docker image to deploy'
required: true
rollout-status:
description: 'Wait for rollout to complete'
required: false
default: 'true'
runs:
using: 'composite'
steps:
- name: Set up kubectl
uses: azure/setup-kubectl@v3
- name: Configure kubectl
run: |
echo "${{ inputs.kubeconfig }}" | base64 -d > kubeconfig
export KUBECONFIG=kubeconfig
- name: Deploy
run: |
kubectl set image deployment/myapp app=${{ inputs.image }} -n ${{ inputs.namespace }}
- name: Wait for rollout
if: inputs.rollout-status == 'true'
run: |
kubectl rollout status deployment/myapp -n ${{ inputs.namespace }}
4.5 ArgoCD 实战
4.5.1 Application 定义
# application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: myapp-production
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: production
source:
repoURL: https://github.com/myorg/myapp.git
targetRevision: main
path: k8s/overlays/production
kustomize:
images:
- myapp=myapp:v1.2.3
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: true
allowEmpty: false
syncOptions:
- CreateNamespace=true
- PrunePropagationPolicy=foreground
- PruneLast=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers:
- /spec/replicas
revisionHistoryLimit: 10
4.5.2 ApplicationSet 批量部署
# applicationset.yaml
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: myapp-clusters
namespace: argocd
spec:
generators:
- clusters:
selector:
matchLabels:
environment: production
template:
metadata:
name: 'myapp-{{name}}'
spec:
project: production
source:
repoURL: https://github.com/myorg/myapp.git
targetRevision: main
path: k8s/base
helm:
valueFiles:
- values-{{name}}.yaml
parameters:
- name: image.tag
value: v1.2.3
destination:
server: '{{server}}'
namespace: myapp
syncPolicy:
automated:
prune: true
selfHeal: true
4.6 CI/CD 最佳实践
4.6.1 流水线设计原则
| 原则 | 说明 |
|---|---|
| 快速反馈 | 失败的构建应该在 10 分钟内通知开发者 |
| 幂等性 | 流水线可以被安全地重复执行 |
| 并行执行 | 独立的阶段应该并行运行 |
| 缓存依赖 | 减少重复下载依赖的时间 |
| 失败隔离 | 早期阶段失败应阻止后续阶段 |
4.6.2 安全最佳实践
# 安全配置示例
environment:
# 使用 Secret 管理敏感信息
DATABASE_URL: postgres://user:${DB_PASSWORD}@host/db
API_KEY: ${{ secrets.API_KEY }}
# 不在日志中暴露敏感信息
- name: Build
script: |
echo "Building version ${{ github.sha }}"
# 不要 echo 敏感变量
4.6.3 流水线性能优化
# 依赖缓存
- name: Cache node_modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
# 并行矩阵
jobs:
test:
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- name: Run tests (shard ${{ matrix.shard }})
run: npm test -- --shard=${{ matrix.shard }}/4
4.7 本章小结
| 工具 | 适用场景 | 特点 |
|---|---|---|
| Jenkins | 大型企业,多项目 | 高度可定制,插件丰富 |
| GitLab CI | GitLab 团队 | 集成度高,YAML 配置 |
| GitHub Actions | GitHub 生态 | 免费额度高,Marketplace 丰富 |
| ArgoCD | Kubernetes 部署 | GitOps 原生,声明式 |
📌 下一章预告
下一章我们将学习 容器化技术 Docker,包括:
- Docker 核心概念
- Dockerfile 编写最佳实践
- Docker Compose 编排
- 镜像优化与安全
💡 提示:CI/CD 流水线的设计应该遵循"快速失败"原则,尽可能早地发现并报告问题。