Django と Google Cloud Storage で Cache Busting

Kunihiko Kido
VELTRA Engineering
Published in
8 min readNov 12, 2019
Photo by Kiarash Mansouri on Unsplash

Django 標準の Cache Busting を実現する仕組み。こんな便利な機能があるの知らなかったので、そのまとめです。

Cache Busting って何?

Web アプリケーションで提供している CSS や JS、画像ファイルなどの静的なファイルはブラウザに読み込まれると、ブラウザ側でキャッシュされるように提供します。同じファイルはサーバーへアクセスするよりもブラウザにキャッシュされているものを利用する方が、ページの表示も早くなるからです。

しかし、CSS を修正して公開したにもかかわらず、ブラウザにキャッシュされた古い CSS が使われているため、いっこうに新しくなりません。困りますよね?

Cache Busting とは、明示的に新しい静的ファイルをブラウザに読み込ませるための仕組みです。

Cache Busting を実現する2つの方法

Cache Busting を実現するための方法には以下の2つがあります。

  • ファイル名を変更する
  • クエリーパラメータをつける

ファイル名を変更する場合は、 css/base.css を css/base.7e51483be17a.css のように変更して提供する方法です。

クエリーパラメータをつける方法は、 css/base.css?v=7e51483be17a のようにバージョン番号などを付与して提供する方法です。

どちらが良いのでしょうか?

クエリーパラメータで指定する方法の場合、パラメータは任意の文字列で自由に指定できてしまうため、ページ A と ページ Bでは、違うパラメータが指定されていてもエラーは表示されません。個人的には存在しないファイルが設定されていればエラー(404など)になるファイル名を変更して提供する方が好みです。

Django で提供されている Cache Busting

Django で提供されている Cache Busting の仕組みは、ファイル名を変更する方法です。

それでは、使い方を見ていきましょう。

ハッシュ値付きのファイルを作成する

はじめに、manage.py collectstatic コマンドを使って、事前にハッシュ値付きの静的なファイルを事前に生成します。

ハッシュコード付きのファイルを自動生成するには、 STATICFILES_STORAGE の設定を以下のように変更してから manage.py collectstatic コマンドを実行します。

# settings.pySTATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'

collectstatic コマンドで集められたファイルを見てみると、オリジナルのファイルとハッシュ値付きのファイルが確認できます。

base.css
base.7e51483be17a.css

なお、ファイルの内容が変更されていなければ、ハッシュ値も変わりません。

また、ハッシュ値付きのファイルを覗くと、ファイル内で他のファイルを参照している記述もハッシュ値付きのファイル名に変換されています。

テンプレートの書き方

テンプレートでハッシュ値付きのファイル名に変換するには、必ず static タグを使って、オリジナルのファイル名を指定するようにします。

<link rel="stylesheet" href="{% static "css/base.css" %}">

このように記述して、期待するハッシュ値付きのファイル名でレスポンスされるにはいくつか条件があります。

<link rel="stylesheet" href="static/css/base.7e51483be17a.css">
  • DEBUG が False であること
  • STATICFILES_STORAGE の設定が django.contrib.staticfiles.storage.ManifestStaticFilesStorage になっていこと
  • collectstatic コマンドを使って、静的ファイルが集められていること(staticfile.json ファイルが作成されていること)

ハッシュ値付きファイル名レンダリングの仕組み

Django は、テンプレートに記述されたオリジナルファイル名をどうやってハッシュ値付きのファイル名に変換しているのかと言うと、 collectstatic コマンドを実行した際に作成された staticfiles.json ファイルを参照して、変換しています。

staticfiles.json には、オリジナルのファイル名とハッシュ値付きのファイル名がペアになって管理されています。

デフォルトでは、 STATIC_ROOT に作成されます。

static/staticfiles.json

アプリケーションと静的ファイルを同じサーバーで提供するようなアーキテクチャーの場合、ここまでの知識があれば、Django で Cache Busting もすぐに提供可能です。

Google Cloud Storage で静的ファイルをホスティング

django-storage パッケージを使う前提で話を進めます。django-storage パッケージは、静的ファイルの保存先を S3 や Google Cloud Storage など外部のストレージサービスを利用する際に使うパッケージです。

まずは、settings.py から、以下の設定では、DEFAULT_FILE_STORAGE には GoogleCloudStorage を指定し、STATICFILES_STORAGE には、ManifestStaticFilesStorage を指定しています。

# settings.pyDEFAULT_FILE_STORAGE = 'storages.backends.gcloud.GoogleCloudStorage'
STATICFILES_STORAGE = 'mysite.storage.ManifestStaticFilesStorage'

STATICFILES_STORAGE には、GoogleCloudStorage を拡張した ManifestGoogleCloudStorage クラスを作って指定する情報も見かけましたが、この方法だとSTATIC_URL の値は使われずに、静的ファイルの URL が決定してしまうため、意図した動作が確認できませんでした。

そのため、STATIC_URL を使って、URLを組み立ててくれるManifestStaticFilesStorage を拡張して STATICFILES_STORAGE に設定しました。

# storage.pyimport osfrom django.conf import settings
from django.contrib.staticfiles.storage import \
ManifestStaticFilesStorage as BaseManifestStaticFilesStorage
class ManifestStaticFilesStorage(BaseManifestStaticFilesStorage):
def read_manifest(self):
"""
Looks up staticfiles.json in Project directory
"""
manifest_location = os.path.abspath(
os.path.join(settings.BASE_DIR, self.manifest_name)
)
try:
with open(manifest_location) as manifest:
return manifest.read().decode()
except FileNotFoundError:
return None

※ 上記のコードは Django プロジェクト直下に staticfiles.json が配置されていることを前提にしています(デフォルトは、STATIC_ROOT 直下を参照するようです)

検証した環境は少し特殊で、media 系と static 系 の Google Cloud Storage のバケットは別々で管理されています。同じなら一般の情報のように GoogleCloudStorage を拡張する方法でも良さそうです(未検証)。

Google Cloud Storage 固有の設定はこれくらいで、後は基本に戻って

  1. manage.py collectstatic コマンドを使ってハッシュ値付きのファイルと staticfiles.json の作成
  2. 作成した静的ファイルを gsutil コマンドを使って、Google Cloud Storage へ配置
  3. DEBUG=False になってますか?
  4. テンプレート内の静的ファイルの指定は、static タグ使ってますか?

あたりに気をつければ OK です。

まとめ

静的ファイルのキャッシュ期間は長くしたい。できれば1年くらい。Django なら標準の Cache Busting の仕組みを使って簡単・確実に実現できる。あとは、staticfiles.json をバージョン管理に含めるか?デプロイワークフローで作成するように構成するのか?悩ましいところ。

--

--