SSH経由でコマンドを並列実行するツールまとめ

# SSH経由でコマンドを並列実行するツール

複数台のサーバに対して、SSH経由でコマンドを並列実行するためのツールのまとめです。たくさんのサーバを一度に操作したい、そんなときに便利なツールの紹介です。

## CUI(コマンド)系

- parallel-ssh (pssh)
    - http://code.google.com/p/parallel-ssh/
- Fabric
    - http://www.fabfile.org/
- Ansible
    - http://www.ansible.com/

## GUI(ブラウザ)系

- Jenkins
    - https://jenkins-ci.org/
- Rundeck
    - http://rundeck.org/

## ライブラリ

- net-ssh
    - https://github.com/net-ssh/net-ssh

# 各ツールの特徴
客観的なツール紹介はGoogleとかで検索した方がより正確なので、ここでは主観的な使ってみた感想で紹介します。

## parallel-ssh (pssh)
- **事前準備ほぼなしで、１つのコマンドを並列実行したいときに便利。**
- sshの並列版なイメージで、すぐ使えます。
- 同時実行数の指定可能。
- 標準出力が実行が終わった順。順番が、固定じゃないのは注意。
- 複数のコマンドを実行する場合、ひたすらpssh実行なので手数が多い場合は他ツールを使った方が楽かも。
- **ここ数年更新されていない？**

## Fabric
- **よく実行するコマンドの組み合わせを、効率的に実行したいときに便利。**
- コマンドは事前にfabfileにタスクとして書いておく。
- デフォルトだと１台ずつ直列に実行だけど、オプション指定で並列実行。
- 同時実行数の指定可能。
- 並列実行の場合、**各サーバの結果がリアルタイムに混ざって表示される**。(行頭に対象記載あるので区別はできるけど…)
- fabfileはpythonで書くので、pythonの知識必要。

## Ansible
- **１つのツールでコマンド実行だけでなく、deployment, configuration management, orchestration全部やりたい人向けなツール。**
- ワンライナーでのコマンド実行や、playbookにタスクを書いて実行するの両方ができます。
- 最低限/etc/ansible/hostsにサーバグループの定義が必要
- 同時実行数の指定可能。
- いろいろできるので、全部知ろうとすると結構大変そう。使いたい機能だけ使うのスタンスで。

## Jenkins

- **アプリ開発用のCIツールですが、SSH pluginを使えばリモートサーバへのSSH実行もできます**。
- 実行コマンドは、事前にjobとして登録が必要。
- jobの作り方によりますが、結果を素早く見るのに向いていないです。
- どちらかというとコマンドを定期実行する(cronみたいに)用途向け。
- 同時実行数の指定可能(同時ビルド数)。
- 他のCUIツールをJenkinsから実行して、Jenkinsがもっている次のメリットを追加する的な使い方の方が多いかも。(他ツールのラッパーとしてJenkinsを使う)
   - ウェブ画面から実行
   - 実行ユーザ履歴を残す
   - (Jenkinsで実行したジョブの)実行ログを残す
   - 定期実行
   - エラーだったらメールを投げたり
- **本来CIツールなので、そのあたりは割り切りが必要**。

## Rundeck
今回初めて触った感想です。

- **サーバでのコマンド実行がしやすいジョブスケジューリングツールです。**
- Jenkinsと同じく次のようなことがやりたい場合、便利です。
   - ウェブ画面から実行
   - 実行ユーザの履歴を残す
   - 実行ログを残す
   - 定期実行
   - エラーだったらメールを投げたり
- 画面が今どきのツールぽくてきれいです。
- **なぜかweb画面から対象サーバが追加できない？**Project作った後、XMLファイルをエディタで編集して追加するようです。
- 正規表現で指定した対象サーバに、任意のコマンドを投げる簡易実行機能あり。
- **コマンド実行対象のサーバを、正規表現でフィルターできるのが便利**。設定ファイルにあらかじめ書いておいたサーバを、正規表現でフィルターする感じです。
- ジョブ登録後？は同時実行数の指定可能。
- **ちょっと触っただけですが設定方法に一貫性がない気がします**。対象サーバとかはエディタで編集、Projectの設定はJava propertiesを画面から編集、SSH接続用のパスワードは全体設定にあるKey Storageにあらかじめキーと値の登録が必要とか。
- ~~Jenkinsに比べて遅い気がします。~~（遅かったのは、試したときに意図せず低速な共有領域で動かしてたせい。ちょっと使った程度だとレスポンスに差はないみたい）
- 日本語対応弱いらしいです。

