AWS Lambda（Node.js）においてmiddyを使ってAmazon RDSに接続する方法

AWS Lambda（Node.js）において、[middy](https://middy.js.org/)を使ってAmazon RDSに接続する方法を紹介します。

## はじめに

### データベースエンジン

この記事ではデータベースエンジンとしてAurora MySQLを使用している場合の例を示しますが、他のデータベースエンジンの場合も基本的な実装は変わりません。

### プログラミング言語

この記事ではソースコードをTypeScriptで書いているので型定義をインストールしたり型アノテーションを記述したりしていますが、JavaScriptで書く場合は不要です。

### ミドルウェア

middyでRDSに接続するために、[@middy/db-manager](https://www.npmjs.com/package/@middy/db-manager)を使います。
@middy/db-managerでは、データベースクライアントとして[Knex.js](http://knexjs.org/)が使われます。

## 接続情報をAWS Secrets Managerで管理していない場合

接続情報をAWS Secrets Managerで管理していない場合、ミドルウェアとして[@middy/db-manager](https://www.npmjs.com/package/@middy/db-manager)のみを使います。

### パッケージをインストール

middyを使うために、[@middy/core](https://www.npmjs.com/package/@middy/core)パッケージをインストールします。

```sh
$ npm i -d @middy/core
```

middyでRDSに接続するために、[@middy/db-manager](https://www.npmjs.com/package/@middy/db-manager)パッケージをインストールします。

```sh
$ npm i -d @middy/db-manager
```

また、データベースエンジンに応じた[パッケージ](http://knexjs.org/#Installation-node)をインストールします。
今回はAurora MySQLなので、[mysql](https://www.npmjs.com/package/mysql)パッケージをインストールします。

```sh
$ npm i -d mysql
```

Lambdaの型定義として、[@types/aws-lambda](https://www.npmjs.com/package/@types/aws-lambda)パッケージをインストールします。

```sh
$ npm i -D @types/aws-lambda
```

### ハンドラーを作成

middyを用いて、ハンドラーをつくります。

```ts:src/index.ts
import lambda from 'aws-lambda'
import Knex from 'knex'
import middy from '@middy/core'
import dbManager from '@middy/db-manager'

type Event = any
interface Context extends lambda.Context {
  db: Knex
}

const handler = middy(async (
  event: Event,
  context: Context
): Promise<void> => {
  const users = await context.db.select('*').from('users')
  console.log(users)
})

handler.use(dbManager({
  config: {
    client: 'mysql',
    connection: {
      host: '127.0.0.1',
      port: 3306,
      user: 'your_database_user',
      password: 'your_database_password',
      database: 'myapp_test'
    }
  }
}))

export { handler }
```

詳しく解説していきます。

```ts
interface Context extends lambda.Context {
  db: Knex
}
```

@middy/db-managerを使うとコンテキストの`db`プロパティにKnexインスタンスが割り当てられるので、`db`プロパティを持つ`Context`を定義しています。

```ts
const handler = middy(async (
  event: Event,
  context: Context
): Promise<void> => {
  const users = await context.db.select('*').from('users')
  console.log(users)
})
```

ハンドラーを`middy`関数でラップすることで、middyfy（middy化）しています。
ハンドラー内では、`context.db`からKnex.jsのAPIを用いてデータベース操作が行なえます。

```ts
handler.use(dbManager({
  config: {
    client: 'mysql',
    connection: {
      host: '127.0.0.1',
      port: 3306,
      user: 'your_database_user',
      password: 'your_database_password',
      database: 'myapp_test'
    }
  }
}))
```

ハンドラーで、ミドルウェアとして`dbManager`を使っています。
`config`には、Knex.jsの設定オブジェクトを渡します。
ここでは例示のために接続情報をべた書きしていますが、実際には環境変数やAWS Secrets Managerで管理すべきでしょう。

## 接続情報をAWS Secrets Managerで管理している場合

接続情報をAWS Secrets Managerで管理している場合、ミドルウェアとして[@middy/db-manager](https://www.npmjs.com/package/@middy/db-manager)と[@middy/secrets-manager](https://www.npmjs.com/package/@middy/secrets-manager)を使います。

### パッケージをインストール

middyを使うために、[@middy/core](https://www.npmjs.com/package/@middy/core)パッケージをインストールします。

```sh
$ npm i -d @middy/core
```

middyでRDSに接続するために、[@middy/db-manager](https://www.npmjs.com/package/@middy/db-manager)パッケージをインストールします。

```sh
$ npm i -d @middy/db-manager
```

また、データベースエンジンに応じた[パッケージ](http://knexjs.org/#Installation-node)をインストールします。
今回はAurora MySQLなので、[mysql](https://www.npmjs.com/package/mysql)パッケージをインストールします。

```sh
$ npm i -d mysql
```

middyでSecrets Managerに接続するために、[@middy/secrets-manager](https://www.npmjs.com/package/@middy/secrets-manager)パッケージをインストールします。

```sh
$ npm i -d @middy/secrets-manager
```

Lambdaの型定義として、[@types/aws-lambda](https://www.npmjs.com/package/@types/aws-lambda)パッケージをインストールします。

```sh
$ npm i -D @types/aws-lambda
```

### ハンドラーを作成

middyを用いて、ハンドラーをつくります。

```ts:src/index.ts
import lambda from 'aws-lambda'
import Knex from 'knex'
import middy from '@middy/core'
import dbManager from '@middy/db-manager'
import secretsManager from '@middy/secrets-manager'

type Event = any
interface Context extends lambda.Context {
  db: Knex
}

const MIDDY_RDS_SECRET_KEY = 'MIDDY_RDS_SECRET'

const handler = middy(async (
  event: Event,
  context: Context
): Promise<void> => {
  const users = await context.db.select('*').from('users')
  console.log(users)
})

handler.use(secretsManager({
  secrets: {
    [MIDDY_RDS_SECRET_KEY]: 'secret_name'
  },
  cache: true,
  throwOnFailedCall: true
}))

handler.use({
  before: (handler, next) => {
    interface Secret {
      host: string
      port: number
      username: string
      password: string
      dbname: string
    }
    interface Connection {
      host: string
      port: number
      user: string
      password: string
      database: string
    }
    interface Context extends lambda.Context {
      [MIDDY_RDS_SECRET_KEY]: Secret | Connection
    }
    const context = handler.context as Context
    const secret = context[MIDDY_RDS_SECRET_KEY] as Secret
    context[MIDDY_RDS_SECRET_KEY] = {
      host: secret.host,
      port: secret.port,
      user: secret.username,
      password: secret.password,
      database: secret.dbname
    }
    return next()
  }
})

handler.use(dbManager({
  config: {
    client: 'mysql'
  },
  secretsPath: MIDDY_RDS_SECRET_KEY,
  removeSecrets: true
}))

export { handler }
```

詳しく解説していきます。

```ts
interface Context extends lambda.Context {
  db: Knex
}
```

@middy/db-managerを使うとコンテキストの`db`プロパティにKnexインスタンスが割り当てられるので、`db`プロパティを持つ`Context`を定義しています。

```ts
const handler = middy(async (
  event: Event,
  context: Context
): Promise<void> => {
  const users = await context.db.select('*').from('users')
  console.log(users)
})
```

ハンドラーを`middy`関数でラップすることで、middyfy（middy化）しています。
ハンドラー内では、`context.db`からKnex.jsのAPIを用いてデータベース操作が行なえます。

```ts
handler.use(secretsManager({
  secrets: {
    [MIDDY_RDS_SECRET_KEY]: 'secret_name'
  },
  cache: true,
  throwOnFailedCall: true
}))
```

ハンドラーで、ミドルウェアとして`secretsManager`を使っています。
`secrets`では、コンテキストのどのプロパティに対してSecrets Managerのどのシークレットを割り当てるかを指定します。
`cache`を`true`にすることで、キャッシュを有効化しています。
`throwOnFailedCall`を`true`にすることで、シークレットの取得に失敗した場合にエラーをスローするようにしています。

```ts
handler.use({
  before: (handler, next) => {
    interface Secret {
      host: string
      port: number
      username: string
      password: string
      dbname: string
    }
    interface Connection {
      host: string
      port: number
      user: string
      password: string
      database: string
    }
    interface Context extends lambda.Context {
      [MIDDY_RDS_SECRET_KEY]: Secret | Connection
    }
    const context = handler.context as Context
    const secret = context[MIDDY_RDS_SECRET_KEY] as Secret
    context[MIDDY_RDS_SECRET_KEY] = {
      host: secret.host,
      port: secret.port,
      user: secret.username,
      password: secret.password,
      database: secret.dbname
    }
    return next()
  }
})
```

Secrets Managerに保存されているシークレットを、Knex.jsに渡す形式に変換しています。
シークレットの形式は、データベースエンジンによって異なります。[^1]

```ts
handler.use(dbManager({
  config: {
    client: 'mysql'
  },
  secretsPath: MIDDY_RDS_SECRET_KEY,
  removeSecrets: true
}))
```

ハンドラーで、ミドルウェアとして`dbManager`を使っています。
`secretsPath`には、Secrets Managerのシークレットが割り当てられているコンテキストのプロパティを指定します。
`removeSecrets`を`true`にすることで、データベースに接続後にコンテキストからシークレットを削除しています。

[^1]: [Lambda ローテーション関数の作成に使用できる AWS テンプレート - AWS Secrets Manager](https://docs.aws.amazon.com/ja_jp/secretsmanager/latest/userguide/reference_available-rotation-templates.html)