Skilore

Docker CI/CD

GitHub Actionsを中心に、Dockerイメージのビルド自動化・テスト・レジストリプッシュ・デプロイパイプラインを構築する。

83 分で読めます41,369 文字

Docker CI/CD

GitHub Actionsを中心に、Dockerイメージのビルド自動化・テスト・レジストリプッシュ・デプロイパイプラインを構築する。


この章で学ぶこと

  1. GitHub ActionsによるDockerイメージの自動ビルド・プッシュのワークフロー設計を理解する
  2. マルチプラットフォームビルドとキャッシュ戦略による高速化手法を習得する
  3. ステージングから本番までのデプロイパイプラインを構築できるようになる
  4. セキュリティスキャンとイメージ署名をCI/CDに統合する手法を理解する
  5. GitLab CI / CircleCIなど他のCI/CDツールでのDocker連携パターンを把握する

前提知識

このガイドを読む前に、以下の知識があると理解が深まります:

  • 基本的なプログラミングの知識
  • 関連する基礎概念の理解
  • モニタリング の内容を理解していること

1. Docker CI/CDパイプラインの全体像

パイプラインアーキテクチャ

CodeBuildTestDeploy
Push───►Image───►Scan───►Release
Verify
│              │               │               │
     ▼              ▼               ▼               ▼
  git push     docker build    trivy scan     docker push
  PR作成       multi-stage     unit test      kubectl apply
  tag作成      layer cache     integration    docker compose

CI/CDパイプラインの原則

CI/CDパイプラインの5原則
1. 再現可能性 同じコミットから常に同じイメージを生成
2. 不変性 ビルド済みイメージは変更しない
3. 高速性 キャッシュとパラレル実行で最適化
4. 安全性 シークレット管理、スキャン、署名
5. 可観測性 ビルドログ、メトリクス、通知の統合

CI/CDツール比較表

ツール Docker連携 特徴 無料枠
GitHub Actions Docker公式Action GitHub統合、GHCR連携 2,000分/月
GitLab CI Docker-in-Docker 組み込みレジストリ 400分/月
CircleCI Docker Executor 高速、Docker Layer Cache 6,000分/月
AWS CodeBuild ECR連携 AWSネイティブ 100分/月
Jenkins Docker Plugin 自己ホスト、高カスタマイズ性 無制限(自己ホスト)

2. GitHub Actions基本構成

コード例1: 基本的なDockerビルド・プッシュ

# .github/workflows/docker-build.yml
name: Docker Build and Push
 
on:
  push:
    branches: [main, develop]
    tags: ["v*"]
  pull_request:
    branches: [main]
 
env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}
 
jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
 
    steps:
      # 1. チェックアウト
      - name: Checkout repository
        uses: actions/checkout@v4
 
      # 2. Docker Buildxのセットアップ
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
 
      # 3. レジストリへのログイン
      - name: Log in to Container Registry
        if: github.event_name != 'pull_request'
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
 
      # 4. メタデータの抽出(タグ、ラベル)
      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            # ブランチ名タグ
            type=ref,event=branch
            # PRナンバータグ
            type=ref,event=pr
            # セマンティックバージョニング
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=semver,pattern={{major}}
            # Git SHA(短縮)
            type=sha,prefix=sha-
            # latest(mainブランチのみ)
            type=raw,value=latest,enable={{is_default_branch}}
 
      # 5. ビルド & プッシュ
      - name: Build and push Docker image
        uses: docker/build-push-action@v5
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          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

タグ戦略のフロー

git push main
    └──► ghcr.io/user/app:main
         ghcr.io/user/app:sha-abc1234
         ghcr.io/user/app:latest

git push develop
    └──► ghcr.io/user/app:develop
         ghcr.io/user/app:sha-def5678

git tag v1.2.3
    └──► ghcr.io/user/app:1.2.3
         ghcr.io/user/app:1.2
         ghcr.io/user/app:1
         ghcr.io/user/app:sha-ghi9012
         ghcr.io/user/app:latest

Pull Request #42
    └──► ghcr.io/user/app:pr-42  (プッシュされない)

タグ戦略の比較

戦略 用途 特徴
セマンティックバージョン v1.2.3 本番リリース 人間が読みやすい
Git SHA sha-abc1234 全ビルド 完全な追跡可能性
ブランチ名 main, develop 開発・ステージング 自動更新される
タイムスタンプ 20240115-1030 CI/CD内部 時系列順序が明確
latest latest 開発用途のみ 本番で使用禁止

3. テスト統合

コード例2: テスト・セキュリティスキャン統合パイプライン

# .github/workflows/ci-pipeline.yml
name: CI Pipeline
 
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
 
