• ホーム
  • ブログ
  • S3 + GitHub Actions で Turborepo Remote Cache を自前構築し、モノレポ CI を高速化した話

S3 + GitHub Actions で Turborepo Remote Cache を自前構築し、モノレポ CI を高速化した話

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

鶴田 篤広

ソフトウェアエンジニア

作成日:

TurborepoGitHub ActionsCI/CDAWS S3モノレポパフォーマンス最適化
S3 + GitHub Actions で Turborepo Remote Cache を自前構築し、モノレポ CI を高速化した話 - Vercel の有料 Remote Cache を使わずに、S3 と GitHub Actions ...

TL;DR

  • Turborepo の Remote Cache を S3 + GitHub Actions で自前構築
  • Vercel の有料プラン不要で、コストとセキュリティを改善
  • キャッシュヒット時に CI ビルド時間を最大 90% 削減(5分 → 30秒)
  • OIDC 認証で安全に AWS リソースにアクセス
  • GitHub Actions Cache との違い: ブランチ制限なし、タスクハッシュベースの自動キャッシュ無効化
  • Docker ビルドとの併用で、複数キャッシュ戦略を最大活用
  • 主要ツール: trappar/turborepo-remote-cache-gh-action + S3

はじめに

前回の記事では、CDK から GitHub Actions に移行して Docker ビルドを並列化し、CI/CD 時間を約 80 分から 20 分に短縮した方法を紹介しました。その記事の最後で触れた「Turborepo の Remote Cache」について、今回は詳しく解説します。

モノレポ構成のプロジェクトでは、複数のパッケージやアプリケーションを管理するため、ビルドやテストに時間がかかりがちです。Turborepo は、タスクの依存関係を理解し、変更のないパッケージのビルドをスキップすることで、ビルド時間を大幅に短縮してくれます。

しかし、Local Cache だけでは CI 環境でその恩恵を受けることができません。CI の各ジョブは毎回クリーンな状態から始まるため、過去のビルド結果を再利用できないからです。

この問題を解決するのが Remote Cache です。ビルド結果をリモートストレージに保存し、CI の各ジョブ間で共有することで、キャッシュヒット時にビルドをスキップできます。

この記事では、Vercel の有料 Remote Cache サービスを使わずに、S3 と GitHub Actions で自前の Remote Cache を構築した方法を紹介します。

Turborepo Remote Cache とは

Local Cache の限界

Turborepo は、タスクの実行結果をキャッシュすることで、同じ入力に対する再実行をスキップします。デフォルトでは、このキャッシュは .turbo ディレクトリにローカル保存されます。

# 初回実行: キャッシュミス
$ turbo run build
...
Tasks: 5 successful, 5 total
Cached: 0 cached, 5 total
Time: 2m 30s
 
# 2回目実行: キャッシュヒット(変更がない場合)
$ turbo run build
...
Tasks: 5 successful, 5 total
Cached: 5 cached, 5 total
Time: 0.5s  # 劇的に高速化

しかし、CI 環境では毎回新しいランナーが割り当てられるため、.turbo ディレクトリは空の状態から始まります。つまり、ローカル開発では享受できるキャッシュの恩恵が、CI では得られないという課題があります。

Remote Cache で解決

Remote Cache は、ビルド結果をリモートストレージ(S3、Azure Blob、Google Cloud Storage など)に保存し、異なるマシン間で共有する仕組みです。 ダイアログ1

これにより:

  • CI Job A でビルドした結果が S3 に保存される
  • CI Job B が同じコードをビルドする際、S3 からキャッシュを取得してスキップ
  • CI Job C も同様にキャッシュを活用

GitHub Actions Cache との比較

GitHub Actions には標準で actions/cache によるキャッシュ機能が提供されています。では、なぜ Turborepo Remote Cache を別途導入するのでしょうか?

