# CDN

CDNとは、Content Delivery Networkの略称で、コンテンツを高速に配信するためのサービスになります。CDNというと、キャッシュをするためのサービスというイメージがあるかもしれませんが、キャッシュ以外にもCDNは多くの機能を提供しています。例えば、画像の最適化で見たように画像のリサイズや、エッジサーバによるレンダリング、JavaScript、CSSのminify、gzip化によるリソース最適化など、多岐に渡ります。 また、セキュリティ面でもCDNは有用です。WAFで悪意のあるリクエストを弾いたり、DDosによる攻撃やBotアクセスの対策をCDN側で行うことができます。

CDNの基本的な機能としては以下のようなものが挙げられます。

  • エッジサーバーによる高速配信
  • キャッシュ設定
  • 画像の最適化
  • エッジコンピューティング
  • セキュリティ
    • WAF
    • DDos対策
    • Bot対策

この章では、CDNの基本的な仕組みを理解し、CDNを活用したパフォーマンスの最適化を見ていきたいと思います。具体的には、キャッシュの仕組みと設定方法、エッジコンピューティングによるレンダリング手法、画像CDNの活用などを見てみましょう。

# CDNの仕組み

CDNの基本的な構成は次の2つになります。

  • エッジサーバー
  • GSLB(グローバルロードバランサー)

エッジサーバとは、世界中に配置されている配信用のサーバーになります。nginxやVarnishなどで構成され、CDN用に構築されています。そして、クライアントをこのエッジサーバーへ適切に振り分けるのがGSLBになります。GSLB (opens new window)とは、グローバルサービスロードバランシングで、世界中に配置されたエッジサーバーに対してトラフィックを分散させる機能を有します。GSLBにより、負荷を分散させ、レイテンシーを低減させることができます。

GSLB

出典: Fastly (opens new window)

CDNの基本的な仕組みは、エッジサーバによる地理的な分散を行い、クライアントに一番近い場所から配信を行うというものです。例えば、アメリカから日本へアクセスする際、配信元のサーバーが日本にある場合は物理的な距離が生じます。物理的な距離があることで、配信パフォーマンスに影響を及ぼす可能性があります。

クライアントが最初にコンテンツをリクエストするときは、TCP/IPでの接続を行います。TCP/IPの接続が確立すると、クライアントと配信元サーバーで3ウェイハンドシェイク (opens new window)をする必要があります。HTTPSの場合は、TLSのハンドシェイクでさらにやりとりが増えます。地理的に近い国内であればそれほど問題にならないかもしれませんが、国を越えてのアクセスになると、この往復のやりとりによるレイテンシーが発生する可能性があります。

配信するサーバーだと物理的距離が生じる

CDNのエッジサーバーがアメリカにあれば、わざわざ日本まで来なくても物理的に近いサーバーでこのハンドシェイクを行うことができます。その際、エッジサーバを経由して日本に来ますが、同一CDN内ならば、Keep-AliveでTCPの接続を維持したまま通信を行える可能性があります。そのため、直接アメリカから日本へアクセスするよりも、高速に配信をすることができるようになります。また、アメリカのエッジサーバでキャッシュがあれば、配信元サーバーにアクセスせずにコンテンツを取得できるので、さらに高速にできるでしょう。

エッジサーバーなら距離が近くなる

# CDNによるパフォーマンスの最適化

CDNができることは多岐に渡りますが、フロントエンドのパフォーマンスを高めるためには次のような施策が有効的です。

  • 静的リソースのキャッシュ
  • エッジレンダリング
  • 画像CDNの活用

# 静的リソースのキャッシュ

Webアプリケーションのコンテンツ配信において、キャッシュの仕組みを理解することは重要になります。キャッシュの格納場所は、ローカルやProxyなどさまざまですが、一般的なアプリケーションで使用されるキャッシュは、ブラウザーのキャッシュとCDN上のキャッシュになります。いずれもHTTPキャッシュ (opens new window)というしくみが使われます。HTTPキャッシュは、HTTPヘッダのCache-Controlを使って制御します。そのため、Cache-Controlの設定を正しく理解し、アプリケーションの特性に合わせて適切に対応する必要があります。

# プライベートキャッシュと共有キャッシュ

