【Go】Lambda + RDSをIAM認証で接続する

## はじめに
現在、API Gateway + Lambda + RDSを使ってWebアプリケーションを作っています。LambdaとRDSのIAM接続というのを見つけて、Goで試してみたので備忘録です。

## IAM認証で接続するメリット・デメリット
### メリット
####コールドスタート問題の解決
アプリケーションでLambdaとRDSを接続することが避けられていた理由は、とにかく接続に時間がかかるためです。Webアプリを使っていて、DB接続に10秒も20秒も待ってられないですよね。（通称VPC Lambdaの10秒の壁？）

LambdaがVPC内で通信を行うにはENI生成を行う必要があり、コールドスタートとなってしまいます。しかし、IAM認証で接続することで、LambdaをVPC内に設置する必要がなくなるので、問題とされていた10秒の壁が解決できるのです！

【朗報】
Lambda関数のVPC環境でのコールドスタートが改善されたようです？

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

### デメリット
####同時接続数が限られる
RDSの同時接続数に上限があり、それ以上は接続できません。RDSのサイズによりますが、db.t2.microの場合、1秒間に10接続まで。

参考：[IAM データベース認証の MySQL の制限事項](https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.html#UsingWithRDS.IAMDBAuth.ConnectionsPerSecond)

私が今回作っているアプリは20人程度しか使わない、かつ使用頻度も低い、かつ社内のシステムなので、とりあえず大丈夫かなという軽い気持ちで考えていますが、もっと大人数が使うWebアプリだと厳しい制限です…。

【朗報】
2019年末に行われたre:Invent 2019で発表された、[RDS Proxy](https://aws.amazon.com/jp/rds/proxy/)を利用すると、この同時接続数の制限から解放されるかもしれないという朗報が！！(現在はプレビュー版です。)

LambdaとRDSを接続するデメリットはなくなったかもしれないです！


## 手順
LambdaからRDSのIAM認証での接続は下記の流れで進めていきます。

1. RDSの構築
1. IAM認証用のユーザーの作成
1. テーブルの作成
1. SSL証明書のダウンロード
1. IAMポリシーの作成
1. Lambda関数の作成
1. ソースコードの実装
1. ソースコードのアップロード
1. MySQLのIP制限

## やってみる
### 1. RDSの構築
私が使用したMySQLのバージョン：**MySQL 5.7.22**
(MySQL 8.0.16ですると、SSL経由でデータベースに接続を許可する設定が後で出てくるんですが、私の力ではできなかったので5.7系で作り直しました…。)

設定内容のうち、特別な箇所のみご紹介します。

<img width="946" alt="スクリーンショット 2020-02-19 9.14.21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/2a62b32f-135a-0ff2-a5b0-ed01de32015c.png">


サブネットグループ：**パブリックサブネット**でサブネットグループを作成し、選択
パブリックアクセス可能：**あり**を選択

<img width="946" alt="スクリーンショット 2020-02-19 9.17.14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/cf582c9e-8621-6cdb-b184-231cbf10c0ee.png">


データベース認証：**パスワードと IAM データベース認証**を選択

セキュリティグループでIP制限をすると、Lambdaからアクセスできません。ここでも躓いてしまったので、お気をつけください！！

その他は扱いたいデータ量などを鑑みて設定してください！

### 2. IAM認証用のユーザーの作成
前項で作成したDBにマスターユーザーでログインします。

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

IAM認証用のユーザーを作成します。Hostは **%** を指定します。

```
> CREATE USER '[ユーザー名]'@'%' IDENTIFIED WITH AWSAuthenticationPlugin as 'RDS';
```

SSL経由でデータベースに接続を許可します。下記では、SELECT、INSERT、UPDATE、DELETEを許可しています。

```
> GRANT SELECT,INSERT,UPDATE,DELETE ON [DB名].* to '[ユーザー名]'@'%' REQUIRE SSL;
```

### 3. テーブルの作成
今回は以下のようなテーブルを作成しました。

```
> select * from Info;
+----+-------------+-----------+------------------+
| ID | Name        | Address   | TEL              |
+----+-------------+-----------+------------------+
|  1 | 山田太郎     | 東京都     | 090-0000-0000    |
+----+-------------+-----------+------------------+

```

### 4. SSL証明書のダウンロード
`main.go`と同じディレクトリにダウンロードしてください。

```
$ wget https://s3.amazonaws.com/rds-downloads/rds-ca-2019-root.pem
```

### 5. IAMポリシーの作成
以下のポリシーを作成します。

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "rds-db:connect"
            ],
            "Resource": [
                "arn:aws:rds-db:[リージョン]:[アカウントID]:dbuser:[リソースID]/[DBに作成したユーザー名]"
            ]
        }
    ]
}
```

### 6. Lambda関数の作成
Lambda関数は特に複雑な設定をする必要がなく、通常通り作成するため省略します。
前項で作成したポリシーをアタッチし、VPCは非VPCのままにします。

### 7. ソースコードの実装
```main.go
package main

