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

2015-09-30

Let's Chatを Cloud Foundry で動かす

「Cloud Foundry 百日行」第69日目は、nodejs製のチャットツールLet’s Chat です。
Let’s ChatはAPI経由の連携が可能な為、CI等の開発ツールとの連携を行う事で
ChatOpsに利用可能なアプリケーションとなっています。

本アプリはMongoDBを利用する為、第68日目 spring-boot-cf-service-broker-mongoの記事が公開された事で記事化が可能になりました。

基本情報

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

  • 1) ソースコードの取得
  • 2) 事前準備
  • 3) アプリの起動
  • 4) 動作確認

1. ソースコードの取得

$ git clone https://github.com/sdelements/lets-chat
$ cd lets-chat
$ ls
app         CHANGELOG.md     extras        media         Procfile             _sources            Vagrantfile
app.js      CONTRIBUTING.md  Gruntfile.js  migroose.js   README.md            templates
app.json    defaults.yml     LICENSE       migrootions   screenshot.png       TROUBLESHOOTING.md
bower.json  docker           locales       package.json  settings.yml.sample  uploads

2. 事前準備

2.1. MongoDBのサービスインスタンス作成

$ cf create-service "Mongo DB" "Default Mongo Plan" lets-mongo

2.2. アプリの事前push

アプリを–no-startでpushし、作成したサービスインスタンスをアプリに紐づけます。

$ cf push lets --no-start
$ cf bind-service lets lets-mongo

紐づけたサービスの情報を『cf env』を実行し、credentialsのuri部分をメモします。

$ cf env lets
:
System-Provided:
{
 "VCAP_SERVICES": {
  "Mongo DB": [
   {
    "credentials": {
     "uri": "mongodb://68608217-650b-4b26-91df-e70ad5105a34:password@192.168.15.82:27017/f9d8d716-8426-42a6-bd62-585aecfe43d1"
    },
    "label": "Mongo DB",
    "name": "lets-mongo",
    "plan": "Default Mongo Plan",
    "tags": [
     "mongodb",
     "document"
    ]
   }

cf set-envでMONGOLAB_URIという値に
先ほどメモしたcredentialsの値を設定します。

$ cf set-env lets MONGOLAB_URI "mongodb://68608217-650b-4b26-91df-e70ad5105a34:password@192.168.15.82:27017/f9d8d716-8426-42a6-bd62-585aecfe43d1"

3. アプリの起動

$ cf start lets
:
App lets was started using this command `npm start`

Showing health and status for app lets in org morika-t / space morika-t as morika-t...
OK

requested state: started
instances: 1/1
usage: 256M x 1 instances
urls: lets.10.244.0.34.xip.io
last uploaded: Mon Aug 31 08:07:43 UTC 2015
stack: cflinuxfs2
buildpack: Node.js

     state     since                    cpu    memory           disk      details
#0   running   2015-08-31 05:12:36 PM   0.0%   203.5M of 256M   0 of 1G

成功しました。

4. 動作確認

ブラウザからアプリにアクセスし
“Sign in”の下にある”I need an account”をクリックします。

登録情報を入力し、”Register”をクリックします

先ほど登録した情報を入力し”Sign in”をクリックするとログインします。

登録直後はルームが1つも存在していない為、右上の+をクリックします。

ルーム名等を入力し、”Save changes”をクリックします

登録したルームに入室可能になります。

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



投稿者:NTTソフトウェア株式会社 森川 健

2015-09-29

spring-boot-cf-service-broker-mongoを Cloud Foundry で動かす

「Cloud Foundry 百日行」第68回目は,spring-boot-cf-service-broker-mongo です。
このアプリは、Cloud Foundry (以下CF) の service broker として機能するアプリで、百日行第2回目のpostgresql-cf-service-brokerと同じ位置づけのアプリケーションとなり、デプロイする事でmongodbが簡単にサービスとして利用可能になります。

基本情報

全体の概要は以下のようなイメージとなります。

全体の流れは概略以下の様になっています:

  1. MongoDB(ver 2.6) インスタンスの準備
  2. spring-boot-cf-service-broker-mongo のデプロイ
  3. Service Broker 化

MongoDB(ver 2.6) インスタンスの準備

今回は検証用ということで,MongoDB(ver 2.6) は Docker を使って用意することにしました。

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

  • 1) Docker のインストール
  • 2) MongoDB Clientのインストール
  • 3) MongoDB Docker Image の起動

Docker のインストール

CFは Ubuntu 上で動作するので,同じ Ubuntu 上で MongoDB Docker image を動かします。まずは Ubuntu に Docker をインストールします。

$ sudo apt-get install -y docker.io

確認:

$ sudo docker version
Client:
 Version:      1.8.2
 API version:  1.20
 Go version:   go1.4.2
 Git commit:   0a8c2e3
 Built:        Thu Sep 10 19:19:00 UTC 2015
 OS/Arch:      linux/amd64

Server:
 Version:      1.8.2
 API version:  1.20
 Go version:   go1.4.2
 Git commit:   0a8c2e3
 Built:        Thu Sep 10 19:19:00 UTC 2015
 OS/Arch:      linux/amd64

MongoDB Clientのインストール

※こちらの手順はMongoDBの動作確認の為の手順となります。spring-boot-cf-service-broker-mongo の動作には必須ではない為、こちらの手順は省略してしまっても問題ありません。
$ sudo apt-get install -y mongodb-clients

確認:

$ mongo --version
MongoDB shell version: 2.4.9

MongoDB Docker Image の起動

Docker Image の取得

$ sudo docker pull mongo:2.6

※MongoDBのバージョンがタグ指定で2.6になっているのはspring-boot-cf-service-broker-mongoが2.6でないと正常に動かない為です。

Docker image の起動

ホストのポート 27017 をコンテナーのポート 27017 に転送に設定し、取得した Docker Image を起動します。

$ sudo docker run -p 27017:27017 -d mongo:2.6

接続確認

$  mongo --port 27017
MongoDB shell version: 2.4.9
connecting to: 127.0.0.1:27017/test

以上でMongoDBインスタンスの準備は完了です。

spring-boot-cf-service-broker-mongo のデプロイ

続いてspring-boot-cf-service-broker-mongoをCF上にデプロイしていきます。

以下手順の概要です:

  • 0) デプロイ準備
    • 0.1) JDK, Gradle のインストール
    • 0.2) ソースコードの取得
    • 0.3) ソースコードの修正
    • 0.4) アプリのビルド
    • 0.5) Org, Space の作成
    • 0.6) Application Security Group の開放
  • 1) デプロイ
    • 1.1) アプリのPush

デプロイ準備

JDK, Gradle のインストール

このアプリは名前からもわかるようにspring-bootのアプリケーションですのでJDKとGradleを導入します。

  • Oracle JDK8

OracleのJDK 8のページから環境に応じたtar.gzを取得します。
※記事執筆時点の最新版はjdk-8u60-linux-x64.tar.gzでした。

$ ls -lha jdk-8u60-linux-x64.tar.gz
-rw-rw-r-- 1 morika-t morika-t 173M Aug  7 08:32 jdk-8u60-linux-x64.tar.gz

