駅奪取チームの
id:konakawa です。
以前駅奪取で、デプロイ戦略に起因して、特定ケースにおいてサーバのファイルのタイムスタンプが巻き戻ってしまうことがありました。 これにより、キャッシュバスティングをすり抜けて古いキャッシュが混ざってしまい、不具合の原因となってしまいました。
本記事では、この問題について概説し、とった対応を含めて紹介します。 同じようなインフラ構成のサービスの役に立てば幸いです。
問題概要
デプロイ戦略について
駅奪取では、最新のファイルが以下の2通りの経路で各 EC2 インスタンスへ届けられる仕組みとなっています。
- デプロイ時: デプロイ用のサーバーから、rsync でファイル一式を配布
- メタデータも同期される
- オートスケール時: AMI から起動したインスタンスが git pull を実行
- Git は内容が同一のファイルは更新しないため、その場合はメタデータが更新されない
これにより、後述するような特定ケースにおいてはファイルのメタデータの異なるインスタンスが混在するような構成になっていました。
問題となる時系列
本プロダクトで、あるときのフロントビルド結果の変遷として以下のようなものがありました。
- ある時点でのビルド結果(以下 A とする)が存在する
- 変更が入ったビルド結果(以下 B とする)がデプロイされる
- 上記変更を revert したビルド結果(以下 A' とする)がデプロイされる

単に変更とそのリバートを順に反映しており、ありふれた経緯かと思います。 この経緯では A と A' は、内容は同一ですがファイルのタイムスタンプが異なるものになります。
以上の手順を踏んだとき、インスタンスが持つビルド結果は
- デプロイ時点で起動していたインスタンス: A'
- オートスケールで上がってきたインスタンス: AMI の持つビルド結果が A の場合、更新されず A のまま
という状態が混在することになります。
キャッシュ
このプロダクトでは、キャッシュバスティングのために、静的ファイルの URL をそのファイルのタイムスタンプ(mtime)を元に生成します。 すなわち、ファイルの更新があった際は URL が更新されるため、古いキャッシュを掴み続けることがない仕組みです。
しかし前述のように、オートスケールで起動したインスタンスでは、ファイルが A 時点のタイムスタンプを持ったままになります。 そうしたインスタンスから返される HTML には、 A のタイムスタンプを元にした静的ファイルへの URL が埋め込まれます。
CDN やブラウザのキャッシュは、このタイムスタンプ情報を含む URL をキーに紐づきます。 ここで、例えば以下のようなシナリオで、古い A の URL に新しい B の内容をキャッシュすることがあります。
- A のタイムスタンプを使った URL へのリクエストが発生し、そのキャッシュの TTL が切れていればオリジンへリクエストを行う。 このとき B が最新である期間であれば、オリジンから B のファイルが返され、A の URL に B の内容がキャッシュされる状態になる。
- ブラウザでキャッシュを掴んでいる場合、If-Modified-Since によりキャッシュの更新確認を行う。 キャッシュのタイムスタンプとオリジンのファイルのタイムスタンプを比較して、後者の方が新しければオリジンのファイルを使うが、 オリジンでも巻き戻りが起こっていれば、ブラウザで掴んだキャッシュを使用することになる。
この状態でパスに用いているタイムスタンプの巻き戻りが起きると、キャッシュバスティングが正しく機能しなくなります。 すなわち、最新のファイルの内容が A' であるにも関わらず、古い B の内容が使われてしまいます。
この結果、ビルド結果に A(') のものと B のものが混在することになり、不具合が発生する原因となってしまいます。
対策
FSx 化やあるいはイミュータブルインフラ化など、インフラ側の仕組みを変えることで、今回の mtime 巻き戻りのような状態の不一致が起きなくなります。 これらは本件以外にも有効ですが、比較的重い対応になります。 これらを導入するまでビルドの反映をリバートできないというのは不便なため、少なくとも本件の対応としてはこれらは見送りました。
オートスケールで上がってくるインスタンスが最新の情報を取得する方法を git pull から rsync に揃えるというのも1つですが、rsync で配る元のサーバが生きていることに依存してしまうため、これも避けたいです。
そこで、ファイルの名前自体に内容ハッシュを付加する対応をしました。 これであれば非常に軽量に入れられ、かつ前述したところの A と B はファイル自体が違うため、キャッシュが混じることもなくなります。
まとめ
- インスタンスの状態を更新するのに git pull を用いていたが、ファイルの内容が変わらない場合はメタデータが更新されない
- タイムスタンプの巻き戻りが発生することで、キャッシュバスティングがあっても古いキャッシュが混じる
- パスやクエリパラメータでなく、ファイル名レベルで変更を反映することで解決できた
ここまでご覧いただきありがとうございます。 本記事が似た問題に遭遇した方などの参考になれば幸いです。
モバファクでは中途採用・新卒採用ともに絶賛募集中です。
会社の情報については、モバファクブログでも発信しています。
技術好きな方、モバファクにご興味をお持ちの方は、ぜひご応募ください!
・モバファクブログ:https://corpcomn.mobilefactory.jp/
・採用サイト:https://recruit.mobilefactory.jp/