2019/12/17

rsyncの悲劇 〜本番環境を消し飛ばす前に覚えておきたいこと〜


この記事は本番環境でやらかしちゃった人 Advent Calendar 2019 17日目の記事です。


はじめまして、ダーシノ(@bc_rikko)です。

突然ですが、懺悔します。
私は転職して10ヶ月で2回も本番環境をぶっ飛ばしました。お客様をはじめ、関係各位には多大なるご迷惑をおかけしたことを、ここでお詫び申し上げます。


1回目は2015年11月27日、入社27日目のこと。
gitの設定ミスにより壊れたブランチをmasterにforce pushしてしまい、CIが流れて本番環境が壊れた。原因はpush.defaultなのだが、詳しくはすでに記事を書いているのでそちらを読んでほしい。
2回目は翌年9月1日、入社してちょうど10ヶ月たった日のことだ。
またしても本番環境をぶっ飛ばした。しかも、前回より盛大に……。

タイトルにもあるようにrsyncコマンドが原因だ。
当記事では、この「rsyncの悲劇」を二度と繰り返さないために原因・対策を共有したい。


勘の鋭い方は「rsync」と「本番環境でやらかしちゃった」という組み合わせでオチにお気づきと思いますが、しばらくの間お付き合いください。

TL;DR


  1. デプロイスクリプトを修正する
  2. 想定とは異なるディレクトリでrsync --deleteが実行される
  3. 本番環境が消し飛ぶ
  4. 復旧作業するもバージョン管理されていない秘蔵モジュールが消滅する
  5. 完全復旧できず……。


前置き: デプロイまわりの環境


入社当時のデプロイ環境はGitHubのmasterブランチにマージされるとCIが動き、冗長化された2台のWebサーバ(本番環境)にファイルが転送される仕組みだった。



知識なき者は、ときに大胆な行動をとる


元SIerのシステムエンジニアで、Windows+C#で業務システムを開発していた。UNIXの知識はほとんどなく、できることといえばcatとtailでログファイルを見る程度だった。

そんな私がデプロイスクリプトの修正担当になった。
要件は「プロジェクトのディレクトリ構造が変わったので、それをデプロイできるようにする」というもの。

修正箇所は、カレントディレクトリを2台の本番サーバにrsyncしている部分だった。
DEPLOY_TARGETS=()
DEPLOY_TARGETS[0]=user@prod-1.example.com
DEPLOY_TARGETS[1]=user@prod-2.example.com

for target in ${DEPLOY_TARGETS[@]}; do
  rsync -av --delete -e ssh ./ ${target}:${DEPLOY_PATH}
done

スクリプトが最低限動くことを確認したが、他のコマンドの関係でローカルでテストできなかった。


「でも、まぁ、大した修正していないしうまく行くっしょ!」


初心者特有の根拠なき謎の自信を暴走させ、そのままPullRequestを出した。


追記: 2019/12/17 18:20
Twitterやブコメで「ステージング環境ないの?」とコメントがあったので補足します。
ステージング環境はもちろんあり、デプロイは成功しました。そのため本番行っても問題ないでしょう!とマージしたところ……当記事のとおりです。
後述する秘蔵のモジュールに関しては、ステージング環境にはなく本番環境を参照していました。



本番環境が消し飛ぶ


リリースの時間になり、問題のPullRequestがmasterブランチにマージされた。そして、CIが流れ始めた。

今日のデプロイはやけに時間がかかるなー、いつもは数分で終わるのに。
そんなことを思いながらデプロイが完了するのを待っていた。


おわかりいただけただろうか?
いつもよりデプロイに時間がかかっていた理由に。


次の瞬間……。


何も表示されないんですが?!?!?!?!


デプロイにあんなに時間がかかっていたのは、rsync --deleteが本番環境(2台分)のファイルをすべて削除していたからだったのだ。

--deleteフラグとは、転送元と転送先のディレクトリをそっくり同期するもの
rsync -a --deleteの意味は記事最後に補足をまとめたので、知らない方はそちらを参照ください。



秘蔵モジュールが消滅し、サービスの一部が機能しなくなる


git revertをして再度デプロイした。そして、正常にデプロイが終わり、本番環境が息を吹き返した……(感涙)

と、思ったのも束の間。
カスタマーサポートの方から次々に連絡が入ってきた。

「○○機能が動かないんですが?」
「フィードバックが送れないです」
「お知らせが表示されていません」


?!?!?!?!?!?

元に戻したはずなのに、WHY?


rsyncに-vフラグをつけていたのを思い出し、CIツールに出力されているrsyncのログを探した。しかし、当時はログサイズに上限がありすべて取得できなかった。

