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

2015-09-25

Mattermost を Cloud Foundry で動かす

「Cloud Foundry 百日行」第66日目は,Go + Node.js (+ Ruby) で書かれた Slack のオープンソース版の代替 Mattermost です。チーム・コミュニケーション・ツールとして最近とみに利用が増えている Slack ですが,Mattermost はその代替としてはかなりよくできていると感じました。公式サイトには,Mattermost を作った理由として, 「ロックインされるのが嫌だった」 と書かれていますが,そういう人にとっては有力な選択肢の一つになるのではないでしょうか。

基本情報

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

  • 1) ソースコードの取得
  • 2) 起動方法の検討
  • 3) Cloud Foundry 向け改造
  • 4) アプリのプッシュ準備
  • 5) サービスの作成及びアプリとのバインド
  • 6) アプリの起動
  • 7) 動作確認

1. ソースコードの取得

GitHub からソースコードを clone します。

$ git clone https://github.com/mattermost/platform.git

ディレクトリーを移動して,v0.6.0 を checkout します。

$ cd platform/
$ git checkout v0.6.0

2. 起動方法の検討

#とにかく動かしてみたい,という方は,
# https://github.com/nota-ja/platform.git を clone または fetch し,
# b264cba92acd606edf1f0dfa2814c5413aaa5575 を checkout した状態で,
「4. アプリのプッシュ準備」 まで進んでください。

Mattermost の README.md には,全て Docker を使うインストール方法しか書かれていませんが, 開発者向け文書 には,make を使う方法が示されていました。そこで Makefile を見てみると,MySQL と Redis の構築に Docker を使い,本体の動作に関わる部分を自力で構築する方法が示されていました。具体的には,Makefile の installrun のプロセスを再現できれば,Cloud Foundry 上で動かすことができそうです。

make install の内容をまとめると以下のようになります。

  • a) Godep のインストール
  • b) MySQL のインストール
  • c) Redis のインストール
  • d) web/react/ ディレクトリーでの npm install

make run の内容をまとめると以下のようになります。

  • e) web/static/js ディレクトリーの作成
  • f) react processor の起動
  • g) go web server の起動
  • h) compass watch の起動

しかし,別途調べた結果, GitHub の “Heroku Installation” という issue によると,「Dockerfile を参考にせよ」ということだったので, 該当する Dockerfile を見てみると,こちらでは Redis を使っていない模様です。

さらに, ChangeLog に,”Removed use of Redis to simplify on-premise installation” という一文を見つけたので,Redis は不要と判断しました。

まとめると,

  • a) Godep のインストール ← Cloud Foundry の go-buildpack にあるので不要
  • b) MySQL のインストール ← Cloud Foundry 環境では create-service で作成可能
  • c) Redis のインストール ← 不要
  • d) web/react/ ディレクトリーでの npm install

  • e) web/static/js ディレクトリーの作成
  • f) react processor の起動
  • g) go web server の起動
  • h) compass watch の起動

となりました。

これを Cloud Foundry 上で実現しようとすると,まず問題になるのが,Node.js (React), Golang, Ruby (Compass) という3つの異なる言語の実行環境をどう用意するかということです。

解決策として考えたのは以下の2つでした。

  • X) heroku-buildpack-multi を使い,nodejs-buildpack, go-buildpack, ruby-buildpack を適用
  • Y) heroku-buildpack-multi を使い,heroku-buildpack-apt と go-buildpack を適用
    & heroku-buildpack-apt で nodejs と ruby-compass をインストール

本稿では Y 案を採用し,最終的に動かすことができたのですが,起動処理がかなり複雑になってしまったので,そのあたりは反省材料として別の機会に活かしたいと考えています。

では,この検討を踏まえたコードの改造について,以下で見ていきます。

3. Cloud Foundry 向け改造

#<再掲>
#とにかく動かしてみたい,という方は,
# https://github.com/nota-ja/platform.git を clone または fetch し,
# b264cba92acd606edf1f0dfa2814c5413aaa5575 を checkout した状態で,
「4. アプリのプッシュ準備」 まで進んでください。

3.1. heroku-buildpack-multi 及び heroku-buildpack-apt 用設定ファイルの作成

2節で Y 案を採用すると決めたので,まず heroku-buildpack-multi と heroku-buildpack-apt 用の設定ファイルを作成します。

