Chuyển đến nội dung chính

第 21 課:微服務的 CI/CD 管道

用於多服務的 CI/CD 架構、每個服務的管道、建置 → 測試 → 掃描 → 部署流程、容器映像建置和推送、自動化測試策略(單元、整合、合約、E2E)、monorepo 與 polyrepo CI/CD。

🏗️ 建築 — 第 21 課 第 21 課:微服務的 CI/CD 管道

雲端原生微服務架構

第 7 部分:CI/CD 和部署策略

亞洲開發網

第 21 課:微服務的 CI/CD 管道

簡介

只有當您能夠快速、安全地部署時,微服務才會帶來價值。如果沒有良好的 CI/CD,微服務將成為比單體更大的負擔-必須手動部署數十個服務,每次部署都是一個危險的事件。

微服務中 CI/CD 的目標:每個團隊自信且獨立地每天多次部署其服務。


1. CI/CD 架構概述

1.1 每個服務的管道

每個服務都有自己的管道,由程式碼變更觸發:

Microservices CI/CD Model:

team-order pushes to order-service repo
       ↓
GitHub Actions (order-service pipeline)
       ↓
Build → Test → Scan → Push Image → Deploy Staging → Deploy Prod

team-payment pushes to payment-service repo (cùng lúc)
       ↓
GitHub Actions (payment-service pipeline)
       ↓
Build → Test → Scan → Push Image → Deploy Staging → Deploy Prod

→ Hai pipeline chạy song song, độc lập hoàn toàn

1.2 管道階段

┌─────────────────────────────────────────────────────────────┐
│                       CI Pipeline                           │
│                                                             │
│  ┌─────────┐  ┌────────┐  ┌────────┐  ┌─────────────────┐ │
│  │  Build  │→ │  Test  │→ │  Scan  │→ │  Build & Push   │ │
│  │         │  │        │  │        │  │  Container Image│ │
│  └─────────┘  └────────┘  └────────┘  └────────┬────────┘ │
└──────────────────────────────────────────────── ┼ ─────────┘
                                                  │
┌──────────────────────────────────────────────── ┼ ─────────┐
│                       CD Pipeline               │          │
│                                                 ▼          │
│  ┌────────────────┐  ┌─────────────────────────────────┐  │
│  │ Deploy Staging │→ │     Integration Tests / E2E     │  │
│  └────────────────┘  └──────────────┬──────────────────┘  │
│                                     │ (pass)               │
│                       ┌─────────────▼──────────────┐       │
│                       │  Manual Approval (Prod)     │       │
│                       └─────────────┬──────────────┘       │
│                                     ▼                       │
│                       ┌─────────────────────────┐          │
│                       │   Deploy Production      │          │
│                       │   (Canary → Full)        │          │
│                       └─────────────────────────┘          │
└─────────────────────────────────────────────────────────────┘

2. CI 管道 — GitHub Actions

2.1 Java服務的完整Pipeline

# .github/workflows/ci.yml
name: CI Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

env:
  REGISTRY: registry.example.com
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_DB: test_db
          POSTGRES_PASSWORD: test
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-retries 5
      redis:
        image: redis:7
        options: --health-cmd "redis-cli ping" --health-retries 5

    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'
          cache: gradle

      - name: Run tests
        run: ./gradlew test integrationTest --no-daemon
        env:
          SPRING_DATASOURCE_URL: jdbc:postgresql://localhost:5432/test_db
          SPRING_REDIS_HOST: localhost

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-results
          path: build/reports/tests/

      - name: Code coverage check
        run: ./gradlew jacocoTestCoverageVerification --no-daemon
        # Fail nếu coverage < 80%

  security-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: SAST — SonarQube
        uses: SonarSource/sonarcloud-github-action@master
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

      - name: Dependency vulnerability scan
        run: ./gradlew dependencyCheckAnalyze --no-daemon
        # OWASP Dependency-Check, fail on CVSS >= 7

  build-image:
    needs: [build-and-test, security-scan]
    runs-on: ubuntu-latest
    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}
      image-digest: ${{ steps.build.outputs.digest }}

    steps:
      - uses: actions/checkout@v4

      - name: Generate image metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=sha,prefix=sha-
            type=ref,event=branch
            type=semver,pattern={{version}}

      - name: Build and push image
        id: build
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          provenance: true   # SLSA provenance
          sbom: true         # Software Bill of Materials

      - name: Container image vulnerability scan (Trivy)
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
          format: 'sarif'
          exit-code: '1'
          severity: 'CRITICAL,HIGH'

2.2 使用 Pact 進行合約測試

消費者驅動的合約測試確保服務之間的 API 相容性:

// Order Service (Consumer) định nghĩa contract
@ExtendWith(PactConsumerTestExt.class)
@PactTestFor(providerName = "payment-service")
class PaymentServiceContractTest {

    @Pact(consumer = "order-service")
    RequestResponsePact createPact(PactDslWithProvider builder) {
        return builder
            .given("payment method is valid")
            .uponReceiving("a charge request")
                .path("/charges")
                .method("POST")
                .body(new PactDslJsonBody()
                    .stringType("orderId")
                    .numberType("amount")
                    .stringValue("currency", "VND"))
            .willRespondWith()
                .status(201)
                .body(new PactDslJsonBody()
                    .stringType("chargeId")
                    .stringValue("status", "SUCCESS"))
            .toPact();
    }

    @Test
    void testChargeRequest(MockServer mockServer) {
        PaymentClient client = new PaymentClient(mockServer.getUrl());
        ChargeResponse response = client.charge(new ChargeRequest("O-001", 500000, "VND"));
        assertThat(response.getStatus()).isEqualTo("SUCCESS");
    }
}
# CI: verify Payment Service còn match contract
- name: Publish contract
  run: ./gradlew pactPublish
  env:
    PACT_BROKER_URL: https://pact-broker.internal
    PACT_BROKER_TOKEN: ${{ secrets.PACT_TOKEN }}

