Caru.moeCARU.moe
プロジェクトスキル経歴ブログ
ブログ一覧

© 2026 Caru

  • X
  • Discord
  • メール
Turborepo

CABG-012 · 2026.10.11

Turborepoを使いこなしてCIもローカルも速くしよう

並列実行で、モノレポのビルドやテストが2倍以上速く!Turborepoの活用法をまとめました

Monorepo · CI · GitHub Actions

はじめに

どうも、Caruです!

Turborepoは「設定の意味が分かりにくい」と敬遠されがちです。自分も最初はそうでした。でも使ってみると、入れるだけで効く機能と、少し設定すると効く機能がはっきり分かれています。この記事では機能ごとに「こんなことができる」「どれくらい速くなる」「ここで詰まる」を書いていきます。節ごとに独立しているので、気になる機能から読んでください。

先に数字を並べます。自分が開発しているCLIツールのモノレポ(12タスク)で測ったものです。12タスクを1つずつ実行すると1m30.0sかかりますが、turboを入れただけで並列実行になって40.1sで終わります。ローカルキャッシュは2回目から効いて、check-typesが5.294sから152msに。別のgit worktreeを作っても同じキャッシュが使われて、12タスクが655msで済みます。--affectedなら、ワークフローのファイルだけの変更で実行されるタスクは0です。Remote Cacheに全部ヒットすれば、CIのturboのステップは1.137sで終わります。速い!

ただ、キャッシュは万能ではありません。効くのは同じ入力でもう一度実行したときだけで、Remote Cacheにしてもこの条件は変わりません。なので、キャッシュを正しく効かせたうえで、--affectedも組み合わせます。turbo 2.11時点の話です。

この記事で扱うのは次の内容です。

  • Turborepoとは何か、似たツールとの違い
  • 並列実行
  • ローカルキャッシュと、git worktreeでの共有
  • actions/cacheでCIに持ち越す
  • --affectedで変更された部分だけ実行する
  • Remote CacheでGitHub Actionsの外とも共有する
  • よく使う小ワザ

Turborepoとは

Turborepoは、モノレポのタスク(build、test、check-typesなど)を速く実行するためのビルドシステムです。公式サイトは今、「the build system for coding agents」と名乗っています。モノレポ向けに作られていますが、パッケージが1つのリポジトリでも、タスクの実行は速くなります。

導入は、すでに書いてあるpackage.jsonのscriptsと依存関係をそのまま使い、リポジトリ直下にturbo.jsonを1つ追加するだけです。パッケージマネージャーはpnpm、npm、yarn、bunのどれでも構いません。依存関係のインストールはパッケージマネージャーの仕事のままで、turboはworkspacesの設定からパッケージ同士の関係を読み取り、タスクを実行します。

turboが足してくれるのは、依存関係の順序を守った並列実行、ローカルキャッシュとRemote Cache、変更の影響を受けたタスクだけを選ぶ--affectedの3つです。この記事もこの3つの使い方が中心です。

TurborepoはRust製(1.11でGoからの移植が完了)で、ライセンスはMITです。作者はJared Palmerで、2021年末にVercelが買収してオープンソースにし、今もVercelがメンテナンスしています。Vercelのアカウントは必須ではありません。デフォルトでは実行したマシンにだけキャッシュし(ローカルキャッシュ)、Remote Cacheは任意で、Vercelのものでも自前ホストでも使えます。2.11では、futureFlagsの実験的機能として、Go、Rust、Pythonのワークスペース(go.work、Cargo workspaces、uv workspaces)も対象にできます。

似たツールとの違い

Turborepoと同じように、モノレポのタスクを依存関係の順に実行するツールはほかにもあります。違いは、タスクの実行以外に何を担当するかです。

  • Turborepo: 既存のpackage.jsonのscriptsをそのまま実行します。依存関係のインストール、パッケージの公開、Node.jsなどのツールのバージョン管理は行いません
  • pnpm単体: pnpm -r run <script>で、各パッケージのscriptを依存先から順に実行します。同時に実行する数のデフォルトは4とCPUコア数の小さいほうで、--workspace-concurrencyで変更できます。--filter "...[origin/main]"を付けると、origin/mainから変更されたパッケージと、それに依存するパッケージだけを実行します。タスクの結果はキャッシュしません。pnpm 12には、キャッシュ機能のある実験的なpnpm pipelineコマンドもあります
  • Nx: タスクの実行とキャッシュに加えて、プラグインがプロジェクト同士の関係をソースコードから、タスクをvite.config.tsのようなツールの設定ファイルから推測します。フレームワークごとのコードを生成するgenerator、nx affected、バージョン管理と公開を行うnx releaseも付属します。プラグインを追加すると、JS/TS以外のプロジェクトも扱えます。Nx Cloud(無料のHobbyプランあり)を使うと、リモートキャッシュと、複数のCIマシンへのタスク分散を利用できます。ライセンスはMITです
  • Lerna: パッケージのバージョン管理とnpmへの公開が中心です。2022年からNxチームがメンテナンスしていて、v6以降はタスクの実行をNxに任せています。依存関係のインストールやリンクは行いません
  • moon: Rust製で、タスクの実行に加えて、Node.jsなどのツールを指定したバージョンでダウンロードして使うツールチェーン管理も行います。Node.js、Bun、Deno、Go、Rustに対応しています
  • Bazel: Google製のビルド・テストツールで、多くの言語を同じ仕組みでビルドします。パッケージごとにStarlarkという言語でBUILDファイルを書き、hermeticなビルド(ホスト環境から隔離され、同じ入力なら同じ出力になるビルド)を行います。JS/TSには、pnpmをもとにしたAspect製のrules_jsを使います

選ぶときの目安は次の通りです。

  • キャッシュが不要で、依存関係の順に実行できれば十分: pnpm単体
  • 既存のscriptsに、並列実行・キャッシュ・--affectedを追加したい: Turborepo
  • フレームワークごとのプラグインや、複数マシンへのタスク分散を使いたい: Nx
  • パッケージのバージョン管理と公開が主な目的: Lerna
  • ツールのバージョン管理も一緒にしたい: moon
  • 複数の言語を、ホスト環境に左右されない形でビルドしたい: Bazel

