ランサーズ(Lancers)エンジニアブログ > AWS > CloudFront > EC2オリジンのCloudFrontで静的ファイルをキャッシュした話
CloudFront

EC2オリジンのCloudFrontで静的ファイルをキャッシュした話

kanazawa|2017年12月11日
AWS

ランサーズ Advent Calendar 2017 11日目の記事です。

インフラエンジニアの金澤です。

CloudFrontでサムネイルをキャッシュした話に続きまして、静的ファイルをキャッシュした手順も記録として残しておきたいと思います。

導入に至った経緯

Appサーバーの負荷とレスポンス遅延

当時は、サーバー費用をそこまでかけられないフェーズで、なるべく抑えた運用を心掛けていました。

AppサーバーのEC2は、CPU使用率が80%~90%になってもサービスが落ちたりすることはありませんが、50%を超えるとサーバーレスポンスに影響が出始めます。

レスポンスを悪化させないためには、CPU使用率はなるべく50%以下に抑えたいところです。

静的ファイルのサーバー分離

静的コンテンツへのリクエスト数は、動的コンテンツの10倍以上あります。

静的ファイルのアクセスをAppサーバーから分離させればAppサーバーの接続数を大幅に削減でき、リソースを動的コンテンツに集中させることができます。

Webサイトホスティング設定をしたS3に分離する方法もありましたが、リリースフローが複雑になります。

CloudFrontにキャッシュさせれば、リリースフローも変更する必要がなく、エッジローケーションからの配信になるのでブラウザレスポンスの改善も期待できます。

CloudFrontの費用

img.lancers.jpにサムネイルをキャッシュしたときの月額費用は約月4万円(当時)でした。

サムネイルのアクセスと静的コンテンツのアクセス数は大体同じくらいでした。

CloudFrontが値下げされた時期でもあり、Appサーバー1台分よりも費用が抑えられそうな見積りができ、CloudFrontにキャッシュさせる案を採用することにしました。

導入手順

CloudFront用のドメインを定義

静的ファイル用に以下のドメインを定義します。

  • static.lancers.jp
    • CloudFrontのAレコードAlias
  • static-origin.lancers.jp
    • WWWサーバーで設定するドメイン

www.lancers.jpドメインでアクセスしていた静的ファイルを、以下のようにstatic.lancers.jpドメインでアクセスさせるようにします。

https://www.lancers.jp/touch_icon.png

https://static.lancers.jp/touch_icon.png

static.lancers.jpドメインでアクセスすると、CloudFrontを経由してキャッシュされ、2回目以降はCloudFrontのエッジロケーションから配信されます。

WWWサーバーの設定

static-origin.lancers.jp用のホスト設定を定義します。
Nginxの場合は以下のようになります。(動的コンテンツのphpはアクセスできないように設定)

server {
    listen 80;
    server_name static-origin.lancers.jp;
    root /var/www/lancers/app/webroot;
 
    location ~ \.(css|gif|ico|jpeg|jpg|js|otf|pdf|png|svg|swf|ttf|woff|woff2|zip)$ {
        expires max;
    }
 
    location ~ \.php$ {
        deny all;
    }
 
    location ~ /\. {
        deny all;
    }
}

CloudFrontのディストリビューションを作成

static-origin.lancers.jpをオリジンとしたCloudFrontのディストリビューションを作成します。
static.lancers.jpのAレコードAliasにCloudFrontのドメインを関連付けます。

静的ファイルのアクセスをstatic.lancers.jpに変更

www.lancers.jp でアクセスされていた静的ファイルをstatic.lnacers.jpに変更します。
静的ファイル読み込み箇所のソースを以下のように修正します。

<img src="/touch_icon.png">
↓
<img src="<?= Configure::read('static.url') ?>/touch_icon.png">

この対応は1300箇所以上あり、地道な作業になりました。
ただし、一度に全て変更する必要はないため、徐々に対応していきました。

導入後の問題とその解決方法

クエリストリングのハッシュ化

CloudFrontのキャッシュ更新は、img.lancers.jpのときと同様、クエリストリングを付与し、静的ファイル更新時にクエリストリングも更新させる必要があります。

しかし、この更新作業が開発者、特にデザイナーの方々の実装負荷を上げてしまうことになりました。

img.lancers.jpのときは、更新日時をDBから取得していましたが、静的ファイルの場合は?v1.4のようなバージョン表記にしたため、都度更新する必要があります。

そこで、アプリエンジニアの方に作っていただいたのが、git mergeのタイミングでハッシュ値を生成させる仕組みです。

具体的には、.git/hooks/post-merge でcommit_hash.phpというファイルを生成します。

commit_hash.php には、以下の様なハッシュ値の定義が出力されます。

Configure::write('static_file_version', '78152cf38634670a1a631275f1288f34a9d71e90'));

このハッシュ値をクエリストリングにすることで、リリース毎にクエリストリングが切り替わる仕組みです。
https://static.lancers.jp/touch_icon.png?v=78152cf38634670a1a631275f1288f34a9d71e90

リリースの順番

静的ファイルにCloudFrontを適用後、リリース時にハッシュ値を更新したにも拘わらず、キャッシュが更新されない事象が度々発生していました。

全てのソースを転送し終わる前にcommit_hash.phpを転送してしまうと、静的画像が更新される前にクエリストリングのハッシュ値が更新されてしまい、古い静的画像が再びキャッシュされてしまうためです。

リリース時は、最新のソースを全て転送し、最後にcommit_hash.phpを転送する必要があります。

また、Appサーバーが複数ある場合は、各Appサーバーへのリリースの時間差で同様の問題が発生することもあります。
commit_hash.phpの転送は、全サーバーへ極力同じタイミングで行う必要があります。

※シンボリックリンクを切り替えるリリース方式の場合も同様に、切り替えを最後に、そして極力同時に行う必要があります。

CORS設定

例えば、WEBフォントのwoffファイルは、サイズが大きい為、特にCloudFrontにキャッシュさせたいファイルですが、当初、woffファイルをstatc.lancers.jpで参照させるとCORSのエラーが出力されていました。

この問題を解決させるためには、static.lancers.jp側にwww.lancers.jpからの利用を許可させる必要があります。

具体的には、以下のようにAccess-Control-Allow-Originヘッダを付与する必要があります。

Access-Control-Allow-Origin: https://www.lancers.jp

Nginx側で以下のように設定しました。

server {
    listen 80;
    server_name static-origin.lancers.jp;
    root /var/www/lancers/app/webroot;
 
    set $cors "";
    if ($http_origin ~* https://www\.lancers\.jp$) {
        set $cors "true";
    }
 
    location ~ \.(css|gif|ico|jpeg|jpg|js|otf|pdf|png|svg|swf|ttf|woff|woff2|zip)$ {
        if ($cors = "true") {
            add_header Access-Control-Allow-Origin "$http_origin";
        }
        expires max;
    }
…

最後に

静的ファイルのアクセス負荷対策として、WebサーバーとAppサーバーを分離させる方法がよく紹介されていますが、CloudFrontを導入すれば、Webサーバー側の役目をほぼ担ってくれます。

大きなサーバー構成の変更が必要なく、ブラウザレスポンスの改善も期待できますので、CloudFrontにキャッシュさせるという方法も選択肢の一つになるかと思います。