NRIネットコム Blog

NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

キャッシュ制御の観点で見る CloudFront

本記事は  AWSアワード記念!夏のアドベントカレンダー  7日目の記事です。
🎆🏆  6日目  ▶▶ 本記事 ▶▶  8日目  🏆🎆

すっかり夏ですね、単純に嫌です。 日が落ちないと外に出るのも厳しい暑さですが皆様いかがお過ごしでしょうか。 西です。

今年は無事 2024 Japan AWS All Certifications Engineer に残れましたので AWSアワード記念!夏のアドベントカレンダー 7 番手です。 はたしていつまで All Certifications Engineer に残り続けられるのでしょうか。

さて、本題です。 今回も例によって Amazon CloudFront (CloudFront) についての記事です。

CloudFront はリクエストのあった Web コンテンツをキャッシュすることが主な役割です。 しかし、コンテンツにはキャッシュして良いものと、すべきでないものがあリます。 キャッシュすべきでないものには、例えば個人情報や機密情報のような秘匿性の高い情報を表現するコンテンツや、同じリクエストパスでもユーザ毎に結果が異なる場合のコンテンツが挙げられます。 これらを適切に制御することで、予期せぬ挙動や漏洩事故を防いだ安全なコンテンツ配信を実現することができます。

そこで今回は CloudFront の機能の中でもキャッシュに関わるものにフォーカスし、その設定や制御について整理します。 では、それぞれどのような形式でのキャッシュ制御が可能なのかを見ていきましょう。

CloudFront おさらい

言わずと知れた CloudFront ですが、少しだけおさらいです。

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/Introduction.html

Amazon CloudFront は、ユーザーへの静的および動的なウェブコンテンツ (.html、.css、.js、イメージファイルなど) の配信を高速化するウェブサービスです。CloudFront では、エッジロケーションというデータセンターの世界的ネットワークを経由してコンテンツを配信します。CloudFront でサービスを提供しているコンテンツをユーザーがリクエストすると、リクエストはエッジロケーションにルーティングされ、レイテンシー (遅延時間) が最小になります。これにより、コンテンツは可能な限り最高のパフォーマンスで配信されます。

世界中に存在するエッジロケーション等の拠点を活用し、ユーザにより近い場所でコンテンツをキャッシュ / 配信することを実現しています。 ちなみにそれら拠点の数ですが、以下の数だけ配置されているそうです。(参考)

拠点種別 配置されている数
リージョン別エッジキャッシュ (REC) 13
Points of Presence (エッジロケーション) 600 以上
埋め込み Points of Presence 600 以上

※ 2024/07/16 現在

規模が大きすぎて全く想像ができません、とりあえずとても多いです。

CloudFront のコンテンツ配信におけるキャッシュ

さて、今回はそんな CloudFront のキャッシュ機能について整理するのですが、一言にキャッシュと言っても複数種類存在します。

今回のような Web コンテンツ配信の文脈で考えれば、一般的に想像するのは CloudFront (CDN) のキャッシュかと思います。 一方、ビューワー側でもキャッシュが存在します。 身近な例だと、普段僕たちが利用するようなブラウザのキャッシュですね。

そのため、CloudFront のキャッシュ制御においては、CloudFront 側だけでなくビューワー側のキャッシュも考慮する必要があります。

今回は CloudFront のキャッシュとブラウザのキャッシュのそれぞれを制御するための機能について順に整理していきます。

CloudFront のキャッシュを制御する

ではここからは本題のキャッシュ制御について述べていきます。 CloudFront におけるキャッシュ制御には、キャッシュポリシーという設定が利用できます。

まずは特別な設定は行わず、デフォルト設定で作成した CloudFront ディストリビューションからコンテンツを取得してみます。 デフォルト設定では、キャッシュポリシーに CachingOptimized というマネージドポリシーが設定されています。