例にするモノレポ

例にするモノレポの構成。apps/server(Bun + Hono)とapps/web(React + Vite)のビルド結果をbun build --compileで1つのバイナリにまとめ、リポジトリ直下のdist/に出力する。apps/site(Fumadocs + vinext)はdocs/を読んでCloudflare Workersにデプロイされる

数字を測ったのは、pnpm + Turborepoで組んだCLIツールのモノレポです。プライベートリポジトリなので、構成だけ書いておきます。

  • apps/server: Bun + Hono。APIサーバーとCLI本体で、bun build --compileで単一バイナリにしてリポジトリ直下のdist/に出力します。apps/webのビルド結果もこのバイナリに埋め込みます
  • apps/web: React + Vite + TanStack Router。管理画面のSPAです
  • apps/site: ドキュメントサイト。vinext上のFumadocsで、ページはリポジトリ直下のdocs/から生成し、Cloudflare Workersにデプロイします
  • packages/api: oRPCのルーターとスキーマ
  • packages/ui、packages/config: 共有コンポーネントと共有設定

CIはGitHub Actionsで、pnpm turbo run check-types test buildを実行します。全部で12タスクです。ローカル環境の数字は、特に書いていない限り、turbo 2.11.6で、同じマシン・同じコミットのこの12タスクを実行したものです。

入れるだけで並列実行になる

並列実行は、turboを入れた時点で使えています。設定は要りません。--cache=local:でローカルキャッシュを無効にして12タスクを実行すると40.1s、同じ12タスクを--concurrency=1で1つずつ実行すると1m30.0sです。実行するタスクも依存関係の順序も同じで、違うのは同時に実行する数だけです。--concurrencyのデフォルトは10なので、依存先が終わったタスクは最大10個まで同時に実行されます。何も設定していないのに半分以下です!

キャッシュは同じ入力なら実行しない

キャッシュは、ローカルでもCIでも同じ設定で効きます。逆に設定が間違っていると、CIやRemote Cacheを通して間違った成果物が配られます。いちばん大事な設定なので、詰まりやすいところと直し方を厚めに書きます。

2回目からは一瞬で終わる

turboはタスクを実行する前に、入力からハッシュを計算します。入力は、パッケージ内のgit管理下のファイル、envやglobalEnvに書いた環境変数の値、globalDependencies、依存先タスクのハッシュなどです。同じハッシュのキャッシュがあればタスクは実行せず、outputsに指定したファイルを復元してログを再生します。この成果物とログのひとまとまりを、アーティファクトと呼びます。

入力のハッシュが同じならoutputsを復元してログを再生し、違えば実行して結果を保存する流れ

turbo run check-types(5タスク)は、--forceでキャッシュを無視すると5.294s、2回目は152msでした。>>> FULL TURBOの表示が気持ちいいです。

 Tasks:    5 successful, 5 total
Cached:    5 cached, 5 total
  Time:    152ms >>> FULL TURBO

パッケージの境界をまたぐファイルには設定が要る

キャッシュが正しく効くかどうかは、タスクがパッケージの外のファイルをどう読み書きするかで決まります。パターンは3つで、それぞれ設定が違います。

パッケージの境界をまたぐファイル。web#check-typesの生成物はoutputsに、site#buildが読むdocs/はinputsに、server#buildが出力するdist/はcache: falseで扱う

  • 生成したファイルをほかのタスクが読む: outputsに指定する
  • パッケージの外のファイルを読む: inputsに$TURBO_ROOT$で追加する
  • パッケージの外に出力する: $TURBO_ROOT$でoutputsに書くか、cache: falseにする

順番に書きます。

outputsはヒットしたときに復元されるファイル

outputsに書いたファイルは、キャッシュにヒットしたときに復元されます。省略するか空にすると、キャッシュされるのはログだけで、ファイルは戻ってきません。Next.jsのbuildなら[".next/**", "!.next/cache/**", "!.next/dev/**"]が定番です。.next/cacheは本番ビルドに要らず、入れてもアーティファクトが太るだけです。

詰まりやすいのは、キャッシュミスのときは成功するのに、ヒットすると失敗するケースです。公式ドキュメントにも「その場合はoutputsの設定が間違っている可能性が高い」とあります。自分もまさにこれを踏みました。

踏んだのはcheck-typesです。apps/webのcheck-typesは、tscの前にtsr generateでTanStack Routerのルートツリーをsrc/routeTree.gen.tsに、typed-css-modulesでCSS Modulesの型を.css-types/に生成します。この生成物はほかのタスクやエディタも読みます。ところがキャッシュにヒットすると生成ステップが実行されないので、outputsに入れていない限りファイルがありません。直し方は、生成物をoutputsに入れるだけです。

// These generate the CSS Module types and the route tree before compiling, and other
// tasks and the editor read them, so a cache hit has to restore them.
"web#check-types": {
  "dependsOn": ["^check-types"],
  "outputs": [".css-types/**", "src/routeTree.gen.ts"],
},

inputsでパッケージの外のファイルも入力にできる

inputsのデフォルトはパッケージ内のgit管理下ファイル全部です。外のファイルも入力にしたいときは、../ではなく$TURBO_ROOT$から始まるグロブで、リポジトリルートからの相対パスを書きます。inputsを指定するとデフォルトは丸ごと置き換わるので、残したいなら$TURBO_DEFAULT$も一緒に書きます。

リポジトリ直下のdocs/からページを生成するapps/siteなら、こうなります。

// The site renders its pages from the repo root's docs/, so a change there has to miss the cache.
"site#build": {
  "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/docs/**"],
},

これを忘れると、docs/を変更してもsite#buildがキャッシュにヒットして、古いページのままの成果物が復元されます。見た目は成功なので気づきにくいです。

パッケージの外に出力するタスクはcache: falseも手

