Vagrant で Symfony2 の環境を構築する

Symfony2 を扱う必要が出てきたので構築のメモ

Vagrant の設定

下記の Vagrantfile を利用した
GitHub - simshaun/symfony-vagrant: Deprecated. Check out Puphpet.
こいつを、適当なフォルダ作ってぶちこむ。

$ cd /path/to/work/folder
$ git clone --recursive git://github.com/simshaun/symfony-vagrant.git my_symfony_folder
$ cd my_symfony_folder

Vagrantfile を覗くと、デフォルトだと上記で作ったフォルダが symfony のプロジェクトルートになっているっぽい。それだとREADMEとかが上書きされちゃうし、プロジェクトファイルが散らかっちゃって管理しにくいので、別のフォルダをつくってそこに入れた。Vagrantfile の web_apps をいじればいいっぽい。バーチャルホストも切れるっぽい。
# vagrant/Vagrantfile

web_apps = {
-  "project1" => {
-    "host_project_folder"  => "../",
-    "guest_project_folder" => "/home/vagrant/web-app",
-    "guest_docroot"        => "/home/vagrant/web-app/web",
-    "server_name"          => "localhost",
-    "server_aliases"       => ["*.localhost"],
-    "php_timezone"         => "America/New_York"
-  }
+  "site1" => {
+    "host_project_folder"  => "../app1/",
+    "guest_project_folder" => "/home/vagrant/site1",
+    "guest_docroot"        => "/home/vagrant/site1/web",
+    "server_name"          => "site1",
+    "server_aliases"       => ["*.site1"],
+    "php_timezone"         => "Asia/Tokyo"
+  },
+  "site2" => {
+    "host_project_folder"  => "../app2/",
+    "guest_project_folder" => "/home/vagrant/site2",
+    "guest_docroot"        => "/home/vagrant/site2/web",
+    "server_name"          => "site2",
+    "server_aliases"       => ["*.site2"],
+    "php_timezone"         => "Asia/Tokyo"
+  }
}

上記のように変更したら、共有先のフォルダを作る。Composer で Symfony2 を落としてくる。

Composer のインストール

色々インストール方法はあるけど、今回はComposerを使うようにした。

$ curl -sS https://getcomposer.org/installer | php -d detect_unicode=Off # エラー回避するために detect_unicode=Off に
$ sudo chmod 755 composer.phar
$ sudo mv composer.phar /usr/local/bin/composer

Symfony2 インストール

上記で app1、app2 とフォルダを設定したので、それぞれについて composer を実行する

$ composer create-project symfony/framework-standard-edition app1 2.4.2
$ composer create-project symfony/framework-standard-edition app2 2.4.2

最後の2.4.2はSymfonyのバージョン。今は2.4.2が最新らしい。インストール中に色々聞かれるけど、基本的に全部デフォルトでいいかも。DB の設定とかはあとで直せるし。

仕上げ

もしバーチャルホストを切っていたら、ホスト側にその名前を追加しておく。上記の設定なら

33.33.33.10 site1
33.33.33.10 site2

そしてプロビジョニング。

$ cd vagrant
$ vagrant up

どうやら上記のVagrantfileは共有フォルダだけしか反映されないようなので、apache の conf ファイルを書き足す。下記は一例
# vagrant ssh
# sudo rm /etc/apache2/site-enabled/localhost.conf
# sudo vi /etc/apache2/site-enabled/app1.conf

<VirtualHost *:80>
  ServerName site1
  ServerAlias *.site1
  DocumentRoot /home/vagrant/site1/web

  LogLevel debug
  ErrorLog /home/vagrant/site1/app/logs/error.log
  CustomLog /home/vagrant/site1/app/logs/access.log combined

  RewriteEngine On
  RewriteLog /home/vagrant/site1/app/logs/rewrite.log
  RewriteLogLevel 0

  php_value date.timezone Asia/Tokyo

  <Directory /home/vagrant/site1/web>
    php_admin_value upload_tmp_dir /tmp/site1
  </Directory>
</VirtualHost>

設定ファイルの配置ができたら apache 再起動

$ sudo service apache2 restart

で、http://site1/ にアクセスするとサイトが見れるはず。ssh しなくてもホスト側の app1 や app2 以下のファイルをいじれば反映される。
この際、Symfony の新規インストールの場合は config.php にアクセスすることで設定をするが、ホストからアクセスする場合はIP制限がかかっていて設定できないため、下記のファイルにホスト側のIPを追記しておく
# app1/web/config.php