.buildpacks (heroku-buildpack-multi 用設定ファイル) を以下の内容で作成します。go-buildpack は,今回使っている Cloud Foundry 環境に入っているのと同じ v1.3.1 を指定することにしました。

$ cat .buildpacks
https://github.com/ddollar/heroku-buildpack-apt.git
https://github.com/cloudfoundry/go-buildpack.git#v1.3.1

同時に,Aptfile (heroku-buildpack-apt 用設定ファイル) を以下の内容で作成します。compass については,Ruby を apt で入れて,compass を gem として入れるのではなく,ruby-compass というパッケージがあったのでそれを入れることにしました。

$ cat Aptfile
nodejs
npm
ruby-compass

3.2. 起動スクリプトの作成

2節で示した「起動までに必要な処理」を再度まとめます。

  • d) web/react/ ディレクトリーでの npm install
  • e) web/static/js ディレクトリーの作成
  • f) react processor の起動
  • g) go web server の起動
  • h) compass watch の起動

このうち,d の npm install については,Y 案を採用した結果ステージング時の実行は不可能になったため,

  • Cloud Foundry にアプリをプッシュする前に実行しておく
  • 起動スクリプト内で実行する

の2つの選択肢が残ったのですが,後者ではアプリ起動の度に npm install が走ることになり,起動時間が延びる上に時間の無駄なので,前者を採用することにしました。

そうなると,起動スクリプトで実行が必要なのは e, f, g, h となり,スクリプトは (Node.js や Ruby / Compass が heroku-buildpack-apt で適切にインストールされている前提で) 以下のようになるはずでした。

(起動スクリプトのイメージ)

mkdir -p web/static/js

echo starting react processor
pushd web/react
npm start &
popd

echo starting compass watch
pushd web/sass-files
compass watch &
popd

echo starting go web server
platform -config config/config.json

make run との違いは

  • go web server と compass の起動順序を入れ替えた
    Cloud Foundry では最後に起動されたプロセスが重要になることがあるので,念のため入れ替えた
  • go web server の起動にコンパイル済みバイナリを使うようにした
    ステージング時にコンパイル済みバイナリが生成され,それを使うのが go-buildpack の標準作法なので

くらいで,基本的には make run と同じになることを想定していました。

しかし実際には,「Node.js や Ruby / Compass が heroku-buildpack-apt で適切にインストールされている前提」が間違っていたため,かなり patchy な修正をこのスクリプトに施すことが必要でした。

最終的に起動スクリプトは以下のようになりました。

$ cat start.sh
#!/bin/bash

set -x

source .profile.d/000_apt.sh

mkdir -p web/static/js

sed --in-place=.0 's/^#!\/usr\/bin\/nodejs/#!\/usr\/bin\/env nodejs/g' .apt/usr/bin/npm
sed --in-place=.01 's/require("\.\.\/lib/require("..\/share\/npm\/lib/g' .apt/usr/bin/npm
sed --in-place=.0 "s/\/usr\/share\/node-mime/\/home\/vcap\/app\/.apt\/usr\/share\/node-mime/g" .apt/usr/lib/nodejs/mime.js

pushd $HOME/.apt/usr/bin
ln -s nodejs node
popd

pushd web/react/node_modules/.bin
ln -s ../browserify/bin/cmd.js browserify
ln -s ../envify/bin/envify envify
ln -s ../eslint/bin/eslint.js eslint
ln -s ../jest-cli/bin/jest.js jest
ln -s ../uglify-js/bin/uglifyjs uglifyjs
ln -s ../watchify/bin/cmd.js watchify
popd

echo starting react processor
pushd web/react
NODE_PATH=$HOME/.apt/usr/lib/nodejs:$HOME/.apt/usr/share/npm/node_modules:$PWD/node_modules npm start &
popd

# echo starting compass watch
# pushd web/sass-files
# compass watch &
# popd

echo starting go web server
source .profile.d/go.sh
platform -config config/config.json || nc -l -k $PORT

