Validation with AWS CDK (addValidation)
Tags: aws, awscdk, cdk, typescript
Published: 2023-06-16 14:09:45

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.

```sh
❯ cdk --version
2.31.0 (build b67950d)
```

## addValidation

This time, I will use the method `addValidation` provided by **class Node(->Construct.node)** (see [this doc](https://docs.aws.amazon.com/cdk/api/v2/docs/constructs.Node.html#addwbrvalidationvalidation)).

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

```typescript
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.

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](https://docs.aws.amazon.com/cdk/api/v2/docs/constructs.IValidation.html)).

```typescript
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.

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](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-wafv2-webacl.html#cfn-wafv2-webacl-scope)).

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

```

### sample-app.ts

```typescript
const app = new cdk.App();

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

### config.ts

```typescript
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

```typescript
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

```typescript
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;
  }
}
```

## 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.

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

### 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.

```typescript
export interface Config {
  scopeType: string;
}

export interface ConfigStackProps extends StackProps {
  config: Config;
}
```

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`.

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

### sample-app-stack.ts

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

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

    const scopeType = props.config.scopeType;
```

Then comes the **main subject, the `addValidation` part**.

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

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

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`.

```typescript
export class WafRegionValidator implements IValidation {
```

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**.

```typescript
  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;
  }
```

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

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

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

Then try `cdk synth`.

```sh
❯ 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

```

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.
```

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.
```

## 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!