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が提供するバリデートメソッドにあやかることができます。

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


メリット

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


単純にバリデート処理の内容がスタッククラスに入らないため、スタック構成の見通しが良くなるというのがいいですよね。


ユースケース

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


例えば、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 {
  scopeType: string;
  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.


最後に

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

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


Twitter始めました!

良かったらぜひ。お知り合い増やせたら嬉しいです。

Twitter ID → @365_step_tech