この連載の記事(全4回)
- 【第1回】Next.jsのキャッシュを複数インスタンスで共有する ── GCSカスタムキャッシュハンドラ実装記
- 【第2回】「ローカルでは動いたんですけどね」── 本番でしか出ない404と、一度戻したリリースの記録
- 【第3回】たった1行の欠落が、3分の瞬断を77分の障害に変えた
- 【第4回】「本番でしか出ないバグ」は、本当に本番でしか出ないのか(この記事)
第1回で私たちは「ISRとCustom Cache Handlerは、本番のビルド・実行構成でしか本性を現さない」という教訓を得ました。第3回では、その教訓が横展開されていなかったために77分の障害を起こしました。結論から書くと、この障害はCIでCache-Controlを1行検査するだけで防げたものでした。
この最終回で書きたいのは、その先です。障害後、それぞれの欠陥について「本当にどこでも見つけられなかったのか」を1つずつ検証してみました。結果は、正直かなり悔しいものでした。ローカルで3行のコマンドを叩けば見つかるものが混ざっていたのです。
そしてもうひとつ。この調査ではAI(Claude Code)をフルに使いました。十数万行のログを数分で捌いてくれた一方で、3回、自信満々に間違えました。その中身も包み隠さず書きます。
「ローカルでは動いたんですけどね……」
ISR、SSG、エッジキャッシュ、CDN。このあたりを触っていると、必ず一度は「ローカルと検証では完璧に動いたのに、本番だけ壊れる」という体験をします。しかも本番なので落ち着いて調査もできない。
何度かそれを経験すると、チームの中に「この領域は本番でしか分からないから仕方ない」という空気が生まれます。これ自体は経験に裏打ちされた、それなりに正しい認識です。
その「仕方ない」は、いつ思考停止に変わるのか
問題は、この認識がいつの間にか「だから事前には防げない」という結論にすり替わることです。私たちがまさにそれでした。
後から検証してわかったのは、「本番固有の要素」と「devモードでは見えないだけの要素」がまったく別物だったということです。前者はGCSの認証、CDNの有無、インスタンス台数といった、環境そのものに紐づくもの。後者は単に next build していないから見えないだけで、ローカルのproductionビルドを立ち上げれば普通に観測できるものです。
この2つを一緒くたにして「本番でしか分からない」と括った瞬間に、手が届く範囲まで諦めてしまう。9か月間バグを放置した本当の理由は、技術的な難しさではなく、この雑な線引きにありました。
しかも、これが初めてではありませんでした。第1回・第2回で書いた「初回リリース→6日後に全件404→一括切り戻し」の一件では、障害の13日前に実施したテストで、当該URLが404であることを実際に記録していました。それを「CMSにコンテンツが無いから」と解釈して見送っていたのです。症状は観測できていたのに、原因の可能性を区別しなかった。
「見えなかった」のではなく「見えていたが、そう解釈しなかった」。今回の77分障害はその再演でした。だから対策は、注意力を高めることではなく、解釈の余地を残さない仕組みに寄せる必要があります。
こんな方に読んでほしい
- 「本番でしか出ないバグ」を、事前に検出する手立てがないか探している
- 検証環境と本番の構成差が大きく、テストの費用対効果に悩んでいる
- 障害調査やコードレビューにAIを使い始めた/使うか迷っている
- AIが出した結論をどこまで信じてよいか、判断基準がほしい
まず、発見可能性を棚卸しする
障害後、5つの欠陥それぞれについて「ローカル(dev)/検証環境(dev)/本番のどこで発見できたか」「本当はどこで見つけられたか」を表にしました。これが今回いちばん価値のあった作業です。