tar.gzの展開

$ tar zxfv jdk-8u60-linux-x64.tar.gz

JAVA_HOMEの設定とPATHを通します。

$ export JAVA_HOME=~/work/jdk1.8.0_60
$ export PATH=$JAVA_HOME/bin:$PATH

バージョン確認

$ java -version
java version "1.8.0_60"
Java(TM) SE Runtime Environment (build 1.8.0_60-b27)
Java HotSpot(TM) 64-Bit Server VM (build 25.60-b23, mixed mode)
  • Gradle
$ wget https://services.gradle.org/distributions/gradle-2.5-all.zip
$ unzip gradle-2.5-all.zip

GRADLE_HOMEの設定とPATHを通します。

$ export GRADLE_HOME=~/work/gradle-2.5
$ export PATH=$GRADLE_HOME/bin:$PATH

バージョン確認

$ gradle -version
------------------------------------------------------------
Gradle 2.5
------------------------------------------------------------

Build time:   2015-07-08 07:38:37 UTC
Build number: none
Revision:     093765bccd3ee722ed5310583e5ed140688a8c2b

Groovy:       2.3.10
Ant:          Apache Ant(TM) version 1.9.3 compiled on December 23 2013
JVM:          1.8.0_60 (Oracle Corporation 25.60-b23)
OS:           Linux 3.13.0-55-generic amd64

ソースコードの取得

アプリのソースコードをGitHubからcloneします。

$ git clone https://github.com/spgreenberg/spring-boot-cf-service-broker-mongo
$ cd spring-boot-cf-service-broker-mongo

ソースコードの修正

アプリをビルドする前に、今回の検証環境のcf-release v211でサポートされるservice broker APIバージョン2.5にアプリを対応させる為に、ソースコードの修正を行います。
※執筆時点のspring-boot-cf-service-broker-mongoの最新版コードで対応するバージョンはservice broker APIのバージョン2.4

Service Broker API versionのヘッダーチェック箇所を無効化する修正

参考URL: https://github.com/cloudfoundry-community/spring-boot-cf-service-broker/blob/a30748549291e6ab2b0b793be3514b249708f47c/README.md#api-version-verification

$ vi src/main/java/org/cloudfoundry/community/servicebroker/mongodb/config/CatalogConfig.java
$ git diff src/main/java/org/cloudfoundry/community/servicebroker/mongodb/config/CatalogConfig.java
diff --git a/src/main/java/org/cloudfoundry/community/servicebroker/mongodb/config/CatalogConfig.java b/src/main/java/org/cloudfoundry
index 295b844..9b51e87 100644
--- a/src/main/java/org/cloudfoundry/community/servicebroker/mongodb/config/CatalogConfig.java
+++ b/src/main/java/org/cloudfoundry/community/servicebroker/mongodb/config/CatalogConfig.java
@@ -8,6 +8,7 @@ import java.util.Map;
 import org.cloudfoundry.community.servicebroker.model.Catalog;
 import org.cloudfoundry.community.servicebroker.model.Plan;
 import org.cloudfoundry.community.servicebroker.model.ServiceDefinition;
+import org.cloudfoundry.community.servicebroker.model.BrokerApiVersion;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;

@@ -15,6 +16,11 @@ import org.springframework.context.annotation.Configuration;
 public class CatalogConfig {

        @Bean
+       public BrokerApiVersion brokerApiVersion() {
+               return new BrokerApiVersion();
+       }
+
+       @Bean

Service Catalogのrequireにsyslog_drainを追加する修正

参考URL:
http://cf-dev.70369.x6.nabble.com/cf-dev-Document-for-service-broker-api-version-2-5-tp537p582.html
http://docs.cloudfoundry.org/services/app-log-streaming.html

$ vi src/main/java/org/cloudfoundry/community/servicebroker/mongodb/config/CatalogConfig.java

$ git diff src/main/java/org/cloudfoundry/community/servicebroker/mongodb/config/CatalogConfig.java
@@ -30,7 +36,7 @@ public class CatalogConfig {
                                                                        getPlanMetadata())),
                                        Arrays.asList("mongodb", "document"),
                                        getServiceDefinitionMetadata(),
-                                       null,
+                                       Arrays.asList("syslog_drain"),
                                        null)));
        }

接続先のMongoDBの設定

$ vi src/main/java/org/cloudfoundry/community/servicebroker/mongodb/config/MongoConfig.java
$ git diff src/main/java/org/cloudfoundry/community/servicebroker/mongodb/config/MongoConfig.java
diff --git a/src/main/java/org/cloudfoundry/community/servicebroker/mongodb/config/MongoConfig.java b/src/main/java/org/cloudfoundry/c
index 4f403c0..d12c07a 100644
--- a/src/main/java/org/cloudfoundry/community/servicebroker/mongodb/config/MongoConfig.java
+++ b/src/main/java/org/cloudfoundry/community/servicebroker/mongodb/config/MongoConfig.java
@@ -14,7 +14,7 @@ public class MongoConfig {

        @Bean
        public MongoClient mongoClient() throws UnknownHostException {
-               return new MongoClient();
+               return new MongoClient("192.168.15.91", 27017);
        }

 }

アプリのビルド

$ ./gradlew build -x test

Org, Space の作成

百日行第2回目のpostgresql-cf-service-brokerと同様にservice brokerをデプロイする専用のorgとspaceを作成します。
今回は過去の記事で作成済みの為、そのまま利用しましたが、新しく作成する場合は以下のようにコマンドを実行します。

$ cf create-org admin
$ cf create-space svcs

Application Security Group の開放

本検証環境ではbosh-liteで構築されており、bosh-lite のデフォルトでは、アプリはプライベート・アドレス (10..., 192.168..*) には接続できません。

今回構築したMongoDBは 192.168.. 上に存在するので、Security Groupの設定を行います。

  • security group の作成

先ほど構築した MongoDB インスタンスにアクセスできるルールを持つ application security group を作成します。

まずはルール(JSON形式)を記述したファイルを作成します。

$ vi ~/spring-boot-cf-service-broker-mongo.security-groups.json
$ cat ~/spring-boot-cf-service-broker-mongo.security-groups.json
[
    {
        "protocol": "tcp",
        "destination": "192.168.15.91",
        "ports": "27017"
    }
]
  • security groupの登録
$ cf create-security-group spring-boot-cf-service-broker-mongo-security-groups ~/spring-boot-cf-service-broker-mongo.security-groups.json

確認:

$ cf security-group spring-boot-cf-service-broker-mongo-security-groups
Getting info for security group spring-boot-cf-service-broker-mongo-security-groups as admin
OK

Name    spring-boot-cf-service-broker-mongo-security-groups
Rules
        [
                {
                        "destination": "192.168.15.91",
                        "ports": "27017",
                        "protocol": "tcp"
                }
        ]

No spaces assigned
  • Default Running Security Group に bind

この設定をする事で同一CF内の全てのアプリが実行時にMongoDBのインスタンスに接続可能になります。

$ cf bind-running-security-group spring-boot-cf-service-broker-mongo-security-groups

デプロイ

アプリの push

