Macの開発環境のメンテナンス用シェルスクリプト

開発マシンのメンテナンスは、やるべきだと分かっていながら後回しにしがちな作業の一つです。

Homebrewのアップデート、Dockerの肥大化したキャッシュ、積み上がったDerivedData——個別には些細でも、放置すれば確実にディスクとパフォーマンスを蝕みます。

本記事では、私が普段使用しているシェルスクリプトを紹介します。

設計方針

メンテナンススクリプトには独特の要件があります。
一部のコマンドが失敗しても、残りは実行を続けてほしい。set -e を使わない理由はここにあります。

set -uo pipefail

-u で未定義変数の参照を防ぎ、pipefail でパイプ中の失敗を検知します。
-e を外すことで、例えば brew doctor が警告を返しても後続の処理は止まりません。

構成

スクリプトは5つのセクションから成ります。

Homebrewの更新とクリーンアップ

brew update && brew upgrade && brew upgrade --cask --greedy
brew cleanup && brew autoremove
brew doctor

--cask --greedy は、自動更新機能を持つアプリ(Chrome、Firefox等)も含めて強制的にアップグレードします。
brew cleanupbrew autoremove で古いバージョンと不要な依存を除去し、brew doctor で環境の整合性を確認します。

Mac App Store

mas upgrade

mas を使い、App Store経由でインストールしたアプリもCLIから更新します。

Xcode関連

xcrun simctl delete unavailable

使われなくなったシミュレータランタイムを削除します。
これだけで数十GBの空きが生まれることもあります。

DerivedDataについては、Xcodeの起動状態を確認してから削除します。

