メインコンテンツまでスキップ

まとめ — 本番リリース前チェックリスト

第1章で「動くコードと運用できるコードの差」を挙げてから、21 章かけて 1 つの注文 API を育ててきました。

最後に、本番へ出す前に確認する 1 枚のリストを置きます。各項目には「落ちると何が起きるか」と、根拠になった章を添えました。

このリストの使い方

全部を一度に満たす必要はありません。 扱う金額や利用者の数で、必要な水準は変わります。

そのかわり、チェックを外すときは何が起きるかを知ったうえで外してください。 そのために各項目へ「落ちると何が起きるか」を書いています。読んで「うちでは起きない」と判断できるなら、それは正しい省略です。

章番号は根拠の在り処です。 項目の意味が分からないときは、その章へ戻ってください。下の「章の索引」にリンクがあります。1 行では判断の理由まで書けません。

設定と秘密

  • env()config/ の外で呼んでいない (第2章) — config:cache した本番でだけ null を掴む
  • .env をリポジトリに入れていない (第2章) — 秘密情報が履歴に残り、消しても追える
  • 本番の APP_DEBUGfalse (第2章・第9章) — スタックトレースと環境変数がレスポンスに出る
  • 500 の本文に例外メッセージを入れていない (第9章) — テーブル名やファイルパスが外へ出る
  • error_id を返して調査可能性を残している (第9章) — 内部情報を出さずに問い合わせへ答えられる

データベース

  • 本番で migrate:fresh を実行する手順が無い (第3章) — 全テーブルが落ちる
  • シーダーを本番で流す手順が無い (第3章) — テストデータが本番に入る
  • マイグレーションを「前進のみ」で設計している (第3章) — rollback が必要な状況では既に手遅れなことが多い
  • 外部キーの削除の連鎖を意図して決めている (第3章) — 商品を消したら過去の注文明細まで消える
  • 在庫の更新に条件付き UPDATE を使っている (第14章) — 同時注文で在庫が負になる
  • トランザクションに attempts: を付けている (第14章) — デッドロックで 1 件が落ちる
  • トランザクションの中に外部 API 呼び出しやメール送信が無い (第14章・第15章) — 再試行で二重に実行される
  • 再送の鍵に有効期限を設けている (第14章) — 鍵が無限に増える
  • 在庫が負になっていないか定期的に見ている (第14章・第20章) — 管理画面からの調整で静かに壊れる

ルーティングと入出力

  • URL に連番の ID を出すかを決めている (第4章) — 連番だと総件数と成長速度が外から分かる
  • ネストしたリソースに scopeBindings() を付けている (第4章) — 他人の子リソースを引ける
  • route:cache を本番でだけ使っている (第4章) — ローカルで変更が反映されず混乱する
  • バリデーションを通ったデータだけを使っている (第5章) — $request->all() は未検証の値を含む
  • nullablesometimes を取り違えていない (第5章) — 送られてこないキーと null は別の状態
  • 422 のレスポンス形を決めている (第5章) — 公開後は変えられない
  • Resource の中でクエリを発行していない (第7章) — 一覧で N+1 になる
  • レスポンスに内部 ID を出すかを決めている (第7章) — 出したら消せない
  • 日付の形式を決めている (第7章) — 途中で変えるとクライアントが壊れる

設計の置き場所

  • singleton にリクエスト固有の状態を持たせていない (第6章) — 次のリクエストへ漏れる
  • サービスクラスをコンストラクタ注入で受けている (第6章) — ファサード直呼びはテストで差し替えづらい
  • 切り出したクラスの凝集度を確かめている (第6章) — 分けただけで関係の薄いメソッドが同居する

一覧 API

  • per_page に上限を付けている (第8章) — per_page=100000 で全件が返る
  • 検索の LIKE が前方一致で、インデックスが効いている (第8章) — 中間一致は全表走査になる
  • 一覧に含めるリレーションを絞っている (第8章) — 1 件あたりの取得が重くなる
  • ソート順が一意に決まる (第8章) — 同値の行がページ間で重複・欠落する

