AWS CDKでfluent interface patternを使ってみた

# 背景

最近、aws cdkをたくさん書いていて、IaCが実現でき、とても便利です。
しかし、パラメーターがたくさんあり、何度も見ていると流れがわかりずらくなり、可読性が悪くなってきます。
そこで、`Fluent Interface Pattern`を知って当てはめてみたら良くなり、パターン化されて認知負荷も下がった気がするので紹介します。

※ビルダーパターンも混ざっているかも
https://qiita.com/takutotacos/items/33cfda205ab30a43b0b1

# Fluent Interface Pattern とは

Fluent Interfaceパターンは、オブジェクト指向プログラミングで、メソッドチェーンを使って操作を直感的に行うためのパターンです。このパターンは可読性を高めるためによく用いられます。 By ChatGPT

要はメソッドチェーンを多用します。

# 環境

node: 19.8.1
typescript: ^5.0.3
aws-cdk: "^2.72.1"


# ノーマルパターン

例えば、ここに`ALB + RDS ServelessV1 + ECS`を定義したコードがあるとします。
それをそのまま書くと下記のようなコードになります。

```normal.ts
import { SubnetType, Vpc } from 'aws-cdk-lib/aws-ec2';
import { DockerImageAsset } from 'aws-cdk-lib/aws-ecr-assets';
import { EcrImage, PropagatedTagSource } from 'aws-cdk-lib/aws-ecs';
import { ApplicationLoadBalancedFargateService } from 'aws-cdk-lib/aws-ecs-patterns';
import { AuroraPostgresEngineVersion, DatabaseClusterEngine, ServerlessClusterFromSnapshot } from 'aws-cdk-lib/aws-rds';
import { Construct } from 'constructs';
import { join } from 'path';

export interface NormalPatternProps {
  snapshotName: string;
}

export class NormalPatternConstruct extends Construct {
  constructor(scope: Construct, id: string, props: NormalPatternProps) {
    super(scope, id);

    const vpc = new Vpc(this, Vpc.name, {});

    const rds = new ServerlessClusterFromSnapshot(this, ServerlessClusterFromSnapshot.name, {
      vpc,
      defaultDatabaseName: 'normal',
      snapshotIdentifier: props.snapshotName,
      engine: DatabaseClusterEngine.auroraPostgres({
        version: AuroraPostgresEngineVersion.VER_10_21,
      }),
      enableDataApi: true,
      scaling: { maxCapacity: 2 },
      vpcSubnets: {
        subnetType: SubnetType.PRIVATE_ISOLATED,
        onePerAz: true,
      },
    });

    const asset = new DockerImageAsset(this, DockerImageAsset.name, {
      directory: join(__dirname, '../../../'),
    });
    const image = EcrImage.fromDockerImageAsset(asset);

    new ApplicationLoadBalancedFargateService(this, ApplicationLoadBalancedFargateService.name, {
      vpc,
      enableExecuteCommand: true,
      enableECSManagedTags: true,
      redirectHTTP: true,
      circuitBreaker: { rollback: true },
      propagateTags: PropagatedTagSource.SERVICE,
      capacityProviderStrategies: [
        { capacityProvider: 'FARGATE', base: 1, weight: 1 },
        { capacityProvider: 'FARGATE_SPOT', base: 0, weight: 9 },
      ],
      taskImageOptions: {
        image,
        enableLogging: true,
        environment: {
          DB_HOST: rds.clusterEndpoint.hostname,
          DB_PORT: rds.clusterEndpoint.port.toString(),
        },
      },
    });
  }
}

```

上記のクラスをインスタンス化すると、`NormalPatternConstruct`の`constructor`が動き、`ALB + RDS + ECS`が作成されます。

```cdk.ts
new NormalPatternConstruct(this, NormalPatternConstruct.name, { snapshotName: "snapshotName" })
```

ちょっと、設定値多くて、読みながら脳内で分解処理しながら読まないといけないですね。

# Fluent Interface Pattern

