cicd-expert

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

CI/CD Expert

CI/CD专家指南

Expert guidance for Continuous Integration and Continuous Deployment, including GitHub Actions, Jenkins, GitLab CI, deployment strategies, and automation best practices.
为持续集成与持续部署提供专业指导,涵盖GitHub Actions、Jenkins、GitLab CI、部署策略及自动化最佳实践。

Core Concepts

核心概念

CI/CD Fundamentals

CI/CD基础

  • Continuous Integration (CI)
  • Continuous Delivery vs Deployment
  • Build automation
  • Test automation
  • Artifact management
  • Deployment strategies (blue-green, canary, rolling)
  • 持续集成(CI)
  • 持续交付与持续部署对比
  • 构建自动化
  • 测试自动化
  • 制品管理
  • 部署策略(蓝绿部署、金丝雀部署、滚动部署)

Pipeline Design

流水线设计

  • Pipeline stages and jobs
  • Parallel execution
  • Dependencies and artifacts
  • Caching strategies
  • Matrix builds
  • Conditional execution
  • 流水线阶段与任务
  • 并行执行
  • 依赖与制品
  • 缓存策略
  • 矩阵构建
  • 条件执行

Security

安全相关

  • Secret management
  • Dependency scanning
  • SAST/DAST
  • Container scanning
  • Supply chain security
  • SBOM generation
  • 密钥管理
  • 依赖扫描
  • SAST/DAST静态/动态应用安全测试
  • 容器扫描
  • 供应链安全
  • SBOM软件物料清单生成

GitHub Actions

GitHub Actions

Workflow Basics

工作流基础

yaml
undefined
yaml
undefined

.github/workflows/ci.yml

.github/workflows/ci.yml

name: CI
on: push: branches: [main, develop] pull_request: branches: [main] workflow_dispatch:
env: NODE_VERSION: '20' DOCKER_REGISTRY: ghcr.io
jobs: test: name: Test runs-on: ubuntu-latest
strategy:
  matrix:
    node-version: [18, 20, 21]

steps:
  - name: Checkout code
    uses: actions/checkout@v4
    with:
      fetch-depth: 0  # Full history for SonarCloud

  - name: Setup Node.js ${{ matrix.node-version }}
    uses: actions/setup-node@v4
    with:
      node-version: ${{ matrix.node-version }}
      cache: 'npm'

  - name: Install dependencies
    run: npm ci

  - name: Run linting
    run: npm run lint

  - name: Run tests
    run: npm test -- --coverage

  - name: Upload coverage
    uses: codecov/codecov-action@v3
    with:
      token: ${{ secrets.CODECOV_TOKEN }}
      files: ./coverage/coverage-final.json
build: name: Build runs-on: ubuntu-latest needs: test
steps:
  - uses: actions/checkout@v4

  - uses: actions/setup-node@v4
    with:
      node-version: ${{ env.NODE_VERSION }}
      cache: 'npm'

  - run: npm ci
  - run: npm run build

  - name: Upload build artifacts
    uses: actions/upload-artifact@v3
    with:
      name: build
      path: dist/
      retention-days: 7
security: name: Security Scan runs-on: ubuntu-latest
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
    uses: github/codeql-action/upload-sarif@v2
    with:
      sarif_file: 'trivy-results.sarif'

  - name: Run npm audit
    run: npm audit --audit-level=high
undefined
name: CI
on: push: branches: [main, develop] pull_request: branches: [main] workflow_dispatch:
env: NODE_VERSION: '20' DOCKER_REGISTRY: ghcr.io
jobs: test: name: Test runs-on: ubuntu-latest
strategy:
  matrix:
    node-version: [18, 20, 21]

steps:
  - name: Checkout code
    uses: actions/checkout@v4
    with:
      fetch-depth: 0  # Full history for SonarCloud

  - name: Setup Node.js ${{ matrix.node-version }}
    uses: actions/setup-node@v4
    with:
      node-version: ${{ matrix.node-version }}
      cache: 'npm'

  - name: Install dependencies
    run: npm ci

  - name: Run linting
    run: npm run lint

  - name: Run tests
    run: npm test -- --coverage

  - name: Upload coverage
    uses: codecov/codecov-action@v3
    with:
      token: ${{ secrets.CODECOV_TOKEN }}
      files: ./coverage/coverage-final.json
