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

2015-07-16

GoBB を Cloud Foundry で動かす

「Cloud Foundry 百日行」第31日目は,Go 言語で書かれた BBS アプリ GoBB です。私の記憶に間違いがなければ,百日行シリーズ初の Golang アプリです。

基本情報

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

  • 1) ソースコードの取得
  • 2) ソースコードの変更
  • 3) Cloud Foundry 環境へのプッシュ
  • 4) 動作確認

1. ソースコードの取得

$ git clone https://github.com/stevenleeg/gobb.git
..
$ cd gobb/

2. ソースコードの変更

このアプリは,少し古い(最終更新が2014年8月)ということもあって,けっこう手を加えないと Cloud Foundry 上で動作しませんでした。

ソースコード変更の概要は以下の通りです。

  • a) .godir の追加
  • b) 待ち受けポートを環境変数からも設定できるよう変更
  • c) データベース接続情報を環境変数からも設定できるよう変更
  • d) Go 言語のコンパイル環境がなくても動作するよう変更
  • e) admin_topbar の復活 (Cloud Foundry とは無関係な変更)
  • f) .cfignore の追加

ステップ数は多いのですが,一つ一つのステップの内容は割とシンプルです。

手順を辿るのが面倒な方は, 修正後のコード を GitHub 上に置いたので,そちらをご利用ください。

a) .godir の追加

Cloud Foundry の go-buildpack では,アップロードされたソースコードを依存関係も含めてコンパイルするための方法として,.godir と Godep の2つが用意されています。

https://github.com/cloudfoundry/go-buildpack/tree/v1.3.1#godir-and-godeps

上記 README に書かれている通り,.godir は obsolete な方法で,Godep を使うのが新しい&正しい方法なのですが,本アプリは Godep を使って書かれていないため,今回は .godir を使う事にしました。

$ git show 1af161d
commit 1af161d4d27685aab467be1c182177bb2ab186ab
Author: Noburou TANIGUCHI <dev@nota.m001.jp>
Date:   Thu Jul 16 11:43:55 2015 +0900

    Set .godir to use with Cloud Foundry go-buildpack

diff --git a/.godir b/.godir
new file mode 100644
index 0000000..68848f4
--- /dev/null
+++ b/.godir
@@ -0,0 +1 @@
+github.com/stevenleeg/gobb

.godir には,ソースコードをアップロードしたものと置き換える対象となるパッケージ・パスを記述します。本アプリでは上に示したように github.com/stevenleeg/gobb となっていますが,こう指定すると $GOPATH/src/github.com/stevenleeg/gobb が,アップロードしたソースコードと置き換えられます。

これを指定しない場合, go get で取得された https://github.com/stevenleeg/gobb のソースコードがそのまま使われるため,次節以降の変更が反映されず,アプリが正常に動作しなくなります。

b) 待ち受けポートを環境変数からも設定できるよう変更

次に,待ち受けポートを環境変数からも設定できるようにソースコードを変更します。

オリジナルのコードでは,アプリの待ち受けポートは設定ファイルで指定するようになっているのですが,Cloud Foundry 上ではシステムが指定する待ち受けポートが動的に変わるので,このままでは動かすのが大変です。そこで,環境変数 PORT が設定されている場合はそちらを待ち受けポートとして使うよう,コードを変更しました。

$ git show 1dc5161
commit 1dc516197a827cbfa343ad235984a274324e2807
Author: Noburou TANIGUCHI <dev@nota.m001.jp>
Date:   Fri Jan 23 01:06:07 2015 +0900

    Respect PORT env

    Making it easy to run on PaaS.

diff --git a/gobb/main.go b/gobb/main.go
index 1440a51..73aba4e 100644
--- a/gobb/main.go
+++ b/gobb/main.go
@@ -10,6 +10,7 @@ import (
        "github.com/stevenleeg/gobb/utils"
        "go/build"
        "net/http"
+       "os"
        "path/filepath"
 )

