ターミナルでリモートサーバーのMySQLに接続するシェルスクリプト

日ごろ、ステージングサーバー、本番サーバー、社内サーバーと複数サーバーのデータベースにSSH接続することがあります。

知識人にポートフォワーディングのやり方を教えてもらい自作のシェルスクリプトで接続していたのですが、何回もサーバーに接続すると前回接続したポートが解放されないことがありました。

[コマンド実行中だけSSHポートフォワーディングを有効化する](https://qiita.com/hmkz/items/8884ce70c13338eb2681) を読んでこれだ！と思ったので、ブラッシュアップした自作シェルスクリプトを紹介します。

# 対象読者

シェルスクリプトあまりよく知らないし、詳しい解説よりもコピペで簡単に接続できる即効性を求めてます、という自分と同じ人。

# 概要

構造はこんな感じです。サーバーと踏み台が複数あります。

![overview.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102284/15295ba8-c936-2432-a010-2cf18cc9e599.png)


サーバー1に接続したつもりで、うっかりサーバー2に対してUPDATEとかDELETEすると事故るので「本番サーバーに接続する」ことや「本番サーバーのデータベースを操作している」ことがわかると嬉しい感じです。

＜シェルスクリプトを動かす環境＞

- SSH
  - 公開鍵認証でサーバーにアクセスできることを前提とします。
- ZSH
  - 接続先の候補選択にファイル補完を利用します。
- MySQLクライアントツール
  - サジェストなど表示する [mycli](https://www.mycli.net/) を使っていますが、要らない場合はMySQLコマンドに読み替えてください。

＜デモ画面＞

`sdb-` をタイプして、タブキーで接続先サーバーを選択します。

![1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102284/be165cb2-a010-90ee-b4ba-8b076440569e.gif)


本番サーバーに接続する時はうっかり操作を防ぐため戒めのメッセージを表示し、確認の `y` をタイプして接続します。パスワードはサーバーのデータベースが要求するものです。

![2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102284/16d714d8-b3e4-4937-9b47-bba87f934d27.gif)


# 踏み台サーバーを設定する

SSHの設定ファイルに踏み台サーバーを記述します。

```shell:~/.ssh/config
Host fumidai1
  Hostname 192.168.1.100
  User user1
  Port 22
  StrictHostKeyChecking=no

Host fumidai2
  Hostname 192.168.1.200
  User user1
  Port 22
  StrictHostKeyChecking=no
```

# シェルスクリプトを作成する

任意のフォルダを作り `sdb-{サーバー名}` という名前のシェルスクリプトを作成します。「**S**SHで**DB**接続」の安易なネーミングなのでプレフィクスは適当に変更してください。

```shell
mkdir ~/sdb && cd $_
touch sdb-foo
```

シェルスクリプトの中身を転記します。

```shell:sdb-foo
#!/bin/bash

PROMPT=foo # mycliのプロンプト、操作中のデータベースやサーバーを判断する任意のテキスト
REMOTE_PORT=4000 # SSH接続のポート、空いているローカルポートならなんでも良い
LOCAL_PORT=3306 # mycliのポート
DB_NAME=foo # 接続するデータベース
REMOTE_HOST=192.168.1.101 # 本番サーバーのアドレス
USER_NAME=user1 # データベースのログインユーザー

# 戒めのメッセージ
echo -e "(esc)[37;41m === CONNECTS TO PRODUCTION SERVER ===(esc)[m"
echo -e "(esc)[31m If the process is aborted, kill port ${REMOTE_PORT} manually.(esc)[m"

# 確認キーのyを要求
read -p "(esc)[31m Do you want to continue? (y/n): (esc)[m" CONFIRM
if [ "${CONFIRM}" != "y" ]; then
  echo "Goodbye!"
  exit 1
fi

# SSH接続
ssh -NL $REMOTE_PORT:$REMOTE_HOST:$LOCAL_PORT fumidai1 &

# バックグラウンド実行のプロセスIDを格納
PID=$!

# mycliでデータベースに接続、タイムラグがないとうまく接続できなかったので1秒待機
sleep 1
mycli -u $USER_NAME -D $DB_NAME -h 127.0.0.1 -P $REMOTE_PORT --prompt="(esc)[31m $PROMPT \R:\m > (esc)[m"

# mycliを抜けたらポートを解放
kill $PID
```

`(esc)` は実際にはbash上で制御キー（エスケープ）が入力されています。コピーして使う時は入力しなおしてください。viのINSERTモードで `Ctrl + Q` に続けて `esc` をタイプし、続けて `esc` で抜けると入力できます。
![escape](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102284/09fcebd5-7315-f85b-f816-fd09e6b9cc71.png)

ファイルをコピーして設定値を変更すれば、他のサーバー用に展開できます。

```shell:sdb-foo-stage
PROMPT=foo-stage # プロンプトを変更
REMOTE_PORT=4001 # ポート番号をずらしたほうが同時接続する時に便利
LOCAL_PORT=3306
DB_NAME=foo
REMOTE_HOST=192.168.1.201 # サーバーのアドレスを変更
USER_NAME=user1
```

戒めは接続先サーバーに合わせてゆるめにアレンジできます。

```shell:sdb-foo-stage
# 黄色の文字
echo -e "(esc)[33m === CONNECTS TO STAGE SERVER ===(esc)[m"
echo -e "(esc)[33m If the process is aborted, kill port ${REMOTE_PORT} manually.(esc)[m"

# 確認キーのyを要求しない
# read -p "(esc)[31m Do you want to continue? (y/n): (esc)[m" CONFIRM
# if [ "${CONFIRM}" != "y" ]; then
#   echo "Goodbye!"
#   exit 1
# fi
```

踏み台が異なる時はsshコマンドの指定を変更します。

```shell
# fumidai2を使う
ssh -NL $REMOTE_PORT:$REMOTE_HOST:$LOCAL_PORT fumidai2 &
```

# シェルスクリプトのパスを通す

実行権限を付与します。

```shell
chmod +x ./sdb-* 

ls -la
> -rwxr-xr-x   1 ringtail003  staff  555  5  6 sdb-bar
> -rwxr-xr-x   1 ringtail003  staff  669  5  6 sdb-foo
> -rwxr-xr-x   1 ringtail003  staff  669  5  6 sdb-foo-stage
```

ZSHがシェルスクリプト群を探せるよう、フォルダのパスを通します。

```shell:~/.zshrc
export PATH="$PATH:$HOME/sdb"
```

ZSHの設定を再読み込みします。

```shell
source ~/.zshrc
rehash
```

# データベースに接続する

ターミナルで `sdb-` とタイプして、接続するサーバーを選択します。

```shell
sdb-

> sdb-foo
> sdb-foo-stage
> sdb-bar
```

なにかSQLを実行してみましょう。

```sql
foo 17:30 > select * from user;
+---------+-------------------------+
| id      | email                   |
+---------+-------------------------+
| 1000000 | testuser1@example.com   |
| 1000001 | testuser2@example.com   |
+---------+-------------------------+
```

接続中に `lsof` コマンドを叩くと、同じポートを使用するプロセスがいくつか起動しています。上2行がSSH接続、下2行はmycliのプロセスを示しています。

```shell
> === CONNECTS TO STAGE SERVER ===
> If the process is aborted, kill port 4001 manually.
```

```shell
lsof -i:4001

> COMMAND   PID        USER   FD   TYPE     DEVICE SIZE/OFF NODE NAME
> ssh     11111 ringtail003    5u  IPv6 0xaabb1111      0t0  TCP localhost:terabase (LISTEN)
> ssh     11111 ringtail003    6u  IPv4 0xaabb1111      0t0  TCP localhost:terabase (LISTEN)
> ssh     11111 ringtail003    7u  IPv4 0xaabb1111      0t0  TCP localhost:terabase->localhost:51114 (ESTABLISHED)
> Python  22222 ringtail003    4u  IPv4 0xaabb1111      0t0  TCP localhost:51114->localhost:terabase (ESTABLISHED)
```

`exit` でmycliを抜けます。

```sql
foo 17:30 > exit;
Goodbye!
```

mycliを抜けるとSSH接続のポートが解放されます。

```shell
lsof -i:4001
```

シェルスクリプトのシンタックスエラーなどで異常終了するとポートが解放されないので、そんな時は自力でポートを解放します。


```shell
lsof -i:4001

> COMMAND   PID        USER   FD   TYPE     DEVICE SIZE/OFF NODE NAME
> ssh     11111 ringtail003    5u  IPv6 0xaabb1111      0t0  TCP localhost:terabase (LISTEN)

kill 11111
```

# おわりに

セッション切れるのが早かったりとまだ改良の余地はありますが `ssh -fNL foo.example.com && mycli...` とアナログにコマンド叩いていた頃に比べると便利になりました。

良ければコピペでお使いください。