if ! pgrep -q Xcode; then
  rm -rf ~/Library/Developer/Xcode/DerivedData/*
fi

ビルドキャッシュを消すだけなので、次回ビルド時に再生成されます。
起動中のXcodeを壊す心配を避けるためのガードです。

パッケージマネージャのキャッシュ整理

npm と pnpm のキャッシュを整理します。
いずれも command -v で存在確認してから実行するため、インストールしていない環境でもエラーにはなりません。

npm cache verify    # 整合性チェック。全削除はしない
pnpm store prune    # 未参照パッケージの削除

npm cache clean --force ではなく verify を選んでいるのは、壊れたエントリだけを除去し、有効なキャッシュは維持するためです。

Docker クリーンアップ

if command -v docker &>/dev/null && docker info &>/dev/null; then
  docker system prune -f --volumes --filter "until=168h"
  docker builder prune -f --filter "until=168h"
fi

docker info でデーモンの起動確認を行い、停止中なら何もしません。
--filter "until=168h" により、1週間以内に使用したリソースは保持されます。
直近の作業を壊さない安全策です。

キャッシュ容量の可視化

最後に ~/Library/Caches の上位10ディレクトリを表示します。

du -sh ~/Library/Caches/* 2>/dev/null | sort -rh | head -n 10

自動削除はしません。
アプリケーションキャッシュの削除判断は人間に委ねるべきです。

使い方

適当な場所に保存して実行権限を付与します。

chmod +x ~/scripts/maintenance.sh

手動で定期実行してもいいですし、launchd でスケジュールしても構いません。
ただし、Homebrewやmasは対話的な認証を求めることがあるため、完全な無人実行には向かない点に留意してください。

スクリプト全文

#!/bin/bash

# -------------------------------------------
# エラーハンドリング方針:
#   メンテナンススクリプトの性質上、一部失敗しても続行したいため
#   set -e は使わず、重要なコマンドのみ個別にハンドリングする。
#   未定義変数の参照とパイプ途中の失敗は検知する。
# -------------------------------------------
set -uo pipefail

# ===========================================
# Homebrewパッケージ更新とクリーンアップ
# ===========================================
brew update && brew upgrade && brew upgrade --cask --greedy
brew cleanup && brew autoremove
brew doctor

# ===========================================
# Mac App Storeアプリのアップデート
# ===========================================
mas upgrade

# ===========================================
# Xcode関連の不要ファイル削除
# ===========================================
xcrun simctl delete unavailable

# Xcodeが起動中でなければDerivedDataを削除
if ! pgrep -q Xcode; then
  rm -rf ~/Library/Developer/Xcode/DerivedData/*
else
  echo "⚠ Xcodeが起動中のためDerivedDataの削除をスキップしました"
fi

# ===========================================
# パッケージマネージャのキャッシュ/ストア整理
# ===========================================
# npm キャッシュの検証(整合性チェックのみ、全削除はしない)
if command -v npm &>/dev/null; then
  echo "--- npm cache verify ---"
  npm cache verify
fi

# pnpm ストアの最適化(未参照の依存パッケージを削除)
if command -v pnpm &>/dev/null; then
  echo "--- pnpm store prune ---"
  pnpm store prune
fi

# ===========================================
# Docker クリーンアップ
# ===========================================
if command -v docker &>/dev/null && docker info &>/dev/null 2>&1; then
  echo "--- Docker system prune ---"
  # 1週間以上前の未使用リソース + 未使用ボリュームを削除
  docker system prune -f --volumes --filter "until=168h"

  echo "--- Docker builder prune ---"
  docker builder prune -f --filter "until=168h"
fi

# ===========================================
# キャッシュ容量の確認(削除は手動判断)
# ===========================================
echo ""
echo "=== ~/Library/Caches 容量 Top 10 ==="
du -sh ~/Library/Caches/* 2>/dev/null | sort -rh | head -n 10
echo ""
echo "※ 不要なキャッシュがあれば手動で削除してください"
echo "  例: rm -rf ~/Library/Caches/<アプリ名>"

まとめ

このスクリプトが行うのは「安全に削除できるものだけを削除し、判断が必要なものは報告する」ことです。
定型作業を自動化するだけでも、ディスク容量とビルド環境の健全性は改善します。

Laravel 8以降は foreignIdFor() を使う

Laravel 8で foreignIdFor() メソッドが導入されました。
外部キーカラムの定義がよりシンプルで安全になる機能です。

foreignIdFor() とは

foreignIdFor() は、モデルクラスから外部キーカラムを自動生成するメソッドです。

Before: 従来の書き方

$table->unsignedBigInteger('user_id');
$table->foreign('user_id')
    ->references('id')
    ->on('users')
    ->onDelete('cascade');

After: foreignIdFor() を使った書き方

$table->foreignIdFor(User::class)
    ->constrained()
    ->onDelete('cascade');

わずか1行で、カラムの作成から外部キー制約の設定まで完結します。

constrained() メソッドについて

constrained() は外部キー制約を自動的に設定するメソッドです。

$table->foreignIdFor(User::class)->constrained();

このコードは以下を自動的に行います:
– モデルクラスから参照先のテーブル名を推測(Userusers
– 主キーカラム id への参照を作成
– 外部キー制約を設定

constrained() を呼ばなければ、カラムのみが作成され、外部キー制約は設定されません。

// カラムのみ作成(制約なし)
$table->foreignIdFor(User::class);

// カラム + インデックスのみ(制約なし)
$table->foreignIdFor(User::class)->index();

主なメリット

1. 型の安全性が向上

参照先のテーブルの主キー型と自動的に一致します。
users.idbigInteger なら、外部キーも unsignedBigInteger として作成されます。

2. タイプミスを防止

// モデルクラスから自動生成されるため、タイプミスが起きない
$table->foreignIdFor(User::class); // → user_id

// 従来の方法だとタイプミスのリスク
$table->unsignedBigInteger('usr_id'); // うっかりミス

3. コードの意図が明確

一目で「これは User モデルへの外部キーだ」と分かります。

注意点

モデルクラスが必要

当然ですが、モデルクラスが存在しないと使えません。

// モデルがない外部サービスのテーブルなど
$table->unsignedBigInteger('external_service_id');

カスタムテーブル名に注意

モデル名とテーブル名が規則通りでない場合は、明示的に指定が必要です。

class Person extends Model {
    protected $table = 'users';
}

// persons テーブルを探してしまうため、明示的に指定
$table->foreignIdFor(Person::class)->constrained('users');

カラム名のカスタマイズ

デフォルトでは {model}_id という名前になります。
カスタマイズする場合は第2引数を使います。

$table->foreignIdFor(User::class, 'author_id')->constrained('users');

複合主キーには非対応

複合主キーへの参照には従来の方法を使う必要があります。

よく使うパターン

// nullable な外部キー
$table->foreignIdFor(User::class)->nullable()->constrained();

// カスケード削除
$table->foreignIdFor(User::class)
    ->constrained()
    ->onDelete('cascade');

// SET NULL
$table->foreignIdFor(User::class)
    ->nullable()
    ->constrained()
    ->onDelete('set null');

// インデックスのみ(外部キー制約なし)
$table->foreignIdFor(User::class)->index();

まとめ

foreignIdFor() は、外部キー定義をより安全で保守しやすくする優れた機能です。
特別な理由がない限り、Laravel 8以降のプロジェクトでは積極的に使用することをお勧めします。

Laravel Policyで認可を正しく実装する

はじめに

APIエンドポイント /api/projects/{id} において、認証(Authentication)は通っているものの、認可(Authorization)の検証が不十分なケースがあります。

例えば、ユーザーAが自身のトークンを用いて、ユーザーBが所有する project_id を指定して GET / PUT / DELETE リクエストを送信した場合、サーバー側で所有権をチェックしていなければ、他人のデータを閲覧・改ざんできてしまいます。

これは典型的な IDOR (Insecure Direct Object Reference) 脆弱性です。

// 脆弱な例
public function update(Request $request, Project $project)
{
    // ログインしていることは確認されているが、
    // $project がログインユーザーのものかは見ていない
    $project->update($request->all());
}

Policyとは

Laravel Policyは、特定のモデルに対する認可ロジックをひとつのクラスに集約する仕組みです。

コントローラーやミドルウェアに if ($project->user_id !== auth()->id()) のようなチェックを散在させる代わりに、Policyクラスに認可ルールを定義し、コントローラーから呼び出します。

これにより以下のメリットが得られます。

  • 認可ロジックの一元管理 ─ 変更時にコントローラーを探し回る必要がありません
  • テストの容易さ ─ Policyクラス単体でユニットテストが書けます
  • 一貫性 ─ authorize メソッドやミドルウェアを通じて、どのエンドポイントでも同じルールが適用されます

Policyで解決する

1. Policyの生成

php artisan make:policy ProjectPolicy --model=Project

2. 認可ロジックの定義

app/Policies/ProjectPolicy.php に各アクションの認可ルールを記述します。

<?php

namespace App\Policies;

use App\Models\Project;
use App\Models\User;

class ProjectPolicy
{
    /**
     * プロジェクトの閲覧権限
     */
    public function view(User $user, Project $project): bool
    {
        return $user->id === $project->user_id;
    }

    /**
     * プロジェクトの更新権限
     */
    public function update(User $user, Project $project): bool
    {
        return $user->id === $project->user_id;
    }

    /**
     * プロジェクトの削除権限
     */
    public function delete(User $user, Project $project): bool
    {
        return $user->id === $project->user_id;
    }
}