①はローカルで3行だった
いちばん効いた欠陥①。私たちは「ISRは本番でしか本性を現さない」と信じていました。半分は正しい。でも、Cache-Controlヘッダの検証だけはローカルで完結します。
next build && next start
curl -sI localhost:3000/存在しないパス/ | grep -i cache-control
# → s-maxage=31536000 ... これが1年の正体
必要なのはproductionビルドだけ。GCSもCDNもCloud Runも要りません。9か月間、誰もこの3行を実行しなかったというだけの話でした。
②はテストコード数行だった
期限切れハンドリングも同じです。「GCSとの結合が必要」と思い込んでいましたが、実際に検証したいのは get() のロジック分岐だけ。GCSクライアントをモックし、expireAt が過去のエントリを渡して、返り値が null でないこと(=staleが返ること)をassertすれば済みます。インフラは一切要りません。
③④は「見つけられない」ではなく「環境を作っていない」
CDNへの焼き付きは、検証環境にCDNが無い以上どうしようもありませんでした。ただしこれは「見つけられない」ではなく「見つけられる環境を作っていない」です。
④のバックエンド maxScale=1 に至っては、コードを1行も読まなくても、検証環境と本番のCloud Run設定を並べて diff を取るだけで見つかります。「フロント4台・バックエンド1台」という非対称は、一度でも並べて見れば誰でも違和感を持ったはずです。設定はコードレビューの対象外になりがちですが、キャパシティ設計はコードと同じくらい壊れるのだと学びました。
費用対効果の順に並べる ─ まずCIでCache-Controlを検査する
以上をふまえて、対策を3レベルに整理しました。上から順にコストが低く、効果が高い順です。

