CloudFront の Cache Policy と Origin Request Policy を理解する

# はじめに

CloudFront の Management Console で Behavior を設定していると、こんな見慣れない機能が表示されるようになっていた。

![SnapCrab_NoName_2020-10-3_14-25-58_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502582/6fc523aa-5a7d-8a43-d5fb-6f05b000adcc.png)

これは何ぞや、と思って調べてみたら 2020/07 のアップデート内容のようだ。

[Amazon CloudFront キャッシュポリシーとオリジンリクエストポリシーを発表](https://aws.amazon.com/jp/blogs/news/amazon-cloudfront-announces-cache-and-origin-request-policies/)

かなり新しい機能で、まだ資料が少なかったので自分の理解のために従来と比較して何がうれしいのかをまとめてみた。

# TL; DR

- この機能の実装前はオリジンへのForwardとキャッシュキーの項目が自動的に一致していた
- Policyの実装によって、キャッシュキーとオリジンへの項目転送を分離してより柔軟で直感的なキャッシュルールを定義できるようになった
- 1度書いた設定を複数の Behavior で再利用できるようになった


# 1. CDN / CloudFront の仕組みの確認

## どのように動いているのか

CDN (Content Delivery Network) はコンテンツを安定して利用者に提供するために用いられるネットワークであると説明される。 しかし、当たり前だが、**CDNで提供されるコンテンツは提供者がCDNによって配信することを指定する必要がある。**
これを実現するために、CDNの背後には提供者がコンテンツを配信するサーバー (CDNの文脈の用語でオリジンサーバー) を配置し、**サービス利用者が直接オリジンサーバーを参照しない設定** が行われる。 そして、サービス利用者はコンテンツ配信を最適化するための実体 (キャッシュサーバー) からコンテンツを取得する。 もちろん、キャッシュサーバーは必要に応じてオリジンサーバーからコンテンツを取得するが、このキャッシュサーバーの設備を高機能化・地理的分散配置をすることでコンテンツ取得の高速化や障害分散を行うことができる。 図示すると以下の通り。

![cloudfront-article03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502582/1aa63960-3a4a-8a4e-fc4b-2a29ede93ca4.png)

CloudFront の場合、Edge Location が各地理別に配置されたキャッシュサーバー(群)である。 利用者(ビュワー)にとって一番都合のいい (故障していない、ネットワーク的な距離が近い) エッジロケーションが DNS によって自動的に選ばれ、利用者が快適にコンテンツを取得できるようにしてくれている。 さらに言えば、**オリジンサーバーへの問い合わせが減り、提供者側インフラの負荷が軽減されるようになる。** サービス提供のインフラ構成視点から見れば、これは大きなメリットである。

上記の説明だけで分かりづらい場合、以下のページでの説明が分かりやすいので併せて読むと良い。

[【図解】CDNとは？仕組みと技術の基礎知識](https://www.kagoya.jp/howto/network/cdn/)


## CloudFront の基本的な活用方針

上記の仕組みを見たとき、コンテンツに更新が無ければ何の問題もない。 しかし、コンテンツの内容は更新・変更される。 その場合は「1度CloudFrontに保存されているコンテンツを破棄し、新しいコンテンツをCloudFrontに登録する」という処理を行わなければならない。 開発者からすると「キャッシュをクリアして、新しいコンテンツを再取得（と再キャッシュ）する」と書いた方が分かりやすいかもしれない。 これをCloudFrontで実現するには２つの方法がある。

- コンテンツが有効な生存時間を設ける
- 手動でキャッシュをクリアする (Invalidations)

オリジンサーバーから提供されるコンテンツには変更の頻度が高いものも、低いものもそれなりにある。 また、全てを手動でクリアしていてはとてもではないがサービスを運用できないため、ほとんどの場合は有効な生存期間を「オリジンサーバーからCDNへの指示」あるいは「CDNそのものの設定」で制御することでコンテンツの更新に対応している。

ここで改めて仕組みを思い出して欲しいのだが、有効なコンテンツがCDN上に無い場合は **オリジンサーバーへの問い合わせを行い、コンテンツを取得する。** この回数が多くなると、CDNを挟んでいるにも関わらず、オリジンサーバーへ直接アクセスしているのと変わらない状態になってしまう。 仮に全てのコンテンツがキャッシュされないのであれば、オリジンサーバーへ直接アクセスするよりも、間にCDNの仕組みを挟んでいる分オーバーヘッドなどがある分逆効果になってしまう。

この事から、**CloudFront (CDN) を効率的に使うためには、コンテンツをうまくキャッシュしつつ最新のコンテンツを利用者に提供する** ことが重要であることが分かる。 一般に利用者からのアクセスに対してCDNがキャッシュを返すことを、キャッシュヒットなどと呼び、総アクセス数に対する割合を **キャッシュヒット率** と呼ぶ。 このキャッシュヒット率はCDNをうまく使えているかをはかる指標の１つである。


## キャッシュすべきではないコンテンツ

一方、キャッシュヒット率を上げることだけを考えてコンテンツをキャッシュしてしまうと「システムで表示された個人情報を不特定多数に公開してしまうこと」もあり得る。 事例や経緯などを詳しく説明してくれている事象にメルカリでの個人情報流出のケースがある。

[CDN切り替え作業における、Web版メルカリの個人情報流出の原因につきまして](https://engineering.mercari.com/blog/entry/2017-06-22-204500/)

ログインした状態でDBから取得したユーザー情報を HTML に直接書き出すような仕組みを採用している場合、リクエストパラメータ (Cookieやヘッダを含む) の値によってレスポンスが大きく変わる場合 (例: 適切なセッション情報がないとリダイレクトが発生するような場合)は、それらがキャッシュされることでサイトに不適切な情報が表示されてしまったり、正常に動かなくなってしまったりする。

**CDNを利用する場合、CDNの特性を理解してこれらの事故を引き起こさないような設定・検証を十分に行うことが重要である** 。

## CloudFront のパス別振り分け機能 (L7 Load Balancing)

CloudFront はアクセスするパス事に個別のオリジンサーバーを指定することが可能になっている。 そのため、よくある構成として、

- `/img/*` や `/video/*` などの静的コンテンツは S3 に配置して S3 Bucket をオリジンにする
- それ以外のコンテンツはアプリケーションサーバーに配置して、アプリケーションサーバーをオリジンにする
- API Gateway をオリジンに指定して、CORSの問題を考えずに1つのドメイン上で動かすサービスとして扱えるようにする (参考記事: https://dev.classmethod.jp/articles/spa-cloudfront-and-api-gateway-voiding-cors/ )

など、サービスの統合点としての利用が可能になっている。 Certification Manager と連携して無料・自動更新のSSL証明書をCloudFrontに配置できるため、この仕組みを利用することでサービスのSSL/TLS化が可能になる。 

また、「アプリケーションサーバーをオリジンに指定して、ここでは全くキャッシュを利用しない」「静的コンテンツをS3に配置し、ここに対してのみキャッシュを利用する」といったように、静的ファイルだけを効率よく取得するといった簡単な L7 Load Balancing のためだけに CloudFront を使うこともできる。 これに何の意味があるのかと思うかもしれないが、静的ファイルは一般にデータサイズが大きい。そして、AWS で ALB を通した構成を組む場合、ALBでの処理データ量に応じた課金要素が存在するし、EC2インスタンスにデータアウトプットに対する不要な負荷が発生する。 それらの問題解決のためだけにも、この構成を採る価値はある。


## 特定パスのコンテンツ閲覧制限機能

CloudFront で指定した特定のパス以下を、署名付きCookieがないと閲覧できないようにできる。 署名付きCookieは AWS API を使って発行する。
この機能を使うことで「利用者に対して効率の良い配信手段であるCDNを利用する」「ログイン情報などがないユーザーにはコンテンツを提供しない（＝CDNを利用した会員制サイトの作成）」というシナリオを実現することができる。
具体的なやり方は以下の説明を参照のこと。

[署名付き Cookie の使用](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/private-content-signed-cookies.html)



# 2. CloudFront でのキャッシュの設定

## キャッシュの生存期間

次に、CloudFront でのキャッシュ指定の方法についてを見ていく。 CloudFront では **CloudFrontでの設定** と **オリジンからのレスポンスに含まれるヘッダ情報** を元にしてどの程度の期間データをキャッシュするかを決定する。

まず、キャッシュ可能となるリクエストは **GET, HEAD, OPTIONS のみ** である (OPTIONSは選択制)。 コンテンツに影響を与える PUT や POST などはキャッシュしないため、これらは考えなくても良い（言い換えると、アプリケーションの作りでPOSTを多用している場合、それらにはキャッシュが効かないということでもあるが…)。

保持期間の仕様について、詳細は以下のページで公開されている。

[コンテンツがエッジキャッシュに保持される期間の管理 (有効期限)](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/Expiration.html)

簡単にまとめると、

- HTTP レスポンスの `Cache-Control` および `Expires` ヘッダーによって指定する
    - Cache-Control の `max-age` で何秒有効かを指定
    - Cache-Control で `max-age` がなく、 `Expires` が指定されている場合は Expires の期間まで有効であることを指定
        - [両方指定されている場合は `max-age` のみが有効] (https://aws.amazon.com/jp/premiumsupport/knowledge-center/cloudfront-cache-files-time/ )
    - Cache-Control に `no-cache` , `no-store` , `private` ディレクティブが含まれる場合は **基本的に** キャッシュしない (後述)
- CloudFrontの設定でレスポンスにこれらのヘッダの情報が含まれない場合、あるいは、範囲外になる数値を強制的に指定できる
    - つまり、 **レスポンスでの指定をCloudFrontの設定で上書きできる**
    - CloudFront では TTL (Time To Live) で「コンテンツが何秒有効か」を指定する
    - `Minimum TTL` <= レスポンスで指定された生存期間 <= `Maximum TTL` であればそのまま利用
    - 範囲外であれば Minimum と Maximum の近い方に寄せられる
    - 指定がない場合は Default TTL が利用される
    - CloudFrontの設定優先なので `Minimum TTL > 0` の場合は Cache-Control に `no-cache` , `no-store` , `private` ディレクティブが含まれていても **設定優先でキャッシュする(!)**

なので、オリジンのことを何も知らない場合であっても、CloudFront の設定のみでキャッシュの生存期間はある程度コントロールが可能であるともいえる。 2020/07 以前の CloudFront の設定では、各 Behavior で Customize を設定することで、これらの TTL の強制数値を指定できていた (図参照)。

![SnapCrab_NoName_2020-10-3_17-2-48_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502582/c70fe760-5399-2714-4ad3-ef40ef8f653a.png)


その他、記事でまとめてくれている方々を紹介しておく。

- https://michimani.net/post/aws-about-cache-spec-of-cloudfront/

## CloudFront キャッシュのキーについて

以下のリクエストは全てオリジンサーバーの同一コンテンツ( `/index` )にアクセスするものである。 が、オリジンサーバーから見た場合はこれらを同一として扱いたい場合も、扱いたくない場合もある。 これら全てのアクセスで同一のコンテンツを返すのであれば、CDNからキャッシュを返すようにしたいし、異なるコンテンツを返すのであれば、 **別のリクエストのキャッシュを返してはいけない** 。

- A. `curl https://www.example.com/index` (単純なGETアクセス)
- B. `curl https://www.example.com/index?s=HOGE` (クエリ付きGETアクセス)
- C. `curl -H 'X-Auth: 12345678' https://www.example.com/index` (Headerに情報が入った状態でのアクセス)

では、これらのアクセスが「同じ」か「違う」かをCloudFrontでどのように判断するのか。　CloudFront は以下の手順で利用者へのコンテンツ配信を行う。

1. **リクエストを元にして、キャッシュキーを生成する**
2. そのキーを使って有効な(生存期間内の)キャッシュオブジェクトが存在しているかをチェックする
3. キャッシュを発見できた場合、そのキャッシュを返す
4. キャッシュを発見できなかった場合、オリジンサーバーに問い合わせを行う
5. オリジンサーバーからのリクエストを設定に従ってキャッシュする。 この時のキャッシュキーは 1. と同じものが使われる

仕組みをよく見れば分かるが、**キャッシュキーの生成手段を変更することで、上記の同一コンテンツのアクセスは同じものか、別のものかを区別できる。** コンテンツ ( `/index` ) だけを見てキャッシュキーを生成するのであれば、上記の3つは同じキーを生成するので同一のキャッシュ領域を共有するし、コンテンツ ( `/index` ) と X-Authヘッダの中身 (ヘッダがない場合は ""と等価とする) の2要素でキーを生成するようにすれば、「A, B」「C」でそれぞれ個別のキャッシュ領域を参照するようになる。

CloudFront のキャッシュキーの生成に使える項目は「コンテンツ」「Cookie」「HTTP Header」「クエリパラメータ」の４つである。 が、次の項目 (オリジンへの転送) が絡んでくるため、これらをぱっと見だけで理解することは難しかったと思われる。


## オリジンサーバーへのリクエスト転送

CloudFront からオリジンサーバーにデータ取得のリクエストを送る場合、**デフォルト設定だと Cookie, Header, クエリパラメータがオリジンサーバーに転送されない。** 
解決策としては、これらそれぞれをCloudFrontからオリジンに転送 (Forward) するオプションがあるので、これらを設定する必要がある。

- [CloudFrontで「Forward Cookies」を「All」にしている時に注意すべき点](https://dev.classmethod.jp/articles/sugano-007-cloudfront/)
- [CloudFront でのクッキーの扱いについて](https://qiita.com/koseki/items/41c777194bc89d68f15f)
- [CloudFrontのForward Cookiesの設定を色々試してみた](https://blog.kozakana.net/2020/03/cloudfront-forward-cookies/)
- [ホストヘッダーをオリジンに転送するように CloudFront を設定する方法を教えてください。](https://aws.amazon.com/jp/premiumsupport/knowledge-center/configure-cloudfront-to-forward-headers/)


最初3つのリンク先では旧設定の「Forward Cookies」についてを記載しているがこれが重要で、**旧設定を利用する場合、Forwardした項目がそのままCloudFrontのキャッシュキーとして利用される** という仕様がある。
これらのリンク先でも説明されているが、何も考えずに「Forward Cookies: ALL」とした場合、個別の値が入った段階でCloudFrontでのキャッシュが効かない、といった問題が起こる。具体例としては [最初のリンク先で説明されている通り](https://dev.classmethod.jp/articles/sugano-007-cloudfront/) Google Analyticsを利用していると、CookieにGA用のユニークな値が埋め込まれるので、これがCloudFront に渡ってしまうとキャッシュが全く効かないといったことが挙げられる。 それ以外にも「ログインしなくてもセッションIDを発行するタイプのサイト」や「ユニークなCSRFトークンを発行してCookieに埋め込むサイト」などではキャッシュが全く効かないといったことが起こり得る。
その他にもアプリケーション側で「利用者分析などのためにUser-Agentを記録したい」「ただし、User-Agentの値に関わらず同一のコンテンツを返す」といったケースの場合、旧設定ではUser-Agentをオリジンに転送すると、User-Agentの値が自動的にキャッシュキーに含まれてしまっていた。 そのため、効率的なキャッシュができないという問題があった。

このように、旧設定では「キャッシュのキー算出」と「オリジンへの項目転送の設定」を個別に設定できないという問題があり、また、キャッシュのキーについてを認識しづらいという副次的な問題もあった。 しかし、新しいポリシー機能により、これらが分離できるようになった。


# 3. Cache Policy と Origin Request Policy

以上の仕様と背景を知った上で Cache Policy と Origin Request Policy についてを見ていく。

![SnapCrab_NoName_2020-10-3_14-25-58_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502582/6fc523aa-5a7d-8a43-d5fb-6f05b000adcc.png)

なお、これまでは Behavior 単位で個別に設定する必要があったが、Policy の実装によって同一内容を再利用することが容易になった。

## Cache Policy

**キャッシュの保持期間** と **キャッシュキー** についてを設定する。 Forward の設定とは関係ない。
初期から設定されている `Managed-CachingOptimized` を見てみると以下の設定になっている。

![SnapCrab_NoName_2020-10-3_17-48-42_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502582/f1fda0f6-5472-9391-a87e-584dbf4e2206.png)

キャッシュキーの期間は `TTL Settings` で。 キャッシュキーの設定は `Cache Key contents` でそれぞれ行う。 キーの設定はここでは全て None だが、Forward と同様に `None`, `All`, `Whitelist` (利用するキーを指定)、`All-Except` (利用しないキーを指定) が指定できる。 ただし、ヘッダは `None`, `Whitelist` のみ。 

なお、「`TTL Settings` を全て0にする」＝「キャッシュを利用しない」＝「キャッシュキーを利用しない」と認識されるので、その時点で `Cache Key contents` は表示されなくなる。

……ただ、よくみると、このデフォルト設定だと **`Minimum TTL` が 1 なので、オリジンから `no-cache` 指定があっても1秒キャッシュされてしまう。** デフォルトでこれが指定されているので、もしキャッシュしてはいけないコンテンツを配信対象とする場合は要注意。

## Origin Request Policy

**CloudFrontからオリジンサーバーにリクエストを送る場合に、利用者から送信された Header, Cookie, クエリを付与して送信するか否かを設定する。** 追記参照だが、Cache Policyで指定している各値に加えて、ここで定義された値をオリジンサーバーに転送する。
例えば、AWSから提供されている `Managed-UserAgentRefererHeaders` ポリシーを利用すると、(Cache Policy で User-Agent ヘッダをキーに指定していなくても) 利用者から送信された `User-Agent` をオリジンサーバーへと転送することができる。 

![SnapCrab_NoName_2020-10-3_18-3-44_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502582/29956f09-de67-3f67-1042-44189bb2c16c.png)

### 2021.02.11追記 

仕様として重要な点が抜け落ちていたので追記。
以下にある通り、Origin Request Policy に設定していない値であっても Cache Policy に含まれている値は自動的にオリジンサーバーへと送信される。 そのため、ここは「キャッシュキーには使わないが、オリジンサーバーに送信したい値」を定義する。

> 2 種類のポリシーは別々のものですが、関連性があります。キャッシュキーに含めるすべての URL クエリ文字列、HTTP ヘッダー、および Cookie (キャッシュポリシーを使用) は、オリジンリクエストに自動的に含まれます。オリジンリクエストポリシーを使用して、オリジンリクエストに含めるが、キャッシュキーには含めない情報を指定します。
> https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/controlling-origin-requests.html

なお、ここの仕様によって [Authorization ヘッダなどは事故を防ぐために Origin Request Policy には設定できない](https://qiita.com/t-kigi/items/6ea9d2656c49b8e40af4) ようになっている。


# まとめ

CloudFrontに新機能 Cache Policy と Origin Request Policy が実装されたので、これらを理解するため、CloudFrontの既存の機能・設定を見ていき、新しく実装された Policy の機能と有用性を確認した。
執筆時現在(2020/10)時点では、まだまだ古い設定での記事が多くヒットするため、新しい機能を使う人向けの説明として機能してくれれば幸いである。