LoginSignup
141
105

More than 3 years have passed since last update.

祝GA‼︎【Go】Lambda + RDS 接続にRDS Proxyを使ってみた

Posted at

はじめに

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にフォーカスしています。
RDS Proxy構成図.png

手順

下記の流れで進めていきます。

  1. VPC、サブネットの作成
  2. セキュリティグループの作成
  3. RDSの構築
  4. 踏み台EC2の構築
  5. テーブル作成
  6. DBユーザの作成
  7. RDS Proxyの構築
  8. 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アドレスが変更になるのでお気をつけてください。

スクリーンショット 2020-03-11 19.38.45.png

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ファイルをインポートします。

スクリーンショット 2020-03-12 18.23.55.png

> 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を選択します。

スクリーンショット 2020-03-11 19.32.53.png

そして、必要なポリシーをアタッチします。

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のコンソールから作成しました。

スクリーンショット 2020-07-05 18.53.25.png

プロキシ識別子(名前)を入力します。
エンジンの互換性ではMySQLとPostgreSQLが選べるようになっていました。ここでプロキシが接続できるDBのタイプを設定します。今回はMySQLなので、MySQLを選択しました。

スクリーンショット 2020-07-05 19.04.08.png

先ほど作成したRDSを選択します。

スクリーンショット 2020-03-16 12.25.36.png

先ほど作成したSecrets ManagerシークレットとIAMロールを選択します。
サブネットはRDSと同じプライベートサブネットを選択します。
セキュリティグループは2で作成したsg-rdsproxyを選択してください。

スクリーンショット 2020-07-05 19.09.34.png

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関数の編集画面で設定できます。

スクリーンショット 2020-03-16 12.26.00.png

カスタム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, &note); 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での実行結果がこちらです。

スクリーンショット 2020-07-05 19.38.47.png

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になったので、これから活用できる場所が増えるのではないかなと思います!

141
105
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
141
105