## net-ssh
- **Ruby用の手軽なSSH接続用のライブラリです**。
- 結果をいろいろ加工したり、集計したりする場合は自前でスクリプト書いた方が早い場合もあるので、そんなときに便利なライブラリです。
- **単体だとサーバの並列実行はできないです**。なので並列実行する場合、Rubyを頑張って書かないとだめ。
- 簡易メソッド(Net::SSH::Connection::Session#exec!)はコマンド結果の文字列は取得できるけど、exit statusは取れない。
- exit statusも使う場合、次のリンク先参照。

[How to get exit status with Ruby's Net::SSH library? - Stack Overflow](http://stackoverflow.com/questions/3386233/how-to-get-exit-status-with-rubys-netssh-library)

※ちなみにAnswerのコメントによると、exit_signalは文字列らしく、Answerのメソッドの一部は、

```rb
      channel.on_request("exit-signal") do |ch, data|
        exit_signal = data.read_long
      end
```

ではなく、こっちになるようです。

```rb
      channel.on_request("exit-signal") do |ch, data|
        exit_signal = data.read_string
      end
```

# 各ツールの実行結果
せっかくなので各ツールでコマンドを実行したときの結果です。
結果の見え方は、人によって好き嫌いあるかなと思うので、参考になればと思います。
結果が１行だけのコマンドだとつまらないので、とりあえずlocalhostにpingを３回投げてみます。
対象サーバは４台(db01,web01,web02,web03)で、並列実行数が制限できるツールについては２に設定しています。

## parallel-ssh
コマンド自体はシンプルです。
標準出力での結果は、終わった順のようです。

```
[root@console pssh-2.3.1]# pssh --version
2.3.1

[root@console pssh-2.3.1]# pssh -H db01 -H web01 -H web02 -H web03 -i -A -p 2 -O StrictHostKeyChecking=no ping -c 3 localhost                                                                  Warning: do not enter your password if anyone else has superuser
privileges or access to your account.
Password: 
[1] 14:48:14 [SUCCESS] web01
PING localhost (127.0.0.1) 56(84) bytes of data.
64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.031 ms
64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.060 ms
64 bytes from localhost (127.0.0.1): icmp_seq=3 ttl=64 time=0.047 ms

--- localhost ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1999ms
rtt min/avg/max/mdev = 0.031/0.046/0.060/0.011 ms
[2] 14:48:14 [SUCCESS] db01
PING localhost (127.0.0.1) 56(84) bytes of data.
64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.026 ms
64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.020 ms
64 bytes from localhost (127.0.0.1): icmp_seq=3 ttl=64 time=0.033 ms

--- localhost ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1998ms
rtt min/avg/max/mdev = 0.020/0.026/0.033/0.006 ms
[3] 14:48:26 [SUCCESS] web03
PING localhost (127.0.0.1) 56(84) bytes of data.
64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.029 ms
64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.049 ms
64 bytes from localhost (127.0.0.1): icmp_seq=3 ttl=64 time=0.043 ms

--- localhost ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1999ms
rtt min/avg/max/mdev = 0.029/0.040/0.049/0.009 ms
[4] 14:48:26 [SUCCESS] web02
PING localhost (127.0.0.1) 56(84) bytes of data.
64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.027 ms
64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.033 ms
64 bytes from localhost (127.0.0.1): icmp_seq=3 ttl=64 time=0.025 ms

--- localhost ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1998ms
rtt min/avg/max/mdev = 0.025/0.028/0.033/0.005 ms
```

## Fabric
事前にfabfile.pyでタスクの定義が必要です。
並列実行すると各サーバの結果がリアルタイムに混ざって表示されます。

```
[root@console ~]# cat fabfile.py
from fabric.api import run

def ping_localhost():
    run('ping -c 3 localhost')

[root@console ~]# fab --version
Fabric 1.10.2
Paramiko 1.15.1

[root@console ~]# fab --list
Available commands:

    ping_localhost

[root@console ~]# fab -H db01,web01,web02,web03 -P -z 2 -p password ping_localhost
[db01] Executing task 'ping_localhost'
[web01] Executing task 'ping_localhost'
[web02] Executing task 'ping_localhost'
[web03] Executing task 'ping_localhost'
[web03] run: ping -c 3 localhost
[web02] run: ping -c 3 localhost
[web02] out: PING localhost (127.0.0.1) 56(84) bytes of data.
[web02] out: 64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.020 ms
[web03] out: PING localhost (127.0.0.1) 56(84) bytes of data.
[web03] out: 64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.032 ms
[web03] out: 64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.030 ms
[web02] out: 64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.030 ms
[web03] out: 64 bytes from localhost (127.0.0.1): icmp_seq=3 ttl=64 time=0.046 ms
[web03] out: 
[web03] out: --- localhost ping statistics ---
[web03] out: 3 packets transmitted, 3 received, 0% packet loss, time 1998ms
[web03] out: rtt min/avg/max/mdev = 0.030/0.036/0.046/0.007 ms
[web02] out: 64 bytes from localhost (127.0.0.1): icmp_seq=3 ttl=64 time=0.037 ms
[web03] out: 
[web02] out: 
[web02] out: --- localhost ping statistics ---

[web02] out: 3 packets transmitted, 3 received, 0% packet loss, time 1998ms
[web02] out: rtt min/avg/max/mdev = 0.020/0.029/0.037/0.007 ms
[web02] out: 

[web01] run: ping -c 3 localhost
[db01] run: ping -c 3 localhost
[web01] out: PING localhost (127.0.0.1) 56(84) bytes of data.
[web01] out: 64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.045 ms
[db01] out: PING localhost (127.0.0.1) 56(84) bytes of data.
[db01] out: 64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.033 ms
[db01] out: 64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.030 ms
[web01] out: 64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.031 ms
[web01] out: 64 bytes from localhost (127.0.0.1): icmp_seq=3 ttl=64 time=0.116 ms
[db01] out: 64 bytes from localhost (127.0.0.1): icmp_seq=3 ttl=64 time=0.035 ms
[web01] out: 
[web01] out: --- localhost ping statistics ---
[db01] out: 
[web01] out: 3 packets transmitted, 3 received, 0% packet loss, time 1998ms
[db01] out: --- localhost ping statistics ---
[web01] out: rtt min/avg/max/mdev = 0.031/0.064/0.116/0.037 ms
[db01] out: 3 packets transmitted, 3 received, 0% packet loss, time 1998ms
[db01] out: rtt min/avg/max/mdev = 0.030/0.032/0.035/0.006 ms
[web01] out: 
[db01] out: 



Done.
```

## Ansible
コマンド実行だけでも、最低限/etc/ansible/hostsでサーバのグループ定義が必要です。
標準出力は、サーバの順番通り？結果は各サーバごと。

```
[root@console ~]# ansible --version               
ansible 1.9.4
  configured module search path = None

[root@console ~]# cat /etc/ansible/hosts
[local]
localhos

[servers]
db01
web01
web02
web03

[root@console ~]# ansible servers -k -f 2 -m shell -a 'ping -c 3 localhost'                                                                                                                   
SSH password: 
db01 | success | rc=0 >>
PING localhost (127.0.0.1) 56(84) bytes of data.
64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.033 ms
64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.036 ms
64 bytes from localhost (127.0.0.1): icmp_seq=3 ttl=64 time=0.041 ms

--- localhost ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1998ms
rtt min/avg/max/mdev = 0.033/0.036/0.041/0.007 ms

web01 | success | rc=0 >>
PING localhost (127.0.0.1) 56(84) bytes of data.
64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.028 ms
64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.043 ms
64 bytes from localhost (127.0.0.1): icmp_seq=3 ttl=64 time=0.036 ms

--- localhost ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1999ms
rtt min/avg/max/mdev = 0.028/0.035/0.043/0.009 ms

web02 | success | rc=0 >>
PING localhost (127.0.0.1) 56(84) bytes of data.
64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.034 ms
64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.032 ms
64 bytes from localhost (127.0.0.1): icmp_seq=3 ttl=64 time=0.041 ms

--- localhost ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2004ms
rtt min/avg/max/mdev = 0.032/0.035/0.041/0.007 ms

web03 | success | rc=0 >>
PING localhost (127.0.0.1) 56(84) bytes of data.
64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.029 ms
64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.033 ms
64 bytes from localhost (127.0.0.1): icmp_seq=3 ttl=64 time=0.034 ms

--- localhost ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1998ms
rtt min/avg/max/mdev = 0.029/0.032/0.034/0.002 ms

```

## Jenkins
Jenkinsだけでやろうとすると設定が面倒なので、SSH実行の部分は他のCUI系のツールで作っておいて、それをJenkinsから実行する形にした方が楽です。
が、今回はJenkins + Jenkinsのプラグインだけでやる方法です。

### 簡単な設定紹介
- Jenkinsの管理->プラグインの管理
   - SSH pluginをインストール
- Jenkinsの管理->システムの設定->同時ビルド数
   - 2(初期設定のまま)
- Jenkinsの管理->システムの設定->SSHリモートホスト
   - 接続先のdb01,web01,web02,web03の接続情報を設定

### ジョブ定義
- フリースタイル・プロジェクトのビルドで各サーバでSSH実行するジョブを作成
- SSH実行は、ビルドのリモートホストでシェル実行
- 各サーバ用ジョブのトリガーとして、ジョブを作ってビルド後の処理に他プロジェクトで、さっきのサーバごとのジョブを実行するようにしました。

### 実行結果

結果の全体です。各ジョブのS列で成功、失敗がわかります。

トリガー用親ジョブ:ping_test_all
各サーバ実行用ジョブ:ping_test_db01, ping_test_web01, ping_test_web02, ping_test_web03

![スクリーンショット 2016-02-06 10.04.24.png](https://qiita-image-store.s3.amazonaws.com/0/95528/4c96c14d-66bc-0350-0777-2193d149d569.png)

各サーバの実行結果は、それぞれのジョブをクリックして、中央下にある
「最新のビルド」をクリックして、左側の「コンソール出力」をクリックすると見れます。

![スクリーンショット 2016-02-06 10.05.26.png](https://qiita-image-store.s3.amazonaws.com/0/95528/4d4fcd0e-18ce-b51c-597a-9223ec306004.png)

他のサーバの結果は省略です。

## Rundeck

設定内容とかは説明長くなって大変なので、すみませんが割愛します。

Reportで各サーバの結果の一覧がみれます。　All Steps OKです。

![スクリーンショット 2016-02-06 17.57.56.png](https://qiita-image-store.s3.amazonaws.com/0/95528/b150e0c3-6fd0-8dfc-8dab-cdfbc94d49ac.png)

各サーバのツリーを開いていくと、各サーバのコマンド結果が見れます。ただし、同時に見れるサーバの結果は１個だけのようです。

![スクリーンショット 2016-02-06 17.58.15.png](https://qiita-image-store.s3.amazonaws.com/0/95528/2e9d6f47-ef37-bad0-97e6-aa430c11df0e.png)

コマンド結果の時系列や、全サーバのコマンド結果を見たい場合Log Outputを見ます。
デフォルトだと、全サーバの結果が時系列で混ざっています。

![スクリーンショット 2016-02-06 17.58.31.png](https://qiita-image-store.s3.amazonaws.com/0/95528/3b8fb3e9-0024-2f23-acb3-a19586b67b32.png)

サーバごとに表示したい場合は、View OptionsのBy Nodeをチェックします。こっちだと全サーバの結果を一度に表示できます。

![スクリーンショット 2016-02-06 17.59.00.png](https://qiita-image-store.s3.amazonaws.com/0/95528/57af7917-8a48-d821-fa62-1d7bd69c74bd.png)


## net-ssh
net-sshとparallelを使ってSSHを並列実行するサンプル。
あくまでもサンプルなので、スクリプトは微妙です。

```ruby:netssh_sample.rb
require 'rubygems'
require 'net/ssh'
require 'parallel'

servers = ['db01', 'web01', 'web02', 'web03']
output = {}

Parallel.each(servers, in_threads: 2) do |server|
  Net::SSH.start(server, 'root', :password => 'password') do |ssh|
    output[server] = ssh.exec!('ping -c 3 localhost')
  end
end

output.each do |server, ret|
  puts server + '-'*30
  puts ret
end
```

```
[root@console ~]# ruby netssh_sample.rb
web01------------------------------
PING localhost (127.0.0.1) 56(84) bytes of data.
64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.035 ms
64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.044 ms
64 bytes from localhost (127.0.0.1): icmp_seq=3 ttl=64 time=0.052 ms

--- localhost ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1998ms
rtt min/avg/max/mdev = 0.035/0.043/0.052/0.010 ms
db01------------------------------
PING localhost (127.0.0.1) 56(84) bytes of data.
64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.029 ms
64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.048 ms
64 bytes from localhost (127.0.0.1): icmp_seq=3 ttl=64 time=0.035 ms

--- localhost ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2002ms
rtt min/avg/max/mdev = 0.029/0.037/0.048/0.009 ms
web03------------------------------
PING localhost (127.0.0.1) 56(84) bytes of data.
64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.035 ms
64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.035 ms
64 bytes from localhost (127.0.0.1): icmp_seq=3 ttl=64 time=0.039 ms

--- localhost ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1998ms
rtt min/avg/max/mdev = 0.035/0.036/0.039/0.005 ms
web02------------------------------
PING localhost (127.0.0.1) 56(84) bytes of data.
64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.033 ms
64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.034 ms
64 bytes from localhost (127.0.0.1): icmp_seq=3 ttl=64 time=0.042 ms

--- localhost ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1998ms
rtt min/avg/max/mdev = 0.033/0.036/0.042/0.006 ms

```

# あとがき

上記ツールについては、どれが一番とかはないかなと思います。それぞれ場面によって、使いやすい・使いにくいがあるので、用途にあったツールを選ぶ参考にしていただけたらうれしいかなと。
ちなみに、個人的には、選んだ時期もあってparallel-ssh、Jenkins、net-sshはよく使っているので、感想の偏りがあるかもしれないです。