オープンソースのPaaSソフトウェア CloudFoundry の技術情報やイベント告知などを掲載します

2015-06-08

EtherSheet を Cloud Foundry で動かす

「Cloud Foundry 百日行」第3回目は,Node.js ベースの spreadsheet アプリ “EtherSheet” です。

基本情報

EtherSheet は,GitHub の README によると,「リアルタイム共同作業が可能なオープンソースの spreadsheet」です。オープンソースの Google Sheets クローン,というのが分かりやすい説明かもしれません。もっとも,今回試用してみた限りでは,機能も安定性も Google Sheets には及ばないと感じましたが..。
手順の概要は以下の通りです。
  1. ソースコードの取得
  2. ソースコードの修正
    今回,Cloud Foundry 上で動作させるに当たって,2点ほど修正が必要だったので,それについて述べます
  3. サービスの作成
    本アプリは MySQL を使うので,MySQL サービスのインスタンスを作成します
  4. アプリの push
    今回は manifest.yml ファイルを使って push してみました
では,始めます。

ソースコードの取得

$ git clone https://github.com/ethersheet-collective/EtherSheet.git
EtherSheet ディレクトリーに入ります。
$ cd EtherSheet/

ソースコードの修正

本アプリを Cloud Foundry上で動作させるに当たり,実地検証の結果,ソースコードの修正が必要と判断しました。
この部分の記事の書き方としては,大きく,
  • (a) まず正しい手順を書き,その後失敗の経験に基づいた注意点を書く
  • (b) 失敗した手順を書き,原因を解明して修正したあと,成功した手順を書く
の2つの方法があると思いますが,今回は(a)で記述を進めることにしました。この辺り,「(b)の方が読みやすい」等ご意見があれば,お知らせいただけるとありがたく思います。
今回の修正のポイントは,
  1. MySQLの接続情報を環境変数から取るようにする
  2. アプリが待ち受けるホスト/ポートの情報を環境変数から取るようにする
の2点です。

MySQLの接続情報を環境変数から取る

本アプリは,データベースの接続情報を設定ファイル (config.js) から取るようになっています。しかし, Cloud FoundryやHerokuのような PaaS では,データベースを service としてカジュアルに結合(bind)/切断(unbind)することを想定し,そうした情報は環境変数として与えられる造りになっています。これに合うよう,ソースコードを修正しました。
実際の修正内容は以下の通りです。既存の動作を変えないよう,環境変数がある場合のみ上書きされ,それ以外は設定ファイルの値を使うようになっています。
$ git show 20f77d7
 1commit 20f77d78c2079966dd6624b7abdca59705017f78
 2Author: Noburou TANIGUCHI <taniguchi.noburou@lab.ntt.co.jp>
 3Date:   Mon Jun 8 12:35:16 2015 +0900
 4
 5    Get database connection credentials from DATABASE_URL env var
 6
 7    For easiness to run on Cloud Foundry / PaaS environment.
 8
 9diff --git a/lib/ethersheet_service.js b/lib/ethersheet_service.js
