mysql2/promise を使用し、VPC内のLambdaから RDS Proxy 経由で RDSに接続してみた [Node.js]

# はじめに
RDS Proxy 経由で MySQL にアクセスするLambdaをVPC内で実行させましたので、手順をまとめます。

以前、`mysql`モジュールを使用して同様の処理をしましたが、`mysql2`モジュールの方がコード量が少ないため、今回は`mysql2`を使用してみます。

https://qiita.com/hirai-11/items/b0b9abc25e8f49c3815b

![スクリーンショット 2022-06-23 22.00.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/6391a70c-3d26-f29d-2a5c-a8db47bbb6c3.png)


# RDS Proxyの作成
RDS Proxyは、以前作成した記事がありますので、こちらを参考にしてください。

https://qiita.com/hirai-11/items/07b25f81f1a983e0191c

# private subnetにLambdaを作成

## 設定

- IAMロール
    - `AWSLambdaENIManagementAccess`を付与します.これによってVPC上でLambdaが実行できます。
- タイムアウト時間
    - 30秒にします。実行に数秒かかります。
- 環境変数
    - RDS_HOSTNAME：proxyのライターエンドポイント
    - RDS_DB_NAME：データベース名
    - RDS_USERNAME：ユーザー名
    - RDS_PASSWORD：パスワード

- VPC
    - private subnetに設置
    - sgの設定は以下の通り

|設定  |タイプ|ポート|ソース|
|---|---|---|---|
| Lambda |All | All|0.0.0.0/0|
| Proxy | MySQL/Aurora |3306|LambdaのSG|
| Aurora | MySQL/Aurora |3306|ProxyのSG|

## mysql2モジュール
Lambdaは、Nodejsを使用します。

`mysql2`モジュールを使用するため、ローカルで`mysql2`モジュールをインストールします。

### LambdaLayerでアップロード
```terminal
$ mkdir nodejs
$ cd nodejs
$ npm init -y
$ ls
package.json

$ npm install mysql2

$ tree
.nodejs
├── node_modules
├── package-lock.json
└── package.json]

$ cd ..

$ zip -r nodejs.zip nodejs
```
nodejsフォルダごとzip化すること。
zip化したnodejs.zipをLambdaLayerにアップしましょう。

具体的なアップロード方法は、以前作成した記事を参考にしてください

https://qiita.com/hirai-11/items/58a968cc940051bd2fbd#lambda-layer%E3%81%AB%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89pythonnodejs%E5%85%B1%E9%80%9A

### Lambdaに直接アップロード
LambdaLayerを使用しない場合、以下のように作成します。
```terminal
$ mkdir nodejs
$ cd nodejs
$ tree
.nodejs
├── package-lock.json
└── package.json

// package name以外はデフォルトにしました
$  npm init -y

$ npm install mysql

$ touch index.js

$ tree
.nodejs
├── index.js
├── node_modules
├── package-lock.json
└── package.json

$ zip -r nodejs.zip *
```

nodejs.zipをLambdaにアップロードします
![スクリーンショット 2022-11-04 12.56.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/cfaef075-973c-a608-2eb7-d106e5bd6f0f.png)



## Lambdaのコード
### INSERTの場合

```js
const hostname = process.env.RDS_HOSTNAME;
const user_name = process.env.RDS_USERNAME;
const password = process.env.RDS_PASSWORD;
const databaseName = process.env.RDS_DB_NAME;
const mysql = require('mysql2/promise');

exports.handler = async (event) => {
  // console.log('event:', JSON.stringify(event, null, 2));

  const connection = await mysql.createConnection({
    host: hostname,
    user: user_name,
    password: password,
    database: databaseName,
  });

  const now = new Date().toLocaleString('ja-JP', { timeZone: 'Asia/Tokyo' });
  const sql = 'INSERT INTO database_table (title, created_at) VALUES (?, ?);';
  const inserts = ['test', now];

  try {
    await connection.beginTransaction();

    const results = await connection.execute(sql, inserts);
    console.log(results);

    await connection.commit();
  } catch (error) {
    await connection.rollback();
    console.log(error.message);
    throw error;
  } finally {
    connection.end();
  }
};
```
今回は、`database_table`というテーブルに、タイトルと作成日時を登録してみました。
`'INSERT INTO databaseTable (title, created_at) VALUES (?, ?);'`

