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

  • 176
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

戦略

  • 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みたいなやつ超便利だなぁ…)