初めてCDKのスタックをコンストラクタを使って構造化してみた

初めまして、駆け出し12冠エンジニアのアスカです。今回、初めてAWS CDKスタックをコンストラクタを用いて構造化してみたので、その方法を記載します。初めてだと調べても書き方に自信が持てなかったです。
# 背景
初期は以下のような構成でbackend-stack.tsに全てリソースを定義して作成していました。
```
backend/
    -bin/
        -bacend.ts
    -lib/
        -backend-stack.ts
    -nodemdules/
    -test/
```
```typescript: bacend.ts
import * as cdk from "aws-cdk-lib";
import { BackendStack } from "../lib/backend-stack";
import { AwsSolutionsChecks, NagSuppressions } from 'cdk-nag';
import { Aspects } from 'aws-cdk-lib';

const app = new cdk.App();

Aspects.of(app).add(new AwsSolutionsChecks());

const backendStack = new BackendStack(app, "BackendStack", {});

```

```typescript: backend-stack.ts
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as s3 from 'aws-cdk-lib/aws-s3';

interface BackendStackProps extends cdk.StackProps { }

export class BackendStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: BackendStackProps) {
    super(scope, id, props);

    const logBucket = new s3.Bucket(this, 'LogBucket', {
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      enforceSSL: true,
      serverAccessLogsPrefix: 'log/',
    });

    // ウェブホスティング用のリソース
    ...
    

    // 認証機能用のリソース
    ...
    

    // API用リソース
   ...
    
  }
}
```
このように1つのファイルにWebホスティング系のリソース群(CloudFrontやS3),認証系のリソース(Cognito),API系リソース群(Lambda,Dynamo,APIゲートウェイ)を全て定義すると大変コードが長くなり、可読性がかなり落ちます。
そこで分割しようと考えました。

# コンストラクタに分割
目標としては、3つのコンストラクタに分割して、以下のようにbackend-stack.tsを整理する事です。
```typescript: backend-stack.ts
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as s3 from 'aws-cdk-lib/aws-s3';
import { Api } from './constructs/api';
import { Auth } from './constructs/auth';
import { WebHosting } from './constructs/web';

interface BackendStackProps extends cdk.StackProps { }

export class BackendStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: BackendStackProps) {
    super(scope, id, props);

    const logBucket = new s3.Bucket(this, 'LogBucket', {
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      enforceSSL: true,
      serverAccessLogsPrefix: 'log/',
    });

    // ウェブホスティングスタックのインスタンス化
    const webhosting = new WebHosting(this, 'WebHosting',{
      logBucket,
    });

    // 認証機能スタックのインスタンス化
    const auth = new Auth(this, 'AuthStack');

    // API機能スタックのインスタンス化
    const api= new Api(this, 'ApiStack');
  }
}
```
ここで、サーバーアクセスログ用バケットに関してはコンストラクタではなくスタックに定義する必要があるため、上記のように記述しています。
コンストラクタに分割するために、まずlibディレクトリ配下にconstructsディレクトリを作成してweb.ts、api.ts、auth.tsを作成します。
```
backend/
    -bin/
        -bacend.ts
    -lib/
        -backend-stack.ts
        -constructs/
            -web.ts
            -auth.ts
            -api.ts
    -nodemdules/
    -test/
```
新しく作成した3つのファイルはそれぞれ以下にように記述します。
```typescript: web.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as cloudfrontOrigins from 'aws-cdk-lib/aws-cloudfront-origins';

export interface WebHostingProps {
  readonly logBucket: s3.IBucket;
}

export class WebHosting extends Construct {
  constructor(scope: Construct, id: string, props: WebHostingProps) {
    super(scope, id);
    
    // Webホスティング用のバケットやCloudFront等を定義
    const websiteBucket = new s3.Bucket(this, 'WebsiteBucket', {
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      enforceSSL: true,
      serverAccessLogsBucket: props.logBucket,
      serverAccessLogsPrefix: 'WebHostingBucketLog/',
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      websiteIndexDocument: 'index.html',
      cors: [{
        allowedMethods: [s3.HttpMethods.GET, s3.HttpMethods.HEAD],
        allowedOrigins: ['*'],
        allowedHeaders: ['*'],
      }],
    });
    ...
  }
}
```

ポイントは以下のインターフェースの定義して、ログバケットを親スタックから引数として受け取れるようにする事です。
```typescript
export interface WebHostingProps {
  readonly logBucket: s3.IBucket;
}
```
extendsでConstructを指定する事で、コンストラクタとしてリソース群を定義出来ます。propsに上記で定義したWebHostingPropsを設定する事でログバケットを指定出来るようになります。
```typescript
export class WebHosting extends Construct {
    constructor(scope: Construct, id: string, props: WebHostingProps) {
```
同様に他のファイルも定義します。
```typescript: auth.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as cognito from 'aws-cdk-lib/aws-cognito';

export class Auth extends Construct {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id);
    // 認証系のリソース群を定義
  }
}

```

```typescript: api.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as dynamodb from "aws-cdk-lib/aws-dynamodb";

export class ApiStack extends Construct {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id);
    // API系のリソース群を定義
  }
}

```
これで、backend-stack.tsからそれぞれにコンストラクタを参照してデプロイ出来るようになりました。
1つのファイルに全て定義するより、構造化してすっきりしました。

# 参考
以下の記事にはめちゃめちゃお世話になりました。ありがとうございます。
- https://tmokmss.hatenablog.com/entry/20221212/1670804620
- https://pages.awscloud.com/rs/112-TZM-766/images/AWS-Black-Belt_2023_AWS-CDK-Basic-2-Features_0831_v1.pdf