#やること
EC2やLambdaからRDSにダイレクトに接続させるのではなく、間にプロキシサーバを挟んでRDSに接続させる方法をとる
同時にCloudFormationで環境の自動構築も行う。
#なぜそうするのか
当初の構成では以下の通り、RDSに対してダイレクトで接続を行なっていました。
Lambda → RDS
EC2 → RDS
このようなRDSに直接接続する構成には問題があるので、間にRDSプロキシを挟む構成にしたいと思います。
Lambda → RDS Proxy → RDS
EC2 → RDS Proxy → RDS
RDSProxyには以下のような特徴があります。
RDS Proxy を使用すると、データベーストラフィックの予測不可能なサージを処理できます。このようなサージを処理しないと、接続のオーバーサブスクリプションや新しい接続の急速な作成に伴って発生し、問題の原因となることがあります。RDS Proxy は、データベース接続プールを確立し、このプール内の接続を再利用するため、毎回新しいデータベース接続を開くためのメモリや CPU オーバーヘッドは不要です。オーバーサブスクリプションからデータベースを保護するために、データベース接続の作成数を制御できます。
例えばLambda関数は、データベースにアクセスするためにコネクションを確立させる必要があります。
しかし、コネクション確立させるためにはデータベースのCPUやメモリを圧迫するため、Lambda関数からデータベースへの同時接続数には上限がありますので、それを超えないようにする必要がありました。
こういった同時接続問題を解決してくれるのがこのRDS Proxyです。
RDS Proxyには接続プール機能があり、コネクションを再利用することができて結果としてコネクション数を抑えることができます。
Lambda + RDSの構成は同時接続数問題がネックとなりアンチパターンとされてきましたが、RDS Proxyはこれを解決してくれます。
参考:[なぜAWS LambdaとRDBMSの相性が悪いかを簡単に説明する][1]
[1]:https://www.keisuke69.net/entry/2017/06/21/121501
#要件と手順
要件は以下の通り
- DBはPostgreSQLを選択
- ユーザーの認証はAWS Secrets Managerを使用する
- RDSのエンドポイント名をホスト名として、ドメイン名と紐付けしてレコードセットを行う
手順は以下の通り
- セキュリティグループの設定
- IAMロール作成
- RDSインスタンスとRDSプロキシ作成
- SecretsManagerの作成
- Route53でレコード設定
##1.セキュリティグループの設定
今回はEC2とLambdaからプロキシを通してRDS接続するので、
EC2・Lambdaからのアクセス先がプロキシになるので、プロキシ側でEC2・Lambdaのインバウンド許可し、
プロキシのターゲット先がRDSになるので、RDS側でプロキシのインバウンド許可します。
従って以下の実装を行います。
- RDSのセキュリティグループのインバウンド許可にRDSプロキシセキュリティグループを追加
- RDSプロキシセキュリティグループのインバウンド許可にEC2とLambdaセキュリティグループを追加
#EC2セキュリティグループ
EC2SecurityGroup:
Type: "AWS::EC2::SecurityGroup"
Properties:
GroupDescription: "EC2 security group"
SecurityGroupEgress:
-
CidrIp: "0.0.0.0/0"
IpProtocol: "-1"
#Lambdaセキュリティグループ
LambdaSecurityGroup:
Type: "AWS::EC2::SecurityGroup"
Properties:
GroupDescription: "Lambda security group"
SecurityGroupEgress:
-
CidrIp: "0.0.0.0/0"
IpProtocol: "-1"
#RDSセキュリティグループ
RDSSecurityGroup:
Type: "AWS::EC2::SecurityGroup"
Properties:
GroupDescription: "RDS connect security group"
SecurityGroupIngress:
-
SourceSecurityGroupId: !Ref RDSProxySecurityGroup
FromPort: 5432
IpProtocol: "tcp"
ToPort: 5432
SecurityGroupEgress:
-
CidrIp: "0.0.0.0/0"
IpProtocol: "-1"
#RDSプロキシセキュリティグループ
RDSProxySecurityGroup:
Type: "AWS::EC2::SecurityGroup"
Properties:
GroupDescription: "RDS Proxy connect security group"
SecurityGroupIngress:
-
SourceSecurityGroupId: !Ref EC2SecurityGroup
FromPort: 5432
IpProtocol: "tcp"
ToPort: 5432
-
SourceSecurityGroupId: !Ref LambdaSecurityGroup
FromPort: 5432
IpProtocol: "tcp"
ToPort: 5432
SecurityGroupEgress:
-
CidrIp: "0.0.0.0/0"
IpProtocol: "-1"
##2.IAMロール作成
プロキシがRDSに接続するには、RDSサービスにアクセスするためのロールとSecretsManagerへアクセスするためのポリシーが必要になります。
従って、以下の内容を行います。
- IAMRoleでRDSのサービスを使うことを許可するロールを作成
- IMAPolicyでSecretsManagerへのアクセス許可を定義
IAMRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- rds.amazonaws.com
Action:
- 'sts:AssumeRole'
ManagedPolicyArns:
- !Ref IAMPolicy
IAMPolicy:
Type: "AWS::IAM::ManagedPolicy"
Properties:
PolicyDocument: |
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "GetSecretValue",
"Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:GetResourcePolicy",
"secretsmanager:DescribeSecret",
"secretsmanager:ListSecretVersionIds"
],
"Effect": "Allow",
"Resource": [
"*"
]
},
{
"Sid": "DecryptSecretValue",
"Action": [
"kms:Decrypt"
],
"Effect": "Allow",
"Resource": [
"*"
],
"Condition": {
"StringEquals": {
"kms:ViaService": "secretsmanager.ap-northeast-1.amazonaws.com"
}
}
}
]
}
Outputs:
IAMRoleArn:
Value: !GetAtt IAMRole.Arn
Export:
Name: "rds-proxy-role-arn"
OutputsでIAMロールを出力しています。
この後作成するRDSProxy
で外部参照するために、こういった記述をしています。
##3.RDSインスタンスとRDSプロキシ作成
RDSインスタンスとプロキシを設定していきます。
これをCloudFormationで記述していきます。
#-----------------------------------------------------------------------------#
Parameters:
#-----------------------------------------------------------------------------#
#VPC Parameter----------------------------------------------------------------#
PrivateSubnetA1:
Description: 'PrivSubA1'
Type: AWS::EC2::Subnet::Id
Default: xxx
PrivateSubnetC1:
Description: 'PrivSubC1'
Type: AWS::EC2::Subnet::Id
Default: xxx
SecurityGroupId:
Description: 'Security Groups'
Type: List<AWS::EC2::SecurityGroup::Id>
Description: Thelist of SecurityGroupIds in your Virtual Private Cloud
#Proxy Parameter-------------------------------------------------------------#
ProxyEngineFamily:
Description: 'The kinds of databases that the proxy can connect to.'
Type: String
AllowedValues:
- MYSQL
- POSTGRESQL
Default: POSTGRESQL
ProxyRequireTLS:
Description: 'specifies whether Transport Layer Security (TLS) encryption is required for connections to the proxy.'
Type: String
AllowedValues:
- true
- false
Default: false
ProxyIdleClientTimeout:
Description: 'The number of seconds that a connection to the proxy can be inactive before the proxy disconnects it.'
Type: Number
Default: 30
ProxyIAMAuth:
Description: 'Whether to require or disallow AWS Identity and Access Management authentication for connections to the proxy.'
Type: String
AllowedValues:
- DISABLED
- REQUIRED
Default: DISABLED
#-----------------------------------------------------------------------------#
Resources:
#-----------------------------------------------------------------------------#
RDSInstance:
Type: 'AWS::RDS::DBInstance'
Properties:
AllocatedStorage: 100
Engine: "postgres"
EngineVersion: 12.7
DBInstanceClass: "db.m5.large"
DBInstanceIdentifier: "rds_db01"
MasterUsername: "postgres"
MasterUserPassword: "password"
DBName: "rds-postgres"
StorageType: gp2
Port: 5432
RDSSubnetGroup:
Type: AWS::RDS::DBSubnetGroup
Properties:
DBSubnetGroupName: rds-subnetgroup"
DBSubnetGroupDescription: RDS Subnetgroup
SubnetIds:
- Ref: PrivateSubnetA1
- Ref: PrivateSubnetC1
RDSProxy:
Type: AWS::RDS::DBProxy
Properties:
DBProxyName: !Sub "rds-proxy01"
EngineFamily: !Ref ProxyEngineFamily
RequireTLS: !Ref ProxyRequireTLS
RoleArn: { "Fn::ImportValue": !Sub "rds-proxy-role-arn" }
Auth:
- AuthScheme: SECRETS
SecretArn: { "Fn::ImportValue": !Sub "rds-proxy-secrets-arn" }
IAMAuth: !Ref ProxyIAMAuth
VpcSecurityGroupIds:
- !Ref SecurityGroupId
VpcSubnetIds:
- Ref: PrivateSubnetA1
- Ref: PrivateSubnetC1
RDSProxyTargetGroup:
Type: AWS::RDS::DBProxyTargetGroup
Properties:
DBProxyName: !Ref RDSProxy
TargetGroupName: default
#-----------------------------------------------------------------------------#
Outputs:
#-----------------------------------------------------------------------------#
RdsEndpointtype:
Value: !GetAtt RDSInstance.Endpoint.Address
Export:
Name: "rds-endpoint"
Parameterでサブネットやセキュリティグループに関するパラメータと
プロキシに関するパラメータを選択できるようにしています。
RDSInstance
でRDSインスタンスを作成しています。
今回はPostgreSQLを選択しています。
RDSProxyではプロキシを作成します。
IAMロールで作成した、プロキシ用のIAMロールを参照、
次で作成するSecretsManagerのArnを参照しています。
最後にOutputsでRDSのエンドポイント名を出力しています。
これは後でやるRoute53のレコードセットの際に必要です。
##4.SecretsManagerの作成
次にSecretsManagerを作成していきます。
###AWS Secrets Managerとは
そもそもSecrets Managerについて、
AWS Secrets Manager とは
以前は、データベースから情報を取得するカスタムアプリケーションを作成したとき、通常、アプリケーション内でデータベースに直接アクセスするための認証情報 (シークレット) を埋め込んでいました。認証情報を更新するときには、単に新しい認証情報を作成する以上のことが必要でした。新しい認証情報を使用するために、アプリケーションを更新する時間を費やさなければなりませんでした。次に、更新されたアプリケーションを配布しました。認証情報を共有している複数のアプリケーションがあり、そのうちの 1 つを更新しなかった場合、アプリケーションは失敗しました。このリスクのために、多くのお客様は定期的に認証情報を更新せずに、実際のところは代わりに別のリスクを選択していました。
Secrets Manager では、コード内のハードコードされた認証情報 (パスワードを含む) を Secrets Manager への API コールに置き換えて、シークレットをプログラムで取得できます。これは、シークレットがコード内に存在しなくなったため、コードを調べる人によってシークレットが侵害されないようにするのに役立ちます。また、指定したスケジュールに従って自動的にシークレットをローテーションするように Secrets Manager を設定することもできます。これにより、長期のシークレットを短期のシークレットに置き換えることが可能となり、侵害されるリスクを大幅に減少させるのに役立ちます
簡単に言うとDB接続への認証情報をAWSのSecrets ManagerっていうサービスのAPIで管理させよう!
その方がセキュリティ的に安全だよ!ということです。
このSecretsManagerのターゲット先にRDSインスタンスのホスト名やらポートを指定します。
それではCloudFormationでコード化しましょう。
#-----------------------------------------------------------------------------#
Resources:
#-----------------------------------------------------------------------------#
RDSProxySecrets:
Type: AWS::SecretsManager::Secret
Properties:
Name: !Sub "${SystemName}-${Environment}-Secrets01"
Description: "This is a Secrets Manager secret for an RDS DB instance"
SecretString:
'{
"username":"postgres",
"password":"password",
"engine":"postgres",
"host":"rds-endpoint",
"port":"5432",
"dbname":"rds-postgres",
"dbInstanceIdentifier":"rds_db01"
}'
Tags:
- Key: Name
Value: !Sub ${SystemName}-${Environment}-Secrets01
#-----------------------------------------------------------------------------#
Outputs:
#-----------------------------------------------------------------------------#
RDSProxySecretsArn:
Value: !Ref RDSProxySecrets
Export:
Name: "rdsproxy-secrets-arn"
SecretString
では決め打ちでRDSInstance
で作ったRDSインスタンスの情報やパスワードを入れました。
ここがシークレットの値に相当します。
公式ではここはSecretString
ではなくGenerateSecretString
プロパティを使用してランダムなパスワードを生成することを推奨されています。
最後にOutputsでSecretsManagerのArnを出力して、RDSProxy
のSecretArn: { "Fn::ImportValue": !Sub "rds-proxy-secrets-arn" }
で外部参照しています。
##5.Route53でレコード設定
最後にRoute53でレコードの設定をしていきます。
#-----------------------------------------------------------------------------#
Parameters:
#-----------------------------------------------------------------------------#
SystemName:
Type: String
Default: xxx
RecordNameRDS:
Type: String
Default: xxx
Description: Route53 New Record Target Name. This is tied to the RDS endpoint.
Route53BaseDomain:
Type: String
Default: com
#-----------------------------------------------------------------------------#
Resources:
#-----------------------------------------------------------------------------#
Route53HostedZone:
Type: "AWS::Route53::HostedZone"
Properties:
Name: !Sub ${SystemName}.${Route53BaseDomain}
VPCs:
- VPCId:
Ref: VpcID
VPCRegion:
Fn::Sub: "${AWS::Region}"
Route53RSGroup:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneId: !Ref Route53HostedZone
Name: !Sub '${RecordNameRDS}.${SystemName}.${Route53BaseDomain}'
ResourceRecords:
- Fn::ImportValue: 'rds-endpoint'
Type: CNAME
TTL: 300
ドメインはお名前.comなどで取得することが可能です。(今回はドメインの取得については割愛)
最初に、ドメイン情報を保持するコンテナである、ホストゾーンを作成します。
DNSのレコードタイプは、今回はRDSのエンドポイントをホスト名として作成しドメインに転送したいのでCNAME
を使います。
ResoureceRecords
でRDSInstance
で作成した、RDSインスタンスのエンドポイントを外部参照してレコードセットを行いました。
#終わりに
今回紹介したのはRDSプロキシとSecrets Managerの基本的な使い方にとどまっているので、
機会があれば応用的な使い方も試してみたいと思います。