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

EC2オリジンのCloudFrontでサムネイルをキャッシュした話

kanazawa|2017年12月04日
Apache

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

インフラエンジニアの金澤です。
少し古いネタになりますが、CloudFrontでサムネイルをキャッシュした手順を記録として残しておきたいと思います。

サムネイルの生成処理について

ランサーズは、2012年5月にAWSに移行しました。
ランサーズでは、プロフィール画像や提案画像をサムネイル処理しています。
例えば、ランサーズのコンペの閲覧一覧で閲覧できるロゴ等の提案画像は、圧縮、縮小されたサムネイル画像です。
(オリジナル画像はS3にあり、仕事を依頼したクライアントしか見ることができません)

これらのサムネイル画像は、オリジナル画像をImageMagickで圧縮、縮小して表示します。
この処理は大きな負荷がかかるため、一度作成したサムネイルはNFSに保管しキャッシュしていました。

サムネイル生成とキャッシュ

ランサーズでは、サムネイル画像をimg.lancers.jpというドメインで管理し、URLでアクセスされたタイミングでサムネイルを生成し、NFSに保管しています。
Apacheで、以下のように設定すると、

<VirtualHost *:80>
    ServerName img.lancers.jp
    DocumentRoot /var/www/lancers/app/tmp/thumbs
    <IfModule mod_rewrite.c>
        RewriteEngine On
        RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f
        RewriteRule ^/([a-z]+)/([0-9a-f]+)/([0-9a-f]+)/([0-9a-f]+)_([0-9]+)_([0-9]+)([_]*)([0-9]*).jpg$ http://www.lancers.jp/file/$1/$5/$6/$8 [R]
    </IfModule>
        <Directory /var/www/lancers/app/tmp/thumbs>
            Order allow,deny
            Allow from all
        </Directory>
</VirtualHost>

以下のように動作します。

  • NFSにサムネイルがキャッシュされていない場合
    • www.lancers.jpにリダイレクト
    • PHP経由でサムネイルを生成して返す
    • 生成したサムネイルをNFSにキャッシュ

  • NFSにサムネイルがキャッシュされている場合
    • キャッシュのサムネイルをそのまま返す

NFSのストレージ逼迫問題

サービスが成長していくにつれ、サムネイルの数も増え、NFSのストレージ容量が逼迫してきました。
当時のEC2は、ストレージ容量を拡張するには、一度AMIを取得してから再作成しなければなりませんでした。
NFSは常時稼働しているので、メンテナンスを入れる必要がありましたが、AMIの取得に3時間以上かかり、メンテナンス時間内に終わるか見積りが難しく、キャッシュ期間を短くすることで対応していました。
また、当時のEC2は1TBまでしかストレージを拡張できなかったので、拡張できてもいつかは限界が来ることが見えていました。

キャッシュをNFSからCloudFrontへ

ストレージ逼迫問題を解決する手段として、サムネイルもS3に格納する方法がまず思い浮かびます。
ですが、ストレージをS3にするとサムネイル存在チェックの遅延時間が長くなるため断念したそうです(当時の担当者談)
そこで、サムネイルをCloudFrontにキャッシュさせ、NFSを撤廃することを試みました。
img.lancers.jpを、ELBからCloudFrontのアドレスに変更し、ELBにはimg-origin.lancers.jpというアドレスを対応させます。

  • CloudFrontにサムネイルがキャッシュされていない場合
    • PHP経由でサムネイルを生成して返す
    • 生成したサムネイルはCloudFrontにキャッシュされる

  • CloudFrontにサムネイルがキャッシュされている場合
    • CloudFrontのキャッシュが返される
      • ELB、Appサーバーには到達しない


Apacheの設定を以下のように変更します。
※/var/www/lancers/app/tmp/thumbs はNFSから各EC2サーバーのディレクトリに設定変更し、サムネイル生成時の一時保管場所としてのみ利用します。

