
はじめに
マイクロサービスは、迅速かつ安全にデプロイできる場合にのみ価値をもたらします。優れた 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