•21 min read

Turborepoリモートキャッシュを大規模運用:Pruned Dockerビルド、GCSキャッシュ、CIパイプライン高速化

Turborepoリモートキャッシュを大規模運用:Pruned Dockerビルド、GCSキャッシュ、CIパイプライン高速化

Turborepoは、JavaScriptおよびTypeScriptのモノレポ向けに設計された高性能なビルドシステムです。その核となる価値提案は、インテリジェントなタスクスケジューリング、コンテンツアドレス型キャッシング、およびリモートキャッシングにあります。このガイドでは、最適化されたDockerビルド、GCSをバックエンドとするリモートキャッシング、およびCIパイプラインの大幅な高速化に焦点を当て、堅牢なTurborepoセットアップの実装について詳しく説明します。

Audio Briefing
0:00 / 0:00

Turborepoのキャッシングメカニズムを理解する

Turborepoのキャッシングは、コンテンツアドレス型(content-addressable)の原則に基づいて動作します。特定のタスク(例:build、test、lint)について、Turborepoは以下の要素に基づいてハッシュを計算します。

  1. 入力ファイル: ソースコード、設定ファイル、およびturbo.jsonのinputsキーの下で設定されたアセット。
  2. 依存関係: 現在のパッケージとその推移的な依存関係のpackage.jsonファイル。
  3. 環境変数: turbo.jsonのenvキーの下で明示的に宣言されたランタイム変数。
  4. Turborepo設定: ルートのturbo.jsonファイル内のグローバル設定。
  5. タスクコマンド: 実行される正確なコマンド。

同一のハッシュを持つタスクが以前に実行された場合、Turborepoはタスクを再実行する代わりにキャッシュされた出力を取得します。この出力はローカルまたはリモートキャッシュに保存できます。

Advertisement

アーキテクチャの概要

最適化されたアーキテクチャは、TurborepoをDockerおよびGoogle Cloud Storage (GCS) と統合し、スケーラブルで効率的なビルドプロセスを実現します。

  1. Turborepoモノレポ: 複数のアプリケーションとパッケージのための一元化されたコードベース。
  2. turbo prune: 特定のアプリケーションをビルドするために必要なモノレポの最小限のサブセットを生成し、Dockerビルドコンテキストを最適化します。
  3. マルチステージDockerビルド: turbo pruneの出力とDockerのレイヤーキャッシングを活用し、軽量で再現性のあるイメージを作成します。
  4. GCSリモートキャッシュ: すべてのCIエージェントと開発者がアクセスできる、Turborepo用の共有された永続的なキャッシュバックエンド。
  5. GitHub Actions: リモートキャッシュを利用して、ビルド、テスト、デプロイのワークフローをオーケストレーションします。

アーキテクチャのトレードオフ

機能利点欠点
turbo prune最小限のDockerコンテキスト、高速なビルド、より小さなイメージDockerfileの複雑さが増す、慎重なturbo.json inputsの定義が必要
マルチステージDockerより小さな最終イメージ、より良いレイヤーキャッシングDockerfileがより複雑になる、注意しないとビルド時の依存関係が肥大化する可能性
GCSリモートキャッシュ共有キャッシュ、高可用性、スケーラビリティ、CI時間の短縮GCSのコスト、初期設定の複雑さ、キャッシュミス時のネットワーク遅延
GitHub Actions統合されたCI/CD、優れたエコシステムベンダーロックイン、大規模リポジトリでのレート制限の可能性

最適化されたDockerビルドのためのturbo pruneの実装

turbo pruneはDockerビルドにとって非常に重要です。モノレポ内の特定のターゲットアプリケーションをビルドするために必要なパッケージとその依存関係のみを含む新しいディレクトリを作成します。これにより、Dockerビルドコンテキストのサイズが大幅に削減され、イメージビルドが高速化され、イメージが小さくなります。

モノレポの構造を考えてみましょう。

/
├── apps/
│   ├── web/
│   └── api/
├── packages/
│   ├── ui/
│   ├── utils/
│   └── db/
├── turbo.json
├── package.json
└── pnpm-lock.yaml

apps/apiをビルドするには、apps/api、packages/utils、およびpackages/dbのみが必要です。turbo pruneがこれを分離します。

turbo.json設定

turbo.jsonがタスクのinputsとoutputsを正確に定義していることを確認してください。これは、正しいハッシュ計算とプルーニングのために不可欠です。