<VirtualHost *:80>
    ServerName img-origin.lancers.jp
    DocumentRoot /var/www/lancers/app/tmp/thumbs
    <IfModule mod_rewrite.c>
        RewriteEngine On
        RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f
        RewriteRule ^/([a-z]+)/([0-9a-f]+)/([0-9a-f]+)/([0-9a-f]+)_([0-9]+)_([0-9]+)([_]*)([0-9]*).jpg$ http://www.lancers.jp/file/$1/$5/$6/$8 [R]
    </IfModule>
    <Directory /var/www/lancers/app/tmp/thumbs>
        Order allow,deny
        Allow from all
    </Directory>
</VirtualHost>

ところが、この設定だとCloudFrontにはキャッシュされません。
img.lancers.jp→www.lancers.jpにリダイレクトされてしまうため、サムネイルの生成過程でCloudFrontを経由しなくなるためです。
img.lancers.jpドメインを維持したままサムネイルを生成させるため、リダイレクトではなく自分自身へプロキシさせる形にしました。

このときのApacheの設定は以下のようになります。

<VirtualHost *:80>
    ServerName img-origin.lancers.jp
    DocumentRoot /var/www/lancers/app/tmp/thumbs
    <IfModule mod_rewrite.c>
        RewriteEngine On
        RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f
        RewriteRule ^/([a-z]+)/([0-9a-f]+)/([0-9a-f]+)/([0-9a-f]+)_([0-9]+)_([0-9]+)([_]*)([0-9]*).jpg$ http://127.0.0.1/file/$1/$5/$6/$8 [P]
    </IfModule>
    <Directory /var/www/lancers/app/tmp/thumbs>
        Order allow,deny
        Allow from all
    </Directory>
</VirtualHost>

※Nginx化したときは、以下のように設定しました。

server {
    listen 80;
    server_name img-origin.lancers.jp;
 
    root /var/www/lancers/app/tmp/thumbs;
 
    location ~ ^/([a-z]+)/([0-9a-f]+)/([0-9a-f]+)/([0-9a-f]+)_([0-9]+)_([0-9]+)([_]*)([0-9]*).jpg$ {
        limit_req zone=one burst=100;
        proxy_set_header X-Forwarded-Server $host;
        proxy_pass http://127.0.0.1/file/$1/$5/$6/$8;
    }
 
    location ~ \.php$ {
        deny all;
    }
 
    location ~ /\. {
        deny all;
    }
}

CloudFrontのTIPS

証明書のアップロード

AWSの証明書の場合

CloudFrontで、AWSのCertificate Managerで作成したSSL証明書を利用したい場合は、「バージニア北部」のリージョンで作成する必要があります。

その他の証明書の場合

ELBとCertificate ManagerCloudFrontで共通の証明書を使いたい場合、マネジメントコンソールではなく、AWS CLIで登録する必要があります。
※詳細はQiitaに書かせていただきました。https://qiita.com/yKanazawa/items/f4362a21ead88888bffd

キャッシュの更新

一度CloudFrontにサムネイルがキャッシュされると、サムネイル画像が更新されても同じURLでは画像が切り替わりません。
そこで、URLにクエリ文字列を付与して対応します。
参考:クエリ文字列パラメータに基づいてキャッシュするように CloudFront を設定する
CloudFrontのBehavior設定で、以下のように設定しておくと、クエリ文字列が変更されたときに、再度オリジンのEC2に読み込みにいきます。

ランサーズでは、サムネイル画像の更新時に更新日時をクエリ文字列に付与して対応しています。
https://img.lancers.jp/userprofile/6/c/6ccc3…e7515_225170_130.jpg?20171130092919

キャッシュの確認

アクセスした画像が、CloudFrontのキャッシュが返したものなのか、オリジンのEC2が返したものなのかを確認したい場合は、Response HeadersのX-Cacheヘッダを確認します。

  • CloudFrontのキャッシュから返された場合
    • X-Cache: Hit From CloudFront
  • オリジンのEC2から返された場合
    • X-Cache: Miss From CloudFront

Chromeの場合は、デベロッパーツールの「Network」タブで確認できます。

アクセスログの取得

CloudFrontを経由すると、WebサーバーのアクセスログのRefererがすべて”Amazon CloudFront”になってしまいます。
(リモートIPアドレスも、CloudFrontのエッジロケーションのものになります)

