Laravelプロジェクトの本番環境で php artisan test を実行してしまい、本番DBの複数テーブルのデータが空になる事故が発生しました。
原因は、Feature Testで使用されていた RefreshDatabase trait が、本番DBを対象に実行されてしまったことです。
php artisan test は、単に状態を確認するための読み取り専用コマンドではありません。
テストコードの内容や設定によっては、DBを作り直したり、テーブルをリセットしたりします。
この記事では、今回起きた事故の内容、直接原因、本来調査していた問題、設計上の反省点、再発防止策を整理します。
事故の背景
今回の作業では、Laravelプロジェクトに郵便番号マスター更新機能を追加していました。
管理画面から郵便番号マスター更新を開始できるようにし、更新状態を保存するためのテーブルとして zipcode_update_states と zipcode_update_logs を追加していました。
しかし、管理画面から「更新開始」を押しても zipcode_update_states が空のままでした。
その原因を調査している途中で、本番環境上で php artisan test を実行してしまいました。
結果として、本番DBの複数テーブルのデータが空になり、バックアップから復旧する必要が発生しました。
直接原因はRefreshDatabaseだった
直接原因は、LaravelのFeature Testに RefreshDatabase trait が使われていたことです。
RefreshDatabase は、テスト実行時にデータベースをマイグレーションし直したり、テストごとにDB状態をリセットしたりするための仕組みです。
これは本来、.env.testing などで指定したテスト専用DBに対して実行するものです。
しかし、本番環境で php artisan test を実行したため、テストが本番DBを対象にしてしまいました。
その結果、本番DBがリセットされ、データが消えました。
php artisan testは安全な確認コマンドではない
今回の事故で最も重要な点は、php artisan test を安全な確認コマンドだと思い込んではいけないということです。
コマンド名に test と付いていても、実行内容は読み取り専用とは限りません。
Laravelのテストコードに以下のようなtraitや仕組みが含まれている場合、DBに影響します。
RefreshDatabaseDatabaseMigrationsDatabaseTransactions- テスト内で実行されるSeeder
- テスト内で実行されるモデル作成や削除処理
特に RefreshDatabase は、本番DBに対して絶対に実行してはいけません。
テストは安全確認のための仕組みですが、実行先のDBを間違えると破壊的な処理になります。
今回のテスト出力から分かったこと
今回のテスト出力では、Tests\Feature\ZipMasterAdminTest はPASSしていました。
つまり、/zip-master/admin のルート、スーパー管理者制限、メンテナンス表示middlewareなどは、コード上は動いていたと判断できます。
一方で、多数のテストは失敗していました。
失敗内容には、Laravel\Jetstream\Features not found や Livewire\Livewire not found が多く含まれていました。
これは、古いJetstreamやLivewire系のテストが残っている一方で、現在の依存関係にはそれらが存在しないことを示しています。
これらの失敗自体は、郵便番号マスター更新機能の不具合とは直接関係がない可能性が高いです。
しかし、テストが失敗する前に RefreshDatabase が動き、本番DBをリセットしてしまった点が問題でした。
本来調べたかった問題
本来調べたかった問題は、管理画面の「更新開始」を押しても zipcode_update_states が空のままだったことです。
更新Service本体に到達していれば、処理のかなり早い段階で zipcode_update_logs にrunningログを作成し、zipcode_update_states にrunning状態を書き込む設計でした。
そのため、zipcode_update_states が空のままなら、ダウンロード失敗やCSV処理失敗ではなく、そもそも更新Serviceまで到達していない可能性が高いです。
原因候補としては、以下が考えられます。
- Webサーバー実行ユーザーから
phpコマンドが見えていない exec、nohup、proc_openなどが本番PHPで無効化されているQUEUE_CONNECTIONがsync以外なのにqueue workerが動いていない- 管理画面からバックグラウンドArtisanを起動する方式が本番環境に合っていない
- 最新修正コミットが本番環境に反映されていない
設計上の反省点
管理画面から直接 exec や nohup で php artisan を起動する方式は、環境依存が強いです。
SSHでは php が使えても、ApacheやPHP-FPMの実行ユーザーではPATHが異なる場合があります。
また、レンタルサーバーや本番PHP設定では、exec が disable_functions に含まれていることがあります。
「ボタンを押したら裏でArtisanを起動する」という設計は便利ですが、起動に失敗した場合に無音になりやすい点が問題です。
本番運用では、管理画面は「更新予約をDBに書く」だけにして、実処理はcronやqueue workerが拾う形の方が安全です。
少なくとも、起動に失敗した場合は、ログへ明示的に残す必要があります。
再発防止策
本番環境でphp artisan testを実行できないようにする
本番環境で php artisan test が実行された場合は、テスト開始前に即停止する仕組みが必要です。
Tests\TestCase やPHPUnit bootstrapで、環境がproductionの場合に例外を投げる方法が考えられます。
<?php
protected function setUp(): void
{
parent::setUp();
if (app()->environment('production')) {
throw new RuntimeException('Production environment cannot run tests.');
}
}
ただし、DBリセットが始まる前に止める必要があります。
そのため、アプリケーション側だけでなく、phpunit.xml、.env.testing、CI側でも多重に防ぐべきです。
.env.testingを必ず用意する
テスト実行時は、本番DBではなくテスト専用DBへ向くように固定します。
小規模なテストであれば、SQLiteのインメモリDBを使う構成が分かりやすいです。
DB_CONNECTION=sqlite
DB_DATABASE=:memory:
MySQLを使う場合でも、本番とは別のDB名を指定する必要があります。
本番サーバーにテストコードを置かない
本番環境にテストコードを配置しない運用も検討すべきです。
composer install --no-dev を使っても、プロジェクト内の tests ディレクトリ自体が残る可能性があります。
本番に不要なテストファイルは、デプロイ対象から外す設計にする方が安全です。
READMEや運用手順に明記する
php artisan test は本番環境で実行禁止であることを、READMEや運用手順に明記します。
特に RefreshDatabase を使用しているプロジェクトでは必須です。
ただし、ドキュメント化だけでは不十分です。
人の注意に頼るのではなく、実行できない仕組みを入れる必要があります。
破壊的なArtisanコマンドを本番で止める
本番環境では、DBを破壊する可能性があるArtisanコマンドを実行しにくくするべきです。
対象となる代表的なコマンドは以下です。
php artisan testphp artisan migrate:freshphp artisan migrate:refreshphp artisan db:wipe
これらは本番環境で即停止する仕組みを検討する必要があります。
バックアップと復旧手順を確認しておく
今回の事故では、バックアップから復旧できました。
しかし、バックアップが存在することと、実際に復旧できることは別です。
事前に復旧手順を確認し、どの時点まで戻せるのか、復旧にどの程度の影響が出るのかを把握しておく必要があります。
長時間処理は先に状態をDBへ残す
管理画面から長時間処理を起動する場合は、まず「開始ボタンが押された」という事実をDBに書き込むべきです。
その後で、ジョブ、cron、queue workerが実処理を拾う構成にします。
これにより、何も起きていないのか、起動に失敗したのか、処理中なのかを判別しやすくなります。
本番調査で比較的安全な確認コマンド
本番環境で調査する場合は、実行前に読み取り専用に近い内容かどうかを確認します。
今回のような背景であれば、以下のような確認コマンドが候補になります。
php artisan tinker --execute="dump(config('queue.default'));"
php artisan tinker --execute="dump(function_exists('exec')); dump(ini_get('disable_functions'));"
which php
php -r "echo PHP_BINARY, PHP_EOL;"
ls -l storage/logs/zipcode-update-background.log
tail -100 storage/logs/zipcode-update-background.log
これらも実行環境やコマンド内容を確認したうえで使うべきですが、少なくとも php artisan test のようにDBをリセットする目的のコマンドではありません。
本番で実行してはいけない危険なコマンド
以下のコマンドは、本番環境で安易に実行してはいけません。
php artisan test
php artisan migrate:fresh
php artisan migrate:refresh
php artisan db:wipe
特に migrate:fresh、migrate:refresh、db:wipe は、名前から見ても破壊的であることが分かります。
一方で、php artisan test は一見すると安全そうに見えるため、より注意が必要です。
まとめ
php artisan test は、本番環境で絶対に実行してはいけません。
特に RefreshDatabase を使っているLaravelプロジェクトでは、実行先DBを間違えると本番データを破壊します。
「テストだから安全」という考え方は危険です。
テストは安全確認のためのものですが、テスト専用DBに向いていることが保証されて初めて安全に実行できます。
再発防止には、.env.testing の整備、CIでの実行制御、本番実行ガード、テストコードのデプロイ対象除外、運用手順への明記が必要です。
また、管理画面から長時間処理を起動する場合は、Webサーバーから直接shell実行する設計に安易に頼らず、DB予約とcron、またはqueue workerを組み合わせた構成にする方が安全です。
本番データを守るためには、注意喚起だけでは不十分です。
危険な操作を実行できない仕組みを、プロジェクト側に組み込む必要があります。

コメント