DEV Community

Cover image for You Don’t Need a Construct for That: Best Practices for Serverless Infrastructure with AWS CDK Blueprints

You Don’t Need a Construct for That: Best Practices for Serverless Infrastructure with AWS CDK Blueprints

If you use AWS CDK (Cloud Development Kit) to create infrastructure as code, you're probably familiar with Constructs and Aspects. Have you heard about CDK Blueprints? You can inject properties at L2 Constructs and apply best practices to all your resources at scale.

In this article, we are going to define best practices for a few resources by understanding the real meaning of CDK building blocks, see how easy it is to define Blueprints in CDK, explore the benefits of property injection, and understand why you don't need a Construct for applying best practices.

Introduction

"Everything in CDK is a Construct", but not everything needs to be a Construct.

No, this article is not only about CDK Constructs. What you're going to read briefly in the next session is a rough summary of the Constructs documentation, the way AWS wanted us to understand them, and understanding the semantics of it is going to make a big difference in your IaC (Infrastructure as Code) project.

For what would you need a Construct?

According to the documentation from CDK, Constructs are the basic building blocks of a CDK app, and they are a collection of one or more resources to be deployed via CloudFormation.

Moreover, Constructs are also defined in 3 levels (L1 for CloudFormation definition, L2 for a small layer of abstraction, and L3 for higher abstraction or often a collection of AWS resources). The documentation also explains that you can create your own Constructs for a specific pattern (example, S3 Bucket to SNS topic).

CDK construct levels

The purpose of them is to help you reach the desired state of your infrastructure as code by grouping resources and logic into ~basically~ classes.

Things you can do with Construct, but you don't need to (or should not?)

What you just read is all that CDK Constructs mean, according to AWS. But it's 2025, and we are creative people. Let's have a look at this example:

import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; import * as logs from 'aws-cdk-lib/aws-logs'; export interface ValidatedLogGroupProps extends logs.LogGroupProps { } export class ValidatedLogGroup extends Construct { public readonly logGroup: logs.LogGroup; constructor(scope: Construct, id: string, props: ValidatedLogGroupProps) { super(scope, id); if (props.retention !== logs.RetentionDays.TWO_WEEKS) { throw new Error('Retention must be set to TWO_WEEKS'); } if (props.removalPolicy !== cdk.RemovalPolicy.DESTROY) { throw new Error('Removal policy must be set to DESTROY'); } this.logGroup = new logs.LogGroup(this, 'LogGroup', props); } } 
Enter fullscreen mode Exit fullscreen mode

Since you write CDK with, in this example, TypeScript, you can validate the Construct props and throw exceptions if they are not compliant with standards defined by you. In practice, this means:

  • You created a new layer of abstraction upon the L2 Construct of LogGroup, which contains default props from CDK Lib that might change between versions. This can lead to operational overhead when bumping versions.
  • You will not allow LogGroups to be created without a two-week retention (in this hypothetical situation, this configuration is mandatory).
  • You write it, you maintain it. The extra logic is going to be maintained by you, even though it is a small class.

Although this is possible, you need to ask yourself the questions: "Is it worth it? Are there other options? Am I misusing CDK Constructs?"

Gladly, we have alternatives

The beauty of imperative IaC is that you can do pretty much everything you want, preferably following the semantics of the tool.

Aspects

Hold on, we are almost there. Let's first see if Aspects help us validate props values.

The documentation starts explaining that Aspects are "a way to apply an operation to all constructs in a given scope, or it could verify something about the state of the constructs, such as making sure that all buckets are encrypted". Ok, things are getting more interesting.

The purpose of an Aspect is to modify resources at the CloudFormation level based on what was defined by a higher-level construct (L2) or throw exceptions if they deviate from a standard, pretty much what we tried to achieve with Constructs above. For example:

class BucketVersioningChecker implements IAspect { public visit(node: IConstruct): void { // See that we're dealing with a CfnBucket if (node instanceof s3.CfnBucket) { // Check for versioning property, exclude the case where the property // can be a token (IResolvable). if (!node.versioningConfiguration || (!Tokenization.isResolvable(node.versioningConfiguration) && node.versioningConfiguration.status !== 'Enabled')) { Annotations.of(node).addError('Bucket versioning is not enabled'); } } } } // Later, apply to the stack Aspects.of(stack).add(new BucketVersioningChecker()); 
Enter fullscreen mode Exit fullscreen mode

Seems like we are getting there, but here are a few important points from the Aspects semantics:

