読者です 読者をやめる 読者になる 読者になる

おぎろぐはてブロ

なんだかんだエンジニアになって10年以上

CloudFront+nginx+S3で動的にgzip圧縮したデータを転送する

前回の記事でCloudFront + S3でgzipで圧縮する方法について書きました。

オリジンがS3の場合、CloudFrontで動的にgzipしたりgzip版を応答してくれたりはせず、ノーマルとgzip版両方をS3にアップして、リンクを張る側で、クライアントのヘッダみて、URLを切り替えろということで、微妙です。 たとえば、ユーザがデータをアップロードするような場合、両方をアップロードしてもらうわけにもいかないので、アップロードしたあとに、S3にgzip圧縮したデータを別途アップしないといけません。

ということで、その解決案として、CloudFrontとS3の間にgzip圧縮するサーバをはさむのを試してみました。

まずレスポンスの確認

S3においたファイルと、それをオリジンにしたCloudFrontディストリビューションがあるとします。

http://s3.amazonaws.com/<bucket name>/10kb.txt
http://<hogehuga>.cloudfront.net/10kb.txt

これらがgzip圧縮してくれないことをまず確認してみます。 curl には --compressed オプションがあり、これを指定するとそのクライアントで対応している圧縮アルゴリズムを Accept-Encoding ヘッダで送ります。(といっても、deflate, gzip くらいだけど)

curl -vs --compressed http://s3.amazonaws.com/<bucket name>/10kb.txt > /dev/null 

S3 に Accept-Encoding: gzip

Content-EncodingやContent-Lengthを確認。 Content-Encodingのヘッダがないということで無視されている。

