capistrano 3 をできるだけシンプルにサーバーにコマンドを流し込むツールとして使いこなす

More than 3 years have passed since last update.


戦略


  • Railsのことは忘れる(そういうこともあるよ)

  • capをサーバーにsshでコマンドを送り込むrake taskの塊と見る

  • 「とある役割のサーバー」に対して「とある処理」を実行して回りたい、という場合を想定


    • 単にrestartだけ切り出して実行するとか、あるコマンドの結果だけ手に入れるとか



  • 個人的にはansibleでもできると思ったけどrubyメンバーに強要できないし、小回り効かせたい


準備

$ bundle init

$ echo gem "capistrano" >> Gemfile
$ bundle install
$ cap install

ちなみにversionは、

$ cap -V

Capistrano Version: 3.2.1 (Rake Version: 10.3.2)

ここまでやると以下のようなディレクトリ構成になる

$ tree

.
├── Capfile
├── Gemfile
├── Gemfile.lock
├── config
│   ├── deploy
│   │   ├── production.rb
│   │   └── staging.rb
│   └── deploy.rb
└── lib
└── capistrano
└── tasks

ちょっとした解説


  • Capfileはrequireなどの設定とlib以下のtaskの読み込みに使う

  • config/deploy.rbには全ステージ共通の設定を書く

  • production.rbとかstating.rbに各ステージの設定を書く


    • サーバーのIPや、専用taskとか



  • lib/capistrano/tasksの下にはオリジナルのtaskを追加したいときに使う


deploy.rbを最低限の設定にする


config/deploy.rb

lock '3.2.1'

# set :pty, true

本当に全部消してとりまこれだけにしてみた。

※後述するけどsudo使いたいならptyを有効にする必要がある


ステージを増やす

今回試す場所が開発サーバーなので、dev.rbをはやしてみる。developmentでもいいと思う。

└─- config

└── deploy
├── production.rb
├── staging.rb
└── dev.rb


config/deploy/dev.rb

role :app,  %w{hoge@example.com}


これは以下のようにステージを指定できることを意味する。

$ cap dev YOUR_TASK_NAME


オリジナルタスクを追加してみる

ミニマムで試すなら、そのままdev.rbに書いてしまう。


config/deploy/dev.rb

role :app, %w{hoge@example.com}

task :ls do
on roles(:app) do
execute "ls"
end
end

これで、



  • hoge@example.comでログインできるappという役割のサーバーを定義


    • (複数書くならスペース区切りで増やせばいい)



  • appに対してのみlsを実行する:lsというタスクを定義

したことになる。

実行してみる。

$ bundle exec cap dev ls


INFO[6a60919c] Running /usr/bin/env ls on example.com
DEBUG[6a60919c] Command: /usr/bin/env ls
DEBUG[6a60919c] <<ここにファイル一覧が表示される>>
INFO[6a60919c] Finished in 1.851 seconds with exit status 0 (successful).

タスクを増やすならnamespaceを付けた方があとあと見通しが良くなる。


config/deploy/dev.rb

namespace :utils do

task :ls do
on roles(:app) do
execute "ls"
end
end
task :perl_version do
on roles(:app) do
execute %[perl -v]
end
end
end

実行してみる。namespace:task_nameというように記述する。

$ bundle exec cap dev utils:perl_version


INFO[444094bb] Running /usr/bin/env perl -v on +HFOWFLWKEJF+WLEHF
DEBUG[444094bb] Command: perl -v
DEBUG[444094bb]
DEBUG[444094bb] This is perl, v5.10.1 (*) built for x86_64-linux-thread-multi
DEBUG[444094bb]
DEBUG[444094bb] Copyright 1987-2009, Larry Wall
DEBUG[444094bb]
DEBUG[444094bb] Perl may be copied only under the terms of either the Artistic License or the
DEBUG[444094bb] GNU General Public License, which may be found in the Perl 5 source kit.
DEBUG[444094bb]
DEBUG[444094bb] Complete documentation for Perl, including FAQ lists, should be found on
DEBUG[444094bb] this system using "man perl" or "perldoc perl". If you have access to the
DEBUG[444094bb] Internet, point your browser at http://www.perl.org/, the Perl Home Page.
DEBUG[444094bb]
INFO[444094bb] Finished in 1.874 seconds with exit status 0 (successful).


