0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Hybrid License System Day 23: CI/CDパイプライン

Last updated at Posted at 2025-12-22

🎄 科学と神々株式会社 アドベントカレンダー 2025

Hybrid License System Day 23: CI/CDパイプライン

統合・デプロイ編 (3/5)


📖 はじめに

Day 23では、CI/CDパイプラインを学びます。GitHub Actions設定、自動テスト実行、Dockerイメージビルド、デプロイ自動化を実装しましょう。


🔄 CI/CDの概要

Continuous Integration (CI)

開発者がコードをプッシュ
    ↓
自動テスト実行
    ↓
コード品質チェック
    ↓
ビルド検証
    ↓
統合成功 ✅ / 失敗 ❌

Continuous Deployment (CD)

CI成功
    ↓
Dockerイメージビルド
    ↓
コンテナレジストリにプッシュ
    ↓
ステージング環境デプロイ
    ↓
自動E2Eテスト
    ↓
本番環境デプロイ(手動承認)

🚀 GitHub Actionsワークフロー

.github/workflows/ci.yml

name: CI Pipeline

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

env:
  NODE_VERSION: '20'
  DOCKER_REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  # ========== テストジョブ ==========
  test:
    name: Run Tests
    runs-on: ubuntu-latest

    strategy:
      matrix:
        service: [api-gateway, auth-service, admin-service]

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
          cache-dependency-path: ${{ matrix.service }}/package-lock.json

      - name: Install dependencies
        working-directory: ./${{ matrix.service }}
        run: npm ci

      - name: Run linter
        working-directory: ./${{ matrix.service }}
        run: npm run lint || true

      - name: Run unit tests
        working-directory: ./${{ matrix.service }}
        run: npm run test

      - name: Run integration tests
        working-directory: ./${{ matrix.service }}
        run: npm run test:integration

      - name: Generate coverage report
        working-directory: ./${{ matrix.service }}
        run: npm run test:coverage

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3
        with:
          files: ./${{ matrix.service }}/coverage/lcov.info
          flags: ${{ matrix.service }}

  # ========== セキュリティスキャン ==========
  security:
    name: Security Scan
    runs-on: ubuntu-latest
    needs: test

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Run npm audit
        run: |
          cd api-gateway && npm audit --audit-level=high || true
          cd ../auth-service && npm audit --audit-level=high || true
          cd ../admin-service && npm audit --audit-level=high || true

      - name: Run Snyk security scan
        uses: snyk/actions/node@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
        with:
          command: test

  # ========== Dockerビルド ==========
  build:
    name: Build Docker Images
    runs-on: ubuntu-latest
    needs: [test, security]

    strategy:
      matrix:
        service: [api-gateway, auth-service, admin-service]

    steps:
      - name: Checkout code
        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.DOCKER_REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}-${{ matrix.service }}
          tags: |
            type=ref,event=branch
            type=ref,event=pr
            type=semver,pattern={{version}}
            type=sha

      - name: Build and push Docker image
        uses: docker/build-push-action@v5
        with:
          context: ./${{ matrix.service }}
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=registry,ref=${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}-${{ matrix.service }}:buildcache
          cache-to: type=registry,ref=${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}-${{ matrix.service }}:buildcache,mode=max

  # ========== E2Eテスト ==========
  e2e:
    name: E2E Tests
    runs-on: ubuntu-latest
    needs: build
    if: github.event_name != 'pull_request'

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Start services with Docker Compose
        run: docker-compose -f docker-compose.ci.yml up -d

      - name: Wait for services to be ready
        run: |
          timeout 60 bash -c 'until curl -f http://localhost:3000/health; do sleep 2; done'

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}

      - name: Install Playwright
        run: |
          npm install -D @playwright/test
          npx playwright install --with-deps

      - name: Run E2E tests
        run: npm run test:e2e

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: playwright-report
          path: playwright-report/

      - name: Cleanup
        if: always()
        run: docker-compose -f docker-compose.ci.yml down

🚢 デプロイワークフロー

.github/workflows/deploy.yml

name: Deploy to Production

on:
  push:
    tags:
      - 'v*.*.*'

env:
  DOCKER_REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}
  DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}

