2012年11月5日月曜日

Fluentdってなんじゃ?(Apache編)

前回は同じサーバー内でコマンドからファイル出力を行いました。
内容としては以下のような流れです。

  +--------------------------------------+
  | server                               |
  +--------------------------------------+
  |                fluentd               |
  |                                      |
  | +-------------+       +------------+ |
  | |    input    |       |   output   | |
  | |-------------| +---> |------------| |
  | |command(tcp) |       |    file    | |
  | +-------------+       +------------+ |
  +--------------------------------------+

今回はApacheのログを別のサーバーにファイル転送してみたいと思います。
以下のようなイメージです。
  +--------------------------------------+   +--------------------------------------+
  | web server (10.0.0.8)                |   | log server (10.0.0.16)               |
  +--------------------------------------+   |--------------------------------------|
  |                fluentd               |   |               fluentd                |
  |                                      |   |                                      |
  | +-------------+       +------------+ |   | +------------+       +-------------+ |
  | |    input    |       |   output   | |   | |   input    |       |    output   | |
  | |-------------| +---> |------------| +---> |------------| +---> |-------------| |
  | |    tail     |       |    tcp     | |   | |   tcp      |       |    file     | |
  | +-------------+       +------------+ |   | +------------+       +-------------+ |
  +--------------------------------------+   +--------------------------------------+


まず、送信側のwebサーバーでは、以下のようにアパッチのログをapache.accessというタグでtailし、tcp送信するように設定します。

source

 ログのフォーマット指定でapacheを指定することでapacheのログ構造をJSON化できるようになります。
そのほかにはsyslogのフォーマットがプリセットで指定できますが、その他アプリのログなどは名前付きキャプチャの正規表現で指定することにより、任意のプロパティ名でJSON化できます。

match

 matchディレクティブで設定するforwardは、Bufferd Outputプラグインというタイプで、デフォルトではメモリにデータをバッファし60秒でflushします。
つまり60秒に一回まとめて送信を行います。今回はわかりやすくリアルタイムで行うよう、flush_intervalを0sとし、0秒でflushするように設定してみます。
また、serverディレクティブで送信先のサーバーのIPを指定します。 特に指定しない場合、ポートは24224番が使用されます。

# vim /etc/td-agent/td-agent.conf
---
<source>
  type tail
  format apache
  path /var/log/httpd/access_log
  tag apache.access
</source>

<match apache.access>
  type forward
  flush_interval 0s
  <server>
    host 10.0.0.16
  </server>
</match>
---

# /etc/init.d/td-agent restart


受信側のサーバーにもtd-agentをインストールし、以下のように、tcpを受信しファイルに出力するように設定します。

source
 webサーバーのtcpから受信するためforward Input Pluginを指定します。

match
 apache.accessタグのデータを/var/log/fluent/apache_access_log***に書き出します。

# mkdir /var/log/fluent
# chown td-agent:td-agent /var/log/fluent
# vim /etc/td-agent/td-agent.conf
---
<source>
  type forward  
</source>

<match apache.access>
  type file
  path /var/log/fluent/apache_access_log
</match>
---

# /etc/init.d/td-agent start

ここで、webサーバーとlogサーバーの間にFWなどがある場合、fluentdのポートを開放する必要があります。 今回はEC2を使用しているので、Security Groupの設定でTCPの24224番を開放します。また、データ転送にはTCPを使用しますが、死活監視にはUDPが使われるので、両方を開放しておきます。



webサーバーのコンテンツにブラウザからアクセスしてみます。