@@ -82,7 +83,10 @@ func main() {

        http.Handle("/", r)

-       port, err := config.Config.GetString("gobb", "port")
+       port := os.Getenv("PORT")
+       if port == "" {
+               port, _ = config.Config.GetString("gobb", "port")
+       }
        fmt.Println("[notice] Starting server on port " + port)
        http.ListenAndServe(":"+port, nil)
 }

PORT が設定されていない場合は,従来通り設定ファイルで指定されたポートを使うようになっています。

c) データベース接続情報を環境変数からも設定できるよう変更

次は,同様の修正をデータベース接続情報についても行います。

環境変数 DATABASE_URL が設定されている場合はそちらをDB接続情報として使い,設定されていない場合は従来通り設定ファイルの値を使うよう,コードを変更しました。

$ git show 14afe27
commit 14afe276d83d11d5b938525f1605daad8dd89ab5
Author: Noburou TANIGUCHI <dev@nota.m001.jp>
Date:   Thu Jan 22 21:15:10 2015 +0900

    Respect DATABASE_URL env

    Making it easy to run on PaaS.

diff --git a/models/connection.go b/models/connection.go
index d43a177..9405eb0 100644
--- a/models/connection.go
+++ b/models/connection.go
@@ -37,13 +37,17 @@ func GetDbSession() *gorp.DbMap {
                db_port = "5432"
        }

