CDKにてスタックを定義したり、パラメータを受け渡す時の「お約束」の部分の整理(parameter.tsの使用)

# 0.記事を書いたきっかけ
CDKきっかけでTypeScriptを書き始めたのもあって、クラスの定義やらパラメータの受け渡しなどについて、いわゆる「お約束のノリ」で実装していた部分があったので、これは良くないと思って自分用にまとめ直しました。

# 1. 今回のディレクトリ構成
```
infra
├── bin
│   └── infra.ts                    # CDKアプリケーションのエントリーポイント
├── cdk.out                         # CDKによる出力ファイル 
├── lib
│   ├── constructs
│   │   └── cloudtrail.ts           # 最初に作成したCloudTrailのコンストラクト。記事では特に触れません。
│   ├── stacks
│   │   ├── cdk-pipeline.ts         # CDK Pipelineのスタック定義
│   │   └── infra-stack.ts          # メインのインフラスタック
├── node_modules                   
├── parameter.ts                    # パラメータ定義用ファイル
├── test                           
├── .gitignore                     
├── cdk.json                        # CDKアプリケーション設定ファイル
├── jest.config.js                  
├── package.json                    
├── package-lock.json               
├── tsconfig.json                   
└── README.md                       
```

# 2. bin/infra.ts (エントリーポイント)
``` typescript
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { DefaultRegion, DevStackParameter } from '../parameter';
import { CdkPipelineStack } from '../lib/stacks/cdk-pipeline';
import { InfraStack } from '../lib/stacks/infra-stack';

const app = new cdk.App(); //cdk.APPはスタックを包含し、CloudFormationをテンプレートとして出力するクラス。

new InfraStack(app, 'CdkTemplatesStack', { //親クラスのcdk.Stackのコンストラクタはconstructor(scope: Construct, id: string, props?: StackProps)なので第三引数は省略可能。
});

new CdkPipelineStack(app, 'CDKPipelineStack', {
  env: { region: DefaultRegion.region }, // CdkPipelineStackの第三引数はcdk.StackProps。{ account?: string, region?: string } が指定できる。
  config: DevStackParameter
});
```

CDKにおいて、基底のクラスとなるはAWS CDK:Constructとのことです。

https://tblog.acomagu.me/p7i/

主に木構造を構成する Construct クラスを定義しています



cdk.Stackのコンストラクター（インスタンス化するときに実行されるメソッド)は以下のようになっています。
```
constructor(scope: Construct, id: string, props?: StackProps)
```

そして、CDKで扱われるAPPやStackなどのクラスは
Construct → cdk.App
Construct → cdk.Stack
Construct → その他のリソース
このようなクラスの継承がなされているので、InfraStackの第一引数にappを入れることができます。

この
InfraStack(app 
の部分によって、

 ``` 
 App (cdk.App)
└── InfraStack(cdk.Stack)
```

のツリー構造が成立します。

<br>

また、第三引数のcdk.StackPropsは、AWS アカウントやリージョンを指定するためのプロパティ `env?: Environment` を含みます。

API リファレンス↓

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.StackProps.html

そして、`Environment` 型は以下のように定義されています
```
{ account?: string, region?: string }
```
そのため、別でパラメータを定義しなくてもこれらの情報を渡すことができます。
DefaultRegionについては、自分でparameter.tsに定義したオブジェクト(型付き定数)です。（後述)
configについては、基本となるcdk.Stackには存在しませんが、CdkPipelineStackにおいて、自分で引数として定義しているので渡すことができます。（後述）

# 3. parameter.ts (値の外だし用ファイル)
``` typescript
export interface Region {
  region: string;
}
export const DefaultRegion: Region = {
  region: 'ap-northeast-1'
};

export interface StackParameter {
  envName: string;
}
export const DevStackParameter: StackParameter = {
  envName: 'dev'
};
```
このように文字列などについてはこのファイルで一元管理することで、定数の管理が容易になります。

記事を書きながら知ったのですが、パラメータ格納に、cdk.jsonのcontextを使用することもできるみたいなのですが、経験がないのと何やら奥が深そうなので今回は触れません。

https://zenn.dev/hassaku63/articles/ecf1ca0cf74342

