セッション概要と自己紹介

小佐野洋氏(以下、小佐野):はい、よろしくお願いします。justInCaseTechnologiesの小佐野といいます。今回は「本番環境にホットフィックスをリリースして破壊した話」というタイトルでお話しします。私のこの話はガチの障害です。障害をどうやって回復したのかとかのお話をします。

そもそも本番環境にホットフィックスって、修正するためリリースするのに、なんでリリースして破壊すんねやと。謎なことが多いとは思いますが、ちょっとその謎に迫ってみたいと思います。

まず、私は小佐野といいます。フルスタックエンジニアで、Android、Webフロント、バックエンドのテックリードを渡り歩いてきたというか。だいたいなんでもできるような感じの人間です。エンジニア歴10年ぐらいで、そろそろ古株になってきた感触はなんかちょっとあります。

会社紹介させてください。justInCaseTechnologiesといいます。どういうことをやっているのかというと、今まで兄弟会社のjustInCaseという保険会社に対して、保険のデジタル化のようなものを提供してきましたが、それを第三者に対して提供しようということをやっている会社です。

事象が起きた経緯

ではさっそくですが、本番環境にホットフィックスをリリースしてどう破壊したのかと何が起こったのかの話を、何があったのか、そもそもどうなっていたのか、なぜ起こったのかの3本立てで話をしようと思います。

それはある日の午前中のことでした。SPAと書いてありますが、リアクトアプリケーションです。「データが表示されない問い合わせがきたので、ちょっと確認してもらえますか」と。

(スライドの)緑色は私です。「確認したところ不具合でした」「修正します」「ホットフィックスとして本番環境にリリースしました」「対応ありがとうございます」。ここで終わっていたら、今日の話はありませんでした。その後に、なにか雲行きが怪しくなります。

「お客さまから問い合わせがきて返信したんですが、今度はサインインができなくなったとの問い合わせがきました」と。このときはわりと楽観的で、「いつものブラウザーキャッシュかな」と思い、「フルリロードやってもらえますか」と返しました。

ところが「同じような問い合わせが、別の方からもきているのですが」と。あれ? なんかおかしいな、と思って調べてみました。そしたら驚愕の事実がわかります。

APIの接続先が、本番環境ではなくなっていました。(スライドに)demoと書いてありますが、上は本番環境で、下は本番環境ではないところです。「ええー!?」と。

そもそもホットフィックスリリースしたときは、そのAPIの接続先などにはまったく関係のないものです。いわゆる、“何もしてないのに壊れた”というやつです。

リアクトアプリケーションの構成

どうしてこうなったのかを話す前に、リアクトアプリケーションがどういうふうになっていたのか。デプロイフローからお話ししようと思います。

ざっくり書くとこんな感じです。本当はRoute53とかいろいろありますが、ちょっとそこは割愛します。

簡単に説明すると、GitHubのメインプランチのマージをトリガーにして、CircleCIのワークフローが起動します。

CircleCIのワークフローは本番環境とステージ環境の2つあり、同時に実行されます。上にあったものが、今回問題が発覚した本番環境です。

CircleCI上でリアクトアプリケーションというかSPAをビルドして、S3にアップロードして、キャッシュを削除する流れになっています。このあたりまでは、よく見る構成だと思います。若干古臭いのはちょっと置いといてください。

怪しいのは「ビルドしている」のプロセス

ではなぜあんなことが起こってしまったんだろう。どうしてこうなった、というところに迫ってみたいと思います。

CircleCIでビルドしたSPAをS3にアップロード。ここの“ビルドした”がなんかちょっと怪しいです。ではこの“ビルドしている”というプロセスは、いったいどうなっているのか。

また、本番環境とステージ環境、それぞれAPIの接続先は違う。環境設定情報を変えていますが、ではそれをビルドのところでどのように解決してるのかを次に話します。

ざっくり書くとこんな感じです。1番目が環境依存の設定を.envにコピー。cpでコピーするという、本当に単純なファイルコピーです。そのあとwebpack buildを実行して、3番で1番でコピーした.envを参照して、dotenvというライブラリが、ビルド中にビルド結果、ディストリビューションに埋め込むことをやります。

1番がおかしくなることは常識的にちょっと考えづらい。今回も変更してない。そのため、2番がちょっと怪しいです。では2番のwebpack build、またはそのwebpackの構成、設定はどうなっていたのかを、これから調べてみましょう。

webpack buildの設定

webpack buildの設定ですが、実は、開発用の設定と本番用の設定が、一緒になっていました。ここですでに察する人もいるかもしれません。本番用の設定と一緒になってたんですが、開発効率向上のためにHardSorceWebpackPluginを使ってました。

「どうして開発効率を向上させるために?」と思われるかもしれませんが、ローカルで開発するときにlessのコンパイル、ビルドが走って1回保存すると、ものすごく時間がかかりました。そのため、そのlessのビルドなどの時間を省略するためにHardSorceWebpackPluginを入れていました。

