Table of Contents
- Table of Contents
- Introduction
- Validation in AWS CDK
- AWS CDK Application Lifecycle
- Validation Methods in AWS CDK
- Supplement: Template Rewriting with addPropertyOverride
- Summary
Introduction
In AWS CDK, validation of values received from users is essential for both those who use AWS CDK to build application and infrastructure environments and those who contribute to AWS CDK itself. Moreover, this validation method can be implemented in basically the same way for both perspectives.
This article introduces how to use validation methods that are common to both "users" and "developers" of AWS CDK.
Validation in AWS CDK
Since AWS CloudFormation operates internally in AWS CDK, if you deploy without validating incorrect values, errors may occur during stack creation or updates in CloudFormation. By performing validation within AWS CDK code, you can terminate the application with an error before CloudFormation deployment is executed, allowing users to prevent deployment errors in advance. This enables safer and more rapid error detection and correction.
AWS CDK Application Lifecycle
The timing of execution for various validation methods in AWS CDK differs according to the application lifecycle of AWS CDK. The AWS CDK lifecycle consists of the following four phases:
- Construct phase
- Prepare phase
- Validate phase
- Synthesize phase
For details on each phase, please refer to the AWS CDK Developer Guide. Basically, most of the CDK code written by users is executed in the first Construct phase.
Validation Methods in AWS CDK
The four validation methods in AWS CDK that we will introduce are:
- Immediate Throw
- Aspects
- addValidation
- Annotations
1. Immediate Throw
This is the simplest validation method when using AWS CDK. Note that the term "immediate throw" is not official naming but rather a name I have given for convenience in this article.
Immediate throw involves checking values received through props passed as the third argument during construct initialization, or arguments passed to construct methods, using "if" statements and similar logic, and generating errors when values are unacceptable for the application.
This method is executed in the Construct phase, which is the earliest stage among the four phases mentioned earlier, where construct generation processing occurs. Since early error detection can localize the impact of errors, it is generally recommended to consider using this method first.
Specifically, you check parameters received through construct props (here MyConstructProps
) against application requirements using "if" statements, and throw errors when requirements are not met.
import { Construct } from 'constructs';
export interface MyConstructProps {
readonly myFlag: boolean;
readonly myParam?: number;
}
export class MyConstruct extends Construct {
constructor(scope: Construct, id: string, props: MyConstructProps) {
super(scope, id);
if (props.myFlag && props.myParam !== undefined) {
throw new Error('myParam cannot be specified when myFlag is true');
}
}
}
Token
There is one point to note with immediate throw: Token.
Token is a mechanism for storing values that will be resolved later in the lifecycle using temporary values. For example, it is used internally when handling number or string type values in AWS CDK that are represented by CloudFormation's !Ref
and determined at deployment time.
This means that if a validation target parameter is a Token, the value is still unknown at this lifecycle stage, making validation impossible. Therefore, when performing validation with immediate throw, you need to check whether the parameter is a Token and skip validation if it is a Token.
Specifically, you use the isUnresolved
method of the Token
class to check if it's a Token, and only check validation conditions when it's not a Token (when it returns false
). If a parameter is a Token, even if the user actually passes a value like 100
, it stores a value unrelated to the actual value like -1.88815458970875e+289
. If you put this through conditional logic like if (param < 0)
, it will cause unexpected errors.
import { Token } from 'aws-cdk-lib';
import { Construct } from 'constructs';
export interface MyConstructProps {
readonly myParam: number;
}
export class MyConstruct extends Construct {
constructor(scope: Construct, id: string, props: MyConstructProps) {
super(scope, id);
// When Token, Token.isUnresolved(props.myParam) returns true
// If props.myParam is a Token, it stores values unrelated to the actual value like -1.88815458970875e+289
if (!Token.isUnresolved(props.myParam) && props.myParam < 0) {
throw new Error('myParam must be 0 or greater');
}
}
}
For example, valueAsString
returned by the CfnParameter
construct, or bucketName
returned by the Bucket
construct which is an L2 construct for Amazon S3 buckets, have values that are resolved during actual deployment, so they are stored as Tokens within CDK code. It's common to pass these parameters to constructs through props. However, since constructs themselves cannot know what comes from where through props parameters, and they always have the possibility of being Tokens, caution is required.
2. Aspects
Next is validation using the Aspects feature.
Aspects is a method for applying operations to all configurations within a specific scope, such as a stack or construct. This is executed in the Prepare phase, which is the second phase of the lifecycle.
Aspects is not specifically a validation feature, but it is effective when you want to centrally validate specific types of resources within a stack. For example, it can be used for validation like "Are versioning settings applied to all S3 buckets in the stack?"
import { App, Aspects, IAspect, Stack, Tokenization } from 'aws-cdk-lib';
import { CfnBucket } from 'aws-cdk-lib/aws-s3';
import { IConstruct } from 'constructs';
// Aspects is implemented by implementing the IAspect interface
export class BucketVersioningChecker implements IAspect {
// Write validation content in the visit method
public visit(node: IConstruct) {
// Since the visit method is applied to all resources inside the Construct passed by Aspects, use conditional logic to execute only for CfnBucket resources
if (node instanceof CfnBucket) {
// Generate an error if bucket versioning configuration is disabled
if (
!node.versioningConfiguration ||
(!Tokenization.isResolvable(node.versioningConfiguration) && node.versioningConfiguration.status !== 'Enabled')
) {
throw new Error('Versioning is not enabled');
}
}
}
}
const app = new App();
const stack = new Stack(app, 'MyStack');
// Apply BucketVersioningChecker to all resources in MyStack
Aspects.of(stack).add(new BucketVersioningChecker());
3. addValidation
Next is the validation method using the addValidation method.
The addValidation
is a method of the Node class that Stack
and Construct
internally hold as variables, and is executed in the Validate phase, which is the third phase of the lifecycle.
The use case for the addValidation
is for delayed evaluation to validate cases where parameter values change not only during construct initialization (when generating a construct with new
) but also after construct methods are called.
For example, "There is an array-type parameter that can be set through both props
and methods, and you want to check the number of elements in that array". This means cases where no array-type parameter is passed through props during construct generation, but elements are added to the array by calling construct methods afterward. In this scenario, if you perform validation immediately in the constructor and throw an error, it would only check the parameter state before any method calls are made. Therefore, the addValidation
is optimal for such cases.
An implementation example of the addValidation
is as follows:
export interface MyConstructProps {
readonly myVariables: string[];
}
export class MyConstruct extends Construct {
public readonly myVariables: string[];
constructor(scope: Construct, id: string, props: MyConstructProps) {
super(scope, id);
this.myVariables = props.myVariables;
// Pass an object of IValidation interface type with a validate method to addValidation
this.node.addValidation({ validate: () => this.validateVariables() });
}
// For readability, the actual validation content is written in a separate method
private validateVariables(): string[] {
const errors: string[] = [];
if (this.myVariables.length > 3) {
errors.push(`myVariables should have at most 3 elements, current count: ${this.myVariables.length}`);
}
return errors;
}
// This method allows adding elements to the array after construct generation
public addVariable(variable: string) {
this.myVariables.push(variable);
}
}
The addValidation method takes an object of IValidation
interface type with a validate(): string[]
method as an argument. You write the actual validation content in the validate
method.
interface IValidation {
validate(): string[];
}
Furthermore, a characteristic of addValidation is that instead of throwing errors, it stores error messages in a string[]
type array and returns them. This allows displaying multiple errors at once.
private validateVariables(): string[] {
const errors: string[] = [];
if (this.myVariables.length > 3) {
errors.push(`myVariables should have at most 3 elements, current count: ${this.myVariables.length}`);
}
return errors;
}
By using addValidation this way, even in cases where props don't pass any elements to the array and elements are added through methods after construct initialization, validation can be delayed and cause errors.
const app = new App();
const stack = new Stack(app, 'MyStack');
const myConstruct = new MyConstruct(stack, 'MyConstruct', { myVariables: [] });
myConstruct.addVariable('variable1');
myConstruct.addVariable('variable2');
myConstruct.addVariable('variable3');
myConstruct.addVariable('variable4');
Error: Validation failed with the following errors:
[MyStack/MyConstruct] myVariables should have at most 3 elements, current count: 4
4. Annotations
Finally, there's the method using Annotations.
Annotations is a feature for attaching errors and warnings to constructs. This chapter focuses on error output methods rather than value checking methods. Since this can be called directly from constructs or from Aspects and addValidation, the processing execution timing depends on those phases, but the triggering (output) of attached errors is performed by the CDK CLI after all phases of the CDK application are completed.
For example, if you generate an error through Annotations within Aspects, that processing itself is executed in the Prepare phase and the error is attached to the construct. However, the error output processing to users is performed after all application phases are completed. If an error is detected by addValidation in the Validate phase after calling Annotations in Aspects, the Annotations error will not be triggered, meaning it will not be output.
Unlike previous methods, Annotations has the characteristic that you can issue warnings instead of errors by using the addWarningV2 method. This makes it effective for expressing "not good but not serious enough for an error". For example, it's often used when deprecated parameters are specified.
import { Annotations } from 'aws-cdk-lib';
import { Construct } from 'constructs';
export interface MyConstructProps {
/**
@deprecated Use `newParam` instead
*/
readonly oldParam?: string;
readonly newParam?: string;
readonly arrayParam: string[];
}
export class MyConstruct extends Construct {
constructor(scope: Construct, id: string, props: MyConstructProps) {
super(scope, id);
// Issue a warning when oldParam is specified
if (props.oldParam !== undefined) {
Annotations.of(this).addWarningV2(
'MyConstruct:oldParam',
`'oldParam' parameter is deprecated. Please use 'newParam' instead.`,
);
}
// Issue an error when arrayParam is empty
if (props.arrayParam.length === 0) {
Annotations.of(this).addError(`'arrayParam' cannot be empty`);
}
}
}
You can also attach errors to constructs using the addError method. The behavior of this error is distinctive.
In Annotations, if an error is attached through the addError
method, the error is output like other validations, but synthesis itself succeeds. Successful synthesis means that even when errors are output, cloud assembly is generated. Cloud assembly refers to manifest.json
and AWS CloudFormation template files generated based on CDK code, stored in the cdk.out directory at the top level of the CDK project. Immediate throw and addValidation do not generate cloud assembly when errors occur.
This leads to different behavior in multi-stack applications. In multi-stack applications, if one stack has an error attached by Annotations, cdk synth
and cdk deploy
will fail for the stack with the attached error. However, synth and deploy will succeed for other normal stacks.
For example, if an application has two stacks, StackA and StackB, and StackA has an error attached by Annotations, cdk synth StackA
will error, but cdk synth StackB
will succeed. With immediate throw and similar methods, if one stack has an error, all other stacks will also fail.
While Annotations has these characteristics, it's generally safer to use immediate throw and similar methods. Cases for attaching errors with Annotations include situations where the CDK resource declaration is correct but errors occur due to environmental factors such as missing context values referenced through context methods. This usage pattern is also recommended in the AWS CDK repository's DESIGN_GUIDELINES.
It would also be the only approach when you want to issue warnings without making them errors.
Supplement: Template Rewriting with addPropertyOverride
In AWS CDK, you can directly rewrite the contents of CloudFormation templates generated during the Synthesize phase using methods like addPropertyOverride. This method allows you to forcibly enable S3 bucket versioning settings, for example.
import { CfnBucket } from 'aws-cdk-lib/aws-s3';
declare const cfnBucket: s3.CfnBucket;
cfnBucket.addPropertyOverride('VersioningConfiguration.Status', 'NewStatus');
This template rewriting process is executed in the Synthesize phase, which is the last phase of the AWS CDK lifecycle. This means that template rewriting is not yet reflected when the various validations mentioned earlier are executed. Therefore, these rewritten contents cannot be properly validated by any method. Caution is needed as unintended errors may occur in the following cases:
- When declaring an S3 bucket construct, versioning configuration is not specified
- addPropertyOverride is called in Aspects (Prepare phase) to set versioning configuration to enabled
- Validation by addValidation (Validate phase) errors when there are S3 buckets without enabled versioning configuration
*policyValidationBeta1 allows validation in this case, but note that it is still an experimental feature.
Summary
We have introduced the following four validation methods in AWS CDK along with their characteristics and usage patterns:
- Immediate Throw
- Aspects
- addValidation
- Annotations
These are validation methods that can be used commonly whether you're "using" or "developing" AWS CDK. By understanding the characteristics of each and selecting appropriate validation methods, let's write safer and more reliable AWS CDK code.
Top comments (0)