キャッシュには様々な種類がありますが、大きく分けてプライベートキャッシュと共有キャッシュに分類できます。プライベートキャッシュは、単一のユーザーのためのキャッシュで、他のユーザーと共有されることのないキャッシュになります。例えば、認証上のトークンやそのユーザーだけに表示するコンテンツなど、プライベートに扱いたいキャッシュになります。格納場所としては、ローカルのブラウザーかLocal Storage、Service Workerになります。

共有キャッシュは、複数のユーザーで使用されるキャッシュになります。HTMLファイルやJavaScriptファイルなど、複数のユーザーに対して公開されるリソースなどが当てはまります。格納場所は、ブラウザーか経路上のProxy、CDNなどが当てはまります。この章では、主にブラウザーとCDNで使用される共有キャッシュについての設定方法を見ていきたいと思います。

# TTLとキャッシュの状態

TTLとは、Time-To-Liveの略称でキャッシュの有効期限を意味します。キャッシュの有効期限が切れると使われなくなるとイメージされますが、実際は有効期限が切れても条件付きで使われることがあります。例えば、配信元のサーバーがダウンしていたため、有効期限切れのキャッシュを利用したなどのケースが当てはまります。

キャッシュの状態は大きく分けて二つあります。

  • Fresh
  • Stale

FreshはTTLの有効期限内で使える状態を指します。Staleは、期限切れだけど条件次第(上記のようなケース)で使える状態を指します。キャッシュを使用する際は、キャッシュが今どの状態になっているか注意しておきましょう。

# HTTPヘッダの設定

共有キャッシュであるHTTPキャッシュを制御するためには、Cache-Controlヘッダを使用します。Cache-Controlヘッダはリクエストまたは、レスポンスでキャッシュに関するディレクティブを指定することができます。指定したディレクティブによってキャッシュの挙動が変わるため、アプリケーションのキャッシュポリシーに合わせて正しく設定する必要があります。一般的によく使われるディレクティブは次のようなものがあります。一つずつ見てみましょう。

  • maxage
  • s-maxage
  • no-store
  • no-cache
  • must-revalidate/proxy-revalidate
  • stale-while-revalidate
  • no-transform

勝手にキャッシュされる?

Cache-Controlのディレクティブが明示的に設定されていなくても、ブラウザーのデフォルトの動作では、キャッシュされることがあります。そのとき、DateとLast-Modifiedというヘッダを用いてキャッシュ期間を計算します。具体的なキャッシュ期間の計算方法は以下になります。

TTL =(Date - Last-Modified)/ 定数(一般的には10)

Dateは現在時刻を表し、Last-Modifiedは最後に更新された時刻を表します。最後に更新された日が10日前なのであれば、10日/10 = 1日となり、1日程度のキャッシュが割り当てられます。Cache-Controlの設定をしていなくても(または、間違っていても)、このようなデフォルトのキャッシュが働くことがあるので注意しておきましょう。

# max-age

max-ageは、指定した値の期間、キャッシュをFreshの状態にすることができます。有効期間は秒単位で指定できます。例えば、次のように設定すると、1時間キャッシュが有効になります。

Cache-Control: max-age=3600

CDNを経由している場合、Ageヘッダを見ることで、キャッシュされてからの経過時間を知ることができます。Age (opens new window)ヘッダは配信元サーバーからのレスポンスには含まれませんが、CDNなどのキャッシュサーバーからクライアントにレスポンスする際に付与されます。次の例では、CDNにキャッシュされてから100秒間経過していることが分かります。

Cache-Control: max-age=3600
Age: 100

注意する点として、max-ageはブラウザー側がリソースを受信した時間ではなく、配信元サーバーでリソースが生成されてからの経過時間を表します。そのため、CDN上でキャッシュして、すでに60秒間保存していた場合は、ブラウザー側では60秒間差し引いて有効期限が計算されます。

Cache-Control: max-age=3600
Age: 60

配信元サーバーでリソースが生成されてからの経過時間

# s-maxage

max-ageはブラウザー、あるいはCDNのディレクティブである一方、s-maxageは、共有キャッシュ特有のディレクティブになります。そのため、CDN上のキャッシュの有効期限を設定する際に使われます。max-ageと同時に指定された場合は、s-maxageが優先されます。

