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

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 でアプリを再起動し,再びブラウザーでアクセスしても,画面は上記のままで,データの永続化もうまくいっていることが確認できました。

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

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