決まった塊でlib以下に移す

大きくなったらlib/capistrano/tasksに移せばいい

lib/capistrano/tasks/utils.rakeというのをつくってみる。


lib/capistrano/tasks/utils.rake

namespace :utils do

task :ls do
on roles(:app) do
execute "ls"
end
end
task :perl_version do
on roles(:app) do
execute %[perl -v]
end
end
end

namespaceのブロックをまるごとコピペでいい。実行のやりかたも変わらない。

$ bundle exec cap dev utils:perl_version


INFO[444094bb] Running /usr/bin/env perl -v on LHFWLKEFJWEFJ
DEBUG[444094bb] Command: perl -v
DEBUG[444094bb]
DEBUG[444094bb] This is perl, v5.10.1 (*) built for x86_64-linux-thread-multi
DEBUG[444094bb]
DEBUG[444094bb] Copyright 1987-2009, Larry Wall
DEBUG[444094bb]
DEBUG[444094bb] Perl may be copied only under the terms of either the Artistic License or the
DEBUG[444094bb] GNU General Public License, which may be found in the Perl 5 source kit.
DEBUG[444094bb]
DEBUG[444094bb] Complete documentation for Perl, including FAQ lists, should be found on
DEBUG[444094bb] this system using "man perl" or "perldoc perl". If you have access to the
DEBUG[444094bb] Internet, point your browser at http://www.perl.org/, the Perl Home Page.
DEBUG[444094bb]
INFO[444094bb] Finished in 1.874 seconds with exit status 0 (successful).

これでdev.rbの中にはサーバーの情報だけ記述されるだけになるので、シンプルになる。


環境変数を渡してコマンドを実行する

bundle exec cap dev utils:echo user_name=kazuph

とかやって、外から自由に値を渡せる。


lib/capistrano/tasks/utils.rake

...

task :echo do
set :user_name, ENV["user_name"] || raise("ユーザー名必須")
on roles(:app) do
execute "echo #{fetch(:user_name)}"
end
end

ENVで受け取って、setすればfetchで取れる(使わないでそのままENVつっこんでもいい)。


実行時にユーザーに入力させる

対話式で入力させてもいい。


lib/capistrano/tasks/utils.rake

...

task :echo do
ask(:user_name, nil) ## ここで聞いてる
on roles(:app) do
execute "echo #{fetch(:user_name) || raise("ユーザー名は必須だよん☆")}"
end
end


実行する。

$ bundle exec cap dev utils:echo

Please enter user_name (): kazuph
INFO[62046886] Running /usr/bin/env echo kazuph on example.com
DEBUG[62046886] Command: echo kazuph
INFO[62046886] Finished in 2.086 seconds with exit status 0 (successful).
DEBUG[62046886] kazuph
INFO[62046886] Finished in 2.086 seconds with exit status 0 (successful).

askのデフォ値を第二引数でnilにすることによって、fetchの返り値がnilなので、raiseするという仕組み。もっといい方法があったら教えてください。


他のタスクをまとめて実行する

invokeでできる。namespaceは必須。

restartとかがわかりやすいですかね。


lib/capistrano/tasks/utils.rake

...

task :restart do
on roles(:app) do
invoke "utils:restart_nginx"
invoke "utils:restart_app"
invoke "utils:restart_mysql"
end
end
task :restart_nginx do
on roles(:app) do
execute "sudo /etc/init.d/nginx restart"
end
end
task :restart_app do
on roles(:app) do
execute "sudo supervisorctl restart all"
end
end
task :restart_mysql do
on roles(:app) do
execute "sudo /etc/init.d/mysql restart"
end
end

※ちなみにsudoが失敗するときのことは後述


連鎖的にタスクを実行する

◯◯の処理のあとに◯◯、みたいなとき。

:add_userのあとに:enable_ssh_loginをやってるけど、:enable_ssh_loginも単体で実行することを考慮するとこうなる。


lib/capistrano/tasks/utils.rake

  task :add_user do