```fluent.ts
import { SubnetType, Vpc } from 'aws-cdk-lib/aws-ec2';
import { DockerImageAsset } from 'aws-cdk-lib/aws-ecr-assets';
import { ContainerImage, EcrImage, PropagatedTagSource } from 'aws-cdk-lib/aws-ecs';
import { ApplicationLoadBalancedFargateService } from 'aws-cdk-lib/aws-ecs-patterns';
import { AuroraPostgresEngineVersion, DatabaseClusterEngine, ServerlessClusterFromSnapshot } from 'aws-cdk-lib/aws-rds';
import { Construct } from 'constructs';
import { join } from 'path';

export interface FluentPatternProps {
  snapshotName: string;
}

export class FluentPattern extends Construct {
  #props: FluentPatternProps;
  vpc: Vpc;
  rds: {
    host: string;
    port: string;
  };
  image: ContainerImage;

  constructor(scope: Construct, id: string, props: FluentPatternProps) {
    super(scope, id);
    this.#props = props;
    this.createVpc()
        .createRDS()
        .buildDockerImage()
        .createECS();
  }

  createVpc() {
    this.vpc = new Vpc(this, Vpc.name, {});
    return this;
  }

  createRDS() {
    const rds = new ServerlessClusterFromSnapshot(this, ServerlessClusterFromSnapshot.name, {
      vpc: this.vpc,
      defaultDatabaseName: 'normal',
      snapshotIdentifier: this.#props.snapshotName,
      engine: DatabaseClusterEngine.auroraPostgres({
        version: AuroraPostgresEngineVersion.VER_10_21,
      }),
      enableDataApi: true,
      scaling: { maxCapacity: 2 },
      vpcSubnets: {
        subnetType: SubnetType.PRIVATE_ISOLATED,
        onePerAz: true,
      },
    });
    this.rds = {
      host: rds.clusterEndpoint.hostname,
      port: rds.clusterEndpoint.port.toString(),
    };
    return this;
  }

  buildDockerImage() {
    const asset = new DockerImageAsset(this, DockerImageAsset.name, {
      directory: join(__dirname, '../../../'),
    });
    this.image = EcrImage.fromDockerImageAsset(asset);
    return this;
  }

  createECS() {
    new ApplicationLoadBalancedFargateService(this, ApplicationLoadBalancedFargateService.name, {
      vpc: this.vpc,
      enableExecuteCommand: true,
      enableECSManagedTags: true,
      redirectHTTP: true,
      circuitBreaker: { rollback: true },
      propagateTags: PropagatedTagSource.SERVICE,
      capacityProviderStrategies: [
        { capacityProvider: 'FARGATE', base: 1, weight: 1 },
        { capacityProvider: 'FARGATE_SPOT', base: 0, weight: 9 },
      ],
      taskImageOptions: {
        image: this.image,
        enableLogging: true,
        environment: {
          DB_HOST: this.rds.host,
          DB_PORT: this.rds.port,
        },
      },
    });
    return this;
  }
}
```

インスタンス化するときは変わらず下記のようになります。

```cdk.ts
new FluentPattern(this, FluentPattern.name, {snapshotName: "snapshotNames"});
```


## ポイント

- `constructor`に処理の流れが書いてある
- クラスメソッド内で使うプロパティは、メンバーとして切り出す(例: #props, vpc, rds等)


## どんな時に有用か

### ・継承時

クラスメソッドとして書くスタックを切り出すと、オーバーライドができ、再利用に便利です。

たとえば、先程の`FluentPattern`クラスを本番適応する時に、RDSだけ既存のRDSを使うときにオーバーライドすると、RDSだけ切り替える事ができます。

#### 例: RDSをsnapshot → 既に存在しているRDSに切り替える

```fluent-prod.ts
class FluentPatternProd extends FluentPattern {
  constructor(scope: Construct, id: string, props: FluentPatternProps) {
    super(scope, id, props);
  }

  override createRDS() {
    const rds = DatabaseInstance.fromDatabaseInstanceAttributes(this, DatabaseInstance.name, {
      instanceIdentifier: 'useExist',
      port: 5432,
      securityGroups: [new SecurityGroup(this, SecurityGroup.name, { vpc: this.vpc })],
      instanceEndpointAddress: '',
    });
    this.rds = {
      host: rds.instanceEndpoint.hostname,
      port: rds.instanceEndpoint.port.toString(),
    };
    return this;
  }
}
```

`constructor`にすべてまとめてしまうと、継承して再利用するときに、`constructor`の内容をoverrideできないです（方法あったらご教示お願いしますmm）





# 参考

- https://dev.to/nas5w/using-the-fluent-interface-pattern-to-create-javascript-objects-2p1n