すると、logサーバー側の/var/log/fluent/apache_access_log.xxxというファイルにJSONが記録されているのがわかります。
tail -1000f /var/log/fluent/apache_access_log.20121105.b4cdb099dbbaa1e42
---
2012-11-05T05:20:11+09:00 apache.access {"host":"219.117.233.241","user":"-","method":"GET","path":"/","code":"403","size":"5039","referer":"-","agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.1 Safari/537.17"}
2012-11-05T05:20:11+09:00 apache.access {"host":"219.117.233.241","user":"-","method":"GET","path":"/icons/poweredby.png","code":"304","size":"-","referer":"http://54.249.23.246/","agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.1 Safari/537.17"}
2012-11-05T05:20:11+09:00 apache.access {"host":"219.117.233.241","user":"-","method":"GET","path":"/icons/apache_pb.gif","code":"304","size":"-","referer":"http://54.249.23.246/","agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.1 Safari/537.17"}
2012-11-05T05:20:11+09:00 apache.access {"host":"219.117.233.241","user":"-","method":"GET","path":"/favicon.ico","code":"404","size":"288","referer":"-","agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.1 Safari/537.17"}

これで、リモートのサーバーにログを送ることができました。 また、このようにApacheのログがオブジェクト化されることで、後々集計集約しやすくなりました。

次回は、ログをDBに送り、抽出してみたいと思います。
以上です。

Fluentdってなんじゃ?

今回はfluentdについて触れてみたいと思います。

普段サーバーの運用をしていて、規模が大きくなりサーバーが分散されるとWebサーバーやアプリケーションのログが各サーバーに散在することになり、ユーザーからの問い合わせや集計などのためにログをかき集めたりする必要が出てきます。
fluentdはイベントログをJSONフォーマットで転送して一箇所に集約するためのツールです。

集計などを行う場合、Apacheなどの生ログはテキストデータなのですが、fluentdを経由しJSON化することで、ログのデータをオブジェクトとして扱うことが出来るので、DBに入れてフィルタをかけて集計なども簡単に行えます。

また、fluentdは入力元となるログの種類や出力先の指定をプラグイン形式で様々なフォーマットに対応して、それぞれを組み合わせることで数多くの用途に答えることができます。
例えば以下のような入出力ができます。

入力元:
  • fluent apiを利用してアプリケーションから直接出力
  • 既存ログのtail
  • TCPソケットからのデータ
  • コマンドの実行結果
出力先:
  • ファイルに書き出み
  • 各種DBへ保存
  • SQSでキューにメッセージ送信
  • S3へ保存
  • Growlで通知
  • メール送信
  • Twitterへ投稿

こうみるとPlaggerを思い出しますね。

fluentdはruby環境が必要ですが、TreasureDataというデータウェアハウスがtd-agentというruby環境込みのstableパッケージを提供しています。td-agentを使用すると、ruby環境がない場合や既存のruby環境を壊したくない場合にとても有用です。今回はtd-agentを利用してみます。

まずはtd-agentをインストールします。
TreasureDataのyumリポジトリを登録してから、yumでインストールを行います。
# vi /etc/yum.repos.d/td.repo
---
[treasuredata]
name=TreasureData
baseurl=http://packages.treasure-data.com/redhat/$basearch
gpgcheck=0

---

# yum install td-agent -y
インストールはこれで完了です。

次に、設定ファイルを見てみます。

設定ファイルはシンプルで、source定義で指定した入力を、マッチしたmatch定義の設定で出力します。
まずは、flluentdのQuickStartにあるように付属のflluent-catを使用して、擬似的なTCPからの入力をローカルのファイルに出力するように設定します。
#vi /etc/td-agent/td-agent.conf

---
<source>
  type forward
  tag test
</source>

<match test>
  type file
  path /var/log/fluent/test
</match>
---

これは、

source
 forward Input Pluginを使用してtcpからの入力にtestというタグをつける

match test 
 file Output Pluginを使用して、タグが「test」にマッチした入力があれば、/var/log/fluent/test*** というファイルに受け取ったデータを出力する

