• ホーム
  • ブログ
  • CDKからGitHub Actionsに移行してDockerビルドを並列化、CI/CD時間を80分から20分に短縮した話

CDKからGitHub Actionsに移行してDockerビルドを並列化、CI/CD時間を80分から20分に短縮した話

鶴田 篤広のプロフィール画像

鶴田 篤広

ソフトウェアエンジニア

作成日:

GitHub ActionsDockerCI/CDAWS CDKDevOpsパフォーマンス最適化
CDKからGitHub Actionsに移行してDockerビルドを並列化、CI/CD時間を80分から20分に短縮した話 - CDKの逐次実行からGitHub Actions Matrix Strategyによる並列実行に移行...

はじめに

マイクロサービスアーキテクチャを採用したプロジェクトでは、複数のDockerイメージをビルドする必要があります。あるプロジェクトでは、APIサービス、ワーカーサービス、データ処理サービス、バッチジョブ、スケジューラーなど、合計5つのDockerイメージを管理していました。

当初、AWS CDK(Cloud Development Kit)を使用してインフラのデプロイとDockerイメージのビルドを一元管理していました。CDKは Infrastructure as Code のツールとして優れていますが、複数のDockerイメージを効率的に並列ビルドすることが困難であり、複数のイメージを持つマイクロサービス構成では大きなボトルネックとなっていました。

実際、5つのDockerイメージをCDKでビルドすると、約80分もの時間を要していました(※実数値を簡略化しています)。この長いビルド時間は開発速度のボトルネックとなり、フィードバックサイクルの遅延やコスト増加につながっていました。

この記事では、CDKからGitHub Actionsへビルドプロセスを移行し、Matrix Strategyを活用して並列ビルドを実現することで、約20分まで短縮(約4倍の高速化)した方法を紹介します。

課題:CDKでの直列ビルドのボトルネック

プロジェクト構成

例として、以下の5つのDockerイメージで構成されたプロジェクトを想定します:

  1. APIサービス - RESTful APIを提供するメインアプリケーション
  2. ワーカーサービス - 非同期タスク処理用のバックグラウンドワーカー
  3. データ処理サービス - データ変換・集計を行う処理エンジン
  4. バッチジョブ - 定期実行される重量級バッチ処理
  5. スケジューラー - タスクスケジューリングと調整を行うサービス

CDKでのDockerビルドの制約

当初、これらのDockerイメージは AWS CDK の DockerImageAsset または DockerImageCode を使用してビルドしていました:

import * as cdk from 'aws-cdk-lib';
import * as ecr from 'aws-cdk-lib/aws-ecr';
import { DockerImageAsset } from 'aws-cdk-lib/aws-ecr-assets';

// CDKでのDockerイメージビルド(逐次実行される)
const apiImage = new DockerImageAsset(this, 'ApiImage', {
  directory: path.join(__dirname, '../docker/api-service'),
});

const workerImage = new DockerImageAsset(this, 'WorkerImage', {
  directory: path.join(__dirname, '../docker/worker-service'),
});

// ... 他の3つのイメージも同様

この方法には大きな課題がありました。実は、CDKは現時点でDockerイメージアセットのビルドをシリアル(順次)実行する仕様になっています。

この問題は私たちだけでなく多くの開発者が直面しており、AWS CDK CLIのGitHubリポジトリにも同様のIssueが報告されています:

Issue #226: Asset Build Parallelization

報告者の例では、5つのDockerイメージを順次ビルドすると50秒以上かかるのに対し、並列化すると約10秒で完了するとのこと。asset-build-concurrencyというCLI引数で並列度を制御できるようにする機能が提案されています。(2025年1月報告、現在も未実装)

