2020/06/30、RDS Proxy がプレビューを終了し、ついに GA (正式リリース) となったことが発表されました。
これまでアンチパターンと言われていた、AWS Lambda と RDS との接続について解決策のひとつとなることから、サーバーレス界隈では待望の機能であったと思います。
この記事では、CloudFormation でこの RDS Proxy を作ってみたいと思います。が、その前に、RDS Proxy について少し振り返ってみたいと思います。
RDS Proxy は AWS Lambda にとって何が嬉しいのか
AWS Lambda はいわゆるサーバーレスのサービスです。そのため、常在的に起動しているリソースは基本的にありません。なんらかのイベントが発生したことを契機に、Lambda 上で実装したアプリケーションを動作させるためのリソースがスピンアップするので、例えば HTTP 要求が着信した、特定の時刻になった、というようなイベントごとに、計算リソースが立ち上がります。
そういったサービスの性質上、同時に発生するイベントの数が多くなればなるほど、起動する AWS Lambda の数が増えることになります。500 件同時に HTTP 要求が着信すれば、500 個の AWS Lambda リソースが立ち上がっている可能性もあります。
この性質と、リレーショナルなデータベース製品との相性が、これまではアンチパターンと言われていました。Lambda のリソースは互いに疎なかたちで起動されることから、誤解を恐れずに言えば、立ち上がった Lambda の数だけデータベースとのコネクションが生成されることになり、これは一般的なフレームワークに搭載されている、いわゆる「コネクションプール」の仕組みとは大きく異なっています。
コネクションが多く生成されるほど、データベースサーバー側の消費メモリも増えることになりますが、上記のような Lambda というサービスの性質上、実装のなかでコネクションプールのような仕組みをもたせることはできず、サービスの外にコネクションプールの役割を果たす何らかのレイヤーを置く必要があるということが、容易に想像できるのではないかと思います。
このコネクションプールとしてのレイヤーを提供する役割を果たすのが、今回 GA された RDS Proxy となります。
RDS Proxy の制約
このように、AWS Lambda としては待望のリリースである RDS Proxy ですが、もちろん銀の弾丸というわけではなく、制約を理解して使っていく必要があります。いくつか、注意すべき制約をピックアップしてみます。
- Aurora を含む、RDS for MySQL 5.6 and 5.7、もしくは RDS for PostgreSQL 10.11 and 11.5 に対応しており、RDS MySQL 8.0 や RDS Oracle などの DB エンジンでは利用できない
- 自身で EC2 インスタンス上に構築した RDB と RDS Proxy を接続することはできない
- 書き込み用のインスタンスのみに対応しており、 読み込み専用のインスタンス (リードレプリカ) に対する RDS Proxy は作れない
- Aurora Serverless や Aurora Multi-Master と Proxy を接続することはできない
特に 3. のリードレプリカと Proxy が接続できない制約は、データベースへの読み込みワークロードをスケールできないので、現時点では大きいと思います。そのため、読み込みに関しては、Proxy を通さないエンドポイントを使う必要があります。ここは今後の対応に期待ですね。
CloudFormation で作ってみる
今回は、MySQL 5.7 互換の Aurora クラスターを構築し、RDS Proxy を CloudFormation から作成してみました。Aurora を配置する VPC やサブネットの ID などは、適宜変更したうえでそのまま使えるかと思います。
なお、CDK でも、2020/07/02 にリリースされた v1.49.0 から L2 Construct が追加され、RDS Proxy がかなり簡潔に書けるようになっています。こちらの README に例があるので、こちらも試してみるとよいかもしれません。
AWSTemplateFormatVersion: 2010-09-09
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: "Database Configuration"
Parameters:
- AuroraUserName
- AuroraDatabaseName
ParameterLabels:
AuroraUserName:
default: "Master User Name"
AuroraDatabaseName:
default: "Database Name"
Parameters:
AuroraUserName:
Type: String
AllowedPattern: "[a-zA-Z0-9-]+"
ConstraintDescription: "must be between 1 to 16 alphanumeric characters."
Description: "The database admin account user name, between 1 to 16 alphanumeric characters."
MaxLength: 16
MinLength: 1
Default: "root"
AuroraDatabaseName:
Type: String
AllowedPattern: "[a-zA-Z0-9_]+"
ConstraintDescription: "must be between 1 to 64 alphanumeric characters."
Description: "The database name, between 1 to 64 alphanumeric characters."
MaxLength: 64
MinLength: 1
Default: "test"
Resources:
### Aurora MySQL のパスワードを生成 ###
AuroraMasterPassword:
Type: AWS::SecretsManager::Secret
Properties:
GenerateSecretString:
GenerateStringKey: password
PasswordLength: 16
SecretStringTemplate: !Sub "{\"username\":\"${AuroraUserName}\"}"
ExcludeCharacters: "\"@/\\"
AuroraSecretAttachment:
Type: AWS::SecretsManager::SecretTargetAttachment
Properties:
SecretId: !Ref AuroraMasterPassword
TargetId: !Ref AuroraCluster
TargetType: AWS::RDS::DBCluster
### Aurora MySQL の生成 ###
AuroraSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
VpcId: vpc-0123456789abcedfg
GroupDescription: "Security Group for Aurora"
SecurityGroupIngress:
- IpProtocol: tcp
SourceSecurityGroupId: !Ref RdsProxySecurityGroup
FromPort: 3306
ToPort: 3306
AuroraSubnetGroup:
Type: AWS::RDS::DBSubnetGroup
Properties:
DBSubnetGroupDescription: "Aurora Subnet Group"
SubnetIds:
- subnet-0123456789abcedfg
- subnet-abcdefg0123456789
AuroraClusterParameterGroup:
Type: AWS::RDS::DBClusterParameterGroup
Properties:
Description: "Aurora Cluster Parameter Group"
Family: aurora-mysql5.7
Parameters:
time_zone: UTC
AuroraCluster:
Type: AWS::RDS::DBCluster
Properties:
DatabaseName: !Ref AuroraDatabaseName
DBClusterParameterGroupName: !Ref AuroraClusterParameterGroup
Engine: aurora-mysql
MasterUsername: !Ref AuroraUserName
MasterUserPassword: !Sub "{{resolve:secretsmanager:${AuroraMasterPassword}:SecretString:password::}}"
DBSubnetGroupName: !Ref AuroraSubnetGroup
VpcSecurityGroupIds:
- !Ref AuroraSecurityGroup
AuroraParameterGroup:
Type: AWS::RDS::DBParameterGroup
Properties:
Description: "Aurora Parameter Group"
Family: aurora-mysql5.7
AuroraInstance1:
Type: AWS::RDS::DBInstance
Properties:
DBClusterIdentifier: !Ref AuroraCluster
DBInstanceClass: db.t3.small
DBParameterGroupName: !Ref AuroraParameterGroup
Engine: aurora-mysql
### RDS Proxy の生成 ###
##### RDS Proxy が Aurora へ接続するためのセキュリティグループ #####
RdsProxySecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
VpcId: vpc-01234567890abcdefg
GroupDescription: "Security Group for RDS Proxy"
##### RDS Proxy が Aurora へ接続するためのパスワードを取得する IAM ロール #####
RdsProxyRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: rds.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: AllowGetSecretValue
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
- secretsmanager:DescribeSecret
Resource: !Ref AuroraSecretAttachment
RdsProxy:
Type: AWS::RDS::DBProxy
Properties:
DBProxyName: rds-proxy-for-aurora
EngineFamily: MYSQL
RoleArn: !GetAtt RdsProxyRole.Arn
Auth:
- AuthScheme: SECRETS
SecretArn: !Ref AuroraSecretAttachment
IAMAuth: DISABLED
VpcSecurityGroupIds:
- !Ref RdsProxySecurityGroup
VpcSubnetIds:
- subnet-0123456789abcedfg
- subnet-abcdefg0123456789
RdsProxyTargetGroup:
Type: AWS::RDS::DBProxyTargetGroup
DependsOn:
- AuroraCluster
- AuroraInstance1
Properties:
DBProxyName: !Ref RdsProxy
DBClusterIdentifiers:
- !Ref AuroraCluster
TargetGroupName: default
Outputs:
RDSProxyEndpoint:
Value: !GetAtt RdsProxy.Endpoint
Export:
Name: rds-proxy-endpoint
AuroraMasterPassword:
Value: !Ref AuroraMasterPassword
Export:
Name: aurora-master-password
RDS Proxy が Aurora に接続するための接続情報を、認証情報を管理するサービスである Secrets Manager に格納する必要がある点や、RDS Proxy の Target Group を Aurora クラスターおよびインスタンスが立ち上がったあとに作成するよう、DependsOn 句で明示的に指定してあげる必要がある点などが、注意が必要です。
Lambda から利用するときは、最後に Outputs で出力した RDS Proxy の接続エンドポイントを、Lambda の環境変数などにインポートし、認証情報は Secrets Manager から取得するように実装すると良いと思います。
まとめ
CloudFormation で RDS Proxy を作る実例はあまり見かけなかったので、本記事にて取り上げてみました。次回は、実際に RDS Proxy を通した環境と通さない環境で、Lambda から接続してみてパフォーマンスがどう変わるのか、試してみたいと思います。