【超便利！?】踏み台完全不要でRDSに接続する楽々対話式スクリプト作った

:::note warn
2023/6/28現在
AWSが想定した動作ではなかったのか、ssh(22)かrdp(3389)以外では利用できなくなったようです。。
:::

## 前置き
2023年6月13日に開催された[AWS re:Inforce](https://reinforce.awsevents.com/)で[EC2 Instance Connect Endpoint (EIC エンドポイント) ](https://aws.amazon.com/jp/about-aws/whats-new/2023/06/amazon-ec2-instance-connect-ssh-rdp-public-ip-address/)によるパブリックIP不要でEC2インスタンスにSSHやRDPができる新機能が発表されました。

リリースを読んだ当初はEC2に対してのみ使用可能なものと思っていましたが、クラスメソッドさんの↓の記事によりRDSにも直接繋げる事ができるということで、今まで踏み台コンテナを立ててポートフォワーディングしていたものを置き換えられるのではと思い、接続用スクリプトを作ってみました。

https://dev.classmethod.jp/articles/how-to-connect-rds-instance-via-eic-endpoint/

## 因みにこれまでは
↓のやり方で踏み台用コンテナを立ててポートフォワーディングしていました。読めば分かりますが要素が多くパッとこれで出来るよと言ってもハードルを感じられる事が多かったです。

https://qiita.com/hedgehog051/items/3f1fa17b842c874bd9f6

## 前提条件

- EC2 Instance Connect Endpoint(EICE)が接続先RDSと同様のVPC内に作成されていること
- RDSのセキュリティグループはEC2 Instance Connect Endpointで使用しているセキュリティグループからのインバウンドが許可されていること
- EC2 Instance Connect Endpointで指定するセキュリティグループはRDSセキュリティグループへのアウトバウンドが許可されていること
- EICE名及びRDSクラスター名に環境名(prod/stg/dev)が含まれていること

## EC2 Instance Connect Endpointの作成

1. VPCエンドポイント作成画面より[EC2 Instance Connect Endpoint]を選択

![スクリーンショット_2023-06-16_4_20_44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/354022/447be59e-1928-cb4c-4422-6ca69e2faf01.png)

1. VPC及びセキュリティグープを選択
※EC2 Instance Connect Endpointで使用するセキュリティグループのインバウンドルールは不要で、ただし、接続先RDSに使われているSGへのアウトバウンドルールは必須

![スクリーンショット_2023-06-16_4_21_39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/354022/40c86ff4-9475-9d7e-b0de-8f3cfa2da865.png)

1. Subnetを選択して「エンドポイントを作成」を押下
※SubnetはプライベートサブネットでもOK

![スクリーンショット_2023-06-16_4_21_47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/354022/b23c115f-0016-7aa7-8087-29282c8d4afb.png)

## スクリプト
※スクリプト内で選択するポートは一般的な番号を入れてます。お好みで変えてください。
※DBへのアクセスは環境毎に可否があると思うので、現場に合わせて使ってください。ここではprodへの書き込みは制限するパターンで書いてます。

```sh
#!/bin/bash

##-----------------
## 第1引数にAWScliのprofileを指定
##-----------------
PROFILE=$1

echo "対象環境を数字から選択してください"
select ENV in "prod" "stg" "dev"
do
    echo "${ENV}が選択されました。"
    ENV=${ENV}
    break
done

if [ -z $PROFILE ]; then
    echo "aws cli プロファイル名を数字から選択してください"
    PROFILES=$(aws configure list-profiles)
    select PROFILE in $PROFILES
    do
        echo "${PROFILE}が選択されました。"
        PROFILE=${PROFILE}
        break
    done
fi

##-----------------
## EICE選択
##-----------------
echo "EC2 Instance Connect Endpointを数字から選択してください"
TARGET_EICE_NAMES=$(aws ec2 describe-instance-connect-endpoints \
    --query 'InstanceConnectEndpoints[]' \
    --profile $PROFILE \
    --output text | grep $ENV | awk '{print $3}')

if [[ -z "$TARGET_EICE_NAMES" ]]; then
    echo "選択した環境にEC2 Instance Connect Endpointは存在しません。"
    exit 1
fi

select TARGET_EICE_NAME in $TARGET_EICE_NAMES
do
    echo "${TARGET_EICE_NAME}が選択されました。"
    TARGET_EICE_NAME=${TARGET_EICE_NAME}
    break
done

TARGET_EICE_ID=$(aws ec2 describe-instance-connect-endpoints \
    --query 'InstanceConnectEndpoints[].InstanceConnectEndpointId' \
    --filters Name=tag:Name,Values=$TARGET_EICE_NAME \
    --profile $PROFILE \
    --output text)

##-----------------
## ReaderWriter選択
##-----------------
echo "接続するエンドポイントの種類を数字から選択してください"
select RDS_ENDPOINT in "Writer" "Reader"
do
    if [ "${ENV}" = "prod" ] && [ "${RDS_ENDPOINT}" = "Writer" ]; then
        echo "Writerはprodでは選択できません。"
    else
        echo "${RDS_ENDPOINT}が選択されました。"
        if [ "${RDS_ENDPOINT}" = "Writer" ]; then
            RDS_ENDPOINT="Endpoint"
        else
            RDS_ENDPOINT="ReaderEndpoint"
        fi
        break
    fi
done

##-----------------
## 接続先RDSエンドポイント選択
##-----------------
echo "接続するDatabaseを数字から選択してください"
TARGET_HOSTS=$(aws rds describe-db-clusters \
            --profile $PROFILE \
            --query 'DBClusters[?contains(Endpoint, `'$ENV'`)].['$RDS_ENDPOINT']' \
            --output text)
select TARGET_HOST in $TARGET_HOSTS
do
    echo "${TARGET_HOST}が選択されました。"
    TARGET_HOST=${TARGET_HOST}
    break
done

TARGET_HOST_IP=$(dig $TARGET_HOST +short | grep '^[0-9]')

##-----------------
## 接続先ポート
##-----------------
echo "接続先ポートを入力してください:"
select TARGET_PORT_OPTION in "入力" "選択"
do
    case ${TARGET_PORT_OPTION} in
        "入力")
            read -p "接続先ポートを入力してください: " TARGET_PORT
            while true
            do
                if [[ ${TARGET_PORT} =~ ^[0-9]+$ ]]; then
                    echo "${TARGET_PORT}が選択されました。"
                    break
                else
                    echo "無効な入力です。ポート番号を入力してください。"
                    read -p "接続先ポートを入力してください: " TARGET_PORT
                fi
            done
            break
            ;;
        "選択")
            select PORT_SELECT in "5432" "3306" "3389"
            do
                echo "${PORT_SELECT}が選択されました。"
                TARGET_PORT=${PORT_SELECT}
                break
            done
            break
            ;;
        *)
            echo "無効な選択です。もう一度選択してください。"
            ;;
    esac
done

##-----------------
## localポート
##-----------------
echo "localポートを入力してください:"
select LOCAL_PORT_OPTION in "入力" "選択"
do
    case ${LOCAL_PORT_OPTION} in
        "入力")
            read -p "localポートを入力してください: " LOCAL_PORT
            while true
            do
                if [[ ${LOCAL_PORT} =~ ^[0-9]+$ ]]; then
                    echo "${LOCAL_PORT}が選択されました。"
                    break
                else
                    echo "無効な入力です。ポート番号を入力してください。"
                    read -p "localポートを入力してください: " LOCAL_PORT
                fi
            done
            break
            ;;
        "選択")
            select PORT_SELECT in "15432" "13306" "13389"
            do
                echo "${PORT_SELECT}が選択されました。"
                LOCAL_PORT=${PORT_SELECT}
                break
            done
            break
            ;;
        *)
            echo "無効な選択です。もう一度選択してください。"
            ;;
    esac
done

##-----------------
## RDS接続
##-----------------
echo "RDSに接続します。"
aws ec2-instance-connect open-tunnel \
    --instance-connect-endpoint-id $TARGET_EICE_ID \
    --private-ip-address $TARGET_HOST_IP \
    --local-port  $LOCAL_PORT \
    --remote-port $TARGET_PORT \
    --profile $PROFILE
```

## 終わりに
ローカルからClinet経由で動作確認しましたが問題なくRDSに接続できました！これまでのやり方ではセットアップに必要な要素が多く若干面倒くささがありましたがそれらが今回の新機能でグッと減り楽になりました。

また、EC2 Instance Connect Endpointは**利用料がかからない**のも嬉しいポイントです。セキュリティ要件的に使用して問題ないかきちんと確認した上で活用していきたいですね。

## おまけ
EC2 Instance Connect Endpointによる接続を制限したい場合は、以下の様なポリシーを適切な箇所(SSOアクセス許可セットやSCP、IAMRoleなど)に付与する事で制限可能です。条件付けはお好みで。

```json
{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Sid": "DenyEICE_OpenTunnel",
			"Effect": "Deny",
			"Action": "ec2-instance-connect:OpenTunnel",
			"Resource": "arn:aws:ec2:*:xxxxxxxxxxxx:instance-connect-endpoint/*"
		}
	]
}
```

---
弊社でQiitaEngineerFesta2023の記事投稿キャンペーンを実施してます。
参加＆投稿お待ちしております！

https://qiita.com/official-events/0cc35c4799f72609b7ec