という意味の設定になります。 設定を記述したら起動します。
#/etc/init.d/td-agent start
そしてfluent-catで以下のようにコマンドからJSONを送ってみます。
その際、引数として、タグとして設定した「test」を渡します。
# echo '{"msg":"Hello World!"}' | /usr/lib/fluent/ruby/bin/fluent-cat test
すると、以下のようなファイルが生成され、時間+タグ+JSONの形式で出力されていることがわかります。
# tail -1000f /var/log/fluent/test.20121105.b4cdae0705726cb91
2012-11-05T01:40:32+09:00 test {"msg":"Hello World!"}


ここまででは、ただファイルに入力したのと変わらないので、便利さがわかりづらいと思います。
次回はログをリモートのサーバーに転送してみたいと思います。
以上です。

2012年9月19日水曜日

Cassandraってなんじゃ?(API編)

Cassandraを利用するケースは、Webアプリケーションやバッチアプリケーションなど、プログラムからのアクセスが多いと思います。
そこで今回は、Cassandraにphpからアクセスしてみたいと思います。

CassandraにphpからアクセスするためのAPIライブラリとして以下のようなものがあるようです。
このうち、Thriftは、Cassandra専用でもPHP専用でもなく、複数の言語間でRPC通信を可能にする汎用フレームワークで、設定コードを元に各言語用のプログラムを生成します。他のライブラリはThriftで生成されたCassandraのlow-level APIをラップしたものになります。

もともとThriftを使おうと思っていましたが、Thriftの最新版 0.8.0は、Cassandra1.1.5では出力されたコードが不十分だったため、一度断念しました。機会があれば再度試してみたいと思います。

今回はphpcassaを試してみました。
phpcassaもThriftを使用しているので、Thriftが組み込まれ、すでにコード生成もされているので、インストールも非常に簡単です。

それでは早速インストールしてみます。
$ cd /usr/local/src
$ wget https://github.com/downloads/thobbs/phpcassa/phpcassa-1.0.a.5.tar.gz
$ tar xzvf phpcassa-1.0.a.5.tar.gz
$ cd phpcassa-1.0.a.5
$ phpize
$ ./configure
$ make
# make install

これでインストールは完了です。

次にアプリケーション用ディレクトリを作成し、phpcassaのライブラリをコピーします。
$ mkdir -p /home/memorycraft/app
$ cd /usr/local/src/phpcassa-1.0.a.5
$ cp -r lib /home/memorycraft/app/
$ cd /home/memorycraft/app/
$ touch test.php
$ tree -L 2
.
|-- lib
|   |-- autoload.php
|   |-- phpcassa
|   `-- thrift
`-- test.php


これでライブラリがそろったので、これらを利用してcassandraにアクセスするコードを書いてみます。
$ vim test.php
<?php
    require(dirname(__FILE__).'/lib/autoload.php');

    use phpcassa\ColumnFamily;
    use phpcassa\ColumnSlice;
    use phpcassa\Connection\ConnectionPool;

    try{
        $servers = array('localhost:9160');
        $pool = new ConnectionPool('Hogebook', $servers);
        $user = new ColumnFamily($pool, 'User');

        //データの挿入
        $user->insert('memorycraftgirl',
            array(
                'email' => 'ng@gmail.com',
                'gender' => 'female',
            )
        );

        //データの更新
        $user->insert('memorycraftgirl', array('email'=>'memorycraft+girl@gmail.com'));

        //キーで全カラムを取得  
        $girl = $user->get('memorycraftgirl');
        echo 'girl = ' . print_r($girl, true);

        //キーでカラムを指定して取得
        $email = $user->get('memorycraftgirl', null, array('email'));
        echo 'email = ' . print_r($email, true);

        //複数キーを指定して取得
        $users = $user->multiget(array('memorycraft', 'memorycraftgirl'));
        echo 'users = ' . print_r($users, true);


        //カラムの削除
        $user->remove('memorycraftgirl', array('email'));
        echo 'girl after remove column = ' . print_r($user->get('memorycraftgirl'), true);

        //行の削除
        $user->remove('memorycraftgirl');
        echo 'girl after remove row = ' . print_r($user->get('memorycraftgirl'), true);

        $pool->close();
    }
    catch(Exception $e){
        echo 'ERROR : ' . print_r($e, true);
    }
