Laravelのスケジューラで定期タスクを自動化する:cron1本から安全運用まで

Laravelのスケジューラで定期タスクを自動化する:cron1本から安全運用まで

LaravelのTask Schedulingは、複数のcronをサーバーに散らす代わりに「cronは1本だけ」「実行内容はLaravel側でコード管理」に寄せられる仕組み。定期メール、集計、ファイル生成、期限切れ処理、外部API同期などを、実行間隔・重複防止・失敗通知・ログ・ロックまで含めて運用設計できる。この記事は、導入の最短ルートから、実務で事故を減らす書き方・監視・キューとの分担までをまとめる。

全体像:cronは1本、実行定義はapp側(コード管理)

基本は「毎分 artisan schedule:run を叩くcron」を1本置き、実際に動かすタスクはLaravelのKernel(Laravel 11以降は routes/console.php 等の構成に合わせて)で定義する。
これにより、タスク追加/修正はデプロイで反映でき、サーバーに手作業でcronを増やさずに済む。

導入手順:まずは“毎分1本”のcronを入れる

Linuxサーバーでの最小構成。実行ユーザー、PHPのパス、アプリのパスを間違えると動かないので、まずはここを固定する。

* * * * * cd /var/www/app && php artisan schedule:run >> /dev/null 2>&1

ポイント:
・cdでプロジェクト直下に移動
・php artisan を実行できるユーザー権限
・/dev/null へ捨てず、最初はログへ吐いて動作確認すると楽

定義場所:スケジュールは“アプリの一箇所”に集約する

スケジュールが複数箇所に散ると、運用で「どれがいつ動くのか」が追えなくなる。
・定義は一箇所にまとめる
・命名規則を付ける(後述の onSuccess/onFailure で識別に使える)
・環境差(stagingだけ/productionだけ)は明示する

基本例:コマンドを作って毎日実行する

定期タスクは “いきなりクロージャで書く” より “Artisanコマンド化” するとテスト・再実行・権限管理がやりやすい。

php artisan make:command CleanupOldFiles

// app/Console/Commands/CleanupOldFiles.php(例)
namespace App\Console\Commands;

use Illuminate\Console\Command;

class CleanupOldFiles extends Command
{
    protected $signature = 'app:cleanup-old-files';
    protected $description = 'Delete old temporary files';

    public function handle(): int
    {
        // 実処理(例)
        // Storage::disk('s3')->delete(...);

        $this->info('done');
        return self::SUCCESS;
    }
}

スケジュール登録:毎日・毎時・毎分をコードで明示する

運用で迷う原因は “何時に動くのかが曖昧” なこと。頻度は明示し、実行タイムゾーンも意識する。

Schedule::command('app:cleanup-old-files')
    ->dailyAt('03:10')
    ->withoutOverlapping(60) // 分:ロック有効期間
    ->name('cleanup-old-files');

タイムゾーン:サーバー時刻とアプリ時刻のズレで事故が起きる

「毎日3時」のつもりが、サーバーがUTCで動いていてズレるのはよくある。
・appのtimezone(config/app.php)
・サーバーOSのtimezone
・DBのtimezone
この3つが混ざると、日次集計や締め処理が壊れやすい。スケジュールは “どのタイムゾーン基準か” を決めて統一する。

重複実行対策:withoutOverlapping で二重起動を防ぐ

タスクが想定より長引くと、次の実行タイミングで二重起動することがある。二重起動が許されない処理(請求、支払い、締め)には必須。

Schedule::command('app:cleanup-old-files')
    ->dailyAt('03:10')
    ->onOneServer()
    ->withoutOverlapping()
    ->name('cleanup-old-files');

多重サーバー対策:onOneServer で“1台だけ”に限定する

Webサーバーが複数台あると、全台で schedule:run が走り、同じタスクが台数分動く。
onOneServer は分散ロック(キャッシュ/Redisなど)を使って “1台だけ” 実行させる。

Schedule::command('app:cleanup-old-files')
    ->dailyAt('03:10')
    ->onOneServer()
    ->withoutOverlapping()
    ->name('cleanup-old-files')
    ->onFailure(function () {
        logger()->error('schedule failed: cleanup-old-files');
        // Slack通知などをここで実施(実装は環境に合わせる)
    })
    ->onSuccess(function () {
        logger()->info('schedule success: cleanup-old-files');
    });

前提:キャッシュドライバが複数台で共有されていること(Redisなど)。ファイルキャッシュだと台ごとに分離されて成立しない。

失敗通知:onFailure / onSuccess で結果を外に出す

スケジューラは“静かに失敗”しやすい。最低限、失敗時通知を付けると復旧が早い。

Schedule::command('app:cleanup-old-files')
    ->dailyAt('03:10')
    ->appendOutputTo(storage_path('logs/schedule-cleanup.log'))
    ->name('cleanup-old-files');

ログ運用:appendOutputTo で“後から追える”状態にする

/dev/null に捨てる運用は、障害時に何も残らない。
・最初はファイルへ吐く
・安定したらログ基盤へ集約(CloudWatch等)
・タスクごとにログを分けると追いやすい

Schedule::command('app:cleanup-old-files')
    ->dailyAt('03:10')
    ->appendOutputTo(storage_path('logs/schedule-cleanup.log'))
    ->name('cleanup-old-files');

長時間処理:スケジュールは“起動”だけ、重い処理はキューへ逃がす

スケジュールが直接重い処理を抱えると、タイムアウト・重複・失敗リトライが難しくなる。
推奨:
・スケジュールはジョブ投入(dispatch)まで
・実処理はキューで分割
・再試行や冪等性はジョブ側で担保
これで「定期実行のトリガー」と「処理本体」が分離される。

// 例:定期的にジョブを投入する
use App\Jobs\RebuildDailyReport;

Schedule::job(new RebuildDailyReport())
    ->dailyAt('02:30')
    ->onOneServer()
    ->name('rebuild-daily-report');

環境ごとの制御:stagingだけ/productionだけを明示する

検証環境で本番同等の処理を回すと、外部APIやメールが暴発することがある。環境で分ける。

if (app()->environment('production')) {
    Schedule::command('app:cleanup-old-files')
        ->dailyAt('03:10')
        ->onOneServer()
        ->withoutOverlapping()
        ->name('cleanup-old-files');
}

実行確認:schedule:list と schedule:run で動作を潰す

cronを待たずに、手元で “定義されているか/動くか” を確認してから本番に入れる。

php artisan schedule:list
php artisan schedule:run

schedule:run は “実行対象があるタイミング” でないと何も起きないので、minutes単位のタスクを一時的に置くと確認しやすい。

よくあるトラブル:動かない/二重実行/時間ズレの典型原因

動かない:
・cronのユーザーが違う、パスが違う、cdしていない
・php のパスが違う(複数バージョン)
・.env が読めていない(権限/パス)
二重実行:
・複数台で回して onOneServer が無い
・タスクが長引き withoutOverlapping が無い
時間ズレ:
・サーバーUTC、アプリJST、DB別タイムゾーンなどが混在
“動かない”はログの出し方を整えると即解決に寄る。

まとめ:Laravelスケジューラを安全に回すための最小セット

・cronは毎分1本、実行定義はアプリでコード管理
・コマンド化して再実行/テスト/監視をしやすくする
・withoutOverlapping と onOneServer で重複実行を潰す
・失敗通知とログを必ず付ける(黙って落ちるのを防ぐ)
・重い処理はキューへ逃がし、スケジューラは“起動役”にする
・タイムゾーンと環境差を明示して事故を減らす