build: name: Build runs-on: ubuntu-latest needs: test
steps:
  - uses: actions/checkout@v4

  - uses: actions/setup-node@v4
    with:
      node-version: ${{ env.NODE_VERSION }}
      cache: 'npm'

  - run: npm ci
  - run: npm run build

  - name: Upload build artifacts
    uses: actions/upload-artifact@v3
    with:
      name: build
      path: dist/
      retention-days: 7
security: name: Security Scan runs-on: ubuntu-latest
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
    uses: github/codeql-action/upload-sarif@v2
    with:
      sarif_file: 'trivy-results.sarif'

  - name: Run npm audit
    run: npm audit --audit-level=high
undefined

Docker Build and Push

Docker构建与推送

yaml
undefined
yaml
undefined

.github/workflows/docker.yml

.github/workflows/docker.yml

name: Docker Build and Push
on: push: branches: [main] tags: ['v*']
env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }}
jobs: build-and-push: runs-on: ubuntu-latest permissions: contents: read packages: write
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=semver,pattern={{major}}.{{minor}}
        type=sha,prefix={{branch}}-

  - 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
undefined
name: Docker Build and Push
on: push: branches: [main] tags: ['v*']
env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }}
jobs: build-and-push: runs-on: ubuntu-latest permissions: contents: read packages: write
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=semver,pattern={{major}}.{{minor}}
        type=sha,prefix={{branch}}-

  - 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
undefined

Deployment Workflow

部署工作流

yaml
undefined
yaml
undefined

.github/workflows/deploy.yml

.github/workflows/deploy.yml

name: Deploy to Production
on: push: tags: ['v*'] workflow_dispatch: inputs: environment: description: 'Environment to deploy to' required: true type: choice options: - staging - production
jobs: deploy: name: Deploy to ${{ inputs.environment || 'production' }} runs-on: ubuntu-latest environment: name: ${{ inputs.environment || 'production' }} url: https://${{ inputs.environment || 'production' }}.example.com
steps:
  - uses: actions/checkout@v4

  - name: Configure AWS credentials
    uses: aws-actions/configure-aws-credentials@v4
    with:
      aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
      aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      aws-region: us-east-1

  - name: Deploy to ECS
    run: |
      aws ecs update-service \
        --cluster ${{ secrets.ECS_CLUSTER }} \
        --service ${{ secrets.ECS_SERVICE }} \
        --force-new-deployment

  - name: Wait for deployment
    run: |
      aws ecs wait services-stable \
        --cluster ${{ secrets.ECS_CLUSTER }} \
        --services ${{ secrets.ECS_SERVICE }}

  - name: Notify Slack
    uses: slackapi/slack-github-action@v1
    with:
      webhook-url: ${{ secrets.SLACK_WEBHOOK }}
      payload: |
        {
          "text": "Deployment to ${{ inputs.environment || 'production' }} successful!",
          "blocks": [
            {
              "type": "section",
              "text": {
                "type": "mrkdwn",
                "text": "✅ *Deployment Successful*\n*Environment:* ${{ inputs.environment || 'production' }}\n*Version:* ${{ github.ref_name }}"
              }
            }
          ]
        }
