Amazon RDS Proxy with AWS Lambda を試してみる

[先日の re:Invent で RDS Proxy がプレビュー版ですが発表されました。](https://aws.amazon.com/jp/rds/proxy/)

AWS Lambda は水平スケールするため RDS と相性が良くないと常々言われてきましたが、この機能でそれが解消されるのでしょうか？というわけで実際機能を試してみようと思います。

## リソース作成

テスト用のVPC、RDS、Lambda Function、RDS Proxy を作成します

![proxy-test.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149365/cc34d62a-bc00-0358-e555-ac6a17c90710.png)

### VPC, RDS 作成
今回 VPC、RDS はさくっと CloudFormation で作成
https://github.com/kobarasukimaro/rds-proxy-test/blob/master/cfn/rds-proxy-sample.yml

#### RDS の設定
パラメータグループで `max_connection` を 20 に設定しています。

### RDS Proxy 作成
RDS Proxy は画面から作成します。 awscli でも `1.16.300` 以上でしたら作成できます。

以下のような感じで作成しました。サブネットが空になってますがちゃんと設定しています。関連付けるデータベースのサブネットが自動的にサジェストされるのであまり迷うことはないかと思います。
シークレットはこの画面から作成しても候補に出てこないで予め作っておいたほうが良いかも。

<img width="841" alt="スクリーンショット 2019-12-12 8.23.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149365/e80efff3-afa4-5b9a-8e44-6c7176773e15.png">
<img width="826" alt="スクリーンショット 2019-12-12 8.23.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149365/0fd69a93-9778-61eb-5148-641ce7d96fd8.png">
<img width="799" alt="スクリーンショット 2019-12-12 8.25.35.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149365/4045e15a-a957-5b94-eae2-0f481c312b8d.png">

<img width="784" alt="スクリーンショット 2019-12-12 8.26.06.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149365/e03e4be0-e7d2-df77-cddf-bab3c298aa48.png">
<img width="810" alt="スクリーンショット 2019-12-12 8.25.59.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149365/756559e7-3bdf-4a39-abbd-592061cd4d7c.png">








### Lambda 作成
Lambda は Serverless Framework で作成します。

```console
sls deploy --stage xxx
```

#### テスト用コード

Python で簡単に作成

```python:lambda
import mysql.connector

DB_USER = os.environ["DB_USER"]
DB_PASSWORD = os.environ["DB_PASSWORD"]
DB_HOST = os.environ["DB_HOST"]
DB_NAME = os.environ["DB_NAME"]

def execute_query(event):
    config = {
        'user': DB_USER,
        'password': DB_PASSWORD,
        'host': DB_HOST,
        'database' : DB_NAME,
    }

    cnx = mysql.connector.connect(**config)
    cursor = cnx.cursor()
    query = ("SELECT SLEEP(10)")
    cursor.execute(query)

def lambda_handler(event, context):
    execute_query(event)
```

#### Lambda Function に RDS Proxy を登録
しなくても Proxy に接続できました。
↓に Proxy を登録すると IAM ロールが作られるので、 IAM 認証必須の場合に必要なのかもしれません。

<img width="1305" alt="スクリーンショット 2019-12-12 13.06.41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149365/bf50c8ac-25fc-a8f8-dd6a-09221b42341e.png">



## 試してみる
最大コネクションが7個ある RDS で、 10秒スリープする Lambda Function を同時に １０並列で実行してみます

### 実行の前に
RDS に設定した `max_connection` は 20 ですが、
RDS 自体がコネクション 4個と RDS Proxy が 8個確保していて、さらにテスト中はコネクションの様子を見るためにコンソールから１個コネクションを使っているため実質的に Lambda が使えるコネクションは 7個となります。

```console:processlist
MySQL [test]> show processlist;
+-----+---------------+---------------------+------+---------+------+-------------+------------------+
| Id  | User          | Host                | db   | Command | Time | State       | Info             |
+-----+---------------+---------------------+------+---------+------+-------------+------------------+
|   2 | rdsproxyadmin | xxx.xxx.xxx.xxx:xxxx | NULL | Sleep   |    0 | cleaning up | NULL             |
|   3 | rdsproxyadmin | xxx.xxx.xxx.xxx:xxxx  | NULL | Sleep   |    0 | cleaning up | NULL             |
|   4 | rdsproxyadmin | xxx.xxx.xxx.xxx:xxxx  | NULL | Sleep   |    0 | cleaning up | NULL             |
|   5 | rdsproxyadmin | xxx.xxx.xxx.xxx:xxxx  | NULL | Sleep   |    0 | cleaning up | NULL             |
|   6 | rdsproxyadmin | xxx.xxx.xxx.xxx:xxxx | NULL | Sleep   |    0 | cleaning up | NULL             |
|   9 | rdsadmin      | localhost           | NULL | Sleep   |    1 | cleaning up | NULL             |
|  11 | rdsadmin      | localhost           | NULL | Sleep   |    0 | cleaning up | NULL             |
|  55 | rdsadmin      | localhost           | NULL | Sleep   |   14 | cleaning up | NULL             |
|  56 | rdsproxyadmin | xxx.xxx.xxx.xxx:xxxx | NULL | Sleep   |    0 | cleaning up | NULL             |
|  57 | rdsproxyadmin | xxx.xxx.xxx.xxx:xxxx | NULL | Sleep   |    0 | cleaning up | NULL             |
|  67 | rdsadmin      | localhost           | NULL | Sleep   |   34 | cleaning up | NULL             |
| 122 | rdsproxyadmin | xxx.xxx.xxx.xxx:xxxx | NULL | Sleep   |    0 | cleaning up | NULL             |
| 124 | admin         | xxx.xxx.xxx.xxx:xxxx  | test | Query   |    0 | starting    | show processlist |
+-----+---------------+---------------------+------+---------+------+-------------+------------------+
13 rows in set (0.01 sec)
```

### Proxy なし
まずは今まで通り、普通に RDS に接続した動作を試してみます

#### 実行
Serverless Framework でローカルから10回実行

```console:lambdainvoke
$ for ((i=0; i<10; i++)); do serverless invoke --function proxy-test --stage xxx -r ap-northeast-1 &; done
```

#### 結果
CloudWatch で結果を見てみると、10回中 3回失敗しているのを確認できます
![スクリーンショット 2019-12-10 9.58.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149365/4e6e307f-eae9-65cd-2186-b567c43ec7c5.png)

CloudWatch Logs を見ると `Too many connections` が発生していたので想定通りの挙動となります

```console:error
$ aws logs get-log-events --log-group-name /aws/lambda/xxxxxxxxx  --log-stream-name "2019/12/10/[\$LATEST]xxxxxxxxxxxx"
<中略>
[ERROR] OperationalError: 1040 (08004): Too many connections
<中略>
```

### Proxy あり
DB の接続先を RDS Proxy に変更して試してみます

#### 実行
先ほどと同じように Serverless Framework でローカルから10回実行

```console:lambdainvoke
$ for ((i=0; i<10; i++)); do serverless invoke --function proxy-test --stage xxx -r ap-northeast-1 &; done
```

#### 結果
CloudWatch で結果を見てみると、10回中 全て成功しているのを確認できます
<img width="229" alt="スクリーンショット 2019-12-10 13.20.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149365/f720925c-768d-16ee-6d2b-c314f0fddd1d.png">

Duration を見てみると Max が約 20秒、 Min が約 10秒となってるので、max connection からあぶれたコネクションは空きが出るまで待っているものと思われます
<img width="252" alt="スクリーンショット 2019-12-10 13.22.31.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149365/d79366cc-d301-4484-2481-fc435f1e73a4.png">

ログも見てみましたが、 20秒かかっている実行はコネクションで待っているのが確認できます（赤枠がコネクション待ち）
<img width="814" alt="スクリーンショット 2019-12-10 13.25.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149365/762e4c5e-4783-0b50-ec4f-dfc5fe8383bb.png">

RDS Proxy が想定通りの役割を果たしてくれています。 Lambda + RDS 使う場合の大きな心配事が一つ解消されるので GA になるのが楽しみですね。

## ハマりどころ
### CloudFormation から作成したシークレットだとエラー
最初 CloudFormation から RDS 用のシークレットを作成して RDS Proxy で読み込んでみたのですが、

```
Client authentication failed for user "{DBユーザー名}" with auth-plugin "mysql_native_password" and TLS on. Reason: Invalid credentials.
```

というエラーが出てコネクションが張れませんでした。
画面からシークレットを作成した場合は上手くいったので、 CloudFormation 側で何か足りないものがあったとは思いますが原因は今の所不明です。


### Lambda でコネクションするときに Too many connections が出てしまう
これはテスト用にコネクション数の設定をいじっていたのが原因だったのですが `max_connection` を 1 や 10 に設定したところ、 RDS と RDS Proxy がコネクションを確保するのに気づかなかったので、そっちの確保が優先されて Lambda からコネクションが張れませんでした。 RDS のデフォルトのコネクション数で使う場合には全く問題はないかと思います。

### 設定は正しいのに Lost connection が出てコネクションが張れない
リソースは全て構築したしコネクション数の設定も問題ないし、さあ試そうと思ったところ、 Lambda で

```
[ERROR] InterfaceError: 2055: Lost connection to MySQL server at '{RDS Proxy のDNS}:3306', system error: 8 EOF occurred in violation of protocol (_ssl.c:1076)
```

というエラーが出てコネクションできませんでした。
どうやら `mysql-connector-python` のバージョンが原因で、 `8.0.17` だとこのエラーが発生するようです。
また、Python 3.7 でこの事象を確認してます。 3.8 では未確認です。

## やり残し
- フェイルオーバーの速度比較
- RDS 直と Proxy のレイテンシー比較
  - [パフォーマンスについてはこちらで検証されています](https://tech.fusic.co.jp/posts/2019-12-11-aws-lambda-with-rds-proxy-performance-1/)
- Python3.8 と mysql-connector-python >= 8.0.17 が動作するかどうか
- Lost connection の原因

## コード
今回試したコード一式はこちら
https://github.com/kobarasukimaro/rds-proxy-test