Cache-Control: s-maxage=3600

s-maxageとmax-ageの適用範囲

例えば、max-ageだけを指定した場合はブラウザーとCDNでmax-ageの値を使用します。

Cache-Control: max-age=3600

max-ageだけの適用範囲

s-maxageを追加した場合は、CDN側だけs-maxageの値が優先されます。ブラウザー側は引き続きmax-ageの値が使われます。

Cache-Control: max-age=3600, s-maxage=1000

s-maxageが優先される

s-maxageだけ指定した場合は、CDN、ブラウザー両方でs-maxageの値が使われます。

Cache-Control: s-maxage=1000

s-maxageだけの場合

TIP

CDNベンダーによってキャッシュの扱いは異なるので、使用しているCDNのキャッシュポリシーを確認してください。

# no-store

no-storeは、キャッシュを格納しないという設定です。キャッシュを保存しないので、リクエストごとに配信元サーバーへリソースを取得します。そのため、キャッシュをしたくないときはno-storeを指定しましょう。

Cache-Control: no-store
# no-cache

no-cacheは、キャッシュを保存することを許可しますが、利用する前に再検証することを要求します。no-cacheとあると、一見、キャッシュを使わないように見受けられますが、キャッシュ自体は使うことが前提としてあります。 no-storeがキャッシュを保存しないという設定である一方、no-cacheは、キャッシュを使うときに、有効でない場合は使用してはいけないという設定になります。

つまり、キャッシュを使う際には配信元サーバーへ毎回問い合わせを行い、そのキャッシュが有効でないときは使用しないということになります。 そのため、毎回更新を確認し、更新がなければキャッシュが使用され、更新されていればすぐに再取得が実行されます。

no-cacheの動きを簡単に見てみましょう。初回アクセス時は、キャッシュ自体がないので配信元サーバーにリソースを取得しキャッシュを保存します。

Cache-Control: no-cache

no-cacheで初回アクセス

2回目以降は、条件付きリクエストを送信し、キャッシュが有効か検証を行います。キャッシュが有効ならキャッシュを使い、無効なら配信元サーバーからリソースを取得します。

no-cacheで2回目以降のアクセス

CDNを経由する場合もフローは同じです。毎回キャッシュの検証を行い、有効ならばCDNからクライアントへキャッシュを返し、無効ならば配信元サーバーへリソースを取得します。no-cacheは、更新性の高いアプリケーションのときに有用です。更新がなければキャッシュが使用され、更新されていれば即座に新しいリソースが取得されます。

# stale-while-revalidate

stale-while-revalidateは、キャッシュの有効期限切れのときに、何秒間Staleされたキャッシュを使用するかを指定することができます。Staleされたキャッシュとは、有効期限が切れたキャッシュのことです。下記の例では、キャッシュは1時間有効ですが、1時間後でも古いキャッシュを100秒間使うことができます。その間に、再検証が実行されて最新のリソースをバックグランドで取得します。再検証が成功した場合は、キャッシュが更新され、再検証が失敗した場合でも、100秒間は古いキャッシュが使われ続けます。

Cache-Control: max-age=3600, stale-while-revalidate=100

stale-while-revalidateを使うことで、キャッシュの有効期限が切れたタイミングで発生する同期的な再検証を回避することができます。通常、配信元サーバーへ再検証するときは、同期的にリクエストが実行されるため、待機状態が発生する可能性があります。しかし、stale-while-revalidateの場合は、バックグラウンドで再検証を実行するため、配信元サーバーへリクエストするときの待機時間が発生せず、新しいキャッシュをすぐに使うことができます。

初回アクセス時は配信元サーバーからリソースを取得し、キャッシュを保存します。

初回アクセス時はリソースを取得し、キャッシュを保存

1時間の間はキャッシュが有効なので、キャッシュを取得します。

1時間の間はキャッシュが有効

1時間後にキャッシュの有効期限が切れますが、100秒間は古いキャッシュ(Stale)を使うことができます。その間に、バックグラウンドでキャッシュの再検証を配信元サーバーへリクエストします。再検証が成功したら新しいキャッシュを保存します。

1時間後にキャッシュが切れ、バックグラウンドでキャッシュを保存

