【AWS】RDS ProxyをCloudFormationで構築する

RDS Proxyが2020/6/30にGAされましたね。
最近RDS Proxyに触れる機会があり、サクッと消せるようにCloudFormation(以下CFnと略)を使って構築しました。その時書いたCFnテンプレートを一例としてご紹介します。

RDS Proxyの詳細や運用の勘所については説明しませんので、公式ドキュメントなどを参照してください。

# 要件

- プロキシ先はAurora
- ユーザとパスワードの一般的な認証
    - Secrets Managerを利用
- TLSは必須ではない

# CFnテンプレート

テンプレートが100行超えたのでGitHub Gistにアップロードしました。
テンプレートの全容を見たい方は[homoluctus/rds_proxy.yml](https://gist.github.com/homoluctus/92939c6c9442ebba51f2fa8094124439)を参照してください。

# 解説

## Parameters

```yaml
Parameters:
  # DB Proxy
  ProxyName:
    Type: String
  ProxyEngineFamily:
    Type: String
    AllowedValues:
      - MYSQL
      - POSTGRESQL
  ProxyIdleClientTimeout:
    Type: Number
  ProxyRequireTLS:
    Type: String
    AllowedValues:
      - true
      - false
    Default: false
  ProxyVpcSecurityGroupIds:
    Type: List<AWS::EC2::SecurityGroup::Id>
  ProxyVpcSubnetIds:
    Type: List<AWS::EC2::Subnet::Id>

  # DB Proxy Target Group
  ProxyTargetConnectionBorrowTimeout:
    Type: Number
  ProxyTargetMaxConnectionsPercent:
    Type: Number
  ProxyTargetMaxIdleConnectionsPercent:
    Type: Number
  ProxyTargetDBClusterIdentifiers:
    Type: CommaDelimitedList

  # Secrets Manager
  SecretsManagerRoleName:
    Type: String
  SecretsManagerName:
    Type: String
  SecretsManagerKMSKeyId:
    Type: String
  SecretsManagerManagedPolicyName:
    Type: String
```

## Resources

### DB Proxy

```yaml
RDSProxy:
  Type: "AWS::RDS::DBProxy"
  Properties:
    Auth:
    # Secrets Managerを利用してユーザ名とパスワードで認証
    # 予めSecretsは作成しておく必要があり
      - AuthScheme: SECRETS
        IAMAuth: DISABLED
        SecretArn: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${SecretsManagerName}"
    # 作成するRDS Proxyの名前
    DBProxyName: !Ref ProxyName
    # 2020/08/17時点でMySQLとPostgreSQLがサポートされている
    EngineFamily: !Ref ProxyEngineFamily
    # Proxyとクライアント間の接続がアイドル状態の時に接続切断するまでの秒数
    IdleClientTimeout: !Ref ProxyIdleClientTimeout
    # TLSが必須かどうか
    RequireTLS: !Ref ProxyRequireTLS
    # RDS Proxyに設定するIAM Role
    # 今回はSecrets Managerから値を取得して復号可能なRoleをアタッチ
    RoleArn: !GetAtt SecretsManagerRole.Arn
    # RDS Proxyにアタッチするセキュリティグループ
    VpcSecurityGroupIds: !Ref ProxyVpcSecurityGroupIds
    # RDS Proxyを構築するVPC Subnet
    VpcSubnetIds: !Ref ProxyVpcSubnetIds
```

### DB Proxy Target Group

今回はRDS Proxyの裏にはRDS Auroraが存在すると仮定します。

```yaml
RDSProxyTargetGroup:
  Type: "AWS::RDS::DBProxyTargetGroup"
  Properties:
    # コネクションプールの設定
    ConnectionPoolConfigurationInfo:
      # コネクションプールが使用可能になるまでの待機秒数
      # Proxyが使用可能な最大接続数を使い切っていて、その全接続を使用中の場合に適用される
      ConnectionBorrowTimeout: !Ref ProxyTargetConnectionBorrowTimeout
      # RDS 最大接続数のうちProxyが使用可能な接続数の割合
      MaxConnectionsPercent: !Ref ProxyTargetMaxConnectionsPercent
      # RDSへの接続数のうちアイドル状態の接続数の割合
      # 上限はMaxConnectionsPercentで値が大きければ再度接続するオーバーヘッドが減少する
      MaxIdleConnectionsPercent: !Ref ProxyTargetMaxIdleConnectionsPercent
    # Proxyが接続するRDS Auroraクラスターの識別子
    DBClusterIdentifiers: !Ref ProxyTargetDBClusterIdentifiers
    # AWS::RDS::DBProxyで作成したRDS Proxyの名前
    DBProxyName: !Ref RDSProxy
    # ターゲットグループ名はdefaultを無条件に指定
    # 2020/08/17時点ではdefaultであることが必須
    TargetGroupName: default
```

### IAM Policy

```yaml
SecretsManagerManagedPolicy:
  # 使いまわせるようにインラインポリシーではなくユーザ管理のポリシーを作成
  Type: "AWS::IAM::ManagedPolicy"
  Properties:
    Description: "Get values from Secrets Manager"
    ManagedPolicyName: !Ref SecretsManagerManagedPolicyName
    Path: /
    PolicyDocument:
      Version: "2012-10-17"
      Statement:
        # Secrets ManagerからRDS Proxy用に作成したSecretsを取得
        - Effect: Allow
          Action:
            - "secretsmanager:GetSecretValue"
          Resource: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${SecretsManagerName}"
        # 取得したSecretsを復号する
        - Effect: Allow
          Action:
            - "kms:Decrypt"
          Resource: !Sub "arn:aws:kms:${AWS::Region}:${AWS::AccountId}:key/${SecretsManagerKMSKeyId}"
          Condition:
            StringEquals:
              kms:ViaService: !Sub "secretsmanager.${AWS::Region}.amazonaws.com"
```

### IAM Role

```yaml
SecretsManagerRole:
  Type: "AWS::IAM::Role"
  Properties:
    AssumeRolePolicyDocument:
      Version: "2012-10-17"
      Statement:
        # RoleをRDSに渡す
        - Effect: Allow
          Principal:
            Service:
              - "rds.amazonaws.com"
          Action:
            - "sts:AssumeRole"
    Description: "Use for RDS Proxy"
    # 作成したユーザ管理ポリシーを指定
    ManagedPolicyArns:
      - !Ref SecretsManagerManagedPolicy
    Path: /
    RoleName: !Ref SecretsManagerRoleName
```

# 終わりに

アンチパターンとされてきたLambda + RDSの構成を解決するRDS Proxyは素晴らしい :clap: :tada: :confetti_ball:
ただSQLによってはピン留めとか発生するのでプロダクションで運用する際には慎重に検討した方がいいでしょう

# Reference

- https://aws.amazon.com/jp/blogs/aws/amazon-rds-proxy-now-generally-available/
- https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbproxy.html
- https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbproxytargetgroup.html