undefined
name: Deploy to Production
on: push: tags: ['v*'] workflow_dispatch: inputs: environment: description: 'Environment to deploy to' required: true type: choice options: - staging - production
jobs: deploy: name: Deploy to ${{ inputs.environment || 'production' }} runs-on: ubuntu-latest environment: name: ${{ inputs.environment || 'production' }} url: https://${{ inputs.environment || 'production' }}.example.com
steps:
  - uses: actions/checkout@v4

  - name: Configure AWS credentials
    uses: aws-actions/configure-aws-credentials@v4
    with:
      aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
      aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      aws-region: us-east-1

  - name: Deploy to ECS
    run: |
      aws ecs update-service \
        --cluster ${{ secrets.ECS_CLUSTER }} \
        --service ${{ secrets.ECS_SERVICE }} \
        --force-new-deployment

  - name: Wait for deployment
    run: |
      aws ecs wait services-stable \
        --cluster ${{ secrets.ECS_CLUSTER }} \
        --services ${{ secrets.ECS_SERVICE }}

  - name: Notify Slack
    uses: slackapi/slack-github-action@v1
    with:
      webhook-url: ${{ secrets.SLACK_WEBHOOK }}
      payload: |
        {
          "text": "Deployment to ${{ inputs.environment || 'production' }} successful!",
          "blocks": [
            {
              "type": "section",
              "text": {
                "type": "mrkdwn",
                "text": "✅ *Deployment Successful*\n*Environment:* ${{ inputs.environment || 'production' }}\n*Version:* ${{ github.ref_name }}"
              }
            }
          ]
        }
undefined

Reusable Workflows

可复用工作流

yaml
undefined
yaml
undefined

.github/workflows/reusable-test.yml

.github/workflows/reusable-test.yml

name: Reusable Test Workflow
on: workflow_call: inputs: node-version: required: true type: string working-directory: required: false type: string default: '.' secrets: codecov-token: required: true
jobs: test: runs-on: ubuntu-latest defaults: run: working-directory: ${{ inputs.working-directory }}
steps:
  - uses: actions/checkout@v4
  - uses: actions/setup-node@v4
    with:
      node-version: ${{ inputs.node-version }}
      cache: 'npm'
      cache-dependency-path: ${{ inputs.working-directory }}/package-lock.json

  - run: npm ci
  - run: npm test -- --coverage

  - uses: codecov/codecov-action@v3
    with:
      token: ${{ secrets.codecov-token }}
name: Reusable Test Workflow
on: workflow_call: inputs: node-version: required: true type: string working-directory: required: false type: string default: '.' secrets: codecov-token: required: true
jobs: test: runs-on: ubuntu-latest defaults: run: working-directory: ${{ inputs.working-directory }}
steps:
  - uses: actions/checkout@v4
  - uses: actions/setup-node@v4
    with:
      node-version: ${{ inputs.node-version }}
      cache: 'npm'
      cache-dependency-path: ${{ inputs.working-directory }}/package-lock.json

  - run: npm ci
  - run: npm test -- --coverage

  - uses: codecov/codecov-action@v3
    with:
      token: ${{ secrets.codecov-token }}

Usage in another workflow

Usage in another workflow

jobs: test-backend: uses: ./.github/workflows/reusable-test.yml with: node-version: '20' working-directory: './backend' secrets: codecov-token: ${{ secrets.CODECOV_TOKEN }}
undefined
jobs: test-backend: uses: ./.github/workflows/reusable-test.yml with: node-version: '20' working-directory: './backend' secrets: codecov-token: ${{ secrets.CODECOV_TOKEN }}
undefined

Composite Actions

复合动作

yaml
undefined
yaml
undefined

.github/actions/setup-project/action.yml

.github/actions/setup-project/action.yml

name: 'Setup Project' description: 'Setup Node.js and install dependencies'
inputs: node-version: description: 'Node.js version' required: false default: '20'
runs: using: 'composite' steps: - uses: actions/setup-node@v4 with: node-version: ${{ inputs.node-version }} cache: 'npm'
- name: Install dependencies
  shell: bash
  run: npm ci

- name: Cache build
  uses: actions/cache@v3
  with:
    path: |
      dist
      .next/cache
    key: ${{ runner.os }}-build-${{ hashFiles('**/package-lock.json') }}
name: 'Setup Project' description: 'Setup Node.js and install dependencies'
inputs: node-version: description: 'Node.js version' required: false default: '20'
runs: using: 'composite' steps: - uses: actions/setup-node@v4 with: node-version: ${{ inputs.node-version }} cache: 'npm'
- name: Install dependencies
  shell: bash
  run: npm ci