10index e527c0e..58aa2c4 100644
11--- a/lib/ethersheet_service.js
12+++ b/lib/ethersheet_service.js
13@@ -5,6 +5,7 @@ var uuid = require('node-uuid').v4;
14 var async = require('async');
15 var async = require('async');
16 var csv = require('csv');
17+var url = require('url');
18 var _ = require('underscore');
19 var Sheet = require('./models/sheet');
20 var SheetCollection = require('./models/sheet_collection');
21@@ -15,12 +16,26 @@ var SheetCollection = require('./models/sheet_collection');
22 var EtherSheetService = module.exports = function(config){
23   var es = this;
24   es.config = config;
25+
26+  /* for Cloud Foundry */
27+  var dbUrl = url.parse(process.env.DATABASE_URL);
28+  if (dbUrl) {
29+    es.config.db_host = dbUrl.hostname;
30+    es.config.db_port = dbUrl.port;
31+    var path = dbUrl.path.slice(1).split(/[?#]/);
32+    es.config.db_name = path[0]
33+    var auth = dbUrl.auth.split(':');
34+    es.config.db_user = auth[0];
35+    es.config.db_password = auth[1];
36+  }
37+
38   events.EventEmitter.call(this);
39   this.connectionHandler = function(){};
40   es.db = new ueberDB.database(
41     es.config.db_type, {
42     user: es.config.db_user,
43     host: es.config.db_host,
44+    port: es.config.db_port,
45     password: es.config.db_password,
46     database: es.config.db_name
47   });

アプリが待ち受けるホスト/ポートの情報を環境変数から取る

前項と同様に,本アプリでは,アプリが待ち受けるホスト/ポートの情報を設定ファイル (config.js) から取るようになっています。しかし Cloud Foundry環境では,アプリの起動・停止が簡単に行えるよう,アプリのインスタンスはコンテナー内で動作し,待ち受けポートも61001番以降から自動で採番したものを環境変数に入れるようになっており,ユーザーが指定できるようにはなっていません。
そこで,アプリの待ち受けポートも環境変数から取れるよう,ソースコードを修正しました。
実際の修正内容は以下の通りです。前項と同様,既存の動作を変えないよう,環境変数がある場合のみ上書きされ,それ以外は設定ファイルの値を使うようになっています。
$ git show bf3e351
 1commit bf3e3510bd6e8393a5d10e81427110e3f34ed493
 2Author: Noburou TANIGUCHI <taniguchi.noburou@lab.ntt.co.jp>
 3Date:   Mon Jun 8 12:40:36 2015 +0900
 4
 5    Get host / port to listen from VCAP_APP_HOST / VCAP_APP_PORT env var
 6
 7    For easiness to run on Cloud Foundry / PaaS environment.
 8
 9diff --git a/lib/ethersheet_service.js b/lib/ethersheet_service.js
10index 58aa2c4..2efb0d5 100644
11--- a/lib/ethersheet_service.js
12+++ b/lib/ethersheet_service.js
13@@ -18,6 +18,8 @@ var EtherSheetService = module.exports = function(config){
14   es.config = config;
15
16   /* for Cloud Foundry */
17+  es.config.host = process.env.VCAP_APP_HOST || es.config.host || 'localhost';
18+  es.config.port = process.env.VCAP_APP_PORT || es.config.port || 8080;
19   var dbUrl = url.parse(process.env.DATABASE_URL);
20   if (dbUrl) {
21     es.config.db_host = dbUrl.hostname;

サービスの作成

本アプリは永続化ストレージとして MySQL を使うので,MySQL サービスのインスタンスを作成します。
今回利用した MySQL サービスは, cf-mysql-release を利用して構築しました。これは postgresql-cf-service-broker とは異なり,アプリのデプロイは全く関係ないので,今回構築方法等については触れませんが,記事化のご要望等あれば何か考えたいと思います。
サービス種別の一覧:
$ cf marketplace
Getting services from marketplace in org nota-ja / space 100 as nota-ja...
OK

service      plans                    description
PostgreSQL   Basic PostgreSQL Plan*   PostgreSQL on shared instance.
p-mysql      100mb-dev, 1gb-dev       A MySQL service for application development and testing
......
サービスの作成:
$ cf create-service p-mysql 100mb-dev my4es
Creating service instance my4es in org nota-ja / space 100 as nota-ja...
OK

アプリのpush

これで準備が整ったので,いよいよアプリをpushしていきます。
今回は Application Manifest ファイル (manifest.yml) を使って push してみました。
Cloud Foundryにアプリをpushする際には (例えば cf push コマンドのヘルプを見るとわかりますが) 非常に多くのオプションがあります。メモリ上限, ディスク上限, インスタンス数, ホスト名, ドメイン名, 起動コマンド, push時に起動するか,…, 等々。同じアプリをpushするとき,これらをpushの度に指定する面倒さを軽減するための機能が,Application Manifest ファイルです。先述したような設定をこのファイルに書いておくことで,同じ設定でpushを繰り返す場合は単に cf push とするだけで済むようになります。
今回使用した manifest.yml の内容は以下の通りです。
$ cat manifest.yml
---
applications:
  - path: .
    name: es
    random-route: true
    memory: 256M
    instances: 1
    services:
      - my4es
このファイルがあるディレクトリーで cf push を実行します。
$ cf push
Using manifest file /home/nota-ja/workspace/100/EtherSheet/manifest.yml
......
requested state: started
instances: 1/1
usage: 256M x 1 instances
urls: es-inactive-fluffer.10.244.0.34.xip.io
last uploaded: Mon Jun 8 01:01:31 +0000 2015
stack: lucid64

     state     since                    cpu    memory        disk      details
#0   running   2015-06-08 10:02:28 AM   0.0%   72M of 256M   0 of 1G
アプリが起動しました。
ちなみに,上記の manifest.yml の記述は,だいたい以下の手順と等価です。
$ cf push es -p . --random-route -m 256m -i 1 --no-start
$ cf bind-service es my4es
$ cf restage

動作確認

アプリが無事起動したので,実際にブラウザーからアクセスしてみます。

初期画面


GOTO SHEET ボタンを押すと初期状態の spreadsheet に遷移します。

spreadsheet の初期状態


セルに値を入力

A1 セルに 123 を入力してみます。

合計関数

合計を取る sum 関数を A3 セルに入力してみます。



正しい計算結果 579 が表示されました。

FUNCTIONS ペインから関数を入力

左上の右側のボタンをクリックして,FUNCTIONS ペインを表示し,そこから sqrt 関数を選択してみます。

sqrt_def という文字列が入力されました。
入力された文字列を正しい関数名 sqrt に修正し,A3 セルを引数に与えてみます。



A4 セルに計算結果が表示されました。

SHEETS ペイン

左上の左側のボタンをクリックして,SHEETS ペインを表示させます。

新シートの作成


newSheet ボタンをクリックし,新シートを作成しました。
新しいシートは空です。

CSVのエクスポート

exportCSV ボタンをクリックすると,生のCSVファイルがブラウザー上に表示されます。

ここでブラウザーのメニューから 保存 を実行すると,保存ダイアログが出てくるので,保存します。

CSVのインポート

Sheet2 に,先ほどエクスポートしたCSVをインポートしてみます。
importCSV ボタンをクリックすると,インポート・ダイアログが表示されます。

「ファイルを選択」をクリックすると,ファイル選択ダイアログで先ほど保存したCSVファイルを選択すると,インポートの準備が整います。

Upload ボタンをクリックすると,インポートされる…はずですが,エラーが表示されました。

この時のアプリのログは以下のようになっていました。
2015-06-08T10:36:55.40+0900 [App/0]      OUT POST /HRgZgHUlR5nvSmb74kT5/pubsub/589/5rukm6ta/xhr 0ms (unfinished)
2015-06-08T10:36:58.15+0900 [App/0]      ERR /home/vcap/app/lib/ethersheet_service.js:161
2015-06-08T10:36:58.15+0900 [App/0]      ERR   csv().from(data.toString())
2015-06-08T10:36:58.15+0900 [App/0]      ERR   ^
2015-06-08T10:36:58.15+0900 [App/0]      ERR TypeError: object is not a function
2015-06-08T10:36:58.15+0900 [App/0]      ERR     at EtherSheetService.createSheetFromCSV (/home/vcap/app/lib/ethersheet_service.js:161:3)
2015-06-08T10:36:58.15+0900 [App/0]      ERR     at /home/vcap/app/lib/server.js:86:10
2015-06-08T10:36:58.15+0900 [App/0]      ERR     at fs.js:271:14
2015-06-08T10:36:58.15+0900 [App/0]      ERR     at Object.oncomplete (fs.js:107:15)
今回全く触っていない部分のエラーなので,未完成なのかもしれません。
ブラウザーの「戻る」ボタンをクリックすると,接続エラー画面が。

指示通りリロードしてみましたが, 502 Bad Gateway の文字が。

仕方ないのでアプリを再起動してみます。
$ cf restart es
Stopping app es in org nota-ja / space 100 as nota-ja...
OK

Starting app es in org nota-ja / space 100 as nota-ja...

0 of 1 instances running, 1 starting
1 of 1 instances running

App started


OK

App es was started using this command `npm start`

Showing health and status for app es in org nota-ja / space 100 as nota-ja...
OK

requested state: started
instances: 1/1
usage: 256M x 1 instances
urls: es-inactive-fluffer.10.244.0.34.xip.io
last uploaded: Mon Jun 8 01:01:31 +0000 2015
stack: lucid64

     state     since                    cpu    memory          disk      details
#0   running   2015-06-08 10:38:34 AM   0.0%   73.1M of 256M   0 of 1G
無事再起動しました。
先ほど入力したのシートの URL にアクセスしてみると…。

正常に表示され,データも残っていました。
以上で一通りの動作確認は終わりです。

今回使用した環境