CloudFrontで画像最適化CDNを構築した際の「+α」の共有
はじめに
CloudFrontで画像最適化を行うCDNを構築したので、その知見の共有です。
構成としては「CloudFrontを前段に、Lambda@Edge(Node.js)でsharpというライブラリを動かす」というもので、そのCDNに対して、TANOMUという、弊社で開発してるBtoBのSaaSからリクエストが飛ぶ、という流れです。
この構成に関する記事はインターネットにたくさんあるので、今回はより細かい「+α」を共有します。
AWSのブログによる、同様の構成の紹介は下記です。
この記事では、上記のAWSのブログとの差に焦点を当てて、解説します。
本題
Viewer-Request関数にて、CloudFront Functionsを利用する
上記のAWSのブログでは、(おそらくCloudFront Functionsがまだ無かったためか)Viewer-Request関数でLambda@Edgeを利用していますが、今回はCloudFront Functionsを利用するようにしました。
CloudFront Functions利用に合わせて、後述する署名で用いるシークレットの保存・呼び出しに、CloudFront KeyValueStoreを利用しました。
今回はAWS CDKで構築したのですが、CloudFront KeyValueStoreを利用することで、コードにシークレットを記述せずにすみ、また「CLIから実行時にシークレットを渡す」のような追加の実装も必要ない、というメリットがありました。
Viewer-Request関数にて、署名の検証をする
アプリ側でURLとパラメータから署名を生成して、Viewer-Request関数でその署名を検証することで、アプリ以外からのリクエストを弾くようにしました。
今回は、後述するようにキャッシュバージョンをクエリパラメータに乗せるようにしたので、攻撃者がキャッシュバージョンを無限にインクリメントすることで、無限に新しい画像最適化を実行させることが可能だったため、署名の検証でこれを防ぐようにしました。
キャッシュのバージョンをクエリパラメータに乗せる
画像最適化の結果、想定外の出力がされた場合に、キャッシュをクリアできるように、クエリパラメータでキャッシュのバージョン番号を指定するようにしました。
想定してる流れは、下記のようになります。
- ブラウザから
xxx.jpg?cache=1
にリクエストが行われる - Lambad@Edgeにて画像最適化が行われ、
/xxx_cache-1.jpg
というパスで、S3に保存される - CloudFrontでキャッシュが保存されたうえで、ブラウザに返却される
- ブラウザでキャッシュが保存されたうえで、画像が描画される
ここで、描画された画像になんらかの異常があった場合、
- アプリ側でキャッシュバージョンをインクリメント
- ブラウザから
xxx.jpg?cache=2
にリクエストが行われる - Lambad@Edgeにて画像最適化が行われ、
/xxx_cache-2.jpg
というパスで、S3に保存される - (以下おなじ)
とすることで、キャッシュクリアを容易にすることができます。
Origin-request関数にて、Lambda@Edgeで、画像最適化を行う
上記のAWSのブログでは、Origin-Response関数でLambda@Edgeを呼んでいますが、今回はOrigin-Request関数で、Lambda@Edgeを呼ぶようにしました。
その理由としては、Lambad@Edgeの制限として、「オリジンレスポンスイベント」の「ヘッダーと本文を含む、Lambda 関数によって生成されたレスポンスのサイズ」が1MBですが、アプリの仕様として画像サイズの上限はそれを超える、としていたためです。
そこで、Origin-Request関数で「指定されたURIに最適化後の画像が無ければ、最適化後の画像を生成して、対象のURIに保存」とすることで、この制約を回避しました。
Discussion