S3から配信する静的WebコンテンツにCache-Controlを設定してキャッシュ対策

Insight EdgeのLead Engineerの日下です。

弊社ではちょっとしたWebアプリを作るときに、AWSを用いたサーバーレスアーキテクチャで

フロントエンド CloudFront + S3 + SPA(React等)
バックエンド API Gateway + Lambda

という構成をしばしば使います。

今回は、この構成においてありがちなキャッシュによるバージョン不整合の対策について紹介します。

SPAにおけるキャッシュ問題

上記の構成は安価かつスケーラブルにSPAを運用できることが魅力ですが、 フロントエンドの静的ファイルに対してブラウザのキャッシュやCloudFrontのエッジキャッシュが働いてしまい、 アプリの更新がうまく反映されなかったり、フロントエンドとバックエンドのバージョン不整合の原因になることがあります。

jsやcssファイルにはハッシュ値が付与されるものの、呼び出し元であるindex.htmlにキャッシュが効いてしまうとjsやcssも古いファイルが参照されるため、 それによってキャッシュされたものが呼び出されたり、リンク切れが発生したりしてしまいます。
デプロイのたびにCloudFrontのキャッシュを削除(Invalidation)してもブラウザのキャッシュには対策できないため ユーザー側でフルリロードしなければ最新化されずバージョン不整合などの原因になります。

アプリの処理としてバージョン整合性チェックを実装する方法もありますが、今回はお手軽な方法として、 index.htmlファイルにCache-Controlを設定してキャッシュを無効化する方法を紹介します。

S3のオブジェクトメタデータにキャッシュ動作を設定する

CloudFront + S3 で配信するファイルは、S3のオブジェクトメタデータにCache-Control等を設定することでHTTPヘッダに反映できます。
マネジメントコンソールで確認・設定する場合、index.htmlのファイルの詳細画面にてプロパティタブのメタデータの項目に設定します。

S3のオブジェクトメタデータ設定

デプロイ時にAPIで設定

自動デプロイに組み込むためにAWS CLIでもやってみます。
CLIにはオブジェクトメタデータのみを編集するコマンドが無いため、以下のように aws s3api copy-object を使用してメタデータを編集します。 その際、Content-Typeなど他のメタデータも明示的に指定しないと失われてしまうため、--content-typeオプション等で指定しています。

# 既存のオブジェクトにメタデータを設定
aws s3api copy-object --bucket your-web-contents-bucket --key index.html --copy-source your-web-contents-bucket/index.html --metadata-directive REPLACE  --cache-control "no-cache, no-store" --content-type "text/html"

このコマンドは既存オブジェクトのメタデータを編集できますが、 aws s3 sync 等でファイルをアップロードした後にメタデータ編集を実行する手順だと、そのタイムラグの間にキャッシュ設定未反映のファイルがユーザに配信されてしまう可能性があります。 アップロードと同時にキャッシュ設定を反映させるには、先にindex.html以外を aws s3 sync で更新し、 最後にindex.htmlを aws s3 cp コマンドでメタデータ設定とともにアップロードすると良いでしょう。

# index.html以外をアップロード
aws s3 sync ./build s3://your-web-contents-bucket --exclude index.html --delete
# index.htmlをメタデータ設定とともにアップロード
aws s3 cp ./build/index.html s3://your-web-contents-bucket/index.html --metadata-directive REPLACE --cache-control "no-cache, no-store" --content-type "text/html"

CloudFront経由で配信されたindex.htmlのヘッダをブラウザの開発者ツールで確認してみます。
ちゃんと設定されていますね。

レスポンスヘッダ