観点 GitHub Actions Cache Turborepo Remote Cache
容量制限 リポジトリあたり 10GB(デフォルト) ストレージ依存(S3 なら実質無制限)
保持期間 7 日間アクセスがないと自動削除 自分で設定可能(ライフサイクルルール)
キャッシュキー 手動でキーを設計(ファイルハッシュ等) タスクの inputs から自動計算
ブランチ制限 現在のブランチ、デフォルトブランチ、PR のベースブランチのみ ブランチ制限なし
キャッシュ無効化 キーの変更で手動管理 inputs の変更で自動無効化
用途 汎用的(node_modules、ビルドツール等) Turborepo タスク専用

GitHub Actions Cache の特徴

GitHub 公式ドキュメントによると、GitHub Actions Cache には以下の制約があります:

  • 容量: リポジトリあたり 10GB(2025年11月のアップデートで、Pro/Team/Enterprise プランでは従量課金で 10GB 超も可能に)
  • 保持期間: 7 日間アクセスがないキャッシュは自動削除
  • ブランチスコープ: キャッシュは作成されたブランチにスコープされ、他のブランチからは制限付きでのみアクセス可能
  • キー長制限: キャッシュキーは最大 512 文字

参考: GitHub Actions cache size can now exceed 10 GB per repository

Turborepo Remote Cache の特徴

Turborepo 公式ドキュメントによると、Turborepo Remote Cache は以下の特徴があります:

  • タスクハッシュベース: turbo.json で定義した inputs からハッシュを自動計算し、キャッシュキーを生成
  • ブランチ制限なし: どのブランチからでも同じハッシュならキャッシュを共有
  • 自動無効化: inputs に含まれるファイルが変更されると自動的にキャッシュが無効化
  • ストレージ選択: S3、Azure Blob、GCS など、任意のストレージを使用可能

使い分けの指針

GitHub Actions Cache が適しているケース:

  • node_modules のキャッシュ(package-lock.json をキーに使用)
  • ビルドツールのキャッシュ(Gradle、Maven など)
  • 単一パッケージのプロジェクト

Turborepo Remote Cache が適しているケース:

  • モノレポ構成でパッケージ間の依存関係がある
  • ビルド成果物を異なるブランチ間で共有したい
  • キャッシュの自動無効化を Turborepo に任せたい

今回のケースでは、両方を併用しています:

  • node_modules → GitHub Actions Cache
  • Turborepo タスクの成果物 → Turborepo Remote Cache (S3)

なぜ自前の S3 なのか

Turborepo は公式に Vercel Remote Cache を提供しています。しかし、今回のケースでは自前の S3 を選択しました。その理由は以下の通りです:

観点 Vercel Remote Cache 自前 S3
コスト 有料プラン必要(Pro/Enterprise) S3 の従量課金のみ
データの場所 Vercel のインフラ 自社の AWS アカウント
カスタマイズ 制限あり 自由にカスタマイズ可能
セキュリティ Vercel に依存 自社ポリシーで管理

特に、機密性の高いコードを外部サービスに送信したくない場合や、既存の AWS インフラを活用したい場合は、自前構築が適しています。

アーキテクチャ概要

自前 Remote Cache の全体構成は以下の通りです:

ダイアログ2

使用する主なツール:

実装手順

1. AWS リソースの準備

まず、キャッシュを保存する S3 バケットと、GitHub Actions からアクセスするための IAM ロールを作成します。

S3 バケットの作成

# S3 バケットを作成(バケット名は一意にする必要があります)
aws s3 mb s3://your-turborepo-cache-bucket --region ap-northeast-1
 
# ライフサイクルルールを設定(古いキャッシュを自動削除)
aws s3api put-bucket-lifecycle-configuration \
  --bucket your-turborepo-cache-bucket \
  --lifecycle-configuration '{
    "Rules": [
      {
        "ID": "DeleteOldCache",
        "Status": "Enabled",
        "Filter": {},
        "Expiration": {
          "Days": 30
        }
      }
    ]
  }'

IAM ロールの作成(OIDC 認証)

GitHub Actions の OIDC プロバイダーを使用することで、長期的なアクセスキーを保存せずに、安全に AWS リソースにアクセスできます。

まず、AWS アカウントに GitHub の OIDC プロバイダーを登録します(未登録の場合):