jobs:
  # ========== デプロイ準備 ==========
  prepare:
    name: Prepare Deployment
    runs-on: ubuntu-latest

    outputs:
      version: ${{ steps.get_version.outputs.version }}

    steps:
      - name: Get version from tag
        id: get_version
        run: echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT

      - name: Create GitHub Release
        uses: actions/create-release@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag_name: ${{ github.ref }}
          release_name: Release ${{ github.ref }}
          draft: false
          prerelease: false

  # ========== ステージング環境デプロイ ==========
  deploy-staging:
    name: Deploy to Staging
    runs-on: ubuntu-latest
    needs: prepare
    environment:
      name: staging
      url: https://staging.license-system.example.com

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Deploy to staging
        uses: appleboy/ssh-action@v1.0.0
        with:
          host: ${{ secrets.STAGING_HOST }}
          username: ${{ secrets.DEPLOY_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /opt/license-system
            docker-compose pull
            docker-compose up -d
            docker-compose exec -T api-gateway npm run health-check

      - name: Run smoke tests
        run: |
          curl -f https://staging.license-system.example.com/health
          curl -f https://staging.license-system.example.com/api/v1/health

  # ========== 本番環境デプロイ(手動承認) ==========
  deploy-production:
    name: Deploy to Production
    runs-on: ubuntu-latest
    needs: deploy-staging
    environment:
      name: production
      url: https://license-system.example.com

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Deploy to production
        uses: appleboy/ssh-action@v1.0.0
        with:
          host: ${{ secrets.PRODUCTION_HOST }}
          username: ${{ secrets.DEPLOY_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /opt/license-system

            # Blue-Green Deployment
            docker-compose -f docker-compose.green.yml pull
            docker-compose -f docker-compose.green.yml up -d

            # ヘルスチェック
            sleep 10
            curl -f http://localhost:3000/health

            # トラフィック切り替え
            docker-compose -f docker-compose.blue.yml down
            mv docker-compose.green.yml docker-compose.blue.yml

      - name: Notify deployment success
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          text: 'Deployment to production successful!'
          webhook_url: ${{ secrets.SLACK_WEBHOOK }}

🔧 Dockerfile最適化

マルチステージビルド

# api-gateway/Dockerfile
# ========== ビルドステージ ==========
FROM node:20-alpine AS builder

WORKDIR /app

# 依存関係のみ先にインストール(レイヤーキャッシュ活用)
COPY package*.json ./
RUN npm ci --only=production

# ソースコピー
COPY . .

# ========== 本番ステージ ==========
FROM node:20-alpine

# セキュリティ: 非rootユーザー作成
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

WORKDIR /app

# ビルドステージから成果物コピー
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --chown=nodejs:nodejs . .

# ユーザー切り替え
USER nodejs

# ポート公開
EXPOSE 3000

# ヘルスチェック
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"

# 起動コマンド
CMD ["node", "src/index.js"]

📦 デプロイ戦略

Blue-Green Deployment

# docker-compose.blue.yml(現行環境)
version: '3.8'

services:
  api-gateway-blue:
    image: ghcr.io/yourorg/license-system-api-gateway:v1.0.0
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
# docker-compose.green.yml(新環境)
version: '3.8'

services:
  api-gateway-green:
    image: ghcr.io/yourorg/license-system-api-gateway:v1.1.0
    ports:
      - "3001:3000"
    environment:
      - NODE_ENV=production

Canary Deployment

# nginx.conf - トラフィック分散(90% Blue, 10% Green)
upstream backend {
    server api-gateway-blue:3000 weight=9;
    server api-gateway-green:3000 weight=1;
}

server {
    listen 80;

    location / {
        proxy_pass http://backend;
    }
}

🔔 通知設定

Slack通知

# .github/workflows/notify.yml
- name: Notify on failure
  if: failure()
  uses: 8398a7/action-slack@v3
  with:
    status: ${{ job.status }}
    text: |
      Deployment failed!
      Commit: ${{ github.sha }}
      Author: ${{ github.actor }}
    webhook_url: ${{ secrets.SLACK_WEBHOOK }}
    fields: repo,message,commit,author,action,eventName,ref,workflow

📊 デプロイメトリクス

デプロイ頻度測定

- name: Record deployment
  run: |
    echo "Deployment at $(date -u +%Y-%m-%dT%H:%M:%SZ)" >> deployments.log
    git add deployments.log
    git commit -m "Record deployment"
    git push

MTTR (Mean Time To Recovery) 追跡

- name: Calculate MTTR
  if: failure()
  run: |
    LAST_SUCCESS=$(git log --grep="Deployment successful" -1 --format=%ct)
    CURRENT_TIME=$(date +%s)
    MTTR=$((CURRENT_TIME - LAST_SUCCESS))
    echo "MTTR: $MTTR seconds" >> $GITHUB_STEP_SUMMARY

🎯 次のステップ

Day 24では、Kubernetes対応を学びます。Kubernetesマニフェスト作成、Service/Deployment設定、ConfigMap/Secret管理、Ingress設定について詳しく解説します。


🔗 関連リンク


次回予告: Day 24では、K8s Deployment設計とHPA(Horizontal Pod Autoscaler)を詳しく解説します!


Copyright © 2025 Gods & Golem, Inc. All rights reserved.

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?