- name: Cache build
  uses: actions/cache@v3
  with:
    path: |
      dist
      .next/cache
    key: ${{ runner.os }}-build-${{ hashFiles('**/package-lock.json') }}

Usage

Usage

steps:
  • uses: actions/checkout@v4
  • uses: ./.github/actions/setup-project with: node-version: '20'
undefined
steps:
  • uses: actions/checkout@v4
  • uses: ./.github/actions/setup-project with: node-version: '20'
undefined

Jenkins

Jenkins

Declarative Pipeline

声明式流水线

groovy
// Jenkinsfile
pipeline {
    agent {
        docker {
            image 'node:20-alpine'
            args '-v /var/run/docker.sock:/var/run/docker.sock'
        }
    }

    environment {
        NODE_ENV = 'production'
        DOCKER_REGISTRY = 'ghcr.io'
        IMAGE_NAME = "${env.DOCKER_REGISTRY}/${env.GIT_ORG}/${env.GIT_REPO}"
    }

    options {
        buildDiscarder(logRotator(numToKeepStr: '10'))
        timeout(time: 1, unit: 'HOURS')
        timestamps()
        disableConcurrentBuilds()
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
                script {
                    env.GIT_COMMIT_SHORT = sh(
                        returnStdout: true,
                        script: 'git rev-parse --short HEAD'
                    ).trim()
                }
            }
        }

        stage('Install Dependencies') {
            steps {
                sh 'npm ci'
            }
        }

        stage('Lint') {
            steps {
                sh 'npm run lint'
            }
        }

        stage('Test') {
            parallel {
                stage('Unit Tests') {
                    steps {
                        sh 'npm run test:unit -- --coverage'
                    }
                    post {
                        always {
                            junit 'test-results/unit/*.xml'
                            publishHTML([
                                reportDir: 'coverage',
                                reportFiles: 'index.html',
                                reportName: 'Coverage Report'
                            ])
                        }
                    }
                }

                stage('Integration Tests') {
                    steps {
                        sh 'npm run test:integration'
                    }
                    post {
                        always {
                            junit 'test-results/integration/*.xml'
                        }
                    }
                }
            }
        }

        stage('Build') {
            steps {
                sh 'npm run build'
                archiveArtifacts artifacts: 'dist/**/*', fingerprint: true
            }
        }

        stage('Docker Build') {
            when {
                branch 'main'
            }
            steps {
                script {
                    docker.build("${env.IMAGE_NAME}:${env.GIT_COMMIT_SHORT}")
                }
            }
        }

        stage('Security Scan') {
            parallel {
                stage('Dependency Check') {
                    steps {
                        sh 'npm audit --audit-level=high'
                    }
                }

                stage('Container Scan') {
                    when {
                        branch 'main'
                    }
                    steps {
                        sh """
                            trivy image \
                                --severity HIGH,CRITICAL \
                                --exit-code 1 \
                                ${env.IMAGE_NAME}:${env.GIT_COMMIT_SHORT}
                        """
                    }
                }
            }
        }

        stage('Push Image') {
            when {
                branch 'main'
            }
            steps {
                script {
                    docker.withRegistry("https://${env.DOCKER_REGISTRY}", 'docker-credentials') {
                        docker.image("${env.IMAGE_NAME}:${env.GIT_COMMIT_SHORT}").push()
                        docker.image("${env.IMAGE_NAME}:${env.GIT_COMMIT_SHORT}").push('latest')
                    }
                }
            }
        }

        stage('Deploy to Staging') {
            when {
                branch 'main'
            }
            steps {
                script {
                    kubernetesDeploy(
                        configs: 'k8s/staging/*.yaml',
                        kubeconfigId: 'kubeconfig-staging'
                    )
                }
            }
        }

        stage('Smoke Tests') {
            when {
                branch 'main'
            }
            steps {
                sh 'npm run test:smoke -- --env=staging'
            }
        }

        stage('Deploy to Production') {
            when {
                branch 'main'
            }
            input {
                message 'Deploy to production?'
                ok 'Deploy'
            }
            steps {
                script {
                    kubernetesDeploy(
                        configs: 'k8s/production/*.yaml',
                        kubeconfigId: 'kubeconfig-production'
                    )
                }
            }
        }
    }

    post {
        always {
            cleanWs()
        }
        success {
            slackSend(
                color: 'good',
                message: "Build succeeded: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
            )
        }
        failure {
            slackSend(
                color: 'danger',
                message: "Build failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
            )
        }
    }
}
groovy
// Jenkinsfile
pipeline {
    agent {
        docker {
            image 'node:20-alpine'
            args '-v /var/run/docker.sock:/var/run/docker.sock'
        }
    }

    environment {
        NODE_ENV = 'production'
        DOCKER_REGISTRY = 'ghcr.io'
        IMAGE_NAME = "${env.DOCKER_REGISTRY}/${env.GIT_ORG}/${env.GIT_REPO}"
    }

    options {
        buildDiscarder(logRotator(numToKeepStr: '10'))
        timeout(time: 1, unit: 'HOURS')
        timestamps()
        disableConcurrentBuilds()
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
                script {
                    env.GIT_COMMIT_SHORT = sh(
                        returnStdout: true,
                        script: 'git rev-parse --short HEAD'
                    ).trim()
                }
            }
        }

        stage('Install Dependencies') {
            steps {
                sh 'npm ci'
            }
        }

        stage('Lint') {
            steps {
                sh 'npm run lint'
            }
        }

        stage('Test') {
            parallel {
                stage('Unit Tests') {
                    steps {
                        sh 'npm run test:unit -- --coverage'
                    }
                    post {
                        always {
                            junit 'test-results/unit/*.xml'
                            publishHTML([
                                reportDir: 'coverage',
                                reportFiles: 'index.html',
                                reportName: 'Coverage Report'
                            ])
                        }
                    }
                }

                stage('Integration Tests') {
                    steps {
                        sh 'npm run test:integration'
                    }
                    post {
                        always {
                            junit 'test-results/integration/*.xml'
                        }
                    }
                }
            }
        }

        stage('Build') {
            steps {
                sh 'npm run build'
                archiveArtifacts artifacts: 'dist/**/*', fingerprint: true
            }
        }

        stage('Docker Build') {
            when {
                branch 'main'
            }
            steps {
                script {
                    docker.build("${env.IMAGE_NAME}:${env.GIT_COMMIT_SHORT}")
                }
            }
        }

        stage('Security Scan') {
            parallel {
                stage('Dependency Check') {
                    steps {
                        sh 'npm audit --audit-level=high'
                    }
                }

                stage('Container Scan') {
                    when {
                        branch 'main'
                    }
                    steps {
                        sh """
                            trivy image \
                                --severity HIGH,CRITICAL \
                                --exit-code 1 \
                                ${env.IMAGE_NAME}:${env.GIT_COMMIT_SHORT}
                        """
                    }
                }
            }
        }

        stage('Push Image') {
            when {
                branch 'main'
            }
            steps {
                script {
                    docker.withRegistry("https://${env.DOCKER_REGISTRY}", 'docker-credentials') {
                        docker.image("${env.IMAGE_NAME}:${env.GIT_COMMIT_SHORT}").push()
                        docker.image("${env.IMAGE_NAME}:${env.GIT_COMMIT_SHORT}").push('latest')
                    }
                }
            }
        }

        stage('Deploy to Staging') {
            when {
                branch 'main'
            }
            steps {
                script {
                    kubernetesDeploy(
                        configs: 'k8s/staging/*.yaml',
                        kubeconfigId: 'kubeconfig-staging'
                    )
                }
            }
        }

        stage('Smoke Tests') {
            when {
                branch 'main'
            }
            steps {
                sh 'npm run test:smoke -- --env=staging'
            }
        }

        stage('Deploy to Production') {
            when {
                branch 'main'
            }
            steps {
                input {
                    message 'Deploy to production?'
                    ok 'Deploy'
                }
                script {
                    kubernetesDeploy(
                        configs: 'k8s/production/*.yaml',
                        kubeconfigId: 'kubeconfig-production'
                    )
                }
            }
        }
    }

    post {
        always {
            cleanWs()
        }
        success {
            slackSend(
                color: 'good',
                message: "Build succeeded: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
            )
        }
        failure {
            slackSend(
                color: 'danger',
                message: "Build failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
            )
        }
    }
}