10.0.x.xx [10/Jun/2014:17:32:30 +0900] 6.124.41.xx.xxx - - [10/Jun/2014:17:32:30 +0900] "GET / HTTP/1.1" 200 24154 "-" "Amazon CloudFront" xxx ms
10.0.x.xx [10/Jun/2014:17:31:39 +0900] 60 66.249.xx.xxx - - [10/Jun/2014:17:31:39 +0900] "GET /proposal/search/award HTTP/1.1" 200 12331 "-" "Amazon CloudFront" xxx ms
10.0.x.xx [10/Jun/2014:17:32:09 +0900] 60 66.249.xx.xxx - - [10/Jun/2014:17:32:09 +0900] "GET /proposal/search/award HTTP/1.1" 200 12331 "-" "Amazon CloudFront" xxx ms

Refererを確認するときは、CloudFrontのアクセスログをS3から取得する必要があります。

アクセスログの設定

CloudFrontのGeneral設定で、以下のように設定すれば

S3バケットにログが格納されます。

アクセスログフォーマット

以下のフォーマットで保存されています。

#Version: 1.0
#Fields: date time x-edge-location sc-bytes c-ip cs-method cs(Host) cs-uri-stem sc-status cs(Referer) cs(User-Agent) cs-uri-query cs(Cookie) x-edge-result-type x-edge-request-id x-host-header cs-protocol cs-bytes time-taken
2014-10-01  19:50:21    MIA50   417 66.249.71.8 GET xxxxxxxxxxxxx.cloudfront.net    /proposal/f/e/fec...ec2_3989074_150.jpg  307 -   Googlebot-Image/1.0 20140929163745?timestamp=1410048000048  -   Miss    dxn...maQ==    img.lancers.jp  http    307 0.400
2014-10-01  19:50:22    MIA50   417 66.249.71.8 GET xxxxxxxxxxxxx.cloudfront.net    /proposal/b/5/b5b...abd_3915699_150.jpg  307 -   Googlebot-Image/1.0 20140912094937?timestamp=1410048000041  -   Miss    aRH...mlw==    img.lancers.jp  http    307 0.372
2014-10-01  19:50:25    MIA50   417 66.249.71.8 GET xxxxxxxxxxxxx.cloudfront.net    /proposal/0/2/029...b70_3981251_150.jpg  307 -   Googlebot-Image/1.0 20140926205158?timestamp=1410048000036  -   Miss    _MS...gRg==    img.lancers.jp  http    307 0.342
2014-10-01  19:50:28    MIA50   417 66.249.71.8 GET xxxxxxxxxxxxx.cloudfront.net    /proposal/d/d/dd6...245_3966683_150.jpg  307 -   Googlebot-Image/1.0 20140923170904?timestamp=1410048000045  -   Miss    MLd...kJw==    img.lancers.jp  http    307 0.368

アクセスログ解析

CloudFrontのアクセスログは、10~20分単位で圧縮保存されるので、ファイル数が膨大になります。
AWS CLIで一括ダウンロードして解析を行います

2014-10-02のログ全てダウンロード

aws s3 sync s3://ログバケット名/cloudfront/ ./ --exclude "*" --include "*2014-10-02*"

2014-10-02のログから、リモートIPアドレスが「xxx.xxx.xxx.xxx」であるものを抽出する

gunzip -c *2014-10-02* | grep xxx.xxx.xxx.xxx

CloudFront導入効果

  • NFSのストレージ逼迫問題の解決
    • NFSにキャッシュする必要がなくなったため、NFSは必要なくなりました
  • ブラウザレスポンスの改善
    • 約0.5s程改善されました
  • AppサーバーのCPU負荷改善
    • AppサーバーのCPU負荷が約10%程改善しました

最後に

CloudFrontの事例は、S3との連携が主に紹介されていますが、S3以外をオリジンとして利用することも可能です。
今回は、EC2で生成したサムネイルをCloudFrontにキャッシュする事例を紹介させていただきました。
ランサーズでは、サムネイル以外の静的画像にもCloudFrontを利用していますので、その事例も後日紹介させていただければ幸いです。