?>

このコードでは前回作成したHogebookのUserというColumn Familyに対して、挿入/更新/取得/削除を行い、その都度結果や、Column Familyの状態を表示するようにしました。

それでは実行してみます。
$ php test.php
girl = Array
(
    [email] => memorycraft+girl@gmail.com
    [gender] => female
)
email = Array
(
    [email] => memorycraft+girl@gmail.com
)
users = Array
(
    [memorycraft] => Array
        (
            [email] => memorycraft@gmail.com
            [gender] => male
        )

    [memorycraftgirl] => Array
        (
            [email] => memorycraft+girl@gmail.com
            [gender] => female
        )

)
girl after remove column = Array
(
    [gender] => female
)
ERROR : cassandra\NotFoundException Object
(
    [message:protected] => 
    [string:Exception:private] => 
    [code:protected] => 0
    [file:protected] => /home/memorycraft/app/lib/phpcassa/ColumnFamily.php
    [line:protected] => 308
    [trace:Exception:private] => Array
        (
            [0] => Array
                (
                    [file] => /home/memorycraft/app/lib/phpcassa/ColumnFamily.php
                    [line] => 299
                    [function] => _get
                    [class] => phpcassa\ColumnFamily
                    [type] => ->
                    [args] => Array
                        (
                            [0] => memorycraftgirl
                            [1] => cassandra\ColumnParent Object
                                (
                                    [column_family] => User
                                    [super_column] => 
                                )

                            [2] => cassandra\SlicePredicate Object
                                (
                                    [column_names] => 
                                    [slice_range] => phpcassa\ColumnSlice Object
                                        (
                                            [start] => 
                                            [finish] => 
                                            [reversed] => 
                                            [count] => 100
                                        )

                                )

                            [3] => 
                        )

                )

            [1] => Array
                (
                    [file] => /home/memorycraft/app/test.php
                    [line] => 43
                    [function] => get
                    [class] => phpcassa\ColumnFamily
                    [type] => ->
                    [args] => Array
                        (
                            [0] => memorycraftgirl
                        )

                )

        )

    [previous:Exception:private] => 
)

概ね期待通りの動作になっています。
また、存在しないキーを取得しようとすると例外が返る仕様のようです。

前回少し触れましたが、Cassandraはメモリテーブルを使用するため、このテストプログラムを実行したときの感覚としてはmemcacheへアクセスしたときと同じような体感速度でした。
今回は、localhostへのアクセスでしたが、クラスタを形成した場合のパフォーマンスなど、今後さらに調査してみたいと思います。

2012年9月17日月曜日

Cassandraってなんじゃ?

今回はCassandraをさわってみました。
Cassandraは、もともとFacebookが開発したKVSで、現在はApache Cassandraとしてオープンソース化されています。

Cassandraには以下の特徴があります。
  • 分散方式はAmazonのDynamoと同様なConsistentHashing方式
  • ストレージ方式はGoogleのBigTagleと同様なコミットログ/メモリテーブル/ディスクテーブル方式
  • CAP定理の3つの要素では、一貫性よりも可用性と分割耐性を重視
  • データモデルは列指向
  • アプリケーションからはThrift APIを使用

KVSのモデルはどれも一長一短で、Cassandraの場合デフォルトでは一貫性保証が弱いとされていますが、一貫性オプションの選択ができ、すべてのサーバーが最新の値に更新されてから読み出しができるようにすることも可能です。

Cassandraのデータの保持構造は以下のような階層で保存されます。
  • Keyspace:RDBのデータベースにあたるもの
  • Column Family:RDBのテーブルにあたるもの
  • Row:RDBのレコードにあたるもの
  • Column:RDBのカラムにあたるもの、name, value, timestampから構成される