そのあとは、新しいキャッシュを使うことができます。

新しいキャッシュを使う

上記の例では、クライアントと配信元サーバーのやりとりでしたが、CDNを経由する場合でも同じように動作します。Fastly (opens new window)Google Cloud CDN (opens new window)Cloudflare (opens new window)CloudFront (opens new window)はstale-while-revalidateに対応しています。

# must-revalidate/proxy-revalidate

must-revalidateは、有効期限が切れた(Stale)キャッシュを再利用させないためのディレクティブで、必ず配信元サーバーへ再検証をリクエストします。通常、キャッシュの有効期限が切れた場合、配信元サーバーへリソースをリクエストしますが、もし配信元サーバーがダウンしていた場合、古いキャッシュ(Stale)が再利用されます。アプリケーションによっては、サーバーがダウンしたときはキャッシュを再利用せずに強制的に再検証して、エラーを返して欲しいという要件があるかもしれません。APIでJSONデータを返すときなどが当てはまるでしょう。must-revalidateを指定すると、古いキャッシュが再利用されず、常に配信元サーバーへ問い合わせをします。

Cache-Control: max-age=10,must-revalidate

no-cacheとの違いは、no-cacheが毎回、配信元サーバーへ再検証をリクエストする一方、must-revalidateはmax-ageと一緒に使うため、有効期限内であればキャッシュが使われます。有効期限が切れたときだけ、配信元サーバーへ必ずリクエストを送るというものです。

# no-transform

no-transformは、Proxyなどの中間経路でオブジェクトに対して操作を行うことを許可しない設定です。オブジェクトの操作とは、JavaScriptやCSSの圧縮などが当てはまります。通常、ほとんどの場合でこのような圧縮は問題になりませんが、もし明示的にオブジェクトへの操作をしたくない場合に使うことができます。

Cache-Control: no-transform
# public/private

publicは、共有キャッシュとして使うことを明示的に示す場合に使われます。ただ、publicを指定しなくてもデフォルトで共有キャッシュは保存されるので、通常は設定する必要はありません。認証が必要なコンテンツで、一部だけ未認証でもキャッシュしたいなどのケースなどが当てはまります。しかし、うっかり認証ページでも使って、プライベートな情報が共有されてしまうなどの事例もあり得るため、注意が必要です。

privateは、ブラウザーなどのクライアントだけで使用するキャッシュに対して使われます。そのため、CDNや経路上にあるProxyに対してキャッシュを制限することができます。

# キャッシュさせたくない場合

クライアント側のキャッシュが原因で、ユーザーごとに挙動が変わるというケースは多々あるかと思います。画像が表示されたり、されなかったり、スタイルが変わっているなど、リリース後によくあるパターンでしょう。そのような場合、キャッシュをしないことが先決になります。 上記のCache-Controlの設定方法を踏まえると、キャッシュさせない設定は以下のようになります。

Cache-Control: private,no-store,no-cache,must-revalidate
  • private / CDNでのキャッシュを防ぐ
  • no-store / キャッシュの保存を防ぐ
  • no-cache / キャッシュを使うことを防ぐ
  • must-revalidate / 配信元サーバーがダウンしたときにキャッシュが再利用されることを防ぐ

# エッジレンダリング

静的リソースに関しては、上記のHTTPキャッシュの設定をすることで配信パフォーマンスを改善させることができます。しかし、動的コンテンツに対してはHTTPキャッシュだけでは対応できません。動的コンテンツとは、SSRなどで生成されるコンテンツを指し、ユーザーごとに異なるようなコンテンツや、更新性の高いコンテンツのことをいいます。

CDNでは、この動的コンテンツを生成する仕組みとして、エッジワーカーと呼ばれる機能があります。エッジワーカーとは、エッジサーバー上で任意のプログラムを実行できる環境のことをいい、CloudflareではCloud Workerというサービスを提供しています。エッジワーカーでは、JavaScript環境が提供されているため、このサーバー上でレンダリングを実行し、動的コンテンツを生成することができます。

通常、SSRでは、クライアントから配信元サーバーへ毎回アクセスが必要になり、物理的な距離が発生してしまいます。そのため、ユーザーによってはページの読み込みが遅れてしまう可能性があります。

SSRを使ったとき

