RailsのレスポンスをCloudFrontで一定期間キャッシュする方法

# はじめに
弊社のサービスの中には、数秒間で数千人〜数万人のアクセスが来るサービスがあります。
applicationサーバーにECS、キャッシュサーバーにElastiCacheを使っているものもあり、そうなると、サーバーの負荷・サーバー台数を増やすことによるAWSコストの問題がどうしても上がってきます。

今後はサーバーレス化という視野ももちろんありますが、
特定の条件化では、
`CloudFrontのキャッシュを利用することでECSやElastiCacheにそもそもリクエストが行かないようにする`
という戦略も有効と思いますので、今回の記事を書くに至りました。

# 条件
今回の条件は以下です。
- 一定時間の間、固定の値を返すだけのAPI（ステータスとして公開状態を返すAPI）
- 数秒間で数千人〜数万人のアクセスが来る画面では、上記APIのみ実行する
- これに耐えるため、公開状態はElastiCacheで保持している（ECS経由）
- ECSを経由するため、ECSのタスク数を増やす必要がある
- ElastiCache自体も負荷に耐えうるスペックにしないといけない

# 方法概要
Cache-Controlヘッダを利用します。
Cache-Controlヘッダは、CloudFrontなどのCDNや、ブラウザに対してキャッシュ可否・有効期限などを指示することができます。

Cache-Controlヘッダについて詳しくは以下ご欄ください。

https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Cache-Control

今回実施する方法を簡単に言うと、以下です。
- CloudFrontでCache-Controlヘッダを利用するキャッシュポリシーを設定する
- RailsのレスポンスでCache-Controlヘッダのmax-ageディレクティブを指定する

# 事前説明
ここで、もう少し事前の説明をさせていただければと思います。

### ヘッダーを使用して個々のオブジェクトのキャッシュ保持期間を制御する方法
概要を簡潔に説明すると、以下です。

- Cache-Control および Expires ヘッダを使用し、制御する
- Cache-Control max-ageディレクティブを用いて、CloudFront がオリジンサーバーからオブジェクトを再度取得するまでの間の、キャッシュに保持する期間 (秒単位) を指定できる
- 3600秒（1時間）に設定したい場合は、`Cache-Control: max-age=3600` とする
- Expiresヘッダでは、`Sat, 27 Jun 2015 23:59:59 GMT`のように有効期限切れ日時を指定できる

