はじめに
Amazon CloudFrontって、CDN でしょ。
キャッシュしてくれて、HTTPS 対応も楽だから「とりあえず使っとく」そんな認識でした。
そんな折、たまたま読んだ公式ブログ(Amazon CloudFrontリクエストのライフサイクルを図解する)で、思わず「え、CloudFrontってこんなに色々やってたの!?」と衝撃を受けました。
これまで「CloudFront = 静的ファイルを高速配信するもの」くらいに思っていたのですが、それは機能のごく一部でしかなかったようです。
本記事は、「CloudFront、ただの CDN じゃない🤯」という気付きを元に、どんなことができるのか、代表的な機能を広く浅くざっくり整理したメモです。
全体像の入り口として読んでもらえたらうれしいです。
“ディストリビューション” はCloudFrontにとっての設計書
マネコンで CloudFront を設定しようと思うと、最初にやるのが「ディストリビューションを作成」。
ディストリビューションは、CloudFront が受け取ったリクエストをどう処理するか定義する設計書です。オリジンはどれ?キャッシュキーは? TTL は?ビヘイビア(振る舞い)は?
※ 以降で取り上げる説明は、ディストリビューションの設定次第で挙動が異なる場合があります。
DNS名前解決:ユーザーのリクエストは誰が受けるのか
POP(Point of Presence、別名 エッジロケーション)は、世界中に数百カ所存在し、CloudFrontの CDN 性能を支えています。
Route 53 と合わせて使用するなら、Aliasレコードで CloudFront のドメイン名を指定する、という設定がお馴染みかと思います。Aliasレコードとは、超雑にいうと動的に A/AAAAレコードを返す仕組みです。
Route 53 は、ユーザーから example.com への名前解決リクエストが来た際、裏側で CloudFront のドメイン(例: d123456abcdef8.cloudfront.net)を解決します。CloudFront の権威 DNS は、問合せ元(Route 53のリゾルバ)の地理情報やネットワーク状況、POPの負荷などを考慮して、最適な POP の IPアドレスリスト(通常は複数)を返します。Route 53はその結果を受け取り、A/AAAAレコードとしてクライアント(ブラウザ)に返します。
ブラウザはそのうちの 1 つの IP アドレスに対してリクエストを投げます。
※本記事では、CloudFront の基本的なユニキャスト構成を前提としています。Anycast 静的 IP アドレスを使った構成については対象外です。
CloudFrontがTLS終端なわけ
CloudFront は ACM(AWS Certificate Manager)と連携しているため、深い知識がなくとも、いとも簡単にクライアントと CloudFront 間の通信を HTTPS 化できます。
…ゆえに、TLS 終端とはつまりどういうことか考えたこともなく。お恥ずかしながら、私は TLS の暗号化範囲に URL パスまで含まれていると知りませんでした。
――― URLパス、クエリパラメータ、HTTPメソッド etc…
暗号化された状態では何も見えません。見えるのは IPアドレスと SNI くらい。
この状態では、WAFに渡すことも、キャッシュキーを見ることもできません。つまり、CloudFrontは “復号しないと何もできない” のです。 “見る”ためにまず復号するのは必然。見えて始めて、何をするか/しないか判断できます。
そう考えると、とにかく全リクエストをまず CloudFront が最前線で受け止めることも、CloudFront が TLS終端であることもごく当たり前な話ですね。
キャッシングは複数レイヤーで
CloudFront は複数のレイヤーに分かれています。
クライアントからのリクエストは、復号されたあと、まず POP が受け取ります。そのうえで、POP がキャッシュを持っていればそれをレスポンスとして返しますが、持っていない場合は、リージョン別エッジキャッシュに転送し、そこにもキャッシュがない場合に初めてオリジンサーバーから取得する、という流れを取ります。
リージョン別エッジキャッシュで保有できるキャッシュは、個別の POP よりも大きいため、そこでキャッシュが見つかればオリジンサーバーに転送する必要がなく、パフォーマンス向上が期待できます。
※ Origin Shield を有効にしている場合は、オリジンサーバーの前にもう 1 レイヤー追加されます。
復路のレスポンスでは、次回のリクエストに備えて、自分のキャッシュに追加しつつ、レスポンスを返すという逆の流れをとります。
ただし、キャッシュ対象外の場合(たとえば、GET リクエストと HEAD リクエスト、+ 設定次第では OPTIONS リクエスト 以外の HTTP メソッド)、POP はリージョン別エッジキャッシュを経由せず、直接オリジンに転送します。賢い。
CloudFront × WAF × ALB で安全な構成にする
CloudFront を CDN としてではなく、第一防衛線として活用する構成もあります。
たとえば、CloudFront では AWS WAF と連携して、SQLi や XSS といった攻撃を意図したリクエストがサーバーに到達する前にブロックすることで、ウェブアプリケーションや API を保護することができます(ブロックする内容は WAF のルール次第)。
さらに、ALB(Application Load Balancer)へのアクセスを CloudFront 経由に制限することで、ALB のパブリックアクセスを遮断できます。
ただし、CloudFront による防御はあくまで第一段階であり、POST リクエストなどは基本的にオリジンまで到達します。アプリケーション側でも適切なバリデーションや認証が必要です。
エッジでの関数実行で、リクエストやレスポンスをカスタマイズ
独自のコードを記述して、CloudFront が処理する HTTP リクエストやレスポンスをカスタマイズすることができます。このコードは、ユーザーの近くで実行するため、レイテンシーを最小限に抑えることができます。また、サーバーやその他のインフラストラクチャの管理不要で使用できるのも魅力です。
アタッチするコードは、エッジ関数と呼ばれ、CloudFront Functions と Lambda@Edge の 2 種類があります。
CloudFront FunctionsとLambda@Edgeの違いと使い分け
CloudFront Functions は、JavaScriptベースで、ヘッダー処理や URL 書き換えなどの軽量処理に特化したオプションです。起動時間が 1 ミリ秒未満、毎秒数百万のリクエストを処理するようにすぐにスケールできるというのが強みです。
一方、Lambda@Edge は、Node.js または Python 関数の Lambda 関数として動作し、外部サービスとの連携やリクエストボディの操作など、より複雑で重たい処理が可能です。1 秒あたり数千のリクエストまで自動的にスケーリングします。
(参考:CloudFront Functions を使用してエッジでカスタマイズする, Lambda@Edge を使用してエッジでカスタマイズする)
CloudFront Functions の方が圧倒的に高速かつ安価で、スケーラビリティにも優れていますが、HTTP リクエストのボディにアクセスできない、ネットワークやファイルシステムへのアクセスが制限されているなど、できることが限られているため用途を見極める必要があります。
まずは CloudFront Functions で検討し、難しそうであれば Lambda@Edge を検討、という流れがよさそうに思いました。
ユースケースや両者の違いについてさらに知りたい方は、公式ドキュメントが参考になります。
👉 CloudFront Functions と Lambda@Edge の違い
おわりに
まだまだ理解が浅いところも多く、整理しきれていない部分だらけですが、CloudFront を調べながら得た気付きを記録としてまとめました。
これはあくまで自分のためのメモですが、同じように「CloudFrontってなんだっけ?」と思ったどなたかの一助になれば幸いです。
なお、本記事では触れていませんが、CloudFront の設計において最も難しいのは「キャッシュ戦略」かもしれません。
どのリクエストをどの粒度でキャッシュするか、どの情報をキーに含めるか。それ次第でパフォーマンスもセキュリティも大きく変わります。
そこはまた別のテーマとして、あらためて整理してみたいと思います。