Hatena::ブログ(Diary)

cooldaemonの備忘録 RSSフィード

2009-10-20

Ruby Enterprise Edition + Webistrano

諸事情によりシステム標準の rubyRuby Enterprise Edition で入れた ruby を同居させていたのですが、Webistrano は、models/deployment.rb の deploy_in_background! というメソッドの中で直接 ruby コマンドを実行しているので、システム標準の ruby が呼ばれてしまいます。

とりあえず ruby を /opt/ruby-enterprise-X.X.X-YYYYMMDD/bin/ruby に置換して対応しました。

それ以外は、特に手を加えず Ruby Enterprise Edition 経由で入れた Phusion Passenger 上で問題なく動いているのですが・・・なんか嫌だなぁ

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 なんて使ってられないなぁ。

2007-09-06

rails + lighttpd + fastcgi の環境で、fastcgi の起動・停止・再起動を capistrano に任せる

rails + lighttpd + fastcgi の環境で、fastcgi の起動・停止・再起動を capistrano に任せる作業に、少しハマったので作業メモを残す。

deploy.rb の内容や、他の詳しい解説は他のウェブ上資料に譲る。

app_name/script/spin

./script/process/spawner fcgi -p 11000 -i 3 -r 5

実行権限を忘れずに。

app_name/config/lighttpd.conf

fastcgi.server      = ( ".fcgi" => (
    "localhost-11000" => ( "host" => "127.0.0.1", "port" => 11000 ),
    "localhost-11001" => ( "host" => "127.0.0.1", "port" => 11001 ),
    "localhost-11002" => ( "host" => "127.0.0.1", "port" => 11002 ),
    "localhost-11003" => ( "host" => "127.0.0.1", "port" => 11003 ),
    "localhost-11004" => ( "host" => "127.0.0.1", "port" => 11004 ),
) )

上記を追加する。


以上で、初期で準備される deploy.rb に特別な task を何も追加しなくとも、cap コマンドのオプション deploy:start deploy:stop deploy:restart が動く。

ただし、spawner や reaper は、fastcgi の面倒を見てくれるが lighttpd の面倒は見てくれない。

lighttpd の面倒を cap コマンドで見る場合は、spin に lighttpd -f ./config/lighttpd.conf 等の起動コマンドを追加し、deploy:stop の task として killall lighttpd 等を追加する必要がある。

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 を実行する際にエラーが出るので注意。

2007-01-16

AR を使わない validation (ファイルアップロードのサンプル)

Form に1対1で対応するモデル(?) ActionForm を使ってみた。

インストール方法は、こちら→京の路|RailsのActiveFormの使い方

class HogeFile < ActiveForm
  attr_accessor :name, :type, :size, :data

  validates_format_of :type, :with => /^text/, :message => 'テキスト形式のファイルを選択して下さい'

  validates_each :size do |recode, attr, value|
    message = '';
    if value.blank? || value.to_i <= 0 then
      message = 'ファイルを選択して下さい (ファイルを選択しても、このメッセージが表示される場合は、ファイルのサイズが 0 ではない事を、ご確認下さい)'
    elsif 5_000_000 < value.to_i then
      message = 'サイズが 5M 以下のファイルを選択して下さい'
    end
    recode.errors.add attr, message if !message.blank?
  end

  def file=(file)
    self.name = File.basename(file.original_filename).gsub(/^?w._-/, '') ?
      if file.respond_to?(:original_filename)
    self.type = file.content_type.chomp if file.respond_to?(:content_type)
    self.size = file.size               if file.respond_to?(:size)
    self.data = file.read               if file.respond_to?(:read)
  end

  def save
    File.open('tmp/uploads/' + self.name, 'wb') do |f|
      f.write(self.data)
    end
  end
end

ActiveForm を plugin 配下に設置してしまえば、model 内で即継承可能。(require 不要)

attr_accessor でアクセサを作って、validates_xxx_of で値の検証を行える。

ファイルはフォームから、@params[:hoge][:file] として入ってくるように作るので

file= メソッドを定義して、プロパティの初期化を行っている。

(この辺りの説明は、ActiveForm の source 読んだ方が早い)

class HogeController < ApplicationController
  def index
    @hoge_file = HogeFile.new if @hoge_file.nil?
    # do something..
  end

  def upload
    @hoge_file = HogeFile.new( params[:hoge] )
    if @hoge_file.valid? then
      @hoge_file.save
    else
      index
      render :action => 'index'
    end
  end
end

「index」 呼んで「render :action => 'index'」してるのがダサい。

<%= error_messages_for :hoge_file %>
<%= form_tag({:action => 'upload'}, {:multipart => true}) %>
<%= file_field :hoge, :file %>
<%= submit_tag 'upload' %>
<%= end_form_tag %>

error_messages_for を使いたいが為に、HogeController#index で「@hoge_file」の初期化を行っている。ダサい。

全体的に、勉強不足。rails 熟練者の方に、美しい書き方をご指導願いたいなぁ・・・。