DS課の古川です。
RDS Proxyの登場により、AWS LambdaとAmazon RDS間接続のボトルネックを回避できるようになりました。
今回は、RDS Proxyを経由してAWS LambdaからAmazon RDSに接続する手順を試してます。
はじめに
最大同時接続数
RDBには最大同時接続数が存在し、CPUやメモリに依存する。つまり、アクティブな接続数を増やしたい場合、それなりのスペックのDBを用意する必要がある。
LambdaとRDBの接続
AWSLambdaは、ウォームスタートを除き1リクエストあたり1コンテナ作られる。
つまり、リクエストがあるとコンテナが作られて起動され、同時にDBへのコネクションを張る。
ウォームスタートの場合は、グローバルスコープ(グローバル変数)を利用することで接続オーバーヘッドを減らすことが可能。
1コンテナあたり1コネクション張るので、リクエストが多いサービスでは、RDBの最大同時接続数に達してしまう。
コネクションプールを使ってコネクションを使いまわせばいいのでは?と思うが、Lambdaでコネクションプーリングを実現するのは難しい。
RDS Proxy
RDS Proxyが、LambdaとRDBの仲介役となることで、必要となるDBへのコネクションプールを確立および管理し、既存のコネクションを再利用することで、LambdaからのDB接続を少なく抑える。
また、RDS Proxyは自動的にスケーリングされるため、コネクション管理に必要なメモリとCPUリソースを効率化できる。
また、RDS Proxyはフェイルオーバーに対して耐障害性が高く、元のDBインスタンスが使用できなくなると、アイドル状態のアプリケーション接続を切断せずにスタンバイDBに接続する。
リソース
- ネットワ-ク
- VPC
- Security Group
- Lambda用
- RDS Proxy用
- RDS用
- Amazon RDS
- RDS Proxy
- Amazon Secrets Manager
- IAM Role(RDS Proxy用)
- AWS Lambda
※各Security Groupのパラメータを以下に記載
Security Group
Lambda用
Inbound
Protocol | Port Range | Source |
---|---|---|
– | – | – |
outbound
Protocol | Port Range | Source |
---|---|---|
all | all | anywhere |
RDS Proxy用
Inbound
Protocol | Port Range | Source |
---|---|---|
tcp | 3306 | Lambda用Security Group |
outbound
Protocol | Port Range | Source |
---|---|---|
all | all | anywhere |
RDS用
Inbound
Protocol | Port Range | Source |
---|---|---|
tcp | 3306 | RDS Proxy用Security Group、踏み台用Security Group |
outbound
Protocol | Port Range | Source |
---|---|---|
all | all | anywhere |
RDSとRDS Proxyを同じSecurity Groupに紐付けると疎通エラーとなるので、個別に分けた方が良いかと思います。
※参考
- 「このセキュリティグループは、プロキシの接続先のデータベースへのアクセスを許可する必要があります。同じセキュリティグループが、アプリケーションからプロキシへのイングレスと、プロキシからデータベースへのエグレスに使用されます。
例えば、データベースとプロキシに同じセキュリティグループを使用するとします。この場合は必ず、そのセキュリティグループ内のリソースが同じセキュリティグループ内の他のリソースと通信できるように指定してください。」
手順
1. データベースを作成
- Amazon RDS for MySQL(MySQL 8.0.23, db.t2.micro)を使用。(Auroraでも可)
- VPC, DB Subnet Group, Security Groupは事前に用意
- シングル構成
- DB認証はパスワード認証
参考) DB インスタンスを作成する
2. 接続テスト
踏み台サーバー or Cloud9を用意
DBにテストデータを作成するために、同じVPC内に踏み台サーバーを用意する。
参考)踏み台サーバー作成方法
今回はSSH接続ではなく、Session Manager接続で踏み台サーバにアクセスします。
詳しい説明はこちらです。
RDSに接続
1. データベースを作成
で作成したRDSのエンドポイントを参照し、DBにログインします。
$ mysql -u admin -h <RDSのエンドポイント> -p
テスト用のテーブルを作成
mysql> show databases; mysql> create database test; mysql> create table test.sample(id int, value varchar(10)); mysql> INSERT INTO test.sample VALUE(1, "taro"); mysql> INSERT INTO test.sample VALUE(2, "hanako"); mysql> select * from test.sample; +------+--------+ | id | value | +------+--------+ | 1 | taro | | 2 | hanako | +------+--------+ 2 rows in set (0.00 sec)
テストデータが作成されていることを確認できました。
3. Secrets Managerの設定
RDS Proxyへの接続は、RDSのパスワードではなくSecrets Managerと連携します。
シークレットのタイプにて「Amazon RDSデータベースの認証情報」にチェックします。
認証情報にて、今回は1. データベースを作成
で作成したユーザーとパスワードを設定します。
暗号化キーはDefaultのKMSキーを使用します。
データベースにて1. データベースを作成
で作成したDBを選択します。
シークレットの値を確認したところ、以下のシークレットが自動生成されます。
- username
- password
- engine
- host(RDSのエンドポイント)
- port
- dbInstanceIdentifier
4. RDS Proxyの起動
RDSコンソール画面にて、プロキシを作成します。
プロキシの設定にて、任意のプロキシ識別子を入力、エンジンの互換性はMySQL、他の項目はデフォルトで設定します。
ターゲットグループの設定にて、プロキシに関連付けるRDSインスタンスを選択します。
今回は、1. データベースを作成
で作成したRDSインスタンスを選択し、最大接続数は100%に設定します。
接続にて、3. Secrets Managerの設定
で作成したシークレットを選択します。
IAMロールは、「IAMロールを作成」を選択することで新規作成します。
※重要
IAM 認証は、今回はRDS Proxyへパスワード認証でアクセスするために、「無効」を選択します。「有効」にすると、後の手順が少し変わるので、注意が必要です。
サブネットは、DBサブネットグループに登録している2つのサブネットを選択します。
※サブネットは少なくとも2つ以上設定する必要があります。
最後に、「拡張されたログ記録を有効にする」にチェックを入れ、プロキシによって処理されたクエリの詳細ログを記録します。
RDSコンソール画面にて、プロキシが追加されステータスが「利用可能」になっていることを確認し、作成されたプロキシを選択します。
ターゲットグループを確認し、ステータスが「利用可能」になっていることを確認します。
※Tips
ターゲットグループのステータスが「使用不可」になっていることがあります。
その場合は、AWS CloudShellにてエラー理由を見ることができます。
$ aws rds describe-db-proxy-targets --db-proxy-name test-lambda-rds-proxy
自動生成されたIAM Roleを確認します。
以下のポリシーが作られました。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "GetSecretValue", "Action": [ "secretsmanager:GetSecretValue" ], "Effect": "Allow", "Resource": [ "arn:aws:secretsmanager:ap-northeast-1:*******:secret:lambda_rds_proxy-P1Hnp9" ] }, { "Sid": "DecryptSecretValue", "Action": [ "kms:Decrypt" ], "Effect": "Allow", "Resource": [ "arn:aws:kms:ap-northeast-1:*******:key/5af55ba5-a7de-47a9-95c3-7abd677ac6b0" ], "Condition": { "StringEquals": { "kms:ViaService": "secretsmanager.ap-northeast-1.amazonaws.com" } } } ] }
5. Lambda関数の作成
今回は、Severless FrameworkとPython3.9にてAWS Lambdaを作成します。
環境変数にDBの認証情報を設定してますが、本来はSecrets Mangaerにて管理するのが望ましいです。
※AWSLambdaPowertoolsPythonにてSecretManagerからシークレットを取得する方法を、次回別記事にて投稿します。
serverless.yml
service: poc frameworkVersion: '2' provider: name: aws runtime: python3.9 lambdaHashingVersion: 20201221 region: ${opt:region, "ap-northeast-1"} stackName: ${self:service} package: patterns: - '!.devcontainer/**' - '!.venv/**' - '!node_modules/**' - '!.npmignore' - '!package-lock.json' - '!package.json' - '!Pipfile' - '!Pipfile.lock' - '!workspace.code-workspace' - '!lambda-rds-proxy.md' plugins: - serverless-python-requirements functions: LambdaRDSProxyFunction: handler: src.lambda_rds_proxy.handler name: ${self:service}-lambda-rds-proxy description: RDSプロキシ接続用 memorySize: 256 timeout: 30 layers: - arn:aws:lambda:${self:provider.region}:017000801446:layer:AWSLambdaPowertoolsPython:4 environment: DB_ENDPOINT: <RDS Proxyのエンドポイント> DB_USER: <DB認証ユーザー> DB_PASSWORD: <DB認証パスワード> DB_NAME: <テスト用DB> vpc: securityGroupIds: - <Lambda用セキュリティグループ> subnetIds: - <アクセスしたいサブネット>
lambda_rds_proxy.py
Python上でSQLを実行するために、 pymysqlモジュールを使用します。
今回はシンプルにsampleテーブルのデータを全て取得します。
import os import sys import pymysql from aws_lambda_powertools import Logger from aws_lambda_powertools.utilities.typing import LambdaContext DB_HOST = os.environ["DB_ENDPOINT"] DB_USER = os.environ["DB_USER"] DB_PASSWORD = os.environ["DB_PASSWORD"] DB_NAME = os.environ["DB_NAME"] logger = Logger(level='INFO', service=__name__) @logger.inject_lambda_context() def handler(event=None, context=LambdaContext): try: # RDS Proxyに接続 connect = pymysql.connect(host=DB_HOST, user=DB_USER, passwd=DB_PASSWORD, db=DB_NAME, connect_timeout=5) with connect.cursor() as cursor: cursor.execute("select * from sample") result = cursor.fetchall() # selectのみなので、commit()は不要 # conn.commit() logger.info(result) connect.close() except pymysql.MySQLError as e: logger.error("ERROR: Unexpected error: Could not connect to MySQL instance.") logger.error(e) sys.exit() except Exception: logger.error("ERROR: Unexpected error: Could not get value") sys.exit() return result
以下のコマンドにてAWSリソースをデプロイします。
$ npx sls deploy
Lambdaコンソール画面にて、Lambda関数がデプロイされていることを確認できました。
※重要
RDS ProxyへIAMRole認証をする場合、Lambdaコンソール画面にて、データベースプロキシの追加を設定する必要があります。
しかし、今回はパスワード認証を選択したので、データベースプロキシの追加は不要
となります。
6. Lambdaを実行
Lambdaコンソール画面にて、テストを実行します。
[ [ 1, "taro" ], [ 2, "hanako" ] ]
無事テストデータを取得できました。
気をつけるポイント
- RDS ProxyのパスワードはSecrets Managerで管理する
- RDS Proxyのサブネットは2つ以上設定する
- Security GroupをRDSとRDS Proxyで同じものを使用すると、RDS Proxyのターゲットに登録できない
- RDSにクエリを実行する場合、boto3ではなくpymysql
- RDS Proxyへのアクセス設定をパスワード認証にすると、LambdaとRDS Proxyの関連付けが不要になる
手順は以上となります。
参考
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/services-rds-tutorial.h
https://twitter.com/Keisuke69/status/1278129993986396160?s=20