if (!in_array(@$_SERVER['REMOTE_ADDR'], array(
     '33.33.33.1',  //この辺を追加
     '127.0.0.1',
     '::1',
 ))) {

# app1/web/app_dev.php

if (isset($_SERVER['HTTP_CLIENT_IP'])
     || isset($_SERVER['HTTP_X_FORWARDED_FOR'])
     || !in_array(@$_SERVER['REMOTE_ADDR'], array('33.33.33.1', '127.0.0.1', 'fe80::1', '::1')) // IP を追加
 ) {

そして、http://site1/config.php とかにホスト側のブラウザでアクセスし、設定していく。基本的には composer から入れていればこのへんの設定はできているはず。
あとはこの状態で git リポジトリに登録しておく

$ cd app1
$ git init
$ git add .
$ git rm --cached -r src/Acme* # Demoアプリは含めない
$ git commit -m 'initial commit'

Sass とか Compass とか使えるようにする

このへん を参考にした

$ vagrant ssh
$ sudo gem install sass --no-ri --no-rdoc
$ sudo gem install compass --no-ri --no-rdoc
$ exit

そして、config.yml の assetic の項目に次のように設定を追加
# vi app1/config/config.yml

assetic:
    debug:          %kernel.debug%
    use_controller: false
    filters:
        sass:    ~
        compass: ~

そして、Twig テンプレートを使う際に、下記のように指定してあげる。

{% stylesheets filter="compass"
    "@DemoBundle/Resources/public/sass/main.sass"
    "@DemoBundle/Resources/public/sass/header.sass"
    "@DemoBundle/Resources/public/sass/footer.sass"
%}
    <link rel="stylesheet" href="{{ asset_url }}" />
{% endstylesheets %}

CakePHP に Guard を導入

Guard とは何か?

Guard とは、ruby のライブラリ。ファイルの変更を監視して、変更を検知したら指定の処理を実行する。この性質を利用して、ファイルが変更されたらテストを実行するということに用いられる。
Guard のソースコード

前準備

  • CakePHP のプロジェクト、テストがあるやつ。
  • PHPUnit のインストール
  • ruby のインストール

上記が済んでいる前提ですすめる。

guard のインストール

今回は Gemfile からインストールする。もし bundle をインストールしてなければ

gem install bundle --no-ri --no-rdoc

でインストールしておく。
CakePHP のプロジェクトの app 直下に Gemfile を作成しておく

$ cd {$CakePHP_Project}/app
$ vim Gemfile

# Gemfile

source 'https://rubygems.org'

group :development, :test do
  gem 'guard-shell'
  gem 'ruby_gntp' # 通知用のライブラリ. 監視だけならいらない
end

app 直下で下記を実行する

$ bundle install

guard の設定

Gemfile と同じ階層に Guardfile を設置する。

# Guardfile
guard :shell do
  watch(%r{Controller/(.+)\.php}) {|m| `Console/cake test app AllController --some-option`}
  watch(%r{Model/(.+)\.php}) {|m| `Console/cake test app AllModel`}
end

watch 〜 の行が監視するファイルと、変更されたら実行するシェルスクリプトになっている。このへんは各自のプロジェクトによって変更する。Plugin をテストする場合は `Console/cake test Hoge AllPlugin` のようなシェルを実行するとよい。

監視を開始する

app 直下で、下記のコマンドを実行する

$ bundle exec guard

変更を通知する

ホストマシンで実行してるなら、このへんにあるライブラリを利用すればよい。
vagrant などで VM 上で監視している場合は、フックが必要。なぜか GNTP ではゲスト(Ubuntu13.10)からホスト(Windows7)への通知が notification: ではうまくいかなかった。適当に --log-tap オプションを追加して、テスト結果を tmp/tests フォルダ以下に出すようにして、そのファイルを監視して自力で通知した。なんかいい方法あったら教えてください。

# Guardfile

guard :shell do
  watch(%r{Controller/(.+)\.php}) {|m| `Console/cake test app AllController --log-tap tmp/tests/controller.tap`}
  watch(%r{tmp/tests/(.+)\.tap}) {|m| `ruby notify.rb`}
end

そして通知用のスクリプトを Guardfile と同じ階層においた
# notify.rb

# coding: utf-8                                                                                                    
require 'ruby_gntp' #growlを呼ぶためのgem                                                                          
                                                                                                                   
f = open("tmp/tests/controller.tap")                                                                                  
lastlog = f.read                                                                                                   
f.close                                                                                                            
                                                                                                                   
GNTP.notify({                                                                                                      
    :app_name => "growl",
    :title    => "Notice",
    :text     => tap,
    :host     => "192.168.56.1",
    :passwd   => "pass"
})    

それから、Windows にも growl があるっぽいので下記を参考に growl をインストールした。
Ubuntu ServerのGuardからGrowl for Windowsにネットワーク経由で通知する | | Scimpr Blog

Error が起きる場合

下記を参考にして guard の 2.x 系をアンインストールする
【Laravel】ERROR - Could not load &#39;guard/phpunit&#39; or find class Guard::Phpunitが出た時の対処法 - ikemonn's blog

$ gem list guard
guard (2.5.1, 1.8.3)
guard-phpunit (0.1.4)
guard-shell (0.6.1)
$ gem uninstall guard -v='2.5.1'

CakePHP 用のモナドプラグイン Monaca

CakePHPモナドのライブラリ移植した。モナドがよくわかってないけど、Maybe ぐらいは使えないと今後やっていけないんじゃないかと思うぐらいに便利そうなので、勉強がてらそのへんに転がっていたライブラリを CakePHP2 に移植してみた。
ソースコードこちら

使い方はとりあえず参考にしたライブラリの方の記事を訳す。

使い方

値をモナドでラッピングするには、コンストラクタか unit 関数を用いる。ラッピングしたモナドに対しては bind 関数を用いて関数呼び出しを行う。

App::uses('Identity', 'Monaca.Lib');
$monad = new Identity(1);
$monad->bind(function($value) { var_dump($value); });
// int(1) が表示される

bind 関数を用いた関数呼び出しは、新しくモナドインスタンスでラッピングされた値を返す。

App::uses('Identity', 'Monaca.Lib');
$monad = new Identity(1);
$monad->bind(function($value) {
        return 2 * $value;
    })->bind(function($value) {
        var_dump($value);
    });
// Prints int(2)

PHP は純関数型言語ではないので、生の値を取り出す実装もしてある。

App::uses('Identity', 'Monaca.Lib');
$monad = new Identity(1);
var_dump($monad->extract());
// Prints int(1)

Scala にある Option みたいな取り方も実装した。

  • get($key)
  • getOrElse($key, $default)
  • getOrCall($key, callable $function)
  • getOrThrow($key, Exception $exception)

第一引数のキーは、値が配列の場合にオフセットかキーを指定する。スカラ値の場合は null を渡せばいい。

Maybe モナド

有用なモナドのうちのひとつは Maybe モナドだ。Null である可能性がある値をラッピングすることでヌルポを防ぐ。

App::uses('Maybe', 'Monaca.Lib');
$monad = new Maybe(1);
$monad->bind(function($value) { var_dump($value); });
// int(1) が表示される

$monad = new Maybe(null);
$monad->bind(function($value) { var_dump($value); });
// 処理されない

List モナド

リストに何か処理をしたい場合はこれを使うといい。

App::uses('ListMonad', 'Monaca.Lib');
$monad = new ListMonad(array(1, 2, 3, 4));
$doubled = $monad->bind(function($value) { return 2 * $value; });
var_dump($doubled->extract());
// array(2, 4, 6, 8) が表示される

ただし、このクラスを用いる場合は配列以外を渡すと例外がおこるので気を付けること。
また、連想配列の場合は HashMonad を使う。

Defered

バッチかなんかでは Deferred も有用だ。

App::uses('Deferred', 'Monaca.Lib');
$promise = new Deferred();
$success = function($result) { Log('success!'); };
$failure = function($result) { LogError('error!'); };
$promise->when($success, $failure);
$some_long_process = function($promise) {
    // 処理が成功
    $result = 'whatever the process result';
    if (true) {
        $promise->succeed($result);
    } else {
        $promise->fail($result);
    }
}

コンポジション

これらのモナドの中身はさらにモナドでラッピングでき、非常に有用である。

$monad = new ListMonad(array(1, 2, 3, null, 4));
$newMonad = $monad->bind(function($value) { return new Maybe($value); });
$doubled = $newMonad->bind(function($value) { return 2 * $value; });
var_dump($doubled->extract());
// array(2, 4, 6, null, 8);

これが多次元配列だった場合も

$monad = new ListMonad(array(array(1,2), array(3,4), array(5,6));
$newMonad = $monad->bind(function($value) { return new ListMonad($value); });
$doubled = $newMonad->bind(function($value) { return 2 * $value; });
var_dump($doubled->extract());
// array(array(2, 4), array(6, 8), array(10, 12))

また有用なunit関数のコールバック定数も用意してある。

$newMonad = $monad->bind(Maybe::UNIT);
// $newMonad = $monad->bind(function($value) { return new Maybe($value); }); と同じ意味

実践

例えば、下記のような多次元配列を処理する場合

$posts = array(
    array("title" => "foo", "author" => array("name" => "Bob", "email" => "bob@example.com")),
    array("title" => "bar", "author" => array("name" => "Tom", "email" => "tom@example.com")),
    array("title" => "baz"),
    array("title" => "biz", "author" => array("name" => "Mark", "email" => "mark@example.com")),
);

このような値の特定の値が欲しい場合、まず下記のような関数を実装する。

function index($key) {
    return function($array) use ($key) {
        return isset($array[$key]) ? $array[$key] : null;
    };
}

そしてモナドを使う。

$postMonad = new ListMonad($posts);
$names = $postMonad
    ->bind(Maybe::UNIT)
    ->bind(index("author"))
    ->bind(index("name"))
    ->extract();

モナドを使わないでの実装は、こんな感じになるはず。

$names = array();
foreach ($posts as $post) {
    if (isset($post['author'])) {
        if (isset($post['author']['name'])) {
            $names[] = $post['author']['name'];
        }
    }
}

その他CakePHP用に追加した関数

CakePHPモナドっぽく取得したらうまそうなやつを実装した。
・セッションの取得
・コンフィグの取得
・POST/GETパラメタの取得
・View の値の取得方法

ざっくり、MonadController を継承すると下記のような関数が使えるようになるので、AppController にこいつを継承させて使うことをお勧めする。ちなみに、CakePHP よろしくキーを 'foo.bar' のような形で指定しても array( 'foo' => array('bar' => 'baz') ) のようなオブジェクトのキーを指定できる。

HogeController.php

App::uses('MonadController', 'Monaca.Lib');

class HogeController extends MonadController {

    public function index() {
        // セッションをオプショナルに取得できる
        $aSession = $this->getSession('a');
        $bSession = $this->getSessionOrElse('b', 'default value');
        $cSession = $this->getSessionOrCall('c', function() { LogError('セッション C がないです'); return 'default'; });
        $dSession = $this->getSessionOrThrow('d', new Exception('セッションDがないです'));

        // コンフィグをオプショナルに取得
        // こちらも OrElse, OrCall, OrThrow を使える。用例は割愛。
        $aConfig = $this->getConfig('a');

        // POST/GET パラメタをオプショナルに取得
        // こちらも OrElse, OrCall, OrThrow を使える。用例は割愛。
        $aPost = $this->getPost('a');
        $aGet = $this->getQuery('a');

        // POST/GET どちらかを取得、同じキーがある場合はPOSTが優先
        // こちらも OrElse, OrCall, OrThrow を使える。用例は割愛。
        $aInput = $this->getInput('a');
    }
}

同じことが View でもできる。Controller から渡された値がない場合のデフォ値を利用できる。
こっちも getOrCall, getOrThrow は割愛。

<ul>
    <li><?php echo $this->getOrElse('hoge', 'fuga'); ?></li>
</ul>

もしコントローラーが継承できない場合はコンポーネントにしてあるのでそれを使う。ビューはビュークラスを指定することで使える。

App::uses('MonadComponent', 'Monaca.Controller/Component');
App::uses('MonadView', 'Monaca.View');

class FooController extends Controller {
  $components = array('Monaca.Monad'); // MonadComponent をロードする

  public function __constructor($request = null, $response = null) {
    parent::__construct($request, $response);

    // viewClass を指定することで MonadView が使える
    $this->viewClass = 'Monaca.Monad';
  }
}

まとめ

結論としては、自分の認識としてはモナドは依然としてよくわからない。けど、どこかの誰かが言ってたけど、モナド自体はわからなくてもモナドは使えるだけで良いのではないか、という話。確かにモナドは知らなくても、上記のサンプルがあれば使えそう。
適当に使ってるうちに多少理解深まるはず。

else のコメントの書き方

else のコメント、どこで書くのがいいかという話があったので、自分の思いを書いておく。
自分はこんな感じで書く。

// true の場合
if (hoge) {
    // なんかしょり
}
// false の場合 <- ここ!
else {
    // あんなしょり
}

一見すると見づらいかもしれないけど、最終的にこんな感じが今のところしっくりきてる。
理由はバージョン管理がしやすいしコードレビューもしやすいから。


例えば、下記のようにコメントを書いたとする。

if (hoge == 1) {
    foo();
} else { // 1 以外だとここに来ます!
    bar();
}

これだと、条件分岐が増えた場合は下記のようになる。

if (hoge == 1) {
    foo();
} else if (hoge == 2) {
    baz();
} else { // 1 か 2 以外だとここに来ます!
    bar();
}

この場合、diffは下記のようになる。

- } else { // 1 以外だとここに来ます!
+ } else if (hoge == 2) {
+    baz();
+ } else { // 1 か 2 以外だとここに来ます!

シンプルだと何をやったかわかるけど、こういうのが増えると管理が大変になる。複雑になってくると、ひとつひとつ条件が変わったかを確認する必要がでてきちゃったりして効率がわるい。
なので、if 〜 else if 〜 else はそれぞれをブロックとして分離して書いといたほうがいいと思う。

if (hoge == 1) {
    foo();
}
// hoge = 1 以外の処理!
else {
    bar();
}

この場合なら、以下のように変更になった場合

if (hoge == 1) {
    foo();
}
else if (hoge == 2) {
    baz();
}
// hoge が 1 か 2 以外の処理!
else {
    bar();
}

こんなかんじになるけど、diffとしては

- // hoge = 1 以外の処理!
+ else if (hoge == 2) {
+     baz();
+ }
+ // hoge が 1 か 2 以外の処理!

こんな感じになる。

つまり、コメントが変更されたことによって差分はでるものの、この場合は else if ブロックが挿入されただけっていうのがわかりやすい。コードレビューも捗る。

ソースコードが信用できない

「他人の書いたコードは信用できない」
「昨日の自分は他人だ」

というのは、良く言われることだ。では、どこまで信用しないのか?
例えば、コードが信用できないのならば、そのシステム自体が信用できないだろう。つまり、それはリリースしてはいけないものだ。
システムが信用できないのならば、そのサービスも信用してはいけない。
さらに言えば、それを用いたサービスを展開する会社が信用できない。
その会社と取引している会社/地域/国が信用できない。
人間ってなんだろう?


おちんぎんもらって生活していかねばならないので、そうそう疑ってもいられないだろう。ならば検証するしかない。そうだ、テストを書こう!・・・そのテストコード、信用できますか?テストする内容は妥当ですか?テストできてますか?機械語から検証できてますか?


う〜む・・・
そのへんのラインを自分にもつしかないんだろうか

Ubuntu 12.04 に GitLab 5.2 と Redmine 2.3 を構築

今まで CentOS でやってきたけど、Ubuntu で構築したらとても楽チンだったのでメモ

事前準備

まずは GitLab を下記のQiitaの記事とほぼ同じように構築
Ubuntu 12.04.2 LTSにGitLab5.2をインストール - Qiita
ついでにサービスに必要なものも全部いれてしまう。

# apt-get update
# apt-get upgrade
# apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev \
libgdbm-dev libreadline-dev libncurses5-dev libffi-dev curl git-core \
openssh-server redis-server checkinstall libxml2-dev libxslt-dev \
libcurl4-openssl-dev libicu-dev postfix python nginx \
imagemagick  libmagickcore-dev libmagickwand-dev \
mysql-server mysql-client libmysqlclient-dev

デフォルトのrubyは削除しておく。共用のサーバーとか個人のマシンとかならrbenvとかrvmとかで共存させてもいいとおもう。

# apt-get remove ruby1.8
ruby のインストール

GitLab が ruby2.0 よりも1.9.3のがいいらしいので、そっちを入れる。

# mkdir /tmp/ruby && cd /tmp/ruby
# curl --progress http://ftp.ruby-lang.org/pub/ruby/1.9/ruby-1.9.3-p429.tar.gz | tar xz
# cd ruby-1.9.3-p429
# ./configure
# make
# make install
# gem install bundler --no-ri --no-rdoc

GitLab のインストール

Gitユーザーを追加
# adduser --disabled-login --gecos 'GitLab' git

git ユーザーで作業をする

# sudo su git
# cd /home/git
gitlab-shell をインストール
# git clone https://github.com/gitlabhq/gitlab-shell.git
# cd gitlab-shell/
# git checkout -b v1.5.0 v1.5.0
# cp config.yml{.example,}
# vim config.yml # ドメインやSSLを使うならそのへんの設定を変更する
# ./bin/install
# cd ../
gitlab をデプロイ
# cd /home/git
# git clone https://github.com/gitlabhq/gitlabhq.git gitlab
# cd /home/git/gitlab
# git checkout -b v5.2.1 v5.2.1
# cp config/gitlab.yml{.example,}
# vim config/gitlab.yml # いろいろ設定
# chown -R git log/ # ログや一時ディレクトリを作成して書き込み可能にしておく
# chown -R git tmp/
# mkdir tmp/pids/
# mkdir tmp/sockets/
# mkdir public/uploads
# chmod -R u+rwX  log/
# chmod -R u+rwX  tmp/
# chmod -R u+rwX  public/uploads
# cp config/puma.rb{.example,}
# git config --global user.name "GitLab" # Gitの設定
# git config --global user.email "gitlab@example.com"
MySQLの設定
# cp config/database.yml{.mysql,}
# mysql -u root -p

ここで $password は任意のものを入力し、上記のdatabase.ymlにそのパスワードを記述すること。

mysql> CREATE USER 'gitlab'@'localhost' IDENTIFIED BY '$password';
mysql> CREATE DATABASE IF NOT EXISTS `gitlabhq_production` DEFAULT CHARACTER SET `utf8` COLLATE `utf8_unicode_ci`;
mysql> GRANT SELECT, LOCK TABLES, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER ON `gitlabhq_production`.* TO 'gitlab'@'localhost';
mysql> exit;

接続チェック

# mysql -u gitlab -p -D gitlabhq_production

gem から必要なものをインストール

# gem install charlock_holmes --version '0.6.9.4' --no-ri --no-rdoc
# bundle install --deployment --without development test postgres
# bundle exec rake gitlab:setup RAILS_ENV=production

ここのあたりで失敗する場合は、git ユーザーで gitlab-shell の ./bin/install を実行するのを忘れてたりするはず。
あとは起動チェック。

# bundle exec rake gitlab:env:info RAILS_ENV=production
# bundle exec rake sidekiq:start RAILS_ENV=production
起動スクリプトの設置
# su
# cp lib/support/init.d/gitlab /etc/init.d/gitlab
# chmod +x /etc/init.d/gitlab
# update-rc.d gitlab defaults 21

こんなかんじでGitlabの設置は完了。

Redmine

redmine は /home/ 直下じゃなくて /home/project/redmine に作ります。なんとなくユーザー領域にサービスがボコボコできるのいやなんです。

ソースのダウンロード
# mkdir -m 755 /home/project
# cd /home/project
# git clone https://github.com/redmine/redmine.git
# cd redmine
MySQL 設定
# cp config/database.yml{.example,}
# mysql -u root -p

ここで $password は任意のものを入力し、上記のdatabase.ymlにそのパスワードを記述すること。

mysql> CREATE USER 'redmine'@'localhost' IDENTIFIED BY '$password';
mysql> CREATE DATABASE IF NOT EXISTS `redmine` DEFAULT CHARACTER SET `utf8` COLLATE `utf8_unicode_ci`;
mysql> GRANT SELECT, LOCK TABLES, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER ON `redmine`.* TO 'redmine'@'localhost';
mysql> exit;
各種設定

今回はApache2じゃなくてnginxで動かす。せっかくなのでgitlabで作ったpumaファイルをコピーして流用する。

# cp /home/git/gitlab/config/puma.rb config/puma.rb
# vim config/puma.rb # gitlab 用の設定を redmine 用に変更
# vim Gemfile # puma をインストールするために " gem "puma", '~> 2.0.1' " を追加
# cp -pR config/configuration.yml{.example,}
# vim config/configuration.yml # 各種設定
# bundle install --without development test postgresql sqlite
# rake generate_secret_token
redmine インストール
# RAILS_ENV=production rake db:migrate
# RAILS_ENV=production rake redmine:load_default_data
# mkdir tmp public/plugin_assets
# chown -R www-data:www-data /home/project
# chmod -R 755 files log tmp public/plugin_assets
起動スクリプト作成

これもせっかくなのでgitlabのを流用。ちょっと変更した点が多かったのでソースそのまま乗っけてみる。

#! /bin/bash

# Redmine
# Maintainer: @randx
# App Version: 2.3

### BEGIN INIT INFO
# Provides:          redmine
# Required-Start:    $local_fs $remote_fs $network $syslog
# Required-Stop:     $local_fs $remote_fs $network $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Redmine ticket management service
# Description:       Redmine ticket management service
### END INIT INFO


APP_ROOT="/home/project/redmine"
APP_USER="www-data"
DAEMON_OPTS="-C $APP_ROOT/config/puma.rb"
PID_PATH="$APP_ROOT/tmp/pids"
SOCKET_PATH="$APP_ROOT/tmp/sockets"
WEB_SERVER_PID="$PID_PATH/puma.pid"
NAME="redmine"
DESC="Redmine service"

check_pid(){
  if [ -f $WEB_SERVER_PID ]; then
    PID=`cat $WEB_SERVER_PID`
    STATUS=`ps aux | grep $PID | grep -v grep | wc -l`
  else
    STATUS=0
    PID=0
  fi
}

execute() {
  sudo -u $APP_USER -H bash -l -c "$1"
}

start() {
  cd $APP_ROOT
  check_pid
  if [ "$PID" -ne 0 -a "$STATUS" -ne 0 ]; then
    # Program is running, exit with error code 1.
    echo "Error! $DESC $NAME is currently running!"
    exit 1
  else
    if [ `whoami` = root ]; then
      execute "rm $SOCKET_PATH/redmine.socket"
      execute "RAILS_ENV=production bundle exec puma $DAEMON_OPTS"
      execute "mkdir -p $PID_PATH > /dev/null  2>&1 &"
      echo "$DESC started"
    fi
  fi
}

stop() {
  cd $APP_ROOT
  check_pid
  if [ "$PID" -ne 0 -a "$STATUS" -ne 0 ]; then
    ## Program is running, stop it.
    kill -QUIT `cat $WEB_SERVER_PID`
    execute "mkdir -p $PID_PATH > /dev/null  2>&1 &"
    rm "$WEB_SERVER_PID" >> /dev/null
    echo "$DESC stopped"
  else
    ## Program is not running, exit with error.
    echo "Error! $DESC not started!"
    exit 1
  fi
}

restart() {
  cd $APP_ROOT
  check_pid
  if [ "$PID" -ne 0 -a "$STATUS" -ne 0 ]; then
    echo "Restarting $DESC..."
    kill -USR2 `cat $WEB_SERVER_PID`
    execute "mkdir -p $PID_PATH > /dev/null  2>&1 &"
    if [ `whoami` = root ]; then
      execute "mkdir -p $PID_PATH > /dev/null  2>&1 &"
    fi
    echo "$DESC restarted."
  else
    echo "Error, $NAME not running!"
    exit 1
  fi
}

status() {
  cd $APP_ROOT
  check_pid
  if [ "$PID" -ne 0 -a "$STATUS" -ne 0 ]; then
    echo "$DESC / Puma with PID $PID is running."
  else
    echo "$DESC is not running."
    exit 1
  fi
}

## Check to see if we are running as root first.
## Found at http://www.cyberciti.biz/tips/shell-root-user-check-script.html
if [ "$(id -u)" != "0" ]; then
    echo "This script must be run as root"
    exit 1
fi

case "$1" in
  start)
        start
        ;;
  stop)
        stop
        ;;
  restart)
        restart
        ;;
  reload|force-reload)
        echo -n "Reloading $NAME configuration: "
        kill -HUP `cat $PID`
        echo "done."
        ;;
  status)
        status
        ;;
  *)
        echo "Usage: sudo service redmine {start|stop|restart|reload}" >&2
        exit 1
        ;;