認証と認可

  • 平文のトークンを発行時にしか見せていない (第11章) — DB にはハッシュしか無い
  • トークンをログに出していない (第11章・第21章) — ログを見られる人が全員なりすませる
  • ログイン試行に回数制限をかけている (第13章) — 総当たりが通る。第11章の時点では穴が空いたままだった
  • 一覧 API で認可が効いている (第12章) — 1 件の漏れと違い、全件が漏れる
  • Gate::before を安易に使っていない (第12章) — 管理者の判定が全 Policy を素通りする
  • 認可を Resource で代用していない (第12章) — 見せない項目を消しても、取得自体は成功している
  • 認可の判断をフロントエンドに任せていない (第12章) — API を直接叩けば通る

流量制御

  • 公開しているエンドポイントを一覧にして、それぞれに制限が要るかを判断している (第13章) — api グループには既定でレート制限が含まれず、明示的に付けたルートだけが制限される
  • ID 列挙への対策がある (第13章) — 通常のレート制限だけでは足りない
  • 扱わないイベント種別も 200 で返している (第13章) — 5xx を返すと送信側が再送を繰り返す
  • Webhook の署名を検証している (第13章) — 誰でも決済完了を送れる
  • 429 の発生数をログに残している (第13章・第21章) — 正常な利用者が弾かれていることに気づけない

非同期処理

  • リスナーの失敗が本体の処理を巻き込まない形にしている (第15章) — メール送信の失敗で注文が消える
  • 通知の宛先を確かめている (第15章) — 開発中のテストメールが利用者へ飛ぶ
  • QUEUE_CONNECTIONsync のままになっていない (第16章) — キューに積んだつもりが同期実行される
  • ワーカーが落ちたときに気づける (第16章・第21章) — 誰も処理しないまま溜まる
  • failed_jobs を見る仕組みがある (第16章・第17章) — 記録はされるが誰も見ていない
  • ジョブが冪等になっている (第17章) — リトライで二重に課金・送信される
  • タイムアウトを retry_after より短くしている (第17章) — 同じジョブが二重に走る
  • 1 つのジョブを大きくしすぎていない (第17章) — 途中で失敗すると全部やり直しになる
  • デプロイ手順に queue:restart が入っている (第17章・第21章) — 古いコードが動き続ける

キャッシュ

  • キャッシュが落ちたときに何が起きるかを確かめている (第18章) — 全リクエストが DB へ流れて共倒れになる
  • キャッシュの消し忘れが無い (第18章) — 静かに古い値を返し続ける
  • 何もかもキャッシュしていない (第18章) — 整合性の問題が増えるだけで速くならない

ファイル

  • ディスク名を省略していない (第19章) — 既定値が変わると非公開のファイルが公開側へ移る
  • public/ へ直接置いていない (第19章) — デプロイで消え、S3 へ移せなくなる
  • throwtrue にしている (第19章) — 書き込みの失敗が戻り値でしか分からない
  • アップロードされたファイル名をそのまま使っていない (第19章) — 送信側が付けた名前は信用できない
  • 署名付き URL の有効期限が短い (第19章) — 発行時の 1 回しか認可を見ない
  • 孤児のファイルを掃除する仕組みがある (第19章・第20章) — トランザクションはファイルを巻き戻さない

定期処理

  • cron に schedule:run の 1 行が登録されている (第20章) — 登録を忘れると何も起きず、エラーも出ない
  • config/app.phptimezone とサーバーの時刻の関係を把握している (第20章) — 片方だけ変えるとずれる
  • 複数台に増やす前に onOneServer() を入れ、共有キャッシュを使っている (第20章) — 全台で同じ処理が走る。filearray のキャッシュでは何も防げない
  • withoutOverlapping() のロック期限を明示している (第20章) — 既定の 24 時間は短い周期の処理には長すぎる
  • スケジューラが止まっていることに気づける (第20章・第21章) — 静かに何も起きなくなる

ログと監視

  • slack チャンネルの level を下げている (第21章) — 既定は critical なので Log::error が届かない
  • LOG_LEVEL を複数チャンネルで使い回していない (第21章) — ファイルのレベルを下げると Slack へ全部流れる
  • ログのローテーションがある (第21章) — single はディスクを埋める
  • リクエスト ID をログに通している (第21章) — 同時実行のログが混ざって追えない
  • 機密情報を落とすプロセッサを全チャンネルに付けている (第21章) — stack 側に書いても効かない
  • error に想定内のエラーを混ぜていない (第21章) — 本当に調べたい例外が埋もれる
  • 死活監視のエンドポイントを認証とレート制限の外に置いている (第21章) — 監視サービスがトークンを持つことになる