-       db, err := sql.Open("postgres",
-               "user="+db_username+
-                       " password="+db_password+
-                       " dbname="+db_database+
-                       " host="+db_hostname+
-                       " port="+db_port+
-                       " sslmode=disable")
+       data_source := os.Getenv("DATABASE_URL")
+       if data_source == "" {
+               data_source = "user=" + db_username +
+                       " password=" + db_password +
+                       " dbname=" + db_database +
+                       " host=" + db_hostname +
+                       " port=" + db_port +
+                       " sslmode=disable"
+       }
+
+       db, err := sql.Open("postgres", data_source)

        if err != nil {
                fmt.Printf("Cannot open database! Error: %s\n", err.Error())
diff --git a/utils/migrations.go b/utils/migrations.go
index 122acbb..c0bcf70 100644
--- a/utils/migrations.go
+++ b/utils/migrations.go
@@ -40,12 +40,17 @@ func generateGooseDbConf() *goose.DBConf {
                db_port = "5432"
        }

+       openstr := os.Getenv("DATABASE_URL")
+       if openstr == "" {
+               openstr = fmt.Sprintf("user=%s dbname=%s password=%s port=%s host=%s sslmode=disable", db_userna
+       }
+
        goose_conf = &goose.DBConf{
                MigrationsDir: migrations_path,
                Env:           "development",
                Driver: goose.DBDriver{
                        Name:    "postgres",
-                       OpenStr: fmt.Sprintf("user=%s dbname=%s password=%s port=%s host=%s sslmode=disable", db
+                       OpenStr: openstr,
                        Import:  "github.com/lib/pq",
                        Dialect: &goose.PostgresDialect{},
                },

d) Go 言語のコンパイル環境がなくても動作するよう変更

本アプリのコードは,なぜか Go 言語のコンパイル環境がないと実行できないようになっています。Go 言語のバイナリは静的リンクで生成され,ライブラリーの動的リンクを必要としません。本来,生成されたバイナリ・ファイル1つを持って行けば(バイナリ五感の環境なら)どこでも動く点が Go の売りの一つなので,これはやや本末転倒感があります。

そこで,環境変数 RUN_WITHOUT_GOENV が真であれば,Go 言語のコンパイル環境なしでも動くよう,コードを変更しました。環境変数 RUN_WITHOUT_GOENV が未設定または偽であれば,従来通りの動作となります。

$ git show ec23f4a
commit ec23f4a7730b64a8011f4ab7f27127859fcfb705
Author: Noburou TANIGUCHI <dev@nota.m001.jp>
Date:   Sun Jan 25 16:24:02 2015 +0900

    Run without Golang environment dependency

diff --git a/gobb/main.go b/gobb/main.go
index 73aba4e..7b6bc63 100644
--- a/gobb/main.go
+++ b/gobb/main.go
@@ -69,6 +69,9 @@ func main() {
        if selected_template == "default" {
                pkg, _ := build.Import("github.com/stevenleeg/gobb/gobb", ".", build.FindOnly)
                static_path := filepath.Join(pkg.SrcRoot, pkg.ImportPath, "../templates")
+               if utils.RunWithoutGoenv {
+                       static_path = filepath.Join(utils.Buildpath, "templates")
+               }
                r.PathPrefix("/static/").Handler(http.FileServer(http.Dir(static_path)))
        } else {
                static_path := filepath.Join(base_path, "templates", selected_template)
diff --git a/utils/migrations.go b/utils/migrations.go
index c0bcf70..173983b 100644
--- a/utils/migrations.go
+++ b/utils/migrations.go
@@ -24,6 +24,9 @@ func generateGooseDbConf() *goose.DBConf {
        db_hostname, _ := config.Config.GetString("database", "hostname")
        db_port, _ := config.Config.GetString("database", "port")
        migrations_path := filepath.Join(pkg.SrcRoot, pkg.ImportPath, "../db/migrations")
+       if RunWithoutGoenv {
+               migrations_path = filepath.Join(Buildpath, "db", "migrations")
+       }

        db_env_hostname, _ := config.Config.GetString("database", "env_hostname")
        db_env_port, _ := config.Config.GetString("database", "env_port")
diff --git a/utils/path.go b/utils/path.go
new file mode 100644
index 0000000..00169fb
--- /dev/null
+++ b/utils/path.go
@@ -0,0 +1,23 @@
+package utils
+
+import (
+       "log"
+       "os"
+       "path/filepath"
+       "strconv"
+)
+
+var RunWithoutGoenv bool = func() bool {
+       run_without_goenv, _ := strconv.ParseBool(os.Getenv("RUN_WITHOUT_GOENV"))
+       return run_without_goenv
+}()
+
+var Buildpath string = func() string {
+       // Assume the executable exists directly under BUILDPATH/bin
+       buildpath, err := filepath.Abs(filepath.Join(filepath.Dir(os.Args[0]), ".."))
+       if err != nil {
+               log.Fatal(err)
+       }
+       log.Printf("BuildPath buildpath='%v'\n", buildpath)
+       return buildpath
+}()
diff --git a/utils/template.go b/utils/template.go
index f7c36f1..4917702 100644
--- a/utils/template.go
+++ b/utils/template.go
@@ -128,6 +128,9 @@ func RenderTemplate(
        if selected_template == "default" {
                pkg, _ := build.Import("github.com/stevenleeg/gobb/gobb", ".", build.FindOnly)
                base_path = filepath.Join(pkg.SrcRoot, pkg.ImportPath, "../templates/")
+               if RunWithoutGoenv {
+                       base_path = filepath.Join(Buildpath, "templates")
+               }
        } else {
                base_path, _ = config.Config.GetString("gobb", "base_path")
                base_path = filepath.Join(base_path, "templates", selected_template)

e) admin_topbar の復活 (Cloud Foundry とは無関係な変更)

次に,これは動作確認中に気づいた問題に対する修正なのですが, admin_topbar というテンプレート要素が削除されてしまったせいでアプリが正常に動作しないため,これを復活させました。

Cloud Foundry とは完全に無関係な修正で,普通はやらないのですが,今回は動作確認に支障を来したので特別に行いました。

$ git show 3ef8584
commit 3ef858494b3a9f1f986218610b5c849bc1d51f5a
Author: Noburou TANIGUCHI <dev@nota.m001.jp>
Date:   Sun Jan 25 16:57:34 2015 +0900

    Resurrect admin_topbar

diff --git a/templates/base.html b/templates/base.html
index f412540..759cb55 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -1,3 +1,12 @@
+
+<table class="box_tabs">
+    <tr>
+        <td><a href="/admin">General</a></td>
+        <td><a href="/admin/boards">Boards</a></td>
+        <td><a href="/admin/users">Users</a></td>
+    </tr>
+</table>
+
 <!DOCTYPE html>
 <html>
   <head>

f) .cfignore の追加

最後に,(これは必須ではないのですが) Cloud Foundry 上にアプリをプッシュする際,余分なファイルをアップロードしないよう .cfignore というファイルを作ってそこに不要なファイルを指定します。

$ git show 3d7bc1a
commit 3d7bc1a9f0960b8483529fead74af77bfde0d6d6
Author: Noburou TANIGUCHI <dev@nota.m001.jp>
Date:   Sun Jan 25 16:24:27 2015 +0900

    Add .cfignore not to upload unneccesary files to Cloud Foundry

diff --git a/.cfignore b/.cfignore
new file mode 100644
index 0000000..ed4e977
--- /dev/null
+++ b/.cfignore
@@ -0,0 +1,5 @@
+*.swp
+.DS_Store
+dbconf.yml
+templates/static/.sass-cache
+bin/gobb

.cfignore は,.gitignore と似た (同じ?) 記法で不要ファイルを指定する仕組みです。詳しくは

https://docs.cloudfoundry.org/devguide/deploy-apps/prepare-to-deploy.html#exclude

をご覧ください。

2. Cloud Foundry 環境へのプッシュ

ここまでで必要なソースコードの修正は終わりましたので,次にアプリを Cloud Foundry 上にプッシュします。

が,実際にプッシュするその前に。

ここまで何度か出てきた「設定ファイル」を作っておきます。サンプルが ./gobb/gobb.sample.conf にあるので,それをコピーして1行だけ書き換えます。

$ cp ./gobb/gobb.sample.conf ./gobb/gobb.conf
$ emacs ./gobb/gobb.conf
..

出来上がったファイルはこちらです:

$ cat gobb/gobb.conf
;; This section deals with various gobb-specific settings
[gobb]
site_name=gobb
cookie_key=encrypt_your_cookies
posts_per_page=15
threads_per_page=30
enable_signatures=true
port=8080

;; Base URL of your site. Don't include the http:// but DO
;; include the trailing slash.
;;
;; Example: example.com/forum/
base_url=gobb.10.244.0.34.xip.io/

;; The base path is a directory that houses any custom
;; gobb things (eg, templates, css modifications, etc.)
;; This field is optional (disabled by default), but we
;; reccommend that you set it to where your config file
;; is located (keep things organized!)
;base_path=/path/to/gobb/directory

;; This section deals with the database connection. It's
;; definitely not optional, so you should fill it in now.
[database]
username=db_username
password=db_password
database=db_name
hostname=localhost
port=5432

;; These options are used for docker port redirection.
;; env_hostname=POSTGRES_PORT_5432_TCP_PORT
;; env_port=POSTGRES_PORT_5432_TCP_ADDR

;; If you have a Google Analytics account, put your information
;; in this section (optional)
[googleanalytics]
tracking_id=
account=

実際には,(上述の通り) 次の1行を書き換えただけです。

$ git diff --no-index gobb/gobb.sample.conf gobb/gobb.conf
diff --git a/gobb/gobb.sample.conf b/gobb/gobb.conf
index c9f3011..7593a07 100644
--- a/gobb/gobb.sample.conf
+++ b/gobb/gobb.conf
@@ -11,7 +11,7 @@ port=8080
 ;; include the trailing slash.
 ;;
 ;; Example: example.com/forum/
-base_url=localhost:8080/
+base_url=gobb.10.244.0.34.xip.io/

 ;; The base path is a directory that houses any custom
 ;; gobb things (eg, templates, css modifications, etc.)

本来このファイルは .gitignore に含まれていて Git 管理の対象外なのですが,一応 こちら に置いてあるので,必要な方は参考にしてください。

準備が整ったので,いよいよ Cloud Foundry の操作に入ります。

まずは,データベース・サービスを作成します。GoBB の README では PostgreSQL を使うよう書いてあるので,PostgreSQL サービスを作成します。

$ cf create-service PostgreSQL "Basic PostgreSQL Plan" pg4gobb
Creating service instance pg4gobb in org nota-ja / space 100 as nota-ja...
OK
..

次にアプリを (データベースを使うので) 非起動状態でプッシュします。

$ cf push gobb --no-start
..
OK

アプリとサービスをバインドします。

$ cf bind-service gobb pg4gobb
Binding service pg4gobb to app gobb in org nota-ja / space 100 as nota-ja...
OK

バインドすると環境変数 VCAP_SERVICES が見えるようになるので,そこから DATABASE_URL に設定する値を取得します。

$ cf env gobb | grep postgres
     "uri": "postgres://a61ab179-f1d3-4daa-9864-3f4ec9f8cc81:g06isuvj22nqng5s9ni2fs9n1@192.168.15.91:5432/a61ab179-f1d3-4daa-9864-3f4ec9f8cc81"

環境変数 DATABASE_URL を設定します。

ここで注意点が一つ。今回使う PostgreSQL は SSL/TLS 接続をサポートしていないので, DATABASE_URL を設定する時は先に取得した値の後ろに ?sslmode=disable を付ける必要があります。

 cf set-env gobb DATABASE_URL "postgres://61bda49d-5172-4938-9584-4f6624a212f6:8lapo7ba10tcv6liia9r6dmb61@192.168.15.91:5432/61bda49d-5172-4938-9584-4f6624a212f6?sslmode=disable"
Setting env variable 'DATABASE_URL' to 'postgres://61bda49d-5172-4938-9584-4f6624a212f6:8lapo7ba10tcv6liia9r6dmb61@192.168.15.91:5432/61bda49d-5172-4938-9584-4f6624a212f6?sslmode=disable' for app gobb in org nota-ja / space 100 as nota-ja...
OK
TIP: Use 'cf restage' to ensure your env variable changes take effect

環境変数 RUN_WITHOUT_GOENV を設定します。

$ cf set-env gobb RUN_WITHOUT_GOENV true
Setting env variable 'RUN_WITHOUT_GOENV' to 'true' for app gobb in org nota-ja / space 100 as nota-ja...
OK
TIP: Use 'cf restage' to ensure your env variable changes take effect

環境変数の設定を確認します。

$ cf env gobb
..
User-Provided:
DATABASE_URL: postgres://a61ab179-f1d3-4daa-9864-3f4ec9f8cc81:g06isuvj22nqng5s9ni2fs9n1@192.168.15.91:5432/a61ab179-f1d3-4daa-9864-3f4ec9f8cc81?sslmode=disable
RUN_WITHOUT_GOENV: true
..

正しく設定されています。

起動コマンドを指定して再プッシュします。--migrate は,データベースのスキーマを設定するためのオプションです。2回目以降の起動では (スキーマに変更がない限り) 不要です。

$ cf push gobb -c "bin/gobb --config gobb/gobb.conf --migrate"
Updating app gobb in org nota-ja / space 100 as nota-ja...
..
requested state: started
instances: 1/1
usage: 256M x 1 instances
urls: gobb.10.244.0.34.xip.io
last uploaded: Thu Jul 16 02:06:51 UTC 2015
stack: cflinuxfs2
buildpack: Go

     state     since                    cpu    memory         disk      details
#0   running   2015-07-16 11:08:38 AM   0.0%   4.7M of 256M   0 of 1G

起動しました。

4. 動作確認

ブラウザからアプリにアクセスします。

最初の画面はこのようになっています:

右上の【register】をクリックしてユーザー登録画面に入り,ユーザーを登録します:

登録が済むとログイン画面に遷移するので,今登録したユーザーでログインします:

ログインすると初期画面と似た画面が出てきますが,右上が少し変化しています:

右上の真ん中の【admin】(ユーザー名 “admin” と管理画面の “admin” が被ってしまってちょっとわかりにくくなってしまいました) をクリックして,管理画面に入ります:

左上の【Boards】をクリックしてボード作成画面に入ります:

ボード名と説明を記入して【create】をクリックします:

トップ画面に戻ると,先ほど作った test ボードが見えるので,クリックして入ってみます:

ボードの画面はこんな感じです:

右上の【new thread】を押して,新しいスレッドを作ります:

スレッドが作られ,投稿が表示されました:

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