以下,スクリプトの内容について簡単に解説します。「そんな説明はいらない」という方は読み飛ばしてください。

  • 5 行目
    .profile.d/000_apt.sh は heroku-buildpack-apt を使ってインストールされた apt のパッケージを使うための環境変数設定が入ったファイルなので,heroku-buildpack-apt を使った際はこれを source する方が良いようです

  • 9 - 11 行目
    heroku-buildpack-apt でインストールされた nodejs パッケージに含まれているスクリプトの一部に,特定の固定的なパスを前提としたものがあり,そのままでは動作しなかったので,環境に合うよう sed で置換しました

  • 13 - 15 行目
    apt-get install でインストールされた nodejs パッケージでは,通常 node で起動されるコマンドの名前が nodejs になっています(有名な問題のようです)
    ところがスクリプトの一部で nodejs ではなく node コマンド名指しで呼び出しを行っているものがあり,そのままでは正常に動作しなかったので,シンボリック・リンクを張って node でもコマンドを呼び出せるようにしました

  • 17 - 24 行目
    Cloud Foundry の cf CLI は, cf push でアプリのファイルをアップロードする際, シンボリック・リンクを無視する仕様 になっています
    しかし,今回 npm start で呼び出されるスクリプトはシンボリック・リンク経由で呼び出されるため,そのままでは正常に起動しません
    そこで今回は(起動に必要なことが明示的にわかっているものだけでなく)同一ディレクトリーにあったシンボリック・リンクを全て復活するようにしました(暗黙的に呼び出されている可能性に対する保険)

  • 28 行目
    エラーが起きる度に,必要なパスを NODE_PATH に加えていった結果こうなりました

  • 31 - 34 行目
    今回は ruby-compass パッケージをインストールしたのですが, /usr/lib/ruby/1.9.1/rubygems/custom_require.rb:36:in 'require': cannot load such file -- compass (LoadError) というエラーが発生して起動できませんでした
    こちらも環境変数等を見直せば解決できたかもしれないのですが,時間がなかったのと compass のタスク (sass をコンパイルして css を生成する) の頻度/重要度がそれほど高くないと判断し,手元で一度 compass watch を実行して,その時点で生成された css ファイルだけで対応する方針とし,コメントアウトしました

  • 37 行目
    .profile.d/go.sh は go-buildpack によって生成されるファイルです
    go-buildpack でビルドされたバイナリへのパス設定が入っていました

  • 38 行目
    || nc -l -k $PORT については,本稿の最後「おまけ」で説明します

以上で起動スクリプトについては終わりです。

3.3. Go バージョンの修正

Mattermost の v0.6.0 では,Godeps/Godep.json で go1.4 を使うよう指定されていますが,今回使う go-buildpack v1.3.1 では既にこのバージョンはサポート外になっていたので,サポートされている go1.4.2 を使うよう修正しました。

