実践!AWS CDK #8 抽象化

題字・息子たち
2021.06.07

はじめに

今回のお題は抽象化です。
文字通り 抽象クラス を作っていきます。

前回の記事はこちら。

抽象クラスとは

ざっくり解説。

  • 抽象メソッド(中身のないメソッド)を 1 つ以上持つクラス
  • サブクラスでは抽象メソッドの実装(オーバーライド)が必須
  • 抽象クラスをインスタンス化することはできない
    • 「抽象クラスを継承したサブクラス」をインスタンス化して使う

このクラスを利用してリソース作成に必要な処理を抽象化&共通化していきます。

既存の作り

現在のソースコードはこのようになっています。

VPC

lib/resource/vpc.ts

import * as cdk from '@aws-cdk/core';
import { CfnVPC } from '@aws-cdk/aws-ec2';

export class Vpc {
    public vpc: CfnVPC;

    constructor() { };

    public createResources(scope: cdk.Construct) {
        const systemName = scope.node.tryGetContext('systemName');
        const envType = scope.node.tryGetContext('envType');

        this.vpc = new CfnVPC(scope, 'Vpc', {
            cidrBlock: '10.0.0.0/16',
            tags: [{ key: 'Name', value: `${systemName}-${envType}-vpc` }]
        });
    }
}

サブネット

lib/resource/subnet.ts

import * as cdk from '@aws-cdk/core';
import { CfnSubnet, CfnVPC } from '@aws-cdk/aws-ec2';

export class Subnet {
    public public1a: CfnSubnet;
    public public1c: CfnSubnet;
    public app1a: CfnSubnet;
    public app1c: CfnSubnet;
    public db1a: CfnSubnet;
    public db1c: CfnSubnet;

    private readonly vpc: CfnVPC;

    constructor(vpc: CfnVPC) {
        this.vpc = vpc;
    };

    public createResources(scope: cdk.Construct) {
        const systemName = scope.node.tryGetContext('systemName');
        const envType = scope.node.tryGetContext('envType');

        this.public1a = new CfnSubnet(scope, 'SubnetPublic1a', {
            cidrBlock: '10.0.11.0/24',
            vpcId: this.vpc.ref,
            availabilityZone: 'ap-northeast-1a',
            tags: [{ key: 'Name', value: `${systemName}-${envType}-subnet-public-1a` }]
        })
        ~ 省略 ~
    }
}

以下 2 点、上記のハイライト部分が同じような処理を行っているのでこれらを抽象クラスに移動させます。(createResources() は宣言だけ)

  • createResources() でのリソース生成
  • リソース名のプレフィックス作成
    • systemName & envType

ディレクトリ構成

ディレクトリ構成は次のようにします。(ハイライト部分を追加)

lib
└── resource
    ├── abstract
    │   └── resource.ts
    ├── vpc.ts
    └── subnet.ts
  • abstract
    • リソースに関する抽象クラスを格納するディレクトリ
    • 多分 1 ファイルだけになると思う
  • resource.ts
    • リソース用の抽象クラスに関するプログラムを記述するファイル
    • resource 配下のクラスはすべてこの抽象クラスを継承する

それでは、いざ実装!

実装

抽象クラス

lib/resource/abstract/resource.ts

import * as cdk from '@aws-cdk/core';

export abstract class Resource {
    constructor() { }

    abstract createResources(scope: cdk.Construct): void;

    protected createResourceName(scope: cdk.Construct, originalName: string): string {
        const systemName = scope.node.tryGetContext('systemName');
        const envType = scope.node.tryGetContext('envType');
        const resourceNamePrefix = `${systemName}-${envType}-`;

        return `${resourceNamePrefix}${originalName}`;
    }
}

抽象 を表す修飾子 abstract を利用し、クラス宣言をしています。コンストラクタ(初期化処理)は空。

そして以下が抽象メソッドの宣言です。

abstract createResources(scope: cdk.Construct): void;

class と同様 abstract 修飾子を付与しています。
ここでは次の 3 点のみを指定しています。

  • メソッド名
    • createResources
  • 引数の名前と型
    • 名前:scope
    • 型:cdk.Construct
  • 戻り値の型:void
    • ※void は戻り値 無し です

抽象クラスとしては ここまで決めてやったから、あとの中身はサブクラス側で好きに実装してね ということです。実際 VPC とサブネットではそれぞれリソースの生成処理が異なるため、これでいいのです。

そしてこちらがリソース名を作成するメソッドです。

protected createResourceName(scope: cdk.Construct, originalName: string): string {
    const systemName = scope.node.tryGetContext('systemName');
    const envType = scope.node.tryGetContext('envType');
    const resourceNamePrefix = `${systemName}-${envType}-`;

    return `${resourceNamePrefix}${originalName}`;
}

引数にオリジナルのリソース名を受け取り、プレフィックス(システム名-環境-)が付いたリソース名を返却します。サブクラスでのみ利用するため protected 修飾子を付与しています。

