Bash スクリプトから複数サーバに対して ssh を発行するときの備忘録

株式会社オズビジョンのユッコ (@terra_yucco) です。
今日は業務内に遭遇した bash の小ネタ。

# 複数サーバに対して ssh を発行するときの備忘録
## 単純な場合

- サーバ名は `hoge01` から `hoge10` までの連番とする
- 役割が同じで、鍵ファイルも同一

### スクリプト版

```terminal:script1.sh
$ cat script1.sh
#!/bin/bash -u
for i in $(seq 1 10); do
  ssh user@hoge$( printf %02d ${i} ) "openssl version"
done
$
$ bash script1.sh
OpenSSL 1.0.1k-fips 8 Jan 2015
OpenSSL 1.0.1k-fips 8 Jan 2015
OpenSSL 1.0.1k-fips 8 Jan 2015
OpenSSL 1.0.1k-fips 8 Jan 2015
OpenSSL 1.0.1k-fips 8 Jan 2015
OpenSSL 1.0.1k-fips 8 Jan 2015
OpenSSL 1.0.1k-fips 8 Jan 2015
OpenSSL 1.0.1k-fips 8 Jan 2015
OpenSSL 1.0.2k-fips  26 Jan 2017
OpenSSL 1.0.2k-fips  26 Jan 2017
$
```

### コマンドライン版

```terminal:CUIからのワンライナー
$ for i in $(seq 1 10); do ssh user@hoge$( printf %02d ${i} ) "openssl version"; done
OpenSSL 1.0.1k-fips 8 Jan 2015
OpenSSL 1.0.1k-fips 8 Jan 2015
OpenSSL 1.0.1k-fips 8 Jan 2015
OpenSSL 1.0.1k-fips 8 Jan 2015
OpenSSL 1.0.1k-fips 8 Jan 2015
OpenSSL 1.0.1k-fips 8 Jan 2015
OpenSSL 1.0.1k-fips 8 Jan 2015
OpenSSL 1.0.1k-fips 8 Jan 2015
OpenSSL 1.0.2k-fips  26 Jan 2017
OpenSSL 1.0.2k-fips  26 Jan 2017
$
```

## サーバ名が連番でなくて鍵ファイルも違うような場合

例えば

- key01:hoge01
- key02:fuga02
- key03:foo03
- key04:bar04

### 最初の 1 台しか処理されない書き方

こんな場合は (bash らしくはないのかもしれないが) 例えば変動が少ないならこんな風にしたくなる。
でも、この書き方をするとなぜか最初の 1 台分の結果しか取れない。
これにすごく長時間嵌った。

```terminal:script2.sh
$ cat script2.sh
#!/bin/bash -u
readonly servers=$( cat << EOS
  key01 hoge01
  key02 fuga02
  key03 foo03
  key04 bar04
EOS
)

echo "${servers}" | while read key name; do
  ssh -i ~/.ssh/${key} user@${name} "openssl version"
done
$
$ bash script2.sh
OpenSSL 1.0.1k-fips 8 Jan 2015
$
```

### なぜそうなるのか

これに対する回答が stackoverflow にあった。

- [ssh breaks out of while-loop in bash [duplicate]](https://stackoverflow.com/questions/9393038/ssh-breaks-out-of-while-loop-in-bash)

>The problem is that ssh reads from standard input, therefore it eats all your remaining lines. You can just connect its standard input to nowhere:
`ssh $USER@$SERVER "cd ${REMOTE_PATH}; mkdir -p $i" < /dev/null`
You can also use ssh -n instead of the redirection.

ssh は標準入力から読み取りを行うので、残りの行全てを飲み込んでしまう。
なので、標準入力をどこでもないところに繋いであげる必要がある。
また、リダイレクトではなく `ssh -n` でもよい。

### 解消策

なので、今回のスクリプトはこのようにすればよかった。

```terminal:script2_improved.sh
$ cat script2_improved.sh
#!/bin/bash -u
readonly servers=$( cat << EOS
  key01 hoge01
  key02 fuga02
  key03 foo03
  key04 bar04
EOS
)

echo "${servers}" | while read key name; do
  ssh -n -i ~/.ssh/${key} user@${name} "openssl version"
done
$
$ bash script2_improved.sh
OpenSSL 1.0.1k-fips 8 Jan 2015
OpenSSL 1.0.2k-fips  26 Jan 2017
OpenSSL 1.0.2k-fips  26 Jan 2017
OpenSSL 1.0.1k-fips 8 Jan 2015
$
```

`-n ` という 3byte の威力、おそるべし。

### man の記載

#### バージョン情報

```
$ ssh -V
OpenSSH_6.2p2, OpenSSL 1.0.1e-fips 11 Feb 2013
```

#### 記載内容

※読みやすいように段落調整しました。

>`-n`
Redirects stdin from /dev/null (actually, prevents reading from stdin).  This must be used when ssh is run in the background.  A common trick is to use this to run X11 programs on a remote machine.  For example, ssh -n shadows.cs.hut.fi emacs & will start an emacs on shadows.cs.hut.fi, and the X11 connection will be automatically forwarded over an encrypted channel.  The ssh program will be put in the background.  (This does not work if ssh needs to ask for a password or passphrase; see also the -f option.)

標準入力を `/dev/null` からのものにリダイレクトする。(実際には stdin からの読み込みを妨げる)
バックグラウンドで ssh を走らせるときに使わなければならない。
これを使うよくあるケースとしては、X11 のプログラムをリモートマシン上で動かす場合。
例えば `ssh -n shadows.cs.hut.fi emacs` としたのちに shadows.cs.hut.fi 上で emacs を起動すると、X11 接続が暗号化されたチャネルを通って自動的に連携される。ssh プログラウはバックグラウンドに配置される。(ssh でパスワードやパスフレーズ確認が必要な場合には機能しない、`-f` のオプションも参照のこと)

## Conclusion

コードの構造自体は間違っていないはずなのに！としばらく悩みましたが、これで次からは迷わないで済みそうです。