ロジックはシンプルで、プロジェクトの user_id とログインユーザーの id が一致するかどうかだけを返します。

false が返れば、Laravelは自動的に 403 Forbidden レスポンスを返します。

3. Policyの登録

Laravel 8以降では、Policyの自動検出が有効です。
App\Models\Project に対して App\Policies\ProjectPolicy という命名規則に従っていれば、手動登録は不要です。

Laravel 10以前では AuthServiceProvider で明示的に登録することも可能です。
なお、Laravel 11以降では AuthServiceProvider がデフォルトのスケルトンから削除されているため、命名規則に従わない場合は AppServiceProviderboot メソッド内で Gate::policy() を使って登録してください。

// Laravel 10以前: AuthServiceProvider
protected $policies = [
    Project::class => ProjectPolicy::class,
];

// Laravel 11以降: AppServiceProvider(命名規則に従わない場合)
use Illuminate\Support\Facades\Gate;

public function boot(): void
{
    Gate::policy(Project::class, ProjectPolicy::class);
}

4. コントローラーでの適用

コントローラーの各メソッドで $this->authorize() を呼ぶだけで適用できます。

class ProjectController extends Controller
{
    public function show(Project $project)
    {
        $this->authorize('view', $project);

        return ProjectResource::make($project);
    }