ほとんどのリソースで必要な処理となるので、共通処理としてスーパークラスに書いておきます。これでこのクラスを継承しているクラスは各自で処理を実装することなくこのメソッドを利用できます。こちらは抽象クラスは直接関係ありません。ただのスーパークラス(親クラス)とサブクラス(子クラス)の関係です。

VPC

主な変更箇所はハイライト部分です。

lib/resource/vpc.ts

import * as cdk from '@aws-cdk/core';
import { CfnVPC } from '@aws-cdk/aws-ec2';
import { Resource } from './abstract/resource';

export class Vpc extends Resource {
    public vpc: CfnVPC;

    constructor() {
        super();
    };

    createResources(scope: cdk.Construct) {
        this.vpc = new CfnVPC(scope, 'Vpc', {
            cidrBlock: '10.0.0.0/16',
            tags: [{ key: 'Name', value: this.createResourceName(scope, 'vpc') }]
        });
    }
}

まず extends Resource という表記でこのクラスは抽象クラス Resource のサブクラスとなりました。またサブクラスのコンストラクタでは必ずスーパークラスのコンストラクタを呼び出す必要があるため super() というコードも追加しています。

これまではこの Vpc クラスが独自に createResources() というメソッドを実装していましたが、ここでは抽象クラスのメソッドをオーバーライドしています。このメソッドを実装しない場合、次のようなエラーが出力されます。

Non-abstract class 'Vpc' does not implement inherited abstract member 'createResources' from class 'Resource'.

createResources() メソッドを実装しなさい、ということです。
このように抽象クラスを利用することで、実装漏れやタイプミスなどをシステム側で制限できます。(補完も効きます)

またリソース名の作成処理は this.createResourceName() メソッドを呼び出すだけで実施できるようになりました。このクラスからシステム名や環境を取得する処理を排除できたことになります。

サブネット

lib/resource/subnet.ts

import * as cdk from '@aws-cdk/core';
import { CfnSubnet, CfnVPC } from '@aws-cdk/aws-ec2';
import { Resource } from './abstract/resource';

export class Subnet extends Resource {
    public public1a: CfnSubnet;
    public public1c: CfnSubnet;
    public app1a: CfnSubnet;
    public app1c: CfnSubnet;
    public db1a: CfnSubnet;
    public db1c: CfnSubnet;

    private readonly vpc: CfnVPC;

    constructor(vpc: CfnVPC) {
        super();
        this.vpc = vpc;
    };

    createResources(scope: cdk.Construct) {
        this.public1a = new CfnSubnet(scope, 'SubnetPublic1a', {
            cidrBlock: '10.0.11.0/24',
            vpcId: this.vpc.ref,
            availabilityZone: 'ap-northeast-1a',
            tags: [{ key: 'Name', value: this.createResourceName(scope, 'subnet-public-1a') }]
        });
        this.public1c = new CfnSubnet(scope, 'SubnetPublic1c', {
            cidrBlock: '10.0.12.0/24',
            vpcId: this.vpc.ref,
            availabilityZone: 'ap-northeast-1c',
            tags: [{ key: 'Name', value: this.createResourceName(scope, 'subnet-public-1c') }]
        });
        this.app1a = new CfnSubnet(scope, 'SubnetApp1a', {
            cidrBlock: '10.0.21.0/24',
            vpcId: this.vpc.ref,
            availabilityZone: 'ap-northeast-1a',
            tags: [{ key: 'Name', value: this.createResourceName(scope, 'subnet-app-1a') }]
        });
        this.app1c = new CfnSubnet(scope, 'SubnetApp1c', {
            cidrBlock: '10.0.22.0/24',
            vpcId: this.vpc.ref,
            availabilityZone: 'ap-northeast-1c',
            tags: [{ key: 'Name', value: this.createResourceName(scope, 'subnet-app-1c') }]
        });
        this.db1a = new CfnSubnet(scope, 'SubnetDb1a', {
            cidrBlock: '10.0.31.0/24',
            vpcId: this.vpc.ref,
            availabilityZone: 'ap-northeast-1a',
            tags: [{ key: 'Name', value: this.createResourceName(scope, 'subnet-db-1a') }]
        });
        this.db1c = new CfnSubnet(scope, 'SubnetDb1c', {
            cidrBlock: '10.0.32.0/24',
            vpcId: this.vpc.ref,
            availabilityZone: 'ap-northeast-1c',
            tags: [{ key: 'Name', value: this.createResourceName(scope, 'subnet-db-1c') }]
        });
    }
}

変更内容は VPC と同じなので説明はしません。ペタッと貼っとくだけ。

VPC、サブネットどちらのコードも createResources() の処理が少しだけ短く書けるようになりました。このように抽象クラスを利用することでサブクラスに対するルールの義務付けや共通処理の集約が可能となります。

GitHub

今回のソースコードは コチラ です。

おわりに

抽象化を利用して、似たような宣言や処理を抽象クラスに寄せることができました。これも AWS CDK ならではの機能ですね。(CFn ではできません)

しかし VPC のコードはまずまずといったところなのですが、サブネットに関してはまだ不満が残ります。同じようなリソース生成処理を繰り返し行ってしまっていますね。次はこちらを改善しましょう。

次回のお題は「リファクタリング」です。

リンク