apps/serverは、bun build --compileで単一バイナリをリポジトリ直下のdist/に出します。こういう成果物も$TURBO_ROOT$/dist/**のようにoutputsに書けます。ただ、コンパイルは1秒程度でキャッシュする価値が薄く、ネイティブバイナリは共有キャッシュと相性が悪いので(「共有する前に気をつけること」で書きます)、server#buildはcache: falseで十分です。"<package>#<task>"の書き方で、パッケージごとにタスクの設定を上書きできます。devもcache: falseで、persistent: trueを付けます。

環境変数はenvに書いた分だけハッシュに入る

envModeはデフォルトで"strict"です。envやglobalEnvに書いた変数だけがタスクに渡り、値はハッシュにも入ります。passThroughEnv / globalPassThroughEnvに書いた変数はタスクに渡りますが、ハッシュには入りません。使い分けの目安は、成果物に埋め込まれる値(NEXT_PUBLIC_*やAPIのURL)はenv、devに渡すだけの値やデプロイ用のシークレットはpassThroughEnvです。

"loose"は罠です。全部の環境変数がタスクに渡り、envに書き忘れても動きます。ただし書いていない変数はハッシュに入りません。ドキュメントの例だと、プレビュー用のMY_API_URLでビルドしたあと本番用にビルドすると、キャッシュにヒットしてプレビュー用の値が入ったままの成果物が出てきます。"envMode": "loose"が残っていたら、消してデフォルトのstrictに戻し、タスクに渡す変数をenvかpassThroughEnvに書きます。

もうひとつ、turboは.envをタスクに読み込みません。.envを変更したときにキャッシュを外したいなら、"inputs": ["$TURBO_DEFAULT$", ".env*"]のようにinputsへ追加します。ツールのバージョンを固定しているファイル(miseならmise.toml)はglobalDependenciesに入れておくと、Nodeやpnpmを上げたときに全タスクのハッシュが変わります。

効かないときは--dryと--summarize

turbo run build --dry(--dry=jsonも可)で、実行せずに何が実行されるかとそのハッシュを確認できます。turbo run build --summarizeは.turbo/runs/<run-id>.jsonに入力・ハッシュ・所要時間・キャッシュしたファイルを出力します。2回分を比較すれば、ハッシュが変わった理由がわかります。

git worktreeを作ってもキャッシュはそのまま使える

これは地味にうれしい機能です。git worktreeは、1つのリポジトリから別のディレクトリにもう1つチェックアウトを作成するgitの機能です。cacheDirを設定していなければ、git worktree addで作成したworktreeは、メインのworktreeの.turbo/cacheをそのまま使います。

同じコミットのworktreeで同じ12タスクを実行すると、11/12がキャッシュにヒットして655msで終わりました。実行されるのはcache: falseのserver#buildだけで、この11/12がこのモノレポの最大ヒット数です。まとめには• Remote caching disabled, using shared worktree cacheと出ます。worktreeには.turbo/cacheがなく、apps/web/distやsrc/routeTree.gen.tsのような成果物はメインのworktreeのキャッシュから復元されます。

そのworktreeでapps/webのファイルを1つ変更して実行すると、web#check-types、web#test、web#buildとserver#check-typesが実行され(server#buildは毎回実行されます)、7/12がヒットして4.864sでした。

複数のAIコーディングエージェントを別々のworktreeで動かす使い方と相性がよく、公式のAIガイドの「Git worktrees for parallel agents」もこの点を挙げています。互いのキャッシュ済みの結果をそのまま使えます。

注意点がひとつ。アーティファクトは中身を書き換えずに復元されるので、worktreeの絶対パスを含む成果物は、ヒットしたあとに別のチェックアウトを指すことがあります。避けたいなら、.turbo/cacheのような相対パスのcacheDirを明示します。worktreeごとに別のキャッシュになり、共有されなくなります。

CIにもキャッシュを持ち越せる

CIは毎回新しいマシンで始まるので、何もしなければ.turbo/cacheは空で、タスクが毎回全部実行されます。キャッシュが空のCIでは、turboのステップに1m13.9sかかりました(この計測時は11タスクでした)。前の実行のキャッシュを次に持ち越すには、GitHub Actionsならactions/cacheで.turbo/cacheを保存するだけです。

- uses: actions/checkout@v7
  with:
    persist-credentials: false
    fetch-depth: 0
- uses: jdx/mise-action@v5
- run: pnpm install --frozen-lockfile
- uses: actions/cache@v6
  with:
    path: .turbo/cache
    key: turbo-${{ runner.os }}-${{ github.sha }}
    restore-keys: turbo-${{ runner.os }}-
- run: pnpm check:ci
- env:
    TURBO_CACHE_MAX_SIZE: 1GB
  run: pnpm turbo run check-types test build

keyにコミットSHAが入っているので毎回新しいエントリが保存され、restore-keysの前方一致で直近のエントリが復元されます。Turborepoの公式GitHub Actionsガイドと同じパターンです(fetch-depth: 0は--affected用で、あとで書きます)。

毎回.turbo/cacheを丸ごと保存するので、放っておくとキャッシュは育ち続けます。TURBO_CACHE_MAX_SIZE(turbo.jsonならcacheMaxSize)を設定しておくと、turbo runの開始時に上限を超えた分を古い順に削除してくれます。GitHub Actions側のキャッシュはリポジトリあたりデフォルト10GBで、7日間アクセスがないエントリは削除されます。PRの実行では、ベースブランチのキャッシュも使えます。

重いタスクがミスすると、ヒット数のわりに縮まない

actions/cacheを入れても、CIの時間が期待ほど縮まないことがあります。時間を決めるのはヒットの数ではなく、どのタスクがミスしたかです。このモノレポではsite#buildがdocs/を読むので、docs/を変更したpushでは毎回ミスします。site#buildはいちばん重いタスク(ローカルでキャッシュなしだと単体で38.5s)なので、12タスク中8つがヒットしたpushでも、turboのステップには1m0.1sかかりました。

入力が変わった重いタスクのミスは、キャッシュでは避けられません。できるのは、--summarizeや--dryで意図しないミスがないかを確認することと、--affectedで実行するタスクそのものを減らすことです。

--affectedで関係あるタスクだけ実行する

turbo run build --affectedのように付けると、変更の影響を受けるパッケージのタスクだけが実行されます。比較対象はデフォルトでmain...HEAD(TURBO_SCM_BASE / TURBO_SCM_HEADで変更可)で、GitHub Actions上ではpull_requestでもpushでも自動で取得してくれます。公式のCIガイドも、Remote Cacheを使わずにCIの仕事を減らす手段としてこれを挙げています。

詰まりやすいのは、付けたのに全タスクが実行されるケースです。--affectedにはgitの履歴が必要で、actions/checkoutはデフォルトで深さ1のshallow clone(直近のコミットしか持たないclone)なので、そのままだと比較できずに全部変更扱いになります。直し方はfetch-depth: 0で履歴を全部取得することです。さっきのYAMLにfetch-depth: 0があるのはこのためです。

futureFlagsで判定をタスク単位にできる

"futureFlags": {
  // `--affected` selects a task only when a changed file is among its inputs, so a change outside
  // the packages (docs/, the workflows) runs only the tasks that read it.
  "affectedUsingTaskInputs": true,
  // A pull request checks out detached, with its base branch only as origin/<branch>.
  "githubActionsRemoteBaseRefFallback": true,
},

affectedUsingTaskInputsがないと判定はパッケージ単位で、パッケージ内のどのファイルを変更してもそのパッケージの全タスクが選ばれます。有効にするとタスク単位になり、変更ファイルがそのタスクのinputsに含まれるときだけ選ばれます。site#buildに追加した$TURBO_ROOT$/docs/**のように、パッケージの外の変更も拾えます。ルートのpackage.json、turbo.json(c)、ロックファイル、globalDependenciesのファイルを変更したときは、このフラグがあっても全タスクが選ばれます。

affectedUsingTaskInputsを有効にすると、docs/の変更ではsite#buildとsite#deployだけが選ばれ、serverのタスクはスキップされる

githubActionsRemoteBaseRefFallbackは、PRのベースブランチがorigin/<branch>としてしかないときに、それを比較対象にする設定です。actions/checkoutはdetached(どのブランチにも乗っていない状態)でチェックアウトするので、これがないとベースが見つからないことがあります。

変更の種類で選ばれるタスクはここまで減る

変更の種類ごとに--affected --dryで選ばれるタスクを数えるとこうなります(scriptのあるタスクだけ数えています。全部実行すると12タスクです)。

変更したファイル 選択されたタスク
docs/ のMarkdownだけ 1(site#build)
ワークフローのファイルだけ 0
apps/web だけ 7

apps/webだけの変更で選ばれた7タスクは、ローカルでキャッシュなしだと5.17sで終わります。12タスク全部だと40.1sです。一方、docs/だけの変更は1タスクなのに38.5sかかります。site#buildがいちばん重いからです。--affectedはタスクの数を減らすだけなので、残ったタスクが重ければそのぶん時間はかかります。

キャッシュが効いている状態なら、影響のないタスクは--affectedがなくてもヒットします。なので--affectedが時間を縮めるのは、cache: falseのタスク(デプロイや本物のコンテナを使うテストはこれでしかスキップできません)と、キャッシュが空か期限切れのときです。

PRでは付けて、リリースでは付けない

PRとdevelopへのpushでは--affectedを付け、リリースタグを付けるmainへのpushでは全タスクを実行する、という使い分けができます。シェルの${AFFECTED:+--affected}で、AFFECTEDが空でないときだけフラグが付きます。

- env:
    AFFECTED: ${{ (github.event_name == 'pull_request' || github.ref == 'refs/heads/develop') && 'true' || '' }}
  run: pnpm turbo run check-types test build ${AFFECTED:+--affected}

実行されないタスクがあるので、後続でバイナリを使うステップは、バイナリがあるときだけ実行するようにしておきます。

デプロイや重いテストにも効く

cache: falseのタスクでもinputsはaffectedの判定に使われます。これを使うと、デプロイやDockerを使うテストを、変更の影響があるときだけ実行できます。

// The deploy script builds the site for Cloudflare itself. CI runs it when the site is affected.
"site#deploy": {
  "dependsOn": ["^build"],
  "cache": false,
  "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/docs/**", "$TURBO_ROOT$/.github/workflows/site.yml"],
  "passThroughEnv": ["DOTENV_PRIVATE_KEY_PRODUCTION"],
},
// Real containers, so never cached. Affected by the drivers, the tests, and the engine versions.
"server#test:docker": {
  "cache": false,
  "inputs": ["src/live/**", "src/**/*.docker.test.ts", "package.json", "$TURBO_ROOT$/.github/workflows/docker.yml"],
},
run: pnpm turbo run deploy --filter=site ${AFFECTED:+--affected}

