AWS CDKでバリデーションを行う(addValidation)

概要

AWS CDKを使っていて、CDKが提供するバリデーションのメソッドがあったため使ってみました。


目次

目次


前提

TypeScriptでCDKを使用しています。

また、CDKはv2を使用しております。

❯ cdk --version
2.31.0 (build b67950d)


背景

CDKでリソースを作ろうとしていて、あるパラメータのバリデーションを行いたいと思っていて、以下のような実装をしていました。

export class SampleAppStack extends Stack {
  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);

    this.validate();

    /* 以下リソース作成処理 */
  }

  private validate() {
    /* 以下バリデート処理 */
  }
}


そしてcdk synthをしてみると、、、

❯ npx cdk synth
/Users/goto/github/sample-app-stack/node_modules/constructs/src/construct.ts:362
        throw new Error(`the construct "${this.path}" has a "${method}()" method which is no longer supported. Use "construct.node.addValidation()" to add validations to a construct`);
              ^
Error: the construct "SampleAppStack" has a "validate()" method which is no longer supported. Use "construct.node.addValidation()" to add validations to a construct
    at Node.validate (/Users/goto/github/sample-app-stack/node_modules/constructs/src/construct.ts:362:15)
    at /Users/goto/github/sample-app-stack/node_modules/aws-cdk-lib/core/lib/private/synthesis.js:1:2940
    at visit (/Users/goto/github/sample-app-stack/node_modules/aws-cdk-lib/core/lib/private/synthesis.js:3:64)
    at visit (/Users/goto/github/sample-app-stack/node_modules/aws-cdk-lib/core/lib/private/synthesis.js:3:141)
    at validateTree (/Users/goto/github/sample-app-stack/node_modules/aws-cdk-lib/core/lib/private/synthesis.js:1:2875)
    at Object.synthesize (/Users/goto/github/sample-app-stack/node_modules/aws-cdk-lib/core/lib/private/synthesis.js:1:598)
    at App.synth (/Users/goto/github/sample-app-stack/node_modules/aws-cdk-lib/core/lib/stage.js:1:1866)
    at process.<anonymous> (/Users/goto/github/sample-app-stack/node_modules/aws-cdk-lib/core/lib/app.js:1:1164)
    at Object.onceWrapper (events.js:520:26)
    at process.emit (events.js:400:28)

Subprocess exited with error 1


「スタックのクラスにvalidateメソッド作っちゃダメ、"construct.node.addValidation()"を使いなさい。」

ということだったので、調べて使ってみたという経緯になります。


addValidation

今回使うのはclass Node(->Construct.node)が提供するaddValidationというメソッドになります。

addValidationによってコンストラクタのバリデーションを登録でき、synthesize時に発火されます。

class Node · AWS CDK

public addValidation(validation: IValidation): void

Adds a validation to this construct.

When node.validate() is called, the validate() method will be called on all validations and all errors will be returned.


addValidationは、interface IValidationのオブジェクトを引数に持つメソッドとなります。

IValidationというinterfaceは、validateというメソッドを持ち、コンストラクタのバリデートを行うものになります。

interface IValidation · AWS CDK

public validate(): string[]

Validate the current construct.

This method can be implemented by derived constructs in order to perform validation logic. It is called on all constructs before synthesis.


つまり、interface IValidationimplementsするvalidatorクラスを作成し、validate()メソッドに実際に行うバリデーションロジックを実装します。

そしてそのvalidatorクラスのインスタンスを、スタッククラス内でaddValidationに渡してあげることで、CDKが提供するバリデートメソッドにあやかることができます。

※コードの具体例は後述します。


ユースケース

  • スタックへ渡すパラメータ・コンテキストの情報に対してバリデートしたい


例えば、AWS WAF v2を作成するCDKスタックを作っているとします。

WAF v2のWebAclにはScopeというパラメータがあり、これはREGIOANL | CLOUDFRONTという二つの文字列を格納できます。

AWS::WAFv2::WebACL - AWS CloudFormation


WAFをELBやAPI Gatewayなどにアタッチする際はREGIOANL、一方でグローバルサービスであるCloudFrontにアタッチしたい場合はCLOUDFRONTを指定します。

しかしそこには制約があり、CLOUDFRONTをScopeに指定する場合は、us-east-1リージョンでWAFをデプロイする必要があります。(でないとWAFをCloudFrontにアタッチできません。)

Note

