クラウド事業部エンジニアの川勝です。
今回は Amazon RDS Proxy をつかって Amazon Aurora MySQL に接続するサーバーレスアプリケーションを構築するサンプルを作成したのでその方法を解説したいと思います。
構成図
Amazon API Gateway -> AWS Lambda -> Amazon RDS Proxy -> Amazon Aurora MySQL
という経路になっています。
VPC には Private Subnet オンリーで頑張っていますが、実用的には Public Subnet に DB 接続用の踏み台インスタンスが必要になるかなと思います。
(まとめで書いていますがそもそも Public Subnet に配置でいい気もします)
Amazon CloudFormation スタック
AWS SAM-CLI で構築していますので、Amazon CloudFormation のスタックごとに解説します。
スタックのネストについてはここでは詳しく取り上げませんので、ドキュメントを参照してください。
template.yaml を親に cloudformation 配下にテンプレートを分割して配置しています。
1.
2├── template.yaml
3└── cloudformation
4 ├── rds.yaml
5 ├── sam.yaml
6 ├── secrets.yaml
7 └── vpc.yaml
各テンプレートは Description などは省いて最低限 sam deply でデプロイが完了する状態を目指しています。
まずは親スタックのテンプレート。
template.yaml
1AWSTemplateFormatVersion: '2010-09-09'
2Transform: AWS::Serverless-2016-10-31
3Description: >
4 blog-rds-proxy
5 Parent Template
6
7Parameters:
8 VPCCidr:
9 Type: String
10 Default: 10.1.0.0/16
11 PrivateSubnet1Cidr:
12 Type: String
13 Default: 10.1.0.0/24
14 PrivateSubnet2Cidr:
15 Type: String
16 Default: 10.1.1.0/24
17 RDSMasterUsername:
18 Type: String
19 Default: root
20 RDSDatabaseName:
21 Type: String
22 Default: blog_rds_proxy
23
24Resources:
25 VPCStack:
26 Type: AWS::Serverless::Application
27 Properties:
28 Location: cloudformation/vpc.yaml
29 Parameters:
30 VPCCidr: !Ref VPCCidr
31 PrivateSubnet1Cidr: !Ref PrivateSubnet1Cidr
32 PrivateSubnet2Cidr: !Ref PrivateSubnet2Cidr
33 SecretsStack:
34 Type: AWS::Serverless::Application
35 Properties:
36 Location: cloudformation/secrets.yaml
37 Parameters:
38 RDSMasterUsername: !Ref RDSMasterUsername
39 RDSStack:
40 Type: AWS::Serverless::Application
41 Properties:
42 Location: cloudformation/rds.yaml
43 Parameters:
44 RDSDatabaseName: !Ref RDSDatabaseName
45 RDSMasterUserSecretArn: !GetAtt SecretsStack.Outputs.RDSMasterUserSecretArn
46 PrivateSubnet1Id: !GetAtt VPCStack.Outputs.PrivateSubnet1Id
47 PrivateSubnet2Id: !GetAtt VPCStack.Outputs.PrivateSubnet2Id
48 DBClusterSecurityGroupId: !GetAtt VPCStack.Outputs.DBClusterSecurityGroupId
49 RDSProxySecurityGroupId: !GetAtt VPCStack.Outputs.RDSProxySecurityGroupId
50 SAMStack:
51 Type: AWS::Serverless::Application
52 Properties:
53 Location: cloudformation/sam.yaml
54 Parameters:
55 PrivateSubnet1Id: !GetAtt VPCStack.Outputs.PrivateSubnet1Id
56 PrivateSubnet2Id: !GetAtt VPCStack.Outputs.PrivateSubnet2Id
57 FunctionSecurityGroupId: !GetAtt VPCStack.Outputs.FunctionSecurityGroupId
58 RDSMasterUsername: !Ref RDSMasterUsername
59 RDSDatabaseName: !Ref RDSDatabaseName
60 RDSProxyEndpoint: !GetAtt RDSStack.Outputs.RDSProxyEndpoint
61 RDSProxyArn: !GetAtt RDSStack.Outputs.RDSProxyArn
62
63Outputs:
64 APIEndpoint:
65 Value: !GetAtt SAMStack.Outputs.APIEndpoint
66 RDSProxyEndpoint:
67 Value: !GetAtt RDSStack.Outputs.RDSProxyEndpoint
68 GetSecretValueByCLI:
69 Value: !GetAtt SecretsStack.Outputs.GetSecretValueByCLI
SAM-CLI を使うので、子スタックは Type: AWS::Serverless::Application で定義しています。詳細についてはドキュメントを参照してください。
Parameters について。
VPC の Cidr は実行するAWSアカウントに合わせます。
RDS の DatabaseName は デプロイ後に Create Database するのが面倒だったのでインスタンス作成時に作成されるように設定しています。
MasterUser のパスワードについては後述の AWS Secrets Manager で生成するため Parameters で指定していません。
各スタックに渡す Parameters の中にはスタックの Outputs で定義した出力値を渡しているものがあります。詳細については以下を参照してください。
VPC Stack
Amazon Aurora MySQL は VPC 内に構築が必要ですので VPC から作っていきます。
多くの場合 データベース は Private Subnet 内に構築することが多いと思います。今回は記述量の簡略化のために Private Subnet のみで以下リソースを配置します。
- Amazon Aurora MySQL
- Amazon RDS Proxy
- AWS Lambda
vpc.yaml
1AWSTemplateFormatVersion: '2010-09-09'
2Description: >
3 blog-rds-proxy
4 for VPC template
5
6
7Parameters:
8 VPCCidr:
9 Type: String
10 PrivateSubnet1Cidr:
11 Type: String
12 PrivateSubnet2Cidr:
13 Type: String
14
15Resources:
16 # VPC
17 VPC:
18 Type: AWS::EC2::VPC
19 Properties:
20 CidrBlock: !Ref VPCCidr
21 EnableDnsHostnames: 'true'
22 EnableDnsSupport: 'true'
23 InstanceTenancy: default
24 PrivateSubnet1:
25 Type: AWS::EC2::Subnet
26 Properties:
27 AvailabilityZone: ap-northeast-1a
28 CidrBlock: !Ref PrivateSubnet1Cidr
29 VpcId: !Ref VPC
30 PrivateSubnet2:
31 Type: AWS::EC2::Subnet
32 Properties:
33 AvailabilityZone: ap-northeast-1c
34 CidrBlock: !Ref PrivateSubnet2Cidr
35 VpcId: !Ref VPC
36 PrivateRouteTable:
37 Type: AWS::EC2::RouteTable
38 Properties:
39 VpcId: !Ref VPC
40 PrivateSubnet1RouteTableAssociation:
41 Type: AWS::EC2::SubnetRouteTableAssociation
42 Properties:
43 SubnetId: !Ref PrivateSubnet1
44 RouteTableId: !Ref PrivateRouteTable
45 PrivateSubnet2RouteTableAssociation:
46 Type: AWS::EC2::SubnetRouteTableAssociation
47 Properties:
48 SubnetId: !Ref PrivateSubnet2
49 RouteTableId: !Ref PrivateRouteTable
50
51 # SecurityGroup
52 FunctionSecurityGroup:
53 Type: AWS::EC2::SecurityGroup
54 Properties:
55 GroupDescription: for Lambda Function
56 VpcId: !Ref VPC
57 DBClusterSecurityGroup:
58 Type: AWS::EC2::SecurityGroup
59 Properties:
60 GroupDescription: for DB Cluster
61 VpcId: !Ref VPC
62 SecurityGroupIngress:
63 - IpProtocol: tcp
64 FromPort: 3306
65 ToPort: 3306
66 SourceSecurityGroupId: !Ref RDSProxySecurityGroup
67 RDSProxySecurityGroup:
68 Type: AWS::EC2::SecurityGroup
69 Properties:
70 GroupDescription: for RDS Proxy
71 VpcId: !Ref VPC
72 SecurityGroupIngress:
73 - IpProtocol: tcp
74 FromPort: 3306
75 ToPort: 3306
76 SourceSecurityGroupId: !Ref FunctionSecurityGroup
77
78Outputs:
79 PrivateSubnet1Id:
80 Value: !Ref PrivateSubnet1
81 PrivateSubnet2Id:
82 Value: !Ref PrivateSubnet2
83 FunctionSecurityGroupId:
84 Value: !Ref FunctionSecurityGroup
85 DBClusterSecurityGroupId:
86 Value: !Ref DBClusterSecurityGroup
87 RDSProxySecurityGroupId:
88 Value: !Ref RDSProxySecurityGroup
Amazon Aurora クラスターは複数の AvailabilityZone に属した Subnet が必要なため PrivateSubnet は2つ作成しています。
SecurityGroup もこのテンプレートにまとめています。SecurityGroup では各リソース間のアクセス制限をしています。
DBClusterSecurityGroup を Amazon Aurora MySQL に割り当てて RDSProxySecurityGroup からのアクセスを許可。
RDSProxySecurityGroup を Amazon RDS Proxy に割り当てて FunctionSecurityGroup からのアクセスを許可。
FunctionSecurityGroup は AWS Lambda に割り当てます。
Secrets Stack
Amazon RDS Proxy から Amazon Aurora MySQL への認証情報(username, password)は AWS Secrets Manager に保存する必要があります。保存する形式は以下のような JSON 形式で保存します。
※AWS Secrets Manager でのデータベース認証情報の設定
1{"username":"your_username","password":"your_password"}
username のみ Parameters から受け取り password は AWS Secrets Manager で作成するようにしています。
secrets.yaml
1AWSTemplateFormatVersion: '2010-09-09'
2Description: >
3 blog-rds-proxy
4 for Secrets template
5
6Parameters:
7 RDSMasterUsername:
8 Type: String
9
10Resources:
11 # SecretsManager
12 RDSMasterUserSecret:
13 Type: AWS::SecretsManager::Secret
14 Properties:
15 GenerateSecretString:
16 SecretStringTemplate: !Sub '{"username": "${RDSMasterUsername}"}'
17 GenerateStringKey: password
18 PasswordLength: 16
19 ExcludeCharacters: '"@/\'
20
21Outputs:
22 RDSMasterUserSecretArn:
23 Value: !Ref RDSMasterUserSecret
24 GetSecretValueByCLI:
25 Value: !Sub >
26 aws secretsmanager get-secret-value
27 --secret-id ${RDSMasterUserSecret}
28 --region ${AWS::Region}
29 --query SecretString
Outputs の GetSecretValueByCLI は作成した Password の確認用の AWS-CLI のコマンドです。
RDS Stack
Amazon Aurora MySQL と Amazon RDS Proxy を作成します。
rds.yaml
1AWSTemplateFormatVersion: '2010-09-09'
2Description: >
3 blog-rds-proxy
4 for RDS template
5
6Parameters:
7 RDSMasterUserSecretArn:
8 Type: String
9 RDSDatabaseName:
10 Type: String
11 PrivateSubnet1Id:
12 Type: String
13 PrivateSubnet2Id:
14 Type: String
15 DBClusterSecurityGroupId:
16 Type: String
17 RDSProxySecurityGroupId:
18 Type: String
19
20Resources:
21 # RDS
22 DBCluster:
23 Type: AWS::RDS::DBCluster
24 Properties:
25 MasterUsername: !Sub '{{resolve:secretsmanager:${RDSMasterUserSecretArn}:SecretString:username}}'
26 MasterUserPassword: !Sub '{{resolve:secretsmanager:${RDSMasterUserSecretArn}:SecretString:password}}'
27 Engine: aurora-mysql
28 EngineVersion: 5.7.mysql_aurora.2.10.0
29 DatabaseName: !Ref RDSDatabaseName
30 DBSubnetGroupName: !Ref DBClusterSubnetGroup
31 VpcSecurityGroupIds:
32 - !Ref DBClusterSecurityGroupId
33 DBInstance1:
34 Type: AWS::RDS::DBInstance
35 Properties:
36 DBClusterIdentifier: !Ref DBCluster
37 DBSubnetGroupName: !Ref DBClusterSubnetGroup
38 Engine: aurora-mysql
39 EngineVersion: 5.7.mysql_aurora.2.10.0
40 DBInstanceClass: db.t3.small
41 DependsOn: DBCluster
42 DBClusterAttachment:
43 Type: AWS::SecretsManager::SecretTargetAttachment
44 DependsOn: DBCluster
45 Properties:
46 SecretId: !Ref RDSMasterUserSecretArn
47 TargetId: !Ref DBCluster
48 TargetType: AWS::RDS::DBCluster
49 DBClusterSubnetGroup:
50 Type: AWS::RDS::DBSubnetGroup
51 Properties:
52 DBSubnetGroupDescription: for DB Cluster
53 SubnetIds:
54 - !Ref PrivateSubnet1Id
55 - !Ref PrivateSubnet2Id
56 # RDS Proxy
57 RDSProxy:
58 Type: AWS::RDS::DBProxy
59 Properties:
60 DBProxyName: blog-rds-proxy-for-db-cluster
61 EngineFamily: MYSQL
62 RequireTLS: True
63 RoleArn: !GetAtt RDSProxyRole.Arn
64 Auth:
65 - AuthScheme: SECRETS
66 SecretArn: !Ref RDSMasterUserSecretArn
67 IAMAuth: REQUIRED
68 VpcSecurityGroupIds:
69 - !Ref RDSProxySecurityGroupId
70 VpcSubnetIds:
71 - !Ref PrivateSubnet1Id
72 - !Ref PrivateSubnet2Id
73 RDSProxyTargetGroup:
74 Type: AWS::RDS::DBProxyTargetGroup
75 DependsOn:
76 - DBCluster
77 - DBInstance1
78 Properties:
79 DBProxyName: !Ref RDSProxy
80 DBClusterIdentifiers:
81 - !Ref DBCluster
82 TargetGroupName: default
83 RDSProxyRole:
84 Type: AWS::IAM::Role
85 Properties:
86 AssumeRolePolicyDocument:
87 Version: 2012-10-17
88 Statement:
89 - Effect: Allow
90 Principal:
91 Service: rds.amazonaws.com
92 Action: sts:AssumeRole
93 Policies:
94 - PolicyName: AllowGetSecretValue
95 PolicyDocument:
96 Version: 2012-10-17
97 Statement:
98 - Effect: Allow
99 Action:
100 - secretsmanager:GetSecretValue
101 - secretsmanager:DescribeSecret
102 Resource:
103 - !Ref DBClusterAttachment
104 - !Ref RDSMasterUserSecretArn
105
106Outputs:
107 RDSProxyEndpoint:
108 Value: !GetAtt RDSProxy.Endpoint
109 RDSProxyArn:
110 Value: !GetAtt RDSProxy.DBProxyArn
DBCluster の MasterUsername, MasterUserPassword は SecretsStack で作成した シークレットのArnから取得するようになっています。
Engine についてはサンプルなので Amazon Aurora Serverless を使いたいところでしたが、 2021/08 現在 Amazon RDS Proxy は Amazon Aurora Serverless に対応していませんでした。残念。
AWS::RDS::DBCluster のパラメータに明示的に設定していませんが、Amazon RDS Proxy から Amazon Aurora MySQL へはパスワード認証になるためIAM認証(EnableIAMDatabaseAuthentication)は無効にしておく必要があります。
Amazon RDS Proxy は AWS Lambda から IAM認証で接続する想定のため AWS::RDS::DBProxy の Auth で IAMAUth: REQUIRED に、また IAM認証には TLS が必須ですので RequireTLS: True にもしておく必要があります。
RDSProxyRole では AWS Secrets Manager から認証情報を取得するため、必要な Policy (secretsmanager:GetSecretValue, secretsmanager:DescribeSecret) を付与しています。
SAM Stack
最後に SAMStack では AWS::Serverless::Function で AWS Lambda と Amazon API Gateway を作成します。Amazon API Gateway は HTTP API を使用します。
sam.yaml
1AWSTemplateFormatVersion: '2010-09-09'
2Transform: AWS::Serverless-2016-10-31
3Description: >
4 blog-rds-proxy
5 for SAM Template
6
7Parameters:
8 PrivateSubnet1Id:
9 Type: String
10 PrivateSubnet2Id:
11 Type: String
12 RDSMasterUsername:
13 Type: String
14 FunctionSecurityGroupId:
15 Type: String
16 RDSDatabaseName:
17 Type: String
18 RDSProxyEndpoint:
19 Type: String
20 RDSProxyArn:
21 Type: String
22
23Resources:
24 # LambdaFunction
25 BlogRDSProxyFunction:
26 Type: AWS::Serverless::Function
27 Properties:
28 CodeUri: ../src/
29 Handler: main
30 Runtime: go1.x
31 Timeout: 29
32 VpcConfig:
33 SecurityGroupIds:
34 - !Ref FunctionSecurityGroupId
35 SubnetIds:
36 - !Ref PrivateSubnet1Id
37 - !Ref PrivateSubnet2Id
38 Environment:
39 Variables:
40 RDS_PROXY_ENDPOINT: !Ref RDSProxyEndpoint
41 RDS_USER: !Ref RDSMasterUsername
42 RDS_DATABASE_NAME: !Ref RDSDatabaseName
43 Events:
44 CatchAllApi:
45 Type: HttpApi
46 Properties:
47 Path: '{proxy+}'
48 Method: ANY
49 Policies:
50 - Version: 2012-10-17
51 Statement:
52 - Effect: Allow
53 Action: rds-db:connect
54 Resource: !Sub arn:aws:rds-db:${AWS::Region}:${AWS::AccountId}:dbuser:${RDSProxyArn}/${RDSMasterUsername}
55 - AWSLambdaVPCAccessExecutionRole
56
57Outputs:
58 APIEndpoint:
59 Value: !Sub https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/
Amazon RDS Proxy にアクセスするため AWS Lambda は VPC 上に配置します。Private Subent でも Amazon API Gateway から接続はできるようです。
AWS Lambda の IAM Role には Amazon RDS Proxy へ IAM認証で接続するため Policy に rds-db:connect を付与する必要があります 。
余談ですが rds-db:connect で指定しているリソースは ドキュメント では以下のように記載されています。
1arn:aws:rds-db:region:account-id:dbuser:DbiResourceId/db-user-name
Amazon RDS のコンソールでは DbiResourceId は明示的に表示されているのですが、Amazon RDS Proxy のコンソールでは表示されていません。この ID は Arn の末尾の値になるので、最初はテンプレート上で Arn から文字列を分割して取り出したりしていたのですが 実は Arn そのままでいいようです。末尾の ID だけ指定すると同一アカウントの同一リージョンのリソースと判定されるようです。(別のアカウントのリソースだったりする場合はフルの Arn を指定する必要がある)
以上でリソースの作成は完了です。
AWS Lambda 実装(Go)
AWS Lambda の実装面では Amazon RDS Proxy への IAM認証について解説します。
1import (
2 "crypto/tls"
3 "crypto/x509"
4 "database/sql"
5 _ "embed"
6 "errors"
7 "fmt"
8 "net/http"
9 "os"
10
11 "github.com/aws/aws-sdk-go/aws/credentials"
12 "github.com/aws/aws-sdk-go/service/rds/rdsutils"
13
14 "github.com/go-sql-driver/mysql"
15)
16
17const (
18 routeInitialize = "/initialize"
19 routeLoad = "/load"
20)
21
22const (
23 driverName = "mysql"
24 mysqlPort = 3306
25)
26
27//go:embed AmazonRootCA1.pem
28var amazonRootCA1 []byte
29
30var (
31 awsRegion = os.Getenv("AWS_REGION")
32 rdsProxyEndpoint = os.Getenv("RDS_PROXY_ENDPOINT")
33 rdsUser = os.Getenv("RDS_USER")
34 databaseName = os.Getenv("RDS_DATABASE_NAME")
35)
36
37func dbConnect() (*sql.DB, error) {
38 host := fmt.Sprintf("%s:%d", rdsProxyEndpoint, mysqlPort)
39 cfg := &mysql.Config{
40 User: rdsUser,
41 Addr: host,
42 Net: "tcp",
43 Params: map[string]string{
44 "tls": "true",
45 },
46 DBName: databaseName,
47 AllowCleartextPasswords: true,
48 AllowNativePasswords: true,
49 }
50
51 paswd, err := rdsutils.BuildAuthToken(
52 cfg.Addr,
53 awsRegion,
54 cfg.User,
55 credentials.NewEnvCredentials(),
56 )
57 if err != nil {
58 return nil, fmt.Errorf("rds build auth token error occurred: %w", err)
59 }
60 cfg.Passwd = paswd
61
62 err = registerRDSMysqlCerts(http.DefaultClient)
63 if err != nil {
64 return nil, fmt.Errorf("register rds mysql certs error occurred: %w", err)
65 }
66
67 fmt.Printf("dsn: %s\n", cfg.FormatDSN())
68 db, err := sql.Open(driverName, cfg.FormatDSN())
69 if err != nil {
70 return nil, fmt.Errorf("sql open error occurred: %w", err)
71 }
72
73 if err := db.Ping(); err != nil {
74 return nil, fmt.Errorf("db ping error occurred: %w", err)
75 }
76 return db, nil
77}
78
79// refs: https://github.com/aws/aws-sdk-go/issues/1248#issuecomment-374837105
80func registerRDSMysqlCerts(c *http.Client) error {
81 rootCertPool := x509.NewCertPool()
82 if ok := rootCertPool.AppendCertsFromPEM(amazonRootCA1); !ok {
83 return errors.New("couldn't append certs from pem")
84 }
85 if err := mysql.RegisterTLSConfig("rds", &tls.Config{RootCAs: rootCertPool, InsecureSkipVerify: true}); err != nil {
86 return err
87 }
88 return nil
89}
上記はデータベース接続箇所を抜き出しています。
Amazon RDS Proxy のIAM認証では TLS が必要なため証明書を登録する必要があります。 (registerRDSMysqlCerts)
通常の Amazon RDS への IAM認証とは使用する証明書が異なるため注意が必要です。
上記実装では AmazonRootCA1.pem というファイル名で配置して go:embed で変数に埋め込んでいます。(便利!)
Go の場合のIAM認証をつかった接続のドキュメント では、dsn は以下で作成されています。
1dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?tls=true&allowCleartextPasswords=true",
2 dbUser, authToken, dbEndpoint, dbName,
3)
github.com/go-sql-driver/mysql@1.4.0 から allowCleartextPasswords=true に加えて allowNativePasswords=true が必要になったようなので以下のように config を作成しました。
1cfg := &mysql.Config{
2 User: rdsUser,
3 Addr: host,
4 Net: "tcp",
5 Params: map[string]string{
6 "tls": "true",
7 },
8 DBName: databaseName,
9 AllowCleartextPasswords: true,
10 AllowNativePasswords: true,
11}
cfg.FomartDSN() というメソッドで config に応じた dsn の文字列を返してくれます。
(これを書いたあとに実装を読んでると mysql.NewConfig() で作成すると allowNativePasswords はデフォルト true がセットされて返ってくるようでした。)
パスワードは IAM 認証では BuildAuthToken で一時パスワードを発行してから config にセットしています。
Go でのデータベース接続は以上です。
まとめ (感想)
さて、ここまでやってなのですが、サーバーレスで API というアプリケーションの場合で Amazon RDS 使うなら Public Subnet に配置するほうが使いやすいかもしれません。
というのも運用面ではデータベースへ接続するクライアントアプリケーションが必要になると思いますが、 Private Subnet にデータベースがあるとクライアントからの接続用に踏み台インスタンス立てる必要がでてきます。そうなってくるとサーバレスアプリケーションなのに踏み台インスタンスを管理する必要があり本末転倒?になりそうな気がしました。
既存の Private Subnet に配置された Amazon RDS をつかったアプリケーションがあって、一部 AWS Lambda で処理する、、というときが今回のようなケースになるかなと思いました。
ちなみに意地でも踏み台インスタンス立てない、ってやったのでテーブル作成のクエリとかデータ投入・取得は専用のエンドポイントをつくって AWS Lambda 越しに実行して確認しました。(素直に踏み台つくったほうが検証がスムーズだったと思います…)
以上、AWS Lambda から Amazon RDS Proxy を使う場合の参考になれば幸いです。