「Cloud Foundry 百日行」第65日目は,PHP ベースのカレンダー・システム UNICALE です。公式サイトには「少人数のスケジュール,工数管理に適した機能を搭載しています」とありますが,実際かなりシンプルで,使い始めるのは非常に簡単でした。とりあえず複数人でスケジュールを共有したい,という目的で使ってみるのには良いのではないでしょうか。
基本情報
-
公式サイト
http://www.unicale.com/ -
ソースコード
公開された source code 管理システムは見つからなかったので,以下からダウンロードしました
http://www.unicale.com/download/
本記事では最新版の 2.0.3 を使いました -
その他参考情報
本アプリはデータをファイルに保存するので,通常のデプロイだと Cloud Foundry では再起動等で消えてしまうのですが,今回は 百日行の WordPress の記事 に倣い, FUSE / sshfs を使ってファイルのリモートへの保存に挑戦してみました
以下はそれに関連して参考にしたページです- Getting Started with WordPress on Cloud Foundry | Pivotal P.O.V.
Cloud Foundry に WordPress をデプロイする記事
リモート・ファイル・ストレージの利用を前提に書かれています - Did I miss some sort of annoucement about FUSE support?
Cloud Foundry の ML における FUSE サポートに関する議論 - cf-ex-wordpress/.extensions/wordpress/extension.py
WordPress で sshfs のマウントを行う WordPress 拡張のコード
- Getting Started with WordPress on Cloud Foundry | Pivotal P.O.V.
手順の概要は以下の通りです。
- 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
マウントしたディレクトリーのアクセス権を変更する処理です- UNICALE の readme.txt にある,アクセス権の変更の記述 に従うものです
(もしかしたら不要な処理かもしれません)
- UNICALE の readme.txt にある,アクセス権の変更の記述 に従うものです
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 のドキュメント に記されいるので,詳しくはそちらをご覧ください。またこれまでの百日行でも Shaarli や Tiny 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
でアプリを再起動し,再びブラウザーでアクセスしても,画面は上記のままで,データの永続化もうまくいっていることが確認できました。
以上で動作確認は終わりです。とにかく気軽にスケジュール共有を始めたいという用途には向いていると思いました。
今回使用したソフトウェア
- cf-release (v211)
https://github.com/cloudfoundry/cf-release/tree/v211
( https://github.com/cloudfoundry/cf-release/tree/2121dc6405e0f036efa4dba963f7f49b07e76ffa ) - bosh-lite
https://github.com/cloudfoundry/bosh-lite/tree/552dc6869600c5350eb7ffb4fb9c9c5e79e3889d - CF CLI (v6.12.0-8c65bbd-2015-06-30T00:10:31+00:00)
https://github.com/cloudfoundry/cli/releases/tag/v6.12.0 - UNICALE
http://www.unicale.com/downloads/unicale_203 (v2.0.3)- 本記事で使用した修正版(一部セキュリティ上の理由でダミー化)
https://github.com/nota-ja/unicale/tree/cf-100-day-challenge-065 (commit hash: 96f669c72ccae79080e19cbe0c47b19a3d7ec85d)
- 本記事で使用した修正版(一部セキュリティ上の理由でダミー化)