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

レッスン 21: マイクロサービス用の CI/CD パイプライン

マルチサービスの CI/CD アーキテクチャ、サービスごとのパイプライン、ビルド → テスト → スキャン → デプロイ フロー、コンテナ イメージのビルドとプッシュ、自動テスト戦略 (ユニット、統合、コントラクト、E2E)、モノリポジトリとポリリポジトリの CI/CD。

🏗️ アーキテクチャ — レッスン 21 レッスン 21: マイクロサービス用の CI/CD パイプライン

クラウドネイティブのマイクロサービスアーキテクチャ

パート 7: CI/CD および導入戦略

xdev.asia

レッスン 21: マイクロサービス用の CI/CD パイプライン

はじめに

マイクロサービスは、迅速かつ安全にデプロイできる場合にのみ価値をもたらします。優れた CI/CD がなければ、マイクロサービスはモノリスよりも負担になります。数十のサービスを手動でデプロイする必要があり、それぞれのデプロイメントは危険なイベントです。

マイクロサービスにおける CI/CD の目標: 各チームは自信を持って独立して、1 日に複数回、サービスをデプロイします。


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 アクション

2.1 Java サービスの完全なパイプライン

# .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. モノレポとポリリポ

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 モノレポ

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

利点:

  • サービス全体にわたるアトミックコミット
  • 共有コードのリファクタリングが簡単
  • コードベース全体を 1 か所で検索できる

短所:

  • CI はスマートでなければなりません: ビルド/テスト サービスのみが変更されます
  • 管理するにはNx、Turborepo、Bazelなどのツールが必要です

4.3 NX を使用した Monorepo CI (影響を受ける検出)

# .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 ミリ秒未満で実行されます。

統合テスト: 実際の DB/キャッシュ (TestContainers)、テスト リポジトリ、メッセージ ハンドラーを使用してサービスをテストします。

契約テスト: 協定 — 消費者が契約を定義し、プロバイダーが検証します。

E2E テスト: UI については Playwright/Cypress、API については REST 保証 — 最も重要なフローのみスモーク テストを行います。


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 が影響を受けるテスト/ビルド サービスのみが変更されます

次の記事: ArgoCD を使用した GitOps