Hatena::ブログ(Diary)

cooldaemonの備忘録 RSSフィード

2008-10-07

Webistrano で Catalyst で作ったアプリをデプロイする

とある Subversionリポジトリとするプロジェクトで、デプロイツールとして Capistrano を使用しようと思ったのですが、私と同僚の開発環境が異なる事を理由に、前から目を付けていた Webistrano を試してみました。

動作環境

apache2.2.9
mysql5.0.67
ruby1.8.6
rails2.1.0
passenger2.0.3
capistrano2.5.0
webistranoRevision 189

webistrano 本体は http://labs.peritor.com/svn/webistrano/trunk/ からチェックアウトしたものを使用しています。

基本的な使い方

下記を眺めると、何となく理解できます。

Screencasts – Peritor Webistrano – Trac

Catalyst で作ったアプリをデプロイする

Export capfile メニューから Capfile を参照すると、一行目で load 'deploy' していたので /path/to/lib/capistrano/recipes/deploy.rb を眺めながら下記の Recipe を書いてみました。

下記は古い Recipe です。最新は、gist: 20946 ― GitHub にあります。新しい版は、:shared_dirs を指定する事で :shared_children にディレクトリのリストを追加できるようになっています。

namespace :deploy do

  desc "finalize update."
  task :finalize_update, :except => { :no_release => true } do
    run "chmod -R g+w #{latest_release}" if fetch(:group_writable, true)

    run <<-CMD
      rm -rf #{latest_release}/log &&
      ln -s #{shared_path}/log #{latest_release}/log
    CMD
  end

  desc "restart app servers."
  task :restart, :roles => :app, :except => { :no_release => true } do
    sudo "/usr/local/etc/rc.d/apache22 stop"
    sudo "/usr/local/etc/rc.d/apache22 start"
  end

  task :before_setup do
    set :shared_children, %w(log)
  end

end
finalize_update

share ディレクトリ配下 には log ディレクトリのみ配置したかったので、上記のように上書きしました。

restart

FreeBSD 上で mod_perl を使用しているので、上記のように上書きしました。

before_setup

setup 時、shared_children に設定されているディレクトリが share 配下に作られるのですが、configuration の value に %w(log) と入れても "%w(log)" とされる為、苦肉の策で before_xxx で上書きしました。

何か良い手、ないですかね?

新しい版では、before_setup を定義せずに :shared_children を書き換えてます。

感想

「デプロイ元の環境に依存せず、Web U/I から簡単にデプロイできる」という事を差っ引いても、複数の Stage を登録でき、細かい用途毎に Recipe を分割管理できるので、利便性が非常に高く感じられます。

これに慣れると、素の capistrano なんて使ってられないなぁ。

2008-06-25

TTSite で作った MyApp::View::TT を Catalyst::View::Templated を使うように書き換えてみた

package MyApp::View::TT;

use strict;
use warnings;
use base 'Catalyst::View::Templated';
use Template;

MyApp->config->{'View::TT'} = {
    EVAL_PERL          => 0,
    TEMPLATE_EXTENSION => '.tt2',
    CATALYST_VAR       => 'c',
    INCLUDE_PATH       => [
        MyApp->path_to('root', 'src'),
        MyApp->path_to('root', 'lib'),
    ],
    PRE_PROCESS        => 'config/main',
    WRAPPER            => 'site/wrapper',
    ERROR              => 'error.tt2',
};

sub new {
    my ($class, $c, $args) = @_;
    my $self = $class->next::method($c, $args);

    $self->{engine} = Template->new({
        %{$self->context->config},
        %$args,
    });
    return $self;
}

sub _render {
    my ($self, $template, $stash, $args) = @_;
    my $engine = $self->{engine};

    $engine->process($template, $stash, \my $output,) or die $engine->error;
    return $output;
}

1;

問題なければ、Helper を書く。

使い方はこんな感じ。

$c->view('TT')->template('path_to/baz');
$c->stash->{foo} = 'bar';
$c->detach('View::TT');

テンプレートの指定は省略可能。

2008-03-06

MVC の実現の仕方(私の場合)

以下のエントリを拝見させて頂いて、思った事のメモ。