# 初回リクエスト
~
❯ curl -I https://cloudfront.example.com/image_1.jpeg
HTTP/2 200
content-type: image/jpeg
content-length: 1137190
date: Sun, 14 Jul 2024 06:07:41 GMT
last-modified: Sat, 13 Jul 2024 07:09:57 GMT
etag: "2628908debe1fe7152937557a4ff70d6"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Miss from cloudfront
via: 1.1 d9725486e8ca2b6f74ce31294643d08e.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT51-C3
x-amz-cf-id: BFc-gY8IYrBjIfZ59TWTq2cPrB8r2gW2SayovjUNhEXpLOT2ohcDKQ==

# 2 度目のリクエスト
~
❯ curl -I https://cloudfront.example.com/image_1.jpeg
HTTP/2 200
content-type: image/jpeg
content-length: 1137190
date: Sun, 14 Jul 2024 06:07:41 GMT
last-modified: Sat, 13 Jul 2024 07:09:57 GMT
etag: "2628908debe1fe7152937557a4ff70d6"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Hit from cloudfront
via: 1.1 586ca8ee044fb65ddc72566c6af4481c.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT51-C3
x-amz-cf-id: 8I_s-_LJVOikSEDLk9WZtYaWZ7srhQGAdFFTGHBhXhsvGkE1pMCJ4g==
age: 13

URL はぼかしますが、以上の結果は jpeg コンテンツを取得するために 2 度同じ内容で実行した curl コマンドのものです。 2 度目の取得では x-cache: Hit from cloudfront となっていて、キャッシュがヒットしたことがわかります。 また、age: 13となっていることも確認でき、キャッシュされてから 13 秒経過していることもわかります。

html 等のファイルについても同様です。

# 1 度目のリクエスト
~
❯ curl -I https://cloudfront.example.com
HTTP/2 200
content-type: text/html
content-length: 125
date: Sun, 14 Jul 2024 06:17:17 GMT
last-modified: Sat, 13 Jul 2024 05:53:38 GMT
etag: "c8a7e776e14d24189159f617ae2b3a90"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Miss from cloudfront
via: 1.1 3624dcb577839347c98e4e269e665ccc.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT51-C3
x-amz-cf-id: vfR3PzuzKEcWXm5bZLUdaK8WzsAXuRtcs1pAH25LYr-pSZ9fdo4ecQ==

# 2 度目のリクエスト
~
❯ curl -I https://cloudfront.example.com
HTTP/2 200
content-type: text/html
content-length: 125
date: Sun, 14 Jul 2024 06:17:17 GMT
last-modified: Sat, 13 Jul 2024 05:53:38 GMT
etag: "c8a7e776e14d24189159f617ae2b3a90"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Hit from cloudfront
via: 1.1 3624dcb577839347c98e4e269e665ccc.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT51-C3
x-amz-cf-id: p-vb8ayrwWZ960p_aUDOTtvp6R8pOCl18_KcGp1vXh3TjMqLiR21FA==
age: 8

このように、特別な設定を施さない場合でも CloudFront はコンテンツをキャッシュしてくれます。 しかし、冒頭で述べた通りキャッシュさせたくないコンテンツがある場合や、細やかな制御を行いたい場合はどうすれば良いのでしょうか。

キャッシュポリシーを利用する

CloudFront でキャッシュするコンテンツを制御するには、キャッシュポリシーという設定を利用します。 CloudFront の設定であるビヘイビアにキャッシュポリシーを関連付けることで利用できます。 ビヘイビアはリクエストのパスパターン毎に作成できるため、パスパターン毎にキャッシュポリシーを使い分けることができます。 例えば「/image のパスパターンで返すコンテンツは全てキャッシュさせるが、/api のパスパターンで返すコンテンツはキャッシュさせない」というようなことも可能です。

なお、キャッシュポリシーはマネージドポリシーをそのまま利用することもできますし、自身で作成したものを利用することもできます。

マネージドポリシーを利用する

まずはマネージドポリシーから見ていきます。 キャッシュポリシーのマネージドポリシーは以下のドキュメントで紹介されています。

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/using-managed-cache-policies.html#managed-cache-caching-optimized

