2015-11-02

shooter.io を Cloud Foundry で動かす

「Cloud Foundry 百日行」第91日目は、多人数で遊べるシューティングゲーム shooter.io です。
Goで書かれたサーバ shooter-server と、HTTPサーバ shooter-html5 が連携して動くアプリです。
なお、今回のアプリはサーバー間の通信にWebSocketが使われているため、Proxy等の設定によってはアプリが正常に動作しない可能性もありますので、ご了承下さい。

基本情報

今回の手順の概要は以下の通りです。

  • 1) shooter-server のデプロイ
    • 1.1) ソースコードの入手
    • 1.2) Serviceの作成
    • 1.3) 事前準備
    • 1.4) Cloud Foundryへのデプロイ
  • 2) shooter-html のデプロイ
    • 2.1) ソースコードの入手
    • 2.2) 事前準備
    • 2.3) Cloud Foundryへのデプロイ
  • 3) 動作確認

1. shooter-server のデプロイ

1.1. ソースコードの入手

$ git clone https://github.com/xiam/shooter-server
$ cd shooter-server/
shooter-server$ ls
LICENSE  Makefile  README.md  shooter.png  src
shooter-server$ ls src/
agent.go  control.go  entity   fn.go  main.go   player.go   scores.go  ship
bullet    diff        fire.go  item   Makefile  powerup.go  sector.go

Go言語で書かれたアプリです。
README によると、以降のサーバ起動手順は、