fetch(:user_name) || ask(:user_name, nil)
on roles(:app) do
user_name = fetch(:user_name) || raise("ユーザー名は必須")
execute %[useradd {user_name}]
invoke "utils:enable_ssh_login"
end
end
task :enable_ssh_login do
fetch(:user_name) || ask(:user_name, nil)
on roles(:app) do
user_name = fetch(:user_name) || raise("ユーザー名は必須")
execute %[mkdir -p /home/#{user_name}/.ssh]
execute %[cp /root/.ssh/authorized_keys /home/#{user_name}/.ssh/]
execute %[chmod 700 /home/#{user_name}/.ssh]
execute %[chmod 600 /home/#{user_name}/.ssh/authorized_keys]
execute %[chown -R #{user_name}.#{user_name} /home/#{user_name}/.ssh]
end
end

まずfetchしてnilならaskする。これなら、invoke先のaskは実行されない仕組み。便利。


sudoが失敗する

INFO[29ab7cc8] Running /usr/bin/env sudo whoami on +LHKFlkL+FHLEWJF

DEBUG[29ab7cc8] Command: sudo whoami
DEBUG[29ab7cc8] sudo
DEBUG[29ab7cc8] :
DEBUG[29ab7cc8] sudo を実行するには tty がなければいけません。すみません
DEBUG[29ab7cc8]
cap aborted!

なんか謝られたけど、ttyがないとだめとのこと。

cap3は以下のようにすると設定できる。


config/deploy.rb

lock '3.2.1'

set :pty, true

これでOK。


sshkitを使いこなす

capのDSLのonとかの部分って実はsshkitそのもの。

これがキモ。

詳しくは以下を参照とうい感じなのですが、正直これを知ると知らないとではコマンド実行のバリエーションに大きな影響が出る。

ちょっと触ってみる。


lib/capistrano/tasks/utils.rake

  task :other_whoami do

on roles(:app) do
execute :whoami
as :fuga do
execute :whoami
end
end
end

こうすると一つ目のexecuteだとhoge, 2つ目のexecuteでfugaと表示される。

ちなみに実際に使用されるコマンドがそれぞれ以下のようになる。

# 最初のexecute

/usr/bin/env whoami

# 2つ目のexecute
/usr/bin/env if ! sudo -u fuga whoami > /dev/null; then echo "You cannot switch to user 'fuga' using sudo, please check the sudoers file" 1>&2; false; fi on example.com

勉強になりますね。

あと実行するディレクトリを限定するwithinとか、複数ホストパラレル実行するか、シーケンシャルに実行するか、間隔を何秒あけて実行するとか、execute時のコマンドを独自に定義するとかエロエロできるので試してみるといいです。


download & upload

実はこれもsshkit自体の機能で実現できる。

まずはdownload。

maillogをdownloadしたくて、一度ifでファイルがあるかどうか確認してから実行してる。この例だとカレントディレクトリにそのまま落としてきてる。


lib/capistrano/tasks/utils.rake

  task :download_maillog do

on roles(:app) do
file_name = "/var/log/maillog"
if test "[ -f #{file_name} ]"
download! file_name, '.'
end
end
end

次はupload。


lib/capistrano/tasks/utils.rake

  task :upload_dir do

on roles(:app) do
execute :mkdir, '-p', '/home/hoge/cap_sample'
upload! "./Gemfile", '/home/hoge/cap_sample/Gemfile'
execute :ls, '/home/hoge/cap_sample'
end
end

ディレクトリごとuploadもできる。


lib/capistrano/tasks/utils.rake

  task :upload_dir_all do

on roles(:app) do
execute :mkdir, '-p', '/home/hoge/cap_sample'
upload! '.', '/home/hoge/cap_sample', recursive: true
execute :tree, '-L 2 /home/hoge/cap_sample'
end
end

楽ちん♪


まとめ

開発で一時的に使うサーバーの場合は、プロビジョニングするつもりまではないが、ふとしたタイミングで入れたいツールとか、一度だけ実行したいコマンドがあるとか、そういうことはたくさんありそう。

ansibleなどのツールを使ってもいいけど、そのためにロールを書くのもだるい。手元にRubyの環境があればいいだけなので、デプロイまでいかずともshellでやりたくもないことを記述するならcapはちょうどいいDSLかもしれない。

(あ、でもansibleのinlinefileみたいなやつ超便利だなぁ…)