メインコンテンツへスキップ

HTTP 200なのに壊れている|Cloudflare運用で無音の故障を6件見つけた記録

サイトの障害は、落ちてくれれば気づける。厄介なのはHTTP 200を返しながら壊れている状態である。編集部はCloudflare上で運用しているサイト群で、この形の故障を6件見つけた。削除した記事が7日間配信され続け、設定ファイルが本番に存在せず、定期実行が毎日成功しながら成果は0件だった。

目次

この記事の立ち位置

サイトが落ちれば気づける。監視が鳴り、利用者から連絡が来る。

厄介なのは、HTTP 200を返しながら壊れている状態である。 応答コードは正常、画面も一見それらしい。監視は鳴らない。誰も気づかないまま、何週間も何か月も続く。

編集部はCloudflare上で複数のサイトを運用している。この記事は、そこで見つかった無音の故障6件と、検知するために入れた仕組みの記録である。

6件の内訳は、設定ファイルが本番に存在しなかった、レイアウト名の誤りが素通しされた、キャッシュで入口だけ到達できた、定期実行が成功のまま成果ゼロだった、プレビュー用の設定が本番のタグを止めなかった、計測タグが一度も発火していなかった、である。いずれも応答コードは正常だった。

いずれも実際に起きたことで、多くは自動的には検知されなかった。人が別の作業をしていて偶然気づいた。

事例1:設定ファイルが本番に存在しなかった

最も影響が大きかった例である。

何が起きたか

サイトの応答ヘッダーとリダイレクトを設定するファイルが、本番環境に一度も配置されていなかった。

原因は、静的サイト生成ツールの仕様である。アンダースコアで始まるファイル名は、出力先のディレクトリに含まれない。設定ファイルの名前がアンダースコアで始まっていたため、ビルドのたびに落ちていた。

デプロイのスクリプトは、出力先をそのまま配信していた。設定ファイルが無くてもサイトは正常に見える。

何が壊れていたか

2つの機能が効いていなかった。

削除した記事の強制的な404が効いていなかった。 削除済みの記事198本のうち35本が、HTTP 200で全文(104KB)を配信し続けていた。

キャッシュの制御が効いていなかった。 配信基盤の既定値が適用され、記事のURLが7日間キャッシュされる状態になっていた。このため、削除しても7日間は配信が続く。

どう気づいたか

配信基盤が用意している別ドメインと、本番ドメインの応答を比べた。別ドメインでは404、本番では200という差が出た。

この比較をしなければ、本番だけを見て「200が返る、正常」と判断していた。

対策

デプロイの手順に、設定ファイルを出力先へ明示的にコピーする段階を追加した。加えて、コピー後にファイルが存在し、中身が空でないことを確認している。

設定の書き方はCloudflare Pagesのヘッダー設定リダイレクト設定に従っている。書き方が正しくても、本番に存在しなければ効かない。

事例2:レイアウトの誤りが素通しされた

何が起きたか

記事のメタ情報で指定するレイアウトの名前が、実在しないものになっていた。

静的サイト生成ツールは、これをエラーにしない。 存在しないレイアウトが指定されると、本文だけを出力する。ヘッダーもナビゲーションも、ページタイトルも、構造化データも無い、裸のHTMLが生成される。

どれくらい続いたか

主要なまとめページ10本と、1つの製品紹介ページが、数か月この状態だった。

ファイルサイズを比べると、正常なページが55KBに対し、壊れたページは7KB。並べれば一目で分かる差である。

なぜ気づかなかったか

ページを開けば、本文は表示される。内容も正しい。「デザインが崩れている」とすら見えない場合がある。

そして、この種のページは検索結果にも出にくくなるが、元々アクセスが少ないページだと、減ったことに気づけない。

対策

デプロイ前に、記事が指定しているレイアウト名が実在するかを照合する検査を追加した。存在しない名前が1つでもあればデプロイを中止する。

設定の仕様はJekyllの設定ドキュメントにあるが、「エラーにしない」という挙動が、そのまま無音の故障になる。