make
cd src
go get -d
make
MONGO_HOST="<MONGODB_HOST>" ./shooter-server -listen <LISTEN_IP>:<LISTEN_PORT>
  • MONGODB_HOST → MongoDBのある場所のIPアドレス。デフォルトの値は “127.0.0.1” 。(参考
  • LISTEN_IP : LISTEN_PORT → HTTPサーバからの接続待ち受けIPアドレス/PORT。デフォルトの値は “127.0.0.1:3223” 。(参考

となっているようです。
さて、恐らくですが、 go-buildpack を使った場合は、2つのディレクトリに渡って make を実行し、かつその間に go get も適切なタイミングで実行し・・・といった処理はできない可能性が高いです。
もちろんGo言語とgo-buildpackの仕様を共にしっかりと理解している方であれば可能であるかもしれませんが、残念ながら私には今回の執筆期間ではそのような手段を編み出すことはできませんでした。
しかし幸いなことにGo言語には、binaryさえ作ってしまえば外部のライブラリ等に依存せずに実行ができるという強みがあります。
ということで、今回はこれを活かし、Goのバイナリを事前にローカルの環境で作ってしまい、その後Cloud Foundryで動作させる、という手段をとってみようと思います。
使うBuildpackは binary-buildpack です。

1.2. Serviceの作成

さて、 shooter-server はゲームスコアなどの保存にMongoDBを使うとのことなので、 こちらの記事 で作成したMongoDB Serviceを使います。

shooter-server$ cf marketplace
Getting services from marketplace in org ukaji / space default as ukaji...
OK

service      plans                     description   
Mongo DB     Default Mongo Plan*       A simple mongo implementation   
PostgreSQL   Basic PostgreSQL Plan*    PostgreSQL on shared instance.   
p-mysql      100mb, 1gb                MySQL databases on demand   
p-redis      shared-vm, dedicated-vm   Redis service to provide a key-value store

* These service plans have an associated cost. Creating a service instance will incur this cost.

TIP:  Use 'cf marketplace -s SERVICE' to view descriptions of individual plans of a given service.
shooter-server$ cf create-service "Mongo DB" "Default Mongo Plan" shooter-mongo
Creating service instance shooter-mongo in org ukaji / space default as ukaji...
OK

バインドも済ませておきましょう。

shooter-server$ cf push shooter-server --no-start
・・・
shooter-server$ cf bind-service shooter-server shooter-mongo
Binding service shooter-mongo to app shooter-server in org ukaji / space default as ukaji...
OK

1.3. 事前準備

MongoDB接続周りの修正

1.2で作成したMongoDBに接続する箇所の修正を行います。
scores.go を見てみると、このアプリはデータベースのHost名とDatabase名のみで接続を行っています。

shooter-server$ cat src/scores.go
・・・
        settings = db.Settings{
                Host:     host,
                Database: defaultDatabase,
        }

        if sess, err = db.Open("mongo", settings); err != nil {
                log.Fatal("db.Open: ", err)
        }
・・・

しかし、 cf env で見ることができるServiceの環境変数中の uri の値を見て分かる通り、Cloud Foundry上で作成したMongoDBのServiceにはUsername、Passwordが設定されているため、このままでは正しく接続することはできません。

shooter-server$ cf env shooter-server
{
 "VCAP_SERVICES": {
  "Mongo DB": [
   {
    "credentials": {
     "uri": "mongodb://ad55761d-bd50-40f1-ae0f-875cc1c92e96:password@192.168.15.91:27017/7f7607ab-db63-465a-95c9-2b2318c5d68d"
    },
    "label": "Mongo DB",
    "name": "shooter-mongo",
    "plan": "Default Mongo Plan",
    "tags": [
     "mongodb",
     "document"
    ]
   }
  ]
 }
}

そこで今回は、「起動時に環境変数 MONGO_URI でURI( mongodb://<username>:<password>@<host>:<port>/<database> )を指定すればDBとの接続を行う」という方針で修正を行います。もちろん従来の MONGO_HOST が指定されている場合の動作にも影響が及ばないようにしましょう。

shooter-server$ vi src/scores.go
・・・
import (
        "log"
        "os"
        "strings"
        "time"
        "upper.io/db"
        "upper.io/db/mongo"
)
・・・
var settings mongo.ConnectionURL
var sess db.Database
var scores db.Collection
・・・
func init() {
        var err error

        var mongoURI = ""
        host := os.Getenv("MONGO_HOST")
        uri := os.Getenv("MONGO_URI")

        if uri != "" {
                mongoURI = uri
        } else if host != "" {
                mongoURI = host
        } else {
                mongoURI = defaultHost
        }

        parsedURI, _ := mongo.ParseURL(mongoURI)

        settings = mongo.ConnectionURL {
                Address:  parsedURI.Address,
                Database: parsedURI.Database,
                User:     parsedURI.User,
                Password: parsedURI.Password,
        }

        if sess, err = db.Open("mongo", settings); err != nil {
                log.Fatal("db.Open: ", err)
        }

        log.Printf("Connected to mongo://%s.\n", mongoURI)

・・・
shooter-server$ git diff --no-prefix src/scores.go
diff --git src/scores.go src/scores.go
index 1d5b173..bb6666f 100644
--- src/scores.go
+++ src/scores.go
@@ -20,7 +20,7 @@ import (
        "strings"
        "time"
        "upper.io/db"
-       _ "upper.io/db/mongo"
+       "upper.io/db/mongo"
 )
 
 type mark struct {
@@ -29,7 +29,7 @@ type mark struct {
        Created time.Time `json:"-" bson:"created"`
 }
 
-var settings db.Settings
+var settings mongo.ConnectionURL
 var sess db.Database
 var scores db.Collection
 
@@ -41,22 +41,32 @@ const (
 func init() {
        var err error
 
+       var mongoURI = ""
        host := os.Getenv("MONGO_HOST")
+       uri := os.Getenv("MONGO_URI")
 
-       if host == "" {
-               host = defaultHost
-       }
+        if uri != "" {
+                mongoURI = uri
+        } else if host != "" {
+                mongoURI = host
+        } else {
+                mongoURI = defaultHost
+        }
 
-       settings = db.Settings{
-               Host:     host,
-               Database: defaultDatabase,
-       }
+       parsedURI, _ := mongo.ParseURL(mongoURI)
+
+        settings = mongo.ConnectionURL {
+                Address:  parsedURI.Address,
+                Database: parsedURI.Database,
+                User:     parsedURI.User,
+                Password: parsedURI.Password,
+        }
 
        if sess, err = db.Open("mongo", settings); err != nil {
                log.Fatal("db.Open: ", err)
        }
 
-       log.Printf("Connected to mongo://%s/%s.\n", host, defaultDatabase)
+       log.Printf("Connected to mongo://%s.\n", mongoURI)
 
        scores, err = sess.Collection("scores")
        if err != nil {

これでOKです。
環境変数 MONGO_URI にMongoDBのデータベース情報を参照するURIが書かれていればそれを用いるようになりました。
また、従来通り MONGO_HOST にHost名だけを指定している場合はその場所を参照し、MongoDBに関する環境変数が何も与えられていない場合は、localhostを探しに行くという設定も生きています。

バイナリファイルの作成

今回はbinary-buildpackを使うので、ローカル環境でGoのbinaryを作ってしまいます。
手順は README の通りです。

shooter-server$ go version
go version go1.4 linux/amd64

goのversionは1.4を用いています。

shooter-server$ make
mkdir -p $GOPATH/src/
ln -sf $PWD/src $GOPATH/src/shooter.io
shooter-server$ cd src/
shooter-server/src$ go get -d
shooter-server/src$ make
go build -o shooter-server

shooter-serverというbinaryファイルができていればOKです。

shooter-server/src$ ls shooter-server
shooter-server

manifestファイルの作成

今回デプロイに必要なのはこのbinaryファイルだけなので、余分なファイルをアップロードしないためにもbinaryファイルとmanifestファイルのみが入ったディレクトリを別に作っておきましょう。

go-server$ ls
manifest.yml  shooter-server

今回はgo-serverという別の適当なディレクトリを用意しました。
なお、manifestファイルの中身はこのようになっています。

go-server$ vi manifest.yml
---
applications:
- name: shooter-server
  domain: 192.168.15.91.xip.io
  buildpack: binary_buildpack
  memory: 16M
  command: ./shooter-server -listen :$PORT
  env:
    MONGO_URI: "mongodb://ad55761d-bd50-40f1-ae0f-875cc1c92e96:password@192.168.15.91:27017/7f7607ab-db63-465a-95c9-2b2318c5d68d"
  services:
  - shooter-mongo

command ではbinaryの起動とあわせて、ListenするPORT番号を環境変数から取得しています。
環境変数 MONGO_URI には cf env で見ることができるMongoDB接続のためのuriをそのまま記述しました。
なお、今回検証を行った環境では 192.168.15.91.xip.io を指定した場合にWebSocketが通るようになるので、manifest中で domain を設定しています。

1.4. Cloud Foundryへのデプロイ

では、Cloud Foundryへのデプロイを行います。

go-server$ cf push
・・・
-----> Uploading droplet (2.2M)

1 of 1 instances running

App started


OK

App shooter-server was started using this command `./shooter-server -listen :$PORT`

Showing health and status for app shooter-server in org ukaji / space default as ukaji...
OK

requested state: started
instances: 1/1
usage: 16M x 1 instances
urls: shooter-server.192.168.15.91.xip.io
last uploaded: Thu Oct 29 09:45:48 UTC 2015
stack: cflinuxfs2
buildpack: binary_buildpack

     state     since                    cpu     memory      disk      details   
#0   running   2015-10-29 06:46:01 PM   28.3%   5M of 16M   0 of 1G 

OKのようです。
発行されたURLは後ほど使います。

2. shooter-html5 のデプロイ

2.1. ソースコードの入手

さて、続いてHTTPのサーバ、 shooter-html5 のデプロイに移ります。
まずはソースコードの入手から。

$ git clone https://github.com/xiam/shooter-html5
$ cd shooter-html5/src/
shooter-html5/src$ ls
assets  css  index.html  js

HTML+JavaScriptのアプリです。

2.2. 事前準備

shooter-server への接続先設定

まずは先ほどデプロイした shooter-server のURLを、WebSocketの接続先として登録します。

shooter-html5/src$ vi js/main.js 
// Websocker server address.
var WEBSOCKET_SERVICE = 'ws://shooter-server.192.168.15.91.xip.io/w/';
・・・
shooter-html5/src$ git diff --no-prefix js/main.js
diff --git src/js/main.js src/js/main.js
index b114c65..5e4cbbd 100644
--- src/js/main.js
+++ src/js/main.js
@@ -1,5 +1,5 @@
 // Websocker server address.
-var WEBSOCKET_SERVICE = 'ws://shooter.io/w/';
+var WEBSOCKET_SERVICE = 'ws://shooter-server.192.168.15.91.xip.io/w/';
 
 // Frames configuration.
 var FRAMES_PER_SECOND = 24;

今回のアプリのソースコードのファイル構成を眺めてみると、2箇所程Symbolic Linkが張られている所があります。

shooter-html5/src$ tree js
js
├── controller.js
├── entity.js
├── fire.js
├── game.js
├── isMobile.js -> isMobile.min.js
├── isMobile.min.js
├── jquery.js -> jquery.min.js
├── jquery.min.js
├── json2.js
├── layer.js
├── license-sm2.txt
├── lifebar.js
├── main.js
├── powerup.js
├── radar.js
├── require.js
├── score.js
├── screen.js
├── ship.js
├── sm2.js
├── sound.js
├── swf
│   └── soundmanager2.swf
├── util.js
└── ws.js

ローカルで起動させている分にはこのままでも良いのですが、実は、Cloud FoundryのCLIは、pushの際にSymbolic Linkを無視する仕様になっています。

※参考

一時凌ぎな解決策ではありますが、ここではSymbolic Linkを外してHard Linkに変更しておきましょう。
もちろん中身をCopyしたファイルを用意しておくのもOKです。

shooter-html5/src$ unlink js/isMobile.js 
shooter-html5/src$ ln js/isMobile.min.js js/isMobile.js
shooter-html5/src$ unlink js/jquery.js 
shooter-html5/src$ ln js/jquery.min.js js/jquery.js 

manifestファイルの作成

こちらもmanifestファイルを作っておきます。

shooter-html5/src$ vi manifest.yml 
---
applications:
- name: shooter
  domain: 192.168.15.91.xip.io
  buildpack: staticfile_buildpack
  memory: 16M

2.3. Cloud Foundryへのデプロイ

cf push でデプロイします。

shooter-html5/src$ cf push
・・・
-----> Uploading droplet (4.3M)

1 of 1 instances running

App started


OK

App shooter was started using this command `sh boot.sh`

Showing health and status for app shooter in org ukaji / space default as ukaji...
OK

requested state: started
instances: 1/1
usage: 16M x 1 instances
urls: shooter.192.168.15.91.xip.io
last uploaded: Thu Oct 29 07:01:49 UTC 2015
stack: cflinuxfs2
buildpack: staticfile_buildpack

     state     since                    cpu    memory        disk      details   
#0   running   2015-10-29 04:01:59 PM   0.0%   5.2M of 16M   0 of 1G  

OKですね。

3. 動作確認

発行されたURLにアクセスします。

名前を登録して START >> を押すとゲーム開始です。

操作方法はシンプルで、

  • 上下左右キーで移動
  • SPACEキーで射撃

これだけです。
自機のライフが0になるまで敵を撃墜し続け最終スコアを競う、というゲームのようです。

おまけ

さて、今回は2つのCloud Foundryアプリケーションが連携して動くものをご紹介しました。
折角なので、一回の cf push で両方のデプロイが終わるような設定にしてみましょう。
方法としては簡単で、1つのmanifestファイルから2つのアプリケーションを呼び出す、という方法を取ります。

適当なディレクトリ配下に、 shooter-serverのbinaryが入ったgo-serverのディレクトリと shooter-html5 のディレクトリを配置します。

$ tree -L 3
.
├── go-server
│   ├── manifest.yml
│   └── shooter-server
└── shooter-html5
    ├── LICENSE
    ├── README.md
    └── src
        ├── assets
        ├── css
        ├── index.html
        ├── js
        └── manifest.yml

必要なmanifestファイルは以下の通りです。

$ vi manifest.yml
---
applications:
- name: shooter-server
  domain: 192.168.15.91.xip.io
  path: go-server/
  buildpack: binary_buildpack
  memory: 16M
  command: ./shooter-server -listen :$PORT
  env:
    MONGO_URI: "mongodb://ad55761d-bd50-40f1-ae0f-875cc1c92e96:password@192.168.15.91:27017/7f7607ab-db63-465a-95c9-2b2318c5d68d"
  services:
  - shooter-mongo
- name: shooter
  domain: 192.168.15.91.xip.io
  path: shooter-html5/src/
  buildpack: staticfile_buildpack
  memory: 16M

path という項目を使い、デプロイを行うソースコードが入ったディレクトリを相対パスで指定しています。
もう個別のmanifestファイルは特に必要ないので消しておきましょう。

$ rm go-server/manifest.yml
$ rm shooter-html5/src/manifest.yml 

ファイル構成は以下のようになっているはずです。

$ tree -L 3
.
├── go-server
│   └── shooter-server
├── manifest.yml
└── shooter-html5
    ├── LICENSE
    ├── README.md
    └── src
        ├── assets
        ├── css
        ├── index.html
        └── js

この状態でデプロイを行います。

$ cf push
・・・
-----> Uploading droplet (2.2M)

1 of 1 instances running

App started


OK

App shooter-server was started using this command `./shooter-server -listen :$PORT`

Showing health and status for app shooter-server in org ukaji / space default as ukaji...
OK

requested state: started
instances: 1/1
usage: 16M x 1 instances
urls: shooter-server.192.168.15.91.xip.io
last uploaded: Fri Oct 30 02:28:48 UTC 2015
stack: cflinuxfs2
buildpack: binary_buildpack

     state     since                    cpu     memory        disk      details   
#0   running   2015-10-30 11:29:01 AM   19.3%   4.6M of 16M   0 of 1G      
・・・
-----> Uploading droplet (4.3M)

1 of 1 instances running

App started


OK

App shooter was started using this command `sh boot.sh`

Showing health and status for app shooter in org ukaji / space default as ukaji...
OK

requested state: started
instances: 1/1
usage: 16M x 1 instances
urls: shooter.192.168.15.91.xip.io
last uploaded: Fri Oct 30 02:29:23 UTC 2015
stack: cflinuxfs2
buildpack: staticfile_buildpack

     state     since                    cpu    memory        disk      details   
#0   running   2015-10-30 11:29:35 AM   0.0%   5.2M of 16M   0 of 1G

cf push コマンド1回で、2つのデプロイが実行されるようになりました。

今回使用したソフトウェア