jobs:
  # === ユニットテスト ===
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Run unit tests in Docker
        run: |
          docker compose -f docker-compose.test.yml run --rm \
            --build \
            test npm run test:ci
 
      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-results
          path: coverage/
 
  # === Lint & 静的解析 ===
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Lint Dockerfile
        uses: hadolint/hadolint-action@v3.1.0
        with:
          dockerfile: Dockerfile
          failure-threshold: warning
 
      - name: Lint docker-compose files
        run: |
          docker compose -f docker-compose.yml config -q
          docker compose -f docker-compose.prod.yml config -q
 
  # === イメージビルド ===
  build:
    needs: [test, lint]
    runs-on: ubuntu-latest
    outputs:
      image-digest: ${{ steps.build.outputs.digest }}
      image-tag: ${{ steps.meta.outputs.version }}
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
 
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
 
      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
 
      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/${{ github.repository }}
          tags: |
            type=sha,prefix=sha-
 
      - name: Build and push
        id: build
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          provenance: true
          sbom: true
 
  # === セキュリティスキャン ===
  security-scan:
    needs: [build]
    runs-on: ubuntu-latest
    permissions:
      security-events: write
    steps:
      - uses: actions/checkout@v4
 
      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ghcr.io/${{ github.repository }}:${{ github.sha }}
          format: "sarif"
          output: "trivy-results.sarif"
          severity: "CRITICAL,HIGH"
          exit-code: "1"  # CRITICAL/HIGH が見つかったら失敗
 
      - name: Upload Trivy scan results
        if: always()
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: "trivy-results.sarif"
 
  # === Dockerfile ベストプラクティスチェック ===
  dockerfile-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Run Dockle
        uses: erzz/dockle-action@v1
        with:
          image: ghcr.io/${{ github.repository }}:${{ github.sha }}
          exit-code: "1"
          exit-level: "WARN"
 
  # === 統合テスト ===
  integration-test:
    needs: [build]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Run integration tests
        env:
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker compose -f docker-compose.integration.yml up -d
 
          # ヘルスチェック待ち
          for i in $(seq 1 30); do
            if docker compose -f docker-compose.integration.yml exec -T api \
              wget -q --spider http://localhost:8080/health 2>/dev/null; then
              echo "Service is healthy"
              break
            fi
            echo "Waiting for services... ($i/30)"
            sleep 2
          done
 
          # テスト実行
          docker compose -f docker-compose.integration.yml run --rm \
            test npm run test:integration
 
          # クリーンアップ
          docker compose -f docker-compose.integration.yml down -v
# docker-compose.test.yml
version: "3.9"
 
services:
  test:
    build:
      context: .
      target: test  # テスト用ステージ
    volumes:
      - ./coverage:/app/coverage
    environment:
      NODE_ENV: test
      DATABASE_URL: postgres://test:test@db:5432/testdb
 
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: test
      POSTGRES_PASSWORD: test
      POSTGRES_DB: testdb
    tmpfs:
      - /var/lib/postgresql/data  # テストはメモリ上で高速実行
# docker-compose.integration.yml
version: "3.9"
 
services:
  api:
    image: ghcr.io/${GITHUB_REPOSITORY}:${IMAGE_TAG}
    environment:
      NODE_ENV: test
      DATABASE_URL: postgres://test:test@db:5432/testdb
      REDIS_URL: redis://redis:6379
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/health"]
      interval: 5s
      timeout: 3s
      retries: 10
 
  test:
    build:
      context: .
      target: test
    environment:
      API_URL: http://api:8080
      NODE_ENV: test
    depends_on:
      api:
        condition: service_healthy
 
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: test
      POSTGRES_PASSWORD: test
      POSTGRES_DB: testdb
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U test"]
      interval: 5s
      timeout: 3s
      retries: 5
    tmpfs:
      - /var/lib/postgresql/data
 
  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

4. キャッシュ戦略

コード例3: 高度なキャッシュ設定

# .github/workflows/cached-build.yml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
 
      # 方式1: GitHub Actions Cache(推奨)
      - name: Build with GHA cache
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository }}:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max
 
      # 方式2: レジストリキャッシュ
      # cache-from: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache
      # cache-to: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache,mode=max
 
      # 方式3: ローカルキャッシュ
      # cache-from: type=local,src=/tmp/.buildx-cache
      # cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max

キャッシュ方式の比較表

方式 速度 容量制限 CI間共有 設定の簡便さ コスト
GHA Cache 高速 10GB 同一リポ 最も簡単 無料
Registry Cache 中速 無制限 全環境 中程度 レジストリ料金
Local Cache 最速 ディスク依存 不可 簡単 無料
Inline Cache 中速 イメージ内 全環境 簡単 無料
S3 Cache 中速 無制限 全環境 やや複雑 S3料金

キャッシュの動作原理

初回ビルド(キャッシュなし)
┌──────────────────────────────────┐
│ Layer 1: FROM node:20-alpine     │  ← ダウンロード
│ Layer 2: COPY package*.json      │  ← 新規作成
│ Layer 3: RUN npm ci              │  ← 新規作成(遅い)
│ Layer 4: COPY . .                │  ← 新規作成
│ Layer 5: RUN npm run build       │  ← 新規作成
└──────────────────────────────────┘
合計: 3分

