DEV Community

k.goto for AWS Community Builders

Posted on • Edited on

Validation with AWS CDK (addValidation)

I used AWS CDK and used the validation methods provided by CDK because they were available.

Assumptions

I am using CDK with TypeScript.

I also use v2 CDK.

❯ cdk --version
2.31.0 (build b67950d)
Enter fullscreen mode Exit fullscreen mode

addValidation

This time, I will use the method addValidation provided by class Node(->Construct.node) (see this doc).

The addValidation allows you to register construct validation, which will be fired at synthesize time.

public addValidation(validation: IValidation): void
Enter fullscreen mode Exit fullscreen mode

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.

The method addValidation will take an object of interface IValidation as an argument.

The interface IValidation will have a method validate to validate the construct (see this doc).

public validate(): string[]
Enter fullscreen mode Exit fullscreen mode

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.

In other words, create a validator class that implements interface IValidation and implement the actual validation logic in the validate() method.

Then, by passing an instance of the validator class to addValidation in the stack class, you can take advantage of the validation methods provided by the CDK.

Use Cases

  • I want to validate against the parameter context information passed to the stack.

For example, let's say I am building a CDK stack that creates AWS WAF v2.

The WebAcl for WAF v2 has a parameter called Scope which can contain two strings, REGIOANL | CLOUDFRONT (in this page).

When attaching WAF to ELB, API Gateway, etc., specify REGIOANL, while if you want to attach to CloudFront, a global service, specify CLOUDFRONT.

However, there is a restriction: if you specify CLOUDFRONT as the Scope, you must deploy WAF in the us-east-1 region. (Otherwise, the WAF cannot be attached to CloudFront.)

Note

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

So, in this CDK validation, "If Scope is CLOUDFRONT, make an error if a region other than us-east-1 is specified ".

Code

Composition

Some files and codes other than the main one in this article are omitted.

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

Enter fullscreen mode Exit fullscreen mode

sample-app.ts

const app = new cdk.App();

new SampleAppStack(app, "SampleAppStack", configStackProps);
Enter fullscreen mode Exit fullscreen mode

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",
  },
};

Enter fullscreen mode Exit fullscreen mode

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", {
      ...
      ...(省略)
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

Description

sample-app.ts

Since env.region is contained in the stack parameter object called configStackProps defined in config.ts, which is explained in the next section, by passing it as a stack props (3rd argument), deployment in the specified region is possible.

new SampleAppStack(app, "SampleAppStack", configStackProps);
Enter fullscreen mode Exit fullscreen mode

config.ts

This file defines the parameters to be passed to the constructor of the stack class.

I define an interface called ConfigStackProps that inherits from StackProps (interface) to be passed to the constructor of Stack, and an interface called Config that defines its own parameters.

export interface Config {
  scopeType: string;
}

export interface ConfigStackProps extends StackProps {
  config: Config;
}
Enter fullscreen mode Exit fullscreen mode

It will store parameters of its own Config type as well as env and other parameters including region that you originally wanted to pass in ConfigStackProps with StackProps.

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

sample-app-stack.ts

Since the configStackProps defined earlier is passed in the stack constructor, I get the scopeType from it.

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

    const scopeType = props.config.scopeType;
Enter fullscreen mode Exit fullscreen mode

Then comes the main subject, the addValidation part.

Create an instance of the validator class (see below) and pass it to this.node.addValidation.

    const wafRegionValidator = new WafRegionValidator(scopeType, this.region);
    this.node.addValidation(wafRegionValidator);
Enter fullscreen mode Exit fullscreen mode

This will validate parameters by the CDK before synthesize.

waf-region-validator.ts

Here are the details of the validator class used earlier in the stack class to pass to addValidation.

This implements an interface called IValidation.

export class WafRegionValidator implements IValidation {
Enter fullscreen mode Exit fullscreen mode

I need to implement the method public validate(): string[], so I implement the validation logic here. The return value is an array of error messages.

  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;
  }
Enter fullscreen mode Exit fullscreen mode

The logic is implemented by push the array in case of a validation error, and return an empty array in case of a non-error, so that the validation passes.

Then, when the number of elements in the return value is greater than zero, CDK internally executes thow new Error.

Also, since the return type is an array of strings, this means that multiple error messages can be returned.

Verification

In the config.ts explained above, set scopeType to CLOUDFRONT and specify ap-northeast-1 for region on purpose.

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

This addValidation is validated at synthesize time, not at runtime, and can be checked with the cdk synth command.

Then try 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

Enter fullscreen mode Exit fullscreen mode

Thus, when the validation did not pass, the error message stored in the array in validate() earlier was output.

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

Also, since the return type of validate() is string[], it is possible to output multiple errors as follows when storing multiple error messages.

Error: Validation failed with the following errors:
  [SampleAppStack] Test Error 1.
  [SampleAppStack] Test Error 2.
Enter fullscreen mode Exit fullscreen mode

Advantages

It is possible to validate constructs and stacks without using addValidation.

However, addValidation allows for multiple property errors to be output at once (so that errors can be returned in an array instead of throwing them when they occur), which improves the development experience compared to handling new errors with if statements in the constructor. And addValidation can also validate not only input values, but also values generated after resource creation.

Separating the layers of validation and stack configuration, and encapsulating the validation process in the validator class has the advantage of higher maintainability since there is no need to modify the stack class itself if you want to change the validation method. This has the advantage of being more maintainable. The separation of responsibilities makes the code cleaner and improves the perspective of the stack structure.

Finally.

When doing validation with CDK, I used to generate my own Error instances for validation, but I happened to find such a method and used it.

Have fun with CDK!

Top comments (0)