Amazon CloudFrontとRename Distributionパターンの実装

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Amazon CloudFrontの使いどころ

Amazon CloudFrontはオリジンサーバの代わりにコンテンツをキャッシュして配信してくれるサービスです。キャッシュの入っているサーバをエッジサーバと言います。東京リージョン内では、東京と大阪にエッジロケーション(エッジサーバが置いてある場所)があります。全世界で数十拠点あり、1つのオリジンサーバから全世界に配信することができます。オリジンサーバには、S3かWebサーバの載っているEC2インスタンスかAWS管理下かどうかに関係なくドメイン名を指定することができます。

キャッシュを指定する場所

Webサーバがコンテンツをユーザに届ける際、どの程度キャッシュするか指定します。CloudFrontを使う場合、オリジンサーバのキャッシュ時間を引き継ぐか、独自に設定することができます。キャッシュする時間が長ければオリジンサーバへの負荷はもちろん、エッジサーバへのアクセスも減りますのでコストメリットが大きいです。しかし、コンテンツを更新したい場合、キャッシュが有効期限を過ぎないと正しく更新してくれません。そこで、キャッシュを更新する方法が重要になってきます。

動作確認の準備

実際にどのように動作するか確認するために環境を整えたいと思います。オリジンサーバにEC2を指定してApacheを起動しておきます。次に、CloudFrontを使ってエッジサーバとして指定します。オリジンサーバにはtest.cssというファイルを1つ置きました。このファイルがどこでどのようにキャッシュされるかお楽しみに。アクセスログはtail /var/log/httpd/access_logで確認をします。

まずはオリジンサーバへブラウザから直接アクセスしてみます。ログは以下のようになりました。

http://cache.akari7.net/test.css
210.XXX.XXX.XXX - - [24/Aug/2012:16:13:45 +0000] "GET /test.css HTTP/1.1" 200 29 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_1) AppleWebKit/536.25 (KHTML, like Gecko) Version/6.0 Safari/536.25"

続いて3回続けてアクセスしてみると3件のアクセスログが出力されていました。

210.XXX.XXX.XXX - - [24/Aug/2012:16:14:42 +0000] "GET /test.css HTTP/1.1" 200 29 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_1) AppleWebKit/536.25 (KHTML, like Gecko) Version/6.0 Safari/536.25"
210.XXX.XXX.XXX - - [24/Aug/2012:16:14:43 +0000] "GET /test.css HTTP/1.1" 200 29 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_1) AppleWebKit/536.25 (KHTML, like Gecko) Version/6.0 Safari/536.25"
210.XXX.XXX.XXX - - [24/Aug/2012:16:14:44 +0000] "GET /test.css HTTP/1.1" 200 29 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_1) AppleWebKit/536.25 (KHTML, like Gecko) Version/6.0 Safari/536.25"

ちなみに、1回目のアクセスのレスポンスはHTTPステータスコード200番(成功)でコンテンツをダウンロードします。2回目以降コンテンツが更新されていなければHTTPステータスコード304番(変更無し)ということで、コンテンツ自体はダウンロードしていないようです。

CloudFrontからアクセスをする

先ほどはオリジンサーバに直接アクセスしましたので、今度はエッジサーバからアクセスしてみたいと思います。CloudFrontでは、キャッシュ用にドメインを割り当てる際にディストリビューションを作成します。作成の際には以下のようにキャッシュの設定としてオリジンを引き継ぐか自分で指定するか選べます。今回はオリジンを引き継ぐことにします。

また、リクエストに対する振る舞いとしてビヘイビアを設定します。ここでは、クエリーストリングも有効にしておいてください。

以下はCloudFrontからアクセスした際のオリジンサーバのログです。何度アクセスしても1回しかログが出ませんでした。理由は、CloudFrontの動きとして、オリジンサーバ側で有効期限の設定が無い場合、勝手に24時間の有効期限を設定するためです。もし、勝手に24時間の有効期限を設定してほしく無ければ、CloudFront側のビヘイビア指定でカスタムを選び、TTLをゼロに設定しましょう。

