【実務・中級編】GitHubリポジトリの「巨大なバイナリ」管理:Git LFSの限界とストレージコストを抑える最適化のヒント – バージョン管理・CI/CD活用バイブル

GitHubリポジトリ「巨大バイナリ汚染」の処方箋:Git LFSの限界を超え、ストレージコストを零点へ導く最適化術

テックリードの皆さん、日々の開発でお使いのGitHubリポジトリ、「肥大化」していませんか?

UnityやUnreal Engineを用いたゲーム開発、大規模な機械学習モデルの重みファイル、あるいは膨大なデザインアセットなどを扱うプロジェクトでは、気づけば`.git`ディレクトリが数10GB、いや100GBを超えているという光景も珍しくありません。

「Git LFS(Large File Storage)を導入しているから大丈夫」――そう思っていませんか?
残念ながら、LFSは銀の弾丸ではありません。LFSの帯域制限、ストレージ追加料金の高騰、そして何より「過去にコミットされてしまった巨大ファイル」の亡霊は、LFSを導入しただけでは消えてくれません。

今回は、Git LFSの限界を突破し、リポジトリの軽量化とストレージコストの最適化を極限まで推進するための実践的なアプローチを、現場のプロフェッショナルの視点から解説します。

—

1. Git LFSの限界と「隠れたコスト」

Git LFSは、ポインタファイルの実体と実ファイルを分離することでリポジトリの軽量化を図るデファクトスタンダードです。しかし、大規模開発の現場では以下の壁に直面します。

1. 帯域幅とストレージの二重課金:GitHubの標準プランに含まれるLFSのデータ転送量とストレージ容量には上限があります。これを超過すると、追加の「Data Pack」を購入し続けるハメになります。
2. クローン・チェックアウトの遅延:CI/CD環境や新規メンバーのオンボーディング時、LFSオブジェクトのダウンロードに数十分を費やすことになります。
3. 過去の歴史(History)の呪い:LFS導入前に誤ってコミットされた数GBのバイナリは、`.git`のオブジェクトデータベースの奥底に永遠に残り続け、リポジトリを圧迫し続けます。

この「過去の歴史」を根絶しない限り、いくらLFSを入れてもリポジトリの肥大化は止まりません。ここで登場するのが、破壊的かつ不可欠なリポジトリ浄化ツールです。

—

2. 過去の不要な履歴の完全消去:`git filter-repo` の実戦投入

かつては `git filter-branch` が使われていましたが、現在は公式も推奨する `git filter-repo`(Python製)一択です。処理速度が圧倒的に速く、メモリ効率も桁違いに優れています。

> ⚠️ 警告: 以下の操作はリポジトリの歴史を書き換えます(コミットハッシュが変わります)。実行前に必ずチームメンバー全員に周知し、すべての作業ブランチをマージ・退避させた上で、リポジトリのミラークローンに対して実行してください。

実践:巨大バイナリを歴史から抹消する手順

1. バックアップとしてミラークローンを作成(絶対に原典を直接触らない)
git clone –mirror https://github.com/your-org/heavy-repo.git
cd heavy-repo.git

2. 100MB以上のファイルをすべて歴史から完全に削除する
※ git-filter-repoがインストールされている前提 (pip install git-filter-repo)
git filter-repo –strip-blobs-bigger-than 100M

3. ガベージコレクションとローカルストレージの最適化を強制
git reflog expire –expire=now –all
git gc –prune=now –aggressive

4. リモートへ強制プッシュ(–force-with-leaseを使用)
git push origin –force –all
git push origin –force –tags

この操作により、`.git`ディレクトリのサイズは劇的に縮小します。ただし、チームメンバー全員が「古いリポジトリの削除と再クローン」を行う必要があるため、実行タイミングはスプリントの切り替え時など計画的に行いましょう。

—

3. バイナリ・アセットの分離:サードパーティストレージ + サブモジュール戦略

頻繁に変更されない巨大なアセットや、ビルド済みバイナリをGitリポジトリ(およびLFS)で管理すること自体を見直すべきです。

推奨するアーキテクチャは、「コードと軽量設定はGitHub、巨大バイナリはS3/GCS等のオブジェクトストレージ」の分離です。これをGit Submodule、あるいはカスタムCLIスクリプトでシームレスに結合します。

実用的なアセット同期スクリプトのベストプラクティス

リポジトリのルートに `scripts/pull-assets.py` を配置し、開発者が `git clone` した後に一発でバイナリをフェッチできるようにします。

!/usr/bin/env python3
“””
AWS S3またはGCSからプロジェクトに必要な巨大バイナリアセットを安全に取得するスクリプト。
Git LFSの帯域制限を回避するためにチーム内で標準化しています。
“””
import os
import subprocess
import sys
from pathlib import Path

アセットの保存先とリモートストレージの定義
ASSET_DIR = Path(“assets/binaries”)
MANIFEST_FILE = Path(“assets/manifest.sha256”)
S3_BUCKET_URL = “s3://your-company-game-assets-prod/binaries/”

def check_dependencies():
“””AWS CLIがインストールされているか確認”””
if subprocess.run([“aws”, “–version”], capture_output=True).returncode != 0:
print(“Error: AWS CLI is not installed. Please install it first.”, file=sys.stderr)
sys.exit(1)

