🛹

CloudFrontで学ぶstale-while-revalidate、stale-if-errorディレクティブ

2023/12/09に公開

📌 はじめに

こんにちは!@Ryo54388667です!☺️
普段は都内でフロントエンドエンジニアとして業務をしてます!
主にTypeScriptやNext.jsといった技術を触っています。

先日、CloudFrontがstale-while-revalidateに対応したという話を耳にしました。
https://dev.classmethod.jp/articles/cloudfront-stale-while-revalidate-stale-if-error/

自分の中でstale-while-revalidateの挙動のイメージが曖昧だったので、検証ついでに記事を書いてみました。フロントエンドの開発では、stale-while-revalidateという単語よりも、「SWR」という単語のほうが耳馴染みのある人が多いかもしれません。Vercel製のフェッチライブラリーであるSWRをフロントエンド開発で利用することが多いことに由来してそうです。

ついでの、ついでに、CloudFrontはstale-if-errorディレクティブにも対応したので、こちらの検証も行います。

📌 概要

stale-while-revalidateの概要

MDNでは下記のように書かれています

レスポンスディレクティブの stale-while-revalidate は、キャッシュを再検証している間、古いレスポンスの再利用が可能なことを示します。
https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Cache-Control#ブラウザーの互換性

名前通りの定義です。。
より平易な言い方をすると、「キャッシュが切れたので、再びサーバーに問い合わせる間、古いコンテンツを返しておくよ!」 という感じです!

ブラウザとCloudFront, オリジンを例に挙動を確認します。下に一連のフローの図があるので、合わせて見ると良いかとおもいます。
まず、コンテンツがCloudFrontにキャッシュされている場合(図①)、ブラウザからのリクエストをCloudFrontが受け取り、キャッシュされたコンテンツをレスポンスします。
これはキャッシュが効いている間なので、コードベースでいうと、max-ageの範囲内のケースに該当します。続いて、もしキャッシュもstale-while-revalidateも無効の場合(図③)、ブラウザからのリクエストがきた時、CloudFrontはオリジンに対して問い合わせを行います。このようにしてブラウザにコンテンツを返却します。

この部分の課題は、ブラウザがリクエストしてからレスポンスを受け取るまでのスピードが遅くなります。 この課題を解決できるのが、stale-while-revalidateディレクティブです。キャッシュが無効になり、stale-while-revalidateが有効な場合(図②)、ブラウザからリクエストがあると、CloudFrontは以前までキャッシュされていたコンテンツをブラウザにレスポンスします。それと同時に、オリジンのほうにリクエストを行い、新しいコンテンツをキャッシュします。なので、その次にブラウザからリクエストがあると、先ほど裏側でキャッシュしたコンテンツをレスポンスします。このような仕組みなので、ブラウザがリクエストしてからレスポンスを受け取るまでのスピードが早くなります!👏

stale-if-errorの概要

まずは公式の定義から。

レスポンスディレクティブの stale-if-error は、オリジンサーバーがエラー(500、502、503、504)でレスポンスを返したときに、キャッシュが古いレスポンスを再利用できることを指示します。
https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Cache-Control#ブラウザーの互換性:~:text=されます。-,stale-if-error,-レスポンスディ

該当するのは下の図の④の箇所です。キャッシュが無効になっていて、CloudFrontがオリジンにリクエストを行った時、オリジンからエラーのレスポンスがあると、ブラウザにエラーレスポンスを行うのではなく、代替するようなコンテンツをブラウザにレスポンスします。この挙動を踏まえると、オリジンのエラーが解消されるまでの予想時間や、古いコンテンツが許容可能である最長の期間を考慮して設定することになるのかなと思います。

📌 検証準備

今回はLambdaをオリジンとし、CloudFrontと連携します。
ブラウザで目視しやすいように、HTMLをレスポンスさせることにします!

作成手順としては、
1. Lambda の作成
2. CloudFrontとの接続
の順です。

1.Lambda の作成

まず、Lambdaには下記のコードを書きます。

export const handler = async (event) => {
  try {
        // 日本時間(JST)の日付と時刻を取得
        const now = new Date().toLocaleString("ja-JP", { timeZone: "Asia/Tokyo" });

        // HTMLコンテンツを作成
        const htmlContent = `
        <html>
            <head><title>Lambda Test Page</title></head>
            <body style="text-align: center;" >
                <h1>CreatedAt</h1>
                <h1>${now}</h1>
            </body>
        </html>
        `;

        // CloudFrontに返却するレスポンスを生成
         return {
            statusCode: 200,
            headers: {
                'Content-Type': 'text/html',
                "Cache-Control": "max-age=60, stale-while-revalidate=30"  // <== 重要なのはこの一行
            },
            body: htmlContent,
        };
    } catch (error) {
        console.error('Error:', error);
    }
};

セキュリティ上、あまり良くはないですが簡易的な検証なのでフルオープンなLambdaにします。
関数URLという機能を設定します。これを行うとAPI Gateway を挟む必要がありません!便利!
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-urls.html
ブラウザでは下記の画像のような表示になります。

2.CloudFrontとの接続

CloudFrontnの設定を行います。
CloudFrontのオリジンを先ほどのLambdaのURLを設定します。
連携だけだと、基本的にはこれで終了なのですが、今回はキャッシュの挙動を確認したいので、その設定を行う必要があります。

キャッシュポリシーのセレクトボックスで、デフォルトだと「CachingDisabled」になっているので、ここを「CachingOptimized」に変更します。
これによって、コード内のCache-Controlを反映できるようになります。

これでLambddaとCloudFrontとの連携の設定は終了です。
アクセスするURLがAWS コンソールのCloudFrontの画面に表示されているのでそちらをメモしておきます。*******.cloudfront.net/形式のものです。