テスト

  • 認可のテストがある (第10章) — 他人のデータが見える不具合は、テストが無ければ気づけない
  • 外部との通信をテストから出している (第10章) — 相手のサービスが落ちるとテストが落ちる
  • テストが実行順に依存していない (第10章) — 単体で走らせると落ちる
  • 落ちたテストを消して green にしていない (第10章) — 消した瞬間にその契約は無くなる
  • 足した配線に、それが消えたら落ちるテストがある (第19章・第20章) — ジョブの投入やスケジュール登録は消しても他が通る

デプロイの手順

第21章で並べた手順です。毎回実行するものと、環境を作るときに 1 回のものを分けています。

# 毎回
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan queue:restart
  • .env の DB 接続先を変えたデプロイでは config:cache を先に打っている (第21章) — 前回のキャッシュが残っていると migrate が古い接続先へ流れる
  • queue:restart がキャッシュの更新より後にある (第21章) — 先に打つと再起動したワーカーが古い設定を読む

環境を作るときの分は 2 つです。

php artisan storage:link
# crontab に 1 行
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
  • storage:link自分のデプロイ方式に合わせて配置している (第19章・第21章) — リリースごとにディレクトリを作る方式では public/storage が毎回消えるので、毎回のデプロイに入れる必要がある

章の索引

どの章で何を決めたかの一覧です。読み返すときの入口として使ってください。

決めたこと
第1章題材と完成形のエンドポイント一覧
第2章bootstrap/app.php の構成、env()config/ の中だけ
第3章テーブル設計、enum キャスト、#[Fillable]、ファクトリ
第4章apiResource()apiPrefix: 'api/v1'、ルートモデルバインディング
第5章Form Request で形式チェックの境界を固める
第6章サービスクラスとコンテナ、StockChecker の分離
第7章API Resource でレスポンスの形を決める
第8章ページネーション、フィルタ、N+1 の検出
第9章例外を HTTP へ翻訳する。変換は bootstrap/app.php に集める
第10章Pest で API の振る舞いを固定する
第11章Sanctum のトークン認証、ゲスト注文の引き継ぎ
第12章Gate と Policy、一覧 API の認可
第13章自作ミドルウェア、Webhook の署名検証、RateLimiter
第14章条件付き UPDATE、attempts:、再送の一意制約
第15章イベントとリスナー、Notification
第16章Job クラス、dispatchqueue:work
第17章冪等性、リトライ、failed_jobsqueue:restart
第18章キー設計、スタンピード対策、無効化
第19章ディスクの使い分け、署名付き URL、ジョブでの PDF 生成
第20章routes/console.php、cron 1 行、未決済注文の自動キャンセル
第21章チャンネルと stackContext、マスク、デプロイ手順

この連載で扱わなかったこと

第1章と目次で範囲外とした項目を、もう一度置いておきます。必要ないという意味ではなく、この連載の射程の外という意味です。

項目どこで宣言したか補足
Blade / Livewire / Inertia第1章全章が JSON を返す API の話でした
ブロードキャスト (Reverb / Echo)第1章サーバー側だけ設定しても、受け取るクライアントが要ります
設計論 (DDD の戦術パターン / クリーンアーキテクチャ)第1章下の「次に読む」で扱います
Docker / サーバー構築 / CI/CD目次第21章は Laravel 側の設定に限定しました

デプロイ手順をスクリプトにして自動で流す形は、この連載の外側にあります。第21章までで「何を実行するか」は決まっているので、それをどう自動化するかが次の話になります。

次に読む

この連載は「標準構成でどう書くか」を扱いました。次の 3 つが、それぞれ別の方向へ伸ばします。

  • Laravel × DDD × クリーンアーキテクチャ実践ガイド — 同じ機能をどの層に置くかで組み立て直します。第6章で切り出したサービスクラスが、値オブジェクト・エンティティ・集約へ分かれていきます
  • PHPクラス設計ガイド — カプセル化や単一責任といった、クラス 1 つの中の判断基準を扱います。第6章で「凝集度を確かめる」と書いた部分の土台です
  • データベース設計ガイド — 正規化、E-R 図、インデックス設計。第3章でテーブルを作るときに前提としていた理論です

この連載を最初から読み直すなら、上の索引が入口になります。 21 章分を通しで読む必要はありません。手を動かしていて詰まった箇所の章だけを開いてください。