http://d12uf59vXXXXXX.cloudfront.net/test.css
54.XXX.XXX.XXX - - [24/Aug/2012:16:38:26 +0000] "GET /test.css HTTP/1.0" 200 9 "-" "Amazon CloudFront"

CloudFrontでは、ビヘイビアにてURLパターン毎にキャッシュの有効期限を設定できます。拡張子がphpやjsp等の動的なコンテンツの場合には、カスタム指定でTTLを0に設定すれば常にオリジンサーバにアクセスしに行きます。一方で、画像等は1年近く設定してもよいでしょう。ここで困るのは、たまに更新するJSやCSSの場合です。何かバグが合った場合には即座に反映させたいはずです。しかし、更新がなければ長期間を設定してオリジンへのアクセスを減らしたい。ここら辺のバランスをどのように解決するかがポイントです。

キャッシュを更新する方法(Rename)

最も簡単なキャッシュ更新方法はファイル名を変えることです。ファイル名を変えればCloudFrontにキャッシュされたコンテンツが無いという事でオリジンサーバに取得しに行きます。簡単に更新できますが、難点はファイル名がどんどん変わってしまうことです。何か中身を書き換える度にファイル名を変えてしまうのは管理が大変そうですよね。

http://d12uf59vXXXXXX.cloudfront.net/test_version2.css
54.XXX.XXX.XXX - - [24/Aug/2012:16:57:14 +0000] "GET /test_version2.css HTTP/1.0" 200 9 "-" "Amazon CloudFront"

キャッシュを更新する方法(QueryString)

ファイル名を変えずに更新する方法がクエリーストリングです。コンテンツを指定する際にGETパラメータを付与することで、ブラウザが別物であると解釈して新たなコンテンツを取得しにオリジンサーバを見に行きます。パラメータとして日付や時間を書いているサイトは良く見かけますよね。しかし、これも完璧ではありません。ブラウザとCloudFrontの間にあるプロキシサーバーでキャッシュサーバとしてSquid等がある場合、クエリーストリングを無視してしまうことがあるのです。ユーザの環境毎に異なるため問題を発見しづらくやっかいです。

http://d12uf59vXXXXXX.cloudfront.net/test.css?v=20120824
54.XXX.XXX.XXX - - [24/Aug/2012:17:02:10 +0000] "GET /test.css?v=20120824 HTTP/1.0" 200 9 "-" "Amazon CloudFront"

キャッシュを更新する方法(Invalidation)

Invalidationは、CloudFrontがキャッシュしたコンテンツを破棄し、改めてオリジンサーバから取得する命令です。ディストリビューションの設定画面でURL指定をして強制的にキャッシュを更新することができます。

Invalidationの処理が完了した後にブラウザからアクセスしてみるとオリジンサーバのログに出力されていますので最新版を取りに行ってくれましたね。

http://d12uf59vXXXXXX.cloudfront.net/test.css
54.XXX.XXX.XXX - - [24/Aug/2012:17:11:58 +0000] "GET /test.css HTTP/1.0" 200 9 "-" "Amazon CloudFront"

よし、これで完璧!?と思いきや、これにも難点があります。Invalidationのリクエストをしてから作業が完了するまで長くて数分かかります。即時に反映したいときにはもどかしいです。また、1つづつURLで指定する必要があるため面倒です。強制的に更新したいコンテンツのURLをどうしても変えたくない場合にはこれを使うしかないでしょう。

キャッシュを更新する方法(Cachebusting)

ということで、最後のCachebustingについてご紹介します。これは、URL指定で名前を変えてバージョン指定しつつも、実際のファイルは名前を変える事無く最新版のコンテンツを読みに行くテクニックです。