--affectedと--filterを両方付けると、どちらにも当てはまるパッケージだけが対象です。この設定だとサイトは、サイト本体・docs/・依存パッケージ・サイト自身のワークフローファイルのどれかが変わったpushでだけデプロイされます。ワークフローファイルをinputsに入れておくと、ワークフローを直したときにもデプロイが実行されるので安心です。Dockerのドライバテストも、ドライバのコード・テスト・ワークフローが変わったときだけ本物のDBコンテナを起動します(夜間に全部実行するスケジュールは別に組んでいます)。

選ばれるタスクが0でもジョブ自体は実行されるので、turboの前のpnpm install(14s)とpnpm check:ci(7s)はそのままかかります。ここも省きたいならturbo query affectedです。影響のあるパッケージを先に調べて、なければ依存のインストールごと省けます。--exit-codeを付けると、影響がなければ0、あれば1、エラーなら2で終了するので、ジョブの条件分岐に使えます。

Remote CacheでGitHub Actionsの外とも共有できる

Remote Cacheは、キャッシュのアーティファクトをサーバーに置いて、別のマシンからも読み書きできるようにしたものです。デプロイ、別のCI、ビルドサーバー、開発環境など、GitHub Actionsの外とも同じタスクの結果を共有できます。actions/cacheでできるのはGitHub Actionsの中での共有までなので、外に広げたくなったらこちらです。

