CDKプログラミングのパターン

CDKでリソースを作成するとき、様々なプログラミングのパターンを利用できます。
それぞれの方式にどのようなメリット・デメリットがあるかまとめました。
CDK利用前にどのパターンを用いるか、検討の材料にしてください。

## 目次
1. べた書きする
2. 共通の設定をまとめる
3. ループを利用する
4. ラッパーを利用する
4.1. ラッパー関数を利用する
4.2. ラッパークラスを利用する
4.3. ラッパーコンストラクトを利用する
5. クラスの分割戦略
5.1. サービス単位でクラスを分割
5.2. 機能単位でクラスを分割
6. Configファイルへの設定値外出し戦略
6.1. 現在の環境差分のみ外出し
6.2. 将来環境ごとに変わる可能性がある値も外出し
6.3. リソース名も外だしする
6.4. 定数はすべて外出し
6.5. 外出しは行わず、環境ごとに別のコードを利用する


## 1. べた書きする
L1ConstructやL2Constructを順番に宣言的にべた書きしていく方式です。
メリット：
・プログラミング初心者でも読みやすい
デメリット：
・コード量が多くなる。
・重複が多く構造化されていないため、メンテナンスが大変。


## 2. 共通の設定をまとめる
新しい定数を作成したり、[スプレッド構文](https://qiita.com/Nossa/items/e6f503cbb95c8e6967f8)を利用したりすることで、複数のリソースに共通の設定をまとめ、繰り返しを減らします。

メリット：
・重複を排除し、コード量を減らせる。
・1箇所修正すると全リソース修正できる。
デメリット：
・事前にどのように構造化するか検討が必要。
・設定値が分散する。

```ts
const s3CommonProps = {
    blockPublicAccess:s3.BlockPublicAccess.BLOCK_ALL,
    publicReadAccess:false,
    encryption:s3.BucketEncryption.S3_MANAGED,
    enforceSSL:true,
    versioned:true,
    removalPolicy:cdk.RemovalPolicy.DESTROY,
    autoDeleteObjects:true,
}

const myAppBucketLifecycleRules = [{
    enabled:true,
    noncurrentVersionExpiration:cdk.Duration.days(7),
    noncurrentVersionsToRetain:3,
},]


const myAppBucket1 = new s3.Bucket(this,"myAppBuket1" ,{
    ...s3CommonProps,
    bucketName:"fsit-myappbucket1",
    lifecycleRules:myAppBucketLifecycleRules
});

const myAppBucket2 = new s3.Bucket(this,"myAppBuket2" ,{
    ...s3CommonProps,
    bucketName:"fsit-myappbucket2",
    lifecycleRules:myAppBucketLifecycleRules
});
```

## 3. ループを利用する
似たようなリソースを大量に作成する際、ループを利用することもできます。
ループで作成したリソースを配列に格納すると、後から不要なリソースが発生した場合、配列番号がずれます。
辞書の場合、Keyで参照して取り出せるので番号のずれも生じず、分かりやすいです。

メリット：
・コード量を減らせる。
・重複を排除できる。

デメリット：
・宣言的でない。
・クラスや関数と違って、別の個所で再利用できない。


## 4. ラッパーを利用する
プロジェクト独自の設定を反映したリソースを利用するため、L２コンストラクト等を"ラップ"した関数やクラス等を作成して再利用することがあります。[Construct best practices](https://docs.aws.amazon.com/cdk/v2/guide/best-practices.html#best-practices-constructs)というＡＷＳ公式記事で your own "L2+" constructs 等と紹介されていますが、ラッパーだけではコンプライアンス的に不十分なので、[Aspects](https://docs.aws.amazon.com/cdk/v2/guide/aspects.html)等を併用しましょう。
ラッパーとして、①関数、②コンストラクトを継承しないクラス、③コンストラクトを継承したクラス、等が利用できますが、それぞれメリットとデメリットがあります。

メリット：
・プロジェクトや会社の独自のルールを反映したリソースを再利用できる。

デメリット：
・[Construct Hub](https://constructs.dev/) の[AWS ソリューションコンストラクト](https://docs.aws.amazon.com/solutions/latest/constructs/welcome.html)やサードパーティーコンストラクトなどの AWS CDK パッケージを利用できなくなる。
・ラッパーだけではコンプライアンス的に不十分。

### 4.1. ラッパー関数を利用する
関数を用いてラッパーを作成することができます。

メリット：
・s3.Ibucket等、既存のCDKの型を戻り値に設定できる。
・リファクタリングの際、論理IDが変更されない。

デメリット：
・関数内部で複数のリソースを作成した場合、バグの温床になりかねない。
・クラスと違って継承したり、内部にデータを持つことができない。

### 4.2. ラッパークラスを利用する
Constructを継承しないクラスを利用してラッパーを作成することもできます。

メリット：
・クラスなので、内部にデータを持つことができる。
・リファクタリングの際、論理IDが変更されない。

デメリット：
・CDK Constructの各種機能を利用できない。


### 4.3. ラッパーコンストラクトを利用する
Constructを継承したクラスを利用してラッパーを作成することもできます。

メリット：
・CDK Constructの各種機能を利用できる。

デメリット：
・リファクタリングの際、論理IDが変化し、リソースの再作成が行われる。





## 5. クラス/Constructの分割戦略
クラスやConstructの分割には、①サービス単位、②機能単位といった複数のやり方があります。Constructを継承したクラスを越えてリファクタリングする際は論理IDが変化してリソースが再作成される可能性があるため、事前に分割戦略をしっかり検討しましょう。

以下のような構成を３つ作成する場合を例として、それぞれの戦略を解説します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3482344/bee3db8d-f179-1ca3-1c4c-b1f2a36d1041.png)


### 5.1. サービス単位でクラスを分割
S3、Lambda、Logs、Firehose、SubscriptionFilter等、サービス単位でクラスを分ける戦略です。

メリット：
・どのサービスを利用したか、一覧が分かりやすい。

デメリット：
・同じ機能のリソースを複数作成する際、クラス間で参照構造の作成が必要。


```ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import {S3Class} from '../../resources/s3-pattarn/5-1_lib/s3';
import {FirehoseClass} from '../../resources/s3-pattarn/5-1_lib/fiehose';
import {LogClass} from '../../resources/s3-pattarn/5-1_lib/log';
import {SubscriptionFilterClass} from '../../resources/s3-pattarn/5-1_lib/subscriptionfilter';
import {LambdaClass} from '../../resources/s3-pattarn/5-1_lib/lambda';


export class LambdaS3Stack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const myIdList = ["1","2","3"]

    const myS3Class = new S3Class(this,{
      idList:myIdList
    })

    const myFirehoseClass = new FirehoseClass(this,{
      myAppLogBucketDict:myS3Class.myAppLogBucketDict
    })

    const myLogClass = new LogClass(this,{
      idList:myIdList
    })
   
    const mySubscriptionFilterClass = new SubscriptionFilterClass(this,{
      myFirehoseDict:myFirehoseClass.myFirehoseDict,
      myLogGroupDict:myLogClass.myLogGroupDict,
    })

    const myLambdaClass = new LambdaClass(this,{
      myAppBucketDict:myS3Class.myAppBucketDict,
      myLogGroupDict:myLogClass.myLogGroupDict,
    })
  
  }
}
```

### 5.2. 機能単位でクラスを分割
機能単位でConstructやクラスを作成することもできます。いわゆる、L3constructのことです。

メリット
・AWS Solution Constructなどの既存のL3 Costructを活用できる。
・機能単位で再利用が可能。

デメリット
・その機能の中に何が含まれているか、ぱっと見で分からない。
・上手く作成しないと、再利用が困難。再利用性を上げると、引数が増えて説明書が必要になる。
・複数のクラスでS3等のリソースが利用され、同じサービスの設定箇所が散らばる。

以下コードでは、Lambda+S3という機能と、Logs＋Firehose＋S3という機能に分けました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3482344/11e4d35c-d8d6-8ffd-6f60-2c6de0c8bcd4.png)


```ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as logs from 'aws-cdk-lib/aws-logs';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as firehose from 'aws-cdk-lib/aws-kinesisfirehose';
import {LambdaS3Func} from '../../resources/s3-pattarn/5-2_lib/lambda-s3';
import {LogKdfS3Func} from '../../resources/s3-pattarn/5-2_lib/log-kdf-s3';

export class LambdaS3Stack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    for (const id of ["1","2","3"]){

      //Log + Kdf + S3 作成
      const myLogGroup = LogKdfS3Func(this,{id})

      // Lambda + S3 作成
      const myLambda = LambdaS3Func(this,{id,myLogGroup})
    }

  }
}
```


## 6. Configファイルへの設定値外出し戦略
ConfigファイルやCDK.jsonなどに環境差分を外出しすることができます。
jsonだと①コメントがかけない、②CDK独自のEnum型（例：cdk.duratoin.days(30)等）が利用できない、という欠点があるので、.tsファイルを利用しましょう。

メリット：
・複数環境のリソースを同一のコードで管理可能。

デメリット：
・特定の環境のみ利用するリソースを作成する場合、IF文の利用が必要。
・特定の環境のみDBなどをリストアする場合に他の環境のコードに影響が出ないよう、作りの工夫が必要。

### 6.1. 現在の環境差分のみ外出し
現在発生している環境差分のみ外出しする戦略です。
メリット：
・外出しする基準が分かりやすい。
・外出しする値が最小限。
デメリット：
・将来新しい設定値を環境ごとに変えたくなった場合、新たに外出しが必要。

### 6.2. 将来環境ごとに変わる可能性がある値も外出し
メリット：
・Configファイルを変更するだけで、環境ごとの設定を変えられる。
デメリット：
・どこまでパラメータの外出しをするか、基準が曖昧。

### 6.3. リソース名も外だしする
props私のスタック間参照を利用すると、様々なトラブルが発生します。命名規則に沿ったリソース名や、Arnを格納したSSMパラメータ名等、事前に決まっている名前を複数スタックで利用したい場合は、Configファイル等に外出しすることで参照できます。

メリット：
・スタック間参照を避けられる。

デメリット：
・環境差分以外も外出しされるため、環境差分と区別するための構造化が必要。


### 6.4. 定数はすべて外出し
ロジックと定数を別ファイルに分けるというプログラミングの思想があります。この思想にのっとり、すべての定数は外出しすべきいう極端な意見もあります。個人的には、定数をすべて外だしした設定値ファイルを作成するくらいなら、CDKではなくCloudFormationを利用すればよいのではと感じます。
メリット
・設定値とロジックを分離できる。
・外出しする基準が明確
デメリット
・環境差分以外も外出しされるため、環境差分と区別するための構造化が必要。
・コード量が多くなる。

### 6.5. 外出しは行わず、環境ごとに別のコードを利用する
環境差分を外出しすると複数環境を同一のコードで管理可能です。しかし、特定の環境のみDBなどをリストアする場合、他の環境のコードに影響が出る可能性があります。また、特定の環境のみ利用するリソースを作成する場合、IFでの制御が必要です。このような影響を避けるため、環境差分の外出しを行わず、環境ごとに別のコードを作成する方式も考えられます。

メリット：
・リストア時に他の環境に影響しない。

デメリット：
・コード量が増える。
・環境ごとの思わぬ差分が生まれる可能性あり。

## まとめ
CDKでコードを作成する場合、プログラミングを利用するため、CloudFormationより自由度が高く、様々な構成が可能です。事前にどの構成を利用するかプロジェクト内で認識合わせしないと、人によって書き方がバラバラになり、収拾不能なコードが誕生します。基盤観点だけでなく、クラス設計等やコード規約の作成等、アプリケーション観点の詳細設計も行いましょう。