Constrain external CDK parameters not only by type
Tags: aws, cdk, typescript, zod
Published: 2023-07-22 10:43:40

External parameters passed to the stack in CDK are sometimes good with types if they are defined in TypeScript.

So, I came up with the idea of "Let's add a constraint along with the type and validate it" and it was surprisingly good.

---

## Assumptions

CDK is written in TypeScript and uses v2.

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

In order to focus on how to achieve this, files, codes, etc. for non-main parts are omitted.

---

## Summary

### To do

- Pass parameters (config) from outside the stack to the CDK stack
- Define parameters in TypeScript file instead of cdk.json or environment variables to **guarantee** type
- In this case, parameters are not only **typed** but also **validated** with constraints.
- Use Zod for validation and apply the types created from the Zod schema to the parameters

### The good

- More secure by being able to **validate** together as well as **type guarantee**
- You don't have to define a separate type for parameters, you can just use the type you created for **validation**!

---

## Code

I will take the four main files as examples.

- (1)Zod schema (constraint rules and types)
- (2)External parameter definitions
- (3)Validator class
- (4)Stack class

The dependencies are as follows.

- (4) -> (1), (2), (3)
- (3) -> (1), (2) (Does not directly depend on (2), but in effect validates (2))
- (2) -> (1)

### (1) Zod schema (constraint rules and types)

There is a validation module called Zod, which can be used for everything from character counts and options to **defining URLs and emails**.

In this example, I also apply **cron constraints**. (I know this is a bit crude, but it's just a sample...)

Then, **generate a type from the Zod schema** and use it **as a type for external parameters** as described below.

```typescript
import { z } from "zod";

export const StackInputSchema = z.object({
  incomingWebhookUrl: z.string().url().min(1),
  scheduleExpression: z.string().startsWith("cron(").endsWith(")"), // cron式
  senderAddress: z.string().email().min(1),
});

 // Generate type from schema
export type StackInput = z.infer<typeof StackInputSchema>;
```

### (2) Defining external parameters

By defining an interface that extends (inherits from) `StackProps`, I can **inject typed parameters into the CDK stack**.

Since I have just **generated** a type from the **Zod schema,** I can use that type and apply the type to external parameters.

```typescript
import { StackProps } from "aws-cdk-lib";
import { StackInput } from "./types/stack-input";

export interface ConfigStackProps extends StackProps {
  config: StackInput; // <- This
}

 // Parameters themselves are defined here
export const configStackProps: ConfigStackProps = {
  env: {
    region: "ap-northeast-1",
  },
  config: {
    incomingWebhookUrl: "https://hooks.slack.com/services/********",
    scheduleExpression: "cron(0 4 ? * MON-FRI *)",
    senderAddress: "***@***.***",
  },
};
```

### (3) Validator class

This is a class that performs validation.

By using Zod's parse function (`StackInputSchema.parse`), it can throw an error if a constraint defined in the Zod schema is violated (validation error).

```typescript
import { IValidation } from "constructs";
import { StackInput, StackInputSchema } from "../types/stack-input";

export class StackValidator implements IValidation {
  private stackInput: StackInput;

  constructor(stackInput: StackInput) {
    this.stackInput = stackInput;
  }

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

    try {
      StackInputSchema.parse(this.stackInput);
    } catch (e) {
      errors.push(JSON.stringify(e));
    }

    return errors;
  }
}
```

This StackValidator class implements an interface called `IValidation`, which will be used by the CDK Stack class to perform validation by executing the method `addValidation`.

In addition to Zod validation, it is also possible to implement **checks based on original rules/processes** here, so it is quite convenient to **combine handling with Zod constraints**.

- Example.
  - cron time must be in the midnight zone or an error will occur
  - Restrict the domain of the specified email address
  - Create cases where certain parameter combinations are NG

### (4) Stack class

By picking up external parameters from props in constructor, creating a StackValidator instance, and passing it to the addValidation method, I can **perform constraint validation** by Zod.

If you implement validation with this pattern, the **defined validation will be performed during cdk synth**.

```typescript
export class SampleStack extends Stack {
  // Parameters of type created from schema to variable
  private stackInput: StackInput;

  // Apply ConfigStackProps defined in an external parameter file to props.
  constructor(scope: Construct, id: string, props: ConfigStackProps) {
    super(scope, id, props);

    this.init(props);
    this.create();
  }

  private init(props: ConfigStackProps): void {
    this.stackInput = props.config;

    // Here validation rules are applied to the CDK layer
    const stackValidator = new StackValidator(this.stackInput);
    this.node.addValidation(stackValidator);
  }

  private create() {
    // ...
```

---

## Supplemental

External parameters like this could be made **more secure** with not only type but also constraints.

### Templating

The validator class in (4) can be used as a template (copy and paste) **when writing CDK in the future because** the parameters themselves are encapsulated in StackInput and can be written in a way that does not depend on specific parameter types.

### Validation method

This time I created a StackValidator class and used a CDK function called addValidation to perform validation, but it is also possible to perform constraint checking by directly parsing the Zod in the constructor of the stack class without using these functions.

However, the IValidation used in addValidation can output **all of the multiple errors that were violated (see the above article for details), and is recommended because it improves the development experience** over handling errors yourself in the constructor.

By using the functionality provided for validation, you are also **riding the official rails**, which is also a good thing.

Also, by removing the validation process from the stack class, the **look of the stack is improved and maintainability is also improved**, which is also an advantage.

---

## Finally

Type external parameters with CDK. Validate with constraints. Then use the type used in the validation for the external parameter.

I came up with this circular combination and tried it out and it seemed to work well, so I decided to write an article about it.

I know there are not many precedents for this method, but I hope you will find it useful as an example.