書くのも読むのもCIだけなら、ヒットする条件はactions/cacheと同じです。公式ドキュメントにも「Remote Cachingを使わなくてもTurborepoは使える」と書いてあります。使うなら、トークンはOIDCで渡し、書き込むのはCIだけにするのがおすすめです。

VercelのRemote Cacheは、Vercelでホスティングしていなくても全プランで無料です(fair use、つまり常識的な使い方の範囲で、Hobbyはアップロード月100GB、アーティファクトのリクエストは毎分100回まで)。アップロードしたアーティファクトは7日で削除されます。turboに必要なのは、TURBO_TEAM(チームのslug。URLに使われるチーム名です)とトークンの環境変数2つだけです。

CIにはOIDCで渡せる(シークレット不要!)

CIにトークンを持たせる方法はOIDCとPAT(Personal Access Token。自分で発行して保存する長期のトークンです)の2つで、おすすめはOIDCです。GitHub Actionsがジョブごとに発行する短命の身分証明トークンを、Vercel側のトークンと交換する仕組みなので、シークレットを自分で発行して保存しなくて済みます。2026年7月30日にVercelのchangelogで発表されたもので、VercelもPATからOIDCへの移行を勧めています。

ジョブにpermissions: id-token: writeを付けてvercel/setup-turborepo-remote-cache-actionを追加すると、このアクションがトークンを交換して後続のステップに設定します。このトークンでできるのはRemote Cacheへのアクセスだけで、チームに紐づきます。v1.1.0はジョブの終わりにトークンも失効させてくれます(revoke入力、デフォルトtrue)。

Vercel側では先にOIDCポリシーを作ります。Hobbyプランでも使えました。場所はダッシュボードのAll Projects → Settings → Build and Deployment → 「OIDC Policies」で、「VCR Policies」と「Turborepo CLI Policies」の2行が並んでいるセクションです。アクションのREADMEにある直リンクから、追加フォームを直接開くこともできます。

VercelのSettingsにあるOIDC Policiesのセクション。VCR PoliciesとTurborepo CLI Policiesがどちらも0件で、それぞれにAddボタンがある