esac

exit 0

これを /etc/init.d/redmine においておく。そして設定。

# chmod +x /etc/init.d/redmine
# update-rc.d redmine defaults 21

とりあえずこんな感じでredmineも動くはず

Nginx の設定

あとは適当にNginxの設定。gitlab と redmine をバーチャルホストで動かす。SSL利用するのでkeyとかcrtとか設定したけど、必要なければそのへんはコメントアウトでいいかも。
# /etc/nginx/sites-available/gitlab

# GITLAB
# Maintainer: @randx
# App Version: 5.0

upstream gitlab {
  server unix:/home/git/gitlab/tmp/sockets/gitlab.socket;
}

server {
  listen *:443 default_server ssl;         # e.g., listen 192.168.1.1:80; In most cases *:80 is a good idea
  ssl on;
  ssl_certificate /etc/nginx/ssl/server.crt;
  ssl_certificate_key /etc/nginx/ssl/server.key;
  server_name gitlab.hogehoge.com;     # e.g., server_name source.example.com;
  root /home/git/gitlab/public;

  # individual nginx logs for this gitlab vhost
  access_log  /var/log/nginx/gitlab_access.log;
  error_log   /var/log/nginx/gitlab_error.log;

  location / {
    # serve static files from defined root folder;.
    # @gitlab is a named location for the upstream fallback, see below
    try_files $uri $uri/index.html $uri.html @gitlab;
  }

  # if a file, which is not found in the root folder is requested,
  # then the proxy pass the request to the upsteam (gitlab unicorn)
  location @gitlab {
    proxy_read_timeout 300; # https://github.com/gitlabhq/gitlabhq/issues/694
    proxy_connect_timeout 300; # https://github.com/gitlabhq/gitlabhq/issues/694
    proxy_redirect     off;

    proxy_set_header   X-Forwarded-Proto $scheme;
    proxy_set_header   Host              $http_host;
    proxy_set_header   X-Real-IP         $remote_addr;

    proxy_pass http://gitlab;
  }
}