この他にもSuper Columnがありますが必須要素ではありません。

それではインストールしてみます。

JDKのインストール

CassandraはJVM上で動作するため、まずはJDKをインストールします。
OpenJDKはCassandra1.1.5ではセグメンテーション違反が起こったため、Sun SDKを使用しました。
また、RPM版はインストールエラーがでたのでTAR版でインストールします。

ダウンロードはOracleのダウンロードページから行いますが、linuxシェル上では、Acceptボタンの操作ができないので、ブラウザでAcceptをチェックしダウンロードを開始した直後に一時停止した後、通信コンソール上のURLをコピーしてターミナル上でwgetしました。
今回は記事の都合上すべてrootで作業しましたが、適宜専用ユーザーで行なってください。

# yum install -y wget
# cd /usr/local/src
# wget http://download.oracle.com/otn-pub/java/jdk/7u7-b10/jdk-7u7-linux-i586.tar.gz?AuthParam=1347660783_4d176083902db0eb4404c8b5fcd542d0
# mv jdk-7u7-linux-i586.tar.gz\?AuthParam\=1347660783_4d176083902db0eb4404c8b5fcd542d0 jdk-7u7-linux-i586.tar.gz
# tar xzvf jdk-7u7-linux-i586.tar.gz 
# mkdir -p /usr/java/
# mv jdk1.7.0_07  /usr/java/jdk1.7.0_07

JAVA用の環境変数を定義します。
# vim /etc/profile
-----
JAVA_HOME=/usr/java/jdk1.7.0_07
PATH=$PATH:$JAVA_HOME/bin
CLASSPATH=.:$JAVA_HOME/jre/lib:$JAVA_HOME/lib/tools.jar
export JAVA_HOME CLASSPATH
------
# source /etc/profile
# cd /usr/local/src

簡単なコードを書き、コンパイルして動作確認します。
# vim HellowWorld.java
public class HelloWorld
{
    public static void main(String args[])
    {   
        System.out.println("Hello World!");
    }   
}
# javac HelloWorld.java
# java HelloWorld
Hello World!
これでJDKがインストールされました。


Cassandraのインストール

それではCassandraをインストールします。 CassandraはApache Cassandraのダウンロードページのリンクからwgetします。

# cd /usr/local/src/
# wget http://ftp.riken.jp/net/apache/cassandra/1.1.5/apache-cassandra-1.1.5-bin.tar.gz
# tar xzvf apache-cassandra-1.1.5-bin.tar.gz 
# mv apache-cassandra-1.1.5 /usr/local/
# cd /usr/local/
# ln -s apache-cassandra-1.1.5/ cassandra


Cassandraの起動

 今回はフォアグラウンドで起動してみます。 正常に動作すれば起動ログが出力されます。
# cd /usr/local/cassandra
# ./bin/cassandra -f


Cassandraのクライアントコンソールの起動

別のシェルからクライアントを起動します。
# ./bin/cassandra-cli --host localhost

[default@unknown]

[default@unknown]と表示されれば、Cassandraのコンソールに入れたことになります。 ここで、keyspaceの操作や、データのCRUDを行います。

以降、コンソール内での操作になります。
本来であればRDBとは概念が異なるため、するべきではないかも知れませんが、 とっつきやすくするために、対応するMySQLの操作を参考に記載します。


Keyspaceの作成

Hogebookという名前でkeyspaceを登録してみます。
MySQLでいうところのCREATE DATABASEにあたります。
[default@unknown] create keyspace Hogebook;
983a327c-bab3-37c3-958d-d856986f4f2b
Waiting for schema agreement...
... schemas agree across the cluster


Keyspaceの切り替え

使用したいKeyspaceをHogebookに切り替えます。
MySQLのUSE databaseにあたります。
[default@unknown] use Hogebook;
Authenticated to keyspace: Hogebook


