SSH接続をせずに踏み台サーバー経由でDBへ接続してみた

## 0. 目標

- SSH接続は使用せず、代わりにAWS Systems Managerを利用する
- 構築した踏み台サーバー（EC2インスタンス）へ接続する
- ローカルPCからDBへ接続し、任意のSQLファイルを実行してDBの初期化等を行えるようにする

# 1. リソースを構築する

ここではAWS CDKを使用しています。

最終成果物は[こちら](https://github.com/glaceef/uv-advent-calendar-2022)にあります。
必要な部分だけを抜粋して説明します。

## 1.1 Bastion Host (EC2インスタンス) の構築

### VPCエンドポイントの作成

```typescript
['ssm', 'ec2messages', 'ssmmessages'].forEach(label => {
  const endpoint = `com.amazonaws.ap-northeast-1.${label}`; // 
  new InterfaceVpcEndpoint(this, `example-interface-vpc-endpoint-${label}`, {
    vpc,
    subnets: privateSubnets,
    service: new InterfaceVpcEndpointService(endpoint, 443),
    privateDnsEnabled: true,
  });
});
```

Systems Manager から VPC へアクセスできるように、VPCエンドポイントを設定します。
これにより、インターネットへアクセスできない（プライベートサブネットに設置されている）EC2インスタンスでも、System Manager で管理できるようになります。

https://aws.amazon.com/jp/premiumsupport/knowledge-center/ec2-systems-manager-vpc-endpoints/

System Manager から接続できるようにしたことで、セッションマネージャーのコンソールを利用した踏み台サーバーへのアクセスと、ローカルPCからポートフォワーディングが可能になります。手順については後述します。

### S3 VPC ゲートウェイエンドポイントの作成

```typescript
new GatewayVpcEndpoint(this, 'example-gateway-vpc-endpoint-s3', {
  vpc,
  subnets: [privateSubnets],
  service: GatewayVpcEndpointAwsService.S3,
});
```

curl など必要なコマンドを yum でインストールするために、S3 VPC ゲートウェイエンドポイントを作成します。

https://aws.amazon.com/jp/premiumsupport/knowledge-center/ec2-al1-al2-update-yum-without-internet/

### 踏み台サーバーの作成

```typescript
const host = new BastionHostLinux(this, 'example-bastion-host', {
  instanceName: 'example-bastion-host',
  instanceType: InstanceType.of(InstanceClass.T2, InstanceSize.MICRO),
  vpc,
  subnetSelection: privateSubnets,
  securityGroup: this.securityGroup,
});
```

踏み台サーバーを作成します。

`ec2.Instance` ではなく `ec2.BastionHostLinux` という踏み台サーバー専用のConstructを利用すると、Amazon Linux 2 を OS とする、最新のSSMエージェントがインストール済みのEC2インスタンスを作成することができます。

### おまけ：構築時に必要なコマンドをインストールする

```typescript
host.instance.addUserData([
  'yum -y update',
  'yum install curl',
  'amazon-linux-extras install postgresql13',
].join(' && '));
```

Amazon Linux 2 では標準で `yum` がインストールされているので、 `yum` の更新と試しに `curl` のインストール、自分はRDSのエンジンを PostgreSQL にしたので、 `psql` コマンドのインストールを同時に行うよう設定しました。

`BastionHostLinux` で構築されたEC2インスタンスは `amazon-linux-extras` というパッケージがインストールされているため、これを利用して `psql` コマンドをインストールしました。

https://aws.amazon.com/jp/premiumsupport/knowledge-center/ec2-install-extras-library-software/

## 2.1 RDSインスタンス の構築

### RDS用セキュリティグループを作成する

```typescript
const rdsSecurityGroup = new SecurityGroup(this, 'example-security-group-rds', {
  securityGroupName: 'example-security-group-rds',
  vpc,
});

rdsSecurityGroup.addIngressRule(
  bastionHostSecurityGroup,
  Port.tcp(5432),
  'Allow the rds to be communicated from bastion-host ec2'
);
```

踏み台サーバー用セキュリティグループからの、ポート5432（PostgreSQLの場合）でのアクセスを許可するように設定します。

# 2. DBへ接続する

## 2.1 セッションマネージャーから踏み台サーバーにアクセスし、DBへ接続する

踏み台サーバーへの接続は、よく「SSH接続をして...」と書かれていることが多いですが、この方法を用いるとSSH接続に必要な設定を行うことなくインスタンスへ接続できます。

1. まずAWSコンソールで [Systems Manager](https://ap-northeast-1.console.aws.amazon.com/systems-manager/home) のページを開きます。
2. 左の「ノード管理」→「セッションマネージャー」を開きます。
3. ここでは現在アクティブなセッションの一覧が表示されます。セッションを開始するために、「セッションの開始」を選択します。
4. セッションの開始が可能なインスタンスが表示されるので、作成した踏み台サーバーのEC2インスタンスを選択し、「セッションを開始する」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/355274/6b6a10ef-5c07-fde8-53fe-b661d9b3e88c.png)
5. コンソールから `psql` コマンドにて（PostgreSQLの場合）DBへ接続します。
![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/355274/b67cec1c-1e26-ec7e-babc-c698cf7a9883.png)

このように、セッションマネージャーを利用した踏み台サーバー経由のRDSへの接続は、すごく簡単に行うことができました。

## 2.2 ポートフォワーディングしてローカルからDB接続する

しかし上記の接続方法では、DBの初期化やSQLファイルの実行をすべて手動で行う必要があります。
それでは不便すぎるので、AWS CLIを使用して踏み台サーバーへのポートフォワーディングを行います。

`aws ssm start-session` というコマンドを使用すると、セッションマネージャーでセッションの開始が可能なインスタンスに対して、ローカルPCからポートフォワーディングをすることができます。

```shell
$ ec2_instance_id=i-0a12bc345def
$ db_host=example-rds.xxxyyyzzz.ap-northeast-1.rds.amazonaws.com
$ db_port=5432
$ local_port=65432 # 何でもよいです

$ aws ssm start-session \
    --target "$ec2_instance_id" \
    --document-name AWS-StartPortForwardingSessionToRemoteHost \
    --parameters "{
        \"host\": [\"$db_host\"],
        \"portNumber\": [\"$db_port\"],
        \"localPortNumber\": [\"$local_port\"]
    }"
```

```shell
$ aws ssm start-session ...(省略)

Starting session with SessionId: user-0abcde12345
Port 65432 opened for sessionId user-0abcde12345.
Waiting for connections...
```

ポートフォワーディングできたようなので、localhost のポート 65432 に対し `psql` コマンドで接続してみます。

```
$ psql -h localhost -p 65432 -U example postgres
Password for user example:
psql (14.5 (Ubuntu 14.5-0ubuntu0.22.04.1), server 13.7)
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)
Type "help" for help.

postgres=>
```

無事接続することができました！
もちろん `psql` コマンドで使用可能な `-f` オプションによる、SQLファイルを直接指定しての実行も可能です。

# 3. まとめ

この方法は2018か19年くらいにはあったようですが、最近一からリソースを構築する機会があり、その時に調べて初めて知りました。
SSH接続をする方法だと、インスタンスごとに鍵の設定をする必要があり、また接続しようとして初めて鍵がないことに気づき別の人に設定してもらったりと結構手間がかかることが多かったので、とても良い方法ではないかなと思います。

また個人的に思うメリット・デメリットは以下のようなものがあると思います。

メリット：
・SSH接続の設定やその鍵管理は不要。
・（SSH接続のために）パブリックサブネットに配置する必要がない。
・RDSへの接続許可を、AWSアカウントの権限設定に集約できる。
・セッションの状態のリアルタイムな確認やログの管理が可能
デメリット：
・AWSではない別のVPSを踏み台サーバーにする場合は、この方法は使用できない。

アクセス権限の管理やアクセスログの取得設定もAWSの管轄になるので、セキュリティの面でもだいぶ扱いやすいのではないかなと思います。
ぜひ使っていきましょう！