CDKをTypeScriptできれいに書くためのTipsとか困ったところ

これは CyberAgent Developers Advent Calendar 2020 の3日目の記事です。

CDKとは

CDKとはCloudFormationをTypeScriptやPythonから記述することができるものです。構成管理ツールは基本的にはjsonyamlで書くのが一般的ですが、プログラミング言語で書くことができるのがCDKの特徴です。

公式ドキュメント

TypeScriptなどで書くことによって共通化や抽象化が容易にできることがメリットとしてとても大きいです。そこで、この記事ではTypeScriptで記述する際のTipsについて紹介したいと思います。

便利なTips

Prefix という class の導入

CDKではスタック内でidが衝突してはいけません。そのためidを秩序立てて定義していかないと被ってしまうということがおきます。また、idをきれいにつけることでそのまま(もしくは少し加工して)リソースの名前とすることもできます。

export type Env = "dev" | "prd";

export default class Prefix {
    prefix: string;

    /**
     * prefixの初期化を行う。`-`で終わらない場合はエラーとなる。
     * @param prefix `-`で終わるprefixにしたい文字列
     */
    constructor(prefix: string) {
        if (!/^.+-$/.test(prefix)) {
            throw new Error("prefix must be 'xxxx-");
        }
        this.prefix = prefix;
    }

    /**
     * dev や prd といった環境識別子と共に初期化するクラスメソッド
     * @param prefix `-`で終わるprefixにしたい文字列
     * @param env `Env`
     */
    static withEnv(prefix: string, env: Env) {
        return new Prefix(`${env}-${prefix}`);
    }

    /**
     * prefixを付与した文字列を生成する。
     * @param str prefixを付与したい文字列
     * @param additionalPrefixs 追加で付けたいprefix
     */
    value(str: string, ...additionalPrefixs: Prefix[]) {
        const additional = additionalPrefixs.reduce(
            (acc, v) => acc + v.prefix,
            ""
        );
        return `${this.prefix}${additional}${str}`;
    }
}

このクラスを介することで安全にprefixを付与することができます。これをしないとテンプレート文字列が乱立して可読性が下がるのと - 忘れに気づくことができません。

そして、途中からidを変えてしまうとそのリソースは一度削除されてしまうので後から変更することは困難です。

AWS リソースのラップしたクラス

TypeScriptなので継承を使うことができます。ラップすることで、それぞれのリソースのidの命名を縛ったり紐づくIAMをそのクラスの中で作って紐づけることが可能です。

export default class CustomEc2 extends ec2.Instance {
    private static RESOURCE_PREFIX = new Prefix("ec2-");

    /**
     * ひとつのインスタンスを作成するクラス
     * @param scope cdk.Construct
     * @param ec2Idx 個々のec2のユニークな識別子 ex. 001, 002, ...
     * @param prefix stack内で使うprefix
     * @param props ec2.InstanceProps
     */
    constructor(
        scope: cdk.Construct,
        ec2Idx: string,
        prefix: Prefix,
        props: ec2.InstanceProps
    ) {
        // stack の id には追加で `ec2` という prefix をつけている。
        const id = prefix.value(ec2Idx, CustomEc2.RESOURCE_PREFIX);
        const instanceName = prefix.value(ec2ID);

        super(scope, id, {
            ...props,
            instanceName,
        });
    }
}

このように ec2.Instance を継承することで、awsリソースをラップすることができます。

このクラスではidの命名を縛ることだけをやっていますが、このconstructorの中でIAM roleを作成して渡ってきたInstancePropsにマージするという使い方も可能です。(その場合はconstructorに渡ってきたIAM roleを上書きしてしまうことになるので工夫は必要になりそうですが。。)

また命名を縛るために先ほど定義したPrefixというクラスを利用しています。名前の振り方がわかりやすくなっているのではないでしょうか?

config の抽象化

configを抽象化することで、設定値を気にせずにインフラ構成を定義できるので便利です。本番と開発環境で設定を変えることがほとんどだと思うのでおすすめです。

export type Conf = {
    INSTANCE_TYPE: ec2.InstanceType;
    INSTANCE_COUNT: number;
};

const devConf: Conf = {
    INSTANCE_TYPE: new ec2.InstanceType("t2.micro"),
    INSTANCE_COUNT: 1,
};

const prdConf: Conf = {
    INSTANCE_TYPE: new ec2.InstanceType("t2.xlarge"),
    INSTANCE_COUNT: 2,
};

export const getConf = (env: Env) => {
    switch (env) {
        case "dev":
            return devConf;
        case "prd":
            return prdConf;
    }
};

スタックを作成するところで getConf を実行してスタックにそのconfigを渡すという実装にすることで、スタック自体を抽象化することができます。けっこう便利です。

困ったところ

constructor で super の前に this を参照できない

これはTypeScriptかJavaScriptの制約です。

class Hoge extends Fuga {
    constructor() {
        const hoge = this.hoge;

        super();
    }
}

上記のようなコードを実行しようと以下のエラーがでます。

'super' must be called before accessing 'this' in the constructor of a derived class.ts(17009)

そもそもconstructorの中でthisを参照するような実装が間違っているという説もありますが、thisが参照できないとしれて勉強になりました。

subnetがよくわからん

vpcやsubnetはコンソールで作成したものを用いていたのですが、これの参照が曲者でした。vpc.publicSubnets としたらpublic subnetを取れるはずなのですが、publicのはずのやつまでprivate扱いになってしまいvpc.publicSubnets == [] になってしまうという。。

subnet idを直接指定して引っ張ってきたかったのですが、それも特に見当たらず vpc.selectSubnets という関数を使って無理やりアタッチするというやり方で解決しました。

僕のそもそものsubnetの設計がゴミすぎたことに起因しているのかもしれませんが、cdkのsubnetだけはよく分からんという所感でした。