import (
	"crypto/tls"
	"crypto/x509"
	"database/sql"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"

	"github.com/aws/aws-lambda-go/lambda"
	"github.com/aws/aws-sdk-go/aws/credentials"
	"github.com/aws/aws-sdk-go/service/rds/rdsutils"
	"github.com/go-sql-driver/mysql"
)

type Response struct {
	UserID   int    `json:"userId"`
	UserName string `json:"userName"`
	Address  string `json:"address"`
	TEL      string `json:"tel"`
}

// os.Getenv（）でLambdaの環境変数を取得
var pemFile = "rds-ca-2019-root.pem"     // SSL証明書
var dbEndpoint = os.Getenv("dbEndpoint") // エンドポイント:ポート番号
var dbUser = os.Getenv("dbUser")         // DBに作成したユーザ名
var dbName = os.Getenv("dbName")         // テーブルを作ったDB名
var awsRegion = os.Getenv("awsRegion")   // RDSのリージョン

func RDSConnect() (interface{}, error) {
	awsCredentials := credentials.NewEnvCredentials()
	authToken, err := rdsutils.BuildAuthToken(
		dbEndpoint,
		awsRegion,
		dbUser,
		awsCredentials,
	)
	if err != nil {
		panic(err.Error())
	}
	connectStr := fmt.Sprintf("%s:%s@tcp(%s)/%s?tls=rds&allowCleartextPasswords=true",
		dbUser, authToken, dbEndpoint, dbName,
	)
	db, err := sql.Open("mysql", connectStr)
	if err != nil {
		panic(err.Error())
	}
	defer db.Close()

	fmt.Println("--- データの取得")

	// データの取得
	info, err := db.Query("SELECT * FROM Info")
	defer info.Close()
	if err != nil {
		panic(err.Error())
	}

	var UserID int
	var UserName string
	var Address string
	var TEL string

	for info.Next() {
		if err := info.Scan(&UserID, &UserName, &Address, &TEL); err != nil {
			panic(err.Error())
		}
		fmt.Println(UserID, UserName, Address, TEL)
	}

	responseMap := Response{}
	responseMap.UserID = UserID
	responseMap.UserName = UserName
	responseMap.Address = Address
	responseMap.TEL = TEL

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

	return string(params), nil
}

func RegisterTLSConfig() {
	caCertPool := x509.NewCertPool()
	pem, err := ioutil.ReadFile(pemFile)
	if err != nil {
		panic(err.Error())
	}
	if ok := caCertPool.AppendCertsFromPEM(pem); !ok {
		panic(err.Error())
	}
	mysql.RegisterTLSConfig("rds", &tls.Config{
		ClientCAs:          caCertPool,
		InsecureSkipVerify: true,
	})
}
func run() (interface{}, error) {
	fmt.Println("--- tsl.Configの登録開始")
	RegisterTLSConfig()
	fmt.Println("--- tsl.Configの登録終了")

	fmt.Println("--- RDS接続開始")
	response, err := RDSConnect()
	if err != nil {
		panic(err.Error())
	}
	fmt.Println("--- RDS接続終了")
	return response, nil
}

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

