ProxyJumpでhostnameを解決しながら踏み台サーバーをジャンプ

# なにこれ
最近こんな案件に当たりました

- サーバーの種類がかなりの台数ある上に、踏み台経由じゃないとアクセスできない
- 認証鍵もポートもユーザー名も全部バラバラ
- 踏み台サーバーは定期的に再生成されIPがたまに変わる

・・・お堅い仕事は結構ですが、とにかくサーバーあたりの情報指定が多くssh/confignが汚染されるのが嫌なので、別ファイルを使ってsshを叩けるようにしたい！と思ったのですが、適当に`ProxyCommand`書いても`ssh: Could not resolve hostname`が出て詰まったので調査がてら勉強しなおしました。
sshの基本についてもついでに書くので、本題にしか興味がない人は[本題](https://qiita.com/mochisuna/items/375b621b3281825d8a97#proxyjump%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E8%B8%8F%E3%81%BF%E5%8F%B0%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E7%B5%8C%E7%94%B1%E3%81%A7%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E3%81%99%E3%82%8B)まで飛ばしてください。

~~最近忙しすぎて年単位で記事書けてなかった＆保守もできてなかったので、記事を書くリハビリを兼ねます~~

# sshでサーバーにアクセス
## 単純にsshコマンドでアクセスする
まずは基本に立ち返って地道にsshします（本題以前の話です。興味ない人は飛ばしてください）
Portが`2222`が空いているIP`192.168.0.1`のサーバーに`mochisuna`という名前でアクセスしたい場合、

1. 公開鍵/秘密鍵を生成（クソ雑ネーム`ssh_my_key`）  
  例：`ssh-keygen -t ed25519 -C "hirokazu.maruta@gmail.com" -f ssh_my_key`
2. 接続先サーバー（`192.168.0.1`）の`~/.ssh/authorized_keys`に公開鍵を追記

を設定して以下のようなコマンドを打つことで、そのサーバーにアクセスすることができるようになります。

```bash
ssh -i ~/.ssh/ssh_my_key mochisuna@192.168.0.1 -p 2222
```

`-iオプション`はどの認証情報を使うか（≒どの鍵の情報を使うか）というもので、事前に提供した公開鍵に合致する秘密鍵を指定する必要があります。
ただ、`ssh-add ~/.ssh/ssh_my_key`とかしてあげれば毎回 `-i オプション`を入れなくともアクセスできるようになります。
ここまでは基本。

では`192.168.0.1`はただの踏み台で、ここを経由して他のサーバーにアクセスしたい場合はどうしましょうか？

多くの場合は次のように `ProxyComannd` をかけてアクセスするでしょう（192.168.0.3にアクセスすることを仮定）

```bash
ssh -i ~/.ssh/ssh_my_key -o ProxyCommand='ssh -i ~/.ssh/ssh_my_key mochisuna@192.168.0.1 -p 2222 -W %h:%p' mochisuna@192.168.0.3
```

内容的には`-oオプション`で踏み台サーバーにsshするコマンドを実行してから改めて目的のサーバーを叩くというようなことをやっているというもので、記法に慣れれば難しくないし便利です。

ただこのケース「ちょっと目的のサーバーにアクセスする」くらいならいいんですが、アクセス先のサーバーが増えてくると、管理・タイピング共にとても大変になってきます。
そこで `.ssh/config` を活用してアクセスします。

## `~/.ssh/config`をカスタマイズしてアクセスする
`~/.ssh/config`にサーバーの情報を記載することでアクセス先の管理を大幅に簡略化できます。

```~/.ssh/config
Host step
  User         mochisuna
  HostName     192.168.0.1
  IdentityFile ~/.ssh/ssh_my_key
  Port         2222

Host web1
  User         mochisuna
  HostName     192.168.0.3
  IdentityFile ~/.ssh/ssh_my_key
  ProxyCommand ssh mochisuna@192.168.0.1 -p 2222 -W %h:%p

Host web2
  User         mochisuna
  HostName     192.168.0.5
  IdentityFile ~/.ssh/ssh_my_key
  ProxyCommand ssh mochisuna@192.168.0.1 -p 2222 -W %h:%p
```

このようにsshコマンドで記述するパラメータを構造体のような形で書いてあげることで、`ssh web2`のような簡単なコマンドだけでアクセスできるようになります。
自動的に定義したパラメータで置換してアクセスしてくれるわけですね。便利ー。

**ただ、この書き方はあまり良くありません**

例えば毎週stepサーバーのIPが変わるような仕組みが取られていると、毎回書き直すのに絶望することになります。・・・まぁそんな設定入れることはないだろうけど、要は起点のIPが変わると保守が大変ということです。

なので以下のように書き換えます。

```~/.ssh/config
Host step
  User         mochisuna
  HostName     192.168.0.1
  IdentityFile ~/.ssh/ssh_my_key
  Port         2222

Host web1
  User         mochisuna
  HostName     192.168.0.3
  IdentityFile ~/.ssh/ssh_my_key
  ProxyCommand ssh step -W %h:%p

Host web2
  User         mochisuna
  HostName     192.168.0.5
  IdentityFile ~/.ssh/ssh_my_key
  ProxyCommand ssh step -W %h:%p
```

こうすればstepサーバーの内容をいい感じに補填してくれるようになるので、stepサーバーのIP設定を変えるだけで`ssh web2`のでアクセスできるようになります。
超便利。

ただ、僕は複数のプロジェクトに関わっているため、全てのプロジェクト設定を`~/.ssh/config`に盲目的に入れていくと大変なことになります。
**`step`だけだと、どのプロジェクトのどこのサーバーに行くかわかったもんじゃないのです。**
そこで`ssh_config`というようなファイルをプロジェクト毎に定義します。
そして`ssh -F sugoi-project/ssh_config web1`のように指定してあげれば、任意の定義ファイルでアクセスができるようになる・・・ハズですが、**実はそのままではやろうとすると `ssh: Could not resolve hostname` が出てアクセスできません。**
ファイルの記載をさらに改造しましょう。

(2023/05/12追記)
@fumiyasさんからこういうアプローチがあるというご意見いただきました。

[プロジェクト毎に接頭辞をつけることで補完が効くようになって便利という話](https://qiita.com/mochisuna/items/375b621b3281825d8a97#comment-17d9a29a8002df6715aa)

確かにこれは賢い。

## ProxyJumpを使って踏み台サーバー経由でアクセスする
**本題です**
`~/.ssh/config`の汚染を避けるため`sugoi-project`プロジェクト配下の`ssh_config`のというファイル（`~/.ssh/config`と同じ記法）を利用することを前提にします。
この時、HOSTの記載が`ProxyCommand ssh step -p 2222 -W %h:%p` のように記載されていた場合`ssh -F sugoi-project/ssh_config web1` のようにしても以下のようなエラーが出てアクセスできないかと思います。

```bash
$ ssh -F sugoi-project/ssh_config
ssh: Could not resolve hostname step: nodename nor servname provided, or not known
kex_exchange_identification: Connection closed by remote host
```
これは`ProxyCommand`が実行される際にsshのプロセスには「`sugoi-project/ssh_config`ファイルからサーバー設定を読み取った」という情報が渡されることがないため `~/.ssh/config`の中から`step`サーバー情報を探してしまうことが原因です。
そこでProxyJumpを利用した形に書き換えます（[OpenSSH 7.3](http://www.openssh.com/txt/release-7.3)以降に限り）

```sugoi-project/ssh_config
Host step
  User         mochisuna
  HostName     192.168.0.1
  IdentityFile ~/.ssh/ssh_my_key
  Port         2222

Host web1
  User         mochisuna
  HostName     192.168.0.3
  IdentityFile ~/.ssh/ssh_my_key
  ProxyJump    step

Host web2
  User         mochisuna
  HostName     192.168.0.5
  IdentityFile ~/.ssh/ssh_my_key
  ProxyJump    step
```

このように定義してあげることで`ssh -F sugoi-project/ssh_config web2`というような形でアクセスできるようになります。
僕はさらに手を抜いて[sshcとかいう適当なコマンドでアクセスできる仕組みを作ってます](https://qiita.com/mochisuna/items/ad93d2602e0b86545993#ssh%E3%82%92%E7%B0%A1%E5%8D%98%E3%81%AB%E3%81%A7%E3%81%8D%E3%82%8B%E3%82%88%E3%81%86%E3%81%AB)。
`sshc ssh_config`って打つだけでほぼ脳死でアクセスできるのでオススメです。

## グループにしてコピペを減らす（2023/05/12追記）
これまでの方法ですでにある程度便利になったのですが、物事は突き詰めたくなるものです。  
例えば、特定のグループ毎に同じユーザー名やポート番号を利用したいケースの場合では、上述の方法でも何度もコピペが発生しファイルの巨大化や管理の面倒さなどが発生してしまいます。  
そこで、これまでの条件に以下のようにケースを追加して考えたいと思います。  

- ユーザー名やポートは特定の機能毎に同じものが利用される
  - アプリケーションサーバー（app1 ~ app4）
  - NTPサーバー（ntp1 ~ ntp3）
  - NMSサーバー（nms1 ~ nms2）
  - syslogサーバー（syslog）

このような場合、以下の書き換えてあげることで記述事項の使い回しができるようになります。

```grouped-ssh
Host app1
  HostName     192.168.0.1
Host app2
  HostName     192.168.0.2
Host app3
  HostName     192.168.0.3
Host app4
  HostName     192.168.0.4

Host ntp1
  HostName     192.168.2.1
Host ntp2
  HostName     192.168.2.2
Host ntp3
  HostName     192.168.2.3

Host nms1
  HostName     192.168.0.15
Host nms2
  HostName     192.168.0.16

Host syslog
  HostName     192.168.1.123
  User         logger
  Port         12322

Host app?
  User         ubuntu
  Port         2022
  IdentityFile ~/.ssh/ssh_app

Host nms?
  User         centos
  Port         2222

Host ntp?
  User         rocky
  Port         1022

Host *
  IdentityFile ~/.ssh/ssh_default
  ProxyJump    step
```

よく見ると `app?` や `nms?` のように定義済みのホストを参照した記述と `*` のように、いかにもワイルドカードな記述があるのがわかるかと思います。  
このように記述すると

- app1 ~ app4はIPは各々のものを利用し、それ以外はユーザー名： `ubuntu` 、Port： `2022` 、IdentityFile: `~/.ssh/ssh_app` を利用する
- nms、 ntpなどもappと同様にそれぞれのユーザー名とPortを利用するが、デフォルトとして `~/.ssh/ssh_default` を利用する

という処理が実行されるようになります。  
規模が大きくなってくる場合は共通化・一般化してあげるようと便利そうですね。

# まとめ
1. `ssh-add` 使うとsshコマンドの`-i`オプションを省略できるけど、それやるくらいなら`.ssh/config`を利用したほうが楽
2. `ProxyCommand`を使うなら外部ファイルではなく`~/.ssh/config`で使う
3. `ProxyCommand`、`ProxyJump`は定義済のHost情報を利用できるのでIPとかはベタ書きしない
4. `ProxyJump`を使えば sshコマンドの`-F`オプションでも定義済Host情報を使えるので便利


stackoverflowでも[ちょくちょく上がる内容](https://stackoverflow.com/questions/28304804/ssh-f-configfile-and-proxycommand)ぽいので、知っておくと便利かもしれません。