事例3:キャッシュで到達できてしまう

別のサービスで起きた例である。

何が起きたか

会員向けの画面一式が、デプロイの不備で出力から丸ごと消えた。13時間にわたって全滅していた。

ところが、入口のURLはHTTP 200を返し続けた。 キャッシュから配信されていたためである。その下にあるログイン画面だけが404を返す、という切り分けにくい形になった。

この形が厄介な理由

「トップは見えるからサーバーは生きている」と判断してしまう。実際には、配信すべきファイルが存在していない。

キャッシュは、障害を隠す方向に働く。 応答が返ることと、正しいものが返ることは別である。

Cloudflareのキャッシュの仕様を理解したうえで、キャッシュを迂回した経路でも確認する必要がある。

対策

監視の対象を、入口のURLだけでなくその下の代表的なページ群に広げた。入口が200でも、下が404なら異常として扱う。

事例4:定期実行が毎日「成功」で0件

何が起きたか

記事を自動生成する定期実行が、毎日「成功」と記録されながら、生成した記事は0件という状態が続いていた。

原因は2段になっている。生成に使うコマンドへのパスが環境の変更で通らなくなり、実行すると即座に失敗するようになった。しかし、実行結果をログファイルに書き出す際にパイプを使っており、パイプ全体の終了コードは最後のコマンドのものになる。ログを書くコマンドは成功するので、全体は成功と記録される。

監視は何を見ていたか

「定期実行が成功したか」を見ていた。 成功していたので、何も鳴らなかった。

どう気づいたか

記事が増えていないことに人が気づいた。自動的には検知されなかった。

対策

パイプの途中で失敗したら全体を失敗にする設定を入れた。加えて、「成功」の定義を「実行が終わったこと」から「成果物が増えたこと」に変えた。

実行の成功と、目的の達成は別の量である。 定期実行の監視は、終了コードではなく成果物の件数を見る。

Cloudflareの定期実行を使う場合も同じで、実行されたことと、意図した処理が完了したことは分けて確認する必要がある。

事例5:プレビュー用の設定が本番タグを止めなかった

何が起きたか

本番と同じ内容のプレビューを作る際、アクセス解析のタグを無効にしようとした。追加の設定ファイルに、解析IDの項目を空の値で書いた。

静的サイト生成ツールの設定の結合は、空の値で既存の値を上書きしない仕様だった。 結果として、既存の本番用IDがそのまま残り、生成されたHTMLには本番の解析タグが入っていた。

なぜ気づかなかったか

ローカルでの確認時に、外部への通信を遮断していた。タグが入っていても、送信が発生しないので表示上は分からない。

生成されたHTMLを開いてタグの有無を確認していれば分かったが、見た目の確認しかしていなかった。

対策

生成物のHTMLを文字列として検査する。 プレビュー用の出力に本番のIDが含まれていないことを、機械で確認する。

pushしても本番は変わらない

運用上、最も誤解しやすい点である。

編集部のメインサイトは、配信基盤のプロジェクトとしてGit連携を持っていない。手動でのデプロイのみである。GitHubへpushしても何も起こらない。

同じアカウント内でも違う

一方で、同じアカウント内の別のプロジェクトはGit連携を持っており、pushで自動的にビルドが走る。

プロジェクトごとに違う。 名前や思い込みで判断すると誤る。

確認の方法

管理APIでプロジェクトの一覧を取得し、ソースの設定とデプロイの契機を確認する。手動なのか、Git連携なのか。

そして、「公開した」と言う前に、本番URLを実際に叩いて変更が入っていることを確かめる。

一覧が打ち切られていた

上記の確認をした際に、別の失敗を踏んだ。

プロジェクトの一覧を取得して「10件」と数えたが、実際には26件あった。 APIの既定のページサイズで打ち切られていた。

何が起きたか

見落とした16件の中に、Git連携を持つプロジェクトが4件含まれていた。その状態でpushを行い、予期しないビルドが走った。

学び