Column Familyの作成

Column Familyを作成します。
UserというColumn Familyを作成してみます。
MySQLではCREATE TABLEに相当します。
作成する際にColumnのソート順を決めるComparatorを指定します。 ここではUTF8Typeを指定しますが、他にBytesType、AsciiType、UTF8Type、LexicalUUIDType、TimeUUIDType、LongTypeがあります。
[default@Hogebook] create column family User with comparator = UTF8Type;      
4ce3585d-9f66-36cb-8433-76bd1acf7827
Waiting for schema agreement...
... schemas agree across the cluster


Column Familyの設定の変更

emailとgenderというカラムのデータ型を定義します。
Column Familyの設定を変更します。
MySQLでのALTER TABLEに該当します。
[default@Hogebook] update column family User with column_metadata =  
... [
... {column_name: email, validation_class: UTF8Type},
... {column_name: gender, validation_class: UTF8Type} 
... ]
... ;
86ed51a4-e3b1-36b7-9cb9-93e4bf1a5fde
Waiting for schema agreement...
... schemas agree across the cluster

※追記:org.apache.cassandra.db.marshal.MarshalException: cannot parse ...とエラーになる場合は、
create column family User with comparator = UTF8Type and default_validation_class=UTF8Type and key_validation_class=UTF8Type;
のようにするといいようです。

データの投入

それではデータを保存してみます。
MySQLでのINSERTやUPDATEにあたります。
この例では'memorycraft'というキーでemail,genderというcolumnを保存します。
このようにCassandraでは、連想配列をつくるようにコマンドをセットするため、INSERTとUPDATEは同じ処理になります。
[default@Hogebook] set User['memorycraft']['email'] = 'memorycraft@gmail.com';
Value inserted.
Elapsed time: 47 msec(s).
[default@Hogebook] set User['memorycraft']['gender'] = 'male';
Value inserted.
Elapsed time: 2 msec(s).
[default@Hogebook] set User['memorycraftgirl']['gender'] = 'female';
Value inserted.
Elapsed time: 3 msec(s).
[default@Hogebook] set User['memorycraftgirl']['email'] = 'memorycraft+girl@gmail.com';
Value inserted.
Elapsed time: 17 msec(s).


データの取得

ここで、投入したデータを取得してみます。
MySQLでいうPKを条件にしたSELECTにあたります。
 
[default@Hogebook] get User['memorycraft'];                   
=> (column=email, value=memorycraft@gmail.com, timestamp=1347799163739000)
=> (column=gender, value=male, timestamp=1347799233322000)
Returned 2 results.
Elapsed time: 2 msec(s).


データの検索

キー以外のカラムの条件でデータを検索したい場合があります。
以下のように行います。
 
[default@Hogebook] get User where gender = 'male';
No indexed columns present in index clause with operator EQ
しかし、エラーになりました。インデックスがないことが原因のようです。
Cassandraではキー以外で検索をかける場合、そのカラムにインデックスを張る必要があります。
そこで、カラムの設定にインデックスを設定します。
[default@Hogebook] update column family User with column_metadata =  
... [
... {column_name: email, validation_class: UTF8Type},
... {column_name: gender, validation_class: UTF8Type, index_type: KEYS} 
... ]
... ;
86ed51a4-e3b1-36b7-9cb9-93e4bf1a5fde
Waiting for schema agreement...
... schemas agree across the cluster

このように、index_type: KEYSとすることで、genderカラムで検索をかけることが可能になります。

[default@Hogebook] get User where gender = 'male';
-------------------
RowKey: memorycraft
=> (column=email, value=memorycraft@gmail.com, timestamp=1347799163739000)
=> (column=gender, value=male, timestamp=1347799233322000)

1 Row Returned.
Elapsed time: 119 msec(s).


データの削除

データを削除してみます。
MySQLでのDELETEに該当します。
[default@Hogebook] del User['memorycraftgirl'];   
row removed.


