LoginSignup
8
3

More than 3 years have passed since last update.

CloudFrontは同じURL+QueryStringでもキャッシュ応答しないケースがある

Last updated at Posted at 2020-03-19

はじめに

あるAPIサービスが、CloudFront + API Gateway + Lambda という構成で稼働しており
CloudFrontは、URL+QueryStringごとにキャッシュし、応答速度をあげるために使用しています。

事象

このAPIサービスには、Go/PHPそれぞれで実装されたプログラムがHTTPリクエストを送ります。
CloudFrontの設定は以下の通り。(関連設定のみ)

設定項目 設定値
Cache Based on Selected Request Headers None
Object Caching Customize
MinTTL:0
MaxTTL: 86400
DefTTL: 86400
Forward Cookies None
Query String Forwarding and Caching Forward all, cache based on all
Compress Objects Automatically No

Go/PHPそれぞれから同じ URL+QueryString でリクエストを送信した場合は、2回目のリクエストは
キャッシュHitする想定でしたが、なぜかMissになります。(X-Cacheヘッダで確認)

原因

Go実装のプログラムでは 標準のnet/httpパッケージを使ってリクエストを送信しています。
net/httpのhttpクライアントはレスポンスがgzip圧縮されていても自動的に展開してくれるため
デフォルトでAccept-Encoding: gzipヘッダが付きます。
CloudFrontは、このAccept-Encodingヘッダの有無でキャッシュが別管理になるため、
同じURL+QueryStringであっても、ヘッダ有無が異なれば再度オリジンアクセスする、という動作になります。
これは、レスポンスが圧縮されているかに関わらず上記の動作になりました。

コメントで指摘あったので追記)
Vary: Accept-EncodingがレスポンスヘッダにあればAccept-Encodingヘッダ有無で
キャッシュが別になるのは理解できますが、今回使用しているオリジンは、レスポンスを圧縮しない、かつ
Varyヘッダを返さない仕様です。

DefaultClientでリクエスト送信 実装例

main.go
package main

import (
    "fmt"
    "net/http"
)

func main() {
    url := "http://example.com/"
    req, err := http.NewRequest(http.MethodGet, url, nil)
    if err != nil {
        fmt.Errorf("%v", err)
        return
    }
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        fmt.Errorf("%v", err)
        return
    }
    defer resp.Body.Close()
}

今回CloudFront設定を素直に?以下のように解釈してたので発見が遅れました
* リクエストヘッダでキャッシュ判定しない
* URL+QueryStringごとにキャッシュする
* 自動圧縮は無効化してるのでAccept-Encodingを見ない

↑このAccept-Encodingを見ないというのが勘違いで、オリジン側では通常Accept-Encodingヘッダによって
gzip圧縮するか判定しているはずで、CloudFrontも正しくキャッシュ(非圧縮を期待しているクライアントに圧縮
データを応答しないこと)するためにAccept-Encodingヘッダをキャッシュ判定に使用していると思われます。

Go側でリクエスト送信時にAccept-Encodingヘッダを付けないようにすればキャッシュがうまく使われるように
なります。以下Go実装の修正イメージ

Accept-Encodingヘッダを付加しないリクエスト送信 実装例

DisableCompressionをtrueにしたTransportを生成して、それを渡してclientを生成してclient.Doする形

main.go
package main

import (
    "fmt"
    "net/http"
)

func main() {
    url := "http://example.com/"
    req, err := http.NewRequest(http.MethodGet, url, nil)
    if err != nil {
        fmt.Errorf("%v", err)
        return
    }
    client := &http.Client{
        Transport: &http.Transport{DisableCompression: true},
    }
    resp, err := client.Do(req)
    if err != nil {
        fmt.Errorf("%v", err)
        return
    }
    defer resp.Body.Close()
}

おわりに

CloudFrontの公式ドキュメントに明確な記述が見つけられなかったので、今回記事にしました。
誰かの助けになれば幸いです。

8
3
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
3