これで確認の準備ができました!👏

📌 stale-while-revalidateとstale-if-errorの挙動の検証

stale-while-revalidate

Lambdaのコード内にあるように、
"Cache-Control": "max-age=60, stale-while-revalidate=30"
がミソです!ただし、stale-while-revalidate=30の意味は「キャッシュされてから30秒間」ではなく、「キャッシュが切れてから30秒間」なので注意が必要です。

この設定の場合、キャッシュの有効期間が60秒で、それ以降30秒間は古いコンテンツをレスポンスしつつ、裏側でオリジンにリクエストするような挙動になります。

ブラウザの表示ベースでいうと、

  • 0s ~ 60s の間 何度ブラウザを更新しても、日時の表示は変更なし。
  • 61s ~ 90s の間 1度目のアクセスでは日時の変更なし。2回目のアクセスで日時が更新されている。

このような挙動になるはずです。
下の画像が検証結果です。

・ 60秒未満にアクセスする。

何度ブラウザ更新しても、同じcreatedAtになってます。キャッシュされていますね!

・ 60~90秒の間にアクセスする。

1度目のアクセスでは古いものを返却しつつ、裏側でキャッシュしているので、日時の表示は変更なし。意図通りになっています。

2回目のアクセスでは、先ほど裏側でキャッシュしたコンテンツが表示されており、日時の変更がされています!!(createdAtが先ほどの1分後になっています。)

stale-if-error

stale-if-errorを試してみるにあたって、少し Lambdaのコードを修正します。
cache-controlの箇所を下記のコードに書き換えます。

export const handler = async (event) => {
   try {
        // 日本時間(JST)の日付と時刻を取得
        const now = new Date().toLocaleString("ja-JP", { timeZone: "Asia/Tokyo" });

        // HTMLコンテンツを作成
        const htmlContent = `
        <html>
            <head><title>Lambda Test Page</title></head>
            <body style="text-align: center;" >
                <h1>CreatedAt</h1>
                <h1>${now}</h1>
            </body>
        </html>
        `;
        // CloudFrontに返却するレスポンスを生成
          return {
             statusCode: 200,
             headers: {
                 'Content-Type': 'text/html',
                 "Cache-Control": "max-age=60, stale-if-error=90" // <=== ここを追加
             },
             body: htmlContent,
         };
     } catch (error) {
         console.error('Error:', error);
     }
};

"Cache-Control": "max-age=60, stale-if-error=90" が重要です!
stale-if-error=90の意味は 「キャッシュが切れてから90秒間」 なので注意が必要です。

この設定の場合、キャッシュの有効期間が60秒で、それ以降90秒間はオリジンにリクエストしてエラーだったら古いコンテンツをレスポンスするような挙動になります。

ブラウザの表示ベースでいうと、

  • 0s ~ 60s の間 何度ブラウザを更新しても、日時の表示は変更なし。
  • 61s ~ 150s の間 日時の更新なし。
  • 150s ~ サーバーエラーの画面が表示される。

のような挙動になるはずです。
stale-while-revalidateの確認方法とは異なり、60秒間の途中で500エラーを発生させたいので、最初にブラウザにアクセスした後、Lambdaのコードを書き換えて、意図的にエラーを出すようにします。

export const handler = async (event) => {
//   try {
        // 日本時間(JST)の日付と時刻を取得
        const now = new Date().toLocaleString("ja-JP", { timeZone: "Asia/Tokyo" });

        // HTMLコンテンツを作成
        const htmlContent = `
        <html>
            <head><title>Lambda Test Page</title></head>
            <body style="text-align: center;" >
                <h1>CreatedAt</h1>
                <h1>${now}</h1>
            </body>
        </html>
        `;
        throw new Error()    // <=== こちらを追加
        // CloudFrontに返却するレスポンスを生成
        //  return {
        //     statusCode: 200,
        //     headers: {
        //         'Content-Type': 'text/html',
        //         // "Cache-Control": "max-age=60, stale-while-revalidate=30"
        //         "Cache-Control": "max-age=60, stale-if-error=90"
        //     },
        //     body: htmlContent,
        // };
    // } catch (error) {
    //     console.error('Error:', error);
    // }
};

下の画像が検証結果です。

・ 60秒未満にアクセスする。

最初の60sはキャッシュされたものが返却されるので割愛します。
stale-while-revalidateの確認した時と同じ挙動です。

この間に、Lambdaのコードを上記のコードに書き換えます。

・ 61~150秒の間にアクセスする。

エラーにならず、先ほどまでキャッシュされていらコンテンツがレスポンスされています!!max-ageの設定だけの場合、オリジンでエラーが発生しているはずなので、ブラウザにエラーがレスポンスされます。

・ 150秒後にアクセスする。

正常にエラーが発生しました!150s後にエラーになっています👌

📌 まとめ

CloudFrontを利用して、stale-while-revalidate,stale-if-errorディレクティブの挙動をまとめてみました。

  • "Cache-Control": "max-age=60, stale-while-revalidate=30" キャッシュの有効期間が60秒で、それ以降30秒間は古いコンテンツをレスポンスしつつ、裏側でオリジンにリクエストするような挙動
  • "Cache-Control": "max-age=60, stale-if-error=90" キャッシュの有効期間が60秒で、それ以降90秒間はオリジンにリクエストしてエラーだったら古いコンテンツをレスポンスするような挙動

Webアプリケーションのレスポンス速度をあげるなら、積極的にEdgeのキャッシュ機能を有効活用していきたいですね!

書き散らしスクラップ

最後まで読んでいただきありがとうございます!
気ままにつぶやいているので、気軽にフォローをお願いします!🥺
https://twitter.com/Ryo54388667

デベキャン

Discussion