OpenShift OnlineでPSGIアプリ立ててWebSocket挿す

# OpenShift OnlineでPSGIアプリ立ててWebSocket挿す

こんにちは、 @debug-ito です。PlackとAnyEventが好きです。よろしくおねがいします。

今回は最近作ったものの宣伝も兼ねて、OpenShift Onlineのチュートリアル的な記事を書いてみようと思います。

## OpenShift Onlineって？

- https://www.openshift.com/

Red Hatが提供しているパブリックPaaSです。Google App EngineやHerokuなんかと同じような感じで、Webアプリケーションとかをホストすることができます。小規模なアプリなら無料で使えます。

OpenShift Onlineでは公式でPerlがサポートされています。使い方は @mercysluck さんの記事が参考になります。

- [OpenShift OnlineでMojolicious::Liteを動かしてみたい！](http://qiita.com/mercysluck/items/e2acd2e801e4e98646aa)

ですが、OpenShiftが公式でサポートしているPerlパッケージはApache httpd + mod_perlな構成になっていて、自分の好きなPSGIサーバを動かせんのです。PlackとAnyEventが大好きな僕としてはなんとしてもTwiggyを動かしてやりたいわけなのです。


## なにを作るのか

というわけで、今回は次のようなWebアプリをOpenShift Onlineで作ってみます。

- 簡単なWebチャットアプリ
- チャットルームはひとつ
- Plackベース
- PSGIサーバはTwiggy
- Webアプリケーションフレームワークは使わない
- チャットメッセージはWebSocketで配信

なお、完成品を http://chattest-debugito.rhcloud.com/ で動かしています。
完成品のソースコードは https://github.com/debug-ito/example-openshift-chat です。


## 開発環境構築

まずはOpenShift Onlineにユーザ登録し、必要なツールをインストールして開発環境を整えます。

- https://www.openshift.com/ から、"ONLINE"の"SIGN UP FOR FREE"をクリックしてユーザ登録。
- https://www.openshift.com/developers/rhc-client-tools-install を参考に、`rhc`ツールをインストールします。
    - Webベースの管理画面からでも結構いじれますが、`rhc`があった方がまあ何かと便利です。
- 初期設定をします。

        $ rhc setup
  
- 初期設定ではSSH公開鍵を登録したり、自分のドメイン名(名前空間とも言う)を登録したりします。SSH公開鍵はあとでgitアクセスする時に必要になります。`rhc`コマンドやWebコンソールから後で登録することもできます。

ここまでくればアプリ開発の準備完了です。


## アプリケーションの雛形を作る

アプリケーションを作るには、`rhc app create`コマンドを使います。

    $ rhc app create -a chattest -t https://raw.github.com/debug-ito/openshift-cartridge-plack/release_0.1.0/metadata/manifest.yml

これを実行すると **すっごい待たされますが、辛抱強くお待ちください。** ほんとすいません。

`rhc app create`コマンドの`-a`オプションはアプリ名(今回は`chattest`とする)、`-t`オプションはアプリケーション種別です。OpenShift用語では、「Webカートリッジ(Web Cartridge)」などと言います。

通常、Perlアプリを作る場合は`-t perl-5.10`としますが、前述の通り、OpenShift公式の`perl`カートリッジはPSGIサーバを選べないので、拙作のPlackカートリッジを使うように指定しています。

-  openshift-cartridge-plack : https://github.com/debug-ito/openshift-cartridge-plack

Plackカートリッジではアプリ作成時に毎回cpanmやPlackなどのモジュールをインストールするので時間がかかります。いっそのことバンドルしてしまった方がいいかもしれませんね。


## Plackカートリッジアプリの雛形

`app create`がうまくいけば`chattest`ディレクトリが作られているはずです。このディレクトリはローカルgitレポジトリになっています。

    $ cd chattest
    $ ls -a
    .  ..  .git  .proverc  README.md  app.psgi  cpanfile  lib  plack_config.pl  t

さらに、アプリケーションはもう稼動しているはずです。http://chattest-YOUR_DOMAIN.rhcloud.com (YOUR_DOMAINは登録したドメイン名)にアクセスすると、"It works!"と言われます。

雛形の構成はこんな感じです。PSGIアプリ開発ではおなじみのものではないでしょうか。

- README.md : このカートリッジの使い方を書いています。
- app.psgi : アプリケーション本体のスクリプトです。PSGIアプリオブジェクトを返します。
- cpanfile : アプリが依存するCPANモジュールを記述します。記法は[cpanfile](https://metacpan.org/pod/cpanfile)形式です。
- .proverc : テストを実行する`prove`コマンドの実行オプションを指定します。
- plack_config.pl : `plackup`コマンドの設定を記述します。
- lib/ : アプリで使う自作モジュールを格納します。このディレクトリは自動的に`PERL5LIB`パスに追加されます。
- t/ : テスト用スクリプトを格納します。

この中であまり馴染みがないのはplack_config.plでしょうか。この内容はこうなっています(コメントなどは除く)

```perl
$ENV{PLACK_ENV} = "development";
## $ENV{PLACK_SERVER} = "Starman";
```

要は、`plackup`コマンドの設定を環境変数経由で行うためのスクリプトです。

今のところ、Plackカートリッジでは[Daemon::Control](https://metacpan.org/pod/Daemon::Control)で作ったスクリプトから`plackup`コマンドを実行してアプリを立ち上げるようにしています。その際、plack_config.plスクリプトをロードして`plackup`の設定をしています。


## アプリを開発する

それでは、ローカルにクローンした`chattest`ディレクトリでアプリを開発します。完成品は https://github.com/debug-ito/example-openshift-chat に置いています。

ポイントになるところだけ抜粋します。まずはplack_config.plです。

```perl:plack_config.pl
$ENV{PLACK_ENV} = "deployment";
$ENV{PLACK_SERVER} = "Twiggy";
```

PSGIサーバとしてTwiggyを指定します。

次はcpanfileです。今のところPlackカートリッジには依存モジュールをスキャンする機能はないので、依存モジュールは全てここに書くようにします。

```perl:cpanfile
requires "ExtUtils::ParseXS", "3.22";
requires "Twiggy";
requires "EV";
requires "Log::Dispatch::FileWriteRotate";
requires "Time::Piece";
requires "Data::Section::Simple";
requires "Plack::App::WebSocket";
requires "Text::Xslate";
```

なお、ExtUtils::ParseXSを明示的に書かないとText::Xslateのインストールでコケます(リトライすればOK)。これは、バージョンの異なるExtUtils::ParseXSモジュールが複数のパスにインストールされるという、Plackカートリッジの少々特殊な環境が原因のようです。


以下では、app.psgiの内容を抜粋しながら順に解説していきます。


### WebSocketエンドポイントを作る

```perl
my %websockets = ();

sub report_number_of_peole {
    my $socket_num = keys %websockets;
    foreach my $conn (values %websockets) {
        $conn->send("---- There are $socket_num people in this chat now.")
    }
}

my $websocket_endpoint = Plack::App::WebSocket->new(
    on_establish => sub {
        my ($conn) = @_;
        $websockets{"$conn"} = $conn;
        $conn->on(message => sub {
            my ($conn, $message) = @_;
            my $cur_time = current_time_str();
            $message = "[$cur_time] $message";
            foreach my $conn (values %websockets) {
                $conn->send($message);
            }
        });
        $conn->on(finish => sub {
            my ($conn) = @_;
            delete $websockets{"$conn"};
            report_number_of_peole();
        });
        report_number_of_peole();
    }
);
```

PSGIアプリでWebSocketコネクションを受け付けるために、拙作の[Plack::App::WebSocket](https://metacpan.org/pod/Plack::App::WebSocket)を使います。こいつは単体でPSGIアプリとなるので、後でmountするもよし、アプリ内でcallするもよし、です。

このアプリでは、受け付けたWebSocketコネクションを`%websockets`内にためておいて、受信したメッセージを単純にブロードキャストします。また、コネクションの出入りがあるたびに現在のコネクション数をブロードキャストします。


### ビューとなるHTMLページを作る

```perl
my $template = Text::Xslate->new(
    path => [{index => get_data_section("index.html")}],
    cache_dir => "$ENV{OPENSHIFT_TMP_DIR}/xslate",
);
my $page_html = $template->render(
    "index", { app_fqdn => $ENV{OPENSHIFT_APP_DNS} }
);
```

[Text::Xslate](https://metacpan.org/pod/Text::Xslate)と[Data::Section::Simple](https://metacpan.org/pod/Data::Section::Simple)を組み合わせています。

OpenShiftでは、アプリのために様々な情報を環境変数として提供しています。ここでは、

- `OPENSHIFT_TMP_DIR` : 一時ファイルを格納するディレクトリ
- `OPENSHIFT_APP_DNS` : このアプリのFQDN

を利用しています。FQDNは後でWebSocketコネクションを張る時に必要となります。

利用できる環境変数のリストは http://openshift.github.io/documentation/oo_user_guide.html#environment-variables を参照してください。


### アプリ本体を作る

```perl
my $app = sub {
    my $env = shift;
    if($env->{PATH_INFO} eq "/websocket") {
        return $websocket_endpoint->call($env);
    }else {
        return [200,
                ["Content-Type" => "text/html; charset=UTF-8",
                 "Content-Length" => length($page_html)],
                [$page_html]];
    }
};
```

単純に、リクエストパスに応じてWebSocketかHTMLレスポンスかを振り分けているだけです。


### loggerをセットして終了

```perl
my $logger = Log::Dispatch::FileWriteRotate->new(
    min_level => "info",
    dir => $ENV{OPENSHIFT_PLACK_LOG_DIR},
    prefix => "access.log",
    size => 10*1024*1024,
    histories => 5
);

builder {
    enable "AccessLog", logger => sub {
        $logger->log(level => "info", message => $_[0]);
    };
    $app;
};
```

ログのローテーションなどはアプリ側で面倒見る必要があるので、今回は[Log::Dispatch::FileWriteRotate](https://metacpan.org/pod/Log::Dispatch::FileWriteRotate)を使ってみました。

Plackカートリッジのログ出力用ディレクトリは`OPENSHIFT_PLACK_LOG_DIR`環境変数で取得できます。ここに吐かれたログは

    $ rhc tail chattest

としてリアルタイムに監視することができます。


### WebSocketを接続する

```javascript
var websocket_url = "ws://<: $app_fqdn :>:8000/websocket";
  (中略)
var ws = new WebSocket(websocket_url);
```

クライアントJavaScriptのうち、WebSocket接続を行う部分がここです。

`<: $app_fqdn :>`はText::XslateによってアプリのFQDNに置換されます。

なお、OpenShiftではWebSocketコネクションの接続先ポートが8000番(SSLを使う場合は8443番)である点に注意が必要です。


## アプリをデプロイする

アプリをデプロイするには、変更をローカルレポジトリにコミットしたあと、それをOpenShiftへプッシュすればOKです。

    $ git add app.psgi
    $ git add cpanfile
       ...
    $ git commit -m "implement WebSocket chat"
    $ git push

すると、Plackカートリッジが必要なCPANモジュールをインストールし、テストを実行してから改めてアプリを立ち上げます。

なお、今のところPlackカートリッジはホットデプロイに対応していません。ビルド中はアプリケーションが停止するので注意してください。


## まとめ

OpenShift Onlineを使ってPlackベースのWebアプリケーション開発をしてみました。今回はあえてWebアプリケーションフレームワークを使いませんでしたが、PSGI互換のものであればどれでも自由に使うことができるでしょう。

OpenShift OnlineではWebアプリケーションの他、各種データベースサーバやJenkinsサーバを立てたり、HAProxyを使ってスケールアウトするアプリを立てたりすることができます。ただ、あまり凝った構成にしようとすると"gear"(リソースの単位)が足りなくなるので有料プランを使う必要があります。

個人的には無料の範囲でもとりあえずWebSocketの挿せるアプリを立ち上げられるので、ちょっと遊ぶには都合がいいと思ってます。

## 次回は

以前ちょっとハマったネタを思い出したのでまた書かせていただきます。