現時点(2025年11月)では、この機能はまだ実装されていません。CDKでの並列ビルドには以下の制約があります:

  1. CDKの仕様による制約

    • Dockerイメージアセットのビルドはシリアル実行が現在の仕様
    • 並列化のオプションが公式にサポートされていない
    • 将来的にはasset-build-concurrencyオプションが追加される可能性
  2. ワークアラウンドの限界

    • 外部ツールで並列化を試みることは可能だが複雑
    • CDKのビルドプロセスとの統合が難しい
    • 安定性やキャッシュ管理の問題が発生しやすい

この制約を回避するため、DockerビルドをCDKから分離し、GitHub Actionsに移行することにしました。

ビルド時間の内訳

各イメージのビルド時間は以下のような想定です(※実際の数値を簡略化しています):

  • APIサービス: 約20分
  • ワーカーサービス: 約18分
  • データ処理サービス: 約16分
  • バッチジョブ: 約15分
  • スケジューラー: 約14分

CDKの逐次実行により、合計で約80分のビルド時間となっていました。

問題点

  1. CDKの仕様によるシリアル実行

    • Dockerイメージアセットのビルドは順次実行される(Issue #226
    • 並列化オプションは2025年1月時点で未実装
    • イメージ数に比例してビルド時間が増加
  2. 長いフィードバックサイクル

    • PRのマージまでに1時間以上待つ必要がある
    • 小さな修正でも全イメージの再ビルドが必要
  3. 開発速度の低下

    • 修正の確認に時間がかかり、開発のテンポが悪化
    • 開発者が別の作業に移行し、コンテキストスイッチが発生
  4. リソースの非効率な利用

    • 1つのイメージのビルド中、CPUやメモリが遊休状態
    • クラウドリソースの利用効率が低い

解決策:GitHub Actions Matrix Strategyへの移行

なぜGitHub Actionsなのか

CDKでの並列化の限界を克服するため、Dockerイメージのビルドプロセスを GitHub Actions に分離しました。この決断の理由は以下の通りです:

  1. 並列実行の容易さと柔軟性

    • Matrix Strategyにより、複数のジョブを簡単に並列実行できる
    • CDKよりも並列度の制御が容易で安定している
    • 各ビルドが独立したランナーで実行され、リソース競合が発生しない
  2. 細かい制御が可能

    • ビルドの順序、依存関係、並列度を細かく制御可能
    • キャッシュ戦略を個別に最適化できる
    • ビルド失敗時の挙動を柔軟に設定可能(fail-fast等)
  3. CDKとの適切な責任分離

    • CDK: インフラ(ECRリポジトリ等)のプロビジョニング
    • GitHub Actions: Dockerイメージのビルドとデプロイ

この構成により、CDKはインフラの管理に専念し、Dockerビルドは並列実行が可能なGitHub Actionsに任せることができます。

Matrix Strategyとは

GitHub ActionsのMatrix Strategyは、同じジョブを異なるパラメータで並列実行する機能です(参考: The matrix strategy in GitHub Actions - RunsOn)。これにより、複数の環境やバージョンでのテスト、複数のDockerイメージのビルドなどを効率的に並列化できます。

CDKでは実現が困難だった並列ビルドを、Matrix Strategyを使うことで簡単に実装できます。

基本的な構造

strategy:
  matrix:
    image:
      - name: api-service
        dockerfile: docker/api-service/Dockerfile
        repo-suffix: api-repository
      - name: worker-service
        dockerfile: docker/worker-service/Dockerfile
        repo-suffix: worker-repository
  max-parallel: 5
  fail-fast: false

このシンプルな設定により、複数のDockerイメージを並列でビルドできます。

実装の詳細

1. ECRリポジトリの事前作成

まず、各Dockerイメージ用のECRリポジトリを事前に作成します。これにより、並列ビルド時の競合を避けることができます:

jobs:
  deploy-ecr-repositories:
    runs-on: ubuntu-latest
    env:
      AWS_REGION: 'us-east-1'
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '22'

      - name: Build Infrastructure
        run: npm run build:infra

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Deploy ECR Stack
        run: npm run deploy:ecr

AWS CDKを使用してECRリポジトリを動的に作成します:

import * as cdk from 'aws-cdk-lib';
import * as ecr from 'aws-cdk-lib/aws-ecr';
import { Construct } from 'constructs';

// リポジトリ名の定義(GitHub Actions のマトリックスと同期)
// ※以下は架空のリポジトリ名です
export const REPOSITORY_NAMES = [
  'api-repository',
  'worker-repository',
  'processor-repository',
  'batch-repository',
  'scheduler-repository',
];

export class EcrStack extends cdk.Stack {
  private readonly APP_NAME = 'myapp'; // ※架空のアプリケーション名
  private readonly env: 'stg' | 'prod';
  private readonly appNameAndEnv: string;

  constructor(scope: Construct, id: string, env: 'stg' | 'prod', props?: cdk.StackProps) {
    super(scope, id, props);

    this.env = env;
    this.appNameAndEnv = `${this.APP_NAME}-${env}`;

    // 全リポジトリを作成
    REPOSITORY_NAMES.forEach((repositoryName) => {
      this.createEcrRepository(repositoryName);
    });
  }

  private makeResourceId(resourceName: string): string {
    return `${this.appNameAndEnv}-${resourceName}`;
  }

  private createEcrRepository(repositoryName: string): ecr.Repository {
    return new ecr.Repository(this, this.makeResourceId(repositoryName), {
      repositoryName: this.makeResourceId(repositoryName),
      removalPolicy: cdk.RemovalPolicy.RETAIN,
      emptyOnDelete: false,
      lifecycleRules: [
        {
          description: 'Keep last 10 images',
          maxImageCount: 10,
        },
      ],
    });
  }
}

2. Matrix Strategyによる並列ビルド

次に、Matrix Strategyを使用して各イメージを並列ビルドします:

build-matrix:
  environment: ${{ inputs.env }}
  needs: [deploy-ecr-repositories]
  if: always() && (needs.deploy-ecr-repositories.result == 'success' || needs.deploy-ecr-repositories.result == 'skipped')
  runs-on: ubuntu-latest
  outputs:
    image-tag: ${{ steps.set-tag.outputs.tag }}

  strategy:
    matrix:
      image:
        - name: api-service
          dockerfile: docker/api-service/Dockerfile
          repo-suffix: api-repository
          build-target: api-service
          remote-cache-port: 32001
        - name: worker-service
          dockerfile: docker/worker-service/Dockerfile
          repo-suffix: worker-repository
          build-target: worker-service
          remote-cache-port: 32002
        - name: data-processor
          dockerfile: docker/data-processor/Dockerfile
          repo-suffix: processor-repository
          build-target: data-processor
          remote-cache-port: 32003
        - name: batch-job
          dockerfile: docker/batch-job/Dockerfile
          repo-suffix: batch-repository
          build-target: batch-job
          remote-cache-port: 32004
        - name: scheduler
          dockerfile: docker/scheduler/Dockerfile
          repo-suffix: scheduler-repository
          build-target: scheduler
          remote-cache-port: 32005
    max-parallel: 5
    fail-fast: false

  env:
    AWS_REGION: 'us-east-1'

3. ビルドステップ

各マトリックスジョブで以下のステップを実行します:

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

  - name: Set Image Tag
    id: set-tag
    run: echo "tag=${{ github.sha }}" >> $GITHUB_OUTPUT

  - name: Configure AWS Credentials
    uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
      aws-region: ${{ env.AWS_REGION }}

  - name: Login to Amazon ECR
    id: login-ecr
    uses: aws-actions/amazon-ecr-login@v2

  - name: Set up Docker Buildx
    uses: docker/setup-buildx-action@v3
    with:
      driver-opts: network=host

  - name: Build and Push Docker Image
    env:
      REPO_NAME: myapp-${{ env.SHORT_ENV }}-${{ matrix.image.repo-suffix }}
    uses: docker/build-push-action@v5
    with:
      context: .
      file: ${{ matrix.image.dockerfile }}
      push: true
      platforms: linux/amd64
      provenance: false
      tags: |
        ${{ steps.login-ecr.outputs.registry }}/${{ env.REPO_NAME }}:${{ github.sha }}
        ${{ steps.login-ecr.outputs.registry }}/${{ env.REPO_NAME }}:latest
      cache-from: type=gha,scope=${{ matrix.image.name }}
      cache-to: type=gha,mode=max,scope=${{ matrix.image.name }}
      build-args: |
        ENV=${{ env.SHORT_ENV }}
        AWS_REGION=${{ env.AWS_REGION }}

4. キャッシュ戦略の最適化

並列ビルドでは、各イメージごとに独立したキャッシュを使用することが重要です:

cache-from: type=gha,scope=${{ matrix.image.name }}
cache-to: type=gha,mode=max,scope=${{ matrix.image.name }}

この設定により:

  • GitHub Actions Cache - type=ghaを使用し、各イメージに異なるscopeを設定
  • スコープ分離 - 各イメージが独立したキャッシュスコープを持ち、競合を防止
  • mode=max - 全レイヤーをキャッシュし、2回目以降のビルドを大幅に高速化

結果と効果

ビルド時間の改善

項目 改善前 改善後 削減率
ビルド時間 約80分(直列) 約20分(並列) 75%削減
最長イメージ 20分 20分 -
合計実行時間 80分 20分 60分短縮

※上記の数値は実際の値を簡略化しています。

並列実行により、最も時間のかかるイメージ(20分)に他の4つのビルドが収まるため、劇的な時間短縮を実現しました。

パフォーマンスメトリクス

想定値(※簡略化しています):

  • 改善前: 5イメージ × 平均16分 = 80分(順次実行)
  • 改善後: max(20分, 18分, 16分, 15分, 14分) = 20分(並列実行)
  • 削減率: 75%
  • 追加コスト: ほぼゼロ(GitHub Actionsの並列実行は無料枠内で実行可能)

その他の効果

  1. 開発速度の向上

    • PRのフィードバックサイクルが大幅に短縮
    • 1日に複数回のデプロイが可能に
    • 開発者の待ち時間が減少し、生産性が向上
  2. コスト削減

    • GitHub Actionsの実行時間が減少
    • 同じ並列度で実行できるため、追加コストなし
    • リソースの効率的な活用
  3. 開発者体験の向上

    • ビルド完了を待つストレスが軽減
    • より頻繁なデプロイによる安心感
    • 早期のフィードバックによる品質向上

実装時の注意点

1. max-parallelの設定

max-parallelを設定しないと、全てのジョブが同時に実行され、リソース不足になる可能性があります:

strategy:
  max-parallel: 5  # 同時実行数を制限

GitHub Actionsにはプランごとの同時実行数制限があります。Freeプランでは、Linux/Windowsランナーで最大20並列、macOSランナーで最大5並列です(参考: GitHub Changelog - Increased Concurrency Limit)。プロジェクトの規模に応じて適切な値を設定しましょう。

2. fail-fastの無効化

デフォルトでは、1つのジョブが失敗すると他のジョブもキャンセルされます。すべてのイメージのビルド結果を確認するため、fail-fast: falseを設定します(参考: Matrix Builds with GitHub Actions | Blacksmith):

strategy:
  fail-fast: false  # 1つ失敗しても他を続行

これにより、1つのイメージビルドが失敗しても、他のイメージのビルドは継続され、部分的な成功を得ることができます。

3. キャッシュの分離

各イメージのキャッシュを分離することで、キャッシュの競合を避けます:

cache-from: type=gha,scope=${{ matrix.image.name }}
cache-to: type=gha,mode=max,scope=${{ matrix.image.name }}

scopeパラメータに各イメージのユニークな名前を設定することで、並列実行時のキャッシュ競合を完全に防止できます。

4. 条件付き実行の活用

ECRリポジトリのデプロイがスキップされた場合でも、ビルドジョブを実行できるようにします:

if: always() && (needs.deploy-ecr-repositories.result == 'success' || needs.deploy-ecr-repositories.result == 'skipped')

これにより、既にリポジトリが存在する場合はデプロイをスキップし、直接ビルドに進むことができます。

アーキテクチャの工夫

インフラとコードの同期

ECRリポジトリ名をコードで一元管理することで、GitHub ActionsとCDKの設定を確実に同期させます:

// CDK側でリポジトリ名を定義(※架空の名前)
export const REPOSITORY_NAMES = [
  'api-repository',
  'worker-repository',
  'processor-repository',
  'batch-repository',
  'scheduler-repository',
];
# GitHub Actions側で同じ名前を使用
strategy:
  matrix:
    image:
      - repo-suffix: api-repository
      - repo-suffix: worker-repository
      # ...

再利用可能なワークフロー

このビルドプロセスを再利用可能なワークフローとして定義することで、複数の場所から呼び出すことができます:

# .github/workflows/reusable-docker-build.yml
name: Reusable Docker Build Workflow

on:
  workflow_call:
    inputs:
      env:
        description: 'Target environment'
        required: true
        type: string
    outputs:
      image-tag:
        description: "Built image tag"
        value: ${{ jobs.build-matrix.outputs.image-tag }}
# .github/workflows/main-ci.yml
jobs:
  call-docker-build:
    uses: ./.github/workflows/reusable-docker-build.yml
    with:
      env: ${{ github.ref == 'refs/heads/main' && 'prod' || 'staging' }}
    secrets: inherit

まとめ

CDKでの逐次実行から、GitHub ActionsのMatrix Strategyによる並列実行に移行することで、複数のDockerイメージのビルド時間を約4倍高速化することができました。この改善により、開発速度の向上、コスト削減、そして開発者体験の向上という複数のメリットを得ることができました。

その他の高速化施策

今回のDocker並列ビルドに加えて、モノレポ全体のビルド高速化のためにTurborepoの Remote Cache も導入しています。Turborepoは、タスクの実行結果をリモートキャッシュに保存し、同じ入力に対するビルドをスキップすることで、CI/CD全体の実行時間をさらに短縮できます。

これらの施策を組み合わせることで、開発サイクル全体の効率化を実現しています。

学習ポイント

  1. 適切なツールの選択 - CDKはインフラ管理に、GitHub Actionsはビルドプロセスに特化
  2. 並列化の重要性 - 独立したタスクは並列実行することで大幅な時間短縮が可能
  3. 責任の分離 - ビルドプロセスとインフラ管理を分離することで、それぞれの強みを活かす
  4. Matrix Strategyの活用 - GitHub Actionsの強力な機能を理解し活用する
  5. キャッシュ戦略 - Docker BuildxキャッシュやTurborepo Remote Cacheなど、適切なキャッシュ設定により、さらなる高速化が可能
  6. インフラとコードの同期 - CDKとGitHub Actionsの設定を一元管理

マイクロサービスやモノレポ構成のプロジェクトでビルド時間に悩んでいる方は、ぜひMatrix Strategyの導入を検討してみてください。

参考文献

クレインテックの強み

私たちは、お客様のビジネスを成功に導くために、以下のような価値を提供します

課題整理と共創

顧客と一緒に課題を洗い出し、本当に必要な機能を明確にします

MVPから始める開発

最小限の機能でMVPを提供し、段階的な改善を行います

高品質で長く使える設計

将来的な拡張を見据えたアーキテクチャで安心して長期間使えます

継続的な改善サポート

リリース後も継続的に改善提案を行い、ビジネスの成長を支援します

開発のご相談はこちら

アプリケーション開発やシステム構築でお困りのことはありませんか?
私たちがあなたの開発プロジェクトをサポートします。

お問い合わせフォームへ

この記事をシェア

開発のご相談

アプリケーション開発やシステム構築でお困りのことはありませんか?私たちがあなたの開発プロジェクトをサポートします。

お問い合わせフォームへ