「Add Turborepo CLI Policy」のフォームで入力するのは、Policy Name、Provider(GitHub Actions)、Issuer URL(https://token.actions.githubusercontent.comが最初から入っています)、Workload identityのGitHubアカウントです。Repository・Workflow・Branch・Audienceは任意で、空にするとアカウント配下の全リポジトリが対象になります。Audienceも空で構いません。フォームの説明に「ワークフローがgetIDToken()にカスタム値を渡すときだけ設定する」とあり、このアクションはaudienceを渡さないので、GitHubのデフォルトであるhttps://github.com/<owner>が使われます。

Add Turborepo CLI Policyのフォーム。Provider、Issuer URL、Workload identity、Repository、Workflow、Branch、Audienceの入力欄が並ぶ

ワークフローはactions/cacheから3か所変わる

check:
  runs-on: ubuntu-24.04
  permissions:
    contents: read
    id-token: write
  steps:
    - uses: actions/checkout@v7
      with:
        persist-credentials: false
        fetch-depth: 0
    - uses: jdx/mise-action@v5
    - run: pnpm install --frozen-lockfile
    # A pull request from a fork gets no OIDC token, so its checks run without the shared cache.
    - if: github.event.pull_request.head.repo.fork != true
      uses: vercel/[email protected]
      with:
        team: ${{ vars.TURBO_TEAM }}
    - run: pnpm check:ci
    - env:
        AFFECTED: ${{ (github.event_name == 'pull_request' || github.ref == 'refs/heads/develop') && 'true' || '' }}
        # A pull request only reads the shared cache, so what it builds never reaches develop, main,
        # or our machines.
        TURBO_CACHE: ${{ github.event_name == 'pull_request' && 'local:rw,remote:r' || 'local:rw,remote:rw' }}
      run: pnpm turbo run check-types test build ${AFFECTED:+--affected}

1つ目は、actions/cacheのステップがvercel/setup-turborepo-remote-cache-actionに置き換わることです。TURBO_TEAMはリポジトリ変数で、シークレットは1つも要りません。forkからのPRにはOIDCトークンが出ないので、ifでこのステップをスキップします。その場合は共有キャッシュなしでチェックが実行されるだけです。

2つ目は、PRの実行にTURBO_CACHEを付けることです。書き方は--cacheオプションと同じで、local:rw,remote:rは「ローカルキャッシュは読み書き、リモートは読むだけ」という意味です。PRでビルドした成果物が、developやmainのCIに届かないようにしています。

3つ目は、TURBO_CACHE_MAX_SIZEが要らなくなることです。ランナーのローカルキャッシュは実行のたびに破棄されるので、上限を設定する意味がなくなりました。

書き込むのはCIだけにする

Remote Cacheに書くのは、developとmainへのpushだけにするのがおすすめです。PRはTURBO_CACHEで読むだけにして、開発環境からも書き込みません。

理由は、あるキーで一度保存されたアーティファクトを、あとから同じキーを読む全員が受け取るからです。turboは中身を見ません。ところが開発環境でビルドしたアーティファクトは、ハッシュが同じでもCIのものと中身が違うことがあります。OSやCPUアーキテクチャはハッシュに入っていませんし(「共有する前に気をつけること」で書きます)、.envもinputsに追加しない限り入りません。ログもそのまま再生されるので、開発環境のアーティファクトにCIがヒットすると、CIのログに開発環境のパスが出てきます。

開発環境で読むだけにするには、--cacheかTURBO_CACHE(たとえばlocal:rw,remote:r)、またはTURBO_REMOTE_CACHE_READ_ONLYを使います。ただしPRの設定と同じで、これらはクライアント側の設定であって、アクセス制限ではありません。Vercelに読み取り専用のトークンはなく、サーバー側で制限できるのはOIDCポリシーのRepository・Workflow・Branchだけです。間違ったアーティファクトが混ざったと思ったら、オーナーがVercelのダッシュボードからRemote Cacheのアーティファクトを削除できます。

効いているかはログで分かる

効いているときは• Remote caching enabledと出て、ヒットしたタスクごとにcache hit, replaying logsが出ます。OIDCなら最後にRevoked the Turborepo access token.も出て、トークンが失効したことが分かります。キャッシュできるタスクが全部ヒットすると、CIのturboのステップは1.137sでした(11/12で、残りの1つはcache: falseのserver#buildです)。1秒です!

トークンが渡っていなくてもturboはエラーにならず、ローカルキャッシュだけで動きます。そのときは• Remote caching disabledと出るので、この行が出ていないかも確認してください。

ヒットしても縮まないのは、turboの前のpnpm install(14s)とpnpm check:ci(7s)です。Remote Cacheに入るのはturbo runのタスクの成果物とログだけで、依存のインストールはパッケージマネージャーのキャッシュの担当だからです。ジョブ全体では、キャッシュできるタスクが全部ヒットしたときが約30s(install 14s、check:ci 7s、turbo 1s)、2/12しかヒットしなかったとき(actions/cacheの回)が約111s(turboのステップに1m17.1s)でした。

OIDCが使えないCIではPATを渡す

OIDCが使えないCIなら、PATをそのまま渡します。

env:
  TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
  TURBO_TEAM: ${{ vars.TURBO_TEAM }}

アーティファクトはVercelのチームに紐づくので、PATでアップロードしたものはOIDCに移行したあとも使えます。

自前でもホストできる

Remote CacheのAPIはOpenAPIの仕様が公開されていて、この仕様を実装したHTTPサーバーなら何でも使えます(turboのどのバージョンもv8のエンドポイントと互換です)。接続先は環境変数TURBO_API(サーバーのURL)、TURBO_TOKEN、TURBO_TEAMの3つで指定し、開発環境でもCIでも同じです。turbo.jsonのremoteCache.apiUrl(とteamSlug / teamId)でも指定できます。teamIdはteam_で始まるときだけ使われます。

選択肢は主にこの3つです。

  • ducktors/turborepo-remote-cache: TypeScript製でいちばん使われているもの(約1.5kスター、2026年10月7日にv2.14.3)。Dockerイメージで実行し、保存先はローカルファイルシステム、S3、GCS、Azure Blob、MinIOなどから選択できます
  • AdiRishi/turborepo-remote-cache-cloudflare: Cloudflare Workers + R2(またはKV)で動くもの(2026年1月19日にv4.0.0)。毎日のcronで古いオブジェクトを削除してくれます
  • brunojppb/turbo-cache-server: Rust製(2026年10月5日に4.0.19)で、S3互換ストレージに保存します。GitHub Actionとしてランナー上で起動する使い方もできます

turboのドキュメントにはTapico/tapico-turborepo-remote-cacheも載っていますが、最後のリリースが2021年で、メンテナンスされていなさそうです。

ducktorsのものは、Dockerイメージを起動するだけで動きます。環境変数はNODE_ENV=production、PORT=3000、TURBO_TOKEN(openssl rand -hex 32で生成したランダムな値)、STORAGE_PROVIDER=local、STORAGE_PATH=turbo-cacheの5つです。STORAGE_PROVIDER=localの保存先はコンテナ内のローカルストレージなので、ボリュームに載せない限り、再デプロイでキャッシュは消えます。試すだけならこれで十分です。GET /v8/artifacts/statusが{"status":"enabled","version":"2.14.3"}のように返ってくれば動いています。

TURBO_API=https://<your-cache-domain> TURBO_TOKEN=<token> TURBO_TEAM=<any name> turbo run check-types build --cache=remote:w

一時的に立てたサーバーに、開発環境からturbo run check-types build(8タスク)を投げた結果です。remote:wはRemote Cacheへの書き込みだけ、remote:rは読み込みだけ、local:はローカルキャッシュなしです。

実行 キャッシュ 時間
--cache=remote:w(アップロードだけ) 0/8 40.1s
--cache=remote:r(リモートから読むだけ) 7/8 855ms
--cache=local:(キャッシュなし) 0/8 41.7s

ミスしている1つは、やはりserver#buildです。

自前ホストは、サーバーとストレージの費用、古いアーティファクトの削除、認証を自分で引き受ける代わりに、fair useの制限がなく、保持期間も決められて、アーティファクトを自分のストレージに置けます。Coolifyならアプリ1つと環境変数5つで動きました。すでにCloudflareを使っているなら、Workers + R2のものも選択肢です。

共有する前に気をつけること

ログもアーティファクトとしてキャッシュされるので、シークレットを出力しないようにします。outputsやenvが間違ったまま共有キャッシュを有効にすると、正しくないアーティファクトが1つあるだけで全員に配られます。共有は、キャッシュの設定が正しくできてからにしましょう。

「書き込むのはCIだけにする」で書いた通り、PRでは読むだけというTURBO_CACHEの設定はアクセス制限ではありません。ブランチをpushできる人なら、PRの中でワークフローを書き換えれば書き込めます。本当に制限したいなら、VercelのOIDCポリシーのBranchをmainとdevelopに限定し、Repositoryも指定します。アカウント全体を対象にしたポリシーだと、コラボレーターを招待しているリポジトリのワークフローからも共有キャッシュに書けることになるので、気になるならリポジトリ単位でポリシーを作るほうが安全です。複数のポリシーが同じトークンに一致すると、policy入力を渡さない限り交換がエラーになるので、ポリシーは1つにしておくのが無難です。

もうひとつ、turboのハッシュにはデフォルトでOSやCPUアーキテクチャが入っていません。Macのdarwin-arm64でビルドしたアーティファクトでも、入力が同じならLinux x64のCIでそのまま復元されます。型チェック、lint、テスト、Next.jsやViteのJSバンドルのようにプラットフォームに依存しない結果ならそれで問題なく、むしろそれを共有するのがRemote Cacheの目的です。問題になるのはネイティブバイナリが成果物になるタスクで、bun build --compileの単一バイナリ、Prismaのエンジン、sharpなどが該当します。server#buildをcache: falseにしているのは、これも理由です。

ドキュメントの「Handling platforms」にある対処は、turbo runの前にプラットフォームとアーキテクチャをファイルに出力し、gitignoreしたうえでinputsに追加する方法です。

// scripts/create-turbo-cache-key.js
const { writeFileSync } = require("fs");
const { platform, arch } = process;
writeFileSync("turbo-cache-key.json", JSON.stringify({ platform, arch }));
"server#build": {
  "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/turbo-cache-key.json"]
}

タスクのinputsに入れれば、そのタスクだけがプラットフォームごとのキャッシュになります。globalDependenciesに入れると全タスクがプラットフォームごとになって、OSが違う開発環境とCIでは共有が効かなくなります。Node.jsのバージョンは、package.jsonのenginesか、globalDependenciesに入れたmise.tomlを変えるとハッシュが変わります。

"remoteCache": { "signature": true }とTURBO_REMOTE_CACHE_SIGNATURE_KEYを設定すると、アーティファクトにHMAC-SHA256の署名が付き、署名がないか不正なものはミス扱いになります。ドキュメントにある通り、これはセキュリティ機能ではなく整合性チェックで、転送の失敗やキャッシュサーバー側の問題を見つけるためのものです(鍵はfutureFlags.longerSignatureKey: trueで32バイト以上を強制できます)。

actions/cacheとどちらを使うか

actions/cacheはGitHub Actionsの実行同士でしか共有されず、Remote CacheはCIが書き込み、開発環境やデプロイ先のサーバーは読むだけにできる

actions/cache VercelのRemote Cache 自前ホスト
必要なもの なし トークンかOIDC(OIDCならSecret不要) サーバーとストレージ、トークン
転送 毎回キャッシュディレクトリを丸ごと復元・保存 各タスクに必要なアーティファクトだけ取得 同左
共有範囲 GitHub Actionsの中だけ 開発環境・他のCI・ビルドサーバーとも 同左
上限 10GB、7日間アクセスなしで削除 無料だがfair use制限、7日で失効 ストレージ次第、保持期間は自分で決める
運用 なし なし 古いアーティファクトの削除と認証は自分で

書き込むのがCIだけなら、ヒットする条件はどちらも同じです。それでもRemote Cacheを選ぶ理由があるとすれば、TURBO_CACHE_MAX_SIZEが要らなくなってキャッシュの大きさを管理しなくてよいことと、OIDCならトークンも管理しないことです。手間はactions/cacheと変わりません。GitHub Actionsの外とも共有したくなったら、Remote Cache一択です。

よく使う小ワザ

TUIでタスクごとにログを読める

地味に便利なのがTUIです。turbo.jsonに"ui": "tui"を書くと(--ui=tuiやTURBO_UI=1でも可)、turbo runの画面が、左にタスクの一覧、右に選択したタスクのログという構成になります。デフォルトのstreamは全タスクのログが1本に混ざって流れるので、turbo devでdev serverやwatchを何本も起動すると、どの行がどのタスクのものか追いにくくなります。TUIならタスクを選んで、そのログだけを読めます。

キー操作はmで一覧が出ます。turbo 2.11.7では次の通りです。

キー できること
↑/↓(k/j) タスクを選ぶ
/ タスク名で一覧を絞り込む(Escで解除)
p 選択のピン留めを切り替える
u/d、t/b ログをスクロールする、先頭・末尾に移動する
h 選択中のタスクのログだけを全画面で表示する(もう一度押すと戻る)
s 全タスクのログをstream形式で流す(もう一度押すと戻る)
Shift+h タスクの一覧を隠す
c マウスで選択したログをクリップボードにコピーする
i タスクに入力を送る(Ctrl+zで戻る)

いちばんうれしいのはiです。persistent: trueのタスクは最初からinteractive扱いで、iを押すとキー入力がそのタスクのstdinに届きます。Vitestのwatchモードでテストを絞り込んだり、Drizzleのマイグレーションの確認に答えたりが、turbo越しにできます。選んだタスクと一覧の表示/非表示は.turbo/preferences/tui.jsonに保存されるので、次にturbo devを起動したときも同じ状態で開きます。CIのようにTTYがない環境では自動でstreamになるので、turbo.jsonに書いたままで大丈夫です(CircleCIだけはTTYがあるのにTUIが落ちるので、TURBO_UI=falseを設定します)。

scriptsはturbo runで書く

turbo buildはturbo run buildの省略形ですが、package.jsonのscriptsにはturbo run <task>と書いておくのが安全です。将来turboに同名のサブコマンドが増えても衝突しません(ドキュメントの例はturbo deploy)。

"scripts": {
  "dev": "turbo run dev",
  "build": "turbo run build",
  "check-types": "turbo run check-types",
  "test": "turbo run test",
  "check": "oxlint --fix && oxfmt --write",
  "check:ci": "oxlint && oxfmt --check"
}

checkとcheck:ciがturboを通っていないのは、oxlintとoxfmtがturboのキャッシュが要らないほど速いからです。全部のタスクをturboに通す必要はなく、パッケージごとに分けてキャッシュする意味がないタスクは、ルートから1回実行するか、//#lintのようなルートタスクにします。TurborepoのOxcガイドもルートタスクを勧めています。

ほかのオプションと設定

  • --filter / -F: 1パッケージだけ実行します(turbo run dev -F web)
  • --log-order: デフォルトはautoで、CIではgrouped、それ以外ではstreamになります。CIではタスクごとにログがまとまり、GitHub Actionsでは折りたためるグループになります
  • --continue: デフォルトはneverで、タスクが失敗するとそこで止まります。dependencies-successfulかalwaysにすると、失敗しても残りのタスクを実行します。CIで全部の失敗を一度に見たいときに便利です
  • --output-logs: errors-onlyなら失敗したタスクのログだけ、new-onlyならキャッシュミスしたタスクのログだけを表示します
  • --graph: タスクの依存関係をsvg、html、mermaidで出力します(turbo run build --graph=graph.svg)。dependsOnの設定が意図通りか、図で確認できます
  • "agentGuidance": false: turboはAIコーディングエージェントから実行されたことを検出すると、ルートのAGENTS.mdに同梱ドキュメントへの案内ブロックを書き込みます。手で管理しているAGENTS.mdに追記されたくなければfalseにします(無効にしても既存のブロックは削除されません)
  • turbo.jsonc: コメントが書けます。設定の「なぜ」を残す場所に使えます
  • node_modules/turbo/docs: インストールしたバージョンと一致するドキュメントが入っています。オプションの確認はオフラインでも済みます

参考

https://turborepo.dev/docsturborepo.devhttps://turborepo.dev/docs/crafting-your-repository/cachingturborepo.devhttps://turborepo.dev/docs/crafting-your-repository/using-environment-variablesturborepo.devhttps://turborepo.dev/docs/crafting-your-repository/constructing-citurborepo.devhttps://turborepo.dev/docs/guides/ci-vendors/github-actionsturborepo.devhttps://turborepo.dev/docs/guides/aiturborepo.devhttps://turborepo.dev/docs/core-concepts/remote-cachingturborepo.devhttps://turborepo.dev/docs/reference/configurationturborepo.devhttps://turborepo.dev/docs/reference/runturborepo.devhttps://turborepo.dev/docs/reference/queryturborepo.devhttps://turborepo.dev/docs/guides/handling-platformsturborepo.devhttps://vercel.com/docs/monorepos/remote-cachingvercel.comhttps://vercel.com/docs/monorepos/remote-caching/external-ci-cdvercel.com

まとめ

機能ごとに、このモノレポで得られたものを並べるとこうなります。

機能 得られるもの
並列実行 開発環境で--concurrency=1だと1m30.0s、デフォルトで40.1s
ローカルキャッシュ 開発環境の2回目: check-types 5.294s → 152ms
git worktreeでの共有 別のworktreeでも11/12ヒットで655ms
actions/cache CIでも前回のキャッシュを復元。重いタスクがミスすると短縮されにくい
--affected ワークフローだけの変更で0タスク、apps/webだけの変更で12タスク中7タスク
Remote Cache ヒットすればCIのturboステップ1.137s。GitHub Actionsの外とも共有できる

turboを入れた時点で並列実行は効いています。キャッシュは同じ入力でもう一度実行したときだけ効くので、--dryや--summarizeで設定を確かめながら、--affectedも組み合わせます。GitHub Actionsの外とも共有したくなったらRemote Cacheです。設定の意味が分かると、turbo.jsonは怖くありません。自分のモノレポでも試してみてください!

参考になったよという方は、Xのフォローやシェアをもらえると嬉しいです!

Caruこの記事を書いた人

Web開発が大好きな、エンジニアを目指す大学生です。

  • GitHub
  • Link集
前の記事CloudflareダッシュボードのAsk AIがかなり便利CABG-011 · 2026.10.10一覧へブログの記事一覧CABG-001 〜 CABG-012

目次

  1. はじめに
  2. Turborepoとは
  3. 似たツールとの違い
  4. 例にするモノレポ
  5. 入れるだけで並列実行になる
  6. キャッシュは同じ入力なら実行しない
  7. 2回目からは一瞬で終わる
  8. パッケージの境界をまたぐファイルには設定が要る
  9. outputsはヒットしたときに復元されるファイル
  10. inputsでパッケージの外のファイルも入力にできる
  11. パッケージの外に出力するタスクはcache: falseも手
  12. 環境変数はenvに書いた分だけハッシュに入る
  13. 効かないときは--dryと--summarize
  14. git worktreeを作ってもキャッシュはそのまま使える
  15. CIにもキャッシュを持ち越せる
  16. 重いタスクがミスすると、ヒット数のわりに縮まない
  17. --affectedで関係あるタスクだけ実行する
  18. futureFlagsで判定をタスク単位にできる
  19. 変更の種類で選ばれるタスクはここまで減る
  20. PRでは付けて、リリースでは付けない
  21. デプロイや重いテストにも効く
  22. Remote CacheでGitHub Actionsの外とも共有できる
  23. CIにはOIDCで渡せる(シークレット不要!)
  24. ワークフローはactions/cacheから3か所変わる
  25. 書き込むのはCIだけにする
  26. 効いているかはログで分かる
  27. OIDCが使えないCIではPATを渡す
  28. 自前でもホストできる
  29. 共有する前に気をつけること
  30. actions/cacheとどちらを使うか
  31. よく使う小ワザ
  32. TUIでタスクごとにログを読める
  33. scriptsはturbo runで書く
  34. ほかのオプションと設定
  35. 参考
  36. まとめ
https://turborepo.dev/docsturborepo.dev
https://turborepo.dev/docs/crafting-your-repository/cachingturborepo.dev
https://turborepo.dev/docs/crafting-your-repository/using-environment-variablesturborepo.dev
https://turborepo.dev/docs/crafting-your-repository/constructing-citurborepo.dev
https://turborepo.dev/docs/guides/ci-vendors/github-actionsturborepo.dev
https://turborepo.dev/docs/guides/aiturborepo.dev
https://turborepo.dev/docs/core-concepts/remote-cachingturborepo.dev
https://turborepo.dev/docs/reference/configurationturborepo.dev
https://turborepo.dev/docs/reference/runturborepo.dev
https://turborepo.dev/docs/reference/queryturborepo.dev
https://turborepo.dev/docs/guides/handling-platformsturborepo.dev
https://vercel.com/docs/monorepos/remote-cachingvercel.com
https://vercel.com/docs/monorepos/remote-caching/external-ci-cdvercel.com