manifest.ymlが既に準備されているのでcf pushのみでpushします。

$ cf push
App started


OK

App mongo-broker was started using this command `SERVER_PORT=$PORT $PWD/.java-buildpack/open_jdk_jre/bin/java -cp $PWD/.:$PWD/.java-bu
ildpack/spring_auto_reconfiguration/spring_auto_reconfiguration-1.10.0_RELEASE.jar -Djava.io.tmpdir=$TMPDIR -XX:OnOutOfMemoryError=$PW
D/.java-buildpack/open_jdk_jre/bin/killjava.sh -Xmx382293K -Xms382293K -XX:MaxMetaspaceSize=64M -XX:MetaspaceSize=64M -Xss995K org.spr
ingframework.boot.loader.WarLauncher`

Showing health and status for app mongo-broker in org admin / space svcs as admin...
OK

requested state: started
instances: 1/1
usage: 512M x 1 instances
urls: mongo-broker.10.244.0.34.xip.io
last uploaded: Thu Sep 24 09:25:12 UTC 2015
stack: cflinuxfs2
buildpack: java-buildpack=v3.0-https://github.com/cloudfoundry/java-buildpack.git#3bd15e1 java-main open-jdk-jre=1.8.0_60 spring-auto-
reconfiguration=1.10.0_RELEASE

     state     since                    cpu     memory           disk      details
#0   running   2015-09-24 06:25:57 PM   11.9%   329.8M of 512M   0 of 1G

Service Broker 化

service broker としてCF環境に認識させます。

本節の手順の概要は以下の通りです:

  • 1) Service Brokerに登録する為のクレデンシャル情報の確認
  • 2) Service Broker として登録
  • 3) Marketplace に公開する為のラベル情報の確認
  • 4) Marketplace に公開

Service Brokerに登録する為のクレデンシャル情報の確認

  • cf logsの–recentオプションでログを確認します。
1$ cf logs mongo-broker --recent

下記のログ部分の『password:』の後がService Brokerに登録する為のパスワードになります。
user名はuser固定です。

12015-09-25T10:32:39.97+0900 [App/0]      OUT 2015-09-25 01:32:39.972  INFO 29 --- [           main] b.a.s.AuthenticationManagerConfiguration :
22015-09-25T10:32:39.97+0900 [App/0]      OUT Using default security password: cbbed27c-b42a-489b-8d1b-ce3cb9c6e779
32015-09-25T10:32:40.24+0900 [App/0]      OUT 2015-09-25 01:32:40.245  INFO 29 --- [           main] s.b.c.e.t.TomcatEmbeddedServletCon
4tainer : Tomcat started on port(s): 61139/http
52015-09-25T10:32:40.24+0900 [App/0]      OUT 2015-09-25 01:32:40.248  INFO 29 --- [           main] o.c.c.s.mongodb.config.Application
6       : Started Application in 7.613 seconds (JVM running for 8.284)

Service Broker として登録

$ cf create-service-broker mongodb user cbbed27c-b42a-489b-8d1b-ce3cb9c6e779 http://mongo-broker.10.244.0.34.xip.io

Marketplace に公開する為のラベル情報の確認

登録する際に使うラベル名は”Mongo DB”となります。
値を確認するにはCatalogConfig.javaのServiceDefinitionの2番目の引数の値を確認します。

$ vi src/main/java/org/cloudfoundry/community/servicebroker/mongodb/config/CatalogConfig.java
 23         @Bean
 24         public Catalog catalog() {
 25                 return new Catalog( Arrays.asList(
 26                                 new ServiceDefinition(
 27                                         "mongo",
 28                                         "Mongo DB",
 29                                         "A simple mongo implementation",
 30                                         true,
 31                                         false,

Marketplace に公開

本手順を実行する事で全ユーザに対してサービスプランを提供します。

$ cf enable-service-access "Mongo DB"

確認:

$ cf service-access
broker: mongodb
   service    plan                 access   orgs
   Mongo DB   Default Mongo Plan   all

$ cf marketplace
service      plans                     description
Mongo DB     Default Mongo Plan*       A simple mongo implementation

動作確認

サービスの作成

$ cf create-service 'Mongo DB' 'Default Mongo Plan' mongodb-test

アプリのclone

MongoDBを使うsinatraの簡易アプリとして以下のアプリをcloneします

https://github.com/nota-ja/app-sinatra-mongodb.git

$ git clone https://github.com/nota-ja/app-sinatra-mongodb.git
$ cd app-sinatra-mongodb

アプリのpush

$ cf push sinatra-mongo --no-start

サービスのバインド

$ cf bind-service sinatra-mongo mongodb-test

アプリの起動

$ cf start sinatra-mongo

MongoDBへの書き込み&読み出し

『1』というキーに対し『Taro』というデータを格納します。

$ curl -X POST http://sinatra-mongo.10.244.0.34.xip.io/service/mongo/1 -d 'Taro'

『Taro』を確認した表示がでます。

Taro

キー名『1』のデータを読み込みます。

$ curl http://sinatra-mongo.10.244.0.34.xip.io/service/mongo/1

格納した『Taro』が返ってきます。

Taro

アプリを一旦再起動します。

$ cf restart sinatra-mongo

再度、キー名『1』のデータを読み込みます。

$ curl http://sinatra-mongo.10.244.0.34.xip.io/service/mongo/1

再起動後もデータが永続化されているので格納した『Taro』が返ってきます。

Taro

今回使用した環境



投稿者:NTTソフトウェア株式会社 森川 健

2015-09-28

fc2blog を Cloud Foundry で動かす

「Cloud Foundry 百日行」第67日目は、有名ブログエンジン fc2blog です。
実はオープンソース化されているので、Cloud Foundry上での動作にチャレンジしてみましょう。

残念ながら一部の機能をうまく動かすことはできませんでしたが、ブログエンジンとしての機能は果たしてくれるところまでは検証ができたため、ご紹介したいと思います。

基本情報

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

  • 1) ソースコード取得
  • 2) MySQLServiceの作成
  • 3) 事前準備
  • 4) Cloud Foundry上へのデプロイ
  • 5) 動作検証
  • 6) 問題点

1. ソースコード取得

$ git clone https://github.com/fc2blog/blog
~~~
$ cd blog
blog$ ls
app  LICENSE.txt  public  README.md

fc2blogのソースコードはPHPで書かれています。
REAME.mdによると環境としてはPHP5.2.17以上、MySQL5.1以上が必要とのことです。

なお、このアプリのデプロイの際には設定ファイルの記述やソースコードの修正が必要なため、変更済みの物も こちら に公開してあります。

2. MySQLServiceの作成

データベースとしてMySQLが要求されているのでMySQLServiceを作成してBindします。
手順はいつも通り --no-start起動create-servicebind-service です。

blog$ cf push fc2blog --no-start
blog$ cf create-service p-mysql 100mb fc2blog-mysql
blog$ cf bind-service fc2blog fc2blog-mysql

3. 事前準備

さて、この章ではいくつか設定ファイルの記述やソースコードの修正が入ります。
修正済みのソースコードを用いて検証中の方は基本的にこの章の作業内容は不要となっているので、4章のデプロイから続きを行って下さい。

