LoginSignup
1
1

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-04-04

背景

最近、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(),
        },
      },
    });
  }
}

上記のクラスをインスタンス化すると、NormalPatternConstructconstructorが動き、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)

参考

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1