Capistranoの使い方

仕事でCapistranoを使っている。使い始めて半年経ったので,使い方をまとめておく。

Capistranoとは

Capistranoは複数のサーバー上でスクリプトを実行するためのオープンソースのツールであり,その主な用途はウェブアプリケーションのソフトウェアデプロイメントである。1つ以上のWebサーバ上のアプリケーションを新しいバージョンにする作業を自動化出来たり,データベースを変更するといった作業もできる。

Capistranoをインストールする

Capistranoはrubyで書かれているので,インストールするにはRubyとRubygemsとBundlerが必要である。
Gemfileという名前のファイルを作成し,以下の記述を追加してbundle installを実行することでインストールすることが出来る。

gem 'capistrano', '~> 3.0.1'

Capistranoの設定ファイルを作成する

Capistranoのインストールが終わったら,Capistranoの設定ファイルを作成する。以下のコマンドを実行する。

bundle exec cap install

実行するとCapfileとconfigディレクトリとlibディレクトリが作成される。

config/deploy/ステージ名.rbを記述する方法

ステージ名.rbにはステージ毎の設定を書くことが出来る。今回はtest.rbという名前で簡単な例を作成する。

サーバはlocalhostを対象にし,そのサーバへはvagrantというユーザでログインし,そのサーバに「Webサーバ」というロールを与えるということを設定している。

server 'localhost', user: 'vagrant', roles: %w{web}

紹介までに留めるが,このファイルには以下のような内容を設定することが出来る。

  • ホスト名
  • ログインユーザ
  • サーバロール
  • SSH設定
  • その他、そのサーバに紐づく任意の設定

config/deploy.rbを記述する方法

config/deploy.rbにはステージ間で共通の設定を記述する。

よくあるのは以下のような設定である。

  • アプリケーション名
  • レポジトリ名
  • 利用するSCM
  • タスク
  • それぞれのタスクで実行するコマンド

上記の項目をconfig/deploy.rbにDSL(Capistrano固有の言語)で記述する。DSLには文法と語彙が存在するので,以下よりこれらについて説明する。

設定値の変更と取得

これを定義するとグローバル変数のようにdeploy.rbやステージ.rbの全域で設定値を取り出すことが出来る。
set :名前, 値で設定し,fetch :名前で設定を取り出している。

set :repo_url, 'git@github.com:whohu/sample_app'
fetch :repo_url #=> "git@github.com:whohu/sample_app"

タスクの定義

実行したいコマンドなどをタスクとして定義することが出来る。
task :タスク名 do; … endのブロックでタスクを設定することが出来る。

task :uptime do
    ここにタスクの内容
end

さらにtaskブロックの中にはrun_locally do; … endもしくはon 対象サーバ do; … endブロックを記述することが出来る。run_locallyブロック内にはローカルマシン上で実行するコマンドを記述する。onブロック内にはサーバ上で実行するコマンドを記述する。

task :uptime do
    run_locally do
        #ここにローカルマシン上で実行するコマンド
    end
    on roles(:web) do
        #ここにサーバ上で実行するコマンド
    end
end

上記の書式内ではonブロックにてWebサーバ(:web)のロールが与えられているサーバのみを作業対象とする設定を行っていることに注意してほしい。

主なコマンド

run_locallyブロックとonブロックで使用することが出来る主なコマンドとして以下の3つがある。

  • executeコマンド
    コマンドを実行することが出来る。
task :uptime do
    run_locally do
        execute "uptime"
    end
    on roles(:web) do
        execute "uptime"
    end
end
  • captureコマンド
    標準出力の内容を受け取ることが出来る。outputに標準出力の内容が代入される。
task :uptime do
    run_locally do
        output = capture "uptime"
    end
    on roles(:web) do
        output = capture "uptime"
    end
end
  • infoコマンド
    ログを出力することが出来る。
task :uptime do
    run_locally do
        output = capture "uptime"
        info output
    end
    on roles(:web) do
        output = capture "uptime"
        info output
    end
end

ログレベルに応じてdebug, info, warn, error, fatalなど文法を使うことが出来る。

実際にレシピを書いてみる

config/deploy/test.rb

