【CloudFront】キャッシュ設計指針を整理してみる

# 想定読者
CloudFrontの役割、目的は理解している人。
キャッシュポリシーをなんとなくで決めてしまっている人。

# キャッシュヒットのしくみ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/215c4dbf-4ba7-08e1-0129-1dec6ef92322.png)

ビューワーリクエスト（ユーザからのリクエスト）のがエッジロケーションに到達すると、リクエスト内容に紐づいた**キャッシュキー**を生成します。

一方、エッジロケーションが保持するキャッシュ内の各オブジェクトにも、固有のキャッシュキーが払い出されます。

つまり、リクエストから生成されるキャッシュキーと、エッジロケーションが保持するキャッシュオブジェクトのキャッシュキーが一致するかどうかで、キャッシュヒットの発生有無が変わるということになります。

このキャッシュヒット率を高めるために、キャッシュキーに含める要素をどのように設計していくかが重要になります。

# キャッシュ設計
デフォルトでは以下の要素に基づき、キャッシュキーが生成されます。
- ディストリビューション名
- オブジェクトパス

リクエストパスごとにキャッシュを生成するシンプルな方法です。

キャッシュキーに含む要素を追加で管理するには、キャッシュポリシーを利用します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/d2cf21ab-a8c8-c3cf-739f-40165f0ec8b4.png)
リクエストヘッダ、クエリ文字列、cookieごとに含める要素を指定します。
上記で対応できない複雑なユースケースでは、別途Lambda@Edgeを用いてカスタマイズするとよいでしょう。

### １．リクエストヘッダ
ユーザのデバイスタイプや場所、ヘッダー構造等リクエストの特性に応じてキャッシュ設計する場合に利用します。
主な要素は以下の通り。

- デバイスタイプ

|# |ヘッダ  |判断条件  |
|---|---|---|
|1  |CloudFront-Is-Android-Viewer  |AndroidOSからのリクエストであるか  |
|2  |CloudFront-Is-IOS-Viewer  |IOSからのリクエストであるか|
|3  |CloudFront-Is-Desktop-Viewer  |デスクトップからのリクエストであるか|
|4  |CloudFront-Is-Mobile-Viewer  |モバイルからのリクエストであるか|
|5  |CloudFront-Is-SmartTV-Viewer  |スマートTVからのリクエストであるか|
|6  |CloudFront-Is-Tablet-Viewer |タブレットからのリクエストであるか|
- 場所

|# |ヘッダ  |判断条件  |
|---|---|---|
|1  |CloudFront-Viewer-City  |ビューワーの市区町村名  |
|2  |CloudFront-Viewer-Country-Name  |ビューワーの国名|
|3  |CloudFront-Viewer-Country-Region |ビューワーのリージョンを表すコード|
|4  |CloudFront-Viewer-Country-Region-Name  |ビューワーのリージョン名|
|5  |CloudFront-Viewer-Latitude  |ビューワーのおよその緯度|
|6  |CloudFront-Viewer-Longitude |ビューワーのおよその経度|
|7  |CloudFront-Viewer-Metro-Code |ビューワーのメトロコード(米国のみ)|
|8  |CloudFront-Viewer-Postal-Code |ビューワーの郵便番号|
|9  |CloudFront-Viewer-Time-Zone |ビューワーのタイムゾーン|
- その他

|# |ヘッダ  |判断条件  |
|---|---|---|
|1  |CloudFront-Forwarded-Proto  |ビューワーのリクエストのプロトコル |
|2  |CloudFront-Viewer-Http-Version   |ビューワーのリクエストのHTTPバージョン|
|3  |Accept|ビューワーの理解できるデータ形式|
|4  |Authorization  |認証情報とメカニズム|
|5  |Host  |送信先のホスト名とポート番号|
|6  |Origin |オリジンのホスト名とポート番号|
|7  |Referer |現在リクエストされているページへのリンク先を持った直前のウェブページのアドレス|
|8  |Access-Control-Request-Method |実際のリクエストを行う前にブラウザが送信する事前フライトリクエストに含まれる。ブラウザが実際のリクエストで使用するHTTPメソッドをサーバーに伝えるためのもの|
|9  |Access-Control-Request-Headers |実際のリクエストを行う前にブラウザが送信する事前フライトリクエストに含まれる。ブラウザが実際のリクエストで使用するHTTPヘッダーをサーバーに伝えるためのもの|

### ２．クエリ文字列
クエリ文字列をキャッシュキーに含めるかを設定できます。
クエリ文字列を明示的に指定して含めることも可能です。
### ３．cookie
考え方はクエリ文字列同様。

# キャッシュ設計指針
設計をする際は以下の二点を考慮すべきかと思います。
- キャッシュキーに含める要素は最小限に
関係のない要素をキャッシュキーに含めてしまうと、同じオブジェクトに対し複数のキャッシュキーを生成することになります。

- アプリケーションの仕様をきちんと理解する
要素を最小限にすると言えど、必要な要素を逃してしまっては元も子もありません。

例として、CloudFront+API Gateway+Lambdaから構成されるバックエンドを考えます。
Lambdaは、クエリ文字列を表示する、シンプルなものとします。
```
export const handler = async(event) => {
    const query = event.queryStringParameters.testQuery;

    const response = {
        statusCode: 200,
        body: `クエリ文字列は${query}です。`,
    };
    return response;
};

```
この時以下の通り、キャッシュポリシーにクエリ文字列を含めていればアプリケーションは意図した通りに動きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/88af4d50-7f17-c239-4956-26f68d7045c3.png)
きちんと動きます。もちろん初めて指定するクエリ文字列の値に対しては該当するキャッシュキーが存在しないので「Miss From CloudFront」となります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/168f36b6-1852-3062-d4d2-1b354a823519.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/a8b7d0da-6067-0230-dca7-a6c67f980bd7.png)

続いてキャッシュポリシーからクエリ文字列を除いてみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/71c25ca7-97f6-a01c-9f7a-f5e33f3aa897.png)
前述した通り、デフォルトで指定されるURLパスによってのみキャッシュされるため、クエリ文字列の値を変更したとしてもレスポンスには反映されません。（意図しないキャッシュオブジェクトがレスポンスされます。）

検証環境では、クエリ文字列指定しない状態でキャッシュを作成しているためか、クエリ文字列の値を変更しても、そもそもエラーとなりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/85313dca-1033-58b6-117a-b8e0db44f95a.png)

# さいごに
キャッシュ設計次第でアプリケーションの意図しない動作を誘発してしまうため、アプリケーションチームとの連携の重要性がいかに重要か再認識できました。
リクエストボディの内容もキャッシュキーに含める場合は、Lambda@Edgeにて処理する必要があるのだろうか。。