背景
最近、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
を定義したコードがあるとします。
それをそのまま書くと下記のようなコードになります。
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
が作成されます。
new NormalPatternConstruct(this, NormalPatternConstruct.name, { snapshotName: "snapshotName" })
ちょっと、設定値多くて、読みながら脳内で分解処理しながら読まないといけないですね。
Fluent Interface Pattern
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;
}
}
インスタンス化するときは変わらず下記のようになります。
new FluentPattern(this, FluentPattern.name, {snapshotName: "snapshotNames"});
ポイント
-
constructor
に処理の流れが書いてある - クラスメソッド内で使うプロパティは、メンバーとして切り出す(例: #props, vpc, rds等)
どんな時に有用か
・継承時
クラスメソッドとして書くスタックを切り出すと、オーバーライドができ、再利用に便利です。
たとえば、先程のFluentPattern
クラスを本番適応する時に、RDSだけ既存のRDSを使うときにオーバーライドすると、RDSだけ切り替える事ができます。
例: RDSをsnapshot → 既に存在しているRDSに切り替える
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)