def pull_assets():
ASSET_DIR.mkdir(parents=True, exist_ok=True)
print(f”[] Syncing binaries from {S3_BUCKET_URL}…”)

# AWS S3から差分同期(ローカルの変更は上書きせず、リモートの正本を優先)
result = subprocess.run([“aws”, “s3”, “sync”, S3_BUCKET_URL, str(ASSET_DIR)])

if result.returncode == 0:
print(“[✓] Asset synchronization completed successfully.”)
else:
print(“[x] Failed to sync assets.”, file=sys.stderr)
sys.exit(result.returncode)

if __name__ == “__main__”:
check_dependencies()
pull_assets()

これをGitHub Actionsのワークフロー内(ビルドやテストの前段)にも組み込むことで、CI環境でもLFSのクォータを消費せずに高速なビルドを実現できます。

—

4. 再発防止:GitHub Actionsによるストレージ監視とガードレール

「一度綺麗にしても、数ヶ月後にはまた巨大ファイルがコミットされている」――これが現場の現実です。これを防ぐには、人間の善意に頼らず、機械的にブロックする仕組み(ガードレール)が不可欠です。

以下のGitHub Actionsワークフローを `.github/workflows/repo-guard.yml` として配置し、PR(プルリクエスト)段階で巨大ファイルの混入を検知・ブロックします。

name: Repository Size & Binary Guard

on:
pull_request:
branches: [ main, develop ]

jobs:
check-file-sizes:
name: Prevent Heavy Binaries in PR
runs-on: ubuntu-latest
steps:

  • name: Checkout Code

uses: actions/checkout@v4
with:
fetch-depth: 2 # 直近の変更差分を取得するために深度を2に設定

  • name: Scan for files larger than 10MB

run: |
echo “Scanning added/modified files for size violations…”
MAX_SIZE_MB=10
MAX_SIZE_BYTES=$((MAX_SIZE_MB 1024 1024))

# PRで追加・変更されたファイルのリストを取得
CHANGED_FILES=$(git diff –name-only –cached HEAD^ HEAD || git diff –name-only origin/${{ github.base_ref }} HEAD)

VIOLATIONS=0
for file in $CHANGED_FILES; do
if [ -f “$file” ]; then
FILE_SIZE=$(stat -f%z “$file” 2>/dev/null || stat -c%s “$file” 2>/dev/null)
if [ “$FILE_SIZE” -gt “$MAX_SIZE_BYTES” ]; then
echo “::error file=$file::File size ($((FILE_SIZE / 1024 / 1024))MB) exceeds the limit of ${MAX_SIZE_MB}MB.”
VIOLATIONS=$((VIOLATIONS + 1))
fi
fi
done

if [ “$VIOLATIONS” -gt 0 ]; then
echo “”
echo “==========================================================”
echo ” ERROR: 許容サイズ(${MAX_SIZE_MB}MB)を超えるバイナリが検出されました。”
echo ” Git LFSを使用するか、外部オブジェクトストレージに配置してください。”
echo “==========================================================”
exit 1
else
echo “No oversized files detected. Good job!”
fi

—

5. チーム開発の生産性を底上げする「設定共有化ルール」

最後に、開発チーム全体でこのポリシーを徹底し、開発スピードを落とさないための実践的なプラクティスを共有します。

① 開発者のための `.gitattributes` の厳格化

LFSを使うべき拡張子、あるいはそもそもコミットすらさせない拡張子を明確に定義し、リポジトリのルートに配置します。

=== Git LFS 対象外(テキスト・コード類) ===

  • text=auto

=== Unreal Engine / Unity アセットの LFS 管理 ===
.uasset filter=lfs diff=lfs merge=lfs -text
.umap filter=lfs diff=lfs merge=lfs -text
.unity filter=lfs diff=lfs merge=lfs -text
.prefab filter=lfs diff=lfs merge=lfs -text
.png filter=lfs diff=lfs merge=lfs -text
.psd filter=lfs diff=lfs merge=lfs -text

=== 完全ブロック対象(絶対にコミットしてはいけないビルド成果物など) ===
.zip export-ignore
.tar.gz export-ignore
.exe export-ignore
.dmg export-ignore

② エディタ・IDEレベルでのガード(VS Code設定)

開発者がローカルでうっかり巨大ファイルをステージングしようとした際に警告が出るよう、`.vscode/settings.json` をチームで共有します。

{
“files.exclude”: {
“/.git”: true,
“/.DS_Store”: true,
“/node_modules”: true,
“/assets/binaries/“: true
},
“git.ignoreLimitWarning”: false,
“git.postCommitCommand”: “none”
}

—

結びに代えて

バージョン管理システムは「コードの歴史」を記録するためのものであり、数ギガバイトのバイナリを力技で溜め込むためのストレージサーバーではありません。

Git LFSを正しく理解し、不要な履歴は `git filter-repo` で躊躇なく焼き払い、巨大アセットは専用のオブジェクトストレージへオフロードする――このアーキテクチャの確立こそが、CI/CDのビルド時間を短縮し、チーム全体の開発体験(DX)を劇的に向上させる唯一の近道です。

あなたのリポジトリの `.git` サイズ、今日から見直してみませんか?

タイトルとURLをコピーしました