Shared Library

共享库

groovy
// vars/deployToKubernetes.groovy
def call(Map config) {
    def namespace = config.namespace
    def deployment = config.deployment
    def image = config.image

    sh """
        kubectl set image deployment/${deployment} \
            ${deployment}=${image} \
            -n ${namespace}

        kubectl rollout status deployment/${deployment} \
            -n ${namespace} \
            --timeout=5m
    """
}

// Usage in Jenkinsfile
@Library('shared-library') _

pipeline {
    stages {
        stage('Deploy') {
            steps {
                deployToKubernetes(
                    namespace: 'production',
                    deployment: 'web-app',
                    image: "${IMAGE_NAME}:${GIT_COMMIT_SHORT}"
                )
            }
        }
    }
}
groovy
// vars/deployToKubernetes.groovy
def call(Map config) {
    def namespace = config.namespace
    def deployment = config.deployment
    def image = config.image

    sh """
        kubectl set image deployment/${deployment} \
            ${deployment}=${image} \
            -n ${namespace}

        kubectl rollout status deployment/${deployment} \
            -n ${namespace} \
            --timeout=5m
    """
}

// Usage in Jenkinsfile
@Library('shared-library') _

pipeline {
    stages {
        stage('Deploy') {
            steps {
                deployToKubernetes(
                    namespace: 'production',
                    deployment: 'web-app',
                    image: "${IMAGE_NAME}:${GIT_COMMIT_SHORT}"
                )
            }
        }
    }
}