```

### 8. ソースコードのアップロード
ビルドして

```
$ GOOS=linux GOARCH=amd64 go build -o main
```

zipして

```
$ zip main.zip main rds-ca-2019-root.pem
```

アップロード！！！
ハンドラは`main`に変更します。

### 実行結果
値を取得することができました！

<img width="920" alt="Lambda.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/81e4c1b7-0742-f539-8214-935026fa8acd.png">

レイテンシーも気になりません！

### 9. MySQLのIP制限
IAM認証と言えどパブリックサブネットにRDSを置いて、セキュリティグループ全開ってセキュリティどうなんだろう？マスターユーザーはパスワード認証やん！！！って思いますよね。そこで、DBのマスターユーザーのIP制限をしました。

```
> RENAME USER '[マスターユーザー名]'@'%' TO '[マスターユーザー名]'@'[許可したいIPアドレス]';
```
私は会社のIPアドレスにして、`/32`まで書いたらログインできずに焦りました…。お気をつけください。VPC内のIPアドレスに設定して、そのVPCの中に立てたEC2からの接続を許可するっていう方法でもいいですね。

#### 複数IPを追加したい！
上記で設定したIPアドレス以外にも、複数の場所からマスターユーザーでアクセスしたく、接続できるIPアドレスを増やす必要があったのですが、`RENAME`だと先ほど指定したIPアドレスに上書きされてしまうので…以下の方法でマスターユーザーを追加(複製)しました！

```
> CREATE USER '[マスターユーザー名]'@'[許可したいIPアドレス]' IDENTIFIED BY '[パスワード]';
> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, RELOAD, PROCESS, REFERENCES, INDEX, ALTER, SHOW DATABASES, CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE, REPLICATION SLAVE, REPLICATION CLIENT, CREATE VIEW, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, CREATE USER, EVENT, TRIGGER ON *.* TO '[マスターユーザー名]'@'[許可したいIPアドレス]' WITH GRANT OPTION;
```

## おまけ
### ローカルからRDSに接続してみた
#### IAMユーザの作成
LambdaにアタッチしたポリシーをIAMユーザーにアタッチします。

#### AWS CLIの設定
作成したユーザーで、CLIの設定を行います。

```
$ aws configure
```
先ほど作成したIAMユーザーのアクセスキーとシークレットキー、リージョンを入力してください。

#### ターミナルから接続
ターミナルからプログラムアクセスをします。SSL証明書のパスは置いてあるところに変更してください。

```
$ RDSHOST="[エンドポイント]"
$ TOKEN="$(aws rds generate-db-auth-token --hostname $RDSHOST --port 3306 --region [リージョン] --username [IAMユーザー名] )"
$ mysql --host=$RDSHOST --port=3306 --ssl-ca=${GOPATH}/src/qiita/rds-ca-2019-root.pem --enable-cleartext-plugin --user=[IAMユーザー名] --password=$TOKEN
```

上記を実行して証明書チェーンを受け入れない場合は、次のコマンドを実行して、古いルート証明書と新しいルート証明書の両方を含む証明書バンドルをダウンロードしてください。

その後、再度上記のコマンドを実行してみてください。（証明書名は変更する。）

```
$ wget https://s3.amazonaws.com/rds-downloads/rds-combined-ca-bundle.pem
```
#### 実行
実行結果は以下の通りとなりました。先ほどと同じですね！

```
> select * from Info;
+----+-------------+-----------+------------------+
| ID | Name        | Address   | TEL              |
+----+-------------+-----------+------------------+
|  1 | 山田太郎     | 東京都     | 090-0000-0000    |
+----+-------------+-----------+------------------+
```
## おわりに
無事、RDSから取得したデータをLambdaに返すことができました！次回はAPI Gatewayを使ってWebブラウザで表示するところまでいきたいです！

RDS Proxyの発表やLambdaのコールドスタートの改善など、もっとLambda + RDSがこれから利用しやすくなりそうですね！また、RDS Proxyも試してみたいなと思ってます。