MVCのモデルはDBじゃなくてもいいんだよ - D-6 相変わらず根無し

個人差があるかとは思いますが、Catalyst を使っていると、 C のテストを書く行為を、私は、そんなに苦に感じない。ただ、C は URI リソースと M を繋ぐラッパと割り切り、複雑なビジネスロジックは M に押し付ける方が、コードがスッキリするし、M 自体を汎用的にできそう。

MVCのモデルはDBじゃなくてもいいんだよ - D-6 相変わらず根無し

じゃあ元々のブクマコメントのModel::DBICはどうすんだよ、いらないのかよ、みたいなところに行き着くかと思うのだが、そこはそれ、モデルに「格納先」を指定するのにDBを抽象化したModel::DBICを使ってやる、っていう手もある

my $c = shift; # Catalyst的なcontextがあると仮定します
MyApp::Model::BlogEntry->create({
    storage => $c->model('DBIC::BlogStorageSchema'),
    blog  => $blog,
    title => $entry_title,
    content => $entry_content
});

これで全く問題なくDBICの良いところを使える。

Catalyst::Model::Adaptor でラップして MyApp::Model::BlogEntry を作り、prepare_arguments を下記のようにすれば・・・

sub prepare_arguments {
    my ($self, $app) = @_;
    return {
        storage => $app->model('DBIC::BlogStorageSchema'),
        map {$_ => $app->req->param($_)} qw(blog title content),
    };
}

C の中で実際に使う際には

$c->model('BlogEntry')->create;

で行けるかな?

2008-01-26

テスト用 DB の準備を行うモジュールを作った

Catalyst 本を参考に、やっつけで書いてみた。

Catalyst 本との違いは fixture に対応している点。

使い方(Test::WWW::Mechanize::Catalyst と連携等)や解説は、近いうちに書く。後で Helper にする。

package MyApp::Test::Database;

use strict;
use warnings;

use Directory::Scratch;
use MyApp::Schema;
use FindBin qw($Bin $Script);
use YAML::Syck;

use base 'Exporter';
our @EXPORT = qw(schema);

my $schema;
my $config;

BEGIN {
    my $tmp = Directory::Scratch->new;
    my $db  = $tmp->touch('database');
    my $dns = "DBI:SQLite:$db";

    $schema = MyApp::Schema->connect($dsn);
    $schema->deploy;

    $config = "$Bin/../conf/myapp_local.yml";
    DumpFile($config, {'Model::DBIC' => {connect_info => [$dsn]}});

    _load_fixture();

    sub _load_fixture {
        my $fixture_file = "$Bin/fixture/$Script";
        $fixture_file =~ s/\.t$/.yml/;
        return if ! -f $fixture_file;

        my $fixture_for = LoadFile($fixture_file);
        while (my ($model, $fixtures) = each %$fixture_for) {
            for (@$fixtures) {
                $schema->resultset($model)->create($_);
            }
        }
    }
}

END {
    unlink $config;
}

sub schema {
    $schema;
}

1;

便利な CPAN Modules があった

Jonathan Rockway 氏作 の DBICx::TestDatabase が良さ気。

SQLite を利用してテスト用の一時データベースを作ってくれる。

source を覗いたら $schema->deploy を実行していた。

私が書いた Module と異なり myapp_local.yml を生成してくれないので、直接 Controller を叩くテストには使えない。

これに tokuhirom 氏作の Test::Fixture::DBIC::Schema を組み合わせて、テスト用の Module を書いてみよう。(後でこのエントリに追記する)

2007-08-06

catalyst で作ったアプリを capistrano でデプロイ

capistrano インストール

gem install capistrano

Capfile を配置

capify コマンドで Capfile と config/deploy.rb を作ってくれるが、今回は自前で用意する。

好きなディレクトリに下記内容の Capfile ファイルを配置する。

catalyst.pl コマンドで作ったディレクトリの直下が解りやすいかも。

load 'conf/deploy'

これで、conf/deploy.rb を読み込んでくれる。

deploy.rb を配置

Capfile で指定した path に下記内容の deploy.rb ファイルを配置する。

require 'capistrano/recipes/deploy/scm'
require 'capistrano/recipes/deploy/strategy'

set :application, "AppName"
set :repository,  "http://domain/path/to/AppName/"