一覧を返すAPIは、総件数と照合する。 多くのAPIは応答に総件数を含んでいる。返ってきた件数と総件数が一致しているかを見ないと、静かに欠けた一覧で判断することになる。

デプロイ前に挟んでいる検査

これらの事故を受けて、デプロイのスクリプトに検査を積み上げた。現在は6段ある。

1. 特定のコミットを別の場所に取り出す

作業中のディレクトリをそのまま配信しない。未コミットの実験が本番に出ることを防ぐ。

2. レイアウト名の実在を照合する

事例2の対策である。存在しない名前が1つでもあれば中止する。

3. 記事の衛生検査

作業報告の混入、段落の重複、本文全体がコードブロックで囲まれている状態、区切り記号の残留を検出する。

この検査を入れたきっかけは、63記事が汚染され、うち6記事は本番で行番号つきのソースコードとして表示されていたことである。ビルドもHTTP 200も通るため、明示的に検査しない限り公開され続ける。

当初は記事のディレクトリだけを対象にしていたが、固定ページが素通りしていた。 実測で固定ページに絵文字が127か所、記事本文に参照データの断片が21か所残っていた。公開される本文は記事だけではない。

4. ビルド

コンテナ内でビルドする。ローカルに実行環境を持たない。

5. 設定ファイルの明示的なコピーと存在確認

事例1の対策である。コピーしたうえで、ファイルが存在し空でないことを確認する。

6. 内部リンクの検査とSEOの監査

公開に実行する。公開後に見つけても遅い。

この検査を入れたきっかけは、全510記事のパンくずリンクが、存在しないURLを指していたことである。日本語のカテゴリ名をURL用の文字列に変換する処理の結果、実在しないパスになっていた。表示は正常で、リンクをクリックして初めて404になる。

検査を入れる順序

安いものから順に並べる。 ビルドは時間がかかるので、その前に落とせるものは落とす。レイアウト名の照合は1秒で終わる。

動かしているWorkerの構成

配信の前段で動かしている処理である。いずれもCloudflare Workersで書いている。

内部ファイルの遮断

リポジトリの構成上、公開したくないディレクトリがある。スクリプト、データ、設計文書、作業ログ。

これらのパスへのアクセスを正規表現で判定し、該当したら404を返す。同時に、キャッシュを禁止するヘッダーと、検索エンジンに登録させないヘッダーを付ける。

該当しない場合は、そのまま配信元へ通す。

クローラ向けの応答

一部のサブドメインについて、検索エンジンのクローラ向けの応答を固定で返している。管理画面や内部向けのページが検索結果に出ないようにするためである。

指定の仕方は検索エンジンによる登録の抑止に従っている。

注意点として、ルートの指定にはクエリ文字列も含まれる。 指定を厳密にしすぎると、パラメータ付きのアクセスが処理されずに抜ける。

wwwありからwwwなしへの転送

複数のドメインについて、wwwありのホスト名でのアクセスを、wwwなしへ301で転送している。

この処理を入れる前は、wwwありのホスト名が404を返していた。 気づいたのは、検索エンジンの管理ツールで登録を確認していたときである。

Workerのルートは配信設定に依存する

運用上の落とし穴として、Workerのルートは、DNSの設定が配信基盤を経由する状態になっていないと効かない。 設定したつもりで効いていない状態が起こり得る。

デプロイ後に動作を確認するには、リアルタイムログで実行結果と例外の有無を見る。デプロイの成功表示だけでは、実行されているかも例外が出ていないかも分からない。

15分ごとに何を見ているか

別のサービスでは、定期実行による監視を入れている。15分ごとに動く。

応答コードだけでは足りない

到達確認に加えて、内容の検査を3つ入れている。

健全性を返すAPIの中身を見る。 応答が200でも、中のデータベース接続の状態が異常と書かれていれば異常として扱う。

応答ヘッダーの処理経路を見る。 どの層が応答を返したかを示す値を確認する。想定と違う層が返していれば、配信の構成が変わっている。

