GitHub Actionsを速く安く回す: matrix・cache・reusable workflow実践
GitHub Actionsのmatrixビルド、cacheで高速化、reusable workflowで重複削減、Environmentsで承認まで。コピペで動くYAMLとコスト削減のコツを実体験で解説。
CIの1回が9分。1日に20回回せば3時間。月に直すと、テストを待つだけで丸2日が溶けていました。
最初は「テストが遅いのは仕方ない」と思っていたんです。でも中身を見たら、毎回ゼロからnpm installして、UbuntuもWindowsも直列で走らせて、同じpushを二重に流していた。つまり、遅かったのはテストじゃなくて、回し方でした。
基本のCIを組むだけなら、誰でも数分でYAMLを書けます。push したらテストする、それだけなら難しくない。本当に効いてくるのは、そこから先です。並列で回し、依存を使い回し、共通部分をまとめ、無駄な実行を止める。 ここを詰めると、同じテスト内容でも待ち時間とお金が半分以下になります。
この記事は、基本のCI構築を終えた人向けに、GitHub Actionsを速く・安く・壊れにくくする応用テクだけを集めました。基礎からやり直したい人は先にCI/CDパイプライン構築ガイドを読んでから戻ってきてください。
この記事の要点
- matrixは「Node 22と24 × UbuntuとWindows」のような組み合わせを一気に並列で回す仕組み。OS依存のバグを早く潰せる。
- cacheは依存関係を保存して使い回す仕組み。
setup-nodeのcache: npmを入れるだけでインストール時間が激減する。鍵はlockfileに紐づけること。 - reusable workflowは共通のジョブを「関数」として切り出す仕組み。
setup-nodeの更新を1ファイルで済ませられる。 - concurrencyで古い実行を自動キャンセルし、matrixの絞り込みでジョブ爆発を防ぐと、課金が目に見えて下がる。
- 速くする工夫と、安全(最小権限・secrets・本番承認)は両立する。むしろ速いCIほど安全に倒しておく価値がある。
公式の一次情報はここで確認しました。matrix(Running variations of jobs)、dependency caching、reusing workflows、concurrency、GITHUB_TOKEN permissions。この記事の例は2026年6月時点で、GitHub-hosted runnerを前提にactions/checkout@v6とactions/setup-node@v6を使います。
matrixで「組み合わせ」を一気に潰す
matrixは、1つのジョブ定義から複数のジョブを自動で生やす仕組みです。料理で言うと、レシピは1つなのに、コンロを4口同時に使って同じ料理を別の鍋で作るイメージ。書く量は変わらないのに、検証の幅が一気に広がります。
僕がmatrixに救われたのは、Windowsだけで落ちるパス処理のバグでした。手元のMacでは通る、CIのUbuntuでも通る、でもユーザーのWindowsで動かない。matrixでWindowsを足した瞬間、PRの段階で真っ赤になって気づけました。
下が、lint・型チェック・テストをNode 2バージョン × OS 2種で回す品質ゲートです。actions/checkout@v6とactions/setup-node@v6は2026年6月時点の新しいmajorです。self-hosted runnerが古いチームは、いきなりv6へ上げず、runner更新を先に確認してください。
name: pr-quality-gate
on:
pull_request:
branches: [main]
push:
branches: [main]
# テストだけなら書き込み権限は要らない。読み取りに絞る
permissions:
contents: read
# 同じブランチに push し直したら、古い実行を止める
concurrency:
group: pr-quality-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
name: node-${{ matrix.node }}-${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
# 1つ落ちても他の組み合わせの結果を最後まで見る
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
node: [22, 24]
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
cache: npm # 依存をキャッシュ
cache-dependency-path: package-lock.json
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Type check
run: npm run typecheck
- name: Test
run: npm test
このYAMLは4ジョブ(22×ubuntu / 22×windows / 24×ubuntu / 24×windows)に展開され、すべて並列で走ります。fail-fast: falseは、1つ落ちても残りを最後まで走らせる設定です。どのOSで、どのバージョンで落ちたかを一覧で見たいときに向いています。逆に「1つでもコケたら即やめて節約」したいならtrueにします。
matrixには、特定の組み合わせだけ足したり外したりする機能もあります。includeで「Windowsのときだけ追加ステップ」、excludeで「この組み合わせは検証不要」を表現できます。最初は2×2くらいで十分です。
cacheで毎回のインストールを使い回す
CIが遅い原因の上位は、ほぼ毎回フルでやり直す依存関係のインストールです。何百ものパッケージを毎回ダウンロードして展開する。これを保存して使い回すのがcacheです。
一番手軽なのは、上のYAMLにも入れたsetup-nodeのcache: npmです。これだけで、依存が変わっていなければインストールがごっそり短くなります。僕のあるリポジトリではnpm ciが110秒から15秒前後まで落ちました。1行で、です。
ただし注意が1つ。cacheの鍵(key)をlockfileに紐づけることです。鍵が固定だと、依存を更新しても古いcacheを使い続けて、「手元では新しいのにCIだけ古い」という気味の悪いバグが出ます。setup-nodeはcache-dependency-pathで自動的にlockfileのハッシュを鍵にしてくれます。
setup-nodeが面倒を見てくれない成果物、たとえばビルド済みファイルやnext.jsの.next/cacheを保存したいときは、actions/cache@v4を直接使います。
- name: Cache build output
uses: actions/cache@v4
with:
path: |
.next/cache
dist
# lockfile と main.ts のハッシュで鍵を作る。
# どちらかが変われば鍵が変わり、新しく作り直す
key: build-${{ runner.os }}-${{ hashFiles('**/package-lock.json', 'src/main.ts') }}
# 完全一致がなくても、近い rune の cache を土台に使う
restore-keys: |
build-${{ runner.os }}-
keyが完全一致すればそのまま復元(cache hit)。一致しなければrestore-keysで前方一致する近いcacheを土台にして、ジョブの最後に新しい鍵で保存します。hashFilesに含めるファイルがポイントで、ここに入れた中身が変わると鍵が変わり、cacheが作り直されます。逆に、ここを雑にすると古い成果物を引きずります。
| キャッシュ対象 | 使うもの | 鍵に含めるべきもの |
|---|---|---|
| npm/yarn/pnpmの依存 | setup-nodeのcache | lockfile(自動) |
| ビルド成果物 | actions/cache@v4 | lockfile+ソースの一部 |
| Dockerレイヤー | buildxのcache-from/to | Dockerfile+依存定義 |
node_modulesを丸ごとcacheする方法もありますが、OS差やpostinstallの副作用で壊れやすいので、まずはsetup-nodeのcache: npmから始めるのが保守的でおすすめです。
reusable workflowで重複をまとめる
同じNodeチェックを3つも4つもワークフローにコピペしていると、必ず事故ります。setup-nodeをv6に上げたいのに、1ファイルだけ更新を忘れてv5のまま、みたいな。コピペは増えた瞬間から負債です。
reusable workflow(再利用可能ワークフロー)は、共通のジョブを別ファイルに切り出して、複数のワークフローから呼び出す仕組みです。プログラミングの「共通関数」と同じ発想ですね。一度書けば、更新は1か所で済みます。
まず、呼ばれる側(共通部品)をworkflow_callで定義します。
# .github/workflows/reusable-node-check.yml
name: reusable-node-check
on:
workflow_call:
inputs:
node-version:
required: false
type: string
default: "24"
permissions:
contents: read
jobs:
check:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: ${{ inputs.node-version }}
cache: npm
cache-dependency-path: package-lock.json
- name: Install
run: npm ci
- name: Check
run: |
npm run lint
npm run typecheck
npm test
呼び出す側は、びっくりするほど短くなります。
# .github/workflows/ci.yml
name: ci
on:
pull_request:
branches: [main]
permissions:
contents: read
jobs:
node-check:
uses: ./.github/workflows/reusable-node-check.yml
with:
node-version: "24"
これで、setup-nodeのmajor更新やcache方針の変更を、共通ファイル1つ直すだけで全ワークフローに反映できます。
落とし穴を1つ。reusable workflowにsecretsは自動では渡りません。 必要なものだけ、呼び出し側でsecrets:を使って明示的に渡すか、secrets: inheritで引き継ぎます。inheritは楽ですが、渡しすぎになりがちなので、本番デプロイ系では「必要なsecretだけ名前を書いて渡す」ほうが安全です。
なお、パッケージごとにインストール手順が違うmonorepoで、無理に1つの共通ワークフローへまとめると、条件分岐だらけになって逆に読めなくなります。共通化するのは「全パッケージで本当に同じ処理」だけにする、という線引きが大事です。
secretsとEnvironmentsで本番だけ守りを固める
速くするほど、CIから本番へ流れる経路は太くなります。だからこそ、デプロイの手前に「人間のひと呼吸」を残しておきます。それがEnvironments(環境)のapproval(承認)です。
下はstaging deployの例です。AWSへはOIDC(OpenID Connect、長期キーを置かずに短命トークンで入る認証方式)で入り、environment: stagingを指定しています。本番(production)には承認必須を設定しておくと、自動化やAIが勝手に公開する事故を止められます。
name: deploy-staging
on:
workflow_dispatch:
push:
branches: [main]
permissions:
contents: read # checkout に必要
id-token: write # OIDC トークン取得だけのための権限
# デプロイは途中でキャンセルしない
concurrency:
group: deploy-staging
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 20
environment: staging # 承認やシークレットを環境ごとに分ける
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-staging
aws-region: ap-northeast-1
- name: Verify caller identity
run: aws sts get-caller-identity
- name: Deploy
run: |
npm ci
npm run build
echo "ここに実際のデプロイコマンド(npx cdk deploy 等)を置く"
ここでid-token: writeはOIDCトークンを取るためだけの権限で、リポジトリへの書き込み権限ではありません。逆にcontents: writeやactions: writeを雑に付けると、デプロイジョブが不要な権限まで持ってしまいます。権限は「そのジョブに本当に要るものだけ」を基本にします。
secretsの扱いは2つだけ覚えておけば大きな事故は防げます。1つ目、logにsecretを出さない。 GitHubは値をマスクしますが、base64化やJSON埋め込み、外部ツールのdebug logまでは保証されません。2つ目、Environmentごとにsecretを分ける。 stagingとproductionで別の鍵にしておけば、片方が漏れても被害が片方で止まります。secretsの基礎はセキュリティ対策ガイドにまとめています。
コストを下げる4つのレバー
GitHub Actionsの料金は、ざっくり「実行時間 × runnerの単価」です。速くすることは、そのまま安くすることでもあります。僕が実際に効果を感じた順に並べます。
- concurrencyで古い実行を殺す。 PRに連続でpushすると、古いチェックが裏で走り続けます。
cancel-in-progress: trueを入れるだけで、無駄な実行が消えます。これが一番ラクで効果が大きい。 - cacheで実行時間を削る。 インストールやビルドが短くなれば、そのまま課金分が減ります。
cache: npmは1行で効く即効薬です。 - matrixを増やしすぎない。 OS 3種 × Node 4種 × パッケージ8個で、なんと96ジョブ。PRでは代表環境だけに絞り、広い検証は夜間(nightly)のスケジュール実行に逃がします。費用と速度を分けて考えるのがコツです。
- 重い処理はrunnerを選ぶ。 WindowsやmacOSのrunnerはLinuxより単価が高めです。WindowsやmacOSでしか確認できないものだけそこで回し、残りはubuntuに寄せます。
特にmatrixのジョブ爆発は、気づかないうちにじわじわ効いてきます。「念のため全部回す」を続けると、PRごとに数十ジョブが毎回走る状態になりがちです。
Claude Codeを「設計レビュー相手」として使う
ここまでのYAMLは、Claude Codeに「CI作って」と一言頼んでも一応それっぽいものは出てきます。でも、権限が広すぎたり、cache鍵が固定だったり、本番に承認がなかったり、という危ない仕上がりになりがちです。
なので僕は、Claude Codeをコード生成係ではなく、設計の壁打ち相手として使います。いきなりYAMLを書かせるより、先に制約を明文化させるんです。
このリポジトリのGitHub Actionsを「速く・安く・安全に」設計してください。
制約:
- matrix は PR では代表環境(ubuntu + Node 24)に絞り、広い検証は nightly に分ける
- cache の鍵は package-lock.json を必ず含め、固定鍵にしない
- 共通の Node チェックは reusable workflow に切り出す
- GITHUB_TOKEN は最小権限。デプロイは OIDC、本番は Environment 承認必須
- fork からの PR では secret が使えない前提で設計する
最後に、想定される失敗例と、それを確認するコマンドも書いてください。
ポイントは「作って」ではなく「制約を守って設計して」と言っていること。広い権限のYAMLが返ってきたら、「各permissionが必要な理由を1行ずつ説明して」と追い質問します。理由を説明できない権限は、だいたい要りません。
なお、Claude Code Action自体をCIに組み込むときは、現在はanthropics/claude-code-action@v1を使い、promptとclaude_argsで渡します。古い@betaやdirect_prompt前提のサンプルはコピペしないでください(v1でpromptに統合されました)。詳しい移行はClaude Code GitHub Actions公式ドキュメントが一次情報です。CI全体の段取りはCI/CDパイプライン構築ガイド、テストの組み方はテスト戦略ガイドが地続きで読めます。
よくある質問
Q. matrixのジョブが多すぎて遅いし高い。どう絞る?
A. PRでは代表1〜2環境に絞り、excludeで不要な組み合わせを外します。OS全種・全バージョンの広い検証は、scheduleで夜間に1回だけ回すと、PRの速度と網羅性を両立できます。
Q. cacheを入れたのに毎回作り直されてhitしない。
A. 鍵(key)が毎回変わっているのが原因です。hashFilesに、実行ごとに変わるファイル(タイムスタンプやビルド成果物そのもの)を含めていないか確認してください。鍵はlockfileやソースなど「依存が変わったときだけ変わる」ものにします。
Q. reusable workflowでsecretがundefinedになる。
A. reusable workflowにsecretsは自動で渡りません。呼び出し側でsecrets: inherit、またはsecrets:ブロックで必要なものを明示的に渡してください。
Q. matrixの結果が1つ失敗しただけで全部止まる。
A. strategy.fail-fastの既定がtrueだからです。全組み合わせの結果を見たいときはfail-fast: falseにします。逆にコスト優先ならtrueのままが正解です。
Q. fork からのPRでsecretやdeployが動かない。
A. 仕様です。GitHubは外部forkのPRにsecretを渡しません(漏洩防止)。デプロイ系のジョブはif: github.event.pull_request.head.repo.full_name == github.repositoryで同一リポジトリのPRだけに限定するのが安全です。
実際に試した結果
この記事のYAMLは、MDX内のcode fenceから取り出してYAMLパーサで構文を通し、on・permissions・concurrency・matrix・cache・reusable workflow・OIDCの形が構文として読めることを確認しています。
数字で一番効いたのは、地味な2つでした。concurrencyのcancel-in-progress: trueで、連打pushの裏側実行が消えたこと。そしてsetup-nodeのcache: npm1行で、npm ciが2桁秒から十数秒に落ちたこと。matrixで網羅性を上げつつ、この2つで待ち時間と課金を同時に削れたのが、いちばん体感の大きい変化でした。
派手な自動化を足す前に、まず「無駄な実行を止める」「依存を使い回す」の2つから入るのを強くおすすめします。実運用ではAWS role ARN、npm scripts、Environment名、各種secretの登録だけはリポジトリごとに置き換えてから動かしてください。
チームでmatrix・cache・reusable workflow・OIDC・本番承認をまとめて設計し直したいなら、Claude Code研修・導入相談で実際のリポジトリを前提に整えられます。個人で手早く始めるなら教材一覧から関連リソースを覗いてみてください。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
制作会社がClaude Codeに触らせる前に決める権限チェックリスト
クライアントサイトを壊さずにAI編集を使うための、制作会社向け権限と確認の型です。
SaaSサポートのバグ報告をClaude Codeで再現手順に変える実務フロー
問い合わせ文をそのまま開発へ投げず、再現手順、証拠、次の一手に整えるサポート向け手順です。
Obsidianの古いメモをClaude Codeの指示書に変える10分ルーチン
Obsidianに溜めたメモが毎回ゴミになる人へ。事実・決定・未確認に仕分けして、Claude Codeがそのまま動ける指示書に変える朝の10分の型を紹介します。