とりあえず、駆け足で簡単なCRUD処理をさらってみました。
ここまでは、一風変わったデータストアというところまでしかわかりません。次はもう少しCassandraらしい部分を調べてみます。

2012年9月5日水曜日

S3ってなんじゃ?(CORS対応)

ごぶさたしております。
S3がCORS対応になったので、少し触れてみたいと思います。

CORSはCrossDomainResourceSharingの略です。
JSONPなどの特殊なケースを除いて通常ajaxなどではクロスドメイン通信は認められていません。
CORSは、通信先のサーバーで条件付きで許可をすることでクロスドメインアクセスを可能にするための仕組みで、W3Cで策定されいてる仕様です。モダンブラウザであればほぼサポートされているかと思います。

CORSではブラウザがクロスドメインのサーバーにリクエストする際に、事前にそのサーバーがこれから行おうとしているリクエストを許可しているかどうかをHTTPメソッドのひとつであるOPTIONSメソッドといくつかのHTTPリクエストヘッダを用いて問い合わせます。これをPreflightリクエストと言います。
そしてそのレスポンスをもってブラウザは通信可能かどうかを判断します。

Preflightリクエストで使用されるヘッダは
  • Origin 
    • リクエスト元のオリジン
    • (http://www.example.com/foo/barからリクエストされる場合、http://www.example.com/)
  • Access-Control-Request-Method 
    • これからリクエストするときに使用するHTTPメソッド
  • Access-Control-Request-Headers
    • これからリクエストするときに使用するHTTPヘッダー

などです。これらは通常ブラウザが自動で判別してリクエストヘッダに付与します。
それに対してレスポンスヘッダには以下の内容がセットされます。
  • Access-Control-Allow-Origin
    • この項目で指定されたOriginにリクエストが許可されます。
  • Access-Control-Allow-Credentials
    • trueならCookieなどユーザーの資格情報をレスポンスに含めることを許可します
  • Access-Control-Expose-Headers
    • CORS API仕様として公開して良いヘッダーフィールド
  • Access-Control-Max-Age
    • このPreflightリクエストの結果のキャッシュ保持秒数
  • Access-Control-Allow-Methods
    • 許可するHTTPメソッドが含まれます
  • Access-Control-Allow-Headers
    • 許可するリクエストヘッダーが含まれます

ブラウザはこのレスポンスヘッダを見てリクエスト可能かどうかを判断します。
通常REST APIなどを実装しWEBサービスを行うサーバーでは、このCORSに対応するような実装をすることで許可した相手にのみAPIを利用できるように実装を行う必要がありますが、今回S3ではそれが簡単な設定を行うだけで利用できるようになりました。

それでは実際にS3のCORS機能を使って具体的にどんなことが行われているのかを見てみます。

まずS3にバケットをつくります。
そこに例として以下のJSONファイルをアップロードします。

{
   "id": "1405028029",
   "name": "Satoru Miura",
   "first_name": "Satoru",
   "last_name": "Miura",
   "link": "https://www.facebook.com/memocra",
   "username": "memocra",
   "gender": "male",
   "locale": "ja_JP"
}

そしてEC2などで立ち上げた別のホストにこのjsonファイルをajaxで読み込む以下のようなHTMLを配置します。

<html>
  <head>
    <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js" ></script>
    <script type="text/javascript">
    $(function(){
      $.ajax({
        url:'http://s3-ap-northeast-1.amazonaws.com/memorycraft/memocra.json',
        type:'GET',
        contentType:"application/json",
        error:function(XMLHttpRequest, textStatus, errorThrown){
          alert("ERROR " + textStatus+ " " + errorThrown);
        },
        success:function(data){
          for(var p in data){
            $("#data").append("<li>"+p+" : "+data[p]+"</li>");
          }
        },
        dataType:'json',
        processData: false
      });
    });
    </script>
  </head>
  <body>
    <h1>Graph</h1>
    <ul id="data"></ul>
  </body>
</html>


WEBブラウザのアドレスバーに直接
http://s3-ap-northeast-1.amazonaws.com/memorycraft/memocra.json
と入力するとロードできますが、別ホストで上記のようなHTMLページからのajaxではドメインが異なるためロードに失敗します。



これはchromeブラウザですが、通信をみるとOPTIONSメソッドで送信しており、以下のヘッダを送信していることがわかります。

  • Access-Control-Request-Headers
    • origin, content-type, accept
  • Access-Control-Request-Method
    • GET
  • Host
    • s3-ap-northeast-1.amazonaws.com
  • Origin
    • http://176.34.47.135


これがPreflightリクエストです。
このあとOrigin、Content-Type、AcceptヘッダをGETメソッドで送りますよと伝えています。
結果は403で返ってきました。

バケットのプロパティを見るとPermissionsタブに、「Edit CORS Configuration」というボタンがあるのでクリックすると以下のようなXMLの設定が表示されます。



<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
    <CORSRule>
        <AllowedOrigin>*</AllowedOrigin>
        <AllowedMethod>GET</AllowedMethod>
        <MaxAgeSeconds>3000</MaxAgeSeconds>
        <AllowedHeader>Authorization</AllowedHeader>
    </CORSRule>
</CORSConfiguration>

CORSRuleにある設定がこのS3バケットで受け付けている設定になります。
これらは前述の
  • Access-Control-Allow-Origin
  • Access-Control-Allow-Methods
  • Access-Control-Max-Age
  • Access-Control-Allow-Headers

レスポンスヘッダに該当し、許可する条件を示しています。
このデフォルト設定をみると、アクセス元はどこでもよく、許可するHTTPでメソッドはGETのみ、認証キャッシュは3000秒で、Authorizationヘッダーのみを許可しています。

Authorizationヘッダーのみの許可なので、本リクエストで送る予定のAccept, Content-Type, Originヘッダーが許可されず403エラーとなっているようです。
ここで、AllowdHeaderを増やしてみます。

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
    <CORSRule>
        <AllowedOrigin>*</AllowedOrigin>
        <AllowedMethod>GET</AllowedMethod>
        <MaxAgeSeconds>3000</MaxAgeSeconds>
        <AllowedHeader>Authorization</AllowedHeader>
        <AllowedHeader>Accept</AllowedHeader>
        <AllowedHeader>Content-Type</AllowedHeader>
        <AllowedHeader>Origin</AllowedHeader>
</CORSRule>
</CORSConfiguration>

もしくはすべてのヘッダーを受け付けるようにワイルドカード*を指定します。

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
    <CORSRule>
        <AllowedOrigin>*</AllowedOrigin>
        <AllowedMethod>GET</AllowedMethod>
        <MaxAgeSeconds>3000</MaxAgeSeconds>
        <AllowedHeader>*</AllowedHeader>
    </CORSRule>
</CORSConfiguration>

すると、GETであればアクセス可能になるので、ajaxリクエストは成功します。



この状態では、どのホストからでもGETリクエストであればアクセスできてしまうので、
接続するOriginを特定してみます。

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
    <CORSRule>
        <AllowedOrigin>http://176.34.47.135</AllowedOrigin>
        <AllowedMethod>GET</AllowedMethod>
        <MaxAgeSeconds>3000</MaxAgeSeconds>
        <AllowedHeader>*</AllowedHeader>
    </CORSRule>
</CORSConfiguration>

ここで先ほどとはまた別のホストから同様にajaxでリクエストしてみると、Preflightリクエストで403が返り、アクセスできないことがわかります。



いままではS3のパーミッションはAWSアカウントを基準としたアクセス制御でしたが、
このようにHTTPレベルでもアクセス制御ができるようになりました。

これによって、S3の使用の仕方がとても広がるのではないかと思います。
以上です。