サイトマップに含まれるURLの件数を数える。 通常は800件前後で、300件を下回ったら異常とする。件数が減るのは、生成の失敗かデータの欠落を意味する。

データの同期も見る

定期的に外部から取得するデータについて、直近の成功記録が48時間以内にあるかと、取得件数が基準を下回っていないかを確認する。

さらに、実行中のまま3時間以上経過している記録があれば、詰まりとして扱う。

通知を2種類に分ける

到達できない状態と、到達できるが中身が異常な状態を、通知の件名で分けている。

前者は緊急で、後者は調査が必要という意味になる。同じ形で通知すると、どちらも見られなくなる。

復旧の通知も出す

状態をWorkers KVに保存し、異常から正常に戻ったときにだけ復旧の通知を出す。

正常が続いている間は通知しない。 定期的に「正常です」と通知すると、通知そのものが読まれなくなる。

監視が鳴っても人が動かなければ同じ

正直に書くべきことがある。

事例3の13時間の障害について、監視は15分ごとに正しく異常を通知していた。 検知は機能していた。

それでも13時間続いた。 通知が届いていたが、対応されなかった。

検知と対応は別の問題

監視を作ると、問題が解決した気になる。しかし検知できることと、対応されることは別である。

通知が多すぎると読まれなくなる。通知の宛先が明確でないと、誰も動かない。夜間や休日の扱いを決めていないと、翌営業日まで放置される。

決めておくこと

誰に届くか。 個人宛か、共有の窓口か。

何分以内に見るか。 決めていないと「気づいたとき」になる。

見たことをどう示すか。 確認した人が何かを残さないと、全員が「誰かが見ているだろう」と考える。

通知の数を絞る。 鳴りすぎる監視は、鳴らない監視と同じである。

事例6:計測タグが一度も発火していなかった

最後の1件である。原因は他の5件と違うが、無音になる仕組みは同じなので、その共通点まで含めて記録する。

何が起きたか

あるサービスのフロントエンドで、アクセス解析・広告・SNSの計測タグを、ビルド時に環境変数から値を埋め込む方式で入れていた。

設定した環境変数が、ビルドの設定に登録されていなかった。その結果、埋め込まれるはずの値が、置換されないままの文字列としてHTMLに残った。

なぜ無音になったか

タグを読み込むコードには、値が正しいかを確認する処理が入っていた。置換されていない文字列を検出すると、タグの読み込みをせずに黙って終了する。

この設計自体は妥当である。不正な値でタグを読み込むより安全だからだ。しかし、黙って終了することが問題だった。

結果として、そのサービスの計測は一度も動いていなかった。 サイトはHTTP 200を返し、画面は正常に表示される。管理画面を開いても、データが0件なのか、そもそも送られていないのかは区別できない。

どう気づいたか

別の作業で複数のサイトの計測状況を横断的に確認していたときに、1つだけ通信が発生していないことに気づいた。

対策

ビルドの前後で、置換されていない文字列がHTMLに残っていないかを検査する。

さらに、公開後の監視にも同じ検査を入れた。トップページを取得し、置換前の文字列が含まれていたら異常として扱う。

この形の一般化

値が無いときに黙って無効化する設計は、無音の故障を作る。

安全側に倒すこと自体は正しいが、倒したことを誰かに伝える必要がある。 ログに出す、監視で検出する、起動時に警告する。黙って無効化してよいのは、無効でも問題がない機能だけである。

無音の故障は検索で特に高くつく

これらの故障が、なぜ放置されやすく、なぜ高くつくのかを整理する。

検索の反映は遅い

ページが壊れても、検索結果からすぐ消えるわけではない。数週間かけて順位が下がる。

逆に、直しても回復に数週間かかる。気づくのが遅れると、損失の期間が伸びる。

減ったことに気づけない

元々アクセスの多いページなら、減れば分かる。しかし大半のページはアクセスが少ない。 少ないものがゼロになっても、合計への影響は小さく見える。

