【AWS CDK】「CDKを用いた1APP2StackでのAurora Global Databaseの実装」

## はじめに
Auroraには、マルチリージョンレプリケーションを構成するGlobal Databaseの機能があります。
[Amazon Aurora Global Database](https://aws.amazon.com/jp/rds/aurora/global-database/)

目的としては、各リージョンにリードレプリカを置いて、リージョン毎のレイテンシーを低減させるためですが、DR(Disaster Recovery)観点でも活用される機能かと思われます。

この機能自体をCDKで実装する場合、いくつかポイントがあるので、そのポイントをご紹介させて頂きます。
本記事では、Stack間のパラメータ引き渡しがない、1APP2Stack構成でのご紹介となります。
※CDKを用いて実装する場合、Stackコードはリージョンに紐つくため、1Stackコードでは実装することは出来ません。ソースとなるプライマリDBクラスタ(Aurora Writerインスタンス)をDeployするリージョンに紐つくStack、セカンダリDBクラスタ(Aurora Readerインスタンス)をDeployするリージョンに紐つくStackの2つに分割する必要があります。

※ 本ブログに記載した内容は個人の見解であり、所属する会社、組織とは全く関係ありません。

## 1APP 2Stack構成でのAurora Global Databaseの実装
本サンプルでは、以下シナリオ構成でAurora　Serverless V2のGlobal Databaseの実装します。

**シナリオ**
ソースとなるプライマリDBクラスタ(Aurora Writerインスタンス)を東京Regionに、セカンダリDBクラスタ(Aurora Readerインスタンス)を大阪RegionにDeployする。

このサンプルは以下6つのセクションに分割されます。
- APPコード
- パラメータファイル
- ソースとなるプライマリDBクラスタ(Aurora Writerインスタンス)用Stackコード
- ソースとなるプライマリDBクラスタ(Aurora Writerインスタンス)用Constructsコード
- セカンダリDBクラスタ(Aurora Readerインスタンス)用Stackコード
- セカンダリDBクラスタ(Aurora Readerインスタンス)用Constructsコード

ポイントとしては、プライマリDBクラスタ側でも、セカンダリDBクラスタ側でも同じ"globalClusterIdentifier"を指定する必要があります。
よって、簡略化、ミスの抑制を意識し、このパラメータを外に出し同じパラメータを参照させるようにしています。
※利用しているパラメータの外だし手法に関しては、以下のリンクの記事をご参照ください。
[【AWS CDK】「CDKにおけるパラメータの外だし方法 ~自前TypeScript実装編~」](https://qiita.com/akwayne/items/619e34b4d94e4ecd542d)

詳細は、それぞれのセクションに分けてご紹介させて頂きます。

### APPコード
globalAuroraがプライマリDBクラスタを東京RegionにDeployします。
replicaAuroraがセカンダリDBクラスタを大阪RegionにDeployします。

~~~typescript:sample-global-aurora.ts
// プライマリDBクラスタ
const globalAurora = new SampleGlobalAuroraStack(app, 'globalAurpra', {
    env: {
    region: "ap-northeast-1",
    account: "xxxx"
  },
tsEnv: envName,
});

// セカンダリDBクラスタ
const replicaAurora =  new SampleReplicaAuroraStack(app, 'replicaAurora', {
    env: {
    region: "ap-northeast-3",
    account: "xxxx"
  },
tsEnv: envName,
});
~~~
APPコードを構成するポイントはStack Constructsに引き渡すprops(引数のセット)です。

**env**
env propsにてDeployする先のRegionを指定することが出来ます。
そのため、globalAuroraではap-northeast-1(東京)を、replicaAuroraではap-northeast-3(大阪)を指定しています。

### パラメータファイル
パラメータは以下のように組んでいます。
nwTokyoの部分はglobalAurora専用、nwOsakaの部分はreplicaAurora専用、globaldbの部分は両Stack共用といった構成にしています。
この共用箇所設けることにより、管理簡略化、ミスの抑制を狙っています。
~~~typescript:Dev.ts
export const nwTokyo = {
  cidr: "10.0.0.0/24",
};
export const nwOsaka = {
  cidr: "10.0.1.0/24",
};

export const globaldb = {
  globalClusterId: "global-test",
};
~~~

### ソースとなるプライマリDBクラスタ(Aurora Writerインスタンス)Stackコード
ソースとなる プライマリDBクラスタとAurora Writerインスタンスを構成するStackコードです。
特徴的な箇所は有りませんが、パラメータ外だしに関連する対応を実施しています。

~~~typescript:sample-global-aurora-stack.ts
  constructor(scope: Construct, id: string, props: SampleGlobalAuroraStackProps) {
    super(scope, id, props);
    const config = require("../config/" + props.tsEnv);
    const globalAurora = new GlobalAurora(this, "globalaurora", {
      cidr: config.nwTokyo.cidr,
      globalClusterId: config.globaldb.globalClusterId
    })
  }
~~~

### ソースとなるプライマリDBクラスタ(Aurora Writerインスタンス)用Constructsコード
ソースとなる プライマリDBクラスタとAurora Writer インスタンスを構成するConstructsコードです。

~~~typescript:sample-global-aurora-constructs.ts
    //Aurora Cluster
    const myMainAuroraCluster = new rds.DatabaseCluster(this, "MainAuroraCluster", {
      engine: rds.DatabaseClusterEngine.auroraPostgres({ version: rds.AuroraPostgresEngineVersion.VER_14_4 }),
      clusterIdentifier: `mainauroracluster`,
      writer: rds.ClusterInstance.serverlessV2("writer", {
        autoMinorVersionUpgrade: false,
        enablePerformanceInsights: true,
        instanceIdentifier: `MainAuroraWriter`,
        performanceInsightRetention: 93,
        scaleWithWriter: true,
      }),
      serverlessV2MaxCapacity: 2,
      serverlessV2MinCapacity: 1,
      backup:{
        retention: cdk.Duration.days(30),
        preferredWindow: "18:00-19:00",
      },
      cloudwatchLogsExports: ["postgresql"],
      cloudwatchLogsRetention: logs.RetentionDays.THREE_MONTHS,
      deletionProtection: false, 
      preferredMaintenanceWindow: "Sat:17:00-sat:18:00",
      subnetGroup: myMainAuroraSubnetGp,
      storageEncrypted: false,
      vpc: myVpc,
    });
    
    // Aurora ClusterをGlobal Database化
    new rds.CfnGlobalCluster(scope, 'MainGlobalDatabase', {
      globalClusterIdentifier: props.globalClusterId,
      sourceDbClusterIdentifier: myMainAuroraCluster.clusterIdentifier
      })
~~~

**注意点**
本構成では、ストレージ暗号化せずに構成しております。
暗号化する場合は、以下propsを指定してください。
* storageEncrypted
* storageEncryptionKey 

また、Global Databaseを利用する場合は、KMSマルチリージョンキーを用いる必要があります。
CDKを用いたマルチリージョンキーの実装に関しては以下をご確認ください。
[KMSマルチリージョンキーの実装](https://qiita.com/akwayne/items/fc697010e6a07f6629ef)

ポイントは作成したAurora ClusterをGlobal Databaseにしているところです。

**new rds.CfnGlobalCluster(scope, 'MainGlobalDatabase', {**
L1 CfnGlobalCluster Constructsを用いてGlobal Database化しています。

**globalClusterIdentifier: props.globalClusterId,**
グローバルデータベースの識別子です。
パラメータファイルからglobalClusterIdを呼び込みglobalClusterIdentifierに入れ込んでいます。
この値がGlobal Databaseを組むときのKeyとなります。

**sourceDbClusterIdentifier: myMainAuroraCluster.clusterIdentifier**
作成したAurora Clusterを指定し、このGlobal Databaseのソースとしています。

### セカンダリDBクラスタ(Aurora Readerインスタンス)用Stackコード
レプリケーション先のセカンダリDBクラスタとAurora Readerを構成するStackコードです。
特徴的な箇所は有りませんが、パラメータ外だしに関連する対応を実施しています。

~~~typescript:sample-replica-aurora-stack.ts
  constructor(scope: Construct, id: string, props: SampleReplicaAuroraStackProps) {
    super(scope, id, props);
    const config = require("../config/" + props.tsEnv);
    new ReplicaAurora(this, "ReplicaAsurora", {
      cidr: config.nwOsaka.cidr,
      globalClusterId: config.globaldb.globalClusterId
    }
    )
  }
~~~

### セカンダリDBクラスタ(Aurora Readerインスタンス)用Constructsコード
レプリケーション先のセカンダリDBクラスタとAurora Reader を構成するConstructsコードです。

~~~typescript:sample-replica-aurora-constructs.ts
    //Aurora Cluster
    const myMainAuroraCluster = new rds.DatabaseCluster(this, "MainAuroraCluster", {
      engine: rds.DatabaseClusterEngine.auroraPostgres({ version: rds.AuroraPostgresEngineVersion.VER_14_4 }),
      clusterIdentifier: `mainauroracluster`,
      writer: rds.ClusterInstance.serverlessV2("writer", {
        autoMinorVersionUpgrade: false,
        enablePerformanceInsights: true,
        instanceIdentifier: `MainAuroraWriter`,
        performanceInsightRetention: 93,
        scaleWithWriter: true,
      }),
      serverlessV2MaxCapacity: 2,
      serverlessV2MinCapacity: 1,
      backup:{
        retention: cdk.Duration.days(30),
        preferredWindow: "18:00-19:00",
      },
      cloudwatchLogsExports: ["postgresql"],
      cloudwatchLogsRetention: logs.RetentionDays.THREE_MONTHS,
      deletionProtection: false, 
      preferredMaintenanceWindow: "Sat:17:00-sat:18:00",
      subnetGroup: myMainAuroraSubnetGp,
      storageEncrypted: false,
      vpc: myVpc,
    });
    
    
    //エスケープハッチ
    const cfnReplicaCluster = myMainAuroraCluster.node.defaultChild as rds.CfnDBCluster
    cfnReplicaCluster.masterUsername = undefined;
    cfnReplicaCluster.masterUserPassword = undefined;
    cfnReplicaCluster.globalClusterIdentifier = props.globalClusterId
~~~
**注意点**
Writerインスタンスから同期されるため、「defaultDatabaseName」は指定できません。


ポイントはAurora Serverless V2のインスタンスを指定しているところと、エスケープハッチをしているところの2箇所になります。

**writer: rds.ClusterInstance.serverlessV2("writer", {**
Readerインスタンスなのになぜwriterって感じですよね？
L2 DatabaseCluster Constructsの仕様により、Wtiterインスタンスを必ず配置する必要があります。Readerだけで書いてしまうとエラーが発生します。
しかし、実際GlobalDatabaseでDeployするとReaderインスタンスとしてdeployされるので、問題はありません。
仕様上仕方ありませんが、紛らわしいですね。


**エスケープハッチ**
L2 DatabaseCluster Constructsでは、GlobalDatabaseに関わる設定をすることが出来ません。
そのため、エスケープハッチで設定を当て込んでいます。
[CDK Constructs](https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/constructs.html)

エスケープハッチの詳しい検討方法等は以下の記事をご参照ください。
[エスケープハッチサンプル](https://qiita.com/akwayne/items/2b44d348fca06a65791a)

まずは、「tree.json」を確認します。
~~~tree.json
                  "Resource": {
                    "id": "Resource",
                    "path": "replicaAurora/ReplicaAsurora/MainAuroraCluster/Resource",
                    "attributes": {
                      "aws:cdk:cloudformation:type": "AWS::RDS::DBCluster",
~~~

replicaAuroraのMainAuroraClusterのPathがResourceになっており、CloudFormation:typeもRDS::DBClusterになっています。
よって、myMainAuroraClusterのdefaultchildはCfnDBClusterにキャスト出来ることがわかります。

**const cfnReplicaCluster = myMainAuroraCluster.node.defaultChild as rds.CfnDBCluster**
エスケープハッチし、CfnDBClusterにキャストし、cfnReplicaCluster に入れています。

**cfnReplicaCluster.masterUsername = undefined;**
**cfnReplicaCluster.masterUserPassword = undefined;**
Read Replicaインスタンスなので、認証情報はWriterと同期します。そのため、undefinedを指定します。

**cfnReplicaCluster.globalClusterIdentifier = props.globalClusterId**
グローバルデータベースの識別子です。
パラメータファイルからglobalClusterIdを呼び込みglobalClusterIdentifierを指定します。
Writerインスタンス側のDBクラスタと同じパラメータを指定しているので、Global DatabaseのReaderとして構成されます。

## Deploy結果
RDSの画面では以下のようになり、無事Global Databaseとして構成されていることがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635217/56ce5a3c-3a46-58ef-c470-5abc2ca03c2d.png)

## まとめ
今回は、CDKを用いたAurora Global Databaseの構成方法をご紹介させて頂きました。
特段難しい箇所があるということではないんですが、ちょこちょこ素直に使えないポイントがあったり、パラメータ外だしに向いていたり、特徴があります。
このようにDeployするリソースにKeyとして同じパラメータを指定しなければいけないケースなんかには、パラメータ外だし構成は向いていますね。
Aurora Global Databaseのご利用をご検討の際は、ご紹介させて頂いた内容を参考にして頂けると幸いです。
※ 本ブログに記載した内容は個人の見解であり、所属する会社、組織とは全く関係ありません。