awscli + jq + peco + tmux-cssh を使って複数 EC2 インスタンスへ簡単 SSH

## TL;DR

![terminal.gif](https://qiita-image-store.s3.amazonaws.com/0/17508/14be4489-c660-0c06-ad68-1111e33204de.gif)

必要なもの (awscli, jq, peco, tmux, tmux-cssh) をすべて入れた状態で

```bash
$ sh -c "tmux-cssh -i <ssh_key> -u <ssh_user> $(aws ec2 describe-instances | jq -r '.Reservations[] | .Instances[] | select(.State.Name != "terminated") | select(has("PublicIpAddress")) | [.PublicIpAddress,.PrivateIpAddress,.State.Name,(.Tags[] | select(.Key == "Name") | .Value // "")] | join("\t")' | peco | awk '{ print $1 }' | tr '\n' ' ')"
```

とすることで、peco で EC2 インスタンスを選んで同時に SSH アクセスすることができます。

:warning: tmux 上で生活している方は、一度 `tmux detach` して tmux の外に出ないと tmux-cssh が実行できません。
また、ターミナル起動時に自動で tmux を立ち上げる設定は無効化しておく必要があります。

## awscli + jq + peco でインスタンスを選ぶ

まず、peco を使って SSH したいインスタンスを選びます。

```bash
$ aws ec2 describe-instances | jq -r '.Reservations[] | .Instances[] | select(.State.Name != "terminated") | select(has("PublicIpAddress")) | [.PublicIpAddress,.PrivateIpAddress,.State.Name,(.Tags[] | select(.Key == "Name") | .Value // "")] | join("\t")' | peco | awk '{ print $1 }' | tr '\n' ' '
```

長いのでそれぞれ分解してみましょう。

### awscli

```bash
$ aws ec2 describe-instances
```

EC2 インスタンス一覧を JSON で取得する

### jq

```
$ jq -r
```

`-r` でクォート付けずに出力する

以下クエリ部分については、

- `.Reservations[] | .Instances[]`
    - すべてのインスタンス
- `select(.State.Name != "terminated")`
    - Terminate されてないインスタンス
    - （`== "running"` でもよさそう）
- `select(has("PublicIpAddress"))`
    - Public IP がアサインされてるインスタンス
- `[.PublicIpAddress,.PrivateIpAddress,.State.Name,(.Tags[] | select(.Key == "Name") | .Value // "")]`
    - Public IP, Private IP, State, Name タグの値を抽出（なければ空文字列）

```
[
  "54.65.201.60",
  "172.31.10.134",
  "running",
  "web-01"
]
[
  "54.65.201.225",
  "172.31.10.130",
  "running",
  "web-02"
]
...
```

- `join("\t")`
    - 抽出した情報をハードタブで結合する (TSV)

```
54.65.201.60    172.31.10.134   running web-01
54.65.201.225   172.31.10.130   running web-02
...
```

### peco

ここまでで得られた情報をよしなに絞り込んで選択する

```
QUERY>
54.65.201.60    172.31.10.134   running web-01
54.65.201.225   172.31.10.130   running web-02
54.65.205.189   172.31.10.131   running web-03
54.65.230.123   172.31.10.132   running worker-01
54.65.202.2 172.31.10.133   running worker-02
```

### awk

```bash
$ awk '{ print $1 }'
```

タブ区切り文字列のうち、先頭の項目（今回だと Public IP）を抽出

```
54.65.201.60
54.65.201.225
```

### tr

```bash
$ tr '\n' ' '
```

1行1インスタンスの形で出力されているものをスペース区切りに変換

```
54.65.201.60 54.65.201.225
```

## tmux-cssh で選んだインスタンスに同時 SSH

[tmux-cssh](https://github.com/dennishafemann/tmux-cssh) は、tmux を使って複数ホストに同時 SSH するツールです。tmux-cssh 上の入力はすべてのホストに反映されるので、複数ホスト上で同一作業を行いたい場合に有用です（構成管理ツールの話はとりあえず置いといて…）。

### tmux-cssh

```bash
$ tmux-cssh -i <ssh_key> -u <ssh_user> <host01> <host02>
```

`-i` で秘密鍵を指定、`-u` でユーザを指定する。
:warning: 今回はすべてのホストでキーペアとユーザが同じである場合のみ有効です。

### `sh -c` いるの？

`<host01> <host02>` を単純に変数展開とかで渡すと、この部分は tmux-cssh の複数引数とみなされず、1つの引数として解釈されます。ホスト名としてスペース区切りの文字列1個渡される形です。

```bash
$ tmux-cssh -i <ssh_key> -u <ssh_user> '<host01> <host02>'
```

これだと実際に実行される ssh コマンドがおかしくなります。

```bash
$ ssh -i <ssh_key> <ssh_user>@'<host01> <host02>'
```

なので、`tmux-cssh -i -u` の部分も含めて1個の文字列としてみなしたうえで、それ自身を eval する必要があるのです。
これだと

```bash
$ ssh -i <ssh_key> <ssh_user>@<host01>
$ ssh -i <ssh_key> <ssh_user>@<host02>
```

ってなります。


とりあえず思いついたのがこれなので、もっといい方法があるかもしれない。

## おわりに

最近仕事で複数（5個以上）インスタンスに同時 SSH して同じコマンドを流し込みたい、ということがあって面倒だったのでこの方法を考えました。
この記事では最初にワンライナーとして紹介してますが、実際自分は [envchain](https://github.com/sorah/envchain) を使っていることもありシェル関数として定義したものを利用しています。まあワンライナーが長い上に SSH ユーザと鍵を指定する必要があるのでどのみち関数定義しておいたほうが普段使いによいでしょう。

あわせて御覧ください: [dtan4/dot.zsh/.zshrc.peco](https://github.com/dtan4/dot.zsh/blob/995a40e943dfb0e3a8bf6e5ddf5dde29305085b8/.zshrc.peco#L63-L87)