詳細はそれぞれ異なりますが、リクエストのヘッダー・Cookie・クエリ文字列に応じた使い分けが想定されています。

マネージドポリシー名 TTL         キャッシュキーに含まれるヘッダー        キャッシュキーに含まれる Cookie キャッシュキーに含まれるクエリ文字列 圧縮オブジェクトのキャッシュ設定
Amplify - 最小: 2 秒
- 最大: 600 秒 (10 分)
- デフォルト: 2 秒
- Accept-Encoding
- Authorization
- CloudFront-Viewer-Country
- Host
全て 全て 有効
CachingDisabled 全て 0 秒 なし なし なし 無効
CachingOptimized - 最小: 1 秒
- 最大: 31,536,000 秒 (365 日)
- デフォルト: 86,400 秒 (24 時間)
- Accept-Encoding なし なし 有効
CachingOptimizedForUncompressedObjects - 最小: 1 秒
- 最大: 31,536,000 秒 (365 日)
- デフォルト: 86,400 秒 (24 時間)
なし なし なし 無効
Elemental-MediaPackage - 最小: 0 秒
- 最大: 31,536,000 秒 (365 日)
- デフォルト: 86,400 秒 (24 時間)
- Origin なし - aws.manifestfilter
- start
- end
- m
Gzip に対して有効
UseOriginCacheControlHeaders - 最小: 0 秒
- 最大: 31,536,000 秒 (365 日)
- デフォルト: 0 秒
- Accept-Encoding
- Host
- Origin
- X-HTTP-Method-Override
- X-HTTP-Method
- X-Method-Override
全て なし 有効
UseOriginCacheControlHeaders-QueryStrings - 最小: 0 秒
- 最大: 31,536,000 秒 (365 日)
- デフォルト: 0 秒
- Accept-Encoding
- Host
- Origin
- X-HTTP-Method-Override
- X-HTTP-Method
- X-Method-Override
全て 全て 有効

数が多いため、分かりやすく選定できるものから見ていきます。

  • CloudFront でキャッシュを行う必要がなく、セキュリティレイヤとしてしてだけ利用したい

CachingDisabled

  • オリジンや利用サービスが AWS Amplify や Amazon Elemental-MediaPackage

Amplify もしくは Elemental-MediaPackage (※ これらポリシーの TTL 設定等が要件の問題にならない場合)

  • コンテンツを原則キャッシュさせたい場合

CachingOptimized あるいは CachingOptimizedForUncompressedObjects

これらポリシーはオリジンサーバが CloudFront へ返すオブジェクト形式が Gzip や Brotil に該当するかどうかで使い分けることになります。

※ Brotil: オープンソースの圧縮アルゴリズム (参考)

  • ヘッダーやクエリ文字列に応じて異なるコンテンツを配信する場合

UseOriginCacheControlHeaders あるいは UseOriginCacheControlHeaders-QueryStrings

※ キャッシュキーに含むのがヘッダーだけで良い場合は UseOriginCacheControlHeaders、クエリ文字列までキャッシュキーに含めたい場合は UseOriginCacheControlHeaders-QueryStrings の利用になります。

要件に応じて、以上のポリシーの中からビヘイビア毎に適切なものを選択し、設定することが重要になります。

自分でキャッシュポリシーを作成して利用する

では次は、マネージドポリシーではニーズに合ったものがなく、実際にキャッシュポリシーを作成する場合を考えます。 今回は例として allow-cache-content というヘッダーをキャッシュキーに含むキャッシュポリシーを作成してみましょう。

作成したポリシーをビヘイビアへ設定して実際にリクエストを実行してみます。

(ヘッダーに渡す値で返す画像コンテンツを決定するように設定しています)