このHardSorceWebpackPluginは、node_modulesの下にキャッシュを生成します。CircleCIのワークフローは一般的なので置いときます。キャッシュパスは、ブランチ名とyam.lockのハッシュ値をパスにしていました。「おや? なんか見えてきたぞ」と。

どこに注目するかは、前回のビルドと今回のビルドを、時系列で整理してみましょう。真ん中に注目してみてください。このCircleCIのキャッシュはブランチ名とyam.lockから生成されるので、yam.lockが変わらなければCircleCIのキャッシュパスは全部一緒です。

CircleCIのキャッシュ保存のほうが遅かったら? CircleCIのキャッシュ取得で使われるビルドキャッシュには、さていったい何が入ってるんでしょうか。問題は環境設定ファイルがビルドキャッシュの中に含まれてしまい、本来設定してほしい内容と、ビルドキャッシュの中身が別になっていた。なので、一時対処として、デプロイフローでnode_modulesのビルドキャッシュを削除したら解消しました。「やったぜ」という感じです。

CircleCIのキャッシュの中身はサボらず確認しよう

今回の学び、大きくわけて3つです。CircleCIのキャッシュは大事。ただ、予期しないタイミングで壊れるので取り扱い注意。今回の場合はビルドキャッシュが環境ごとに変わるという意味で、冪等ではなかったので、そもそも壊れやすい状況だったと。そのため、「ある意味壊れて当然だったんじゃないの」と言われれば、ぐうの音も出ない感じです。

キャッシュしている中身は、ちゃんとサボらず確認しよう。アプリケーションだけでなく、ライブラリをインストールして使うとそれだけですぐ作成されてしまうキャッシュ、すごく見えづらいキャッシュもあります。そこまで含めてちゃんと調べましょうね、というのがあったと思います。

これで最後ですが、開発用の設定とデプロイ用の設定を一緒にするのは悪手。前からやりたいなとは思っていましたが、「今じゃなくていいだろう」と先延ばしにしていった結果、こんな障害を起こしてしまった。「これをやりたい」「あれをやりたい」という吟味はすごく重要ですが、それをしないことによってどんな影響が出てしまい得るのかも含め、やる・やらないは判断しなければいけなかったのではないかという反省です。

最後。公式ページには「分割することを推奨」「Typical Recommended」と書いてあるので、よい子のみなさんはやっていないと思いますが、やらないようにしましょう。webpack5だと、ビルドキャッシュ周りがwebpackの中に入ったりと、けっこう変わってます。そのため、HardSorceWebpackPluginを導入して同じ地雷を踏み抜く方は少なくなったのではないかと思います。

justInCaseではエンジニア募集しているので、興味がある方、エントリーをお願いします。以上です。ありがとうございました。

質疑応答

田仲紘典氏(以下、田仲):はい、ありがとうございました。環境、dotenv周りの話を一緒にすることはそもそもやったことがないですが、やっていてこんなこと起こるんだって思いながら聞いてました。

質問がきています。このケース自体、ステージング・プロダクションの区別をキャッシュに入れるべきなのかという疑問ですが、どうですか。

小佐野:うーん、それは悩ましいですね。冪等にするのであれば、やったほうがよかったのではないか。どちらかというと、キャッシュの中に入っていることを知らなかったこと自体が問題だった、かな?

その環境設定情報はビルドキャッシュというか、CircleCIのキャッシュの中に含まれてしまって、冪等でなくなってしまったことが本質であって、もし入れるなら分割したほうがいい。削除するなら、別に一緒でよかったんじゃないかという印象です。

田仲:なるほど。初期の段階の開発で、そのdotenv自体を、開発環境とステージング環境に分けるとこは考えませんでしたか?そもそもファイルを分けるという話です。

小佐野:いや、分けていました。ただ、それぞれのデプロイフローを分割するにあたり、dotenvとライブラリが、そのプロジェクトの中の.envというファイルを見てリゾルブする流れだったので、ちょっと特殊なデプロイフローになってしまった感じです。

そのため、それぞれ環境ごとの格納しているディレクトリやファイルパスは全部違います。それぞれデプロイするときに引っ張ってくるファイルが全部違う感じ。

田仲:失礼しました。一緒だと思っていました(笑)。

今回たぶんホットフィックスで、超早く開発して、ビルドして、アップしてという感じだったと思います。今後、どういったリリースフローなら、こういったことが絶対起こらないか。環境変数分けるとか、全部分ける以外で何かあったら教えてほしいです。

小佐野:正直そこは模索中なのであえて書きませんでした。デプロイパイプラインとか、リリースパイプラインが共用のCircleCIになってるのは、ちょっともしかしたらよくないのかもと思ったり。

それぞれの環境にリリースするもの、AWSアカウント側にデプロイするものがいてもいいのかなと思ってはいます。ちょっとまだ試行錯誤中というか、どうしたもんかと思っている節はあります。

田仲:なるほど。またどんなふうになったのか教えてください。ちょうど時間になったので終わります。小佐野さん、ありがとうございました。

小佐野:ありがとうございました。