はじめに
2019年末に行われたre:Invent 2019で発表されたRDS Proxyが先日正式リリースになりました!API Gateway + Lambda + RDSを使ってWebアプリケーションを作っていて、そこでRDS Proxyを使ってみたので、備忘録です。
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を設置したときのコールドスタートが改善されていることがわかった
→ IAM認証接続ではパブリックサブネットに置いていたRDSをプライベートサブネットに配置できるようになった - VPCLambda + RDSで構築していたものはエンドポイントをRDS Proxyに向けるだけでOK(コードの修正が不要)
それでは、本題に入っていきます!
構成図
このような構成で作成しました!本記事では、Lambda、RDS Proxy、RDS、踏み台のEC2にフォーカスしています。
手順
下記の流れで進めていきます。
- VPC、サブネットの作成
- セキュリティグループの作成
- RDSの構築
- 踏み台EC2の構築
- テーブル作成
- DBユーザの作成
- RDS Proxyの構築
- 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を選択してください。
その他、特に特別な設定は不要なので省略します。
4. 踏み台EC2の構築
RDSに接続して、ユーザやテーブルを作成するための踏み台EC2をたてます。
今回使用したOS : Amazon Linux 2
セキュリティグループは2で作成したsg-ec2-bastionを選択してください。
こちらも、特に特別な設定は不要なので省略します。
ただ、IPv4パブリックIPを使ってsshで接続するため、自動割り当てパブリックIPは有効に設定します。(これをせずに、プライベートIPで接続を試みましたが、できませんでした。)
※上記の方法(自動割り当てパブリックIP)では、EC2を再起動するごとにパブリックIPアドレスが変更になるのでお気をつけてください。
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ファイルをインポートします。
> 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を使用する
IAMロールの作成
ユースケースはRDS - Add Role to Database
を選択します。
そして、必要なポリシーをアタッチします。
Secrets Managerへのアクセス権限のポリシー
{
"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のコンソールから作成しました。
プロキシ識別子(名前)を入力します。
エンジンの互換性ではMySQLとPostgreSQLが選べるようになっていました。ここでプロキシが接続できるDBのタイプを設定します。今回はMySQLなので、MySQLを選択しました。
先ほど作成したRDSを選択します。
先ほど作成したSecrets ManagerシークレットとIAMロールを選択します。
サブネットはRDSと同じプライベートサブネットを選択します。
セキュリティグループは2で作成したsg-rdsproxyを選択してください。
Cloud Watch Logsでデバッグログを取得したい場合は拡張されたログ記録を有効にするにチェックを入れてください。(Cloud Watch Logsへのアクセス権限も必要です。)
8. Lambda関数の作成
IAMロール
LambdaはVPC内にあるのでENI生成用のポリシーを作成してアタッチします。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Action": [
"ec2:DescribeNetworkInterfaces",
"ec2:DeleteNetworkInterface",
"ec2:CreateNetworkInterface"
],
"Resource": "*"
}
]
}
ログを取得したい場合はCloud Watch Logsへのアクセス権限も必要です。
VPC
Lambda関数の編集画面で設定できます。
カスタムVPCを選択し、RDSとRDS Proxyと同様のVPCとパブリックサブネットを選択します。
セキュリティグループは2で作成したsg-lambdaを選択してください。
ソースコード
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, ¬e); 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での実行結果がこちらです。
RDSから値が取得できました!
びっくりするのは応答時間!!!コールドスタートで接続に10秒程度かかるのでご法度とされていたVPCLambdaですが、こんなにはやくなっているんです。
参考:[発表] Lambda 関数が VPC 環境で改善されます
これは使わない手はない!
おわりに
無事、LambdaからRDS Proxyを介してRDSに接続が可能になりました!これで同時接続数問題も気にしなくていい!!また、VPC内にLambdaを設置したときのコールドスタートが改善されたので、Lambdaを非VPCに設置しなくてもいけるし、RDSがプライベートサブネットに置ける!!素晴らしい!
また、RDSのセキュリティグループにLambdaのセキュリティグループを付加し、コード中のエンドポイントの向きをRDS ProxyからRDSに変更するだけで、このコードのままRDSに接続できることが確認できました。つまり、Lambda + RDSで構築していたものは簡単にRDS Proxyを経由することができるんです!
わたしはこれまでIAM認証でのLambda + RDS接続しか経験がなく、コードの実装に手間取ってしまいましたが、上手く接続できないときの問題の切り分け方などとても勉強になりました!
GAになったので、これから活用できる場所が増えるのではないかなと思います!