# パターン 1 の初回リクエスト
~
❯ curl -I -H "allow-cache-content:allowed_pettern1" https://cloudfront.example.com/image
HTTP/2 200
content-type: image/jpeg
content-length: 1137190
x-amz-id-2: Nb45cq/r8a9VXu8zrJ1vXZNKV3gyg9xz9ODUrVlLYu05YGBc+XatfqfYf3/PebKP0k3f0AnC35U=
x-amz-request-id: 3DC43E0210KKP08G
date: Mon, 15 Jul 2024 01:30:14 GMT
last-modified: Sat, 13 Jul 2024 07:09:57 GMT
etag: "2628908debe1fe7152937557a4ff70d6"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Miss from cloudfront
via: 1.1 0f38e67457dc2472603650b9f0a40962.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT51-C3
x-amz-cf-id: bqzKfrIwpyljYrPr3PaqfSE5SllT-0ZN8ni3KePNazbZKFN_ut9ZDQ==

# パターン 1 の 2 回目のリクエスト
~
❯ curl -I -H "allow-cache-content:allowed_pettern1" https://cloudfront.example.com/image
HTTP/2 200
content-type: image/jpeg
content-length: 1137190
x-amz-id-2: Nb45cq/r8a9VXu8zrJ1vXZNKV3gyg9xz9ODUrVlLYu05YGBc+XatfqfYf3/PebKP0k3f0AnC35U=
x-amz-request-id: 3DC43E0210KKP08G
date: Mon, 15 Jul 2024 01:30:14 GMT
last-modified: Sat, 13 Jul 2024 07:09:57 GMT
etag: "2628908debe1fe7152937557a4ff70d6"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Hit from cloudfront
via: 1.1 32c13fa00a84d4fc52c819bc6c24c684.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT51-C3
x-amz-cf-id: Hx8s6_gN8PXTwd6uAQi3B6tjacYRDThnd_euuFlNdHTOCnznShQ37Q==
age: 26

# パターン 2 の初回リクエスト
~
❯ curl -I -H "allow-cache-content:allowed_pettern2" https://cloudfront.example.com/image
HTTP/2 200
content-type: image/jpeg
content-length: 1007558
x-amz-id-2: I+HjN5g7s1k8bEpbm0pxtaXlBRQ0vR3Wp9y7T08ybIC95NFCikrQiSyoMxbeOU4MmugXEJR79bs=
x-amz-request-id: W9AZ25CCZ9BF58S0
date: Mon, 15 Jul 2024 01:31:54 GMT
last-modified: Sun, 14 Jul 2024 06:39:20 GMT
etag: "16f3d2bfb7bb65378a654c49ed988275"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Miss from cloudfront
via: 1.1 14fa20286bbb856e84a3ad09af9ec060.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT51-C3
x-amz-cf-id: qevStp13oShJ0F81Cu5sOeW0VveEYjiAkuSPomnaCfoD2taeDFGPvA==

# パターン 2 の 2 回目のリクエスト
~
❯ curl -I -H "allow-cache-content:allowed_pettern2" https://cloudfront.example.com/image
HTTP/2 200
content-type: image/jpeg
content-length: 1007558
x-amz-id-2: I+HjN5g7s1k8bEpbm0pxtaXlBRQ0vR3Wp9y7T08ybIC95NFCikrQiSyoMxbeOU4MmugXEJR79bs=
x-amz-request-id: W9AZ25CCZ9BF58S0
date: Mon, 15 Jul 2024 01:31:54 GMT
last-modified: Sun, 14 Jul 2024 06:39:20 GMT
etag: "16f3d2bfb7bb65378a654c49ed988275"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Hit from cloudfront
via: 1.1 5e80337b455f0f562a74f5e19ab45978.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT51-C3
x-amz-cf-id: zaTj2kp0zWs806vMGgf2dRLv1duKad1DNejGbQkTjDpp6PIZw_Id8w==
age: 4

ヘッダーの値が allowed_pettern1 と allowed_pettern2 で異なると ETag の値から別オブジェクトが返されていることがわかります。 また、別の値をヘッダーにセットすると同じ https://cloudfront.example.com/image のパスでリクエストしていても初回はコンテンツがキャッシュされていないことがわかります。

このように、キャッシュポリシーを作成することで、任意のヘッダーをキャッシュキーに含む細やかなキャッシュ制御が可能です。