$ git diff
diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json
index aa46b11..bd2e768 100644
--- a/Godeps/Godeps.json
+++ b/Godeps/Godeps.json
@@ -1,6 +1,6 @@
 {
        "ImportPath": "github.com/mattermost/platform",
-       "GoVersion": "go1.4",
+       "GoVersion": "go1.4.2",
        "Deps": [
                {
                        "ImportPath": "code.google.com/p/freetype-go/freetype",

3.4. go web server の改造

platform コマンドで起動される go web server は,ユーザーからリクエストを受けレスポンスを返す本アプリの外部インターフェイスであり,本体でもあります。

これを Cloud Foundry 上で正しく動作させるためには,これまで百日行で見てきた様々なアプリ同様,

  • 起動時に待ち受けポートを環境変数 $PORT から取る
  • データベース接続情報を環境変数 $DATABASE_URL あるいは $VCAP_SERVICES から取る

という改造が必要になります。

今回の改造の内容は以下の通りです。

3.4.1. 待ち受けポートを環境変数から取得

$ git diff
diff --git a/utils/config.go b/utils/config.go
index 8d9dd11..a70de6f 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -245,6 +245,11 @@ func LoadConfig(fileName string) {
        if err := CheckMailSettings(); err != nil {
                l4g.Error("Email settings are not valid err=%v", err)
        }
+
+       // Reconfigure Port if environment variable 'PORT' is set
+       if port := os.Getenv("PORT"); port != "" {
+               Cfg.ServiceSettings.Port = port
+       }
 }

 func getSanitizeOptions() map[string]bool {

3.4.2. データベース接続情報を環境変数環境変数から取得

以下の変更を utils/config.go に適用してください。

ちなみに, $DATABASE_URL$VCAP_SERVICES もない時は,従来通りの動作をするようになっています。

$DATABASE_URL からの取得

$ git diff
diff --git a/utils/config.go b/utils/config.go
index a70de6f..38a8aab 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -9,6 +9,9 @@ import (
        "net/mail"
        "os"
        "path/filepath"
+
+       "fmt"
+       "net/url"
 )

 const (
@@ -250,6 +253,50 @@ func LoadConfig(fileName string) {
        if port := os.Getenv("PORT"); port != "" {
                Cfg.ServiceSettings.Port = port
        }
+
+       if databaseUrl := os.Getenv("DATABASE_URL"); databaseUrl != "" {
+               // Reconfigure RDBMS connection if env 'DATABASE_URL' is set
+               fmt.Printf("DATABASE_URL: '%s'\n", databaseUrl)
+               driver, dsn, err := parseDatabaseUrl(databaseUrl)
+               if err == nil {
+                       Cfg.SqlSettings.DriverName, Cfg.SqlSettings.DataSource = driver, dsn
+                       fmt.Printf("Cfg.SqlSettings.DriverName: '%s'\n", Cfg.SqlSettings.DriverName)
+                       fmt.Printf("Cfg.SqlSettings.DataSource: '%s'\n", Cfg.SqlSettings.DataSource)
+                       Cfg.SqlSettings.DataSourceReplicas = []string{dsn}
+                       fmt.Printf("Cfg.SqlSettings.DataSourceReplicas: '%s'\n", Cfg.SqlSettings.DataSourceRepli
+               } else {
+                       fmt.Println("Error in parseDatabaseUrl:" + err.Error() + "; Skipped")
+               }
+       }
+}
+
+func parseDatabaseUrl(databaseUrl string) (driver string, dsn string, err error) {
+       uri, err := url.Parse(databaseUrl)
+       if err != nil {
+               return "", "", err
+       }
+       driver = uri.Scheme
+       if driver == "mysql2" {
+               // This is a fix for very ugly Ruby on Rails dependency in Cloud Foundry
+               driver = "mysql"
+       }
+       // unix domain socket is out of scope
+       dsn = fmt.Sprintf("%s@tcp(%s)%s", uri.User.String(), uri.Host, uri.Path)
+       if uri.RawQuery != "" {
+               query := uri.Query()
+               for key, _ := range query {
+                       if key == "reconnect" {
+                               // "reconnect" is not a MySQL server variable
+                               // cf. https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html
+                               query.Del(key)
+                       }
+               }
+               if len(query) > 0 {
+                       dsn = fmt.Sprintf("%s?%s", dsn, query.Encode())
+               }
+       }
+
+       return driver, dsn, nil
 }

 func getSanitizeOptions() map[string]bool {

$VCAP_SERVICES からの取得

diff --git a/utils/config.go b/utils/config.go
index 38a8aab..5fb1913 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -267,6 +267,23 @@ func LoadConfig(fileName string) {
                } else {
                        fmt.Println("Error in parseDatabaseUrl:" + err.Error() + "; Skipped")
                }
+       } else if vcapServices := os.Getenv("VCAP_SERVICES"); vcapServices != "" {
+               // Reconfigure RDBMS connection if env 'VCAP_SERVICES' is set
+               fmt.Printf("VCAP_SERVICES: '%s'\n", vcapServices)
+               if uri, err := getUriFromVcapServices(vcapServices); err == nil {
+                       driver, dsn, err := parseDatabaseUrl(uri)
+                       if err == nil {
+                               Cfg.SqlSettings.DriverName, Cfg.SqlSettings.DataSource = driver, dsn
+                               fmt.Printf("Cfg.SqlSettings.DriverName: '%s'\n", Cfg.SqlSettings.DriverName)
+                               fmt.Printf("Cfg.SqlSettings.DataSource: '%s'\n", Cfg.SqlSettings.DataSource)
+                               Cfg.SqlSettings.DataSourceReplicas = []string{dsn}
+                               fmt.Printf("Cfg.SqlSettings.DataSourceReplicas: '%s'\n", Cfg.SqlSettings.DataSou
+                       } else {
+                               fmt.Println("Error in parseDatabaseUrl:" + err.Error() + "; Skipped")
+                       }
+               } else {
+                       fmt.Println("Error in getUriFromVcapServices:" + err.Error() + "; Skipped")
+               }
        }
 }

@@ -299,6 +316,50 @@ func parseDatabaseUrl(databaseUrl string) (driver string, dsn string, err error)
        return driver, dsn, nil
 }

+func getUriFromVcapServices(vcapServices string) (string, error) {
+       svcsbytes := []byte(vcapServices)
+       var svcsif interface{}
+       if err := json.Unmarshal(svcsbytes, &svcsif); err != nil {
+               return "", fmt.Errorf("Bad JSON in VCAP_SERVICES:'%s'", vcapServices)
+       }
+
+       // just skip if error because there may be config in file
+       svcs, ok := svcsif.(map[string]interface{})
+       if !ok {
+               return "", fmt.Errorf("Unknown Format in VCAP_SERVICES:'%s'", vcapServices)
+       }
+       for _, val := range svcs {
+               // val shoul be an array
+               svcarr, ok := val.([]interface{})
+               if !ok {
+                       return "", fmt.Errorf("Unknown Format in VCAP_SERVICES:'%s'", vcapServices)
+               }
+               svc0 := []byte(fmt.Sprint(svcarr[0]))
+               if ok {
+                       return "", fmt.Errorf("Unknown Format in VCAP_SERVICES:'%s'", vcapServices)
+               }
+               fmt.Printf("svcarr[0]:'%s'\n", svc0)
+               type serviceBinding struct {
+                       Credentials struct {
+                               Uri string `json:"uri"`
+                       } `json:"credentials"`
+                       Label string        `json:"label"`
+                       Name  string        `json:"name"`
+                       Plan  string        `json:"plan"`
+                       Tags  []interface{} `json:"tags"`
+               }
+               var svcbind serviceBinding
+               if err := json.Unmarshal(svc0, &svcbind); err != nil {
+                       return "", fmt.Errorf("Unknown Format in VCAP_SERVICES:'%s'", vcapServices)
+               }
+               if svcbind.Credentials.Uri == "" {
+                       return "", fmt.Errorf("No '[credentials][uri]' in VCAP_SERVICES:'%s'", vcapServices)
+               }
+               return svcbind.Credentials.Uri, nil
+       }
+       return "", fmt.Errorf("Something wrong in parseDatabaseUrl")
+}
+
 func getSanitizeOptions() map[string]bool {
        options := map[string]bool{}
        options["fullname"] = Cfg.PrivacySettings.ShowFullName

3.5. メイル送信設定の修正

Mattermost はユーザーIDとしてメイルアドレスを用い,メイル送信サービスと連携してユーザーへの通知や招待を送ることができます。

#メイル送信機能を使わないこともできます。その場合この節はスキップしていただいてけっこうです。

今回は GMail をメイル送信サービスとして使いました。但し,下に公開するコードではアカウント名,パスワードはダミーの値にしてあります。適宜置き換えてご利用ください。

$ git diff
diff --git a/config/config.json b/config/config.json
index c446b51..701a3c9 100644
--- a/config/config.json
+++ b/config/config.json
@@ -59,13 +59,14 @@
         "InitialFont": "luximbi.ttf"
     },
     "EmailSettings": {
-        "ByPassEmail" : true,
-        "SMTPUsername": "",
-        "SMTPPassword": "",
-        "SMTPServer": "",
-               "UseTLS": false,
-        "FeedbackEmail": "",
-        "FeedbackName": "",
+        "ByPassEmail" : false,
+        "SMTPUsername": "YOUR-GMAIL-ACCOUNT",
+        "SMTPPassword": "YOUR-GMAIL-PASSWORD",
+        "SMTPServer": "smtp.gmail.com:465",
+        "UseTLS": true,
+        "UseStartTLS": false,
+        "FeedbackEmail": "YOUR-GMAIL-ACCOUNT@gmail.com",
+        "FeedbackName": "cf100 mattermost",
         "ApplePushServer": "",
         "ApplePushCertPublic": "",
         "ApplePushCertPrivate": ""

4. アプリのプッシュ準備

前節で,全てのコード変更が終わったので,次にアプリをプッシュする前に必要な作業を行います。3節で述べましたが,必要な作業は以下の2つです。

  • web/react での npm install の実行
  • web/sass-files での compass watch の実行

4.1. web/react での npm install の実行

$ pushd web/react/
$ npm install
..
$ popd

これを実行すると,中に大量の npm モジュールが入った web/react/node_modules/ というディレクトリーが生成されます。

4.2. web/sass-files での compass watch の実行

$ pushd web/sass-files
$ compass watch
>>> Compass is watching for changes. Press Ctrl-C to Stop.
    write /home/nota-ja/repos/mattermost-platform/web/static/css/styles.css
^C
★★★ Happy Styling! ★★★
$ popd

write 〜/web/static/css/styles.css というメッセージが出たら,CTRL-C でプロセスを止めます。

5. サービスの作成及びアプリとのバインド

MySQL サービスを作成します。

$ cf create-service p-mysql 1gb mysql-mm
Creating service instance mysql-mm in org nota-ja / space 100 as nota-ja...
OK

サービスとバインドするために,アプリを停止状態でプッシュします。

$ cf push mm --no-start
Creating app mm in org nota-ja / space 100 as nota-ja...
OK
..
Uploading mm...
Uploading app files from: /home/nota-ja/repos/mattermost-platform
Uploading 42M, 17240 files
Done uploading
OK

npm install 後の状態でプッシュしているため,ファイル数が多く,アップロードに多少時間がかかります。

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

$ cf bind-service mm mysql-mm
Binding service mysql-mm to app mm in org nota-ja / space 100 as nota-ja...
OK
TIP: Use 'cf restage mm' to ensure your env variable changes take effect

6. アプリの起動

起動コマンドと buildpack を指定してアプリを起動します。

$ cf push mm -c './start.sh' -b https://github.com/ddollar/heroku-buildpack-multi.git
Updating app mm in org nota-ja / space 100 as nota-ja...
OK
..
requested state: started
instances: 1/1
usage: 256M x 1 instances
urls: mm.10.244.0.34.xip.io
last uploaded: Thu Sep 17 20:27:38 UTC 2015
stack: cflinuxfs2
buildpack: https://github.com/ddollar/heroku-buildpack-multi.git

     state     since                    cpu     memory           disk      details
#0   running   2015-09-18 05:30:18 AM   24.8%   184.3M of 256M   0 of 1G

起動しました。メモリが若干カツカツにも見えるので,余裕が欲しい場合は増やしてみても良いと思います。

7. 動作確認

今回はブラウザーに Chrome を使っています。これは,他のブラウザーでは正常に動作しなかったことがあったためですが,それがブラウザーによる問題かアプリの問題かデプロイの問題かは切り分けていないので,他のブラウザーで動作しないということではありません。

アクセスすると,以下のような画面が見えるはずです:

メイルアドレスを入力して【Sign up】をクリックすると,確認画面が表示されます:

【Yes, this address is correct】をクリックすると(メイル送信設定を行った場合は)メイルが送られると同時に,チーム名入力画面に遷移します:

チーム名を入力して【Next】をクリックするとチームURLの入力画面になります:

チームURLを入力して【Next】をクリックするとメンバー招待画面になりますが:

ここは一旦【skip this step】をクリックしてスキップしました。

この後ユーザー名,パスワードの入力画面でそれぞれ適当な値を入力すると,ログイン画面にたどりつくので,メイルアドレスとパスワードを入力して【Sign in】をクリックします:

ログイン直後の画面はこんな感じで,Slack によく似ています:

チームの表示色を変え,メッセージを入力してみました:

チャンネルを作ってみます:

できました:

新しいメンバーを招待してみます:

受信したメイルの【Join Team】というリンクをクリックすると,アカウント作成画面に飛びます:

新ユーザー”someone”でログインした直後の画面です:

日本語のメッセージも問題ありません:

以上で動作確認は終わりです。細かい機能はまだ Slack には及ばない印象ですが,基本的な機能は一通り揃っていて,実用的に使えるのではないかと感じました。

おまけ: || nc -l -k $PORT について

3.2. 起動スクリプトの作成 の節で,起動スクリプトの38行目に

platform -config config/config.json || nc -l -k $PORT

というコマンドがありました。この || nc -l -k $PORT については後で説明すると書きましたが,これは実はデバッグ用の記述です。

platform コマンドがなんらかの理由で正常に動作しなかった場合, nc -l -k $PORT が実行されます。すると,「最後に実行されたプロセスが生きている」かつ「指定されたポートが listen されている」状態になり,Cloud Foundry 的にはアプリが正常起動したとみなされます。

アプリが正常起動しなかったと判断された場合,Cloud Foundry のシステムはそのアプリを起動したコンテナーを廃棄してしまいます。これは理に適った振る舞いですが,アプリが起動しない原因を調べたい時は非常に困ります。そこで,必要なプロセスが正常に起動しなかった場合に,その代わりを netcat がしてくれるよう, || nc -l -k $PORT という記述を追加しているわけです。

「Mattermost を Cloud Foundry で動かす」というタイトルとは関係ない話ですが,こういう方法もあるということで,本稿で紹介することにしました。

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