CloudFront Functionsでキャッシュキーの正規化を行ってキャッシュヒット率を向上させてみた

CloudFront Functionsでキャッシュキーの正規化を行ってキャッシュヒット率を向上させてみた

キャッシュヒット率を上げる際にはCloudFront Functionsを使ったNormalizationも検討しよう
Clock Icon2025.02.02

AcceptヘッダーでWebPを配信するか判断するようにしたけどキャッシュヒット率が低い

こんにちは、のんピ(@non____97)です。

皆さんAcceptヘッダーでWebPを配信するか判断するようにしたけどキャッシュヒット率が低いと思ったことはありますか? 私はあります。

以下の記事でAcceptヘッダーでWebPを配信するか判断する方法を紹介しました。

https://dev.classmethod.jp/articles/cloudfront-s3-website-webp-image-delivery/

こちらの記事の中で、以下のようにAcceptヘッダーはブラウザやアクセスの仕方によって大きく異なるため、キャッシュヒット率に影響を与えやすいことを紹介しています。

注意点として、Acceptヘッダーはブラウザやアクセスの仕方によって大きく異なるため、キャッシュヒット率が低下します。

例えば、私のChromeの場合はimage/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7で、Safariはimage/webp,image/avif,image/jxl,image/heic,image/heic-sequence,video/*;q=0.8,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5です。

CloudFrontとS3を使ったWebサイトでWebPの画像を配信してみた | DevelopersIO

キャッシュポリシーで定義したキャッシュキーは、そのキーの値がキャッシュ済みのコンテンツと完全に一致するかどうかでキャッシュを返すか判断をします。

例えば、Acceptヘッダーをキャッシュキーとしている場合、Acceptヘッダーの値がimage/avif,image/webpでアクセスした後に、次回アクセス時のヘッダーがimage/webp,image/avifのように異なる値になっているとキャッシュを返してくれません。

異なるクライアントからの初回アクセス毎にオリジンへ通信をするようにすると、パフォーマンスが悪いですし、オリジンへの負荷も高まりがちです。

今回はその対応としてCloudFront Functionsを使って、キャッシュキーの正規化(Cache Key normalization)を行い、キャッシュヒット率を向上させてみます。

Cache Key normalizationの説明は以下Black Beltの資料が分かりやすいです。

AWS-Black-Belt_2024_AmazonCloudFront-EdgeComputing_0630_v1_pdf.png

抜粋 : AWS Black Belt Online Seminar - Amazon CloudFront (CloudFront Functions / Lambda@Edge 編)

やってみた

検証環境

検証環境は以下のとおりです。

CloudFront Functionsを使ってCloudFrontのキャッシュヒット率を向上させてみた検証環境構成図.png

検証環境は全てAWS CDKでデプロイしました。使用したコードは以下GitHubリポジトリに保存しています。

https://github.com/non-97/cloudfront-s3-website/tree/v4.0.3

こちらのベースとなったコードの詳細な説明は以下記事をご覧ください。

https://dev.classmethod.jp/articles/aws-cdk-cloudfront-s3-website/

https://dev.classmethod.jp/articles/aws-cdk-cloudfront-s3-website-log-analytics/

https://dev.classmethod.jp/articles/cloudfront-s3-website-webp-image-delivery/

CloudFront Functionsの紹介

実行しているCloudFront Functionsは以下のとおりです。

./lib/src/cf2/normalize-webp-cache-key/index.js
async function handler(event) {
  const request = event.request;
  const uri = request.uri;

  // Check WebP support in Accept header
  const headers = request.headers;
  const acceptHeader = headers.accept ? headers.accept.value : '';
  const viewerAcceptWebP = acceptHeader.split(',')
    .some(type => {
      const parts = type.trim().split(";");
      const mimeType = parts[0];
      const params = parts[1] || "";

      if (mimeType === 'image/webp') {
        return true;
      }

      const qMatch = params.match(/q=([0-9.]+)/);
      const q = qMatch ? parseFloat(qMatch[1]) : 1.0;

      if (q < 1) {
        return false;
      }

      return mimeType === '*/*' || mimeType === 'image/*';
    });

  // Regular expression for image extensions
  const imageExtRegex = /\.(jpe?g|png)$/i;

  if (imageExtRegex.test(uri)) {
    request.headers['x-viewer-accept-webp'] = {
      value: `${viewerAcceptWebP}`
    };
  }

  return request;
}

末尾がjpeg or jpg or pngの場合にx-viewer-accept-webpヘッダーに"true""false"を設定するというシンプルなものです。

このx-viewer-accept-webpをキャッシュキーとします。

"true"であれば、クライアントがWebPをサポートしていると判定し、逆に"false"であればクライアントがWebPをサポートしていないと判定しています。

ブラウザによってはWebPをサポートしているがAcceptヘッダーに*/*image/*としか返さないものもあるため、そちらにもサポートするようにしています。ただし、それらの場合本当にWebPをサポートしているのかどうか不明であるためq(重み)が1未満の場合はWebPをサポートしていないと判定しています。

各ブラウザのAcceptヘッダーの既定値はMDN Web Docsにまとまっています。

https://developer.mozilla.org/ja/docs/Web/HTTP/Content_negotiation/List_of_default_Accept_values

その他のポイントは値は文字列である必要があるというところです。もし、booleanで返してしまうと、以下のようなHTTP 503エラーとなります。

The request could not be satisfied.

The CloudFront function returned an invalid value: request.headers value field must be a string. We can't connect to the server for this app or website at this time. There might be too much traffic or a configuration error. Try again later, or contact the app or website owner.
If you provide content to customers through CloudFront, you can find steps to troubleshoot and help prevent this error by reviewing the CloudFront documentation.

curlでAcceptヘッダーを変更しながらアクセス

実際にデプロイして動きを確認してみます。

まず、初回アクセスです。

Acceptヘッダーにimage/webpを付与してアクセスします。

> time curl -I https://www.non-97.net/non__97.png -H "Accept:image/webp"
HTTP/2 200
content-type: image/webp
content-length: 5464
date: Sun, 02 Feb 2025 08:42:04 GMT
last-modified: Fri, 24 Jan 2025 03:43:34 GMT
etag: "54b0857ccfbab67746434fb9042aacff"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Miss from cloudfront
via: 1.1 663c57b4ec4e2561ada30794913fe298.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT12-P1
alt-svc: h3=":443"; ma=86400
x-amz-cf-id: SzZucVEqwoOqnTjdkp6dSiwdO4jxwZm95CqN4sBg3OoWmNGGLThbpw==
x-xss-protection: 1; mode=block
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
strict-transport-security: max-age=31536000

________________________________________________________
Executed in    3.39 secs      fish           external
   usr time   23.17 millis    0.23 millis   22.93 millis
   sys time   25.52 millis    1.67 millis   23.85 millis

Miss from cloudfrontとあることからキャッシュヒットしなかったようです。コンテンツを返すのに3秒もかかっていますね。

再度同じパラメーターでアクセスします。

> time curl -I https://www.non-97.net/non__97.png -H "Accept:image/webp"
HTTP/2 200
content-type: image/webp
content-length: 5464
date: Sun, 02 Feb 2025 08:42:04 GMT
last-modified: Fri, 24 Jan 2025 03:43:34 GMT
etag: "54b0857ccfbab67746434fb9042aacff"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Hit from cloudfront
via: 1.1 fa9e00318667b610e39aa2c387f16a32.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT12-P1
alt-svc: h3=":443"; ma=86400
x-amz-cf-id: f-8Z4NJFibNiHBQRaOn2njz_vrDYqKSfZECsIlViFMXDWYp91Vd00w==
age: 3
x-xss-protection: 1; mode=block
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
strict-transport-security: max-age=31536000

________________________________________________________
Executed in  169.52 millis    fish           external
   usr time   22.78 millis    0.19 millis   22.59 millis
   sys time   19.17 millis    1.04 millis   18.13 millis

はい、キャッシュヒットしました。

Acceptヘッダーの値をimage/webp, testとしてアクセスします。

> time curl -I https://www.non-97.net/non__97.png -H "Accept:image/webp, test"
HTTP/2 200
content-type: image/webp
content-length: 5464
date: Sun, 02 Feb 2025 08:42:04 GMT
last-modified: Fri, 24 Jan 2025 03:43:34 GMT
etag: "54b0857ccfbab67746434fb9042aacff"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Hit from cloudfront
via: 1.1 da8c4d7ff604f51ba4f83ffed7115acc.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT12-P1
alt-svc: h3=":443"; ma=86400
x-amz-cf-id: lNuanoIiArvThyYkzF9RLEY6N49PHYkYD13K7jjsIvwMvkbLesr2tg==
age: 10
x-xss-protection: 1; mode=block
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
strict-transport-security: max-age=31536000

________________________________________________________
Executed in  154.43 millis    fish           external
   usr time   23.65 millis    0.19 millis   23.46 millis
   sys time   22.91 millis    1.06 millis   21.85 millis

Acceptヘッダーをキャッシュミスするとこと、キャッシュヒットしていますね。もちろん、コンテンツもWebPを返しています。

さらにAcceptヘッダーの値をimage/webp, test, 2としてアクセスします。

> time curl -I https://www.non-97.net/non__97.png -H "Accept:image/webp, test, 2"
HTTP/2 200
content-type: image/webp
content-length: 5464
date: Sun, 02 Feb 2025 08:42:04 GMT
last-modified: Fri, 24 Jan 2025 03:43:34 GMT
etag: "54b0857ccfbab67746434fb9042aacff"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Hit from cloudfront
via: 1.1 6f5c56b3519e8f4cd3e201cadf5f5b40.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT12-P1
alt-svc: h3=":443"; ma=86400
x-amz-cf-id: GSw8SzTbBT5lL06cFKqwK-6l1QI_qRT-oJL_vlCDqItYx5Lbp6lXUQ==
age: 25
x-xss-protection: 1; mode=block
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
strict-transport-security: max-age=31536000

________________________________________________________
Executed in  132.59 millis    fish           external
   usr time   15.81 millis  194.00 micros   15.62 millis
   sys time   19.48 millis  996.00 micros   18.49 millis

こちらもキャッシュヒットしていますね。

Acceptヘッダーにimage/webpを含めずにアクセス

Acceptヘッダーを付与していない場合はWebPではなく、PNGを返すことを確認します。

> time curl -I https://www.non-97.net/non__97.png
HTTP/2 200
content-type: image/png
content-length: 76942
date: Sun, 02 Feb 2025 08:42:41 GMT
last-modified: Fri, 24 Jan 2025 03:43:32 GMT
etag: "cac2eacf135495f8eb947890b6c84526"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Miss from cloudfront
via: 1.1 f2f4975292b62b8912a072e49f082cbc.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT12-P1
alt-svc: h3=":443"; ma=86400
x-amz-cf-id: y_jTiyFTKb1VT45kFZrjYMNqF39IpCT236xV9MgNqidcd1yafK9pGA==
x-xss-protection: 1; mode=block
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
strict-transport-security: max-age=31536000

________________________________________________________
Executed in    1.02 secs      fish           external
   usr time   20.54 millis    0.21 millis   20.33 millis
   sys time   19.46 millis    1.13 millis   18.33 millis

キャッシュヒットせずにPNGを返すことが分かります。

他のヘッダーが指定されている場合やimage/webpが含まれない場合もPNGを返すことも確認します。

> time curl -I https://www.non-97.net/non__97.png -H "test:test"
HTTP/2 200
content-type: image/png
content-length: 76942
date: Sun, 02 Feb 2025 08:42:41 GMT
last-modified: Fri, 24 Jan 2025 03:43:32 GMT
etag: "cac2eacf135495f8eb947890b6c84526"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Hit from cloudfront
via: 1.1 fa9e00318667b610e39aa2c387f16a32.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT12-P1
alt-svc: h3=":443"; ma=86400
x-amz-cf-id: Txeh9zJA79P5g6EvShBZimkmweR_9jp78PhU-BoishrXfixlhSpF8Q==
age: 14
x-xss-protection: 1; mode=block
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
strict-transport-security: max-age=31536000

________________________________________________________
Executed in  140.97 millis    fish           external
   usr time   17.90 millis    0.22 millis   17.68 millis
   sys time   26.45 millis    1.34 millis   25.11 millis

> time curl -I https://www.non-97.net/non__97.png -H "Accept:image/png"
HTTP/2 200
content-type: image/png
content-length: 76942
date: Sun, 02 Feb 2025 08:42:41 GMT
last-modified: Fri, 24 Jan 2025 03:43:32 GMT
etag: "cac2eacf135495f8eb947890b6c84526"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Hit from cloudfront
via: 1.1 f22f45735eceb3450fbe806ce121aab8.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT12-P1
alt-svc: h3=":443"; ma=86400
x-amz-cf-id: Han-6KT-EIz9vvy618HeQIgE9cPB7voV_uJYRf51E7xGwkoQDC2pTA==
age: 2224
x-xss-protection: 1; mode=block
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
strict-transport-security: max-age=31536000

________________________________________________________
Executed in  351.61 millis    fish           external
   usr time   25.41 millis    0.20 millis   25.20 millis
   sys time   22.42 millis    1.15 millis   21.27 millis

意図した動作ですね。

クライアント側でx-viewer-accept-webpヘッダーを付与してアクセス

続いて、クライアント側でx-viewer-accept-webpヘッダーを付与してアクセスした時の挙動を確認します。

> time curl -I https://www.non-97.net/non__97.png -H "x-viewer-accept-webp:true"
HTTP/2 200
content-type: image/png
content-length: 76942
date: Sun, 02 Feb 2025 08:42:41 GMT
last-modified: Fri, 24 Jan 2025 03:43:32 GMT
etag: "cac2eacf135495f8eb947890b6c84526"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Hit from cloudfront
via: 1.1 23bc6d6a912d17773e1bf97197cbfc1e.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT12-P1
alt-svc: h3=":443"; ma=86400
x-amz-cf-id: AkPDmff3H7mSA9V_4kRZw3LoHfNwU74_Dx0pgsmGlXvBVIscykWuSQ==
age: 41
x-xss-protection: 1; mode=block
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
strict-transport-security: max-age=31536000

________________________________________________________
Executed in  338.66 millis    fish           external
   usr time   26.68 millis    0.20 millis   26.47 millis
   sys time   23.13 millis    1.21 millis   21.92 millis

> time curl -I https://www.non-97.net/non__97.png -H "x-viewer-accept-webp:true" -H "Accept:image/webp, test, 2"
HTTP/2 200
content-type: image/webp
content-length: 5464
date: Sun, 02 Feb 2025 08:42:04 GMT
last-modified: Fri, 24 Jan 2025 03:43:34 GMT
etag: "54b0857ccfbab67746434fb9042aacff"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Hit from cloudfront
via: 1.1 0ef0d5d7817de0dbb2171006ac28bb0c.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT12-P1
alt-svc: h3=":443"; ma=86400
x-amz-cf-id: 5p3q7rBRzA1gyAgG7jFOOCNxDm3PmrpauG5t0-D2Kko4tv5BbkpFhQ==
age: 120
x-xss-protection: 1; mode=block
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
strict-transport-security: max-age=31536000

________________________________________________________
Executed in  141.13 millis    fish           external
   usr time   16.58 millis    0.19 millis   16.39 millis
   sys time   20.82 millis    1.04 millis   19.78 millis

> time curl -I https://www.non-97.net/non__97.png -H "x-viewer-accept-webp:false" -H "Accept:image/webp, test, 2"
HTTP/2 200
content-type: image/webp
content-length: 5464
date: Sun, 02 Feb 2025 08:42:04 GMT
last-modified: Fri, 24 Jan 2025 03:43:34 GMT
etag: "54b0857ccfbab67746434fb9042aacff"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Hit from cloudfront
via: 1.1 9b8a6e30994167e8de984036681d4ff6.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT12-P1
alt-svc: h3=":443"; ma=86400
x-amz-cf-id: WHoxz5deMdKe3lPh119vp6zz7lR9UoZwT3O50MdcvRRtf5YcSLdbpg==
age: 127
x-xss-protection: 1; mode=block
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
strict-transport-security: max-age=31536000

________________________________________________________
Executed in  151.70 millis    fish           external
   usr time   18.45 millis    0.19 millis   18.26 millis
   sys time   18.87 millis    1.09 millis   17.78 millis

> time curl -I https://www.non-97.net/non__97.png -H "x-viewer-accept-webp:false" -H "Accept:image/webp, test, 2, 3"
HTTP/2 200
content-type: image/webp
content-length: 5464
date: Sun, 02 Feb 2025 08:42:04 GMT
last-modified: Fri, 24 Jan 2025 03:43:34 GMT
etag: "54b0857ccfbab67746434fb9042aacff"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Hit from cloudfront
via: 1.1 8d094829a2df82945a7c7fbea18cea10.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT12-P1
alt-svc: h3=":443"; ma=86400
x-amz-cf-id: ldd4NOuHDY7zQBuYTFLDDw5Q2LWxD_qHA4XcHqtChzduyd_Fl9h-GA==
age: 138
x-xss-protection: 1; mode=block
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
strict-transport-security: max-age=31536000

________________________________________________________
Executed in  173.48 millis    fish           external
   usr time   22.89 millis    0.21 millis   22.68 millis
   sys time   20.49 millis    1.17 millis   19.32 millis

> time curl -I https://www.non-97.net/non__97.png.webp -H "x-viewer-accept-webp:false"
HTTP/2 200
content-type: image/webp
content-length: 5464
date: Sun, 02 Feb 2025 09:12:30 GMT
last-modified: Fri, 24 Jan 2025 03:43:34 GMT
etag: "54b0857ccfbab67746434fb9042aacff"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Miss from cloudfront
via: 1.1 f0499023f5cce9a24cc0ed91910c47ee.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT12-P1
alt-svc: h3=":443"; ma=86400
x-amz-cf-id: kCwa-wCA-0S-P_FamHRGty0jCaZMcPe58eHuluEEoqp0UmPA9UNtdg==
x-xss-protection: 1; mode=block
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
strict-transport-security: max-age=31536000

________________________________________________________
Executed in    1.11 secs      fish           external
   usr time   25.52 millis    0.22 millis   25.30 millis
   sys time   25.15 millis    1.26 millis   23.89 millis

はい、例えクライアント側でx-viewer-accept-webpヘッダーを指定していたとしても、CloudFront Functionsで設定し直す動きをするので、キャッシュを返すか否かに影響を与えません。

異なるブラウザでのアクセス

実際に異なるブラウザでアクセスしてみましょう。

キャッシュをクリアしてからChromeでアクセスします。

2.異なるブラウザでのアクセス_Chrome.png

この時のAcceptヘッダーはtext/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7です。

続いてFirefoxでアクセスします。

3.異なるブラウザでのアクセス_Firefox.png

この時のAcceptヘッダーはtext/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8ですが、キャッシュヒットしていることが分かります。Content-TypeヘッダーからもWebPであることも分かります。

意図した挙動をしていますね。

キャッシュヒット率を上げる際にはCloudFront Functionsを使ったNormalizationも検討しよう

CloudFront Functionsを使ってCloudFrontのキャッシュヒット率を向上させてみました。

キャッシュヒット率を上げる際にはCloudFront Functionsを使ったNormalizationも検討すると良いでしょう。

ただし、CloudFront Functionsは実行回数に対する課金が走ります。(100 万件の呼び出しあたり 0.10 USD)

CloudFront Functionsは現時点でViewer RequestかViewer Responseでしか動作することはできません。つまり、今回の場合はユーザーがWebページにアクセスして、裏側で画像ファイルを読み込む回数分だけCloudFront Functionsが実行されます。

そのため、画像ファイルが大量にあるWebサイトの場合、CloudFront Functionsの料金が負担になることも考えられます。仮にWebPの導入目的が転送量の削減による配信速度向上と転送量課金削減の場合、転送量課金削減の効果は感じづらくなるでしょう。

この記事が誰かの助けになれば幸いです。

以上、クラウド事業本部 コンサルティング部の のんピ(@non____97)でした!

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.