AWS CDK - RDS環境構築（Aurora PostgreSQL）

# 内容

![2023-10-30-08-29-36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/695430/14fdef73-92f5-77c9-9c4f-418895f21ff7.png)

- 上記RDS（Aurora PostgreSQL）を使用する構成イメージをCDKで実装
- マルチAZ対応
- Lambda（Python）からRDSを操作想定
- EC2を使用したRDSに対するssh操作

# 前提知識

## RDSのクラスタ、インスタンスとマルチAZ対応

RDSの構成はAurora、非Auroraで異なるため注意が必要  
[こちら](https://www.stylez.co.jp/columns/aurora_or_non-aurora_rds/)に記載されているように、クラスタはAuroraにのみ存在し、インスタンスの構成、役割は以下のように違いがある  

![2023-10-30-09-30-39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/695430/ab6ccfe8-a970-852a-4165-bb898cdb0a69.png)

[マルチ AZ DB クラスターの概要](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/multi-az-db-clusters-concepts.html#multi-az-db-clusters-concepts-overview)にも記載されているように、  
Writerインスタンスで障害が発生した場合は、Readerインスタンスにフェイルオーバーする

![2023-10-30-09-45-13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/695430/52931475-7396-cd52-25b0-1e806e4b6f3e.png)

また、AuroraのインスタンスはプロビジョンドDBインスタンスと、サーバーレス DBインスタンスが存在する
プロビジョンドDBインスタンスは、インスタンスタイプを選択する
サーバーレス DBインスタンスは、オンデマンドでキャパシティがスケールする

![2023-10-30-11-04-51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/695430/66cc0b82-23c0-5c1f-d0c0-825944092e05.png)

## PostgreSQLのデータベース、スキーマ、テーブル

[こちら](https://www.javadrive.jp/postgresql/schema/index1.html)に記載されているように以下の構成になっている

![2023-10-30-09-41-36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/695430/2fb52cad-6910-2218-df05-3b99269e1120.png)

# CDK実装

基本的に[こちら](https://dev.classmethod.jp/articles/aws-cdk-connect-to-amazon-aurora-db-cluster-from-lambda-function-via-rds-proxy/#toc-1)の[実装](https://github.com/non-97/aws-cdk-connect-to-amazon-aurora-db-cluster-from-lambda-function-via-rds-proxy)を参考に実装

## VPC

maxAzsに3以上を指定する場合は、スタック上のアカウントとリージョンを指定する必要がある

```typescript
    const vpc = new ec2.Vpc(this, `${stackName}-Vpc`, {
      ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),
      maxAzs: 2,
      subnetConfiguration: [
        { name: 'Public', subnetType: ec2.SubnetType.PUBLIC, cidrMask: 24 },
        {
          name: 'Private',
          subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
          cidrMask: 24,
        },
        {
          name: 'Isolated',
          subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
          cidrMask: 24,
        },
      ],
    });
```

上記実装で以下のリソースが生成される

```text
- VPC: 10.0.0.0/16
  - InternetGateway
  - PublicSubnet1: 10.0.0.0/24
    - EIP
    - RouteTable
       0.0.0.0/0 → InternetGateway
       10.0.0.0/16 → local
    - NatGateway
  - PublicSubnet2: 10.0.1.0/24
    - EIP
    - RouteTable
       0.0.0.0/0 → InternetGateway
       10.0.0.0/16 → local
    - NatGateway
  - PrivateSubnet1: 10.0.2.0/24
    - RouteTable
       0.0.0.0/0 → NatGateway
       10.0.0.0/16 → local
  - PrivateSubnet2: 10.0.3.0/24
    - RouteTable
       0.0.0.0/0 → NatGateway
       10.0.0.0/16 → local
  - IsolatedSubnet1: 10.0.4.0/24
    - RouteTable
       10.0.0.0/16 → local
  - IsolatedSubnet2: 10.0.5.0/24
    - RouteTable
       10.0.0.0/16 → local
```

## Security Group

RDSにアタッチするSecurity Group  
RDS ProxyにアタッチするSecurity Group  
RDSのSecrets rotateにアタッチするSecurity Group  
RDSのクライアントにアタッチするSecurity Group  
を作成する

```typescript
    /**
     * Security Group for RDS client
     */
    const rdsClientSg = new ec2.SecurityGroup(this, `${stackName}-RdsClientSg`, {
      vpc,
      securityGroupName: `${stackName}-RdsClientSg`,
      allowAllOutbound: true,
    });

    /**
     * Security Group for RDS Secrets rotate
     */
    const rdsRotateSecretsSg = new ec2.SecurityGroup(this, `${stackName}-RdsRotateSecretsSg`, {
      vpc,
      securityGroupName: `${stackName}-RdsRotateSecretsSg`,
      allowAllOutbound: true,
    });

    /**
     * Security Group for RDS Proxy
     */
    const rdsProxySg = new ec2.SecurityGroup(this, `${stackName}-RdsProxySg`, {
      vpc,
      securityGroupName: `${stackName}-RdsProxySg`,
      allowAllOutbound: true,
    });
    rdsProxySg.addIngressRule(ec2.Peer.securityGroupId(rdsClientSg.securityGroupId), ec2.Port.tcp(5432));

    /**
     * Security Group for RDS
     */
    const rdsSg = new ec2.SecurityGroup(this, `${stackName}-RdsSg`, {
      vpc,
      securityGroupName: `${stackName}-RdsSg`,
      allowAllOutbound: true,
    });
    rdsSg.addIngressRule(ec2.Peer.securityGroupId(rdsClientSg.securityGroupId), ec2.Port.tcp(5432));
    rdsSg.addIngressRule(ec2.Peer.securityGroupId(rdsRotateSecretsSg.securityGroupId), ec2.Port.tcp(5432));
    rdsSg.addIngressRule(ec2.Peer.securityGroupId(rdsProxySg.securityGroupId), ec2.Port.tcp(5432));
```

## RDS

Postgres Versionと、EC2インスタンスタイプを文字列で指定できるようにしている
Parameter Groupに名前付けがCDK未対応のため、`addPropertyOverride`でCFNのプロパティを上書きしている、その際`bindToInstance`で即時リソースを作成している
RDSのインスタンスはプロビジョンドDBインスタンスを使用

```typescript
    const auroraPostgresVersion = '15.3';
    const ec2InstanceType = 'r5.large';

    const vpc = ec2.Vpc.fromLookup(this, `${stackName}-RdsVpc`, { vpcId });
    const privateSubnets = vpc.selectSubnets({
        subnetType: SubnetType.PRIVATE_WITH_EGRESS,
    });
    const isolatedSubnets = vpc.selectSubnets({
        subnetType: SubnetType.PRIVATE_ISOLATED,
    });

    /**
     * RDS Admin User Secret
     */
    const secretManagerName = '/test/postgres/admin';
    const EXCLUDE_CHARACTERS = ':@/" \'';
    const rdsAdminSecret = new sm.Secret(this, `${stackName}-RdsAdminSecret`, {
      secretName: secretManagerName,
      generateSecretString: {
        excludeCharacters: EXCLUDE_CHARACTERS,
        generateStringKey: 'password',
        passwordLength: 32,
        requireEachIncludedType: true,
        secretStringTemplate: '{"username": "postgresAdmin"}',
      },
    });

    /**
     * RDS Subnet Group
     */
    const subnetGroupName = `${stackName}-subnet-group`.toLowerCase();
    const subnetGroup = new rds.SubnetGroup(this, subnetGroupName, {
      description: subnetGroupName,
      vpc,
      subnetGroupName: subnetGroupName,
      vpcSubnets: isolatedSubnets,
    });

    /**
     * RDS Parameter Group
     */
    const auroraPostgresMajorVersion = auroraPostgresVersion.split('.')[0];
    const parameterGroupName = `${stackName}-parameter-group`.toLowerCase();
    const parameterGroup = new rds.ParameterGroup(this, parameterGroupName, {
      engine: rds.DatabaseClusterEngine.auroraPostgres({
        version: rds.AuroraPostgresEngineVersion.of(auroraPostgresVersion, auroraPostgresMajorVersion),
      }),
      description: `${parameterGroupName}: aurora-postgresql`,
    });
    parameterGroup.bindToInstance({});
    const cfnParameterGroup = parameterGroup.node.defaultChild as rds.CfnDBParameterGroup;
    cfnParameterGroup.addPropertyOverride('DBParameterGroupName', parameterGroupName);

    /**
     * RDS Cluster
     */
    const [instanceClass, instanceSize] = ec2InstanceType.split('.');
    const rdsClusterName = `${stackName}-rds-cluster`.toLowerCase();
    const readerInstanceName = `${stackName}-rds-reader-instance`.toLowerCase();
    const writerInstanceName = `${stackName}-rds-writer-instance`.toLowerCase();
    const rdsCluster = new rds.DatabaseCluster(this, rdsClusterName, {
      engine: rds.DatabaseClusterEngine.auroraPostgres({
        version: rds.AuroraPostgresEngineVersion.of(auroraPostgresVersion, auroraPostgresMajorVersion),
      }),
      credentials: rds.Credentials.fromSecret(rdsAdminSecret),
      clusterIdentifier: rdsClusterName,
      deletionProtection: true,
      iamAuthentication: true,
      readers: [
        rds.ClusterInstance.provisioned(readerInstanceName, {
          instanceIdentifier: readerInstanceName,
          instanceType: ec2.InstanceType.of(instanceClass as ec2.InstanceClass, instanceSize as ec2.InstanceSize),
          parameterGroup,
        }),
      ],
      securityGroups: [rdsSg],
      storageEncrypted: true,
      subnetGroup,
      vpc,
      writer: rds.ClusterInstance.provisioned(writerInstanceName, {
        instanceIdentifier: writerInstanceName,
        instanceType: ec2.InstanceType.of(instanceClass as ec2.InstanceClass, instanceSize as ec2.InstanceSize),
        parameterGroup,
      }),
    });

    /**
     * RDS Proxy
     */
    const rdsProxyName = `${stackName}-rds-proxy`.toLowerCase();
    const rdsProxy = new rds.DatabaseProxy(this, rdsProxyName, {
      proxyTarget: rds.ProxyTarget.fromCluster(rdsCluster),
      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
      secrets: [rdsCluster.secret!],
      vpc,
      dbProxyName: rdsProxyName,
      debugLogging: true,
      requireTLS: true,
      securityGroups: [rdsProxySg],
      vpcSubnets: privateSubnets,
    });

    /**
     * RDS Secret rotation
     */
    new sm.SecretRotation(this, `${stackName}-DbAdminSecretRotation`, {
      application: sm.SecretRotationApplication.POSTGRES_ROTATION_SINGLE_USER,
      secret: rdsAdminSecret,
      target: rdsCluster,
      vpc,
      automaticallyAfter: Duration.days(3),
      excludeCharacters: EXCLUDE_CHARACTERS,
      securityGroup: rdsRotateSecretsSg,
      vpcSubnets: privateSubnets,
    });
```

# EC2を使用したRDSに対するssh操作

## EC2 作成

適当なEC2インスタンス（Amazon Linux使用）をPrivate Subnetに1つと、踏み台としてPublic Subnetに1つ作成する

## Security Group 設定

RDSにアタッチしたSG:  
  インバウンドルールにTCPポート5432でPrivate SubnetのEC2にアタッチしたSGを許可する  
Private SubnetのEC2にアタッチしたSG:  
  インバウンドルールにTCPポート22でPublic SubnetのEC2にアタッチしたSGを許可する  
Public SubnetのEC2にアタッチしたSG:  
  インバウンドルールにTCPポート22でマイIPを許可する  

## EC2 接続

マネジメントコンソールでEC2インスタンスを選択し、接続ボタンからSSHクライアントの接続方法に従って、  
Public SubnetのEC2に接続し、Public SubnetのEC2からPrivate SubnetのEC2に接続する

![2023-10-30-10-50-51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/695430/f285a9e1-92fd-6794-c438-4e137bca09d1.png)

## クライアント用モジュールインストール

以下コマンドでrootユーザに切替

```text
sudo su -
```

[こちら](https://qiita.com/panayan/items/634ad5ad895f0c9e5bd0)を参考に以下コマンドでモジュールを検索、インストール

```text
yum search postgresql
yum install postgresql15.x86_64
```

クライアントモジュールでRDSに接続する

```text
psql -h xxxxx.yyyyy.ap-northeast-1.rds.amazonaws.com -U hogeuser -d hogedb
```

基本的なコマンドは以下を参照  
[PostgreSQLの基本的なコマンド](https://qiita.com/H-A-L/items/fe8cb0e0ee0041ff3ceb)

# 参考

- [[AWS CDK] Lambda関数からRDS Proxy経由でAmazon Aurora DBクラスターに接続してみた](https://dev.classmethod.jp/articles/aws-cdk-connect-to-amazon-aurora-db-cluster-from-lambda-function-via-rds-proxy/#toc-1)
- [名前を指定する場合](https://dev.classmethod.jp/articles/specify-names-of-rds-parameter-group-and-option-group-in-aws-cloudformation/)
- [マルチ AZ DB クラスターの概要](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/multi-az-db-clusters-concepts.html#multi-az-db-clusters-concepts-overview)
- [Auroraか（非Auroraな）RDSか。比較した上でどちらを使う？](https://www.stylez.co.jp/columns/aurora_or_non-aurora_rds/)
- [PostgreSQLにおけるデータベース、スキーマ、テーブルの関係](https://www.javadrive.jp/postgresql/schema/index1.html)
- [PostgreSQLの基本的なコマンド](https://qiita.com/H-A-L/items/fe8cb0e0ee0041ff3ceb)
- [Amazon LinuxからPostgreSQL(RDS)へ接続する](https://qiita.com/panayan/items/634ad5ad895f0c9e5bd0)