server 'localhost', user: 'vagrant', roles: %w{web}

config/deploy.rb

set :application, 'finalge_sample_app'
set :repo_url, 'git@github.com:whohu/sample_app.git'

#updateタスクを実行する。Gitからソースコードを取得する。
task :update do
    run_locally do
        application = fetch :application
        if test "[ -d #{application} ]"
            #test.rbで記述した環境下にfinalge_sample_appディレクトリがあれば,ディレクトリ内に移動してgit pullする。
            execute "cd #{application}; git pull"
        else
            #test.rbで記述した環境下にfinalge_sample_appディレクトリが無ければgit cloneする。
            execute "git clone #{fetch :repo_url} #{application}"
        end
    end
end

#上記のupdateタスクが終わったらarchiveタスクを実行する。
task :archive => :update do
    run_locally do
    
        #sbtコマンドでビルドする(sbtはScalaのビルドコマンド)。
        sbt_output = capture "cd #{fetch :application}; sbt pack-archive"
        
        #パスを取得する処理が続く…。
        sbt_output_without_escape_sequences = sbt_output.lines.map { |line| line.gsub(/\e\[\d{1,2}m/, '') }.join
        archive_relative_path = sbt_output_without_escape_sequences.match(/\[info\] Generating (?<archive_path>.+\.tar\.gz)\s*$/)[:archive_path]
        archive_name = archive_relative_path.match(/(?<archive_name>[^\/]+\.tar\.gz)$/)[:archive_name]
        archive_absolute_path = File.join(capture("cd #{fetch(:application)}; pwd").chomp, archive_relative_path)
        
        info archive_absolute_path
        info archive_name
        
        set :archive_absolute_path, archive_absolute_path
        set :archive_name, archive_name
    end
end

#上記のarchiveタスクが終わったらdeployタスクを実行する。
task :deploy => :archive do

    #archive タスクで設定したパスを取得する。
    archive_path = fetch :archive_absolute_path
    archive_name = fetch :archive_name
    release_path = File.join(fetch(:deploy_to), fetch(:application))
    
    on roles(:web) do
        #前回デプロイした際に実行したアプリケーションのプロセスをkillする(2回目以降のデプロイを考慮して)
        begin
            old_project_dir = File.join(release_path, capture("cd #{release_path}; ls -d */").chomp)
            if test "[ -d #{old_project_dir} ]"
                running_pid = capture("cd #{old_project_dir}; cat RUNNING_PID")
                execute "kill #{running_pid}"
            end
        rescue => e
            info "No previous release directory exists"
        end
        
        unless test "[ -d #{release_path} ]"
            #test.rbで記述した環境下にrelease_pathに格納したパスが存在しない場合は新しくディレクトリを作成する。
            execute "mkdir -p #{release_path}"
        end
        
        #archive_path変数が参照しているディレクトリをrelease_path変数が参照しているディレクトリへコピーする。
        upload! archive_path, release_path
        
        #release_path変数が参照しているディレクトリへ移動し,tarコマンドでアーカイブを作成する。
        execute "cd #{release_path}; tar -zxvf #{archive_name}"
        project_dir = File.join(release_path, capture("cd #{release_path}; ls -d */").chomp)
        launch = capture("cd #{project_dir}; ls bin/*").chomp 
        
        #nohupコマンドで起動スクリプトを実行する。
        execute "cd #{project_dir}; ( ( nohup #{launch} &>/dev/null ) & echo $! > RUNNING_PID)"
    end
end

capコマンドでタスクを実行する

必要な設定が記述し終わったらcapコマンドによってタスクを実行する。capコマンドの第1引数がconfig/deploy/test.rbのtestの部分に対応する。第2引数がタスク名でtask :deployのdeployの部分に対応する。

bundle exec cap test deploy

参考
capistrano/capistrano
Capistrano – Wikipedia
入門 Capistrano 3 ~ 全ての手作業を生まれる前に消し去りたい | GREE Engineers’Blog
【入門】Capistrano3で自動デプロイ – Qiita
capistrano 3 をできるだけシンプルにサーバーにコマンドを流し込むツールとして使いこなす – Qiita