出典: Remix and “The Edge” (opens new window)

エッジワーカーを使ったレンダリングの場合、世界中に分散されたエッジサーバー上でコンテンツを生成できるため、地理的なデメリットが無くなります。また、生成されたコンテンツはキャッシュできるため、高速に配信することができます。エッジレンダリングは、配信元のサーバーを分散させることで、常にユーザーから近い場所でレンダリングを実行することができるようになります。

エッジレンダリングを使ったとき

出典: Remix and “The Edge” (opens new window)

例えば、フロントエンドフレームワークのRemix (opens new window)では、デフォルトでCloudflare Workerへのデプロイがサポートされています。RemixのCliを実行すると、次のように選択することができます。

$ npx create-remix@latest

? Where would you like to create your app? remix-cloudflare
? What type of app do you want to create? Just the basics
? Where do you want to deploy? Choose Remix App Server if you're unsure; it's easy to change deployment targets.
  Architect
  Fly.io
  Netlify
  Vercel
  Cloudflare Pages
❯ Cloudflare Workers
  Deno

エッジワーカーを使ったレンダリングを使うことで、配信スピードを大幅に改善させることができます。特に、海外展開しているアプリケーションやWebサイトの場合はメリットが大きくなるでしょう。しかし、CDNベンダーによってエッジワーカーの制約があるため注意が必要です。Cloudflare Workerでは、以下のような制限 (opens new window)があります。(執筆当時)

Cloudflare Workerの制約

出典: Limits (opens new window)

エッジワーカーを提供しているCDNベンダーは以下になります。それぞれ提供するサービスや制約は異なるため、アプリケーションの要件に合わせて検討してみましょう。

# 画像CDNの活用

画像CDNとは、画像配信に特化したCDNです。画像CDNを使うことで、オリジンリソースを変えることなく、画像のサイズ変更や最適化を行うことができます。通常、画像を保存する際は、クライアントあるいはサーバサイドで画像の最適化をしてから保存します。クライアントから使用するときは、そのリソースのURLを指定して画像を表示します。

<img src="https://mydomain.com/test.png" />

もし、画像のフォーマットをWebPやAVIFに対応したり、サイズを変更したい場合は、オリジンのリソースを再度変更しなければなりません。しかし、画像CDNの場合、オリジンのリソースの変更をせずに画像の最適化を行うことができます。例えば、Cloudinaryが提供しているMedia Optimizer (opens new window)では、オリジンサーバの間にCDNとOptimizerを設置し、ユーザーがリクエストしたタイミングで画像の最適化ができるようになっています。

resource-optimization-image-cdn-1.webp

出典: Media Optimizer Overview (opens new window)

この場合、次のステップで画像の最適化が行われます。

  1. ユーザーが画像をリクエストする。
  2. 直近のCDNがリクエストを受け取る。
  3. CDNが対象の画像のキャッシュがあるか確認する。なかった場合はMedia Optimizerへリクエストする。
  4. Media Optimizerがオリジンサーバへリクエストし、画像の最適化を実行する。
  5. Media OptimizerからCDNへ画像を返す。
  6. CDNからクライアントへ画像を返す。その際、最適化された画像をキャッシュしておく。

ユーザーがサイズの変更や最適化をリクエストするときは、次のようにURLのクエリパラメータを指定することでMedia Optimizerが最適化してくれます。

<img src="https://mycloud.mo.cloudinary.net/rest/of/the/path.jpg?tx=w_500,h_500,c_fit" />

この例では、tx=w_500,h_500,c_fitを指定して画像のサイズを最適化しています。

画像のフォーマットはブラウザから判断して自動的に適用してくれたり、レスポンシブにも対応することができます。例えば、Media Optimizerでは、Automatic responsive resizing (opens new window)を使うことで、デバイスに合わせて、画像のサイズを調整してくれます。

/resource-optimization-image-cdn-2.avif

出典: Media Optimizer Overview (opens new window)

画像CDNを使うことで、ユーザーに合わせて、画像を柔軟に最適化することができます。また、キャッシュされた画像はCDNサーバから配信されるため、リソースの取得速度を向上させることができます。画像CDNは、画像の最適化とキャッシュの最適化が同時に行えることが大きなメリットになるでしょう。