おぎろぐはてブロ

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

boto.s3で大文字を含むバケットにアクセスする

大文字を含むバケットをUS Standardでは作ることができるのですが、そのバケットにbotoでアクセスしようとすると以下のように怒られます。

Bucket names cannot contain upper-case characters when using either the sub-domain or virtual hosting calling format.

言ってることは解りますが、じゃ、どうしたらアクセスできるのか。というと、calling_formatというオプションを明示的に指定する必要があります。

calling format とは

S3へのアクセススタイル。以下が定義されている。 詳しくは s3/connection.py を参照。

  • SubdomainCallingFormat: サブドメイン。これがデフォルト
  • VHostCallingFormat: 独自ドメインだと思う
  • OrdinaryCallingFormat: パススタイル
  • ProtocolIndependentOrdinaryCallingFormat: Ordinaryだけどscheme (http/https) を明示しない何か

大文字を含んでいる場合は OrdinaryCallingFormat を利用すればよいです。

指定方法

設定ファイル

[s3]
calling_format = boto.s3.connection.OrdinaryCallingFormat

S3Connectionのコンストラクタ

from boto.s3.connection import OrdinaryCallingFormat, S3Connection

conn = S3Connection(calling_format=OrdinaryCallingFormat)

2.13.2 以前のboto

仕様が変わっていて、設定ファイルでの指定は不可。また、クラスもしくはクラス名ではなく、インスタンスを渡す必要がある。

from boto.s3.connection import OrdinaryCallingFormat, S3Connection

conn = S3Connection(calling_format=OrdinaryCallingFormat())

S3のバケット名の制約

S3では、CNAMEでない場合、パススタイルかバーチャルホストスタイルの2つでバケットにアクセスすることができます。以下はUS Standardの場合。リージョンがこれ以外だと s3-ap-northeast-1.amazonaws.com みたいになります。

http://s3.amazonaws.com/mybucket
http://mybucket.s3.amazonaws.com

エンドポイントについてはここを確認。

Amazon S3のバケット名の制約は、全般としてはDNS準拠となっていますが、米国スタンダードリージョンは歴史的経緯か、少しゆるい制約となっています。

設定したバケット名によって制限が出る場合があるのでまとめてみます。

DNS準拠、仮想ホストスタイル (SSL)利用可

https://mybucket.s3.amazonaws.com のようにワイルドカード証明書でバーチャルホストスタイルでアクセスさせたい場合、一番制限が厳しいものになります。

  • 3~63 文字以内
  • 先頭、末尾は小文字の英文字または数字を使う
  • 先頭、末尾以外は小文字の英文字、数字、およびハイフン(-)を含めることができる

ということで、

^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$

DNS準拠、仮想ホストスタイル (HTTP)利用可

http://my.bucket.s3.amazonaws.com のように非SSLでバーチャルホストスタイルでアクセスさせたい場合。

  • 3~63 文字以内
  • バケット名は、1 つのラベルか、または複数のラベルをピリオド(.)でつなげて構成します。各ラベルには次の規則が適用されます。
    • 先頭、末尾は小文字の英文字または数字を使う
    • 先頭、末尾以外は小文字の英文字、数字、およびハイフン(-)を含めることができる
  • バケット名は、IP アドレスの形式(192.168.5.4 など)にはできない

S3で静的ウェブサイトを独自ドメインで行う場合はドメイン名をそのままバケット名にする必要があります。

米国スタンダードリージョンのみ、仮想ホストスタイル不可

US Standardリージョンのみで作成できます。

  • 255文字以内
  • 大文字と小文字の英文字、数字、ピリオド(.)、ハイフン(-)、アンダースコア(_)を自由に組み合わせられる。

大文字を含んだ場合、小文字として解決しようとするため、バーチャルホストスタイルではNot Foundとなりますし、DNS準拠しないホスト名となった場合はバーチャルホストスタイルでは参照できません。

まとめ

DNS名準拠のバケット名を使おう。

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の場合は独自証明書が必要。

参考

CloudFrontでgzip圧縮したデータを転送する

2015/12 追記

2015年12月にCloudFront Distributionの設定でエッジでgzip圧縮を行うことができるようになりました。便利!

aws.typepad.com

(以下、2013年8月時点での記事となります)

HTMLやCSSはgzipで圧縮して転送したいもの。ということで、CloudFrontでgzip転送する方法を確認したところ、簡単にできる感じでもなかったので軽くまとめる。 ドキュメントはここ

クライアントが Accept-Encoding: gzip をヘッダに含めてリクエストしてきたときのみに圧縮したデータを返却することができる。gzipのみ対応しており、deflateやsdchを指定されても無視する。

とはいえ Accept-Encoding を指定するだけで圧縮してくれるわけではない。これは、そのコンテンツを圧縮すべきかどうかの判断をする術がCloudFront側にないため。(圧縮率が悪いデータを圧縮・展開してもオーバーヘッドがかかるだけ、最悪サイズが増加してしまう場合もあって、一律圧縮するというのは効率がよくありません。)

オリジン側の対応についてみていく。

S3以外のカスタムオリジンの場合

オリジンをS3以外のApacheやNGINXなどのWebサーバを使う場合。

  1. webサーバ側でどのコンテンツを圧縮するか決めて設定する。拡張子を見て圧縮させるようなディレクティブを書いたりする
  2. ふつうにCloudFrontのディストリビューションを作成する
  3. webブラウザがCloudFrontのエッジにリクエストを飛ばす
  4. エッジはAccept-Encoding: gzipのヘッダがあれば、圧縮バージョンのキャッシュを探し、そうでなければ無圧縮のキャッシュを探す
  5. キャッシュがあればそれをwebブラウザに応答し、なければオリジンにリクエストを投げる
  6. エッジからのリクエストで、Accept-Encoding: gzipで且つ、上で圧縮対象にしたコンテンツならば圧縮されたデータを返却し、エッジにキャッシュされる

CloudFrontのエッジでは、圧縮されたものと、無圧縮なものと2パターンのキャッシュを保持するようになる感じ。Webサーバの設定は特に面倒なものではない。

S3がオリジンの場合

動的な生成などがなければ、S3をオリジンにすることが多いと思うのですが、カスタムオリジンよりも設定が面倒です。

  1. 圧縮対象のコンテンツについて、圧縮したものと、無圧縮のものと各2種類ずつデータを用意する。welcome.js があるとして、その圧縮版は welcome.js.gz。
  2. 両方をS3にアップロードする
  3. 圧縮したファイルについては、Content-Encoding: gzip のヘッダをManagement Consoleなどでつける
  4. 動的に生成するHTMLからCloudFrontへのリンクをする際に、クライアントが Accept-Encoding: gzip をヘッダに乗せてきたら welcome.js.gz の方を参照させる。

ということで、S3側はAccept-Encodingに応じて圧縮するとかそういった処理を行わないので、HTML生成時に動的にリンク先を変えろということ。

まとめ

  • CloudFrontは動的にgzip圧縮するようなことはなく、圧縮済みのデータを持たせることで対応する
  • S3をオリジンとした場合、Accept-Encodingを見て圧縮済みか未圧縮のデータを返すかという処理ができない

まだ続きます。。