# OIDC プロバイダーの作成
aws iam create-open-id-connect-provider \
  --url https://token.actions.githubusercontent.com \
  --client-id-list sts.amazonaws.com \
  --thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1

次に、IAM ロールを作成します:

// trust-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::YOUR_ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:*"
        }
      }
    }
  ]
}
// s3-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:ListBucket",
        "s3:DeleteObject"
      ],
      "Resource": [
        "arn:aws:s3:::your-turborepo-cache-bucket/*",
        "arn:aws:s3:::your-turborepo-cache-bucket"
      ]
    }
  ]
}
# IAM ロールの作成
aws iam create-role \
  --role-name turborepo-remote-cache-role \
  --assume-role-policy-document file://trust-policy.json
 
# S3 アクセスポリシーをアタッチ
aws iam put-role-policy \
  --role-name turborepo-remote-cache-role \
  --policy-name S3CacheAccess \
  --policy-document file://s3-policy.json

2. カスタム GitHub Action の作成

Remote Cache サーバーの起動を再利用可能なアクションとしてカプセル化します。

# .github/actions/run-remote-cache-server/action.yml
name: 'Run Remote Cache Server'
description: 'Run server for remote cache'
inputs:
  remote_cache_s3_bucket_name:
    description: 'S3 bucket name for remote cache'
    required: true
  remote_cache_role_arn:
    description: 'IAM role ARN for remote cache'
    required: true
  remote_cache_port:
    description: 'Port for remote cache server'
    required: false
    default: '34444'
  remote_cache_host:
    description: 'Host for remote cache server'
    required: false
    default: 'http://127.0.0.1'
runs:
  using: 'composite'
  steps:
    - name: Configure AWS Credentials
      uses: aws-actions/configure-aws-credentials@v4
      with:
        role-to-assume: ${{ inputs.remote_cache_role_arn }}
        aws-region: ap-northeast-1
 
    - name: Start Turbo Cache Server
      uses: trappar/turborepo-remote-cache-gh-action@main
      with:
        storage-provider: s3
        storage-path: ${{ inputs.remote_cache_s3_bucket_name }}
        port: ${{ inputs.remote_cache_port }}
        host: ${{ inputs.remote_cache_host }}

このアクションは以下のことを行います:

  1. AWS 認証の設定: OIDC を使用して一時的な認証情報を取得
  2. Remote Cache サーバーの起動: trappar/turborepo-remote-cache-gh-action が内部で ducktors/turborepo-remote-cache サーバーを起動
  3. 環境変数の自動設定: TURBO_APITURBO_TOKENTURBO_TEAM が自動的に設定される

3. CI ワークフローへの統合

作成したカスタムアクションを CI ワークフローで使用します。

# .github/workflows/ci.yml
name: CI
on:
  push:
    branches: [main, develop]
  pull_request:
 
jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      id-token: write  # OIDC トークンの取得に必要
      contents: read
 
    steps:
      - uses: actions/checkout@v4
 
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'npm'
 
      - name: Install dependencies
        run: npm ci
 
      # Remote Cache サーバーを起動
      - name: Run remote cache server
        uses: ./.github/actions/run-remote-cache-server
        with:
          remote_cache_s3_bucket_name: ${{ secrets.REMOTE_CACHE_S3_BUCKET_NAME }}
          remote_cache_role_arn: ${{ secrets.REMOTE_CACHE_ROLE_ARN }}
 
      # Turborepo でビルド(キャッシュが自動的に使用される)
      - name: Build
        run: npm run build
 
      - name: Test
        run: npm run test
 
      - name: Lint
        run: npm run lint

GitHub Secrets に以下を設定します:

  • REMOTE_CACHE_S3_BUCKET_NAME: 作成した S3 バケット名
  • REMOTE_CACHE_ROLE_ARN: 作成した IAM ロールの ARN

4. Turborepo 設定