特にレベル1の①──CIにCache-Controlのアサーションを入れる──は、今回の77分障害をマージ前に止められた唯一の施策です。「404を返すレスポンスの s-maxage が600を超えたらCIを落とす」というルールを1つ置くだけ。しかも一度入れれば、将来 revalidate を付け忘れた新規ページも自動で捕まえてくれます。「レビューで気をつける」より、はるかに信頼できます。
実際に選んだ優先順位
revalidate: 60の横展開(19か所・10ファイル)── 完了。最悪の被害が「77分 → 最大60秒の自己回復」になった- bot対策(正規botのallowlist+レート制限)── コード変更ゼロ、CDN設定のみ。今回のスパイクの主成分はbotだった
- Cloud RunのmaxScale 1→4 ── cron排他とDB接続数の確認を終えて次のリリースに同梱
fetchAPIにタイムアウト+リトライ
逆に、技術的には最も効く「キャッシュハンドラのstale対応」は見送りました(理由は第3回に書いたとおりです)。やらない判断も、理由と再着手条件をセットで記録すれば立派な対策になります。
AIに障害調査をさせてみた
ここからは、この調査でAI(Claude Code)を使った話です。まず、圧倒的に速かったことから。
ログの横断集計。十数万行のCloud Runログから「404を返したリクエストのUA分布」「同時間帯のステータスコード内訳」「429の総数」を出す。人間なら半日、AIなら数分でした。
この集計から「スパイクの主成分は実ユーザーではなくbotだった」ことも判明しています。平常時はUAがバラけている(Edge、iPhone Safari、Chrome最新、Googlebot……)のに、スパイク時だけ約1000リクエスト中624が同一の旧バージョンChrome。
しかも全講座ページを機械的に総なめし、各ページのJSが叩く集計APIを703回連打していました。人間のアクセスではありえない挙動です。
もうひとつ大きかったのは、仮説の反証コストが劇的に下がったことです。「この設定が原因では?」と思ったとき、確認コマンドを組み立てて実行し結果を要約するまでが速い。従来なら「確認が面倒だから」と切り捨てていた筋を、全部潰しきれるようになりました。
誤診1:「Strapiのレート制限に引っかかった」
初版のレポートはこう書きました──「バックエンドのStrapiがレート制限を発動している。レート制限の設定値を見直すべき」。もっともらしい。429といえばレート制限です。私も一度は納得しました。
実際は、第3回に書いたとおりCloud Run自身のインスタンス上限でした。もしそのまま対応していたら、存在しない設定を探して時間を溶かしたうえ、真犯人の maxScale=1 は温存されていたはずです。
誤診2:存在しないコードを「影響範囲」に入れた
「同じリスクを持つファイル」の横断調査は、本来AIの得意分野です。ところが出てきたリストには、本番には存在しないファイルが2件混ざっていました。作業ツリーにチェックアウトされていた別ブランチのコードを、本番出荷版だと思い込んで読んでいたのです。
逆に、実際にリスクを持つファイルを1つ丸ごと見落としてもいました。「新デザインのページ群は対策済み」という思い込みが働き、そのグループの中に1ファイルだけ例外があることに気づけなかった。結果、最終的な影響範囲は10か所から19か所・10ファイルに修正されました。
誤診3:直してはいけないファイルを直そうとした
修正対象リストには、getServerSideProps を使うプレビューページも入っていました。しかしSSRに revalidate は無効なプロパティで、付けても効きません。適用前に気づいて除外できましたが、機械的に一括置換していたら意味のない差分を本番に入れていました。
AIは「探索」に強く、「前提」に弱い
3つの誤診には共通点があります。どれも推論の誤りではなく、前提の取り違えでした。どのブランチを見ているか。429を返しているのが誰か。そのページがSSRかSSGか。
AIは与えられた材料の中では極めて正確に推論します。しかし材料そのものが間違っていることには、自分では気づけません。そして厄介なことに、間違った前提から導かれた結論も、正しい結論とまったく同じ自信のある文体で出てきます。
だから最終的にやったのは、AIの結論をひとつずつ実測に置き換えることでした。「revalidate無しは1年キャッシュされるはず」→ 実際にcurlを打つ。
「DB接続数が心配」→ Cloud Monitoringの実測値を引く(ピーク25、プール上限とぴったり一致していました)。「429はアプリのレート制限」→ Cloud Runの生ログを直接読む。AIに広く探索させ、人間が実測で反証する。この往復がいちばん速くて確実でした。
訂正を消さずに残す
運用面でひとつ、意識して変えたことがあります。障害レポートに「【重要な訂正】429の正体」という節を設け、初版がなぜ間違えたかまで書き残しました。
訂正を消して清書すると読みやすくはなりますが、次に同じ材料を見た人がまた同じ勘違いをします。「429を見たらレート制限だと思い込みやすい」という認知の罠こそが、レポートに残す価値のある情報でした。AIを使うと調査のスピードが上がる分、こうした「間違えた過程」が失われやすいので、意識して残す必要があります。
AIが苦手だったこと:「やらない」判断
最後に。AIに聞けば、技術的に正しい改善案はいくらでも出てきます。しかし「移行予定があるから今は投資しない」「逆向きの障害リスクの方が高い」といった判断は、事業の文脈を知る人間の仕事でした。
AIは選択肢を漏れなく並べることには長けていますが、その中から「やらない」を選び、なぜやらないかを説明し、いつ再開するかを決めるのは、まだ人間の領域です。
これはAI駆動開発の入口にすぎない
今回のAI活用は「ログを読ませて仮説を出させ、人間が実測で反証する」という、調査アシスタントとしての使い方でした。AI駆動開発と呼ぶにはまだ初歩的な段階です。
次の段階は、判断を含む処理を任せるAIエージェント化だと考えています。たとえば今回いちばん価値のあった「発見可能性の棚卸し」は、障害のたびに人間が手作業でやっている仕事です。障害レポートとコードとCIの設定を突き合わせ、「この欠陥はどの層で検出できたか」「その検出をどのテストに落とせるか」を提案させるところまでは、十分に自動化の射程に入っています。人間は提案を承認し、優先順位を決める側に回る。
さらにその先には、開発・運用の意思決定そのものを支える基盤があります。障害履歴・監視データ・コード・デプロイ履歴を一つの文脈として扱い、「この変更は過去のどのインシデントと同じ構造か」を提示できるようになれば、Palantir AIPが業務オペレーションに対してやっていることを、開発プロセスに対して行うことになります。
今回の記事に書いたのは断片的な実践ですが、私たちはそれを、この線上にある点のひとつとして捉えています。
- 「本番固有の要素(GCS認証・CDN・台数)」と「devモードでは見えないだけの要素」を分けて考える。後者はローカルのproductionビルドで普通に観測できる
- CIにCache-Controlのアサーションを入れる。「404レスポンスの s-maxage が閾値を超えたら落とす」の1本で、今回の障害はマージ前に止まっていた
- 環境間の設定 diff を定期的にレビューする。maxScaleやconcurrencyの非対称は、コードを読まずに見つかる
- originの404率とCDN経由の404率を別々に監視する。「復旧したのに直らない」76分は、これがあれば数分で気づけた
- AIは探索に強く前提に弱い。広く調べさせ、結論は必ず実測で反証する。そして訂正の過程を消さずに残す