# 4. lib/stack/cdk-pipeline.ts (各スタック)
``` typescript
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { StackParameter } from '../../parameter';

export class CdkPipelineStack extends cdk.Stack {
  constructor (
    scope: Construct, //第一引数の部分がスコープとなる。今回だと、appがスコープとなる。
    id: string, //第二引数がスタックの一意の名前となる。今回だと、CDKPipelineStackとなる。
    props: cdk.StackProps & { //cdk.StackProps は AWS CDK が標準的に提供する型で、region や account を指定できる。
      config: StackParameter // &は型結合。cdk.StackProps と StackParameter の両方のプロパティを持つ型を定義している。
    }
  ) {
    super(scope, id, props); //親クラスcdk.Stackのコンストラクター(初期化)のconstructor(scope: Construct, id: string, props?: StackProps);を呼び出して、スタックを初期化する。

    const envName = props.config.envName; // 実際に渡された引数のDevStackParameterのenvNameを取得

    const bucket = new cdk.aws_s3.Bucket(this, 'SourceCodeBucket', {
      bucketName: 'source-code-bucket-' + envName, //parameter.tsにて定義したdevという定数を使用している。
      versioned: true,
      blockPublicAccess: cdk.aws_s3.BlockPublicAccess.BLOCK_ALL,
      encryption: cdk.aws_s3.BucketEncryption.S3_MANAGED,
      removalPolicy: envName === "dev" ? cdk.RemovalPolicy.DESTROY : cdk.RemovalPolicy.RETAIN,
    });

    const pipeline = hogehoge~~~
  }
}
```

エントリーポイントにて、
``` typescript
new CdkPipelineStack(app, 'CDKPipelineStack', {
  env: { region: DefaultRegion.region },
  config: DevStackParameter
});
```
このようにインスタンス化されていた部分の定義になります。
第一引数のappはscopeとして扱われます。
先ほどの説明と重複しますが、
 ``` 
 App (cdk.App)
└── InfraStack(cdk.Stack)
```

のツリー構造が成立します。

第二引数の'CDKPipelineStack'はidとして扱われます。
これはシンプルにスタックの一意の名前です。

第三引数の
env: { region: DefaultRegion.region }, config: DevStackParameter
はpropsとして扱われます。

propsの型は、
```typescript
cdk.StackProps & { config: StackParameter }
```
このように定義しています。`&`はTypeScriptの型の統合の演算子です。
`StackParameter`は先ほどparameter.tsにて自分で以下のよう定義した型です。
```typescript
export interface StackParameter {
  envName: string;
}
```

cdk.StackPropsは以下のドキュメントの通りです

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.StackProps.html


ですので、最終的には以下のような型になります。
``` typescript
{
  // cdk.StackProps のプロパティ
  readonly env?: {
    account?: string;
    region?: string;
  };
  readonly stackName?: string;
  readonly description?: string;
  readonly tags?: { [key: string]: string };
  readonly terminationProtection?: boolean;

  // 独自に追加したプロパティ
  config: {
    envName: string;
  };
}
```

その結果
```const envName = props.config.envName;```
このような形でアクセスすることができます。
<br>


また、`super` を呼び出すことで、親クラス（この場合は `cdk.Stack`）のコンストラクタが実行され、スタックの基本的な初期化処理が行われます。具体的には以下の処理が行われます
- スタックが親（スコープ。つまりapp）に登録される
- AWS CloudFormation テンプレートの構造に組み込まれる
- 必要なプロパティ（`env` や `stackName` など）が適用される

`super` を呼び出さないと、親クラスの初期化が行われず、エラーになります。


# 5. まとめ
なんとなく頭の中を整理できましたが、

CDKにおいて、基底のクラスとなるAWS CDK:Constructについてや、cdk.jsonなど、まだまだ深掘りが必要な部分はたくさんあるなと感じました。

https://tblog.acomagu.me/p7i/

実際にはコンストラクトまでパラメータを渡したり、ステージでスタックを包括したりするのでさらにややこしくはなりますが、今回の記事の内容を基本として理解すればある程度把握しながら実装できるのではないかと思います。

初学者の書いた記事なので、誤りなどがありましたら、ご指摘いただけると幸いです。