  • You want to modify L1 Constructs (CloudFormation level)
  • You want to inspect (not modify) L2 Constructs props
  • You have to navigate the whole tree of resources and check one by one to inspect

Since we want to leverage the existing L2 LogGroup construct from CDK and modify the props to be compliant with your standards, we could use Aspects and modify the CloudFormation template after the fact - again, because it is possible does not mean it is the best way.

Blueprints

You know the drill: Let's see what the documentation says about CDK Blueprints.

"Use AWS CDK Blueprints to standardize and distribute L2 construct configurations across your organization. With Blueprints, you can ensure that AWS resources are configured consistently according to your organizational standards and best practices. For example, you can automatically enable encryption for all Amazon S3 buckets, apply specific logging configurations to all AWS Lambda functions, or enforce standard security rules for all security groups."

This is exactly what we are trying to achieve, and this is not me saying, but how AWS wants us to apply resource best practices.

Blueprints are powered by property injection, and each Blueprint is responsible for applying default properties to a specific L2 Construct when they are instantiated. We can think about security standards, cost optimization, compliance requirements, etc.

Let's get down to business and create some examples of Blueprints for a few resources:

CloudWatch Log Group

export class LogGroupPropsInjector implements IPropertyInjector { public readonly constructUniqueId: string; constructor() { this.constructUniqueId = LogGroup.PROPERTY_INJECTION_ID; } public inject(originalProps: LogGroupProps, _context: InjectionContext): LogGroupProps { return { ...originalProps, removalPolicy: RemovalPolicy.DESTROY, retention: RetentionDays.TWO_WEEKS }; } } 
Enter fullscreen mode Exit fullscreen mode

Lambda Function

export class MyNodejsFunctionPropsInjector implements IPropertyInjector { public readonly constructUniqueId: string; constructor() { this.constructUniqueId = NodejsFunction.PROPERTY_INJECTION_ID; } public inject(originalProps: FunctionProps, _context: InjectionContext): FunctionProps { return { ...originalProps, runtime: Runtime.NODEJS_22_X, memorySize: 128, timeout: Duration.seconds(30), }; } } 
Enter fullscreen mode Exit fullscreen mode

SQS Queue

export class MyQueuePropsInjector implements IPropertyInjector { public readonly constructUniqueId: string; constructor() { this.constructUniqueId = Queue.PROPERTY_INJECTION_ID; } public inject(originalProps: QueueProps, _context: InjectionContext): QueueProps { return { ...originalProps, visibilityTimeout: Duration.seconds(45), retentionPeriod: Duration.days(4), }; } } 
Enter fullscreen mode Exit fullscreen mode

Some extra remarks about CDK Blueprints:

  • Place default properties before …​originalProps to allow overrides.
  • Place forced properties after …​originalProps to prevent overrides.
  • Use CDK context to enable/disable injectors for testing.

The combination of the two

Since we are doing things "by the book" here, we also need to consider this sentence from the Blueprints docs:

"Blueprints are not a compliance enforcement mechanism. Developers can still override the defaults if needed. For strict compliance enforcement, consider using AWS CloudFormation Guard, Service Control Policies, or CDK Aspects in addition to Blueprints".

Even though it is possible to prevent overrides with Blueprints only (but remember the semantics!), you could combine it with Aspects in the following way:

  1. With your Blueprints in place, your L2 constructs are going to have some properties injected during initialization.
  2. If, for some reason, your Blueprints are not applied to the scope of a specific resource, you can have an Aspect as an extra layer of validation (Aspects inspection happens after construct initialization).
  3. Your Aspect will then check if a specific property was not injected for a specific resource. You can throw an exception or add a warning message saying you could use a pre-existing Blueprint from your project.

Conclusion

In the realm of AWS CDK, while Constructs are fundamental building blocks for defining infrastructure as code, they are not always necessary for enforcing best practices. The introduction of CDK Blueprints offers a more streamlined and efficient approach to standardizing configurations across your organization. By leveraging property injection, Blueprints allow you to apply default properties to L2 Constructs, ensuring consistency in resource configurations without the need for additional abstraction layers.

Ultimately, the combination of Blueprints and Aspects allows for a balance between flexibility and compliance, enabling you to maintain best practices at scale while still allowing for necessary customizations. This approach aligns with AWS's vision of how infrastructure as code should be managed, providing a powerful toolkit for modern cloud infrastructure management.

References

Top comments (0)