configファイルの編集

まずはconfigファイルの修正です。
ここは主にデータベースへ接続するための情報などを書き込む箇所ですが、今回はアプリに割り振られた環境変数

blog$ cf env fc2blog
Getting env variables for app fc2blog in org ukaji / space default as ukaji...
OK
 
System-Provided:
{
 "VCAP_SERVICES": {
  "p-mysql": [
   {
    "credentials": {
     "hostname": "10.244.7.6",
     "jdbcUrl": "jdbc:mysql://10.244.7.6:3306/cf_30cd0cb6_f8fe_4153_ac2f_f70e2b5f7aa2?user=bZb39TC9aZkPDGom\u0026password=6LGBHwEup1ob3NJ0",
     "name": "cf_30cd0cb6_f8fe_4153_ac2f_f70e2b5f7aa2",
     "password": "6LGBHwEup1ob3NJ0",
     "port": 3306,
     "uri": "mysql://bZb39TC9aZkPDGom:6LGBHwEup1ob3NJ0@10.244.7.6:3306/cf_30cd0cb6_f8fe_4153_ac2f_f70e2b5f7aa2?reconnect=true",
     "username": "bZb39TC9aZkPDGom"
    },
    "label": "p-mysql",
    "name": "fc2blog-mysql",
    "plan": "100mb",
    "tags": [
     "mysql"
    ]
   }
  ]
 }
}
{
 "VCAP_APPLICATION": {
  "application_name": "fc2blog",
  "application_uris": [
   "fc2blog.10.244.0.34.xip.io"
  ],
  "application_version": "fbd2db74-bb6e-41a4-90d1-73ad3e137901",
  "limits": {
   "disk": 1024,
   "fds": 16384,
   "mem": 256
  },
  "name": "fc2blog",
  "space_id": "03bf316f-df9e-442e-b127-589e673a5652",
  "space_name": "default",
  "uris": [
   "fc2blog.10.244.0.34.xip.io"
  ],
  "users": null,
  "version": "fbd2db74-bb6e-41a4-90d1-73ad3e137901"
 }
}
 
No user-defined env variables have been set
 
No running env variables have been set
 
No staging env variables have been set

からドメイン名やデータベース名などを取得できるような設定を行います。

blog$ cp public/config.php.sample public/config.php
blog$ vi public/config.php
<?php
 
//error_reporting(-1);
error_reporting(0);
 
// 直接呼び出された場合は終了
if (count(get_included_files())==1) {
  exit;
}
 
// 環境変数からMySQL接続情報を取得
$services = json_decode($_ENV['VCAP_SERVICES'], true);
$service = $services['p-mysql'][0];  // pick the first MySQL service
 
// DBの接続情報
define('DB_HOST',     $service['credentials']['hostname'] . ':' . $service['credentials']['port']);          // dbのホスト名
define('DB_USER',     $service['credentials']['username']);     // dbのユーザー名
define('DB_PASSWORD', $service['credentials']['password']);      // dbのパスワード
define('DB_DATABASE', $service['credentials']['name']); // dbのデータベース名
define('DB_CHARSET',  'UTF8MB4');            // MySQL 5.5未満の場合はUTF8を指定してください
 
// サーバーの設定情報
$application = json_decode($_ENV['VCAP_APPLICATION'], true);
$domain = $application['application_uris'][0];
define('DOMAIN',        $domain);           // ドメイン名
define('PASSWORD_SALT', '1234567890qwertyuiop'); // 適当な英数字を入力してください
 
// 設定クラス読み込み
define('WWW_DIR', dirname(__FILE__) . '/');
require(dirname(__FILE__) . '/../app/core/bootstrap.php');
blog$ git diff --no-index -- public/config.php.sample public/config.php
diff --git a/public/config.php.sample b/public/config.php
index 1c533c4..f2b92cc 100644
--- a/public/config.php.sample
+++ b/public/config.php
@@ -8,18 +8,23 @@ if (count(get_included_files())==1) {
   exit;
 }
 
+// 環境変数からMySQL接続情報を取得
+$services = json_decode($_ENV['VCAP_SERVICES'], true);
+$service = $services['p-mysql'][0];  // pick the first MySQL service
+
 // DBの接続情報
-define('DB_HOST',     'localhost');          // dbのホスト名
-define('DB_USER',     'your user name');     // dbのユーザー名
-define('DB_PASSWORD', 'your password');      // dbのパスワード
-define('DB_DATABASE', 'your database name'); // dbのデータベース名
+define('DB_HOST',     $service['credentials']['hostname'] . ':' . $service['credentials']['port']);          // dbのホスト名
+define('DB_USER',     $service['credentials']['username']);     // dbのユーザー名
+define('DB_PASSWORD', $service['credentials']['password']);      // dbのパスワード
+define('DB_DATABASE', $service['credentials']['name']); // dbのデータベース名
 define('DB_CHARSET',  'UTF8MB4');            // MySQL 5.5未満の場合はUTF8を指定してください
 
 // サーバーの設定情報
-define('DOMAIN',        'domain');           // ドメイン名
-define('PASSWORD_SALT', '0123456789abcdef'); // 適当な英数字を入力してください
+$application = json_decode($_ENV['VCAP_APPLICATION'], true);
+$domain = $application['application_uris'][0];
+define('DOMAIN',        $domain);           // ドメイン名
+define('PASSWORD_SALT', '1234567890qwertyuiop'); // 適当な英数字を入力してください
 
 // 設定クラス読み込み
 define('WWW_DIR', dirname(__FILE__) . '/');
 require(dirname(__FILE__) . '/../app/core/bootstrap.php');
-

PASSWORD_SALT は元の文字列とは別の適当な英数字を設定して下さい。

モジュールの追加

必要なモジュールをphp-buildpackの機能 PHP_EXTENSIONS を用いて導入します。

blog$ mkdir .bp-config
blog$ vi .bp-config/options.json
{
        "PHP_EXTENSIONS": ["mysqli", "pdo", "pdo_mysql", "gettext", "mbstring", "gd"]
}

apache設定ファイルの記述

PaaS上でアプリケーションを動作させる際、アプリケーション開発者の想定しているアプリの形とPaaSプロバイダ側が想定しているアプリの形に、どうしてもズレが生じてしまう場合があります。
今回のアプリは