編集部の実測では、公開している726記事のうち209本(29%)は表示が1回もない。 この中に壊れているページが混ざっていても、数字上は区別できない。

この分布の全体像はAIライティングで726記事を運用した結果にある。上位1本で表示の39.2%を占めるという偏り方をしている。

クローラは人と違うものを見る

事例2のような裸のHTMLは、人が見ると本文が読める。しかしクローラは、タイトルも構造化データも無いページとして扱う。

人の目視で確認しても、クローラにとっての見え方は分からない。検索エンジンの管理ツールで、実際に取得された内容を確認する必要がある。

削除が効いていないと二重に損をする

事例1では、削除したはずの記事が配信され続けていた。これは単に古い情報が出るだけでなく、同じサイト内で内容の近いページが競合する状態を作る。

意図して1本に統合したのに、統合前のページが残っていれば、統合の効果は出ない。

だから、検査は公開前に置く

編集部が内部リンクの検査とSEOの監査を公開前に置いているのは、この理由による。

公開後に見つける仕組みも必要だが、公開後に見つかった時点で、既に数週間の損失が発生している。

無音の故障を見つける手順

既存のサイトで確認するなら、次の順序が効率的である。

1. 生成物のファイルサイズを並べる

同種のページのファイルサイズを並べ、極端に小さいものを探す。 事例2はこれで一目で分かる。

2. 配信基盤の別ドメインと本番を比べる

多くの配信基盤は、本番ドメインとは別に確認用のURLを用意している。両者の応答を比べる。 事例1はこれで見つかった。

3. 削除したはずのURLを叩く

過去に消したページのURLを控えておき、実際に404が返るかを確認する。200が返ったら、削除が効いていない。

4. 設定ファイルが本番に存在するか確認する

ヘッダーやリダイレクトの設定が、配信されている側に存在するかを直接確認する。

5. 定期実行の成果物の件数を見る

終了コードではなく、増えたファイルの数を見る。0件が続いていないか。

6. 計測タグが実際に発火しているか見る

HTMLにタグが入っていることと、発火していることは別である。実際の通信を確認する。

計測そのものを誤って読んだ記録はSearch Consoleの数字を5倍間違えたにまとめている。同じ期間の数字が5倍違って見えた。

確認の際の注意として、観測の窓を十分に取る。 編集部では、短い窓で測ったために正常なサイトを異常と誤判定したことがある。9秒程度は待つ必要がある。

設計として効いたこと

5件を通して、共通する対策を挙げる。

検査を「走る場所」に埋める

検査が存在することと、走っていることは別の量である。 編集部には、必要な検査が実装されていたのに実行されておらず、問題が放置されていた例がある。

デプロイの手順に組み込み、失敗したら中止する。手で実行する検査は、実行されなくなる。

検査が存在するのに走っていなかった例、走っていても製品の異常を捕まえなかった例はE2Eテストが16件落ちたに書いた。

対象が0件なら失敗にする

検査の対象が0件のとき、多くの実装は「問題なし」で正常終了する。これは合格ではなく、検査が届いていない状態である。

編集部では、対象が0件なら中止する扱いに変えた。

正常値を先に知る

「異常かどうか」を判定するには、正常な値を知っている必要がある。サイトマップが800件、ページのサイズが55KB、計測タグの読み込みが4〜5件。

正常値を記録していないと、異常を異常と判定できない。

壊れた実物で検査を確かめる

検査を追加したら、壊れた状態を用意して、実際に止まることを確認する。

編集部では、壊れたデプロイを対象に検査を回して4件が失敗すること、正常な状態では15件すべて通ることを確認した。片方だけでは、何も検出しない検査になっていても気づけない。

まとめ

HTTP 200を返しながら壊れていた故障を6件記録した。

設定ファイルが本番に存在しなかった。 削除済み記事198本のうち35本が全文配信され、記事URLが7日間キャッシュされていた。別ドメインとの応答差で発覚した。

レイアウト名の誤りが素通しされた。 ヘッダーもタイトルも無い7KBのページが数か月公開されていた。正常は55KBである。