# Payment Service CI:
- name: Verify consumer contracts
  run: ./gradlew pactVerify

3. CD 管道 — 部署到 Kubernetes

3.1 更新 GitOps 儲存庫中的圖片標籤

# Sau khi build image, CD pipeline cập nhật manifest repo
- name: Update K8s manifest
  run: |
    git clone https://github.com/myorg/k8s-manifests.git
    cd k8s-manifests
    
    # Update image tag
    yq e '.spec.template.spec.containers[0].image = "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}"' \
      -i services/order-service/overlays/staging/deployment.yaml
    
    git config user.email "[email protected]"
    git config user.name "CI Bot"
    git add .
    git commit -m "chore: update order-service image to ${{ steps.meta.outputs.version }}"
    git push

# ArgoCD auto-sync sẽ detect change và deploy

3.2 部署後的冒煙測試

- name: Wait for deploy and run smoke tests
  run: |
    # Đợi rollout hoàn thành
    kubectl rollout status deployment/order-service \
      -n services-staging --timeout=5m

    # Chạy smoke tests
    ./gradlew smokeTest \
      --tests "com.example.SmokeTest" \
      -Dbase.url=https://staging-api.example.com
// Smoke Test — kiểm tra luồng cơ bản hoạt động
@SpringBootTest
@Tag("smoke")
class SmokeTest {
    @Test
    void healthEndpointIsUp() {
        given()
            .get(baseUrl + "/actuator/health")
            .then()
            .statusCode(200)
            .body("status", equalTo("UP"));
    }

    @Test
    void canCreateAndRetrieveOrder() {
        String orderId = given()
            .contentType(ContentType.JSON)
            .body("""{"customerId":"C-001","items":[{"productId":"P-1","qty":1}]}""")
            .post(baseUrl + "/api/orders")
            .then().statusCode(201)
            .extract().path("id");

        given()
            .get(baseUrl + "/api/orders/" + orderId)
            .then()
            .statusCode(200)
            .body("id", equalTo(orderId));
    }
}

4. Monorepo 與 Polyrepo

4.1 Polyrepo(每個服務單獨的儲存庫)

github.com/myorg/order-service      (1 repo)
github.com/myorg/payment-service    (1 repo)
github.com/myorg/inventory-service  (1 repo)

優點:

  • 團隊完全獨立
  • 更小的程式碼庫,更快的克隆/結帳
  • 每個服務的存取控制

缺點:

  • 難以重構共享程式碼
  • 合約測試更加複雜
  • 很難找到“誰改變了什麼毀了我?”

4.2 Monorepo

github.com/myorg/platform/
  services/
    order-service/
    payment-service/
    inventory-service/
  shared/
    proto/          ← Shared protobuf definitions
    common/         ← Common utilities
  tools/

優點:

  • 跨服務的原子提交
  • 易於重構共享程式碼
  • 在一個地方可以找到整個程式碼庫

缺點:

  • CI 必須智慧:僅更改建置/測試服務
  • 需要 Nx、Turborepo、Bazel 等工具來管理

4.3 Monorepo CI 與 NX(受影響偵測)

# .github/workflows/ci.yml (monorepo)
- name: Detect affected services
  id: affected
  run: |
    AFFECTED=$(npx nx affected:apps --plain --base=origin/main)
    echo "services=$AFFECTED" >> $GITHUB_OUTPUT

- name: Build and test affected services
  run: |
    for service in ${{ steps.affected.outputs.services }}; do
      npx nx test $service
      npx nx build $service
      npx nx docker-build $service
    done

5. 測試策略金字塔

                    /\
                   /  \  E2E Tests
                  / 10%\  (Slow, expensive, validates full flows)
                 /──────\
                /        \ Integration Tests
               /   30%    \ (Test service boundaries, DB, message queues)
              /────────────\
             /              \ Contract Tests
            /     20%        \ (API compatibility between services)
           /──────────────────\
          /                    \ Unit Tests
         /        40%           \ (Fast, isolated, business logic)
        /────────────────────────\

單元測試:測試業務邏輯,無外部依賴項,每次測試運行時間 < 100 毫秒。

整合測試:使用真實資料庫/快取(測試容器)、測試儲存庫、訊息處理程序測試服務。

合約測試:契約-消費者定義合同,提供者驗證。

E2E 測試:Playwright/Cypress 用於 UI,REST Assured 用於 API — 僅冒煙測試是最重要的流程。


6. 最佳實踐

快速失敗:

jobs:
  lint:          # Nhanh nhất, chạy đầu tiên
  unit-test:     # Song song với lint
  integration:   # Sau unit tests
  security-scan: # Song song với integration
  build-image:   # Sau tất cả tests pass

不可變圖像:

❌ Sai: Deploy script pull latest image trên server
✅ Đúng: Mỗi commit → unique image tag (SHA-based)
         Deploy = thay đổi image tag trong manifest

語意版本控制:

feat: → minor version bump (1.2.0 → 1.3.0)
fix: → patch version bump (1.2.0 → 1.2.1)
BREAKING CHANGE → major version bump (1.2.0 → 2.0.0)

總結

概念目的
每項服務的管道獨立部署,互不遮擋
合約測試確保API相容性
不可變映像可重複、可回滾的部署
煙霧測試部署後確認服務正常運作
測試金字塔速度與信心之間的平衡
Monorepo nx 受影響僅測試/建置服務變更

下一篇:GitOps 與 ArgoCD