唯一の手がかりとなる一部欠損したログと実際にデプロイされているディレクトリやファイルを比較したところ、新たな問題を発見した。

なんとサービスで使っていた一部のモジュールが消滅していたのだ。
しかもそのモジュールがgitで管理されておらず、もうどこにもない。


半泣きになりながらSlackで助けを求めた。


かろうじて数ヶ月前にとったバックアップが見つかったので、そこから復旧。
バックアップにもなく完全に失われてしまったモジュールに関しては、仕様を確認し作り直し、ようやく完全?復旧を遂げた。



原因と対策: 悲劇を繰り返さないために


私が考える本事件の原因は4つ。
  1. デプロイスクリプトのテストが不完全だった
  2. 本番で使われているモジュールがバージョン管理されていなかった
  3. 正常性バイアスがあった
  4. 睡眠不足だった

1. デプロイスクリプトのテストが不完全だった

rsync初体験にも関わらず、テストもほどほどにmasterにマージしてしまったのがそもそもの問題だ。本来であれば--dry-runフラグを使ってテストすべきだった。

--dry-runフラグとは、実際には動作しないが、同期内容だけログに表示されるものだ。通常は-vフラグ(動作内容を表示する)と併用する。
$ rsync -av --delete --dry-run ./ /path/to/www
www/index.html
www/lib/script.js
www/style/base.css
www/style/theme.css
このように転送されるディレクトリとファイルがリスト表示される。もちろんDryRunなので実際には転送されていない。これでテストしておけば差分に気づけ、本番環境を消し飛ばす自体にはならなかっただろう。

2. 本番で使われているモジュールがバージョン管理されていなかった

使われているモジュールやファイルがバージョン管理されていれば、たとえ本番環境を消し飛ばしたとしても短時間で復旧できただろう。(といっても、消滅して初めてその存在を知ったので私には何もできなかったが…。)

使っているファイルはすべてバージョン管理しよう。
デプロイに失敗しても、本番環境が消えても、すぐに元通りにできる環境を用意しよう。

3. 正常性バイアスがあった

「いつもできたから今回も大丈夫だろう」
「大して変更していないからテストはいらないだろう」

そういった正常性バイアスは重大なインシデントにつながる。

とくに初心者が陥りがちな根拠なき謎の自信の暴走による失敗。当時の私がそうであったように、「自信」はときには原動力になるが、本番環境を触るときは不要だ。

4. 睡眠不足だった


当日は4時に就寝、7時に起床しており完全に睡眠不足だった。
睡眠不足は判断力を著しく低下させるという研究結果があるように、いつもと違う「何か」に気づけなくなる。(今回の場合はデプロイにいつも以上に時間がかかったこと)
気づいていれば本番環境をすべて削除する前にCIを止められたかもしれない。

本番環境を触る日は十分な睡眠が必要だ。



さいごに


  • rsyncが含まれたスクリプトを初めて実行するときは必ず--dry-runをつけよう
  • ちゃんとテストしよう
  • 使っているファイルはバージョン管理しよう
  • 十分な睡眠をとろう

この記事でrsync --deleteの被害者がひとりでも減れば幸いだ。



補足: rsync -a --delete の意味


rsyncとは、ファイルやディレクトリを同期するコマンドだ。
たとえばローカルのディレクトリをサーバと同期させるには以下のようになる。
$ tree /local/project
.
├ index.html
├ style
|  ├ base.css
|  └ theme.css
└ lib
   ├ script.js
   └ lib.js

$ rsync -a -e ssh /local/project root@web-prod.example.com:/path/to/www

これでローカルの/local/projectのディレクトリとファイルが、web-prod.example.comというサーバの/path/to/wwwに同期される。

さらに今回問題になった--deleteフラグを使うと、ローカルとサーバのディレクトリをそっくり同期させることができる。

たとえば本番環境が以下のような構成だったとする。
web-prod$ tree /path/to/www
.
├ 消しちゃダメ.html
└ lib
   └ 消しちゃダメ.lib.js

$ tree /local/project
.
├ index.html
├ style
|  ├ base.css
|  └ theme.css
└ lib
   ├ script.js
   └ lib.js
このときにrsync --deleteをするとサーバ側の「消しちゃダメ」ファイルが全部消えて、以下のような状態になる。
web-prod$ tree /path/to/www
.
├ index.html
├ style
|  ├ base.css
|  └ theme.css
└ lib
   ├ script.js
   └ lib.js
これでなぜ本番環境が消し飛んだか理解していただけただろうか。


rsync、こわい……。




以上

written by @bc_rikko

2 件のコメント :

  1. rsyncやらかし定番の末尾に/つけたかつけてなかったか問題の話と思ってたんですが違ってました。

    返信削除