バージョン付きのURLをバージョン無しのURLに変換する作業をApacheのmod_rewriteモジュールにやってもらいます。以下のように.htaccessを設定してみてください。これは、js/css/png/jpg/gifの拡張子が付いたバージョン付きURLをバージョン無しとしてリクエストしてくれます。

<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.+)\.(\d+)\.(js|css|png|jpg|gif)$ $1.$3 [L]
</IfModule>

以下のようにブラウザからリクエストをすると、コンテンツが表示されつつ新たにログも残ります。URLが変わっていますのでCloudFrontに新たにキャッシュもされ、2回目以降のアクセスはCloudFrontからの応答となります。

http://d12uf59vXXXXXX.cloudfront.net/test.20120825.css

内部でtest.cssにリライトされて応答します。ログは以下のように出力されていました。中身はtest.cssとなります。

54.XXX.XXX.XXX - - [24/Aug/2012:17:19:53 +0000] "GET /test.20120824.css HTTP/1.0" 404 297 "-" "Amazon CloudFront"

これで、HTML内で指定するjsやcssのキャッシュ指定を長期設定したとしても、万が一のときはファイル名と拡張子の間にバージョンを付与することで最新情報の取得が可能となり、2回目以降はエッジサーバからのキャッシュ応答となり、オリジンサーバへの負荷が大きく減ります。以下は、コンテンツの圧縮、有効期限設定、URL書き換えを設定した .htaccessのサンプルです。

<IfModule mod_deflate.c>
SetOutputFilter DEFLATE
BrowserMatch ^Mozilla/4 gzip-only-text/html
BrowserMatch ^Mozilla/4\.0[678] no-gzip
BrowserMatch \bMSI[E] !no-gzip !gzip-only-text/html
SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png|ico)$ no-gzip dont-vary
SetEnvIfNoCase Request_URI _\.utxt$ no-gzip
AddOutputFilterByType DEFLATE text/plain
AddOutputFilterByType DEFLATE text/html
AddOutputFilterByType DEFLATE text/xml
AddOutputFilterByType DEFLATE text/css
AddOutputFilterByType DEFLATE application/xhtml+xml
AddOutputFilterByType DEFLATE application/xml
AddOutputFilterByType DEFLATE application/rss+xml
AddOutputFilterByType DEFLATE application/atom_xml
AddOutputFilterByType DEFLATE application/x-javascript
AddOutputFilterByType DEFLATE application/x-httpd-php
</IfModule>
 
<ifModule mod_expires.c>
ExpiresActive On
ExpiresDefault "access plus 1 seconds"
ExpiresByType text/html "access plus 1 seconds"
ExpiresByType image/gif "access plus 10 years"
ExpiresByType image/jpeg "access plus 10 years"
ExpiresByType image/png "access plus 10 years"
ExpiresByType image/x-icon "access plus 10 years"
ExpiresByType text/css "access plus 10 years"
ExpiresByType text/javascript "access plus 10 years"
ExpiresByType application/x-javascript "access plus 10 years"
ExpiresByType application/x-shockwave-flash "access plus 10 years"
</ifModule>

<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.+)\.(\d+)\.(js|css|png|jpg|gif)$ $1.$3 [L]
</IfModule>

js/css/画像などの有効期限を10年に設定しているあたりがワイルドだぜぇ。

まとめ

オリジンサーバとエッジサーバの設定を駆使して柔軟にコンテンツのキャッシュ設定・更新をすることができました。コンテンツを高速に安定的に大規模に配信する場合、キャッシュサーバは必須かと思います。CloudFrontは1ファイルから使える低価格で従量課金の破壊的イノベーションなサービスです。使わない理由は無いでしょう。静的なコンテンツを配信するのにオリジンサーバの台数を増やす必要な無いですね。オリジンサーバのコンテンツ有効期限をワイルドに設定して、環境とお財布に優しいコンテンツ配信を行おうぜぇ!

参考資料

Version Control with Cachebusting#

CDP:Rename Distributionパターン

Specifying How Long Objects Stay in a CloudFront Edge Cache (Object Expiration)