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

## 戦略

- Railsのことは忘れる(そういうこともあるよ)
- capをサーバーにsshでコマンドを送り込むrake taskの塊と見る
- 「とある役割のサーバー」に対して「とある処理」を実行して回りたい、という場合を想定
    - 単にrestartだけ切り出して実行するとか、あるコマンドの結果だけ手に入れるとか
- 個人的にはansibleでもできると思ったけどrubyメンバーに強要できないし、小回り効かせたい

## 準備

```shell-session
$ bundle init
$ echo gem "capistrano" >> Gemfile
$ bundle install
$ cap install
```

ちなみにversionは、

```shell-session
$ cap -V
Capistrano Version: 3.2.1 (Rake Version: 10.3.2)
```

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

```shell-session
$ 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を最低限の設定にする

```ruby:config/deploy.rb
lock '3.2.1'
# set :pty, true
```

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

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

## ステージを増やす

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

```
└─- config
   └── deploy
       ├── production.rb
       ├── staging.rb
       └── dev.rb
```

```ruby:config/deploy/dev.rb
role :app,  %w{hoge@example.com}
```

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

```shell-session
$ cap dev YOUR_TASK_NAME
```

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

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

```ruby: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`というタスクを定義

したことになる。

実行してみる。

```shell-session
$ 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を付けた方があとあと見通しが良くなる。

```ruby: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`というように記述する。

```shell-session
$ 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`というのをつくってみる。

```ruby: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のブロックをまるごとコピペでいい。実行のやりかたも変わらない。

```shell-session
$ 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
```

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

```ruby: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つっこんでもいい)。


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

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

```ruby:lib/capistrano/tasks/utils.rake
...

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

実行する。

```shell-session
$ 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とかがわかりやすいですかね。

```ruby: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`も単体で実行することを考慮するとこうなる。

```ruby: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は以下のようにすると設定できる。

```ruby:config/deploy.rb
lock '3.2.1'
set :pty, true
```

これでOK。

## sshkitを使いこなす

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

これがキモ。

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

- capistrano/sshkit
    - https://github.com/capistrano/sshkit
- sshkit/EXAMPLES.md at master · capistrano/sshkit
    - https://github.com/capistrano/sshkit/blob/master/EXAMPLES.md
- Capistrano 3への手引き - 今日のごはんは素麺です
    - http://takkkun.hatenablog.com/entry/2013/10/12/Capistrano_3%E3%81%B8%E3%81%AE%E6%89%8B%E5%BC%95%E3%81%8D

ちょっと触ってみる。

```ruby: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, ２つ目のexecuteでfugaと表示される。

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

```shell-session
# 最初のexecute
/usr/bin/env whoami

# ２つ目の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でファイルがあるかどうか確認してから実行してる。この例だとカレントディレクトリにそのまま落としてきてる。

```ruby: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。

```ruby: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もできる。


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