0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

#ALBはHealthyなのにReactが真っ白になった話 ~Vite assetsとNginxキャッシュで詰まった~

0
Posted at

はじめに

個人開発者向けに、AWS上へWebアプリを公開するための販売基盤を作っています。

構成は、ざっくりこんな感じです。

本番環境へフロントを反映したあと、ALBのTarget Healthを確認するとhealthy

/へアクセスしてもHTTP 200。

「よし、問題なさそうだな!」

この時点では「勝ったな、風呂入ってくる」状態でした。

ブラウザを開くと、そこに表示されたのは販売サイトではなく、真っ白な画面でした。

確認対象 状態
ALB Target Health healthy
/ HTTP 200
React 真っ白

「ALBさん、Healthyって言いましたよね?」

今回は、ALBもNginxも動いているのに、Reactだけが起動できなかった障害について書きます。


今回の構成

販売サイトはReactとViteで作成し、ビルド成果物をEC2上のNginxから配信しています。

EC2上の配信ディレクトリは次のようになっています。

/usr/share/nginx/html/
├── index.html
├── runtime-config.json
└── assets/
    ├── main-xxxxx.js
    └── index-xxxxx.css

index.htmlだけでReactが動くわけではありません。

ブラウザはindex.htmlを取得したあと、そこに記載されたJavaScriptやCSSを追加で取得し、Reactアプリを起動します。


起きたこと

本番反映後、販売サイトのTOPページが白画面になりました。

一方、AWSとサーバー側では次のように見えていました。

確認項目 状態
ALB Target Health healthy
/のHTTP Status 200
Nginx 起動中
EC2 稼働中
ブラウザ表示 真っ白

最初は、

「ALBもEC2もNginxも生きているのに、なぜ画面だけ死んでいるんだ?」

という状態でした。


ALBは何を見てHealthyと判断していたのか

今回のALB Target Groupでは、次のヘルスチェックを設定しています。

設定項目
Protocol HTTP
Port 80
Path /
Success codes 200

かなり簡略化すると、ALBが確認していたのはここです。

今回も/は正常にHTTP 200を返していました。

そのため、ALBのhealthy判定自体は間違っていません。

ただし、ALBはブラウザ上でReactが起動できたかまでは確認していません。

確認対象 意味
ALB Healthy ALBから指定パスへ到達し、期待したStatus Codeが返る
React画面が正常 HTML、JavaScript、CSS、実行時設定がすべて読み込める

この2つは別の話でした。


Nginxのアクセスログに答えがあった

Nginxのaccess.logを確認したところ、見覚えのない404がありました。

GET /assets/main-Che1Uowk.js 404

/は200ですが、JavaScriptは404です。

リクエスト HTTP Status
/ 200
/assets/main-Che1Uowk.js 404

この時点で、ようやく状況がつながりました。

ALBもNginxも動いていました。

Reactを起動するためのJavaScriptだけが、サーバー上から消えていたのです。


原因はViteのハッシュ付きassetsと全削除型デプロイ

Viteで本番ビルドすると、JavaScriptやCSSには内容に応じたハッシュ付きファイル名が付きます。

例えば、旧ビルドと新ビルドでは次のようになります。

旧ビルド:
index.html
assets/main-Che1Uowk.js

新ビルド:
index.html
assets/main-NewHash.js

JavaScriptの内容が変わると、ファイル名も変わります。

今回のデプロイ処理では、新しいfront.zipを展開する前に、Nginxの配信ディレクトリを全削除していました。