以前、下記の記事で`mysql`モジュールで書いたコードよりもコード量が少なかったため、`mysql2`モジュールの使用をおすすめします。

https://qiita.com/hirai-11/items/b0b9abc25e8f49c3815b#lambda%E3%81%AE%E3%82%B3%E3%83%BC%E3%83%89-1

#### mysql2の使用メソッド
- `createConnection`
    - RDSのmysqlと接続します
- `beginTransaction`
    - トランザクションを開始します
- `query`
    - クエリを実行します
- `commit`
    - トランザクション中のデータ処理を確定します
- `rollback`
    - トランザクション中のデータ処理を取り消され、トランザクション実行前の状態に戻りトランザクションを終了します。
- `end`
    - データベースと接続を終了します。

### SELECT文の場合 ①


`INSERT`ではなく、`SELECT`文を使った場合、以下のようになります。
usersテーブルからデータを全て取得する、シンプルな内容です。

```js
const dbInfo = {
  host: process.env.RDS_HOSTNAME,
  user: process.env.RDS_USERNAME,
  password: process.env.RDS_PASSWORD,
  database: process.env.RDS_DB_NAME,
};
const mysql = require('mysql2/promise');

exports.handler = async (event) => {
  const connection = await mysql.createConnection(dbInfo);

  const usersSql = 'SELECT * FROM `users`';
  try {
    await connection.beginTransaction();

    const usersSqlResults = await connection.query(usersSql);
    console.log('Received usersSqlResults[0]:', JSON.stringify(usersSqlResults[0], null, 2));

    await connection.commit();
  } catch (error) {
    await connection.rollback();
    console.log(error.message);
    throw error;
  } finally {
    connection.end();
  }
};
```

### SELECT文の場合 ②
複数のお店のIDからそれぞれのユーザーIDを配列に収め、配列を返すという内容になります。
条件は、お店のオープン時間が12時より早く、かつ、閉店時間が20時より遅いことです。
```js
// db情報は、変数にしてもよい
const dbInfo = {
    host: process.env.RDS_HOSTNAME,
    user: process.env.RDS_USERNAME,
    password: process.env.RDS_PASSWORD,
    database: process.env.RDS_DB_NAME,
};
const mysql = require('mysql2/promise');

const getStoreIds = [23, 25, 29];
const startTime = 'start_time_Monday';
const endTime = 'end_time_Monday';

exports.handler = async (event) => {
  let userId = [];
  for (const storeId of getStoreIds) {
    const connection = await mysql.createConnection(dbInfo);

    const storesInserts = [storeId];
    const storesSql =
      'SELECT `user_id` FROM `stores` WHERE `store_id` = ? AND ' + startTime + ' < 12:00 AND ' + endTime + ' > 20:00;';
    try {
      await connection.beginTransaction();

      const storesSqlResults = await connection.query(storesSql, storesInserts);
      console.log('Received storesSqlResults[0]:', JSON.stringify(storesSqlResults[0], null, 2));

      // 値がなければスキップ
      if (!storesSqlResults[0][0]) continue;

      // 返ったuser_idを配列に加える
      storesSqlResults[0].map((item, index) => {
        userId.push(item.token);
      });

      await connection.commit();
    } catch (error) {
      await connection.rollback();
      console.log(error.message);
      throw error;
    } finally {
      connection.end();
    }
  }
  console.log('Received userId:', JSON.stringify(userId, null, 2));
  return userId;
};
````
SQL内の`storesSql`にある`?`には、`storesInserts`が入ります。

対して、SQLのテーブル名やカラム名に変数を当てたい場合、SQL内に文字列として結合させます。
今回で言いますと、`startTime`と`endTime`ですね。

# 参考記事

https://www.npmjs.com/package/mysql2

https://qiita.com/tatsumi44/items/4b0fe912a49025591945

https://www.sunapro.com/aws-lambda%E3%81%A7mysql2-promise%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%9F%E9%9D%9E%E5%90%8C%E6%9C%9F%E5%87%A6%E7%90%86%E3%82%92%E8%A1%8C%E3%81%86/