set :scm, :subversion
set :deploy_via, :checkout

set(:deploy_to) { "/path/to/#{application}" }
set(:revision)  { source.head }

set(:source)    { Capistrano::Deploy::SCM.new(scm, self) }
set(:real_revision) {
  source.local.query_revision(revision) { |cmd|
    with_env("LC_ALL", "C") {`#{cmd}`}
  }
}

set(:strategy) { Capistrano::Deploy::Strategy.new(deploy_via, self) }

set(:release_name) {
  set :deploy_timestamped, true; Time.now.utc.strftime("%Y%m%d%H%M%S")
}

set(:releases_path) { File.join(deploy_to, "releases") }
set(:current_path)  { File.join(deploy_to, "current") }
set(:release_path)  { File.join(releases_path, release_name) }

set(:releases)         { capture("ls -x #{releases_path}").split.sort }
set(:current_release)  { File.join(releases_path, releases.last) }
set(:previous_release) { File.join(releases_path, releases[-2]) }

set(:current_revision)  { capture("cat #{current_path}/REVISION").chomp }
set(:latest_revision)   { capture("cat #{current_release}/REVISION").chomp }
set(:previous_revision) { capture("cat #{previous_release}/REVISION").chomp}

set(:latest_release) {
  exists?(:deploy_timestamped) ? release_path : current_release
}

set(:run_method) { fetch(:use_sudo, true) ? :sudo : :run }

def with_env(name, value)
  saved, ENV[name] = ENV[name], value 
  yield
ensure
  ENV[name] = saved
end

role :servers, "domain name or ip address", "domain name or ip address"

namespace :deploy do
  desc "deploy."
  task :default do
    update
    restart
  end
  
  task :update do
    transaction do
      update_code
      symlink
    end
  end
  
  task :update_code, :except => { :no_release => true } do
    on_rollback { run "rm -rf #{release_path}; true" }
    strategy.deploy!
    finalize_update
  end

  task :finalize_update, :except => { :no_release => true } do
    stamp = Time.now.utc.strftime("%Y%m%d%H%M.%S")
    asset_paths = %w(images css js).map { |p|
      "#{latest_release}/root/static/#{p}"
    }.join(" ")
    run "find #{asset_paths} -exec touch -t #{stamp} {} ';'; true", :env => { "TZ" => "UTC" }
  end

  task :symlink, :except => { :no_release => true } do
    on_rollback {
      run "rm -f #{current_path}; ln -s #{previous_release} #{current_path}; true"
    }
    run "rm -f #{current_path} && ln -s #{latest_release} #{current_path}"
  end

  task :restart do
    sudo "/usr/local/etc/rc.d/apache22 stop"
    sudo "/usr/local/etc/rc.d/apache22 start"
  end

  desc "rollback."
  task :rollback do
    rollback_code
    restart
  end

  task :rollback_code, :except => { :no_release => true } do
    if releases.length < 2
      abort "could not rollback the code because there is no prior release"
    else
      run "rm #{current_path}; ln -s #{previous_release} #{current_path} && rm -rf #{current_release}"
    end
  end

  desc "setup."
  task :setup, :except => { :no_release => true } do
    dirs = [deploy_to, releases_path]
    run "umask 02 && mkdir -p #{dirs.join(' ')}"
  end

  desc "cleanup."
  task :cleanup, :except => { :no_release => true } do
    count = fetch(:keep_releases, 5).to_i
    if count >= releases.length
      logger.important "no old releases to clean up"
    else
      logger.info "keeping #{count} of #{releases.length} deployed releases"
      directories = (releases - releases.last(count)).map { |release|
        File.join(releases_path, release) }.join(" ")
        invoke_command "rm -rf #{directories}", :via => run_method
    end
  end
end

capistrano/recipes/deploy.rb まる写し。

cap の引数説明
deployサーバにアプリを配置して、apache を再起動
deploy:rollbackリリースを一世代戻して、apache を再起動
deploy:setupサーバに配置先のディレクトリを作成する
deploy:cleanup五世代前のリリースを削除する

role を app と web に分けたら実用に近づきそう。

古い subversion を使っていると、svn info を実行する際にエラーが出るので注意。