rm -rf /usr/share/nginx/html/*
cp -a /tmp/sales-site-front/. /usr/share/nginx/html/

そのため、新しい成果物を配置した時点で、旧main-Che1Uowk.jsはサーバー上から消えました。

一方、一部のブラウザには、適切なCache-Controlが設定されていなかった旧index.htmlが残っていました。

つまり、ブラウザとサーバーで世代がずれていました。

場所 保持していた世代
ブラウザ index.html
サーバー index.htmlと新assets
サーバー上の旧JavaScript 削除済み

スーパーで例えるなら、

  1. 入口では「営業中」と言われた
  2. 少し古い店内マップを渡された
  3. マップに書かれた棚へ行った
  4. その棚の商品は撤去済みだった

という状態です。

ALBは入口を見て「営業中」と判断しました。

しかし、ブラウザが必要としていたJavaScriptという商品は、すでに棚から消えていました。


S3バージョニングから旧JavaScriptを救出

フロントの配布用front.zipは、バージョニングを有効にしたS3バケットで管理していました。

そこで、直前バージョンのfront.zipを取得し、その中から旧JavaScriptを取り出しました。

復旧後は、新旧両方のJavaScriptがHTTP 200を返す状態にしました。

JavaScript HTTP Status
/assets/main-NewHash.js 200
/assets/main-Che1Uowk.js 200

これにより、ブラウザが持っているHTMLの世代にかかわらず、必要なJavaScriptを取得できます。

旧ビルド全体へロールバックしたわけではありません。

現行機能を残したまま、旧HTMLとの互換性を一時的に持たせる復旧方法を取りました。

S3バージョニングを有効にしていた過去の自分には、ちょっと感謝しました。


恒久対応としてキャッシュ方針を変更

今回の根本原因は、HTMLとassetsを同じ感覚で扱っていたことでした。

それぞれ役割が異なるため、キャッシュ方針も分ける必要があります。

index.htmlとclean URL

index.htmlは、どのJavaScriptやCSSを読み込むかを指定する入口です。

古いHTMLがブラウザに残り続けると、削除済みのassetを参照する可能性があります。

そのため、HTMLとclean URLには次のHeaderを設定しました。

Cache-Control: no-cache, no-store, must-revalidate
Pragma: no-cache
Expires: 0

runtime-config.json

runtime-config.jsonには、実行時に利用するAPI Endpointを設定しています。

古い設定が残ると、すでに使っていないAPIへ接続する可能性があります。

Cache-Control: no-store

assets配下のJavaScriptとCSS

一方、Viteのassetsはハッシュ付きファイル名です。

内容が変わるとURLも変わるため、同じURLの内容は変わらない前提で扱えます。

そのため、長期キャッシュを設定しました。

Cache-Control: public, max-age=31536000, immutable

整理すると次のようになります。

対象 キャッシュ方針 理由
index.html キャッシュさせない 参照するassetが変わる
clean URL キャッシュさせない 実体としてHTMLを返す
runtime-config.json キャッシュさせない API設定が変わる
assets/*.js 長期キャッシュ 内容変更時にハッシュとURLが変わる
assets/*.css 長期キャッシュ 内容変更時にハッシュとURLが変わる

実際のNginx設定は次のようにしました。

location = /runtime-config.json {
    add_header Cache-Control "no-store";
    try_files $uri =404;
}

location /assets/ {
    add_header Cache-Control "public, max-age=31536000, immutable" always;
    try_files $uri =404;
}

location / {
    add_header Cache-Control "no-cache, no-store, must-revalidate" always;
    add_header Pragma "no-cache" always;
    add_header Expires "0" always;

    try_files $uri $uri.html $uri/ =404;
}

キャッシュの役割分担は次のイメージです。

HTMLと実行時設定は新しいものを確認し、ハッシュ付きassetsは長期間利用する方針です。


Terraformへ反映してEC2を更新

Nginxの設定を手作業で直すだけでは、EC2が再作成されたときに元へ戻ってしまいます。

そこで、キャッシュ設定をTerraformで管理しているuser_dataへ反映しました。

その後、環境ごとに次の対応を行っています。

Prdでは、更新後に次の状態を確認しました。

確認項目 状態
ASG Instance InService
ALB Target Health healthy
Nginx active
現行JavaScript HTTP 200
旧JavaScript HTTP 200

最後にterraform planを実行し、意図しない差分が残っていないことも確認しました。


ALB HealthyはE2Eテストではない

今回、一番大きかった学びです。

確認方法 確認できる範囲
ALB Target Health ALBからEC2の指定パスまで
HTTP 200確認 対象ファイルのHTTP応答
ブラウザ表示確認 HTML、assets、実行時設定、Reactの動作
E2Eテスト ユーザーが利用する一連の操作

ALBのTarget Healthは、ALBからEC2へアクセスし、設定したパスが期待したStatus Codeを返すかを確認しています。

一方、React/ViteのようなSPAでは、その後にブラウザが行う処理も成功する必要があります。

/が200でも、JavaScriptが404ならReactは起動しません。

インフラのヘルスチェックが正常でも、ユーザー視点のE2Eが成功しているとは限らないことを、白い画面から教わりました。


次に白画面が起きたら、この順番で確認する

今回の経験を踏まえて、次回は次の順番で確認します。

  1. ブラウザのDevToolsを開く
  2. ConsoleでJavaScriptエラーを確認する
  3. NetworkでJS、CSS、runtime-config.jsonの404を確認する
  4. Nginxのaccess.logで実際のリクエストを確認する
  5. Nginxのerror.logで配信エラーを確認する
  6. index.htmlが参照しているasset名を確認する
  7. EC2上に対象assetが存在するか確認する
  8. S3上のfront.zipとEC2上の成果物を比較する
  9. ALB Target Healthを確認する
  10. Terraformの差分を確認する

今回の決め手は、次の組み合わせでした。

リクエスト HTTP Status
/ 200
/assets/main-Che1Uowk.js 404

この状態なら、ALBやNginx全体が停止しているのではなく、HTMLとassetsの世代不一致を疑えます。

正直、最初にブラウザのNetworkタブを見れば、もう少し早く気づけました。

次からはちゃんと最初に見ます。たぶん。


まとめ

本番反映後、ALBのTarget Healthはhealthy/もHTTP 200なのに、Reactの画面だけが真っ白になる障害が発生しました。

原因は、ブラウザに残っていた旧index.htmlが、デプロイ時に削除した旧JavaScriptを参照していたことです。

復旧では、S3バージョニングから旧JavaScriptを取り出し、現行成果物へ互換用assetとして追加しました。

恒久対応として、キャッシュ方針も分離しました。

対象 キャッシュ方針
index.html / clean URL no-cache, no-store
runtime-config.json no-store
assets/*.js / assets/*.css public, max-age=31536000, immutable

ALBは嘘をついていませんでした。

私が確認していた「正常」の範囲が、ユーザー画面まで届いていなかっただけです。

ALBくん、君は何も悪くない。

今回の件で、ブラウザDevTools、Nginx access log、S3バージョニングのありがたさをかなり実感しました。

同じように「ALBはHealthyなのに画面が出ない!」となった方の参考になれば幸いです。


おまけ

この販売基盤は、個人開発者向けにAWS公開環境をTerraformで作れるようにする「ヒトリ開発」の一部として作っています。

アプリは作れるけど、AWS公開、ALB、HTTPS、SSM、CloudWatchあたりで詰まりやすい方に向けて、少しずつ整備しています。

今回のように失敗することもありますが、失敗した内容まで含めて設計と手順へ反映しています。

失敗しない基盤ではなく、失敗しても戻せる基盤を目指しています。

少しでも興味を持ってくれた方がいれば、サイトのぞいてみてください!
白い画面はもう見れませんが...w

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?