Constrain external CDK parameters not only by type
k.goto
Posted on July 22, 2023
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)
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>;
(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: "***@***.***",
},
};
(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;
}
}
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() {
// ...
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.
Posted on July 22, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.