静的フロントエンドのデプロイが思ったより簡単な理由
React、Vue、Angularでアプリを開発したとしよう。ローカルマシンでは問題なくコンパイルできる。npm run build を実行すれば dist フォルダが生成され、HTML、CSS、JavaScriptファイルが出力される。あとはこれらのファイルを実際のユーザーに届けるだけだ。フォルダをアップロードするだけでしょ?
実際は見た目ほど簡単ではない。初めて静的フロントエンドを本番環境にデプロイすると、ほぼ確実にページが壊れる。スタイルシートが半分しか読み込まれなかったり、「ちょっとしたアップデート」の後に何も動かないとユーザーからクレームが来たりする。問題はビルドではない。ビルドが完了した後に何が起こるかが問題なのだ。
誰も警告してくれないキャッシュ問題
ブラウザは静的ファイルを積極的にキャッシュする。これはパフォーマンス上は素晴らしい。リピーターは style.css や app.js がローカルに保存されているため、サイトの読み込みが速くなる。しかし、新しいバージョンをデプロイしても、ブラウザはファイルが変更されたことを認識しない。古い style.css を新しいHTMLと一緒にそのまま使い続ける。結果はページ崩れだ。古いファイルに存在しない新しいCSSクラス、古いバンドルにない関数を呼び出す新しいJavaScript——そういう問題が発生する。
ユーザーにキャッシュをクリアしてもらうよう頼むのは現実的ではない。それはデプロイ戦略ではない。
アセットハッシング:すべてを解決するたったひとつのテクニック
解決策はシンプルで広く使われている。すべてのファイル名にコンテンツハッシュを付与することだ。style.css の代わりに style.a1b2c3.css を生成する。ハッシュはファイルの内容が変わったときだけ変化する。CSSルールを更新するとハッシュが変わり、ファイル名が変わり、ブラウザはそれをまったく新しいファイルとして扱う。古いファイルはサーバー上に残るが使われず、古いURLを保持しているユーザーがアクセスできる状態は維持される。
このテクニックはイミュータブルデプロイと呼ばれる。各バージョンのファイルは一意であり、上書きされることはない。style.css を置き換えるのではなく、style.a1b2c3.css を追加し、ユーザーがページをリフレッシュするにつれて古いファイルは自然に使われなくなる。
最近のフレームワークのほとんどはハッシュ処理を自動で行う。React、Vue、Angular、Svelteはすべてプロダクションビルドでハッシュ付きのファイル名を生成する。ビルド設定でこれを無効にしないように注意するだけでよい。
パイプラインを段階的に構築する
静的フロントエンドのパイプラインには4つのステージがある。ビルド、アップロード、切り替え、検証だ。各ステージには固有の役割とリスクが存在する。
次のフローチャートは4つのステージとキャッシュ無効化の判断を示している。
以下は4つのステージをまとめた最小限のbashスクリプトだ。
#!/bin/bash
set -e # エラー発生時に停止
# 1. ハッシュ付きでビルド
npm run build
# 2. 上書きせずにアップロード(AWS S3の例)
aws s3 cp dist/ s3://my-bucket/ --recursive --no-overwrite
# 3. 参照ポイントを切り替え(シンボリックリンクの更新またはindex.htmlのコピー)
aws s3 cp dist/index.html s3://my-bucket/current/index.html
# 4. エントリポイントのみキャッシュを無効化
aws cloudfront create-invalidation --distribution-id ABC123 --paths "/index.html"
echo "デプロイ完了。"
my-bucket と ABC123 は実際のバケット名とCloudFrontディストリビューションIDに置き換えること。--no-overwrite フラグにより、古いハッシュ付きアセットが上書きされることは決してない。
1. ハッシュ付きでビルド
パイプラインはフレームワークのビルドコマンドを実行する。ほとんどのプロジェクトでは npm run build または yarn build だ。出力は dist または build というフォルダに格納される。そのフォルダ内のすべてのファイルはハッシュ付きの名前を持つ。
ビルドが失敗した場合、パイプラインは停止しなければならない。壊れたビルドがデプロイされることは絶対に避けるべきだ。これは当然のことのように思えるが、多くのチームは急いでいるときにこのチェックをスキップする。スキップしてはいけない。失敗したビルドが何らかの理由でデプロイされると、すべてのユーザーにとってサイトが完全に壊れてしまう。
2. 上書きせずにアップロード
ファイルを保存する場所が必要だ。一般的な選択肢は2つある。
- ストレージバケット(Amazon S3やGoogle Cloud Storageなど)。安価で信頼性が高く、低〜中程度のトラフィックに適している。
- 直接アップロード可能なCDN(Cloudflare Pages、Netlify、Vercelなど)。より高価だが、ファイルがグローバルに配信され、読み込みが高速になる。
どちらを選んでも、既存のファイルを上書きしてはいけない。新しいファイルは古いファイルと一緒にアップロードする。すべてのファイルが一意の名前を持つため、競合は発生しない。古い style.a1b2c3.css と新しい style.d4e5f6.css は同じバケット内で問題なく共存できる。
ここでのリスクは部分的なアップロードだ。パイプラインがHTMLファイルを最初にアップロードし、次にCSS、その後にJavaScriptをアップロードする場合、HTMLのアップロードとCSSのアップロードの間にページを読み込んだユーザーは壊れたサイトを目にすることになる。HTMLが参照する新しいCSSファイルがまだサーバー上に存在しないからだ。
これを避けるには、まずすべてのファイルをアップロードし、すべてのファイルが存在することが確認できてから参照ポイントを切り替える。
3. 参照ポイントを切り替える
最後のステップはエントリポイントの更新だ。静的サイトの場合、エントリポイントは通常、メインのHTMLファイルか、最新バージョンを指すCDN設定である。これは新しいファイルがすべてアップロードされた後にのみ実行する。
バージョン管理されたフォルダ構造(v1/、v2/、v3/)を使うチームもある。デプロイのたびに新しいフォルダが作成される。CDNやWebサーバーは最新のフォルダを指す。このアプローチではロールバックが非常に簡単になる。前のフォルダを指し示すだけでよい。
4. キャッシュを無効化する(ただしエントリポイントのみ)
ハッシュ付きファイル名を使っている場合、個々のアセットに対してCDNキャッシュを無効化する必要はない。各アセットは新しいURLを持つため、CDNはそれを新しいファイルとして扱う。キャッシュ無効化が必要な唯一のファイルはメインのHTMLファイルだ。その名前は通常変更されないからだ。
index.html またはエントリポイントのキャッシュを無効化する。これによりCDNは新しいHTMLを取得し、そのHTMLが新しいハッシュ付きアセットを参照する。他のすべては自動的に解決される。
初めての静的パイプラインのための実践的チェックリスト
今日、静的フロントエンドのパイプラインをセットアップするなら、以下のリストを確認しよう。
- ビルドがハッシュ付きファイル名を生成していること(出力フォルダで確認)
- パイプラインがビルド失敗時に停止すること(部分的なデプロイを防ぐ)
- アップロードが新しいファイルを作成し、古いファイルを決して上書きしないこと
- エントリポイント(HTMLまたはCDN設定)がすべてのファイルのアップロード後にのみ更新されること
- キャッシュ無効化がエントリポイントのみを対象とし、個々のアセットは対象としないこと
- ロールバック計画が存在すること:少なくとも1つ前のバージョンにアクセス可能であること
これが思ったより重要な理由
静的フロントエンドのデプロイは簡単に見える。フォルダをアップロードするだけ、完了。しかし、スムーズなデプロイと壊れたサイトの違いは、多くの場合、たったひとつの詳細にある。コンテンツが変わるとファイル名が変わること。このたったひとつのテクニックがキャッシュ問題を排除し、部分的なアップロードの災害を防ぎ、ポインタを切り替えるだけでロールバックを可能にする。
パイプライン自体は複雑ではない。ビルド、ハッシュ、アップロード、切り替え。しかし、各ステップには無視すると痛い目を見る障害モードが存在する。すり抜けるビルド失敗、ライブファイルを上書きするアップロード、古いHTMLを配信するキャッシュ——これらは理論上の問題ではない。毎日本番環境で発生している。
まずは基本をしっかり押さえよう。堅牢な静的パイプラインは、サーバーサイドレンダリングアプリ、マイクロフロントエンド、フルスタックデプロイなど、より複雑なものの基盤となる。静的ファイルのフォルダを確実にデプロイできなければ、より難しいことに取り組むのは難しいだろう。
シンプルなケースから始めよう。それをマスターし、その先に進むのだ。