curl -vs --compressed http://s3.amazonaws.com/<bucket name>/10kb.txt > /dev/null 
* About to connect() to s3.amazonaws.com port 80 (#0)
*   Trying 207.171.185.200...
* connected
* Connected to s3.amazonaws.com (207.171.185.200) port 80 (#0)
> GET /<bucket name>/10kb.txt HTTP/1.1
> User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8x zlib/1.2.5
> Host: s3.amazonaws.com
> Accept: */*
> Accept-Encoding: deflate, gzip
> 
< HTTP/1.1 200 OK
< x-amz-id-2: qtDomeuOQjZ15aku3kjxkIde4nrkYOmfCuOFFZmqM7jC7u0PM+j5FjqGfwfpB0my
< x-amz-request-id: 2A81C875DFE2380A
< Date: Sat, 24 Aug 2013 06:43:35 GMT
< x-amz-meta-cb-modifiedtime: Fri, 07 Dec 2012 09:37:29 GMT
< Last-Modified: Fri, 07 Dec 2012 09:37:59 GMT
< ETag: "890e08d20d36f30a07bfa6f12cd2766c"
< Accept-Ranges: bytes
< Content-Type: text/plain
< Content-Length: 10056
< Server: AmazonS3
< 
{ [data not shown]
* Connection #0 to host s3.amazonaws.com left intact
* Closing connection #0

CF + S3 に Accept-Encoding: gzip

無視される。

% curl -vs --compressed http://<hogehuga>.cloudfront.net/10kb.txt > /dev/null 
* About to connect() to <hogehuga>.cloudfront.net port 80 (#0)
*   Trying 54.230.126.72...
* connected
* Connected to <hogehuga>.cloudfront.net (54.230.126.72) port 80 (#0)
> GET /10kb.txt HTTP/1.1
> User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8x zlib/1.2.5
> Host: d25zooweal4ijh.cloudfront.net
> Accept: */*
> Accept-Encoding: deflate, gzip
> 
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Content-Length: 10056
< Connection: keep-alive
< Date: Sat, 24 Aug 2013 06:35:42 GMT
< x-amz-meta-cb-modifiedtime: Fri, 07 Dec 2012 09:37:29 GMT
< Last-Modified: Fri, 07 Dec 2012 09:37:59 GMT
< ETag: "890e08d20d36f30a07bfa6f12cd2766c"
< Accept-Ranges: bytes
< Server: AmazonS3
< Age: 303
< Via: 1.0 b059f1ba79e67e3e32832cc6db90d5b0.cloudfront.net (CloudFront)
< X-Cache: Hit from cloudfront
< X-Amz-Cf-Id: qjGjEY7EitPcLXmBAq6vp3tT3q4yB_gv7l9-_FeQg_ftVbqPE2mTRg==
< 
{ [data not shown]
* Connection #0 to host <hogehuga>.cloudfront.net left intact
* Closing connection #0

S3においたgzipファイルにアクセス

元ファイルを gzip -p 10kb.txt で圧縮して、S3にアップロード、メタデータでContent-Encodingをgzipにする。

curl -vs --compressed http://s3.amazonaws.com/<bucket name>/10kb.txt.gz > /dev/null 
* About to connect() to s3.amazonaws.com port 80 (#0)
*   Trying 207.171.187.117...
* connected
* Connected to s3.amazonaws.com (207.171.187.117) port 80 (#0)
> GET /<bucket name>/10kb.txt.gz HTTP/1.1
> User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8x zlib/1.2.5
> Host: s3.amazonaws.com
> Accept: */*
> Accept-Encoding: deflate, gzip
> 
< HTTP/1.1 200 OK
< x-amz-id-2: 14/OLjnbTs/K6EtUiU8KIveux95rgteGtg3mvH29/GAuByVlI6/rkuT0cY04QeAg
< x-amz-request-id: 73B7F16ECFD1B58E
< Date: Sat, 24 Aug 2013 06:50:14 GMT
< Content-Encoding: gzip
< Last-Modified: Sat, 24 Aug 2013 06:49:49 GMT
< ETag: "08e99c736b41e38f83381a6d7d80fd99"
< Accept-Ranges: bytes
< Content-Type: application/x-gzip
< Content-Length: 3236
< Server: AmazonS3
< 
{ [data not shown]
* Connection #0 to host s3.amazonaws.com left intact
* Closing connection #0

nginxを挟む準備

nginxのセットアップ

せっかくなので、先日発表されたNGINX PlusのMarketplaceのAMIを使ってみました。 といっても、特にPlusな機能を使ってはないですが。。

/etc/nginx/nginx.conf を以下のように修正。

http {    

  ....

  gzip  on; 
  gzip_types text/plain;
  gzip_proxied any;
  gzip_http_version 1.0;

  server {
    listen       80 default_server;
    server_name  localhost;


    location / { 
      proxy_pass   http://<bucket name>.s3.amazonaws.com;
    }   
  }
}
  • gzip_types は、デフォルトは text/html が設定されているので、それ以外も指定したい場合は記述する。
  • gzip_proxied と gzip_http_version は、Serving Compressed Files - Amazon CloudFrontに書かれているように、それぞれ any と 1.0 に指定する必要がある。

動作確認

立てたnginxに直接リクエストをなげてみる。

Content-Encodingがzipということで、gzipされている。

curl -vs --compressed http://ec2-54-225-44-42.compute-1.amazonaws.com/10kb.txt > /dev/null
* About to connect() to ec2-54-225-44-42.compute-1.amazonaws.com port 80 (#0)
*   Trying 54.225.44.42...
* connected
* Connected to ec2-54-225-44-42.compute-1.amazonaws.com (54.225.44.42) port 80 (#0)
> GET /10kb.txt HTTP/1.1
> User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8x zlib/1.2.5
> Host: ec2-54-225-44-42.compute-1.amazonaws.com
> Accept: */*
> Accept-Encoding: deflate, gzip
> 
< HTTP/1.1 200 OK
< Server: nginx/1.5.3
< Date: Sat, 24 Aug 2013 07:51:44 GMT
< Content-Type: text/plain
< Transfer-Encoding: chunked
< Connection: keep-alive
< x-amz-id-2: 8WTWlZCJJhrBef+mt/qXUyHl8M5977IL7DBXIK1EbQQ2XtkrcQBHe956BQm0bNvJ
< x-amz-request-id: F37280AC618047F0
< x-amz-meta-cb-modifiedtime: Fri, 07 Dec 2012 09:37:29 GMT
< Last-Modified: Fri, 07 Dec 2012 09:37:59 GMT
< Content-Encoding: gzip
< 
{ [data not shown]
* Connection #0 to host ec2-54-225-44-42.compute-1.amazonaws.com left intact
* Closing connection #0

ちゃんとgzipをAcceptしてないときは、圧縮されない。

curl -vs  http://ec2-54-225-44-42.compute-1.amazonaws.com/10kb.txt > /dev/null 
* About to connect() to ec2-54-225-44-42.compute-1.amazonaws.com port 80 (#0)
*   Trying 54.225.44.42...
* connected
* Connected to ec2-54-225-44-42.compute-1.amazonaws.com (54.225.44.42) port 80 (#0)
> GET /10kb.txt HTTP/1.1
> User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8x zlib/1.2.5
> Host: ec2-54-225-44-42.compute-1.amazonaws.com
> Accept: */*
> 
< HTTP/1.1 200 OK
< Server: nginx/1.5.3
< Date: Sat, 24 Aug 2013 07:54:58 GMT
< Content-Type: text/plain
< Content-Length: 10056
< Connection: keep-alive
< x-amz-id-2: v9R+2mR1ppQOL3d3BCcB2M0pUfLqMWV02VOEGcfYHuMdsY2cv3QpiZ+9ClWUqX63
< x-amz-request-id: E5D30C5B3CF028DC
< x-amz-meta-cb-modifiedtime: Fri, 07 Dec 2012 09:37:29 GMT
< Last-Modified: Fri, 07 Dec 2012 09:37:59 GMT
< ETag: "890e08d20d36f30a07bfa6f12cd2766c"
< Accept-Ranges: bytes
< 
{ [data not shown]
* Connection #0 to host ec2-54-225-44-42.compute-1.amazonaws.com left intact
* Closing connection #0

CloudFrontのオリジンの設定

  • 上でつくったEC2インスタンスをカスタムオリジンに追加する。ほんとはELBとかで冗長化する。
  • デフォルトはS3で、.js とか .cssとか圧縮が効きそうな静的コンテンツをnginx経由にするといい
  • 設定はすぐに反映されないのでしばし待つ。

CloudFront経由の動作確認

CF経由で叩いたらgzipされてきた。Serverはnginxになっている。

curl -vs --compressed http://d25zooweal4ijh.cloudfront.net/10kb.txt > /dev/null
* About to connect() to d25zooweal4ijh.cloudfront.net port 80 (#0)
*   Trying 54.230.125.5...
* connected
* Connected to d25zooweal4ijh.cloudfront.net (54.230.125.5) port 80 (#0)
> GET /10kb.txt HTTP/1.1
> User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8x zlib/1.2.5
> Host: d25zooweal4ijh.cloudfront.net
> Accept: */*
> Accept-Encoding: deflate, gzip
> 
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Connection: close
< Server: nginx/1.5.3
< Date: Sat, 24 Aug 2013 08:14:05 GMT
< x-amz-meta-cb-modifiedtime: Fri, 07 Dec 2012 09:37:29 GMT
< Last-Modified: Fri, 07 Dec 2012 09:37:59 GMT
< Content-Encoding: gzip
< Via: 1.0 87d94077df092e259fba28f63a836031.cloudfront.net (CloudFront)
< X-Cache: Miss from cloudfront
< X-Amz-Cf-Id: cGJmor0SgquEbl4nc0UU6UqUZZEGOdnivx3L8Q7JiSQ4J437Sp-McQ==

Accept-Encoding未指定では、圧縮されてない。

% curl -vs http://<hogehuga>.cloudfront.net/10kb.txt > /dev/null 
* About to connect() to <hogehuga>.cloudfront.net port 80 (#0)
*   Trying 205.251.212.149...
* connected
* Connected to <hogehuga>.cloudfront.net (205.251.212.149) port 80 (#0)
> GET /10kb.txt HTTP/1.1
> User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8x zlib/1.2.5
> Host: <hogehuga>.cloudfront.net
> Accept: */*
> 
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Content-Length: 10056
< Connection: keep-alive
< Server: nginx/1.5.3
< Date: Sat, 24 Aug 2013 08:15:49 GMT
< x-amz-meta-cb-modifiedtime: Fri, 07 Dec 2012 09:37:29 GMT
< Last-Modified: Fri, 07 Dec 2012 09:37:59 GMT
< ETag: "890e08d20d36f30a07bfa6f12cd2766c"
< Accept-Ranges: bytes
< Via: 1.0 21c5a471854ae09b0d11ecbe2274ce47.cloudfront.net (CloudFront)
< X-Cache: Miss from cloudfront
< X-Amz-Cf-Id: 9a5wIN164VzmIRE_1auQCchXoGtM77a4WUVdgZLgK5RnyYKZXgMQ3A==
< 
{ [data not shown]
* Connection #0 to host <hogehuga>.cloudfront.net left intact
* Closing connection #0

ちゃんと、CloudFront側のキャッシュに未圧縮と圧縮済みの両方がのっかる。

まとめ

  • CloudFront + nginx (reverse proxy) + S3 でS3のファイルを動的にgzip圧縮して返却することができた
  • 信頼性の高いCFとS3の間にreverse proxyを置くことでぐっと信頼性が下がるのが微妙である。ELBで冗長化を持たせたりする必要があり、元の構成よりだいぶ複雑になる
  • オリジンがS3でなく、EC2などであれば、オリジンの手前にリバースプロキシをキャッシュ層として挟むことにより、二段キャッシュ構成になり、各エッジからの問い合わせが吸収されてオリジンへの負荷が減るけど、そんなにエッジがない認識なのでさほど負荷削減の効果はないような気はする
  • nginxが応答しない場合に、S3に取りにいってくれれば、冗長性気にしなくてよくなるのだけど、いい方法が思いつかなかった

2013/09/22 追記

  • nginxが応答しない場合に、S3に取りにいってくれれば、冗長性気にしなくてよくなるのだけど、いい方法が思いつかなかった

CloudFrontディストリビューションのCNAMEをRoute53で管理して、Route53はCloudFrontに対してHealthCheckしてS3オリジンにフェイルオーバーできそうな気がする。(ファイルがpublicで署名不要な場合に限る)

署名が必要な場合は、 * CloudFront -> 圧縮サーバ -> S3 * CloudFront -> S3 の2つのディストリビューションをつくって、同じCNAMEでRoute53でデフォルト前者で後者にフェイルオーバーするようにする。 で行けそうな気がする。(検証していない) CNAMEをつくるので、SSLの場合は独自証明書が必要。

参考