DEV Community

k.goto for AWS Community Builders

Posted on

Constrain external CDK parameters not only by type

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.

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

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.

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

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

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: "***@***.***",
  },
};
Enter fullscreen mode Exit fullscreen mode

(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).

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

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.

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() {
    // ...
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)