キャッシュで入口だけ到達できた。 配信物が13時間失われていたが、入口は200を返していた。

定期実行が毎日「成功」で成果0件。 パイプで終了コードが失われていた。実行の成功と目的の達成は別の量である。

プレビュー用の設定が本番タグを止めなかった。 空の値では既存の設定を上書きしない仕様だった。

計測タグが一度も発火していなかった。 埋め込むはずの値が置換されず、読み込み側がそれを検出して黙って無効化していた。安全側に倒すこと自体は正しいが、倒したことを誰にも伝えていなかった。

6件に共通するのは、失敗が正常な応答として現れたことである。落ちてくれれば気づける。返ってくるから気づけない。

監視は正しく鳴っていたが、13時間対応されなかった。 検知と対応は別の問題である。

明日から試せる3つ

1つ目は、同種のページのファイルサイズを並べることである。極端に小さいものがあれば、そこが壊れている。1分で終わる。

2つ目は、過去に削除したURLを3つ叩くことである。200が返ったら、削除が効いていない。

3つ目は、定期実行の監視を「成功したか」から「何件増えたか」に変えることである。

編集部では、AI導入の支援に加えて、こうした運用の仕組みづくりも行っている。動いているように見えるが確かめ方が分からない、という段階の相談については、サービス案内を参照してほしい。

参考にした一次資料

よくある質問

HTTP 200が返っていればサイトは正常ですか。

正常とは限りません。編集部が見つけた例では、削除したはずの記事がHTTP 200で全文を配信し続けていた、レイアウト指定の誤りでヘッダーもタイトルも無い7KBのページが数か月公開されていた、といった状態がありました。いずれも応答コードは200です。監視が応答コードだけを見ていると検知できません。

デプロイが成功すれば反映されていますか。

反映経路によります。編集部のサイトでは、GitHubへのpushでは本番が変わらないプロジェクトがあります。Git連携の有無はプロジェクトごとに異なるため、名前や思い込みで判断できません。デプロイの成功表示ではなく、本番URLを実際に叩いて変更が入っていることを確かめる必要があります。

設定ファイルが本番に存在しないことがあるのですか。

あります。編集部の例では、静的サイト生成ツールがアンダースコアで始まるファイルを出力先に含めない仕様だったため、ヘッダーとリダイレクトの設定ファイルが本番に存在しませんでした。存在しなくてもサイトは200を返すため無音で失敗します。結果として削除済み記事198本のうち35本が全文配信され続けていました。

定期実行の監視は何を見るべきですか。

終了コードではなく成果物の件数です。編集部では、記事生成の定期実行が毎日「成功」と記録されながら、生成した記事は0件という状態が続いていました。実行結果をログに書き出す際のパイプで、失敗した側の終了コードが失われていたためです。実行の成功と目的の達成は別の量として扱ってください。

キャッシュがあると障害の切り分けが難しくなりますか。

なります。編集部が関わったサイトでは、配信の不備でページ群が13時間にわたり失われていましたが、入口のURLはキャッシュから返っていたためHTTP 200のままで、その下の1ページだけが404という状態でした。キャッシュの有無で見え方が変わるため、キャッシュを迂回した経路でも確認する必要があります。

無音の故障を検知するにはどうすればよいですか。

応答コードに加えて、内容の条件を検査します。編集部が運用している監視では、健全性を返すAPIの中身、応答ヘッダーがどの経路で処理されたかを示す値、サイトマップに含まれるURLの件数が基準を下回っていないかの3つを、15分ごとに確認しています。件数や中身を見る検査でなければ、200を返す故障は捕まりません。

関連する取り組み

CONNECTED SERIES
AIで投資の壁を越える
18 本の実装記録。AI 投資の「予測不能」と言われる 9 つの壁を、コードと実データで検証した連載。
note で読む →
CONSULTING
AI導入の無料相談
ALLFORCES が、本記事のような失敗パターンを回避する AI 導入支援を提供しています。まずは課題を聞かせてください。
問い合わせる →