これと redmine
/etc/nginx/sites-available/redmine

# Redmine
# Maintainer: @randx
# App Version: 2.3

upstream redmine {
  server unix:/home/project/redmine/tmp/sockets/redmine.socket;
}

server {
  listen *:443 ssl;         # e.g., listen 192.168.1.1:80; In most cases *:80 is a good idea
  ssl on;
  ssl_certificate /etc/nginx/ssl/server.crt;
  ssl_certificate_key /etc/nginx/ssl/server.key;
  server_name redmine.hogehoge.com;     # e.g., server_name source.example.com;
  root /home/project/redmine/public;

  # individual nginx logs for this gitlab vhost
  access_log  /var/log/nginx/redmine_access.log;
  error_log   /var/log/nginx/redmine_error.log;

  location / {
    # serve static files from defined root folder;.
    # @gitlab is a named location for the upstream fallback, see below
    try_files $uri $uri/index.html $uri.html @redmine;
  }

  # if a file, which is not found in the root folder is requested,
  # then the proxy pass the request to the upsteam (gitlab unicorn)
  location @redmine {
    proxy_read_timeout 300; # https://github.com/gitlabhq/gitlabhq/issues/694
    proxy_connect_timeout 300; # https://github.com/gitlabhq/gitlabhq/issues/694
    proxy_redirect     off;

    proxy_set_header   X-Forwarded-Proto $scheme;
    proxy_set_header   Host              $http_host;
    proxy_set_header   X-Real-IP         $remote_addr;

    proxy_pass http://redmine;
  }
}

