【Day 45】GitHub ActionsキャッシュによるDockerビルド爆速化 〜Buildxとtype=ghaでCIビルド時間を80%削減!大容量.NET SDK&NuGetダウンロードをスキップする実践テクニック〜
「プルリクエストを更新するたびに15分待たされ、開発リズムが完全に崩壊していませんか?」
前回のDay 44では、Gitタグ発行をトリガーにSemVerバージョニングされたDockerイメージをGHCRへ自動公開する、完全トレーサビリティのリリースパイプラインを構築しました。
しかし、CI/CDパイプラインを実務で運用し始めると、チーム全員が直面する巨大な壁があります。
「CIのDockerビルドが遅すぎる……!たった1行のコード修正なのに、なぜ毎回15分も待たされるのか?」
ローカル環境では数秒で終わるDockerビルドが、なぜGitHub Actions上では毎回全工程を最初からやり直してしまうのでしょうか?
本講義(Day 45)では、Docker Buildx と GitHub Actions公式のキャッシュ機能 type=gha を完全連携させ、大容量の.NET SDKや数十個のNuGetパッケージダウンロードをスキップしてビルド時間を80%以上削減(15分 ➔ 2分未満)する極意を徹底解説します!
📌 本日のアジェンダ(講義目次)
- 第1章:なぜCIのDockerビルドは遅いのか? エフェメラル環境の罠
- 第2章:Docker Buildx と type=gha キャッシュの仕組み
- 第3章:キャッシュヒット率を100%引き出す C# Dockerfile 設計術
- 第4章:GitHub Actions ワークフローの実装(cache-from / cache-to)
- 第5章:C#/.NET プロジェクトにおける劇的な高速化検証と運用ルール
1 なぜCIのDockerビルドは遅いのか? エフェメラル環境の罠
手元の開発PC(Docker DesktopやWSL2)で docker build を実行すると、2回目以降のビルドは一瞬で終わります。Dockerが過去にビルドした中間レイヤーをローカルディスクに保存し、CACHED として再利用してくれるからです。
しかし、同じDockerfileをGitHub Actions上でビルドすると、毎回すべてのレイヤーが最初から実行されてしまいます。
この原因は、GitHub Actionsのホステッドランナー(ubuntu-latest など)がエフェメラル(使い捨て)な仮想マシンとして起動する仕様にあります。
- ディスクの完全破棄:ワークフローが終了するたびに仮想マシン全体が破棄されるため、ローカルのDockerレイヤーキャッシュが次回実行時に引き継がれない。
- 巨大SDKの毎回ダウンロード:
mcr.microsoft.com/dotnet/sdk:8.0(約800MB〜1GB以上)を毎回インターネット経由でプルする通信オーバーヘッド。 - NuGetパッケージの全件復元:数十個の外部ライブラリを毎回ゼロからダウンロード・解凍するため、CPUと回線を浪費し10分以上待たされる。
この結果、PRを作成・コミットするたびに15分近いビルド待ちが発生し、エンジニアは「CIが終わるまで別の仕事に手がつかない」というコンテキストスイッチの地獄に陥ります。さらに、GitHub Actionsの月間実行時間枠(無料枠2,000分やチーム課金枠)が急激に枯渇していきます。
2 Docker Buildx と type=gha キャッシュの仕組み
この問題を根本から解決するのが、次世代ビルドエンジン BuildKit を活用する Docker Buildx と、GitHub Actions専用のキャッシュバックエンド type=gha です。
従来の docker build はローカルディスク上のキャッシュしか扱えませんでした。しかし、Buildxは「外部ストレージに対してキャッシュをエクスポート/インポートする能力」を持っています。
💡 Buildx キャッシュパイプラインの3ステップフロー
- Step ① キャッシュ照会(
cache-from: type=gha):
ビルド開始時、GitHub Actions Cache APIと通信し、前回のビルドで保存されたレイヤーキャッシュを自動ダウンロードして展開します。 - Step ② 差分高速ビルド:
変更のないレイヤー(ベースイメージ取得やNuGet復元など)を完全スキップ!変更されたソースコードのコンパイルのみを数秒で完了させます。 - Step ③ キャッシュ保存(
cache-to: type=gha,mode=max):
新しく更新・生成された中間レイヤーキャッシュを、GitHub Actions Cacheストレージへ自動的にアップロードして次回実行に備えます。
ここで極めて重要なのが、cache-to のオプションである mode=max です。デフォルトの mode=min では最終出力イメージのレイヤーしかキャッシュされませんが、mode=max を指定すると、マルチステージビルドの「ビルド中間ステージ(SDK環境やdotnet buildレイヤー)」も含めた全レイヤーがキャッシュ対象となります。
3 キャッシュヒット率を100%引き出す C# Dockerfile 設計術
Buildxとtype=ghaを導入しても、Dockerfileの命令順序が悪いとキャッシュは一切効きません。
Dockerのレイヤーキャッシュには「ある命令でキャッシュが無効化(キャッシュミス)すると、それ以降のすべての後続レイヤーも連鎖的に再実行される」という鉄則があるからです。
よくあるアンチパターンと、キャッシュヒット率を最大化するベストプラクティスを比較してみましょう。
❌ やってはいけないアンチパターン(キャッシュ崩壊)
# アンチパターン:コード全体をはじめにCOPYしてしまう
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . . # 💥 1文字修正しただけで、これ以降の全キャッシュが無効化!
RUN dotnet restore "MyApi.csproj"
RUN dotnet build "MyApi.csproj" -c Release -o /app/build
ソースコードの1行を修正しただけで COPY . . のチェックサムが変わり、その直下の RUN dotnet restore が毎回全件再実行されてしまいます。
✅ 黄金律:依存関係先行コピー(キャッシュ最大化)
# ステージ1: ビルド環境
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# ① 変更頻度が極めて低いプロジェクト定義ファイルのみを先行COPY
COPY ["MyApi/MyApi.csproj", "MyApi/"]
COPY ["MyLibrary/MyLibrary.csproj", "MyLibrary/"]
# ② NuGetパッケージ復元を実行(csprojが同一なら100%キャッシュヒット!)
RUN dotnet restore "MyApi/MyApi.csproj"
# ③ ここで初めて、日常的に変更されるC#ソースコードをCOPY
COPY . .
WORKDIR "/src/MyApi"
RUN dotnet build "MyApi.csproj" -c Release -o /app/build
# ステージ2: 発行
FROM build AS publish
RUN dotnet publish "MyApi.csproj" -c Release -o /app/publish /p:UseAppHost=false
# ステージ3: 実行用ランタイム環境(軽量)
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "MyApi.dll"]
このように、「変更頻度が低い命令を上に、頻繁に変更されるコードを下に配置する」ことで、日々のGitコミット(C#ロジック修正)では dotnet restore が完全にスキップされ、超高速な差分ビルドが実現します。
4 GitHub Actions ワークフローの実装(cache-from / cache-to)
それでは、実際の GitHub Actions ワークフローファイル(.github/workflows/docker-build.yml)に Buildx と type=gha キャッシュを組み込んでみましょう。
name: Docker Build & Push with GitHub Actions Cache
on:
push:
branches: [ "main", "develop" ]
tags: [ "v*" ]
pull_request:
branches: [ "main" ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: リポジトリのチェックアウト
uses: actions/checkout@v4
# 1. QEMUのセットアップ(マルチプラットフォーム対応)
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
# 2. Docker Buildx のセットアップ(次世代BuildKitエンジンを有効化)
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# 3. GHCRへのログイン(PR時は不要な場合スキップ可能)
- name: Log in to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# 4. メタデータの抽出(タグとラベルの自動生成)
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{version}}
type=ref,event=branch
type=ref,event=pr
# 5. Buildx によるビルド&プッシュ(ghaキャッシュ有効化)
- name: Build and push Docker image with Cache
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# 🚀 ここが爆速化の核心!
cache-from: type=gha
cache-to: type=gha,mode=max
cache-from: type=gha:GitHub Actions のキャッシュストレージから、以前保存された同一ブランチ(または親ブランチ)のレイヤーキャッシュを自動取得します。cache-to: type=gha,mode=max:中間ステージ(ビルド用SDKステージ)を含むすべてのレイヤーをキャッシュとして書き戻します。- 認証設定が不要:GitHub Actions内部のトークンを自動利用するため、追加のシークレット設定やAPIキーは一切不要です!
5 C#/.NET プロジェクトにおける劇的な高速化検証と運用ルール
それでは、実際に ASP.NET Core Web API プロジェクトで Buildx + type=gha キャッシュを導入した際のビフォーアフターの実測値を比較してみましょう。
12分以上かかっていたビルド時間が1分台へ劇的に短縮されました!これにより、開発者はプルリクエスト作成後すぐにCIテストの結果を確認でき、開発リズムを妨げられることなく次のタスクへ進むことができます。
⚠️ 実務で知っておくべき運用の落とし穴と制限事項
- リポジトリあたりのキャッシュ上限(10GB):
GitHub Actions Cache はリポジトリ全体で最大 10GB までの制限があります。上限を超えると古いキャッシュから自動削除(LRU方式)されます。不要な一時ファイルがレイヤーに含まれないよう.dockerignoreを徹底しましょう。 - キャッシュ保持期間(7日間):
アクセスされなかったキャッシュは7日後に自動破棄されます。長期間開発が止まっていたブランチでは初回ビルド時に再キャッシュが走ります。 - ブランチ間のキャッシュスコープ:
フィーチャーブランチは、デフォルトブランチ(main)のキャッシュを読み込むことができますが、フィーチャーブランチで作成したキャッシュは他のフィーチャーブランチからは参照できません。そのため、mainブランチで最新のキャッシュを常に温めておく(ウォームアップしておく)運用がベストです。
🎯 Day 45 の重要ポイント総括
- CI環境はエフェメラル:通常のDockerビルドでは毎回仮想マシン破棄に伴いレイヤーキャッシュが消滅する。
- Buildx + type=gha の導入:GitHub Actions専用の外部キャッシュストレージを活用することで、CIランナー間でレイヤーキャッシュを完全共有。
- mode=max の指定:中間ビルドステージ(.NET SDKやビルド用レイヤー)まで含めて確実にキャッシュ保存する。
- Dockerfile順序の徹底:
.csprojのみ先行COPY ➔dotnet restore➔ ソースコードCOPY の順序を守り、キャッシュヒット率を最大化する。 - 80%以上のビルド短縮:大容量.NET SDK取得とNuGet全復元をスキップし、開発リズムとCIコストを大幅改善!
📝 理解度チェックテスト
本日の講義内容を復習してみましょう。クリックすると正解と解説が表示されます。
Q1: GitHub ActionsでDocker Buildxキャッシュを設定する際、中間ビルドステージも含めてすべてキャッシュさせるためのオプションはどれ?
cache-to: type=gha,mode=maxデフォルトの
mode=min では最終成果物イメージのレイヤーしかキャッシュされません。マルチステージビルドのSDK環境や中間コンパイル成果物をキャッシュするには mode=max の指定が必須です。
Q2: C# の Dockerfile で COPY . . を RUN dotnet restore より前に書いてはいけない最大の理由は何?
dotnet restore のキャッシュが無効化されてしまうため.csproj に変更がなくても、直前の COPY . . で変更が検知されるとDockerはキャッシュを破棄し、時間のかかるNuGetパッケージ全件復元を再度やり直してしまいます。
Q3: GitHub Actions Cache のリポジトリごとの容量制限と非アクセス時の保持期間はそれぞれどれくらい?
10GBを超過した場合は古いキャッシュから自動削除されます。また、7日間一度もアクセスされなかったキャッシュも自動的にクリーンアップされます。
NEXT LECTURE
Day 46: プルリクエスト(PR)ごとの一時的プレビュー環境の自動構築
次回は、PRを作成するたびに独立したテスト用コンテナ環境がクラウドに立ち上がり、マージ・クローズ時に自動破棄される「エフェメラル環境(Ephemeral Environments)」の構築に挑戦します!
ステージングサーバーの順番待ちに終止符を打つ、現代的開発フローの極意をお楽しみに!
コメント
コメントを投稿