2回目ビルド(ソースコードのみ変更)
┌──────────────────────────────────┐
│ Layer 1: FROM node:20-alpine     │  ← キャッシュHIT
│ Layer 2: COPY package*.json      │  ← キャッシュHIT
│ Layer 3: RUN npm ci              │  ← キャッシュHIT ★高速
│ Layer 4: COPY . .                │  ← 再作成(変更検知)
│ Layer 5: RUN npm run build       │  ← 再作成
└──────────────────────────────────┘
合計: 30秒

Dockerfileのキャッシュ最適化

# === キャッシュを最大限活用するDockerfile ===
FROM node:20-alpine AS builder
 
WORKDIR /app
 
# 1. パッケージマネージャーのロックファイルだけ先にコピー
# → 依存関係が変わらない限りこのレイヤーはキャッシュされる
COPY package.json package-lock.json ./
 
# 2. 依存関係インストール(最も遅いステップ)
# → ロックファイルが変わった時だけ再実行
RUN --mount=type=cache,target=/root/.npm \
    npm ci
 
# 3. ソースコードをコピー(頻繁に変わる)
COPY tsconfig.json ./
COPY src/ ./src/
 
# 4. ビルド
RUN npm run build
 
# === 本番ステージ ===
FROM node:20-alpine AS production
 
WORKDIR /app
 
# 本番依存関係のみインストール
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci --only=production
 
# ビルド成果物をコピー
COPY --from=builder /app/dist ./dist
 
USER node
CMD ["node", "dist/server.js"]

BuildKit マウントキャッシュ

# BuildKit のキャッシュマウント(--mount=type=cache)を活用
# パッケージマネージャーのキャッシュをビルド間で共有
 
# Go
RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    go build -o /app/server ./cmd/server
 
# Python
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt
 
# Rust
RUN --mount=type=cache,target=/usr/local/cargo/registry \
    --mount=type=cache,target=/app/target \
    cargo build --release
 
# Java/Maven
RUN --mount=type=cache,target=/root/.m2 \
    mvn package -DskipTests
 
# Java/Gradle
RUN --mount=type=cache,target=/root/.gradle \
    gradle build -x test

5. デプロイパイプライン

コード例4: ステージング→本番デプロイ

# .github/workflows/deploy.yml
name: Deploy
 
on:
  push:
    tags: ["v*"]
 
env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}
 
jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    outputs:
      version: ${{ steps.version.outputs.version }}
      digest: ${{ steps.build.outputs.digest }}
    steps:
      - uses: actions/checkout@v4
 
      - name: Extract version
        id: version
        run: echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
 
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
 
      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
 
      - name: Build and push
        id: build
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max
          provenance: true
          sbom: true
 
  # === セキュリティスキャン ===
  security-scan:
    needs: [build]
    runs-on: ubuntu-latest
    steps:
      - name: Run Trivy
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build.outputs.version }}
          severity: "CRITICAL"
          exit-code: "1"
 
  # === ステージングデプロイ ===
  deploy-staging:
    needs: [build, security-scan]
    runs-on: ubuntu-latest
    environment:
      name: staging
      url: https://staging.example.com
    steps:
      - uses: actions/checkout@v4
 
      - name: Deploy to staging
        env:
          VERSION: ${{ needs.build.outputs.version }}
        run: |
          # SSH経由でデプロイ
          ssh -o StrictHostKeyChecking=no deploy@staging.example.com << EOF
            cd /opt/app
            export VERSION=${VERSION}
            docker compose pull
            docker compose up -d --remove-orphans
            docker compose exec -T api wget -q --spider http://localhost:8080/health
          EOF
 
      - name: Run smoke tests
        run: |
          sleep 10
          curl -f https://staging.example.com/health || exit 1
          curl -f https://staging.example.com/api/status || exit 1
 
      - name: Run E2E tests
        run: |
          docker run --rm \
            -e BASE_URL=https://staging.example.com \
            my-e2e-tests:latest \
            npm run test:e2e
 
  # === 本番デプロイ(手動承認後) ===
  deploy-production:
    needs: [deploy-staging]
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://www.example.com
    steps:
      - uses: actions/checkout@v4
 
      - name: Deploy to production
        env:
          VERSION: ${{ needs.build.outputs.version }}
        run: |
          ssh -o StrictHostKeyChecking=no deploy@prod.example.com << 'EOF'
            cd /opt/app
 
            # ローリングデプロイ
            export VERSION=${{ env.VERSION }}
            docker compose pull
            docker compose up -d --remove-orphans --scale api=3
 
            # ヘルスチェック確認
            for i in $(seq 1 30); do
              if docker compose exec -T api wget -q --spider http://localhost:8080/health; then
                echo "Health check passed"
                break
              fi
              echo "Waiting for health check... ($i/30)"
              sleep 2
            done
 
            # 古いイメージの削除
            docker image prune -af --filter "until=168h"
          EOF
 
      - name: Verify deployment
        run: |
          curl -f https://www.example.com/health
          curl -f https://www.example.com/api/status
 
      - name: Notify deployment
        if: success()
        uses: slackapi/slack-github-action@v1.24.0
        with:
          channel-id: "#deployments"
          slack-message: "Deployed v${{ needs.build.outputs.version }} to production"
        env:
          SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}