んで、起動するときは

# service gitlab restart
# service redmine restart
# service nginx restart

とかやる。

メールの設定

せっかくメールと連携できるサービスなので、postfix(送信のみ)を設定しておく。
まずデフォルトでsendmailがはいっていたりするので、それを削除。

# service sendmail stop
# apt-get remove sendmail

んで、postfix の設定ファイルを少しいじる

# vim /etc/postfix/main.cf

このへんはTSL使うとか、ドメイン変えるとかを変更。
そして起動。

# service postfix start

送信をテストする

# mail unko@hoge.com

メール作成を完了するには、最後の行に「.」だけをうってエンターキーを押す。うまく送信できない場合は /var/log/mail.log あたりを参考に修正していく。

こんなかんじでひととおりできるはず。

rbenv + passenger のアップデートは要注意

なんか何回やっても rake-0.10.2 なんてねーよ!って怒られてて、本番環境が稼働してなかった。

原因としては、rbenv の ruby だけバージョンあげてて、passenger で利用する ruby の環境をアップデートしてなかったから。

元々 1.9.3-p286 で passenger をインストールしてたから、利用されるバイナリの設定は下記のように apache の conf に記述してた。

   LoadModule passenger_module /usr/local/rbenv/versions/1.9.3-p286/lib/ruby/gems/1.9.1/gems/passenger-3.0.18/ext/apache2/mod_passenger.so
   PassengerRoot /usr/local/rbenv/versions/1.9.3-p286/lib/ruby/gems/1.9.1/gems/passenger-3.0.18
   PassengerRuby /usr/local/rbenv/versions/1.9.3-p286/bin/ruby

しかし、これが 1.9.3-p327 にあがると、下記のように記述しなきゃいけない。

   LoadModule passenger_module /usr/local/rbenv/versions/1.9.3-p327/lib/ruby/gems/1.9.1/gems/passenger-3.0.18/ext/apache2/mod_passenger.so
   PassengerRoot /usr/local/rbenv/versions/1.9.3-p327/lib/ruby/gems/1.9.1/gems/passenger-3.0.18
   PassengerRuby /usr/local/rbenv/versions/1.9.3-p327/bin/ruby

はじめは動くんだけど、上の rake みたいに、元々は 0.9.2 使っててそれがバージョンアップしたらそこのパスにインストールされていないのでエラーが起こっちゃう。

rbenv で ruby をアップデートする際は、下記のようにするといいと思う。

$ ruby-build install
$ rbenv install new-ruby
$ rbenv global new-ruby
$ rbenv rehash
$ gem install  passenger --no-ri --no-rdoc
$ export PATH=/usr/local/apache2/bin:$PATH # httpd-devel が必要なのでパスを通しておく
$ passenger-install-apache2-module
# ここで上記のような設定に必要なものが出てくるので、それを apache の conf ファイルに記述。

$ /etc/init.d/httpd restart

ハマった・・・