{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**", "build/**"],
      "inputs": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.js", "src/**/*.jsx", "tsconfig.json", "package.json"]
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": [],
      "inputs": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/**/*.spec.ts", "src/**/*.spec.tsx"]
    },
    "lint": {
      "outputs": []
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

turbo pruneを使用したDockerfile

以下は、apiアプリケーション用のマルチステージDockerfileで、turbo pruneを活用しています。

# Stage 1: Build dependencies and application
FROM node:20-alpine AS builder

# Install pnpm globally
RUN corepack enable && corepack prepare pnpm@latest --activate

# Set working directory
WORKDIR /app

# Copy lockfile and package.json files first to leverage Docker cache
# This layer changes infrequently, maximizing cache hits
COPY pnpm-lock.yaml ./
COPY package.json ./
COPY turbo.json ./

# Copy only the pruned monorepo subset for the 'api' application
# This command is executed by the CI/CD pipeline or locally before `docker build`
# Example: `turbo prune --scope=api --docker`
# The output of `turbo prune` is expected in the './.pruned-build' directory
COPY .pruned-build/pnpm-workspace.yaml ./.pruned-build/
COPY .pruned-build/full/ ./.pruned-build/full/
COPY .pruned-build/json/ ./.pruned-build/json/

# Install dependencies using pnpm
# Use --frozen-lockfile to ensure reproducible builds
RUN pnpm install --frozen-lockfile --prod=false

# Copy the actual source code for the pruned packages
# This is crucial: `turbo prune` only copies package.json and lockfiles,
# not the source code itself. We copy it from the original context.
# The `full` directory contains symlinks to the original source.
# We need to copy the actual files.
# This assumes the Docker build context is the root of the monorepo.
COPY apps/api ./apps/api
COPY packages/db ./packages/db
COPY packages/utils ./packages/utils

# Build the 'api' application
# Turborepo will use its cache or build if necessary
RUN pnpm turbo run build --filter=api...

# Stage 2: Production image
FROM node:20-alpine AS runner

# Install pnpm globally (for production dependencies if needed, though often not)
RUN corepack enable && corepack prepare pnpm@latest --activate

WORKDIR /app

# Copy only production dependencies from the builder stage
# This ensures a minimal production image
COPY --from=builder /app/pnpm-lock.yaml ./
COPY --from=builder /app/package.json ./
COPY --from=builder /app/turbo.json ./
COPY --from=builder /app/.pruned-build/pnpm-workspace.yaml ./.pruned-build/
COPY --from=builder /app/.pruned-build/json/ ./.pruned-build/json/

# Install production dependencies
RUN pnpm install --prod --frozen-lockfile

# Copy the built application artifacts from the builder stage
# This includes `dist` directories for the API and its dependencies
COPY --from=builder /app/apps/api/dist ./apps/api/dist
COPY --from=builder /app/packages/db/dist ./packages/db/dist
COPY --from=builder /app/packages/utils/dist ./packages/utils/dist

# Expose port (if applicable)
EXPOSE 3000

# Define the command to run the application
CMD ["node", "apps/api/dist/index.js"]

Dockerコンテキストにおけるturbo pruneの説明:

  1. turbo prune --scope=api --docker: docker buildの前に実行されるこのコマンドは、モノレポのルートに.pruned-buildディレクトリを作成します。
    • pnpm-workspace.yaml: .pruned-build/pnpm-workspace.yamlにコピーされます。
    • full/: すべての関連パッケージのpackage.jsonファイルへのシンボリックリンクが含まれます。
    • json/: すべての関連パッケージのpackage.jsonファイルが含まれますが、フラット化されています。
  2. COPY .pruned-build/...: これらの生成されたファイルをDockerイメージにコピーします。
  3. pnpm install: プルーニングされたpackage.jsonファイルとpnpm-lock.yamlを使用して、pnpmは必要な依存関係のみをインストールします。
  4. COPY apps/api ./apps/api: 重要なことに、turbo pruneはソースファイルをコピーしません。ターゲットアプリケーションとその直接の依存関係のソースコードは、元のモノレポコンテキストから明示的にコピーする必要があります。これが、docker buildコマンドをモノレポのルートから実行する必要がある理由です。
  5. pnpm turbo run build --filter=api...: Turborepoはapiアプリケーションとそのアップストリームの依存関係をビルドします。

GCSを使用したセルフホスト型リモートキャッシュ

Turborepoはさまざまなリモートキャッシュバックエンドをサポートしています。エンタープライズ環境では、クラウドストレージ(GCS、S3)を使用したセルフホスト型ソリューションが、制御性、スケーラビリティ、コスト効率を提供します。

GCSバケットのセットアップ

  1. GCSバケットを作成します: gs://your-turborepo-cache-bucket
  2. 適切なIAM権限を確認します: キャッシュにアクセスするサービスアカウントまたはユーザーには、Storage Object AdminまたはStorage Object CreatorとStorage Object Viewerのロールが必要です。

GCS用のTurborepo設定

Turborepoは環境変数を使用してリモートキャッシングを設定します。

# .env.local or CI environment variables
TURBO_REMOTE_CACHE_SIGNATURE_KEY="your_strong_secret_key_for_signing_cache_requests"
TURBO_REMOTE_CACHE_READ_ONLY="false" # Set to true for read-only environments
TURBO_REMOTE_CACHE_GCS_BUCKET="your-turborepo-cache-bucket"
TURBO_REMOTE_CACHE_GCS_SERVICE_ACCOUNT_KEY="path/to/your/gcs-service-account-key.json" # Or base64 encoded JSON

重要なセキュリティに関する注意: TURBO_REMOTE_CACHE_SIGNATURE_KEYやTURBO_REMOTE_CACHE_GCS_SERVICE_ACCOUNT_KEYをリポジトリに直接コミットしないでください。環境変数、シークレット管理システム(例:GitHub Secrets、GCP Secret Manager)を使用してください。

CI/CDでは、通常、サービスアカウントキーのJSONをbase64エンコードし、シークレットとして保存します。

# Example for GitHub Actions
echo "${{ secrets.GCP_SERVICE_ACCOUNT_KEY_BASE64 }}" | base64 --decode > gcs-key.json
export TURBO_REMOTE_CACHE_GCS_SERVICE_ACCOUNT_KEY=$(pwd)/gcs-key.json
Advertisement

GitHub ActionsによるCIパイプラインの高速化

上記のコンポーネントをGitHub Actionsワークフローに統合することで、CIビルド時間が劇的に短縮されます。

name: CI Build & Test

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  build-api:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0 # Required for Turborepo to correctly detect changed files

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm' # Cache pnpm dependencies

      - name: Install pnpm
        run: corepack enable && corepack prepare pnpm@latest --activate

      - name: Install monorepo dependencies
        run: pnpm install --frozen-lockfile

      - name: Configure Turborepo Remote Cache (GCS)
        env:
          TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
          TURBO_REMOTE_CACHE_GCS_BUCKET: your-turborepo-cache-bucket
          GCP_SERVICE_ACCOUNT_KEY_BASE64: ${{ secrets.GCP_SERVICE_ACCOUNT_KEY_BASE64 }}
        run: |
          echo "${{ env.GCP_SERVICE_ACCOUNT_KEY_BASE64 }}" | base64 --decode > gcs-key.json
          export TURBO_REMOTE_CACHE_GCS_SERVICE_ACCOUNT_KEY=$(pwd)/gcs-key.json
          echo "TURBO_REMOTE_CACHE_GCS_SERVICE_ACCOUNT_KEY=$(pwd)/gcs-key.json" >> $GITHUB_ENV
          echo "TURBO_REMOTE_CACHE_SIGNATURE_KEY=${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}" >> $GITHUB_ENV
          echo "TURBO_REMOTE_CACHE_GCS_BUCKET=your-turborepo-cache-bucket" >> $GITHUB_ENV

      - name: Build API application with Turborepo
        run: pnpm turbo run build --filter=api...

      - name: Run API tests with Turborepo
        run: pnpm turbo run test --filter=api...

      - name: Lint API application with Turborepo
        run: pnpm turbo run lint --filter=api...

      - name: Prune API for Docker build
        run: pnpm turbo prune --scope=api --docker

      - name: Build Docker image for API
        run: docker build -t your-registry/api:${{ github.sha }} -f apps/api/Dockerfile .

      - name: Push Docker image (example)
        # Add Docker login steps here if pushing to a private registry
        # run: docker push your-registry/api:${{ github.sha }}
        run: echo "Docker image built: your-registry/api:${{ github.sha }}"

CIワークフローにおける主要な最適化:

  • actions/checkout@v4とfetch-depth: 0: 正確な変更検出とキャッシングのために、Turborepoが完全なGit履歴にアクセスできるようにします。
  • actions/setup-node@v4とcache: 'pnpm': node_modulesのpnpmをキャッシュし、後続の実行でのpnpm install時間を短縮します。
  • Turborepoリモートキャッシュ設定: TurborepoがGCSキャッシュを指すように環境変数が設定されます。
  • pnpm turbo run build --filter=api...: Turborepoはapiアプリケーションとその依存関係のみをインテリジェントにビルドします。キャッシュヒットが発生した場合、このステップはほぼ瞬時に完了します。
  • pnpm turbo prune --scope=api --docker: Docker用に最適化されたビルドコンテキストを作成します。
  • docker build ... -f apps/api/Dockerfile .: Dockerイメージをビルドします。turbo pruneがコンテキストを削減し、Dockerfileがマルチステージであるため、このステップは大幅に高速化されます。

本番環境での注意点とトラブルシューティング

  1. コード変更がないにもかかわらずキャッシュミスが発生する:

    • 症状: 関連するコードが変更されていないにもかかわらず、Turborepoがcache MISSを報告する。
    • 原因:
      • turbo.json内のinputsまたはoutputsが不正確。ビルドに影響を与えるファイルがinputsにリストされていない場合、その変更はキャッシュを無効にしない。outputsが正しく定義されていない場合、Turborepoは完全な出力を保存しない可能性がある。
      • 環境変数が一貫して設定されていない。turbo.jsonで定義されたenv変数が実行間で変更されると、ハッシュも変更される。
      • 依存関係におけるpackage.jsonまたはpnpm-lock.yamlの変更。
      • actions/checkoutのfetch-depthが浅すぎるため、Turborepoが変更を正確に検出できない。
    • 修正:
      • turbo.json inputsとoutputsを慎重に確認する。git diff --name-only <commit-ish>を使用して変更されたファイルを特定し、それらがカバーされていることを確認する。
      • すべての関連する環境変数がTurborepoに一貫して渡されていることを確認する。
      • actions/checkoutでfetch-depth: 0を設定する。
      • pnpm turbo run <task> --dry-run=jsonを実行して、計算されたハッシュと入力を検査する。
  2. turbo pruneが必要なファイルを含まない:

    • 症状: turbo pruneの後、ターゲットアプリケーションが期待するファイルまたはパッケージが不足しているため、Dockerビルドが失敗する。
    • 原因:
      • turbo pruneコマンドの--scopeが狭すぎるか、依存関係グラフがTurborepoによって正しく理解されていない。
      • Dockerfile内のCOPYコマンドが不完全で、元のモノレポコンテキストから必要なソースファイルがすべてコピーされていない。
    • 修正:
      • pnpm turbo graph --filter=api...を使用して依存関係グラフを確認する。必要なすべてのパッケージがリストされていることを確認する。
      • turbo pruneを実行した後、.pruned-buildディレクトリを手動で検査する。
      • Dockerfile内のCOPYコマンドを再確認し、ターゲットアプリとその直接の依存関係のすべてのソースディレクトリが含まれていることを確認する。turbo pruneはpackage.jsonとロックファイルのみを処理し、ソースは処理しないことを忘れないでください。
  3. GCS権限エラー:

    • 症状: 権限拒否エラーにより、TurborepoがGCSへのアップロードまたはダウンロードに失敗する。
    • 原因: 使用されているサービスアカウントキーが、GCSバケットに対する十分なIAM権限を持っていない。
    • 修正: 特定のGCSバケットに対して、サービスアカウントにStorage Object Adminまたは少なくともStorage Object CreatorとStorage Object Viewerのロールを付与する。TURBO_REMOTE_CACHE_GCS_SERVICE_ACCOUNT_KEYのパスが正しく、ファイルが読み取り可能であることを確認する。
  4. Dockerでのpnpm installが遅い:

    • 症状: turbo pruneを使用しているにもかかわらず、Dockerビルドでpnpm installに時間がかかる。
    • 原因:
      • pnpm-lock.yamlまたはpackage.jsonファイルが頻繁に変更され、pnpm installのDockerレイヤーキャッシュが無効になる。
      • Dockerfileでpnpmキャッシュが使用されていない。
    • 修正:
      • pnpm-lock.yamlがコミットされ、最新の状態に保たれていることを確認する。
      • Dockerfileを、pnpm-lock.yaml、package.json、およびturbo.jsonを最初にコピーし、次にpnpm installを実行するように構成する。これにより、レイヤーキャッシングが最大化される。
      • ローカル開発では共有のpnpmキャッシュボリュームの使用を検討するが、CIではより困難。actions/setup-nodeキャッシュはCIで役立つ。

よくある質問

Q1: Turborepoは、キャッシングのために推移的な依存関係の変更をどのように処理しますか?

Turborepoのハッシュメカニズムは、現在のパッケージとそのすべての推移的な依存関係のpackage.jsonを考慮します。依存関係チェーン内のいずれかのpackage.jsonが変更された場合、またはpnpm-lock.yaml(またはyarn.lock、package-lock.json)が変更された場合、それらのパッケージに依存するタスクのハッシュは無効になり、キャッシュミスが発生します。これにより、正確性が保証されます。

Q2: GCS以外のリモートキャッシュバックエンドを使用できますか?

はい、できます。Turborepoはさまざまなリモートキャッシュプロバイダーをサポートしています。GCSの他に、Vercel Remote Cache(Vercelデプロイメント用)、AWS S3、および汎用HTTPエンドポイントをネイティブでサポートしています。S3の場合、TURBO_REMOTE_CACHE_S3_BUCKET、TURBO_REMOTE_CACHE_S3_REGION、およびAWS認証情報を使用します。カスタムHTTPエンドポイントの場合、TURBO_REMOTE_CACHE_URLを設定します。

Q3: fetch-depth: 0はCIのパフォーマンスとセキュリティにどのような影響を与えますか?

fetch-depth: 0は、Gitに最新のコミットだけでなく、リポジトリの完全な履歴をフェッチするように指示します。

  • パフォーマンス: 非常に大規模で長い履歴を持つリポジトリの場合、チェックアウト時間に数秒追加される可能性があります。しかし、効果的なTurborepoキャッシングによって節約される時間に比べれば、無視できることが多いです。
  • セキュリティ: 完全な履歴をフェッチするということは、より多くのデータがCIランナーにプルされることを意味します。公開リポジトリの場合、これは通常問題になりません。プライベートリポジトリの場合、CI環境が安全であることを確認してください。これは、Turborepoの正確な変更検出のために必要なトレードオフです。

Q4: ローカルでTurborepoのキャッシュ問題をデバッグするにはどうすればよいですか?

pnpm turbo runと一緒に--dry-runフラグを使用します。

  • pnpm turbo run build --filter=api... --dry-run: どのタスクが実行されるか、そしてその理由(例:cache MISS、cache HIT)を表示します。
  • pnpm turbo run build --filter=api... --dry-run=json: 各タスクの計算されたハッシュ、その入力、および出力を含む詳細なJSON出力を提供します。これは、キャッシュミスが発生した理由を理解する上で非常に貴重です。
  • 詳細なログのためにTURBO_LOG_LEVEL=debugを設定します。

Q5: turbo pruneとマルチステージビルドを使用しているにもかかわらず、Dockerイメージがまだ大きいのはなぜですか?

  • 不要なファイルがコピーされている: .dockerignoreファイルを再確認してください。開発専用ファイル、.git、node_modules(pnpm installステップを除く)、およびその他のビルド成果物を除外していることを確認してください。
  • ランタイムイメージにビルド時依存関係が含まれている: プロダクションステージ(例のrunner)が、ビルドされた成果物とプロダクション依存関係のみをコピーしていることを確認してください。node_modulesをbuilderステージから--prodなしで直接コピーすると、開発依存関係が含まれてしまいます。
  • 大きなベースイメージ: node:20-alpineは一般的に軽量です。プロダクションには、node:20(Debianベース)のような大きなベースイメージを避けてください。
  • 残されたビルドツール: runnerステージに、builderステージからのコンパイラ、リンター、その他のビルドツールが含まれていないことを確認してください。例のDockerfileは、特定のdistディレクトリのみをコピーし、プロダクション依存関係のためにpnpmを再インストールすることで、この問題に正しく対処しています。

これらの戦略を綿密に適用することで、組織はCIビルド時間を大幅に短縮し、より迅速なフィードバックループ、より頻繁なデプロイ、および開発者の生産性向上を実現できます。

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement