【CloudFormation】AWS Secrets Managerを使いRDS Proxy経由でRDSに接続する方法

#やること
EC2やLambdaからRDSにダイレクトに接続させるのではなく、間にプロキシサーバを挟んでRDSに接続させる方法をとる
同時にCloudFormationで環境の自動構築も行う。

#なぜそうするのか

当初の構成では以下の通り、RDSに対してダイレクトで接続を行なっていました。

Lambda → RDS
EC2    → RDS


このようなRDSに直接接続する構成には問題があるので、間にRDSプロキシを挟む構成にしたいと思います。


![スクリーンショット 2021-12-26 19.19.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1186674/1821ce28-6193-5454-0062-af4b3fd1fff4.png)


Lambda → RDS Proxy → RDS
EC2 → RDS Proxy → RDS

RDSProxyには以下のような特徴があります。

>
RDS Proxy を使用すると、データベーストラフィックの予測不可能なサージを処理できます。このようなサージを処理しないと、接続のオーバーサブスクリプションや新しい接続の急速な作成に伴って発生し、問題の原因となることがあります。RDS Proxy は、データベース接続プールを確立し、このプール内の接続を再利用するため、毎回新しいデータベース接続を開くためのメモリや CPU オーバーヘッドは不要です。オーバーサブスクリプションからデータベースを保護するために、データベース接続の作成数を制御できます。

https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/rds-proxy.html


例えば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のエンドポイント名をホスト名として、ドメイン名と紐付けしてレコードセットを行う

手順は以下の通り

1. セキュリティグループの設定
1. IAMロール作成
1. RDSインスタンスとRDSプロキシ作成  
1. SecretsManagerの作成 
1. Route53でレコード設定 


##1.セキュリティグループの設定
今回はEC2とLambdaからプロキシを通してRDS接続するので、
EC2・Lambdaからのアクセス先がプロキシになるので、プロキシ側でEC2・Lambdaのインバウンド許可し、
プロキシのターゲット先がRDSになるので、RDS側でプロキシのインバウンド許可します。

従って以下の実装を行います。

- RDSのセキュリティグループのインバウンド許可にRDSプロキシセキュリティグループを追加
- RDSプロキシセキュリティグループのインバウンド許可にEC2とLambdaセキュリティグループを追加


```securitygroup.yaml
#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へのアクセス許可を定義

```IAM.yaml
  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で記述していきます。

```RDS.yaml
#-----------------------------------------------------------------------------#
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 を設定することもできます。これにより、長期のシークレットを短期のシークレットに置き換えることが可能となり、侵害されるリスクを大幅に減少させるのに役立ちます

https://docs.aws.amazon.com/ja_jp/secretsmanager/latest/userguide/intro.html

簡単に言うとDB接続への認証情報をAWSのSecrets ManagerっていうサービスのAPIで管理させよう！
その方がセキュリティ的に安全だよ！ということです。

このSecretsManagerのターゲット先にRDSインスタンスのホスト名やらポートを指定します。

それではCloudFormationでコード化しましょう。

```SecretsManager.yaml
#-----------------------------------------------------------------------------#
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でレコードの設定をしていきます。

```Route53.yaml
#-----------------------------------------------------------------------------#
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インスタンスのエンドポイントを外部参照してレコードセットを行いました。

https://docs.aws.amazon.com/ja_jp/Route53/latest/DeveloperGuide/routing-to-rds-db.html

#終わりに

今回紹介したのはRDSプロキシとSecrets Managerの基本的な使い方にとどまっているので、
機会があれば応用的な使い方も試してみたいと思います。