祝GA‼︎【Go】Lambda + RDS 接続にRDS Proxyを使ってみた

## はじめに
2019年末に行われたre:Invent 2019で発表された[RDS Proxy](https://aws.amazon.com/jp/rds/proxy/)が先日正式リリースになりました！API Gateway + Lambda + RDSを使ってWebアプリケーションを作っていて、そこでRDS Proxyを使ってみたので、備忘録です。

## RDS Proxyって何？
[公式サイト](https://aws.amazon.com/jp/rds/proxy/)によると、次のように説明されています。
> Amazon RDS プロキシは、Amazon Relational Database Service (RDS) 向けの高可用性フルマネージド型データベースプロキシで、アプリケーションのスケーラビリティやデータベース障害に対する回復力と安全性を高めます。

詳細は公式サイトをご確認ください！

### RDS Proxyで何をするのか

RDS Proxyでデータベースへのコネクションプールを確立、管理することで、アプリケーションからのデータベース接続数を少なく抑えることができるというので、今回使ってみました！

Lambda関数は、呼び出すごとに新しいコネクションを作成する必要があります。しかし、LambdaからRDSへの同時接続数には上限があり、これまではコネクション数が上限に達しないようにする必要がありました。これを解決してくれるのがこの**RDS Proxy**です。RDS Proxyを利用することで、既存のコネクションを再利用することができ、コネクション数を抑えることができます。

つまり、Lambda ＋ RDSの構成が避けられていた原因の1つの同時接続数問題がRDS Proxyで解決できるのです！

### 料金

料金は有効になっているデータベースインスタンスの vCPU あたり0.018USD/時間(東京リージョン)となります。ただし、最低料金として、2つのvCPU分の料金がかかるので、1つのvCPUの場合でも0.018 × 2 = 0.036USD/時間かかります。
参考：https://aws.amazon.com/jp/rds/proxy/pricing/


## 結論

とても記事が長くなってしまったので、先に結論をまとめます。

+ RDS Proxyを介してRDSに接続できることが確認できた
+ VPC内にLambdaを設置したときのコールドスタートが改善されていることがわかった<br>→ IAM認証接続ではパブリックサブネットに置いていたRDSをプライベートサブネットに配置できるようになった
+ VPCLambda + RDSで構築していたものはエンドポイントをRDS Proxyに向けるだけでOK（コードの修正が不要）

<br>
それでは、本題に入っていきます！


## 構成図
このような構成で作成しました！本記事では、Lambda、RDS Proxy、RDS、踏み台のEC2にフォーカスしています。
![RDS Proxy構成図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/f0b5be33-0143-dabd-9013-f9ebc7a51228.png)



## 手順
下記の流れで進めていきます。

1. VPC、サブネットの作成
2. セキュリティグループの作成
3. RDSの構築
4. 踏み台EC2の構築
5. テーブル作成
6. DBユーザの作成
7. RDS Proxyの構築
8. Lambda関数の作成


## やってみる
### 1. VPC、サブネットの作成
事前準備として、VPCを作成し、作成したVPCの中にプライベートサブネット、パブリックサブネットを作成します。特別な設定は不要なので作成方法は省略します。

### 2. セキュリティグループの作成
事前準備として、各リソースのセキュリティグループの作成を行います。
#### Lambda
セキュリティグループ name : sg-lambda
インバウンド　

| タイプ | ポート | ソース |
| :--: | :--: | :--: |
| ー | ー | ー |

特にどこからも許可していなくてもAPI Gatewayからは叩くことができます！

#### EC2
セキュリティグループ　name : sg-ec2-bastion
インバウンド

| タイプ | ポート | ソース |
| ---- | ---- | ---- |
| SSH | 22 | 許可したいIPアドレス |

#### RDS Proxy
セキュリティグループ　name : sg-rdsproxy
インバウンド

| タイプ | ポート | ソース |
| ---- | ---- | ---- |
| MySQL/Aurora | 3306 | sg-lambda |

#### RDS
セキュリティグループ　name : sg-rds
インバウンド

| タイプ | ポート | ソース |
| ---- | ---- | ---- |
| MySQL/Aurora | 3306 | sg-ec2-bastion |
| MySQL/Aurora | 3306 | sg-rdsproxy |

### 3. RDSの構築

プライベートサブネットにRDSを立てます。
今回使用したMySQLのバージョン ： MySQL 5.7.22
セキュリティグループは[2で作成した**sg-rds**](#rds)を選択してください。

その他、特に特別な設定は不要なので省略します。

### 4. 踏み台EC2の構築

RDSに接続して、ユーザやテーブルを作成するための踏み台EC2をたてます。
今回使用したOS ： Amazon Linux 2
セキュリティグループは[2で作成した**sg-ec2-bastion**](#ec2)を選択してください。

こちらも、特に特別な設定は不要なので省略します。
ただ、IPv4パブリックIPを使ってsshで接続するため、自動割り当てパブリックIPは**有効**に設定します。（これをせずに、プライベートIPで接続を試みましたが、できませんでした。）

※上記の方法（自動割り当てパブリックIP）では、EC2を再起動するごとにパブリックIPアドレスが変更になるのでお気をつけてください。


<img width="932" alt="スクリーンショット 2020-03-11 19.38.45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/86aab1e5-e122-ca69-fa5d-3e8142b0d89a.png">


### 5. テーブル作成

RDS内にテーブルを作成します。
テーブル内のデータはcsvファイルから取り込むようにしたかったので、今回はcsvファイルを準備しましたが、ただデータをRDSに保存するだけです。


#### csvファイルのアップロード
ローカルから踏み台のEC2にcsvファイルを移動させました。

```
$ scp -i [キーペア名].pem [ファイル名].csv ec2-user@[パブリックIP]:/home/ec2-user/(EC2内の保存したいディレクトリを指定)
```

#### EC2にssh接続

```
$ ssh -i [キーペア名].pem ec2-user@[IPv4パブリックIP]
```

#### MySQLのインストール

```
$ sudo yum update
$ sudo yum install mysql
```

#### RDSへ接続

```
$ mysql --local-infile=1 -h [RDSエンドポイント] -u [マスタユーザ名] -p
```

テーブル内のデータはcsvファイルから読み込むために、`--local-infile=1`のオプションをつけました。

#### テーブルの作成

まず、テーブルの枠を作成します。

```
> CREATE TABLE [テーブル名] ([カラムの指定]);

# 例
> CREATE TABLE m1_champion (id INT(2) AUTO_INCREMENT NOT NULL PRIMARY KEY, name VARCHAR(30) NOT NULL, champion YEAR NOT NULL, formed YEAR NOT NULL, note VARCHAR(30));
```
次に、下記のようなcsvファイルをインポートします。

<img width="359" alt="スクリーンショット 2020-03-12 18.23.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/f5e7f55e-3fac-7df9-9f57-c732fe01ed03.png">



```
> LOAD DATA LOCAL INFILE "[ファイルパス]/[ファイル名].csv" INTO TABLE [テーブル名] FIELDS TERMINATED BY ',' LINES TERMINATED BY '\r\n';
```
これでテーブルが完成しました。

| id | name | champion | formed | note |
| :--: | :--: | :--: | :--: | :--: |
| 1 | ミルクボーイ | 2019 | 2007 | コーンフレーク |
| 2 | 霜降り明星 | 2018 | 2013 | NULL |
| 3 | とろサーモン | 2017 | 2002 | NULL |


### 6. DBユーザの作成
上記の手順通り進めば、現在DBにログインしているので、このままLambdaから接続したときに使うユーザを作成します。

```
> CREATE USER '[ユーザ名]'@'%' IDENTIFIED BY '[パスワード]';
> GRANT SELECT, INSERT, UPDATE, DELETE ON [対象のDB].[対象のテーブル] TO '[ユーザー名]'@'%';
```

上記はSELECT, INSERT, UPDATE, DELETEの権限を許可しています。また、ホスト名には`%`=ワイルドカードを使用し、どこからのアクセスも受け入れるように設定しています。ちなみに、全てのDBとテーブルが対象の場合は`*.*`とします。

### 7. RDS Proxyの構築
いよいよRDS Proxyの構築に入ります。

#### Secrets Manager シークレットの作成
先ほど作成したDBのユーザ名とパスワードを入力し、作成します。
下記URLのAWSの公式ブログに沿って作成してください。
参考：[AWS LambdaでAmazon RDS Proxyを使用する](https://aws.amazon.com/jp/blogs/news/using-amazon-rds-proxy-with-aws-lambda/)

#### IAMロールの作成
ユースケースは`RDS - Add Role to Database`を選択します。

<img width="931" alt="スクリーンショット 2020-03-11 19.32.53.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/c448fb09-7296-29bb-f8d6-22833275258b.png">


そして、必要なポリシーをアタッチします。

##### Secrets Managerへのアクセス権限のポリシー

```JSON
{
    "Version": "2012-10-17",
    "Statement": [
       
        {
            "Sid": "",
            "Effect": "Allow",
            "Action": [
                "secretsmanager:GetResourcePolicy",
                "secretsmanager:GetSecretValue",
                "secretsmanager:DescribeSecret",
                "secretsmanager:ListSecretVersionIds"
            ],
            "Resource": "[シークレットARN]"
        }
    ]
}
```

拡張ログを取得したい場合は**Cloud Watch Logsへのアクセス権限**も必要です。ただ、以下のログに関しては、Cloud Watch Logsへのアクセス権限がなくてもロググループに出力されます。

+ RDS Proxyの起動終了
+ DBへの接続開始終了
+ 警告

#### RDS Proxyの作成
今回はRDS Proxyのコンソールから作成します。

ちなみに、Lambda関数のコンソール上からも作成でき、IAM認証で接続する場合はLambdaと紐付ける手間を省くことができます。ただ、今回のようにDBのユーザ名とパスワードを用いて接続する場合は紐付けは必要ないようなので、RDS Proxyのコンソールから作成しました。

<img width="972" alt="スクリーンショット 2020-07-05 18.53.25.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/accac4b8-7870-aed2-42ca-a9c2e8935e65.png">


プロキシ識別子（名前）を入力します。
エンジンの互換性ではMySQLとPostgreSQLが選べるようになっていました。ここでプロキシが接続できるDBのタイプを設定します。今回はMySQLなので、MySQLを選択しました。

<img width="974" alt="スクリーンショット 2020-07-05 19.04.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/ba823203-94e2-472e-66b8-9023d9216958.png">


先ほど作成したRDSを選択します。

<img width="911" alt="スクリーンショット 2020-03-16 12.25.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/157cfe8e-35e3-c610-8066-0ce8b98084a9.png">


先ほど作成したSecrets ManagerシークレットとIAMロールを選択します。
サブネットはRDSと同じプライベートサブネットを選択します。
セキュリティグループは[2で作成した**sg-rdsproxy**](#rds-proxy)を選択してください。

<img width="973" alt="スクリーンショット 2020-07-05 19.09.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/3fbb3f51-42d3-e56e-1d88-8247495e2854.png">


Cloud Watch Logsでデバッグログを取得したい場合は**拡張されたログ記録を有効にする**にチェックを入れてください。（Cloud Watch Logsへのアクセス権限も必要です。）


### 8. Lambda関数の作成
#### IAMロール
LambdaはVPC内にあるのでENI生成用のポリシーを作成してアタッチします。

```JSON
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeNetworkInterfaces",
                "ec2:DeleteNetworkInterface",
                "ec2:CreateNetworkInterface"
            ],
            "Resource": "*"
        }
    ]
}
```

ログを取得したい場合は**Cloud Watch Logsへのアクセス権限**も必要です。

#### VPC
Lambda関数の編集画面で設定できます。

<img width="931" alt="スクリーンショット 2020-03-16 12.26.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/3f01a237-7416-f836-e610-0c5eb8ac5d84.png">

カスタムVPCを選択し、RDSとRDS Proxyと同様のVPCとパブリックサブネットを選択します。
セキュリティグループは[2で作成した**sg-lambda**](#lambda)を選択してください。

#### ソースコード

```go
package main

import (
	"database/sql"
	"encoding/json"
	"fmt"
	"github.com/aws/aws-lambda-go/lambda"
	_ "github.com/go-sql-driver/mysql"
	"os"
)

type Response struct {
	ID       int            `json:"id"`
	Name     string         `json:"name"`
	Champion string         `json:"champion"`
	Formed   string         `json:"formed"`
	Note     sql.NullString `json:"note"`
}

// os.Getenv（）でLambdaの環境変数を取得
var dbUser = os.Getenv("dbUser")         // DBに作成したユーザ名
var dbPass = os.Getenv("dbPass")         // パスワード
var dbEndpoint = os.Getenv("dbEndpoint") // RDS Proxyのプロキシエンドポイント
var dbName = os.Getenv("dbName")         // テーブルを作ったDB名

func RDSConnect() (*sql.DB, error) {
	connectStr := fmt.Sprintf(
		"%s:%s@tcp(%s:%s)/%s?charset=%s",
		dbUser,
		dbPass,
		dbEndpoint,
		"3306",
		dbName,
		"utf8",
	)
	db, err := sql.Open("mysql", connectStr)
	if err != nil {
		panic(err.Error())
	}
	return db, nil
}

func RDSProcessing(db *sql.DB) (interface{}, error) {

	var id int
	var name string
	var champion string
	var formed string
	var note sql.NullString

	responses := []Response{}
	responseMap := Response{}

	getData, err := db.Query("SELECT * FROM m1_champion")
	defer getData.Close()
	if err != nil {
		return nil, err
	}

	for getData.Next() {
		if err := getData.Scan(&id, &name, &champion, &formed, &note); err != nil {
			return nil, err
		}
		fmt.Println(id, name, champion, formed, note)
		responseMap.ID = id
		responseMap.Name = name
		responseMap.Champion = champion
		responseMap.Formed = formed
		responseMap.Note = note
		responses = append(responses, responseMap)
	}

	params, _ := json.Marshal(responses)
	fmt.Println(string(params))

	defer db.Close()
	return string(params), nil
}

func run() (interface{}, error) {
	fmt.Println("RDS接続 start!")
	db, err := RDSConnect()
	if err != nil {
		panic(err.Error())
	}
	fmt.Println("RDS接続 end!")
	fmt.Println("RDS処理 start!")
	response, err := RDSProcessing(db)
	if err != nil {
		panic(err.Error())
	}
	fmt.Println("RDS処理 end!")
	return response, nil
}

/**************************
   メイン
**************************/
func main() {
	lambda.Start(run)
}

```

### 実行結果
上記の手順でRDS Proxyを用いての接続は完了です。（あれ意外と簡単）
Lambdaでの実行結果がこちらです。

<img width="972" alt="スクリーンショット 2020-07-05 19.38.47.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/567fb65f-1f2d-cca0-42b7-c9f05ee6ea5b.png">



RDSから値が取得できました！

びっくりするのは応答時間！！！コールドスタートで接続に10秒程度かかるのでご法度とされていたVPCLambdaですが、こんなにはやくなっているんです。

参考：[[発表] Lambda 関数が VPC 環境で改善されます](https://aws.amazon.com/jp/blogs/news/announcing-improved-vpc-networking-for-aws-lambda-functions/)

これは使わない手はない！

## おわりに
無事、LambdaからRDS Proxyを介してRDSに接続が可能になりました！これで同時接続数問題も気にしなくていい！！また、VPC内にLambdaを設置したときのコールドスタートが改善されたので、Lambdaを非VPCに設置しなくてもいけるし、RDSがプライベートサブネットに置ける！！素晴らしい！

また、RDSのセキュリティグループにLambdaのセキュリティグループを付加し、コード中のエンドポイントの向きをRDS ProxyからRDSに変更するだけで、このコードのままRDSに接続できることが確認できました。つまり、Lambda + RDSで構築していたものは簡単にRDS Proxyを経由することができるんです！
わたしはこれまで[IAM認証でのLambda + RDS接続](https://qiita.com/maika_kamada/items/ee594d8d30e04f7a5b38)しか経験がなく、コードの実装に手間取ってしまいましたが、上手く接続できないときの問題の切り分け方などとても勉強になりました！

GAになったので、これから活用できる場所が増えるのではないかなと思います！