デプロイフロー

git tag v1.2.3 && git push --tags
    │
    ▼
Build────►Security────►Staging
& PushScanDeploy
(自動)
│
                                        Smoke Test
                                        E2E Test
                                             │
Production
Deploy
(手動承認)
│
                                        Health Check
                                        ローリング更新
                                        Slack通知

ロールバック戦略

# .github/workflows/rollback.yml
name: Rollback Production
 
on:
  workflow_dispatch:
    inputs:
      version:
        description: "Version to rollback to (e.g., 1.2.2)"
        required: true
 
jobs:
  rollback:
    runs-on: ubuntu-latest
    environment:
      name: production
    steps:
      - uses: actions/checkout@v4
 
      - name: Verify image exists
        run: |
          docker pull ghcr.io/${{ github.repository }}:${{ inputs.version }}
 
      - name: Rollback production
        run: |
          ssh deploy@prod.example.com << EOF
            cd /opt/app
            export VERSION=${{ inputs.version }}
            docker compose pull
            docker compose up -d --remove-orphans
 
            # ヘルスチェック
            for i in $(seq 1 30); do
              if docker compose exec -T api wget -q --spider http://localhost:8080/health; then
                echo "Rollback successful - v${{ inputs.version }}"
                break
              fi
              sleep 2
            done
          EOF
 
      - name: Notify rollback
        uses: slackapi/slack-github-action@v1.24.0
        with:
          channel-id: "#deployments"
          slack-message: "ROLLBACK: Production rolled back to v${{ inputs.version }}"
        env:
          SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}

6. マルチプラットフォームビルド

コード例5: ARM64 + AMD64 マルチプラットフォーム

# .github/workflows/multi-platform.yml
name: Multi-Platform Build
 