もう少し深く知りたい方は、[こちら](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/Expiration.html#expiration-individual-objects)を参照いただければと思います。

今回はCache-Control max-ageディレクティブを用います。

### CloudFront がオブジェクトをキャッシュする期間を指定する方法
概要を簡潔に説明すると、以下です。
- CloudFront ディストリビューションのキャッシュ動作の TTL 値に、最小、最大、およびデフォルトの値を設定する
- オリジンからの応答に Cache-Control または Expires ヘッダーを含める

TTL設定値と、Cache-Controlヘッダのmax-ageの値には、
どちらの値が優先されるか？という関係性があります。

もう少し深く知りたい方は、[こちらの表](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/Expiration.html#ExpirationDownloadDist)にまとまっているのでご確認いただければと思いますが、Cache-Control: max-ageのみを含めるパターンについて簡潔に説明すると、以下となります。

- 最小TTL ＜ max-ageの値 ＜ 最大TTLであれば、max-ageの値が適応される - ①
- max-ageの値 ＜ 最小TTLであれば、最小TTLの値が適応される - ②
- 最大TTL ＜ max-ageの値であれば、最大TTLの値が適応される - ③

今回は、Railsのレスポンスからmax-ageを指定し、キャッシュ操作をしたいので、
①のパターンになるように、TTL設定、max-age設定がされていればOKです。

# 設定
それでは実際に設定をしていきます。

# CloudFront
特定のAPIのみキャッシュを効かせるようにしたいので、
まずはビヘイビアでパスパターン、オリジンを設定します。

## ビヘイビア
![スクリーンショット 2024-10-17 14.41.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730383/c55224e7-e6c4-c155-f7f3-f17605551f9b.png)


## キャッシュポリシー
キャッシュポリシーとしては、RecommendedされているUseOriginCacheControlHeadersを指定します。今回はクエリパラメータに応じてキャッシュ可否のハンドリングを行う訳ではないので、下部に表示されている注意書きは無視します。
オリジンリクエストポリシーに関しても同様にRecommendedされているAllViewerを指定します。

![スクリーンショット 2024-10-17 14.41.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730383/89fa3fc0-032a-4d41-0861-d26931ae4859.png)

UseOriginCacheControlHeadersの設定を見ると、TTL設定は以下のようになっています。

![スクリーンショット 2024-10-17 14.42.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730383/9e50ff16-c3e1-690d-3a96-5a9b862c9222.png)



#### TTLとmax-ageどちらが優先されるか？

- レスポンスのCache-Controlヘッダのmax-ageの値は10秒で指定
- 最小TTLは0、max-ageは10、最大TTLは31536000

よって、最小TTL ＜ max-ageの値 ＜ 最大TTL となるため、
max-ageが優先され、10秒のキャッシュが効く形となります。

# Rails
今回は公開・非公開ステータスを返すAPIに対して、Cache-Controlヘッダを設定します。
以下を設定しようと思います。
- 公開の場合は、10秒間キャッシュする
- 非公開の場合は、キャッシュしない

10秒間キャッシュする場合、
Cache-Control ヘッダを Rails から指定したい時は、公開のレスポンスを返す処理部分に以下を記述します。

```
expires_in 10.seconds, public: true
```
これにより、レスポンスヘッダに以下が設定されます。

```
cache-control: max-age=10, public
```


キャッシュしたくない場合、
非公開のレスポンスを返す処理部分に以下を記述します。

```
expires_now
```
これにより、レスポンスヘッダに以下が設定されます。

```
cache-control: no-cache
```

これでRails側の設定は終了です。

他にどんな設定ができるのかの詳細は、以下参考ください。

https://railsdoc.com/page/expires_in


# 検証

## CloudFrontキャッシュの確認方法
以下のレスポンスヘッダの値を見ることで確認できます。

#### x-cache
- Miss from cloudfront : キャッシュがなくオリジンから取得した
- Hit from cloudfront : キャッシュから取得した
- RefreshHit from cloudfront : キャッシュの有効期限が切れていたが変更がなくキャッシュから取得した
- Error from cloudfront : エッジサーバーかオリジンでエラーが発生した

#### age
- ヘッダなし：キャッシュがない
- 数字：キャッシュされてから何秒たっているか

#### date
- 例）Thu, 17 Oct 2024 05:38:04 GMT : オブジェクトがキャッシュがされた時刻

今回は、以下の値のチェックをすることで確認します。
#### キャッシュされていることの確認
- x-cache：Hit from cloudfrontであること
- age：値が10以内であること
- date：値が変わらないこと

#### キャッシュされていないことの確認
- x-cache：Miss from cloudfrontであること
- age：ヘッダがないこと
- date：値が変わること

## max-ageを付与した時キャッシュがされることの確認

結論、ヘッダにCache-Controlの値としてmax-ageを付与した時、キャッシュされていることが確認できました。

まずは一発目リクエストします。
```
curl -i 'https://~~~~~~~~~/published'
HTTP/2 200
content-type: application/json; charset=utf-8
date: Thu, 17 Oct 2024 05:38:04 GMT
..
..
cache-control: max-age=10, public
..
..
x-cache: Miss from cloudfront
```

2秒後に再度リクエストしたところ、以下が返ってきました。

```
curl -i 'https://~~~~~~~~~/published'
HTTP/2 200
content-type: application/json; charset=utf-8
date: Thu, 17 Oct 2024 05:38:04 GMT
..
..
cache-control: max-age=10, public
..
..
x-cache: Hit from cloudfront
age: 2
```

以下が満たされているので、キャッシュされていることがわかります。

>- x-cache：Hit from cloudfrontであること
>- age：値が10以内であること
>- date：値が変わらないこと

キャッシュの反映も瞬時に行われるようですね。

10秒以上経過した後に再度リクエストすると、キャッシュが返されていないことが確認できました。
有効期限も意図通り適応されているようです。
```
curl -i 'https://~~~~~~~~~/published'
HTTP/2 200
content-type: application/json; charset=utf-8
date: Thu, 17 Oct 2024 05:38:17 GMT
..
..
cache-control: max-age=10, public
..
..
x-cache: Miss from cloudfront
```

以下が満たされているので、キャッシュが返されていないことがわかります。
>- x-cache：Miss from cloudfrontであること
>- age：ヘッダがないこと
>- date：値が変わること



## no-cacheを付与した時キャッシュがされていないことの確認
次に、ヘッダにCache-Controlの値としてno-cacheを付与した時、キャッシュされていないことが確認できました。

```
curl -i 'https://~~~~~~~~~/published'
HTTP/2 200
content-type: application/json; charset=utf-8
date: Thu, 17 Oct 2024 06:22:46 GMT
..
..
cache-control: no-cache
..
..
x-cache: Miss from cloudfront
```
4秒後にリクエストをし、キャッシュがされていないことが確認できました。

```
curl -i 'https://~~~~~~~~~/published'
HTTP/2 200
content-type: application/json; charset=utf-8
date: Thu, 17 Oct 2024 06:22:50 GMT
..
..
cache-control: no-cache
..
..
x-cache: Miss from cloudfront
```

以下が満たされているので、キャッシュが返されていないことがわかります。
>- x-cache：Miss from cloudfrontであること
>- age：ヘッダがないこと
>- date：値が変わること

これらの検証により、レスポンスヘッダに付与するCache-Controlの値に応じて、
CloudFrontに期限付きのキャッシュをさせたり、そもそもキャッシュさせないようにしたりといった設定を行うことができました。

# さいごに
常に、キャッシュが一定時間有効になって良いのか？とかキャッシュが無効になるタイミングで問題が起きないか？など、考慮すべきことはもちろんあると思いますが、しっかり検証しつつ知見を溜めていきたい所存です。