For CLOUDFRONT, you must create your WAFv2 resources in the US East (N. Virginia) Region, us-east-1.


そこで、今回のCDKのバリデーションで、「ScopeがCLOUDFRONTの場合、us-east-1以外のリージョンが指定されたらエラーにする」ようなことをしてみます。


コード

構成

※本記事のメイン以外の一部のファイルやコードを省略しています。

.
├── bin
│   └── sample-app.ts
└── lib
     ├── config.ts
     ├── resource
     │   └── sample-app-stack.ts
     └── validator
         └── waf-region-validator.ts


sample-app.ts

const app = new cdk.App();

new SampleAppStack(app, "SampleAppStack", configStackProps);


config.ts

export interface Config {
  scopeType: string;
}

export interface ConfigStackProps extends StackProps {
  config: Config;
}

export const configStackProps: ConfigStackProps = {
  env: {
    region: "us-east-1",
  },
  config: {
    scopeType: "CLOUDFRONT",
  },
};


sample-app-stack.ts

export class SampleAppStack extends Stack {
  constructor(scope: Construct, id: string, props: ConfigStackProps) {
    super(scope, id, props);

    const scopeType = props.config.scopeType;

    const wafRegionValidator = new WafRegionValidator(scopeType, this.region);
    this.node.addValidation(wafRegionValidator);

    const webAcl = new wafv2.CfnWebACL(this, "WebAcl", {
      ...
      ...(省略)


waf-region-validator.ts

export class WafRegionValidator implements IValidation {
  private scopeType: string;
  private region: string;

  constructor(scopeType: string, region: string) {
    this.scopeType = scopeType;
    this.region = region;
  }

  public validate(): string[] {
    const errors: string[] = [];

    if (this.scopeType !== "CLOUDFRONT" && this.scopeType !== "REGIONAL") {
      errors.push("Scope must be CLOUDFRONT or REGIONAL.");
    }
    if (this.scopeType === "CLOUDFRONT" && this.region !== "us-east-1") {
      errors.push("Region must be us-east-1 when CLOUDFRONT.");
    }

    return errors;
  }
}


解説

sample-app.ts

次項でご説明する、config.tsで定義したconfigStackPropsというスタックのパラメータオブジェクトの中にenv.regionが入っているため、これをスタックのprops(第3引数)として渡すことで、指定するリージョンでのデプロイが可能になります。

new SampleAppStack(app, "SampleAppStack", configStackProps);


config.ts

スタッククラスのコンストラクタに渡すパラメータを定義するためのファイルです。

Stackのコンストラクタで渡せるようStackProps(interface)を継承するConfigStackPropsというinterfaceと、自前のパラメータを定義するConfigというinterfaceを定義しています。

export interface Config {
  scopeType: string;
}

export interface ConfigStackProps extends StackProps {
  config: Config;
}


ConfigStackPropsにもともとStackPropsで渡したかったregionを含むenvなどの他に、自前のConfig型のパラメータも格納します。

export const configStackProps: ConfigStackProps = {
  env: {
    region: "us-east-1",
  },
  config: {
    scopeType: "CLOUDFRONT",
  },
};


sample-app-stack.ts

先程定義したconfigStackPropsをスタックのコンストラクタで渡しているため、そこからscopeTypeを取得しています。

export class SampleAppStack extends Stack {
  constructor(scope: Construct, id: string, props: ConfigStackProps) {
    super(scope, id, props);

    const scopeType = props.config.scopeType;


そして、本題のaddValidation部分になります。

validatorクラス(後述)のインスタンスを生成して、this.node.addValidationに渡してあげます。

    const wafRegionValidator = new WafRegionValidator(scopeType, this.region);
    this.node.addValidation(wafRegionValidator);


これでCDKによる、synthesize時(前)にコンストラクタのバリデーションが行われるようになります。


waf-region-validator.ts

先程スタッククラスで使用した、addValidationに渡すためのvalidatorクラスの詳細です。

IValidationというinterfaceをimplementsします。

export class WafRegionValidator implements IValidation {


public validate(): string[]というメソッドを実装する必要があるので、ここにバリデーションロジックを実装します。戻り値はエラーメッセージの配列です。

  public validate(): string[] {
    const errors: string[] = [];

    if (this.scopeType !== "CLOUDFRONT" && this.scopeType !== "REGIONAL") {
      errors.push("Scope must be CLOUDFRONT or REGIONAL.");
    }
    if (this.scopeType === "CLOUDFRONT" && this.region !== "us-east-1") {
      errors.push("Region must be us-east-1 when CLOUDFRONT.");
    }

    return errors;
  }


ロジックの実装としては、バリデーションエラー時には配列にpushし、エラーでないときは空配列をそのまま返すことでバリデートが通ります。

そして、この戻り値の要素数が0より大きい時、CDKが内部でthow new Errorを実行してくれるといった仕組みです。

また戻り値の型はstringの配列ですので、これは複数のエラーメッセージを返すことができるということになります。


検証

上記でご説明したconfig.tsにて、scopeTypeをCLOUDFRONTにして、regionにはわざとap-northeast-1を指定してみます。

  • config.ts
...(省略)
export const configStackProps: ConfigStackProps = {
  env: {
    region: "ap-northeast-1",
  },
  config: {
    scopeType: "CLOUDFRONT",
  },
};


このaddValidationは、実行時でなくsynthesize時にバリデートが行われるため、cdk synthコマンドで確認できます。

そしてcdk synthをしてみます。

❯ npx cdk synth

/Users/goto/github/sample-app-stack/node_modules/aws-cdk-lib/core/lib/private/synthesis.js:2
  `);throw new Error(`Validation failed with the following errors:
           ^
Error: Validation failed with the following errors:
  [SampleAppStack] Region must be us-east-1 when CLOUDFRONT.
    at validateTree (/Users/goto/github/sample-app-stack/node_modules/aws-cdk-lib/core/lib/private/synthesis.js:2:12)
    at Object.synthesize (/Users/goto/github/sample-app-stack/node_modules/aws-cdk-lib/core/lib/private/synthesis.js:1:598)
    at App.synth (/Users/goto/github/sample-app-stack/node_modules/aws-cdk-lib/core/lib/stage.js:1:1866)
    at process.<anonymous> (/Users/goto/github/sample-app-stack/node_modules/aws-cdk-lib/core/lib/app.js:1:1164)
    at Object.onceWrapper (events.js:520:26)
    at process.emit (events.js:400:28)
    at process.emit (domain.js:475:12)
    at process.emit.sharedData.processEmitHook.installedValue [as emit] (/Users/goto/github/sample-app-stack/node_modules/@cspotcode/source-map-support/source-map-support.js:745:40)

Subprocess exited with error 1


このように、バリデートが通らなかったとき、先程のvalidate()内で配列に格納したエラーメッセージが出力されました。

Error: Validation failed with the following errors:
  [SampleAppStack] Region must be us-east-1 when CLOUDFRONT.


またvalidate()の戻り値の型はstring[]なので、複数のエラーメッセージを格納した場合は以下のように複数のエラーを出力することができます。

Error: Validation failed with the following errors:
  [SampleAppStack] Test Error 1.
  [SampleAppStack] Test Error 2.


メリット

addValidationを使わなくてもコンストラクタでバリデーションすることは可能です。(実際そうしている方がほとんどかと思います。)


しかしaddValidationは、複数プロパティのエラーの出力がまとめて出来る(エラー発生時にthrowせず配列でまとめてreturnできる)ので、コンストラクタでif文でハンドリングしてnew Errorするより開発体験が向上するのでおすすめです!


バリデーション部分とスタック構成部分のレイヤーを分けて、validatorクラスにバリデート処理をカプセル化することで、バリデーションの方法を変えたくなった場合もスタッククラス自体には手を加えなくて良いため、保守性が高くなるメリットがあります。

そして責務分けによってコードがすっきりし、スタック構成の見通しが良くなるというのも良かったりします。


何より、バリデーション用として用意されている機能を使うということは、公式のレールに乗っかるという事なのでそれ自体が良いことだと思っています!


応用編

CDK + addValidation + TypeScript + Zodで行うバリデーションのお話です。

go-to-k.hatenablog.com


そのZodをさらに応用して、TypeScriptの型保証をZodスキーマで作った型で賄っちゃおうっていう、CDKスタックへのパラメータの型保証と制約を一緒にやっちゃうというお話です。

go-to-k.hatenablog.com


登壇

このネタで、JAWS-UG CDK支部 #5「〜新春隠し芸大会〜」で登壇させて頂きました!

speakerdeck.com


最後に

CDKでバリデーションを行うとき、自前でErrorインスタンスを生成してバリデーションを行っていたのですが、たまたまこのようなメソッドを見つけたため使ってみました。

CDKはまだ情報も多いというわけではないですが、やはり楽しいですね。