turbo.json でキャッシュ対象を適切に設定します。

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [
        "dist/**",
        ".next/**",
        "!.next/cache/**"
      ],
      "inputs": [
        "src/**",
        "package.json",
        "tsconfig.json"
      ]
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": ["coverage/**"],
      "inputs": [
        "src/**",
        "__tests__/**",
        "vitest.config.*"
      ]
    },
    "lint": {
      "outputs": [],
      "inputs": [
        "src/**",
        "eslint.config.*"
      ]
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  },
  "globalDependencies": [
    ".npmrc",
    "package-lock.json",
    "turbo.json"
  ]
}

ポイント:

  • outputs: キャッシュに保存するファイル/ディレクトリ
  • inputs: これらのファイルが変更されるとキャッシュが無効化される
  • globalDependencies: 全タスクに影響する依存ファイル
  • cache: false: dev タスクはキャッシュしない(常に実行)

並列実行時の工夫

動的ポート割り当て

並列実行される複数のジョブが同じランナー上で動作する場合、同じポートを使用すると競合が発生します。これを避けるため、ランダムなポート番号を動的に割り当てます。

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      # ランダムなポート番号を生成(40000-50000 の範囲)
      - name: Set random port
        id: set-port
        run: echo "port=$((40000 + RANDOM % 10000))" >> $GITHUB_OUTPUT
 
      - name: Run remote cache server
        uses: ./.github/actions/run-remote-cache-server
        with:
          remote_cache_s3_bucket_name: ${{ secrets.REMOTE_CACHE_S3_BUCKET_NAME }}
          remote_cache_role_arn: ${{ secrets.REMOTE_CACHE_ROLE_ARN }}
          remote_cache_port: ${{ steps.set-port.outputs.port }}
 
      - name: Lint
        run: npm run lint
 
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      # 別のジョブでも同様にランダムポートを使用
      - name: Set random port
        id: set-port
        run: echo "port=$((40000 + RANDOM % 10000))" >> $GITHUB_OUTPUT
 
      # ... 以下同様

マトリクスビルドでの固定ポート戦略

前回の記事で紹介した Matrix Strategy と組み合わせる場合、各マトリクスジョブに固定の一意なポート番号を割り当てます。

jobs:
  build-matrix:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        image:
          - name: app
            remote-cache-port: 32001
          - name: api
            remote-cache-port: 32002
          - name: worker
            remote-cache-port: 32003
      max-parallel: 5
      fail-fast: false
 
    steps:
      - uses: actions/checkout@v4
 
      # マトリクスで定義した固定ポートを使用
      - name: Run remote cache server
        uses: ./.github/actions/run-remote-cache-server
        with:
          remote_cache_s3_bucket_name: ${{ secrets.REMOTE_CACHE_S3_BUCKET_NAME }}
          remote_cache_role_arn: ${{ secrets.REMOTE_CACHE_ROLE_ARN }}
          remote_cache_port: ${{ matrix.image.remote-cache-port }}
 
      - name: Build
        run: npm run build:${{ matrix.image.name }}

Docker ビルドでの活用

Docker イメージのビルド時に Turborepo の Remote Cache を活用する場合、いくつかの追加設定が必要です。

Docker Buildx の network=host 設定

Docker コンテナ内から localhost で動作している Remote Cache サーバーにアクセスするには、network=host オプションが必要です。

- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v3
  with:
    driver-opts: network=host  # localhost へのアクセスを許可

環境変数の渡し方

Dockerfile 内で Turborepo を実行する場合、環境変数を build-args として渡します。

- name: Build and Push Docker Image
  uses: docker/build-push-action@v5
  with:
    context: .
    file: ./Dockerfile
    push: true
    build-args: |
      TURBO_API=${{ env.TURBO_API }}
      TURBO_TOKEN=${{ env.TURBO_TOKEN }}
      TURBO_TEAM=${{ env.TURBO_TEAM }}
# Dockerfile
FROM node:22-alpine AS builder
 
ARG TURBO_API
ARG TURBO_TOKEN
ARG TURBO_TEAM
 
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build  # turbo run build が実行され、Remote Cache が使用される

複数キャッシュ戦略の併用

最大限の効果を得るには、複数のキャッシュ戦略を組み合わせます:

  1. GitHub Actions Cache: node_modules のキャッシュ
  2. Turborepo Remote Cache (S3): ビルド成果物のキャッシュ
  3. Docker Layer Cache (GitHub Actions): Docker イメージレイヤーのキャッシュ
- name: Build and Push Docker Image
  uses: docker/build-push-action@v5
  with:
    context: .
    file: ./Dockerfile
    push: true
    # Docker レイヤーキャッシュ(GitHub Actions Cache を使用)
    cache-from: type=gha,scope=${{ matrix.image.name }}
    cache-to: type=gha,mode=max,scope=${{ matrix.image.name }}
    build-args: |
      # Turborepo Remote Cache
      TURBO_API=${{ env.TURBO_API }}
      TURBO_TOKEN=${{ env.TURBO_TOKEN }}
      TURBO_TEAM=${{ env.TURBO_TEAM }}

効果測定

キャッシュヒット率の確認

Turborepo のログからキャッシュヒット率を確認できます:

$ turbo run build
 
Tasks:    10 successful, 10 total
Cached:   8 cached, 10 total  # 10 タスク中 8 タスクがキャッシュヒット
Time:     15.234s

導入前後の比較

今回のケースでは、Remote Cache 導入により以下の効果が得られました(※環境により異なります):

シナリオ 導入前 導入後 効果
フルビルド(初回) 約 5 分 約 5 分 変化なし
変更なし(キャッシュヒット) 約 5 分 約 30 秒 90% 削減
一部変更(部分キャッシュヒット) 約 5 分 約 1-2 分 60-80% 削減

特に、同じコードベースに対する複数の PR や、main ブランチへのマージ後のデプロイなど、キャッシュが有効なシナリオで大きな効果を発揮します。

トラブルシューティング

キャッシュがヒットしない場合

  1. 環境変数の確認: TURBO_APITURBO_TOKENTURBO_TEAM が正しく設定されているか確認
  2. turbo.json の inputs 確認: 意図しないファイルが inputs に含まれていないか確認
  3. globalDependencies の確認: 頻繁に変更されるファイルが含まれていないか確認
# ドライランでハッシュを確認
turbo run build --dry=json
 
# 出力される JSON で taskHash を確認
# 同じ入力であれば同じハッシュになるはず

S3 への接続エラー

  1. IAM ロールの権限確認: S3 への適切なアクセス権限があるか確認
  2. OIDC 設定の確認: trust-policy の sub 条件がリポジトリと一致しているか確認
  3. リージョンの確認: S3 バケットと IAM ロールのリージョンが一致しているか確認

まとめ

S3 と GitHub Actions を使用して Turborepo Remote Cache を自前構築することで、以下のメリットを得ることができました:

  1. コスト削減: Vercel の有料プランを使わずに Remote Cache を実現
  2. セキュリティ: 機密性の高いコードを外部サービスに送信しない
  3. 柔軟性: 自社の AWS インフラと統合し、カスタマイズ可能
  4. CI 高速化: キャッシュヒット時に最大 90% のビルド時間削減

参考文献

こんなお悩みはありませんか?

1

システムの改修・刷新

古いシステムを使い続けているが、そろそろ限界を感じている

2

技術の相談相手がいない

社内にエンジニアがおらず、技術的な判断を相談できる人がいない

3

新規サービスを小さく始めたい

アイデアはあるが、まずは最小限の形で試してみたい

4

業務の効率化・自動化

手作業やExcel管理から脱却し、業務をシステム化したい

5

AIを活用したい

ChatGPTなどのAIを業務に取り入れたいが、どう始めればいいかわからない

このようなお悩みをお持ちの企業様に、
クレインテックが伴走支援いたします。

初回のご相談・お見積もりは無料です。

この記事をシェア

クレインテックに相談する

お客様と一緒に課題を整理し、小さく始めて育てる「共創型開発」を行っています。

「こんなシステムは作れる?」「費用感を知りたい」など、どんな段階でもお気軽にご相談ください。

初回のご相談は無料です。

お問い合わせ