GitLab CI

GitLab CI

yaml
undefined
yaml
undefined

.gitlab-ci.yml

.gitlab-ci.yml

stages:
  • build
  • test
  • security
  • deploy
variables: DOCKER_DRIVER: overlay2 DOCKER_TLS_CERTDIR: "/certs" IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
default: image: node:20-alpine cache: key: ${CI_COMMIT_REF_SLUG} paths: - node_modules/ - .npm/
build: stage: build script: - npm ci --cache .npm --prefer-offline - npm run build artifacts: paths: - dist/ expire_in: 1 week
test:unit: stage: test needs: [build] script: - npm ci --cache .npm --prefer-offline - npm run test:unit -- --coverage coverage: '/All files[^|]|[^|]\s+([\d.]+)/' artifacts: reports: junit: test-results/unit/*.xml coverage_report: coverage_format: cobertura path: coverage/cobertura-coverage.xml
test:integration: stage: test needs: [build] services: - postgres:15 - redis:7 variables: POSTGRES_DB: testdb POSTGRES_USER: testuser POSTGRES_PASSWORD: testpass script: - npm ci --cache .npm --prefer-offline - npm run test:integration artifacts: reports: junit: test-results/integration/*.xml
security:sast: stage: security image: returntocorp/semgrep script: - semgrep --config=auto --json --output=sast-report.json . artifacts: reports: sast: sast-report.json
security:dependency: stage: security script: - npm audit --audit-level=high allow_failure: true
docker:build: stage: build image: docker:24 services: - docker:24-dind before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY script: - docker build -t $IMAGE_TAG . - docker push $IMAGE_TAG only: - main - tags
deploy:staging: stage: deploy image: bitnami/kubectl:latest environment: name: staging url: https://staging.example.com script: - kubectl config use-context $KUBE_CONTEXT_STAGING - kubectl set image deployment/web-app web-app=$IMAGE_TAG -n staging - kubectl rollout status deployment/web-app -n staging --timeout=5m only: - main
deploy:production: stage: deploy image: bitnami/kubectl:latest environment: name: production url: https://example.com script: - kubectl config use-context $KUBE_CONTEXT_PRODUCTION - kubectl set image deployment/web-app web-app=$IMAGE_TAG -n production - kubectl rollout status deployment/web-app -n production --timeout=5m when: manual only: - tags
undefined
stages:
  • build
  • test
  • security
  • deploy
variables: DOCKER_DRIVER: overlay2 DOCKER_TLS_CERTDIR: "/certs" IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
default: image: node:20-alpine cache: key: ${CI_COMMIT_REF_SLUG} paths: - node_modules/ - .npm/
build: stage: build script: - npm ci --cache .npm --prefer-offline - npm run build artifacts: paths: - dist/ expire_in: 1 week
test:unit: stage: test needs: [build] script: - npm ci --cache .npm --prefer-offline - npm run test:unit -- --coverage coverage: '/All files[^|]|[^|]\s+([\d.]+)/' artifacts: reports: junit: test-results/unit/*.xml coverage_report: coverage_format: cobertura path: coverage/cobertura-coverage.xml
test:integration: stage: test needs: [build] services: - postgres:15 - redis:7 variables: POSTGRES_DB: testdb POSTGRES_USER: testuser POSTGRES_PASSWORD: testpass script: - npm ci --cache .npm --prefer-offline - npm run test:integration artifacts: reports: junit: test-results/integration/*.xml
security:sast: stage: security image: returntocorp/semgrep script: - semgrep --config=auto --json --output=sast-report.json . artifacts: reports: sast: sast-report.json
security:dependency: stage: security script: - npm audit --audit-level=high allow_failure: true
docker:build: stage: build image: docker:24 services: - docker:24-dind before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY script: - docker build -t $IMAGE_TAG . - docker push $IMAGE_TAG only: - main - tags
deploy:staging: stage: deploy image: bitnami/kubectl:latest environment: name: staging url: https://staging.example.com script: - kubectl config use-context $KUBE_CONTEXT_STAGING - kubectl set image deployment/web-app web-app=$IMAGE_TAG -n staging - kubectl rollout status deployment/web-app -n staging --timeout=5m only: - main
deploy:production: stage: deploy image: bitnami/kubectl:latest environment: name: production url: https://example.com script: - kubectl config use-context $KUBE_CONTEXT_PRODUCTION - kubectl set image deployment/web-app web-app=$IMAGE_TAG -n production - kubectl rollout status deployment/web-app -n production --timeout=5m when: manual only: - tags
undefined

Deployment Strategies

部署策略

Blue-Green Deployment

蓝绿部署

yaml
undefined
yaml
undefined

GitHub Actions

GitHub Actions

  • name: Blue-Green Deployment run: |

    Deploy to green environment

    kubectl apply -f k8s/green/ kubectl rollout status deployment/app-green -n production

    Run smoke tests

    npm run test:smoke -- --env=green

    Switch traffic to green

    kubectl patch service app -n production -p '{"spec":{"selector":{"version":"green"}}}'

    Keep blue for rollback

    echo "Blue environment kept for rollback"
undefined
  • name: Blue-Green Deployment run: |

    Deploy to green environment

    kubectl apply -f k8s/green/ kubectl rollout status deployment/app-green -n production

    Run smoke tests

    npm run test:smoke -- --env=green

    Switch traffic to green

    kubectl patch service app -n production -p '{"spec":{"selector":{"version":"green"}}}'

    Keep blue for rollback

    echo "Blue environment kept for rollback"
undefined

Canary Deployment

金丝雀部署

yaml
undefined
yaml
undefined

Deploy canary (10% traffic)

Deploy canary (10% traffic)

  • name: Deploy Canary run: | kubectl apply -f k8s/canary/ kubectl set image deployment/app-canary app=$IMAGE_TAG -n production

    Monitor metrics

    sleep 300

    Check error rate

    ERROR_RATE=$(curl -s "$PROMETHEUS_URL/api/v1/query?query=error_rate" | jq -r '.data.result[0].value[1]')
    if (( $(echo "$ERROR_RATE < 0.01" | bc -l) )); then # Promote canary to stable kubectl set image deployment/app-stable app=$IMAGE_TAG -n production kubectl scale deployment/app-canary --replicas=0 -n production else # Rollback canary kubectl scale deployment/app-canary --replicas=0 -n production exit 1 fi
undefined
  • name: Deploy Canary run: | kubectl apply -f k8s/canary/ kubectl set image deployment/app-canary app=$IMAGE_TAG -n production

    Monitor metrics

    sleep 300

    Check error rate

    ERROR_RATE=$(curl -s "$PROMETHEUS_URL/api/v1/query?query=error_rate" | jq -r '.data.result[0].value[1]')
    if (( $(echo "$ERROR_RATE < 0.01" | bc -l) )); then # Promote canary to stable kubectl set image deployment/app-stable app=$IMAGE_TAG -n production kubectl scale deployment/app-canary --replicas=0 -n production else # Rollback canary kubectl scale deployment/app-canary --replicas=0 -n production exit 1 fi
undefined

Rolling Deployment

滚动部署

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app
spec:
  replicas: 10
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 2        # Max 2 pods above desired count
      maxUnavailable: 1  # Max 1 pod unavailable during update
  template:
    spec:
      containers:
      - name: app
        image: myapp:v2
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app
spec:
  replicas: 10
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 2        # Max 2 pods above desired count
      maxUnavailable: 1  # Max 1 pod unavailable during update
  template:
    spec:
      containers:
      - name: app
        image: myapp:v2
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5

Best Practices

最佳实践

Pipeline Design

流水线设计

  • Keep pipelines fast (< 10 minutes for CI)
  • Fail fast on errors
  • Run tests in parallel
  • Cache dependencies
  • Use matrix builds for multiple versions
  • Separate CI and CD pipelines
  • Make pipelines idempotent
  • 保持流水线快速(CI阶段控制在10分钟内)
  • 错误发生时快速失败
  • 并行执行测试
  • 缓存依赖
  • 为多版本使用矩阵构建
  • 分离CI与CD流水线
  • 确保流水线幂等性

Security

安全

  • Scan dependencies for vulnerabilities
  • Scan container images
  • Use least privilege for credentials
  • Rotate secrets regularly
  • Sign commits and artifacts
  • Use private registries
  • Implement SBOM generation
  • 扫描依赖漏洞
  • 扫描容器镜像
  • 为凭证使用最小权限原则
  • 定期轮换密钥
  • 对提交与制品进行签名
  • 使用私有镜像仓库
  • 实现SBOM生成

Artifact Management

制品管理

  • Use semantic versioning
  • Tag images with git SHA
  • Store artifacts in registries
  • Implement retention policies
  • Generate build manifests
  • Track provenance
  • 使用语义化版本
  • 用Git SHA标记镜像
  • 在仓库中存储制品
  • 实现保留策略
  • 生成构建清单
  • 追踪溯源

Monitoring & Observability

监控与可观测性

  • Track build success rate
  • Monitor pipeline duration
  • Alert on failures
  • Log all deployments
  • Track deployment frequency
  • Measure lead time and MTTR
  • 追踪构建成功率
  • 监控流水线时长
  • 失败时触发告警
  • 记录所有部署操作
  • 追踪部署频率
  • 衡量交付周期与平均恢复时间

Anti-Patterns to Avoid

需避免的反模式

No automated tests: Deployments without tests are risky ❌ Manual deployments: Automate all deployments ❌ Shared credentials: Use role-based access ❌ No rollback strategy: Always have a rollback plan ❌ Long-running pipelines: Keep pipelines fast ❌ Environment drift: Use IaC for all environments ❌ No monitoring: Track deployment health ❌ Direct production access: Deploy through pipelines only
无自动化测试:无测试的部署风险极高 ❌ 手动部署:自动化所有部署流程 ❌ 共享凭证:使用基于角色的访问控制 ❌ 无回滚策略:始终准备回滚方案 ❌ 长时运行流水线:保持流水线快速 ❌ 环境漂移:对所有环境使用基础设施即代码(IaC) ❌ 无监控:追踪部署健康状态 ❌ 直接访问生产环境:仅通过流水线进行部署

Resources

资源