on:
  push:
    branches: [main]
 
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3
        with:
          platforms: linux/amd64,linux/arm64
 
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
 
      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
 
      - name: Build and push multi-platform
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          platforms: linux/amd64,linux/arm64
          tags: |
            ghcr.io/${{ github.repository }}:latest
            ghcr.io/${{ github.repository }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

プラットフォーム別ビルドのマトリックス戦略

# 高速化: プラットフォームごとに並列ビルドし、後でマニフェストを統合
jobs:
  build-platform:
    strategy:
      matrix:
        platform:
          - linux/amd64
          - linux/arm64
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
 
      - name: Build and push by digest
        id: build
        uses: docker/build-push-action@v5
        with:
          context: .
          platforms: ${{ matrix.platform }}
          outputs: type=image,name=ghcr.io/${{ github.repository }},push-by-digest=true,name-canonical=true,push=true
          cache-from: type=gha,scope=${{ matrix.platform }}
          cache-to: type=gha,scope=${{ matrix.platform }},mode=max
 
      - name: Export digest
        run: echo "${{ steps.build.outputs.digest }}" > /tmp/digest-${{ strategy.job-index }}
 
      - uses: actions/upload-artifact@v4
        with:
          name: digest-${{ strategy.job-index }}
          path: /tmp/digest-*
 
  # マニフェストリストの作成
  merge:
    needs: [build-platform]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
        with:
          pattern: digest-*
          merge-multiple: true
          path: /tmp/digests
 
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
 
      - name: Create manifest list
        run: |
          digests=$(cat /tmp/digests/digest-*)
          docker buildx imagetools create \
            -t ghcr.io/${{ github.repository }}:latest \
            $digests

7. Docker Compose によるローカルCI再現

コード例6: ローカルで CI パイプラインを再現

# docker-compose.ci.yml
version: "3.9"
 
services:
  lint:
    image: hadolint/hadolint:latest-alpine
    volumes:
      - ./Dockerfile:/Dockerfile:ro
    command: hadolint /Dockerfile
 
  test:
    build:
      context: .
      target: test
    command: npm run test:ci
    environment:
      NODE_ENV: test
      DATABASE_URL: postgres://ci:ci@db:5432/ci_test
    depends_on:
      db:
        condition: service_healthy
 
  security-scan:
    image: aquasec/trivy:latest
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - trivy-cache:/root/.cache/
    command: image --severity HIGH,CRITICAL my-app:test
 
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: ci
      POSTGRES_PASSWORD: ci
      POSTGRES_DB: ci_test
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ci"]
      interval: 5s
      timeout: 3s
      retries: 5
    tmpfs:
      - /var/lib/postgresql/data
 
volumes:
  trivy-cache:
# ローカルでCIパイプラインを実行
docker compose -f docker-compose.ci.yml run --rm lint
docker compose -f docker-compose.ci.yml run --rm test
docker compose -f docker-compose.ci.yml run --rm security-scan
docker compose -f docker-compose.ci.yml down -v

Makefile によるCI/CDタスク管理

# Makefile - CI/CDタスクの統一インターフェース
.PHONY: build test lint scan deploy-staging deploy-production
 
# 変数
IMAGE_NAME := ghcr.io/myorg/myapp
VERSION := $(shell git describe --tags --always)
 
# ビルド
build:
	docker build -t $(IMAGE_NAME):$(VERSION) -t $(IMAGE_NAME):latest .
 
# テスト
test:
	docker compose -f docker-compose.test.yml run --rm --build test
 
# Lint
lint:
	docker run --rm -v $(PWD)/Dockerfile:/Dockerfile \
		hadolint/hadolint:latest-alpine hadolint /Dockerfile
 
# セキュリティスキャン
scan:
	trivy image --severity HIGH,CRITICAL $(IMAGE_NAME):$(VERSION)
 
# 全CIステップ実行
ci: lint test build scan
 
# ステージングデプロイ
deploy-staging:
	VERSION=$(VERSION) docker compose -f docker-compose.staging.yml pull
	VERSION=$(VERSION) docker compose -f docker-compose.staging.yml up -d
 
# 本番デプロイ
deploy-production:
	@echo "Deploying $(VERSION) to production..."
	VERSION=$(VERSION) docker compose -f docker-compose.prod.yml pull
	VERSION=$(VERSION) docker compose -f docker-compose.prod.yml up -d --remove-orphans
 
# クリーンアップ
clean:
	docker compose -f docker-compose.test.yml down -v
	docker image prune -f

8. GitLab CI / CircleCI での Docker CI/CD

GitLab CI の Docker ビルド

# .gitlab-ci.yml
stages:
  - test
  - build
  - scan
  - deploy
 
variables:
  DOCKER_IMAGE: $CI_REGISTRY_IMAGE
  DOCKER_TAG: $CI_COMMIT_SHORT_SHA
 
# テスト
test:
  stage: test
  image: docker:24-dind
  services:
    - docker:24-dind
  script:
    - docker compose -f docker-compose.test.yml run --rm test
 
# ビルド & プッシュ
build:
  stage: build
  image: docker:24-dind
  services:
    - docker:24-dind
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - docker build -t $DOCKER_IMAGE:$DOCKER_TAG -t $DOCKER_IMAGE:latest .
    - docker push $DOCKER_IMAGE:$DOCKER_TAG
    - docker push $DOCKER_IMAGE:latest
 
# セキュリティスキャン
scan:
  stage: scan
  image: aquasec/trivy:latest
  script:
    - trivy image --severity CRITICAL,HIGH $DOCKER_IMAGE:$DOCKER_TAG
 
# ステージングデプロイ
deploy-staging:
  stage: deploy
  environment:
    name: staging
    url: https://staging.example.com
  script:
    - ssh deploy@staging.example.com "cd /opt/app && VERSION=$DOCKER_TAG docker compose up -d"
  only:
    - main
 
# 本番デプロイ(手動)
deploy-production:
  stage: deploy
  environment:
    name: production
    url: https://www.example.com
  script:
    - ssh deploy@prod.example.com "cd /opt/app && VERSION=$DOCKER_TAG docker compose up -d"
  when: manual
  only:
    - tags

CircleCI の Docker ビルド

# .circleci/config.yml
version: 2.1
 
orbs:
  docker: circleci/docker@2.4.0
 
executors:
  docker-executor:
    docker:
      - image: cimg/base:2024.01
 
jobs:
  build-and-push:
    executor: docker-executor
    steps:
      - checkout
      - setup_remote_docker:
          docker_layer_caching: true  # DLC(有料機能)
      - docker/check:
          registry: ghcr.io
          docker-username: GHCR_USER
          docker-password: GHCR_TOKEN
      - docker/build:
          image: $CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME
          registry: ghcr.io
          tag: ${CIRCLE_SHA1:0:8},latest
      - docker/push:
          image: $CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME
          registry: ghcr.io
          tag: ${CIRCLE_SHA1:0:8},latest
 
  security-scan:
    docker:
      - image: aquasec/trivy:latest
    steps:
      - run:
          name: Scan image
          command: |
            trivy image --severity CRITICAL,HIGH \
              ghcr.io/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME:${CIRCLE_SHA1:0:8}
 
workflows:
  build-deploy:
    jobs:
      - build-and-push:
          context: docker-credentials
      - security-scan:
          requires:
            - build-and-push

9. イメージ署名とサプライチェーンセキュリティ

Cosign によるイメージ署名

# GitHub Actions でのイメージ署名
- name: Install Cosign
  uses: sigstore/cosign-installer@v3
 
- name: Sign the image
  env:
    COSIGN_EXPERIMENTAL: "1"
  run: |
    cosign sign --yes \
      ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
 
- name: Verify the signature
  run: |
    cosign verify \
      --certificate-identity "https://github.com/${{ github.repository }}/.github/workflows/docker-build.yml@refs/heads/main" \
      --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
      ghcr.io/${{ github.repository }}:latest

SBOM(ソフトウェア部品表)の生成

# ビルド時にSBOMを自動生成
- name: Build with SBOM
  uses: docker/build-push-action@v5
  with:
    context: .
    push: true
    tags: ghcr.io/${{ github.repository }}:latest
    sbom: true        # BuildKit によるSBOM生成
    provenance: true  # SLSA Provenance の付与
 
# または Syft で明示的にSBOM生成
- name: Generate SBOM with Syft
  uses: anchore/sbom-action@v0
  with:
    image: ghcr.io/${{ github.repository }}:latest
    format: spdx-json
    output-file: sbom.spdx.json
 
- name: Upload SBOM
  uses: actions/upload-artifact@v4
  with:
    name: sbom
    path: sbom.spdx.json

10. AWS ECR / Docker Hub へのデプロイ

AWS ECR へのプッシュ

# .github/workflows/ecr-push.yml
jobs:
  build-push-ecr:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@v4
 
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-ecr
          aws-region: ap-northeast-1
 
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2
 
      - name: Build and push to ECR
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ${{ steps.login-ecr.outputs.registry }}/my-app:${{ github.sha }}
            ${{ steps.login-ecr.outputs.registry }}/my-app:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

Docker Hub へのプッシュ

# .github/workflows/dockerhub-push.yml
jobs:
  build-push-dockerhub:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
 
      - name: Build and push to Docker Hub
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            myorg/my-app:${{ github.sha }}
            myorg/my-app:latest

アンチパターン

アンチパターン1: latest タグのみでのデプロイ

# NG: latestタグだけでデプロイ
services:
  app:
    image: my-app:latest  # どのバージョンが動いているか不明
 
# OK: 明示的なバージョンタグ
services:
  app:
    image: my-app:1.2.3   # 完全なバージョン指定
    # または
    image: my-app:sha-abc1234  # Git SHA で特定

なぜ問題か: latest タグはミュータブル(上書き可能)であり、どのコミットのコードが本番で動いているか追跡できない。ロールバックも困難。

アンチパターン2: CI上でのシークレットのハードコード

# NG: ワークフロー内にシークレットを直書き
- name: Login to Docker Hub
  run: docker login -u myuser -p MyP@ssw0rd!
 
# OK: GitHub Secretsを使用
- name: Login to Docker Hub
  uses: docker/login-action@v3
  with:
    username: ${{ secrets.DOCKERHUB_USERNAME }}
    password: ${{ secrets.DOCKERHUB_TOKEN }}

なぜ問題か: リポジトリにシークレットが漏洩し、認証情報が第三者に悪用される。GitHub Secretsは暗号化されてログにもマスクされる。

アンチパターン3: テストなしでのデプロイ

# NG: ビルドしたら即デプロイ
jobs:
  build-and-deploy:
    steps:
      - uses: docker/build-push-action@v5
      - run: ssh prod "docker pull && docker compose up -d"
 
# OK: テスト→スキャン→ステージング→承認→本番
jobs:
  test: ...
  build: { needs: [test] }
  scan: { needs: [build] }
  deploy-staging: { needs: [scan] }
  deploy-production: { needs: [deploy-staging] }

なぜ問題か: テストやセキュリティスキャンをスキップすると、バグや脆弱性が本番に到達する。ステージングでの検証を経ることで、本番障害のリスクを低減する。

アンチパターン4: ビルドとデプロイの密結合

# NG: 1つのジョブ内でビルドからデプロイまで実行
jobs:
  all-in-one:
    steps:
      - run: docker build .
      - run: docker push
      - run: ssh prod "deploy"
 
# OK: ステージごとに分離し、ゲートを設ける
jobs:
  build: ...
  test: { needs: [build] }
  deploy: { needs: [test], environment: production }

なぜ問題か: 密結合すると、テスト失敗時にもデプロイが実行されるリスクがある。また、同じイメージを複数環境にデプロイする際に再ビルドが必要になる。


実践演習

演習1: 基本的な実装

以下の要件を満たすコードを実装してください。

要件:

  • 入力データの検証を行うこと
  • エラーハンドリングを適切に実装すること
  • テストコードも作成すること
# 演習1: 基本実装のテンプレート
class Exercise1:
    """基本的な実装パターンの演習"""
 
    def __init__(self):
        self.data = []
 
    def validate_input(self, value):
        """入力値の検証"""
        if value is None:
            raise ValueError("入力値がNoneです")
        return True
 
    def process(self, value):
        """データ処理のメインロジック"""
        self.validate_input(value)
        self.data.append(value)
        return self.data
 
    def get_results(self):
        """処理結果の取得"""
        return {
            'count': len(self.data),
            'data': self.data
        }
 
# テスト
def test_exercise1():
    ex = Exercise1()
    assert ex.process(1) == [1]
    assert ex.process(2) == [1, 2]
    assert ex.get_results()['count'] == 2
 
    try:
        ex.process(None)
        assert False, "例外が発生するべき"
    except ValueError:
        pass
 
    print("全テスト合格!")
 
test_exercise1()

演習2: 応用パターン

基本実装を拡張して、以下の機能を追加してください。

# 演習2: 応用パターン
from typing import List, Dict, Optional
from datetime import datetime
 
class AdvancedExercise:
    """応用パターンの演習"""
 
    def __init__(self, max_size: int = 100):
        self._items: List[Dict] = []
        self._max_size = max_size
        self._created_at = datetime.now()
 
    def add(self, key: str, value: any) -> bool:
        """アイテムの追加(サイズ制限付き)"""
        if len(self._items) >= self._max_size:
            return False
        self._items.append({
            'key': key,
            'value': value,
            'timestamp': datetime.now().isoformat()
        })
        return True
 
    def find(self, key: str) -> Optional[Dict]:
        """キーによる検索"""
        for item in reversed(self._items):
            if item['key'] == key:
                return item
        return None
 
    def remove(self, key: str) -> bool:
        """キーによる削除"""
        for i, item in enumerate(self._items):
            if item['key'] == key:
                self._items.pop(i)
                return True
        return False
 
    def stats(self) -> Dict:
        """統計情報"""
        return {
            'total_items': len(self._items),
            'max_size': self._max_size,
            'usage_percent': len(self._items) / self._max_size * 100,
            'uptime': str(datetime.now() - self._created_at)
        }
 
# テスト
def test_advanced():
    ex = AdvancedExercise(max_size=3)
    assert ex.add("a", 1) == True
    assert ex.add("b", 2) == True
    assert ex.add("c", 3) == True
    assert ex.add("d", 4) == False  # サイズ制限
    assert ex.find("b")['value'] == 2
    assert ex.remove("b") == True
    assert ex.find("b") is None
    stats = ex.stats()
    assert stats['total_items'] == 2
    print("応用テスト全合格!")
 
test_advanced()

演習3: パフォーマンス最適化

以下のコードのパフォーマンスを改善してください。

# 演習3: パフォーマンス最適化
import time
from functools import lru_cache
 
# 最適化前(O(n^2))
def slow_search(data: list, target: int) -> int:
    """非効率な検索"""
    for i in range(len(data)):
        for j in range(i + 1, len(data)):
            if data[i] + data[j] == target:
                return (i, j)
    return (-1, -1)
 
# 最適化後(O(n))
def fast_search(data: list, target: int) -> tuple:
    """ハッシュマップを使った効率的な検索"""
    seen = {}
    for i, num in enumerate(data):
        complement = target - num
        if complement in seen:
            return (seen[complement], i)
        seen[num] = i
    return (-1, -1)
 
# ベンチマーク
def benchmark():
    import random
    data = list(range(5000))
    random.shuffle(data)
    target = data[100] + data[4000]
 
    start = time.time()
    result1 = slow_search(data, target)
    slow_time = time.time() - start
 
    start = time.time()
    result2 = fast_search(data, target)
    fast_time = time.time() - start
 
    print(f"非効率版: {slow_time:.4f}秒")
    print(f"効率版:   {fast_time:.6f}秒")
    print(f"高速化率: {slow_time/fast_time:.0f}倍")
 
benchmark()

ポイント:

  • アルゴリズムの計算量を意識する
  • 適切なデータ構造を選択する
  • ベンチマークで効果を測定する

トラブルシューティング

よくあるエラーと解決策

エラー 原因 解決策
初期化エラー 設定ファイルの不備 設定ファイルのパスと形式を確認
タイムアウト ネットワーク遅延/リソース不足 タイムアウト値の調整、リトライ処理の追加
メモリ不足 データ量の増大 バッチ処理の導入、ページネーションの実装
権限エラー アクセス権限の不足 実行ユーザーの権限確認、設定の見直し
データ不整合 並行処理の競合 ロック機構の導入、トランザクション管理

デバッグの手順

  1. エラーメッセージの確認: スタックトレースを読み、発生箇所を特定する
  2. 再現手順の確立: 最小限のコードでエラーを再現する
  3. 仮説の立案: 考えられる原因をリストアップする
  4. 段階的な検証: ログ出力やデバッガを使って仮説を検証する
  5. 修正と回帰テスト: 修正後、関連する箇所のテストも実行する
# デバッグ用ユーティリティ
import logging
import traceback
from functools import wraps
 
# ロガーの設定
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s [%(levelname)s] %(name)s: %(message)s'
)
logger = logging.getLogger(__name__)
 