    public function update(Request $request, Project $project)
    {
        $this->authorize('update', $project);

        $project->update($request->validated());

        return ProjectResource::make($project);
    }

    public function destroy(Project $project)
    {
        $this->authorize('delete', $project);

        $project->delete();

        return response()->noContent();
    }
}

これで、ユーザーAがユーザーBのプロジェクトIDを指定した場合、403 Forbidden が返却されます。

補足: authorizeResourceを使う方法

リソースコントローラーであれば、コンストラクタで一括適用もできます。

class ProjectController extends Controller
{
    public function __construct()
    {
        $this->authorizeResource(Project::class, 'project');
    }

    // 各メソッドからauthorize()の呼び出しを削除できます
}

authorizeResource は、コントローラーのメソッド名とPolicyのメソッド名を自動的にマッピングします(showviewupdateupdate など)。

まとめ

認証(Authentication)はリクエストの送信元が「誰か」を特定するだけであり、「そのリソースへのアクセス権があるか」を判定するのは認可(Authorization)の役割です。
この区別を見落とすと、IDORのような脆弱性が生まれます。

Laravel Policyを使えば、モデルごとの認可ルールを一箇所に集約でき、コントローラーでは $this->authorize() を一行追加するだけで済みます。
まだ導入していない場合は、既存のエンドポイントを一度洗い出して、Policyが適用されているか確認してみてください。

Gemini APIの429リソース不足エラーの回避策

「第3回生成AI Innovation Awards 最優秀賞」受賞プロジェクトで、Gemini APIの429リソース不足エラーへの対処としてExponential Backoff戦略が紹介されていました。

参考

背景

Gemini APIを大規模に運用すると、「429 Resource Exhausted(リソース不足)」エラーは避けられません。Vertex AIでは「スロット購入」という解決策が用意されていますが、ピーク時のアクセス量に合わせて契約すると運用コストが大幅に膨らみます。

今回のプロジェクトでは、「即時レスポンスは必須ではない」というビジネス特性を活かし、コストを抑えつつ処理を確実に完了させるリトライ戦略を採用しています。

Exponential Backoff

リトライ戦略の核となる「Exponential Backoff」は、API呼び出し失敗時に即座に再試行せず、リトライ間隔を指数関数的に延ばしていく手法です。

  • 1回目のリトライ:1秒後
  • 2回目のリトライ:6秒後
  • 3回目のリトライ:30秒後

待ち時間を段階的に増やすことで、サーバー負荷が下がるタイミングを捉えます。無計画な連続リトライによる負荷増大を防ぎながら、最終的にリクエストを成功させる合理的な設計です。

導入時の検討ポイント

この手法はコスト効率に優れる一方、適した用途の見極めが必要です。

  • 処理期限の猶予:完了まで数分〜数十分の遅延が許容されるか
  • リトライ設定の妥当性:最大待ち時間がビジネス要件を逸脱しないか
  • ユーザー体験への影響:バッチや非同期処理として切り離せているか

まとめ

追加コストを抑えながら膨大なリクエストを安定処理できるこの手法は、ビジネス要件によっては最良の選択肢となります。