CloudFront キャッシュポリシーの Minimum TTL = 0/1 は大きく違う

# はじめに

https://qiita.com/t-kigi/items/6d7cfccdb629690b8d29

こちらの記事で @XAU さんから以下の補足を頂いた。

>> CloudFrontの設定優先なので Minimum TTL > 0 の場合は Cache-Control に no-cache , no-store , private ディレクティブが含まれていても 設定優先でキャッシュする(!)

> この場合、基本はオリジンを参照しますが、オリジンに接続不能な場合にキャッシュを返す動作となるようです。
以下ご参考ください。

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/Expiration.html#stale-if-error

この内容を提供いただいてから、CloudFront がデフォルトで提供している管理ポリシーの CachingOptimized ポリシーに Minimum TTL が 1 に設定されているかが見えた気がしたため、その内容についてを記事にした。

# 管理キャッシュポリシー: CachingOptimized

以下で説明されている。 

> このポリシーは、CloudFront がキャッシュキーに含める値を最小限に抑えることで、キャッシュ効率を最適化するよう設計されています。

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

はじめて見た時は、「なぜ Minimum TTL が 1 なのか？」と疑問に思った。 
Minimum TTL が 1 なのでたとえ `Cache-Control: no-cache` がついていてもキャッシュが生成されて情報流出の危険があるのではと思い、普通は Minimum TTL = 0 として no-cache はキャッシュしないようにした方がよいのでは、と考えたためである。

しかし、まさに答えがここにあり、Minimum TTL = 0 の場合、no-cache が付与されているコンテンツに対しては CloudFront はキャッシュを生成しない、というところが重要になってくる。 キャッシュを生成する場合、CloudFront は取得できたコンテンツだけではなく、ETag などの情報も併せて保存する。 そして、この情報を TTL 切れの時にオリジンリクエストにあわせて送付することにより、**オリジンサーバーがリソースを変更していない場合、304 レスポンス (Not Modified) を返してくることが期待できる。**  304 が返ってきた場合、保持しているコンテンツに変更はないのだから、今保持しているキャッシュをそのまま返せることを意味している。 これにより、オリジン - CDN 間の通信量を少なくするだけでなく、CDN を経由した通信が高速化されることが期待できる。
一方、CDN がキャッシュを保持していない場合、ETag などの情報を持たないため、CDNからの問い合わせに対してオリジンサーバーは 200 とコンテンツを返す。 これは通常想定されるフローであるが、大量アクセスがあったり、巨大な変更のないファイルを扱うことを考えると、その差がしっかりと出てくると考えられる。

このことから、CachingOptimized を使うというのは「オリジンから取得するコンテンツがどのようなものであってもキャッシュを生成し、通信の効率化をはかる」ことを宣言しているとも考えられる。 効率が良いことは確かに良いことではあるが、キャッシュ事故が怖いのでデフォルトになっているのは変わらず首をひねるところ。

ちなみに、TTL 切れでオリジンから 304 が返ってきた場合、CloudFront からのレスポンスヘッダーには RefreshHit from CloudFront という値が含まれる。 これは上記の流れから、過去に使ったキャッシュを (CDNの設定的に) 使いまわしていますよ、ということを表している。

## オリジンへの通信ができなくなっている時の挙動

運用をしていると、CloudFront は正常だが、サーバー（オリジン）がダウンしてしまい、コンテンツを提供できなくなるというときがある。 この時、一般的な CDN の挙動としては「オリジンがダウンしていてもコンテンツを提供しつづける」というものがある。
しかし、この挙動を実現させるためには CDN にキャッシュが残っていることが前提となる。　そのため、先に頂いた URL 先の、

> オリジンとの接続が可能な場合、CloudFront はオリジンからオブジェクトを取得し、ビューワーに返します。
> オリジンが接続不能で、さらに最小 TTL が 0 より大きい場合、CloudFront は、先にオリジンから取得済みのオブジェクトを返信します。
> https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/Expiration.html#stale-if-error

という分についてをまとめると、

