Windowsで、パブリックIPを固定していないEC2インスタンスへ、.ssh/configを毎回書き換えずにssh接続したい

こねくり回してようやく解決したので、備忘録。
# 目的
パブリックIPアドレスが固定されていないEC2インスタンスにVS Codeで`git pull`/`push`するときに、
毎回`.ssh/config`を書き換えたくない。  
すなわち、  
**パブリックIPアドレスが固定されていないEC2インスタンスに、毎回`.ssh/config`を書き換えずに`ssh`接続したい。**

# 環境
Windowsで個人開発をしていて、EC2インスタンスをプライベートなGitサーバ兼デプロイサーバとして使っています。  
インスタンスは常時起動ではなく、必要な時だけ起動するので、そのたびにパブリックIPアドレスが変わります。

この上でローカルからリモートへgit経由でアクセスしようとすると、毎回
```console
git remote set-url origin https://[毎回違うパブリックIP]/[.gitの場所]
```
とリモートアドレスを書き換えるか、`.ssh/config`に設定を書き込んだ上で、URLを毎回書き換える必要があります。面倒！  
有料のElastic IPをアタッチすればIPは固定されるけど、少額とはいえもったいない。

そこで無料のEC2 Instance Connectorの出番です。  
AWS CLI経由でsshができるので、これを使う設定を`.ssh/config`に書き込んで、  
VS Codeのコマンドから`git pull`/`push`したりRemote SSHで直接アクセスできるようにします。

# 準備
EC2 Instance Connector経由で、AWS CLIを使ってインスタンスに接続できるのが前提です。  
[AWS: EC2 Instance Connect Endpoint を使用して Linux インスタンスに接続する](https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/connect-using-eice.html) をベースに、とても丁寧で救われたこちらの解説記事を参考にしました。

https://dev.classmethod.jp/articles/update-ec2-instance-connect-endpoint/


IAMに付与する許可ポリシーが一番のハマりポイントだと思います（私はハマった）。  
[AWS: EC2 Instance Connect Endpoint を使用するための IAM アクセス許可の付与](https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/permissions-for-ec2-instance-connect-endpoint.html) に記載されているJSONを適宜編集します。  
既知で単一のエンドポイントにだけ接続したい場合は次の通り：  
```json
{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Sid": "EC2InstanceConnect",
			"Action": "ec2-instance-connect:OpenTunnel",
			"Effect": "Allow",
			"Resource": "arn:aws:ec2:ap-northeast-1:[アカウントID]:instance-connect-endpoint/[エンドポイントID]",
			"Condition": {
				"NumericEquals": {
					"ec2-instance-connect:remotePort": "22"
				},
				"NumericLessThanEquals": {
					"ec2-instance-connect:maxTunnelDuration": "3600"
				}
			}
		},
		{
			"Sid": "SSHPublicKey",
			"Effect": "Allow",
			"Action": "ec2-instance-connect:SendSSHPublicKey",
			"Resource": "*",
			"Condition": {
				"StringEquals": {
					"ec2:osuser": "ami-user"
				}
			}
		},
		{
			"Sid": "Describe",
			"Action": [
				"ec2:DescribeInstances",
				"ec2:DescribeInstanceConnectEndpoints"
			],
			"Effect": "Allow",
			"Resource": "*"
		}
	]
}
```

`ami-username` は[AWS: IAM への EC2 Instance Connect のアクセス許可の付与](https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/ec2-instance-connect-configure-IAM-role.html) にある通り、
- インスタンスのOSがAL2023またはAmazon Linux 2の場合 `ec2-user`
- Ubuntuでは `ubuntu`

です（ただし`ami-username`のままでも動いた）。  
このページにもポリシーのJSONがありますが、これだけだとOpenTunnel許可がないよ、と怒られます。

ポリシーを付与した後、コマンドプロンプトないしPowerShellから

```console
aws ec2-instance-connect ssh --instance-id [インスタンスID] \
--eice-options maxTunnelDuration=3600,endpointId=[エンドポイントID] \
--os-user [インスタンス上のユーザ名]
```

でインスタンスに接続できれば、準備完了です。
# 手順

Windows上のAWS CLIでssh接続できるようになったわけですが、ここから [AWS: EC2 Instance Connect Endpoint を使用して Linux インスタンスに接続する](https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/connect-using-eice.html) に従って
```console
ssh -i [ssh鍵] [インスタンス上のユーザ名]@[インスタンスID] \
    -o ProxyCommand='aws ec2-instance-connect open-tunnel --instance-id [インスタンスID]'
```
を実行すると、

```console
2024-03-06 14:51:13,755 - awscli.customizations.ec2instanceconnect.websocket - ERROR - {"ErrorCode":"AccessDeniedException","Message":"User: arn:aws:iam::[アカウントID]:user/[接続に使うIAMユーザ] is not authorized to perform: ec2-instance-connect:OpenTunnel on resource: arn:aws:ec2:ap-northeast-1:[アカウントID]:instance-connect-endpoint/[エンドポイントID] because no identity-based policy allows the ec2-instance-connect:OpenTunnel action"}

AWS_ERROR_HTTP_WEBSOCKET_UPGRADE_FAILURE: Failed to upgrade HTTP connection to Websocket.
kex_exchange_identification: Connection closed by remote host
Connection closed by UNKNOWN port 65535
```

と再び怒られてしまいます。もー！

## 1. IAMに許可ポリシーを追加する
OpenTunnelその他を許可するインラインポリシーを新しく作り、ユーザに付与します。

```json
{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Sid": "SecureInstanceConnect",
			"Effect": "Allow",
			"Action": [
				"ec2-instance-connect:OpenTunnel",
				"ec2-instance-connect:SendSSHPublicKey",
				"ec2-instance-connect:SendSerialConsoleSSHPublicKey"
			],
			"Resource": [
				"arn:aws:ec2:ap-northeast-1:322811116193:instance/[インスタンスID]",
				"arn:aws:ec2:ap-northeast-1:322811116193:instance-connect-endpoint/[エンドポイントID]"
			]
		}
	]
}
```

これで再び
```console
ssh -i [ssh鍵] [インスタンス上のユーザ名]@[インスタンスID] \
    -o ProxyCommand='aws ec2-instance-connect open-tunnel --instance-id [インスタンスID]'
```
を実行して、インスタンスに接続できればOKです。

## 2. 上記を`.ssh/config`に書き込む
`.ssh/config`に
```console
Host [お好きな名前]
	HostName  [インスタンスID]
	User [EC2上のユーザ名]
	Port 22
	IdentitiesOnly yes
	IdentityFile [ssh鍵]
	StrictHostKeyChecking no
	ProxyCommand "C:\Program Files\Amazon\AWSCLIV2\aws.exe" ec2-instance-connect open-tunnel --instance-id [インスタンスID]
```
を追記します。  
`aws`は`PATH`を通していない場合はフルパスで書く必要があります。  
Windowsだとデフォルトで`C:\Program Files`にインストールされます。この空白で引っかかるので、パス全体をquoteで囲む必要があります。

`StrictHostKeyChecking` を `no` にすると、IPが変わった後の質問を回避できます。  
ただしセキュリティ的には良くないとGitHub Copilot先生が教えてくれました。

最後に

```console
ssh [お好きな名前]
```

でインスタンスに接続できるか確認します。無事接続できたら完了です。

これで、パブリックIPアドレスが固定されていないEC2インスタンスを、`.ssh/config/`に保存された他の接続先と同じように扱えるようになりました。お疲れ様でした。
それでは楽しいsshライフを！