GCP の Cloud Functions を利用して Fastly で高速パージ
Fastly サービスのバックエンドとして Google の Cloud Storage などの静的オブジェクトストアが使用されることがよくあります。このようなストレージバケットに保存されたコンテンツは頻繁に変更されないため、通常 Fastly で長期間キャッシュできます。これはパフォーマンス上、大きなメリットがありますが、変更があった場合、更新されたデータが迅速にエンドユーザーに表示されるようにするには、どうすればよいでしょうか?
これは、頻繁に変更されないコンテンツに関してよく問われる問題です。一般的な解決策として、CDN で非常に短いキャッシュ TTL を設定したり、手動でキャッシュ全体をパージすることが挙げられます。しかしこれらの方法では、Web サイトに変更を加えた場合に、CDN のキャッシュが最新のコンテンツで更新されるまで数時間、または数日間にわたって古いコンテンツがユーザーに表示される可能性があります。
幸い、Fastly ならインスタントパージ機能を使用して、キャッシュ全体、各ファイル、サロゲートキーと呼ばれる共通のタグで特定されるファイルのグループを瞬時にパージできます。また、Google の Cloud Storage は、ステートレスロジックをオンデマンドで実行する GCP のプロダクトの一つ、Cloud Functions をトリガーするイベントを提供します。両社の機能を組み合わせることで、オブジェクトのキャッシュ TTL が非常に長くても、Fastly のエッジクラウドプラットフォームで選択的に瞬時にパージし、更新されたコンテンツをすぐにユーザーに表示することが可能になります。
この記事では、これらの二つの機能を接続する方法を解説し、コンテンツが更新されると Fastly で古いコンテンツがパージされるよう GCS バックエンドを設定する方法をご紹介します。
以下のステップに従うことで、Google サイドで料金が発生する可能性があることにご注意ください。
主なしくみ
理想的な流れ
ユーザーが Web サイトからリソースをリクエストします。すると、Fastly が GCS バックエンドからリソースを取得し、エッジで長期間キャッシュします (通常1年)。
リソースの新しいバージョンが GCS にアップロードされ、リソースが更新されたとします。
GCS イベントがトリガーされ、Cloud Function が実行されます。
Cloud Function によって Fastly API にリクエストが送信され、Fastly にオブジェクトを無効化するよう指示します。
別のユーザーが同じリソースをリクエストしたとします。Fastly はキャッシュにあるオブジェクトを無視し、GCS からリソースを再度取得します。
ちなみに、上記のステップ4におけるパージリクエストの時点では、通常 Fastly のキャッシュからリソースは削除されず、代わりに失効済みとしてマークされます。これは、stale-while-revalidate や stale-if-error などのキャッシュディレクティブを使用したいお客様にとって便利です。そうでない場合でも、オブジェクトを失効済みとしてマークするのは、削除するのと同じことです。
Fastly の使用開始に役立つドキュメント
ここでは、すでに Fastly サービスを運用し、ファイルが保存されている GCS バケットを含むプロジェクトが Google Cloud アカウントに存在することを想定しています。またバケットは以下の条件を満たす必要があります。
Fastly サービスのバックエンドとして設定されている
両社の機能を接続するには、Cloud Functions API を有効にする必要があります。システムに Cloud SDK をインストールする場合は、gsutil
の CLI コマンドを実行できるよう、最新のものを使用してください。ただし、GCP コンソールでボタンをクリックすることでもこれが可能です。
また、GCS のビルトインキャッシュ機能をあらかじめ無効にすることも重要です。オブジェクトをバケットにアップロードする際に以下のようにオブジェクトメタデータが設定されるようにすることでこれが可能です。
gsutil -m \
-h "Cache-Control: public, max-age=0" \
cp -r ~/[PATH_TO_CONTENT]/* gs://[BUCKET_NAME]
さらに setmeta コマンドを使って、アップロード済みのオブジェクトに関連付けられたオブジェクトメタデータを更新することもできます。
このチュートリアルのためにバケットを設定しようとしている場合は、ここで一端手を止めて、HTTP 経由でサンプルファイルにアクセスできるかテストしてみましょう。
curl -i "https://[BUCKET_NAME].storage.googleapis.com/[FILE_PATH]"
レスポンスの HTTP ヘッダーがアップロードしたファイルのコンテンツと一緒に表示されるはずです。特に content-type
と cache-control
に注目し、正しくない場合は上記で説明したようにオブジェクトメタデータを使用して修正します。403 Forbidden のレスポンス
が表示された場合は、バケットが一般にアクセス可能かご確認ください。
Fastly で一定の TTL を設定する
Fastly でキャッシュされたオブジェクトはすべて変更されると無効になるため、すべてのオブジェクトのキャッシュに同じ TTL を適用し、かなり長めに設定することができます。Fastly サービスがこのように設定されていない場合は、以下のステップで一定の TTL を設定できます。
Fastly コントロールパネルの Configuration セクションで Clone をクリックし、サービスの編集可能なバージョンを作成します。
サイドバーで Settings を選択します。
Create your first cache setting をクリックします。
Name セクションに「Set TTL to a year (TTL を1年に設定)」のように分かりやすい名前を入力します。
TTL (seconds) を31536000 (1年を秒で示した数値) に設定します。
Create をクリックします。
Activate をクリックすると、最新の設定が Fastly サービスにデプロイされます。
Cloud Function を作成する
今度は Cloud Function を設定します。このコーディングはローカルマシンでもできますが、Google の Cloud SDK を使用することも可能です。今回は UI を使用します。Cloud Functions のコンソールで CREATE FUNCTION をクリックします。
「fastly-purge」など、任意の名前を入力し、Trigger を「Cloud Storage」に設定し、Event Type で「Finalize/create」を選びます (ファイルが保存されている GCS バケットを選択する必要があります)。
インラインエディターを使用して以下のコードを入力します。
const fetch = require('node-fetch');
const FASTLY_PUBLIC_BASEURL = "https://www.example.com";
exports.fastlyPurge = async (obj, context) => {
const baseUrl = FASTLY_PUBLIC_BASEURL.replace(/\/+$/, '');
const fileName = obj.name.replace(/^\/+/, '');
const completeObjectUrl = `${baseUrl}/${fileName}`;
const resp = await fetch(completeObjectUrl, { method: 'PURGE'})
if (!resp.ok) throw new Error('Unexpected status ' + resp.status);
const data = await resp.json();
console.log(`Job complete for ${fileName}, purge ID ${data.id}`);
};
ここで使用する依存関係は node-fetch モジュールのみです。これを使うことで HTTP リクエストの送信が若干、簡単になります。package.json
タブに移行してこのモジュールを依存関係として追加します。
"dependencies": {
"@google-cloud/storage": "^1.6.0",
"node-fetch": "^2.6.0"
}
最後に、function to invoke を fastlyPurge
に設定して保存します。
GCP によって作成した関数が読み込まれ、有効になるまで数分かかります。これで完了です!GCS バケットのファイルが変更されると、GCP が Fastly に通知を送信し、これらのファイルのキャッシュされたコピーが Fastly のサーバーから削除されます。エッジクラウド全体でキャッシュが削除されるのにかかる平均時間は150ミリ秒です (2019年12月31日現在)。
テストを実施する
まず以下のリクエストを繰り返し実行し、オブジェクトが Fastly によってキャッシュされていることを確認します。
curl -is "https://your.fastly.domain/file/path" 2>&1 | grep -i "x-cache:"
これにより HTTP レスポンスの X-Cache
ヘッダーに一貫して HIT または HIT-CLUSTER が表示されます (恐らく最初は MISS が表示されます)。
x-cache: MISS
x-cache: HIT
x-cache: HIT
x-cache: HIT
ファイルに変更を加え、バケットに再度アップロードします。gsutil
を使ってこれを行うことができますが、バケットエクスプローラーの UI を使用することも可能です。
gsutil -m -h "Content-Type:text/css" -h "Cache-Control:public, max-age=0" cp -r ~/path-to-content/* gs://[BUCKET_NAME]
cURL コマンドを再度実行してオブジェクトを Fastly にリクエストします。初回は、わずかながらレスポンス時間が長くなり x-cache
に MISS または MISS-CLUSTER と表示されます。
重要なのは、ファイルが変更され、バケットにアップロードした変更が反映されていることです (cURL コマンドの「| grep」の部分を削除するとレスポンスのコンテンツを確認できます)。
コードを理解する
関数が obj
と context
の因数で呼び出されます。前者は Cloud Storage オブジェクトを指し、後者はイベントに関するメタデータを提供します。因数の中で興味があるプロパティは obj.name
のみで、これには GCS バケット内で作成、更新、または削除されたオブジェクトの名前が含まれます。また、Fastly サービス経由でオブジェクトを配信しているドメインを確認し、Fastly が理解できるオブジェクトの完全な URL を作成できるようにする必要があります。
const baseUrl = FASTLY_PUBLIC_BASEURL.replace(/\/+$/, '');
const fileName = obj.name.replace(/^\/+/, '');
const completeObjectUrl = `${baseUrl}/${fileName}`;
単一の URL をパージするのは Fastly API では特殊なケースで、api.fastly.com のエンドポイントではなく、パージしたい URL にリクエストが送信されます。この node-fetch ライブラリは、HTTP PURGE のメソッドを使用してリクエストを送信し、Fastly から返されたパージ ID を報告するのに使用されます。Fastly のサポートが必要な場合は、この ID をサポートチームにお伝えください。
const resp = await fetch(completeObjectUrl, { method: 'PURGE'})
const data = await resp.json();
console.log(`Job complete for ${fileName}, purge ID ${data.id}`);
トラブルシューティング
ファイルを編集して GCS にアップロードしたにもかかわらず、Fastly によって配信されるファイルに変更が反映されない場合は、以下のステップでトラブルシューティングを行います。
アップロードに問題はありませんでしたか?
Fastly をバイパスしてバケットに直接オブジェクトをリクエストし、GCS 上でファイルが変更されているか確認します。
curl -i "https://[BUCKET_NAME].storage.googleapis.com/[PATH]"
変更されていない場合は、更新したファイルのバケットへのアップロード、または GCS のビルトインキャッシュ機能に関連した問題が原因であると考えられます。変更されている場合は、Cloud Function に問題があった可能性があります。
Cloud Function が実行されましたか?
Stackdriver ログをチェックし、関数が適切にトリガーされたか確認します。
Google Cloud Functions のコンソールにアクセスします。
作成した関数の名前をクリックします。
ページトップにある View logs をクリックします。
最新のログエントリーのタイムスタンプを確認し、変更したファイルをバケットにアップロードした時間と比較します。
API リクエストは正常に機能しましたか?
バケットのファイルを更新した際に、関数によるアクティビティが発生したことがログに表示されている場合は、ログを確認し、Fastly によってパージリクエストが承認されたことを確認します。正常に機能した場合は、以下のように表示されます。
Function execution started
Job complete for sample.css, purge ID 17953-1516801745-2111226
Function execution took 683 ms, finished with status: 'ok'
このような出力が表示されない場合は、代わりにエラーが記録されている可能性があります。サポートが必要な場合は、コミュニティフォーラムを通じて、または support@fastly.com までメールにていつでもご連絡ください。
次のステップ
今回は、コンテンツの変更時に古いコンテンツを瞬時に無効化しながら GCS バケットのコンテンツを Fastly 経由で配信する基本的な方法をご紹介しました。次のステップとして以下をご検討ください。
個々のアセットだけでなく、静的な Web サイト全体を配信する。上記の設定はアセットの配信に最適ですが、Web サイト全体の場合は、デフォルトのディレクトリ・インデックス・ファイル名 (「index.html」など) と 404 Not Found エラーを配信するファイルが必要になります。Google Cloud Platform でカスタムドメインをご使用の GCS バケットに割り当てるか、または Fastly で追加の Fastly VCL コードを記述することでこれが可能です。
GCS へのアクセスを認証する。バケットへのパブリック読み取りアクセスを許可する代わりに、IAM ユーザーを作成し、それを Storage Object Viewer のロールに割り当て、リクエストごとに GCS のトークンを生成する手順に従います。
x-goog- ヘッダーをすべて削除する。Google によって、Cloud Platform から受信する HTTP レスポンスに多くのヘッダーが追加されます。これらのヘッダーを削除するには、カスタムドメインをバケットに割り当て、GCS での静的 Web サイトのホスティングを有効にするか、またはこれらを削除するようご利用の Fastly サービスを設定する必要があります。
パージを認証する。現状では、一般アクセスが可能な Fastly ドメインに誰でも HTTP リクエストを送信してキャッシュからオブジェクトをパージできます。これに懸念があり、Cloud Function を認証してその関数によってのみパージシグナルを送信できるようにしたい場合は、ご利用のサービスのパージ機能で Fastly API トークンを生成し、認証されたパージを有効にして、生成された API トークンに設定された値と一緒に
Fastly-Key
ヘッダーが送信されるよう Cloud Function を変更します。オーケストレーションをコードで表現する。Hashicorp の Terraform を使用し、Google Cloud Platform と Fastly の設定をキャプチャしてファイルに保存し、バージョン管理できるようにすることをお勧めします。
いろいろご紹介しましたが、肝心なのは、一行の非常にシンプルな Cloud Function で GCS バケットへの変更を Fastly に認識させることができるということです。このようにエレガントな統合によって Web サイトのパフォーマンスと効率が大きく改善され、エンドユーザーによる読み込み時間と Google からの請求額を大幅に削減できます。