工場長のブログ

日々思ったことを書いてます。

AWS SDK for node.jsでDynamoDBをバックエンドにしてチャットアプリケーションを作ってみた。

AWS SDK for node.jsのデベロッパープレビュー版がリリースされたので触ってみた。

AWS SDK for Node.js (Developer Preview)
http://aws.amazon.com/jp/sdkfornodejs/

まだデベロッパープレビュー版なのですべてのサービス向けのAPIが実装されているわけではなくて、現状EC2、S3、DynamoDB、SWFのみに対応済みな状態。こんな感じ↓
f:id:imai-factory:20130111163105j:plain

何をしてみようかというところで、S3を取り扱うようなサンプルコードはちょいちょい見かけるのでDynamoDBを扱って見ることに。socket.ioを触ったことがなくて、触ってみたこともあって、http://www.atmarkit.co.jp/ait/articles/1210/10/news115.htmlを参考にチャットアプリケーションを実装してみた。アーキテクチャはこんな感じ。
express + mongoose + MongoDBが流行ってるので、express + AWS SDK + DynamoDBもなかなかイイぜ、的な。
f:id:imai-factory:20130111163424j:plain
ちなみに、残念ながらDynamoDBにはgemfireやredisのようなmessagingの機能は実装されていません。WebSocketでのpush通信の話と一緒にすると誤解を生むかもなので念のため。

アプリケーション的には接続中のクライアントがルームを指定してメッセージをやりとりできるという、まあ普通なチャットアプリ。この投稿されたメッセージはサーバー側でDynamoDBに格納されます。新規にルームに接続してきたクライアントに対して、当該ルームの最新1時間分のログをDynamoDBから取り出して表示させて、あとは普通にチャットできますよという感じ。

  • 画面を開くとこんな感じ

f:id:imai-factory:20130111164243j:plain

  • room1というルーム(現状、ルーム名はなんでも通る)に接続してメッセージを投稿してみる。

f:id:imai-factory:20130111164342j:plain

  • もう一枚ブラウザをたちあげてroom1に入ってみるとDynamoDBに格納されたメッセージが初期状態として表示されます。

f:id:imai-factory:20130111164615j:plain

とまあいたって普通なチャットアプリケーションです。コードはgithubにアップしました。
https://github.com/imaifactory/nodejs_chat

今回のメインのトピックはDynamoDBを取り扱う際のサンプルコードなので、そこだけ抜粋してみます。

まず、sdkのインストールはnpmで行う。

$ npm install aws-sdk

使う前の準備としてCredentialやRegionの設定をする

$ vi config.json

{
    "accessKeyId":"hoge",
    "secretAccessKey":"hoge",
    "region":"ap-northeast-1"
}

コード内での初期化はこんな感じ。

var aws = require('aws-sdk');
aws.config.loadFromPath('./aws_config.json');

var ddb = new aws.DynamoDB.Client();

このクライアントを使ってDynamoDBの操作を行う。
今回のコードでデータを投げ込んでいるのはこんなコード。
Itemが実際のデータだが、特徴的なのは、Itemの各項目にSという型の指定が必要なこと。S=String, N=Number, B=Binaryという感じ。

            ddb.putItem(
                {
                    TableName:tableName,
                    Item: {
                        topic: {S:clientInfo.room},
                        text:  {S:message.text},
                        name:  {S:clientInfo.name},
                        timestamp: {S:message.timestamp}
                    }
                },
                function(err,data){
                    if(err){
                        console.log(err);
                    }else{
                        io.sockets.to(clientInfo.room).json.emit('message',message);
                    }
                }
            );

データを取り出しているコードはこんな感じ。queryというAPIを使っています。
HashKeyを指定したうえで、RangeKeyが特定の値(今回のアプリケーション的にはタイムスタンプ)よりも大きい(GT)レコードを取得しています。コードからだとわかりにくいけど、RangeKeyConditionのなかのAttributeValueListというところでRangeKey(タイムスタンプ)を指定してます。

        ddb.query(
            {
                TableName:tableName,
                HashKeyValue:{ S:data.room },
                RangeKeyCondition:{
                    ComparisonOperator:'GT',
                    AttributeValueList:[
                        {S:date.timestampDelta(appConfig.timeLine.startTime)}
                    ]
                },
                ScanIndexForward: true,
                AttributesToGet:['topic','text','timestamp','name']
            },
            function(err,data){
                if(err){
                    console.log(err);
                }else{
                    socket.json.emit('initial',data);
                }
            }
        );

今回はputItemとqueryしかつかってないけど、データを1件取得するgetItemやbatchGetItemやbatchPutItemなど、もちろん他にもたくさんAPIはあります。詳細はこちらのAPIリストを参照のこと。
http://docs.amazonwebservices.com/AWSJavaScriptSDK/latest/frames.html

ハマった点として、現状のSDKだとマルチバイトのutf8がうまく通らなかったこと。なので日本語の取扱がうまくいきませんでした。プレビュー版なので修正を待ちますということで。


あと、今回のネタとは直接関係ないですが、AWS上でWebSocketを使う際の注意点として、ELBが非アクティブなセッションを60秒で切ってしまうことがあります。現状、この値は変更できないのでご注意を。

実際ガッツリつかうとなると、セッション管理や維持にマシンのCPUパワーをかなり食うと思うので、このあたりの運用やスケーリングのベストプラクティス的なものはまた今後まとめていきます。