ビューワー側のキャッシュを制御する

ここまでは CloudFront 側でのキャッシュ機能について触れてきました。 一方、コンテンツは CloudFront のような CDN だけにキャッシュされるわけではなく、例えばブラウザのようなビューワー側でもキャッシュされ得るものです。 CloudFront の機能でビューワー側でもコンテンツをキャッシュさせないようにすることも実現可能ですので順に見ていきます。

レスポンスヘッダーポリシー

CloudFront の機能には、リクエストに対するレスポンスに指定したヘッダーを付与することができるレスポンスヘッダーポリシーが存在します。 この設定を利用してビューワーへのレスポンスに Cache-Control ヘッダーを付与することでビューワー側でのキャッシュを制御することが可能です。

では、実際に見てみましょう。 まずは先ほどと同様、あるコンテンツに対して 2 度リクエストを送り、ブラウザの開発者ツールでレスポンスを見てみます。 (CloudFront 側のキャッシュは CachingDisabled に設定しています)

  • 初回

  • 2 度目

同じパスに対してリクエストを送ると 2 度目は 304 Not modified となっており、ブラウザ側のキャッシュが作用していることがわかります。 では、レスポンスヘッダポリシーで Cache-Control: no-cache, no-store, must-revalidate と設定します。

再度同じリクエストを試行します。

  • 初回

  • 2 度目

ほぼ同じ内容ですが、Date ヘッダーからわかる通り 2 度のリクエストを実行しています。 2 度目のリクエストでも 200 OK となっており、同じパスへのリクエストを繰り返してもブラウザのキャッシュではなく CloudFront から再度コンテンツを取得し、利用している事がわかります。

このように、レスポンスヘッダーにキャッシュ制御情報を含む事でビューワー側でのキャッシュの制御を実現することが可能です。

Edge 関数 (Lambda@Edge / CloudFront Functions) によるヘッダ操作

前節ではレスポンスヘッダーポリシーでの制御について述べました。 しかしヘッダー以外の要素を踏まえてキャッシュ制御を行いたい場合もあるかと思います、そんな場合は Edge 関数での実現が可能です。 今回は複雑な事例には触れませんが、例えば CloudFront Functions (CF2) を以下のように実装し、ビューワーレスポンスをトリガーに起動させます。

function handler(event) {
    var response = event.response;
    var headers = response.headers;
    console.log(headers)

    try {
        var contentTypeHeader = headers['content-type'];
        console.log(contentTypeHeader)
        if (contentTypeHeader && contentTypeHeader['value'] === 'image/jpeg') {
            headers['cache-control'] = {
                value: 'no-cache, no-store, must-revalidate'
            };
        }
    } catch (e) {
        console.log("Error processing request:", e.message);
    }

    return response;
}

すると、コンテンツが画像の場合はキャッシュさせないという制御が実現できます。 (得られる結果は先ほどのレスポンスヘッダーポリシーの例と同様ですので省略します)

このようにシンプルなロジックで実現可能な場合は CF2 で実現可能です。 一方、オリジンから渡されるオブジェクト毎に外部のリストと照らして判定を行う必要があるような、少し込み入った処理が必要な場合は Lambda@Edge を採用しましょう。

おわりに

本稿では CloudFront の機能の中でもキャッシュに関わるものにフォーカスし、その設定や制御について触れました。 CloudFront の機能を活用することで、CloudFront 側でキャッシュさせるコンテンツも、ビューワー側でキャッシュされるコンテンツも制御することも可能です。 また、今回はキャッシュさせる / させないの二択で挙動を確認しましたが、TTL を操作することで細やかに有効期限を制御することも可能です。

もちろん今回触れた方法以外にもキャッシュ制御を行う方法はあると思いますが、皆様がコンテンツキャッシュの制御方法を選択する上での一助となれば嬉しいです。

執筆者: 西 洋平


AWS メインのクラウドエンジニア。
ソースコードより文章を書く方が好きです。