def debug_decorator(func):
    """関数の入出力をログ出力するデコレータ"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        logger.debug(f"呼び出し: {func.__name__}(args={args}, kwargs={kwargs})")
        try:
            result = func(*args, **kwargs)
            logger.debug(f"戻り値: {func.__name__} -> {result}")
            return result
        except Exception as e:
            logger.error(f"例外発生: {func.__name__}: {e}")
            logger.error(traceback.format_exc())
            raise
    return wrapper
 
@debug_decorator
def process_data(items):
    """データ処理(デバッグ対象)"""
    if not items:
        raise ValueError("空のデータ")
    return [item * 2 for item in items]

パフォーマンス問題の診断

パフォーマンス問題が発生した場合の診断手順:

  1. ボトルネックの特定: プロファイリングツールで計測
  2. メモリ使用量の確認: メモリリークの有無をチェック
  3. I/O待ちの確認: ディスクやネットワークI/Oの状況を確認
  4. 同時接続数の確認: コネクションプールの状態を確認
問題の種類 診断ツール 対策
CPU負荷 cProfile, py-spy アルゴリズム改善、並列化
メモリリーク tracemalloc, objgraph 参照の適切な解放
I/Oボトルネック strace, iostat 非同期I/O、キャッシュ
DB遅延 EXPLAIN, slow query log インデックス、クエリ最適化

FAQ

Q1: Docker Hub と GitHub Container Registry (GHCR) のどちらを使うべき?

GHCR推奨: GitHub Actionsとの連携がシームレス(GITHUB_TOKEN で認証可能)、リポジトリの可視性と連動、無料枠が十分。Docker Hubはパブリックイメージの配布に適するが、プルレート制限(100回/6時間)がCI環境で問題になることがある。

Q2: CI上でのDockerビルドが遅い場合の対策は?

  1. レイヤーキャッシュ: cache-from: type=gha を設定
  2. マルチステージビルド: テスト用ステージと本番ステージを分離
  3. 依存関係の分離: package.json を先にCOPYし、npm ci のレイヤーをキャッシュ
  4. BuildKitマウントキャッシュ: --mount=type=cache でパッケージキャッシュを共有
  5. 並列ビルド: 独立したサービスは matrix 戦略で並列実行
  6. ランナースペック向上: runs-on: ubuntu-latest-8-cores など大型ランナーを使用

Q3: ロールバックはどうやって行う?

# 即座に前のバージョンに戻す
docker compose pull  # 旧バージョンタグに切り替え
VERSION=1.2.2 docker compose up -d
 
# または特定のSHAに戻す
docker compose up -d --no-deps \
  -e IMAGE_TAG=sha-abc1234 \
  api

タグを使ったイミュータブルなデプロイを行うことで、任意のバージョンへの即座のロールバックが可能になる。

Q4: GitHub Actions の GITHUB_TOKEN でGHCRにプッシュできないときは?

以下を確認する:

  1. ワークフローの permissionspackages: write を設定しているか
  2. リポジトリの Settings > Actions > General > Workflow permissions が "Read and write permissions" になっているか
  3. Organization の場合、パッケージの可視性設定が正しいか

Q5: モノレポでの Docker CI/CD はどう設計するか?

# パスフィルターで変更があったサービスのみビルド
on:
  push:
    paths:
      - "services/api/**"
      - "shared/**"
 
# または matrix 戦略で全サービスを並列ビルド
jobs:
  build:
    strategy:
      matrix:
        service: [api, worker, frontend]
    steps:
      - name: Build ${{ matrix.service }}
        uses: docker/build-push-action@v5
        with:
          context: ./services/${{ matrix.service }}
          tags: ghcr.io/${{ github.repository }}/${{ matrix.service }}:${{ github.sha }}

まとめ

項目 ポイント
GitHub Actions Docker公式Actionで統一。GHCR連携が最も簡便
タグ戦略 セマンティックバージョニング + Git SHA。latestだけに依存しない
キャッシュ GHA Cacheが推奨。レイヤーの順序最適化で効果最大化
セキュリティ Trivyスキャン、Hadolint、GitHub Secretsを必ず使用
テスト Docker Compose でテスト環境を再現。CI とローカルで同一
デプロイ ステージング→承認→本番のゲート付きパイプライン
ロールバック イミュータブルタグで即座にロールバック可能
イメージ署名 Cosign でイメージの真正性を保証
SBOM サプライチェーンの透明性確保
マルチプラットフォーム QEMU + Buildx で ARM64/AMD64 対応

次に読むべきガイド


参考文献

  1. GitHub Actions 公式ドキュメント "Building and testing containers" -- https://docs.github.com/en/actions/publishing-packages/publishing-docker-images
  2. Docker 公式 GitHub Actions -- https://github.com/docker/build-push-action
  3. Docker 公式ドキュメント "CI/CD best practices" -- https://docs.docker.com/build/ci/github-actions/
  4. Hadolint (Dockerfile Linter) -- https://github.com/hadolint/hadolint
  5. Aqua Security Trivy -- https://github.com/aquasecurity/trivy
  6. Sigstore Cosign -- https://github.com/sigstore/cosign
  7. SLSA (Supply chain Levels for Software Artifacts) -- https://slsa.dev/
  8. Docker 公式ドキュメント "BuildKit" -- https://docs.docker.com/build/buildkit/