2020年の7月に発表され大きな話題を読んだRDS Proxyですが、意外とIAM認証やCloudFormationを使った設定方法などの情報が多くなかったので記事にしました。
今回はPython(pymysql)を使用してuser/passでの認証とIAM認証の両方をAWS SAMを用いたCloudFormationで構築しました。
全てを説明すると長くなってしまうので、記事内で省略している部分は以下のリポジトリを参照してください。
https://github.com/yumemi-dendo/sample-aws-rds-proxy
環境
SAM CLI:1.15.0
Python:3.8
user/passでの認証
user/passでの認証は主にIAM認証での要件に合わない場合に使用される接続方法です。
秒間20~200以上の新規コネクション生成が想定される場合はこちらにするべきでしょう。
また通常のRDSへの接続方法と同じなので探せば割と情報がある方法でもあります。
シンプルにSecrets ManagerからDBの認証情報を取得し、それを用いてRDSへ接続します。
RDS Proxyのセットアップ
ネットワークやRDSのセットアップはサンプルのリポジトリを参照してください。
RDS ProxyがDBの認証情報を保存しているSecrets Managerにアクセスできるようにする必要があります。
なのでAWS::RDS::DBProxy
の他にIAMロールを作成し、RDS Proxyにアクセスを許可するMySQLユーザーの認証情報が保存されているSecretsManagerの読み取り権限を付与します。
今回はrootユーザーとlambdaユーザーの二種類を許可しています。
最後に定義したRDS ProxyをAWS::RDS::DBProxyTargetGroup
でRDSに紐付けたら完了です。
この記事では割愛していますがRDS Proxy->RDS間のSecurityGroupのパスを通すのを忘れないようにしましょう。
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 RDSSecretAttachment
- !Ref RDSLambdaUserPassword
RDSProxy:
Type: AWS::RDS::DBProxy
Properties:
DBProxyName: sample-rds-proxy-for-mysql
EngineFamily: MYSQL
RoleArn: !GetAtt RDSProxyRole.Arn
Auth:
- AuthScheme: SECRETS
SecretArn: !Ref RDSSecretAttachment
IAMAuth: DISABLED
- AuthScheme: SECRETS
SecretArn: !Ref RDSLambdaUserPassword
IAMAuth: DISABLED
VpcSecurityGroupIds:
- !Ref RDSProxySecurityGroup
VpcSubnetIds:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
RDSProxyTargetGroup:
Type: AWS::RDS::DBProxyTargetGroup
DependsOn:
- RDSInstance
Properties:
DBProxyName: !Ref RDSProxy
DBInstanceIdentifiers:
- !Ref RDSInstance
TargetGroupName: default
ConnectionPoolConfigurationInfo:
ConnectionBorrowTimeout: 120
MaxConnectionsPercent: 90
MaxIdleConnectionsPercent: 10
Lambdaのセットアップ
Secrets ManagerからDBの認証情報を取得し、それを使用してRDS ProxyにアクセスできるLambdaの設定をしていきます。
まずはLambdaをVPC上で実行するようにVpcConfig
とAWSLambdaVPCAccessExecutionRole
を設定し、Policies
にSecrets Managerの読み取り権限を付与します。
また今回はVPC上でLambdaを実行するため、VPCからSecrets ManagerへアクセスするためにVPCエンドポイントを用意してSecrityGroupなどのネットワークの設定を行う必要があります。
(こちらも割愛しているのでリポジトリを参照してください。)
あとはRDS ProxyのSecurityGroupにLambdaのSecurityGroupを追加すれば完了です。
RDSProxySecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
VpcId: !Ref VPC
GroupDescription: "Security Group for RDS Proxy"
SecurityGroupIngress:
- IpProtocol: "tcp"
FromPort: !Ref RDSDBPort
ToPort: !Ref RDSDBPort
SourceSecurityGroupId: !Ref EC2SecurityGroup
- IpProtocol: "tcp"
FromPort: !Ref RDSDBPort
ToPort: !Ref RDSDBPort
SourceSecurityGroupId: !Ref FunctionSecurityGroup
#
# Lambda
#
FunctionSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: "Lambda Function Security Group"
VpcId: !Ref VPC
GetUserWithDBPassFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: functions/get_user_with_db_pass/
VpcConfig:
SecurityGroupIds:
- !Ref FunctionSecurityGroup
SubnetIds:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
Environment:
Variables:
RDS_PROXY_ENDPOINT: !GetAtt RDSProxy.Endpoint
RDS_SECRET_NAME: "sample-rds-lambda-user"
DB_NAME: "sample_rds_proxy"
Policies:
- Version: 2012-10-17
Statement:
- Effect: Allow
Action: secretsmanager:GetSecretValue
Resource: !Ref RDSLambdaUserPassword
- AWSLambdaVPCAccessExecutionRole
Lambdaのコード実装
Secrets ManagerからDBの認証情報を取得し、それを使用してRDS Proxy経由でRDSにアクセスするLambdaを実装します。
user/passを使用する場合は普通にRDSにアクセスする場合と同じようにusernameとpasswordを設定するだけです。
hostの向き先をRDS Proxyのエンドポイントにすることを忘れないようにしましょう。
(今回はpymysqlを使用していますが、mysql.connectorでも同様です。)
import sys
import os
import boto3
from botocore.client import Config
import json
import pymysql
import logging
logger = logging.getLogger(__name__)
RDS_SECRET_NAME = os.environ['RDS_SECRET_NAME']
RDS_PROXY_ENDPOINT = os.environ['RDS_PROXY_ENDPOINT']
DB_NAME = os.environ['DB_NAME']
def lambda_handler(event, context):
""" SecretsManagerに保存されたDBの認証情報を使用してRDSからデータを取得する。
"""
try:
config = Config(connect_timeout=5, retries={'max_attempts': 0})
client = boto3.client(service_name='secretsmanager',
config=config)
get_secret_value_response = client.get_secret_value(SecretId=RDS_SECRET_NAME)
except Exception:
logger.error("不明なエラーが発生しました。")
raise
rds_secret = json.loads(get_secret_value_response['SecretString'])
try:
conn = pymysql.connect(
host=RDS_PROXY_ENDPOINT,
user=rds_secret['username'],
passwd=rds_secret['password'],
db=DB_NAME,
connect_timeout=5,
cursorclass=pymysql.cursors.DictCursor
)
except Exception as e:
logger.error("不明なエラーが発生しました。")
logger.error(e)
sys.exit()
with conn.cursor() as cur:
cur.execute('SELECT id, name FROM users;')
results = cur.fetchall()
return results
IAM認証
IAM認証ではLambda側でSecrets Managerにアクセスする必要がなくなるので、権限的にもコスト的にも優れています。
しかしIAM認証では秒間あたりの新規コネクション生成数に制限があるので注意する必要があります。
(具体的な制限数に関してはドキュメントに記載がありませんでした。)
RDS Proxyのセットアップ
RDS ProxyでIAM認証を行う場合、TLS/SSLを有効にして紐付けられているSecrets ManagerのIAM認証を有効にする必要があります。
なのでRequireTLS
をTrueにし、Auth
のIAMAuth
をREQUIRED
にします。
それ以外の設定はuser/passの方と同じです。
RDSProxyWithIam:
Type: AWS::RDS::DBProxy
Properties:
DBProxyName: sample-rds-proxy-for-mysql-with-iam
EngineFamily: MYSQL
RequireTLS: True
RoleArn: !GetAtt RDSProxyRole.Arn
Auth:
- AuthScheme: SECRETS
SecretArn: !Ref RDSLambdaUserPassword
IAMAuth: REQUIRED
VpcSecurityGroupIds:
- !Ref RDSProxySecurityGroup
VpcSubnetIds:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
Lambdaのセットアップ
基本的にはuser/passと同じですが、Secrets Managerの代わりにデータベース用のIAMポリシーを付与します。
https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.IAMPolicy.html
Actionはrds-db:connect
を許可し、リソースにはデータベースのユーザーを指定します。
命名規則は以下のようになっています。
arn:aws:rds-db:{region}:{account-id}:dbuser:{DbiResourceId}/{db-user-name}
-
account-id
はDBインスタンスのAWSアカウント番号です。コンソールやRDS, RDS ProxyのArnから確認することができます。 -
DbiResourceId
はDBインスタンスの識別子です。今回はRDS Proxyの値を挿入します。 -
db-user-name
はアクセスを許可するMySQLユーザー名です。複数指定したり*
で指定することも可能です。
実際に値を入れると以下のようになります。
arn:aws:rds-db:eu-west-1:414867676510:dbuser:prx-07af81c332474cf27/lambda
GetUserWithIamFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: functions/get_user_with_iam/
VpcConfig:
SecurityGroupIds:
- !Ref FunctionSecurityGroup
SubnetIds:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
Environment:
Variables:
RDS_PROXY_ENDPOINT: !GetAtt RDSProxyWithIam.Endpoint
RDS_PROXY_PORT: !Ref RDSDBPort
RDS_USER: "lambda"
DB_NAME: "sample_rds_proxy"
Policies:
- Version: 2012-10-17
Statement:
- Effect: Allow
Action: rds-db:connect
Resource: "arn:aws:rds-db:eu-west-1:414867676510:dbuser:prx-07af81c332474cf27/lambda"
- AWSLambdaVPCAccessExecutionRole
Lambdaのコード実装
Secrets Managerから認証情報を取得する代わりにgenerate_db_auth_token
を使用します。
generate_db_auth_token
IAM認証情報を使用してデータベースに接続するために使用する認証トークンを生成します。
またIAM認証をする場合はTLS/SSL接続が必要なので証明書をapp.py
から参照できる場所に保存します。
https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/rds-proxy.html#rds-proxy-security.tls
RDS Proxyを使用する場合はAmazon root CA 1 trust store
が必要となるので以下のURLから取得します。
https://www.amazontrust.com/repository/AmazonRootCA1.pem
最後にpymysqlでの接続時に認証トークンと証明書を読み込ませれば完了です。
import sys
import os
import boto3
import pymysql
import logging
logger = logging.getLogger(__name__)
RDS_PROXY_ENDPOINT = os.environ['RDS_PROXY_ENDPOINT']
RDS_PROXY_PORT = os.environ['RDS_PROXY_PORT']
RDS_USER = os.environ['RDS_USER']
REGION = os.environ['AWS_REGION']
DB_NAME = os.environ['DB_NAME']
rds = boto3.client('rds')
def lambda_handler(event, context):
""" IAM認証でRDS Proxy経由でRDSからデータを取得する。
https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.Connecting.Python.html
TLS/SSLに関しては以下のURLを参照
https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/rds-proxy.html#rds-proxy-security.tls
"""
password = rds.generate_db_auth_token(
DBHostname=RDS_PROXY_ENDPOINT,
Port=RDS_PROXY_PORT,
DBUsername=RDS_USER,
Region=REGION
)
try:
conn = pymysql.connect(
host=RDS_PROXY_ENDPOINT,
user=RDS_USER,
passwd=password,
db=DB_NAME,
connect_timeout=5,
cursorclass=pymysql.cursors.DictCursor,
ssl={'ca': 'AmazonRootCA1.pem'}
)
except Exception as e:
logger.error("不明なエラーが発生しました。")
logger.error(e)
sys.exit()
with conn.cursor() as cur:
cur.execute('SELECT id, name FROM users;')
results = cur.fetchall()
return results
ちなみにmysql.connector
を使用する場合は証明書は不要でそのまま接続することができます。
(なぜかまでは調べきれてないのですが、たぶんライブラリ内に証明書も含まれているのかな…?)
https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.Connecting.Python.html
最後に
IAM認証やCloudFormation周りの情報が全然なくて探すのがめちゃくちゃ大変でした。
最終的に公式ドキュメントを隅々まで読んでなんとか動かすことができました。
RDSに直接繋ぐのと比べ多少性能は落ちるものの今まで問題だったLambdaでのコネクションの問題を解決し、料金的にも超大規模なRDSインスタンスタイプを使わない限りはお手軽なので、API Gatewayなど以外にもイベント実行や5~10分間隔といった短いバッチ処理などでも十分使えるレベルにあるのではないかと思います。
ただ実務で使うにはRDS Proxy周りのクォータに関する情報がほとんどないのでAWSのサポートにあれこれ問い合わせる必要がありそうです。
参考資料
AWS LambdaでAmazon RDS Proxyを使用する
Amazon RDS Proxy による接続の管理
AM 認証および AWS SDK for Python (Boto3) を使用した DB インスタンスへの接続
IAM データベースアクセス用の IAM ポリシーの作成と使用
サーバーレスアプリケーションから Amazon Aurora への IAM ロールベース認証
IAM 認証情報を使用して Amazon RDS MySQL DB インスタンスに対する認証をユーザーに許可する方法を教えてください。
【全世界待望】Public AccessのRDSへIAM認証(+ SSL)で安全にLambda Pythonから接続する