- オリジンに接続不能で CloudFront がキャッシュを持っている場合、そのキャッシュを返す
- オリジンに接続不能で CloudFront がキャッシュを持っていない場合、ユーザーに返せるコンテンツがないのでエラーを返す

ということを言っているだけである。　一方、

> CloudFront が、Cache-Control: no-cache、no-store、および (または) private ディレクティブを含むオリジンからオブジェクトを取得した後、同じオブジェクトに対する別のビューワーリクエストを受け取った場合、CloudFront はこのビューワーリクエストを処理するためオリジンと通信します。
> (英語版) If CloudFront gets an object from the origin that includes the Cache-Control: no-cache, no-store, and/or private directives, and then later CloudFront gets another viewer request for the same object, CloudFront tries to contact the origin to fulfill the viewer request.

これは、**Minimum TTL = 0** の場合の挙動となる。 Minimum TTL > 0 の時でキャッシュ期間中は、例えキャッシュ済オブジェクトに `Cache-Control: no-cache` (など) が付与されていても、キャッシュが有効な間はオリジンへのアクセスは行われない挙動を示した。[^1]

そのため、この部分はヘルプとして不親切な書き方になっていると思う。[^1]

[^1]: あくまでこちらで挙動を確認した限り。 もし Minimum TTL > 0 でキャッシュ期間中に関わらず常にオリジンへのアクセスを行う方法があるなら教えて欲しい。

簡単に検証した結果を以下のマトリクスに示す。 なお、環境は EC2 + nginx のサーバーをオリジンとして、レスポンスに `Cache-Control: no-cache` を付与している。

| 状況 | Minimum TTL | 挙動 |
|---|:-:|---|
| 常時 | 0 |  CloudFront はコンテンツをキャッシュせず、キャッシュ期限切れの時にオリジンにアクセスして 200 とコンテンツを返す |
| 常時 | 10 | CloudFront はコンテンツをキャッシュし、キャッシュの有効期限内の時はオリジンにアクセスを行わない。 キャッシュ期限切れの時にオリジンにアクセスして 304 リクエストを返す (※もちろん、コンテンツの変更が行われていない場合)  |
| オリジンダウン | 0 |  CloudFront は 502 を返す |
| オリジンダウン | 10 | 有効期限が切れていても最後に取得したキャッシュを 200 で返す。 この時、レスポンスの age が CloudFront で設定した TTL 以上になることもあり、RefreshHit from CloudFront が付与される。 |

そのため、「オリジンがダウンしてもウェブサイトのページを表示させ続けたい」といった要望がある時、その対象ページがちゃんとキャッシュされる設定になっている（例: Cache-Control で制御して CDN にキャッシュさせる、アプリケーション事由により no-cache がついているが、キャッシュしても良いので Minimum TTL > 0 を設定する、など）ことを確かめておく必要がある。


## RefreshHit from CloudFront について

RefreshHit from CloudFront がヘッダに表示されていると何か問題がある、という印象だったのだが、これはオリジンダウン時にその時点での最新キャッシュを再利用している時に表示される。
しかし、オリジンコンテンツが 304 を返して古いキャッシュを再利用する場合にも同様に RefreshHit from Cloudfront が表示される。

そのため、このメッセージが出ている = 問題がある、というわけではなく、メッセージ通りに「理由があって古いキャッシュを再利用していますよ」ということを示している。

# まとめ

- CloudFront でキャッシュを持つと、オリジンから 304 (Not Modified) が返ってくることが期待できるようになり、より効率的な通信が行える
- Minimum TTL が 1 (以上) のキャッシュポリシーを使うことで、強制的にキャッシュを使った通信の効率化が実現できる
- CloudFront を利用してオリジンがダウンしてもページを表示させ続けたい時は、コンテンツが CDN にキャッシュさせる設定を行う
  - Minimum TTL > 0 である CacheOptimized ポリシーを使えば強制的に対処できる
- **キャッシュしてはいけないコンテンツを提供するパス以下ではちゃんと Minimum TTL = 0 のキャッシュポリシーを使うこと！！ (ここでは書いてないが、重要なので再掲)**