SERVERのアプリ展開場所 (/var/www/html/ 等)
|-- app/
`-- public/ (←アプリのDocument Root)
    `-- admin/
        `-- install.php

のような構成が製作者サイドでは想定されているようですが、これをCloud Foundry上に何も考えずにデプロイすると、

/home/vcap/app/
`-- htdocs/ (←アプリのDocument Root)
    |-- app/
    `-- public/
        `-- admin/
            `-- install.php

このような展開のされ方をします。
アプリのDocument Rootが異なるとファイルを呼び出すパスが正しく動作しないため、これをApacheの設定ファイルで修正してあげましょう。

blog$ mkdir .bp-config/httpd
blog$ vi .bp-config/httpd/httpd.conf
ServerRoot "${HOME}/httpd"
Listen ${PORT}
ServerAdmin "${HTTPD_SERVER_ADMIN}"
ServerName "0.0.0.0"
DocumentRoot "/home/vcap/app/htdocs/public"
Include conf/extra/httpd-modules.conf
Include conf/extra/httpd-directories.conf
Include conf/extra/httpd-mime.conf
Include conf/extra/httpd-logging.conf
Include conf/extra/httpd-mpm.conf
Include conf/extra/httpd-default.conf
Include conf/extra/httpd-remoteip.conf
Include conf/extra/httpd-php.conf

参考にしたのは こちらのページ です。
ここでは Document Root の項目だけを書き換えています。

composer関連の設定

今回のアプリの中身を見てみると、composer(=PHPのライブラリ依存管理ツール)が使われているようです。

blog$ find -name composer.json
./public/js/jquery/jQuery-Timepicker-Addon/composer.json

Cloud FoundryのPHP Buildpackではcomposerがサポートされているのですが、デフォルトの設定ではアプリケーションのrootにある composer.json ファイルしか読み取ってくれないため、上記の位置にあるような composer.json ファイルを読むような設定を与えてみます。

方法としては、 PHP Buildpackのcomposerサポート機能 の1つ、 COMPOSER_INSTALL_OPTIONS を用いて、 composer install の作業ディレクトリを指定するオプションである --working-dir DIRNAME を使えば良さそうです。

※参考

$ composer --help
Usage:
 help [--xml] [--format="..."] [--raw] [command_name]
 
Arguments:
 command               The command to execute
 command_name          The command name (default: "help")
 
Options:
 --xml                 To output help as XML
 --format              To output help in other formats (default: "txt")
 --raw                 To output raw command help
 --help (-h)           Display this help message
 --quiet (-q)          Do not output any message
 --verbose (-v|vv|vvv) Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
 --version (-V)        Display this application version
 --ansi                Force ANSI output
 --no-ansi             Disable ANSI output
 --no-interaction (-n) Do not ask any interactive question
 --profile             Display timing and memory usage information
 --working-dir (-d)    If specified, use the given directory as working directory.
 
Help:
 The help command displays help for a given command:
  
   php /usr/local/bin/composer help list
  
 You can also output the help in other formats by using the --format option:
  
   php /usr/local/bin/composer help --format=xml list
  
 To display the list of available commands, please use the list command.

設定はお馴染みの .bp-config/options.json 内で完結します。

blog$ vi .bp-config/options.json 
{
        "WEBDIR": "htdocs",
        "LIBDIR": "htdocs/public/js/jquery/jQuery-Timepicker-Addon/",
        "PHP_EXTENSIONS": ["mysqli", "pdo", "pdo_mysql", "gettext", "mbstring", "gd"],
        "COMPOSER_INSTALL_OPTIONS": ["--no-interaction", "--no-dev", "--no-progress", "--working-dir ./public/js/jquery/jQuery-Timepicker-Addon/"]
}

ここで、composerを使う際は WEBDIRLIBDIR を指定するのが一般的のようなので(参考)、併せて記載してあります。

.htaccessファイルの設定

デフォルトの状態でアプリをデプロイをすると、 .htaccess ファイルに書かれた php_value がInvalid commandであるというエラーが表示されてしまいます。

※参考

2015-03-30T17:30:25.38+0900 [RTR]     OUT fc2blog.cf.nttlabs.info - [30/03/2015:08:30:25 +0000] "GET /public/admin/install.php HTTP/1.0" 500 528 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:36.0) Gecko/20100101 Firefox/36.0" 10.0.0.2:60164 vcap_request_id:f0728de1-3393-4eea-7193-5b55ef8adbb7 response_time:0.002464553 app_id:e7dcfe39-8c3d-4dbc-b7ae-ddd36d8ff8a7
2015-03-30T17:30:25.39+0900 [App/0]   OUT 08:30:25 httpd   | [Mon Mar 30 08:30:25.382099 2015] [core:alert] [pid 55:tid 140077086775040] [client 153.142.2.99:24081] /home/vcap/app/htdocs/public/.htaccess: Invalid command 'php_value', perhaps misspelled or defined by a module not included in the server configuration
2015-03-30T17:30:25.39+0900 [App/0]   OUT 08:30:25 httpd   | 153.142.2.99 - - [30/Mar/2015:08:30:25 +0000] "GET /public/admin/install.php HTTP/1.1" 500 528 vcap_request_id=f0728de1-3393-4eea-7193-5b55ef8adbb7 peer_addr=10.0.0.46

これはどうやら
mod_php5.c のモジュールがあれば動かすことができるようですが、 PHP Buildpackでサポートされているモジュール一覧 に無かったため、今回はその場凌ぎの解決策ではありますが条件文を咬ませることでこのエラーを回避しました。
修正箇所は2箇所です。

blog$ cp public/.htaccess public/.htaccess.original
blog$ vi public/.htaccess
<IfModule mod_php5.c>
        php_value display_errors on
</IfModule>
 
RewriteEngine On
RewriteBase /
 
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule . - [L]
 
RewriteRule . index.php [L]
blog$ diff public/.htaccess.original public/.htaccess
1c1,3
< php_value display_errors on
---
> <IfModule mod_php5.c>
>         php_value display_errors on
> </IfModule>
blog$ cp public/admin/.htaccess public/admin/.htaccess.original
blog$ vi public/admin/.htaccess
<IfModule mod_php5.c>
        php_value display_errors on
</IfModule>
 
RewriteEngine On
RewriteBase /admin/
 
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule . - [L]
 
RewriteRule . index.php [L]
blog$ diff public/admin/.htaccess.original public/admin/.htaccess
1c1,3
< php_value display_errors on
---
> <IfModule mod_php5.c>
>         php_value display_errors on
> </IfModule>

ちなみに他には、 php.ini というファイルを作りそこに別途設定を書き込む等の回避策もあるようです。

ファイルの書き込み権限の設定

ファイルに書き込み権限を与えておきます。

blog$ chmod 777 public/uploads/
blog$ chmod 777 app/temp/

manifestファイルの設定

manifestファイルも作っておきましょう。
アプリ名 name はMySQLのServiceにバインドした時と同じ名前を用いて下さい。

blog$ vi manifest.yml
---
applications:
- name: fc2blog
  buildpack: php_buildpack

4. Cloud Foundry上へのデプロイ

ようやくデプロイです。

blog$ cf push
(一部略)
-----> Uploading droplet (34M)

1 of 1 instances running

App started


OK

App fc2blog was started using this command `$HOME/.bp/bin/start`

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

requested state: started
instances: 1/1
usage: 256M x 1 instances
urls: fc2blog.10.244.0.34.xip.io
last uploaded: Mon Sep 28 05:31:20 UTC 2015
stack: cflinuxfs2
buildpack: php_buildpack

     state     since                    cpu    memory          disk      details   
#0   running   2015-09-28 02:32:44 PM   0.0%   49.1M of 256M   0 of 1G 

OKです。

5. 動作検証

README.mdの記述に従い、払いだされたURL(今回の例ではfc2blog.10.244.0.34.xip.io)の /admin/install.php にアクセスします。

項目チェッカーが全て緑表示ならOKです。
Install ボタンを押下します。

次は管理者情報の登録画面です。

こちらは適当な値を設定します。後のログイン時に用いるので値は覚えておきましょう。
Register ボタンを押下します。

インストールが完了するので左側のリンクからログイン画面に遷移し、

先程設定した値でログインします。

こちらが管理者画面です。

試しに新しい記事を作ってみましょう。
新規記事作成は左の Home>New article から。

投稿を行うと、

先ほど作った記事が新たに公開されました。

ブログのデザインも色々なテンプレートが用意されています。
もちろんテンプレートの自作するも可能となっています。

と、ひと通りの機能を無事に動かすことができました、で終われれば良かったのですが、実はこのアプリには、最後まで動かすことができなかった機能があります。

右上に明らかに使用言語を切り替えられそうな見た目のボタンがあるので、これを日本語に切り替えてみたのですが、

ボタン以外日本語になってくれません。

6. 問題点

少しソースコードを調べてみると、日本語化対応(多言語化対応)の箇所で、PHPの gettext モジュールを使った部分が正しく動いていないように見えます。
Noburou Taniguchiさん 調べによると、これはCloud Foundry側に問題がある可能性が大きいようです。

試しに gettext モジュールを使った簡単なアプリをCloud Foundry上で動かしてみます。

$ git clone https://github.com/nota-ja/php-gettext-example
$ cd php-gettext-example/
ukaji@vbx091:~/workspace/php-gettext-example$ for d in locale/*/LC_MESSAGES/; do msgfmt $d/messages.po -o $d/messages.mo; done
php-gettext-example$ cf push php-gettext-example
(一部略)
-----> Uploading droplet (17M)

1 of 1 instances running

App started


OK

App php-gettext-example was started using this command `$HOME/.bp/bin/start`

Showing health and status for app php-gettext-example in org ukaji / space default as ukaji...
OK

requested state: started
instances: 1/1
usage: 256M x 1 instances
urls: php-gettext-example.10.244.0.34.xip.io
last uploaded: Mon Sep 28 06:58:32 UTC 2015
stack: cflinuxfs2
buildpack: PHP

     state     since                    cpu    memory          disk      details   
#0   running   2015-09-28 03:58:50 PM   0.0%   22.6M of 256M   0 of 1G 

gettext モジュールの機能が正しく動いていればこのアプリの画面表示は /home/vcap/app/htdocs/locale: Hello, world になるはずなので、今回のアプリを併せて考えるとCloud Foundry上で gettext 機能がうまく動いていない可能性が高いと言えそうです。

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

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 で動かす」というタイトルとは関係ない話ですが,こういう方法もあるということで,本稿で紹介することにしました。

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

2015-09-24

UNICALE を Cloud Foundry で動かす

「Cloud Foundry 百日行」第65日目は,PHP ベースのカレンダー・システム UNICALE です。公式サイトには「少人数のスケジュール,工数管理に適した機能を搭載しています」とありますが,実際かなりシンプルで,使い始めるのは非常に簡単でした。とりあえず複数人でスケジュールを共有したい,という目的で使ってみるのには良いのではないでしょうか。

基本情報

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

  • 1) ソースコードの取得
  • 2) Cloud Foundry 向け変更
  • 3) sshfs 向け変更
  • 4) アプリのデプロイ
  • 5) 動作確認

1. ソースコードの取得

上で述べたように,今回は最新版(検証時点では2.0.3)を公式サイトからダウンロードします。

$ wget http://www.unicale.com/downloads/unicale_203

拡張子が付いていないのでファイル名を変更します。

$ mv unicale_203 unicale_203.zip

展開してディレクトリーを移動します。

$ unzip unicale_203.zip
$ cd unicale_203/

以下は必須ではありませんが,後の修正のために,Gitで管理することにします。

$ git init
$ git add .
# git commit -m 'Original status'

2. Cloud Foundry 向け変更

この項では sshfs 利用以外に関する変更について述べます。

本アプリをこのままの状態で Cloud Foundry にプッシュすると,起動はするのですが,特定の操作(例えば管理者画面の呼び出し)時に以下のようなエラーがでて正常に機能しませんでした。

2015-09-23T13:10:26.63+0900 [App/0]      OUT 04:10:26 httpd   | [Wed Sep 23 04:10:26.629847 2015] [proxy_fcgi:error] [pid 48:tid 140078898681600] [client 192.168.50.1:40553] AH01071: Got error 'PHP message: PHP Fatal error:  Call-time pass-by-reference has been removed in /home/vcap/app/htdocs/u_admin.php on line 20\n', referer: http://unicale.10.244.0.34.xip.io/

PHP Fatal error: Call-time pass-by-reference has been removed で検索した結果,PHP5.4で「call-time pass-by-reference」機能が削除されたために出るエラーということがわかりました。

現在使っている Cloud Foundry 環境の php-buildpack のバージョンは 3.2.1 で,

$ cf buildpacks
Getting buildpacks...

buildpack              position   enabled   locked   filename
staticfile_buildpack   1          true      false    staticfile_buildpack-cached-v1.0.0.zip
java_buildpack         2          true      false    java-buildpack-v3.0.zip
ruby_buildpack         3          true      false    ruby_buildpack-cached-v1.4.2.zip
nodejs_buildpack       4          true      false    nodejs_buildpack-cached-v1.3.1.zip
go_buildpack           5          true      false    go_buildpack-cached-v1.3.1.zip
python_buildpack       6          true      false    python_buildpack-cached-v1.3.2.zip
php_buildpack          7          true      false    php_buildpack-cached-v3.2.1.zip
binary_buildpack       8          true      false    binary_buildpack-cached-v1.0.0.zip

このバージョンの PHP のデフォルトは 5.4 系最新版です (URL) 。

使用する PHP のバージョンとして 5.3 以下を指定するという対応も考えられるのですが,今回は将来のことも考えて 5.4 以降で動くように修正を積みました。

$ git diff
diff --git a/cheetan/db/textsql.php b/cheetan/db/textsql.php
index 643c3ed..121115f 100644
--- a/cheetan/db/textsql.php
+++ b/cheetan/db/textsql.php
@@ -319,7 +319,7 @@ class CTextDB
                        }
                        if( $this->cmpkey && array_key_exists( $this->cmpkey, $records[0] ) )
                        {
-                               usort( $records, array( &$this, '_cmpfunc' ) );
+                               usort( $records, array( $this, '_cmpfunc' ) );
                        }
                        break;
                }
diff --git a/u_admin.php b/u_admin.php
index 80ae647..36b55e7 100644
--- a/u_admin.php
+++ b/u_admin.php
@@ -17,7 +17,7 @@ function action( &$c )
        $c->set('calname', $confdata['calname']);

        $isLogin = false;
-       if($c->auth->islogin(&$c)){
+       if($c->auth->islogin($c)){
                $loginName = $c->sanitize->html($_SESSION['username']);
                $isLogin = true;
        }else{
diff --git a/u_auth.php b/u_auth.php
index c576579..8630abf 100644
--- a/u_auth.php
+++ b/u_auth.php
@@ -34,7 +34,7 @@ function action( &$c )
                        $account = array('username' => $c->data['uni']['username'],
                                                         'password' => $c->data['uni']['password']
                                                        );
-                       $rtn = $c->auth->login($account,&$c);
+                       $rtn = $c->auth->login($account,$c);
                        if($rtn){
                                $msg.="ログイン成功しました。";
                        }else{
@@ -65,7 +65,7 @@ function action( &$c )
                        $c->set( "tabselected", "{ selected: 1 }" );
                }
        }
-       if($c->auth->islogin(&$c)){
+       if($c->auth->islogin($c)){
                $c->redirect( "./u_admin.php" );
        }else{
                $msg .= "ログインしていません。<br>";
diff --git a/u_member.php b/u_member.php
index 343ddee..5b0ef20 100644
--- a/u_member.php
+++ b/u_member.php
@@ -17,7 +17,7 @@ function action( &$c )


        $isLogin = false;
-       if($c->auth->islogin(&$c)){
+       if($c->auth->islogin($c)){
                $loginName = $c->sanitize->html($_SESSION['username']);
                $isLogin = true;
        }else{
diff --git a/u_member_edit.php b/u_member_edit.php
index 1a33fde..9316a17 100644
--- a/u_member_edit.php
+++ b/u_member_edit.php
@@ -16,7 +16,7 @@ function action( &$c )
        $c->set('calname', $confdata['calname']);

        $isLogin = false;
-       if($c->auth->islogin(&$c)){
+       if($c->auth->islogin($c)){
                $loginName = $c->sanitize->html($_SESSION['username']);
                $isLogin = true;
        }else{

以上で sshfs に関係しない変更は終わりです。

3. sshfs 向け変更

この項は sshfs を使ってアプリのデータ・ファイルをリモートの(永続的な)ファイルシステムに保存するための変更です。ただ試しに使ってみたいという方は,前項までの変更を行ってアプリをプッシュすれば動きますので,本項は読み飛ばしていただいてOKです。

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

  • 1) 永続化対象ファイルの決定
  • 2) マウント対象の準備
  • 3) SSH 鍵ペア及び known_hosts の準備
  • 4) sshfs マウントを実行するスクリプトの作成

3.1. 永続化対象ファイルの決定

まず,どのファイルをリモートのファイルシステムに永続的に保存する必要があるかを調べます。今回は,アプリの readme.txt の「2.3.2 UNICALE2系統でのバージョンアップインストール」内にある

1)【重要】サーバ上の既存のUNICALEインストールフォルダの中のdataフォルダをローカルにコピーするなどしてバックアップ。

という記述から, data/ ディレクトリー内のファイルを永続化すれば良いと判断しました。

3.2. マウント対象の準備

アプリが動作するコンテナーからマウントされるディレクトリーを準備します。セキュリティ上の観点から,本来はユーザーを新たに作って,そのユーザーの home directory 等を使うのが正しいのですが,今回は省力化のためにこのアプリをプッシュしているユーザーを利用し,ディレクトリーのみ別途作成しました。

$ mkdir ~/sshfs
$ pushd ~/sshfs
$ mkdir -p $RANDOM/$RANDOM
$ ls -alF 31226/7675/
total 8
drwxr----- 2 nota-ja nota-ja 4096 Sep 23 20:44 ./
drwxr----- 3 nota-ja nota-ja 4096 Sep 23 20:44 ../
$ popd

3.3. SSH 鍵ペア及び known_hosts の準備

WordPress の記事 を参考に,shfs の認証時に必要となる SSH 鍵ペアと known_hosts を作成します。

アプリのトップ・ディレクトリー直下に .ssh というディレクトリーを作り,そこに SSH 鍵ペアを作成&保存します。

$ mkdir .ssh
$ chmod 700 .ssh
$ ssh-keygen -q -N "" -f .ssh/id_rsa
$ ls -alF .ssh/
total 16
drwx------  2 nota-ja nota-ja 4096 Sep 23 21:55 ./
drwxr----- 12 nota-ja nota-ja 4096 Sep 23 21:54 ../
-rw-------  1 nota-ja nota-ja 1679 Sep 23 21:55 id_rsa
-rw-r-----  1 nota-ja nota-ja  396 Sep 23 21:55 id_rsa.pub

鍵ペアを作る際,パスフレーズは指定しないでください (上の例では, -N "" としてパスフレーズに空文字列を与えることでそれを実現しています)。パスフレーズを指定すると,sshfs の接続時にパスフレーズの入力を interactive に要求されて,そこで処理が止まってしまって,処理が正常に行われません。

#なお,後述の「今回使用したソフトウェア」で紹介している修正版では,
#SSH 鍵ペアの秘密鍵にはダミーの値が入れてあります。
#そのままでは使用できませんので,必ずご自分で鍵ペアを作成してください。

known_hosts の作成も同じ理由によるものです。ssh でこれまで接続したことのないホストに接続すると,

The authenticity of host '192.168.15.91 (192.168.15.91)' can't be established.
ECDSA key fingerprint is 58:e5:73:8b:b1:2e:80:fe:3a:26:dc:d9:63:47:1e:ad.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '192.168.15.91' (ECDSA) to the list of known hosts.

のようなメッセージを目にすることがあると思いますが,これも interactive な操作を必要とするので,自動でマウントしたい今回のようなケースでは避けたい状況になります。そこで予め接続先のホストを known_hosts に登録しておくために行うのが以下の処理です。

$ ssh-keyscan -t rsa 192.168.15.91 > .ssh/known_hosts
# 192.168.15.91 SSH-2.0-OpenSSH_6.6.1p1 Ubuntu-2ubuntu2

以下に類した内容が .ssh/known_hosts ファイルに書き込まれていればOKです。

$ cat .ssh/known_hosts
192.168.15.91 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC2tnBENPpWgE1zpjphWiDWNRZzR8jYAT9wTJru4E4lUNlssO86xCo7kPJbH2pXn+nFji5MvMLSX6Mv1JHD0q4HLgVSZ8yQUuHkO6DxDmOMyl8C00/xWbKagAm86ECo9Hy38tW5s3WiIFv9Zh/mW9Uxn1lSVDLWVwrMUBCesGnS03ZFA6zJGuo2oqX2Ekzy+mPZCUBMeOW73piCcvpurgVQqqPtUeBafMCOcom8uH7WRw0JtlvPsAXiME3Kxsq3JDWyIqESwESCOROXv9CEB5eJXramNIAzN8xUKJDt8uTp0X1rGRCAAAFJbskZQNAN7zGoXADbJRtoR3zJ2DurijAp

最後に,アクセス先のホスト上のユーザーの .ssh/authorized_keys に,先ほど作成した SSH 鍵ペアの公開鍵を登録します。~/.ssh/authorized_keys を壊すとそのホストにアクセスできなくなる可能性が高いので,下記操作の前にバックアップを取っておくことをお勧めします。

$ cat .ssh/id_rsa.pub >> ~/.ssh/authorized_keys

以上で,sshfs 認証の準備が整いました。

3.4. sshfsマウントを実行するスクリプトの作成

最後に,sshfsマウントを実行するスクリプトを作成します。試行錯誤の結果できたのが以下のスクリプトです。

$ cat .profile.d/sshfs.sh
#!/bin/bash

set -x

## Move .ssh to avoid exposure
if [ -d $HOME/htdocs/.ssh ]; then
    mv $HOME/htdocs/.ssh $HOME/
    ## And fix access rights
    chmod 700 $HOME/.ssh
    chmod 600 $HOME/.ssh/*
fi

## Evacuate deployed data files
datadir=$HOME/htdocs/data
savedir=$HOME/tmp/data
mkdir -p $savedir
mv $datadir/* $savedir/

## Unmount $datadir beforehand
fusermount -u $datadir

## Mount remote directory via sshfs
sshfs ${SSH_HOST}:${SSH_PATH} $datadir \
    -C \
    -o IdentityFile=$HOME/.ssh/${SSH_KEY_NAME} \
    -o StrictHostKeyChecking=yes \
    -o UserKnownHostsFile=$HOME/.ssh/known_hosts \
    -o idmap=user \
    -o cache=yes \
    -o kernel_cache \
    -o compression=no \
    -o large_read \
    -o Ciphers=arcfour

## Change access rights
chmod -R a+rwX $datadir

## Write back datafile(s) if they does not exist in mounted directory
for distfile in $savedir/*; do
    fname=$(basename $distfile)
    if [ ! -e $datadir/$fname ]; then
    cp $distfile $datadir/
    fi
done

コメントを見ていただくとわかるように,基本的な構造としては大きく6つの部分から成ります。

  • Move .ssh to avoid exposure
    staging 時に<アプリのトップ・ディレクトリー>直下から <アプリのトップ・ディレクトリー>/htdocs/ 直下に移動された .ssh ディレクトリーを,<アプリのトップ・ディレクトリー>直下に戻す処理です
    • sshfs コマンドでは IdentityFile パラメーターで秘密鍵ファイルを指定できるので,移動する必要はないのですが,htdocs ディレクトリーはWeb公開ディレクトリーであるため設定をきちんとしておかないと外部に公開されてしまう可能性があるので,その危険性を減らすのが主な目的です
    • この際同時に(cf push した際に)変更されてしまったアクセス権を修正しています
      これは ssh 秘密鍵を使う際には必須の修正です
  • Evacuate deployed data files
    cf push でアップロードされた <アプリのトップ・ディレクトリー>/htdocs/data 直下のファイルを退避する処理です
    • マウント対象のディレクトリーを空にするのが目的ですが,初回時等 sshfs マウント先にデータ・ファイルがない場合はこれを書き戻す必要があるので,退避しておきます
  • Unmount $datadir beforehand
    • これは bad knowhow の類の処理です
    • 原因は解明できていないのですが,これがないとなぜか ERR fusermount: failed to access mountpoint /home/vcap/app/htdocs/data: Permission denied のエラーが出るので,事前にアンマウントを行っています
  • Mount remote directory via sshfs
    このスクリプトのメインの処理です
    sshfs コマンドでリモートのディレクトリーをマウントします
    • コマンドライン引数の設定は,以下などを参考にしました
      • https://github.com/dmikusa-pivotal/cf-ex-wordpress/blob/0b8d4347a657e441fbb039ad5c6f6965ab28caf4/.extensions/wordpress/extension.py#L105-L111
      • https://github.com/dmikusa-pivotal/cf-ex-wordpress/blob/0b8d4347a657e441fbb039ad5c6f6965ab28caf4/.extensions/wordpress/extension.py#L39
      • http://blog.cloudfoundry.gr.jp/2015/09/cf100apps-062-WordPress.html#cloud-foundry
        ( SSH_OPTS: '["cache=yes", "kernel_cache", "compression=no", "large_read", "Ciphers=arcfour"]' の箇所)
  • Change access rights
    マウントしたディレクトリーのアクセス権を変更する処理です
  • Write back datafile(s) if they does not exist in mounted directory
    退避させていたデータ・ファイルを書き戻す処理です
    • ただしマウント後のディレクトリーに同名のファイルがある場合は書き戻さないようにしています

なお,このスクリプトの実行は, ADDITIONAL_PREPROCESS_CMDS を使ってアプリ起動前に行います。そのために以下の内容のファイルを .bp-config/options.json として作成します。

$ cat .bp-config/options.json
{
    "ADDITIONAL_PREPROCESS_CMDS": "$HOME/.profile.d/sshfs.sh"
}

ADDITIONAL_PREPROCESS_CMDS については, php-buildpack のドキュメント に記されいるので,詳しくはそちらをご覧ください。またこれまでの百日行でも ShaarliTiny Tiny RSS の記事で触れているので,よければそちらもご覧ください。

最後に,このスクリプトは幾つかの環境変数を消費するので,その設定を楽に行うために manifest.yml を作成しておきます。

$ cat manifest.yml
---
applications:
  - name: unicale
    memory: 256M
    env:
      SSH_HOST: nota-ja@192.168.15.91
      SSH_PATH: /home/nota-ja/sshfs/31226/7675
      SSH_KEY_NAME: id_rsa

アプリ名や環境変数の値は,ご自分の環境に合わせて適宜修正してください。

4. アプリのデプロイ

準備ができたので,アプリをデプロイします。manifest.yml があるので,単に cf push するだけでOKです。

$ cf push
requested state: started
instances: 1/1
usage: 256M x 1 instances
urls: unicale.10.244.0.34.xip.io
last uploaded: Wed Sep 23 16:03:43 UTC 2015
stack: cflinuxfs2
buildpack: PHP

     state     since                    cpu    memory          disk      details
#0   running   2015-09-24 01:04:13 AM   1.5%   29.1M of 256M   0 of 1G

起動しました。

5. 動作確認

デプロイ直後の画面は↓です:

readme.txt の「3.4 パスワード保護について」 にある通り,通常画面にはアクセス制御も何もないので,必要に応じて .htacess などで保護した方が良いと思われます。

また,予め戦国武将らしき5人のメンバーが登録されていますが,これは data/d_member.txt の初期値として設定されているもので,同ファイルの編集,メンバー設定画面により変更(削除含む)が可能です。今回は特に必要もないのでこのまま削除せずに行きます。

新規予定を入力してみます:

【追加】をクリックすると,予定がカレンダー上に追加されます:

画面をスクロールして,右下の【設定】をクリックすると,管理者認証画面に遷移します。初期管理者の情報は readme.txt の「3.5 管理者画面について」 にあるので,その通り入力します:

認証に成功すると,設定画面に遷移します:

「カレンダーの名前」を “CF100” に,「表示開始曜日」を “月” に変更して,【更新】をクリックします:

右上の【戻る】をクリックしてカレンダーに戻ると,変更が反映されているのがわかります:

この状態で cf restart unicale でアプリを再起動し,再びブラウザーでアクセスしても,画面は上記のままで,データの永続化もうまくいっていることが確認できました。

以上で動作確認は終わりです。とにかく気軽にスケジュール共有を始めたいという用途には向いていると思いました。

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