デプロイは正常に終わりました。09:41。サイトもちゃんと表示されました。三時間後にアクセスすると403 Forbidden でした。
サーバーに入ってドキュメントルートを見ます。
ahha.iffthen.com/ assets/ ← これだけ残っている
index.html も .htaccess も各ページのファイルも全部消えていました。assets/ だけが生き残っています。再デプロイすると復旧しました。そして数時間後にまた消えました。
一度目の調査 — 原因が見つからなかった
当然デプロイスクリプトを疑いました。rsync --delete を使っているからです。サーバーを漁りました。
crontab -l→ このサイト関連のジョブはスクレイパーひとつだけcron.log→ 正常実行の記録のみ- スクレイパーのコード →
data/にしか書かず他は触らない
このサイトの中に破壊を起こす要因がありませんでした。それでも破壊は繰り返されます。「rsync –delete のせいだろう」「三時間おきに何か回っているようだ」といった推測を繰り返し、原因未確定のまま調査を打ち切りました。
二度目の調査 — 外を見た
次に取り組むときは接近を変えました。このサイトの中ではなく外を見ました。
/home/<user>/iffthen.com/public_html/ ← 上位ドメインのドキュメントルート
index.html
(上位ドメインのファイル)
ahha.iffthen.com/ ← サブドメインがこの中にある
index.html
assets/
other.iffthen.com/
サブドメインのドキュメントルートが、上位ドメインのドキュメントルートの下位フォルダになっています。このホスティングの構造です。ここで答えが見えました。
- 上位ドメインの
public_html/の mtime が破壊時刻と一致 - 別のサブドメインひとつも同じ時刻に影響
- 上位ドメインには別プロジェクトのランディングページが載っており、そちらもデプロイスクリプトを回していた
そのスクリプトが public_html/ 全体を rsync --delete で配信していました。そして向こうのビルド成果物には当然 ahha.iffthen.com/ フォルダがありません。
rsync --delete other-project/out/ → /home/<user>/iffthen.com/public_html/ out/ にないもの = 削除対象 → ahha.iffthen.com/ の中のファイルが全部消える
assets だけ生き残った理由
ここがヒントだったのに、最初は読めませんでした。相手プロジェクトのビルド成果物にも assets/ というフォルダがありました。名前が同じなので rsync がそのパスを削除対象と見なさなかった。偶然生き残ったのです。
「なぜ assets だけ残るのか」を真剣に掘っていれば、一度目の調査で原因を掴めました。
処方 — 上位ドメイン側を直す
問題は被害者側ではなく加害者側にありました。上位ドメインのデプロイスクリプトに exclude を入れます。
rsync -avz --delete \ --exclude='ahha.iffthen.com/' \ out/ \ <user>@<host>:/home/<user>/iffthen.com/public_html/
GitHub Actions のワークフローと手動デプロイスクリプトの両方に入れました。片方だけ直すと、別の経路でデプロイしたときにまた飛びます。
被害者側(サブドメイン)のスクリプトは触っていません。もともと自分のドキュメントルートだけを対象にし data/ を exclude していたので問題がありませんでした。
復旧
ファイル自体は再デプロイで戻ります。問題はサーバー上でしか生成されないデータでした。スクレイパーが定期的に更新して data/ に積んでいた CSV が飛びました。
- アプリのバンドルに含まれていた初期データから CSV を抽出
- scp でサーバーへアップロード
- スクレイパーの backfill モードで最新回次だけ取り直して append
サーバー上でしか生成されないデータがあるなら、バックアップ経路を先に用意しておくこと。今回はアプリのバンドルに初期データがあったので助かりました。
この事故の構造的な原因
サブドメインのドキュメントルートが上位ドメインのドキュメントルートの下位フォルダであるホスティング構造で、上位ドメインのデプロイスクリプトが --delete を使うと、サブドメインが丸ごと消えます。
似た構造のホスティングはすべて該当します。各サイトを別プロジェクトが管理していると互いの存在を知らないので、さらに危険です。
プロジェクト A の担当:「自分のサイトを配信しているだけだが何が問題?」 プロジェクト B の担当:「なぜ自分のサイトが繰り返し消えるのか」
二人が同じ人物でも、セッションが違えば気づけません。実際そうでした。
スポンサーリンク
スポンサーリンク
予防ルール
1. 上位ドメインのスクリプトにサブドメインの exclude を先に入れる
サブドメインを追加するたびに上位ドメインのスクリプトも直す必要があります。忘れやすいのでスクリプトにコメントを残します。
# このサーバーのサブドメインは public_html/ 配下に置かれる。 # 新しいサブドメインを追加したら必ず下記 exclude に加えること。 --exclude='ahha.iffthen.com/' --exclude='other.iffthen.com/'
2. –delete を使うスクリプトは dry-run を既定値にする
bash scripts/deploy_web.sh # dry-run・アップロード一覧の出力のみ bash scripts/deploy_web.sh --auto # 実際に配信
オプションを明示しないと実際には出ません。事故の後に作ったのではなく元からそうなっていて、おかげでこちら側では事故が起きていません。
3. サーバー上でしか生成されないデータは exclude してバックアップ経路を置く
調査の仕方についての教訓
技術的な内容よりこちらのほうが残りました。
ひとつのプロジェクトの中をどれだけ漁っても原因が出ないなら、外を見ます。
- 上位フォルダの mtime
- 兄弟フォルダ(他のサブドメイン)の状態
- 同じサーバーにある別プロジェクトのデプロイ履歴
- 破壊時刻と他プロジェクトのデプロイ時刻の相関
一度目の調査でこれをやりませんでした。「自分のプロジェクトの問題」という前提を壊さずに一日を使いました。
そして過去のやり取りを先に検索します。後で分かったのですが、別プロジェクトの作業中に「public_html 全体を rsync で配信する」という話が出たことがありました。その記録を探していればずっと速かった。
証拠が消える前にスクリーンショットを撮ります。破壊直後のファイル一覧・mtime・ログは、再デプロイすると全部上書きされます。復旧を急ぐ前に状態を記録しておくことが原因究明に要ります。
この記事について
自社アプリ ahha の Web 資産の運用中に起きた事故をまとめたものです。
破壊時刻と相手プロジェクトのデプロイ時刻の正確な対照は mtime に基づく推定で、サーバーのアクセスログで確定させたわけではありません。他社ホスティングも同じディレクトリ構造かは確認していません。


コメント