Upcoming JavaScript Features: Simplifying Array Combinations with Array.zip and Array.zipKeyed
- Development
Andre Santos Andre Santos
August 11, 2025 • 5 min read
Following on from our deep dive into managing Lambdas with AWS CDK, we talked about how CDK lets you define cloud infrastructure using actual code, not YAML. After shipping a few services, you’ll notice the same setup repeating: buckets, Lambdas, CloudFront, over and over. Different teams will solve the same problems slightly differently — and that’s how things drift.
Why not treat infra like real code? Abstract the repeated stuff into something clean and reusable. That’s where custom CDK constructs come in.
You’ll often hear people talk about “L1,” “L2,” and “L3” constructs — and if you’re new to AWS CDK, you likely won't have any idea what they're talking about. These labels are shorthand for the level of abstraction you’re working with. The 'L's come in three flavors:
We’ll focus on creating and using an L3 construct to simplify a common infra pattern.
Implementation
This L3 custom construct sets up three things for you:
Here’s how it all comes together:
export interface CloudfrontS3WebsiteProps {
bucketName: string;
sourcePath: string;
}
export class CloudfrontS3Website extends Construct {
public readonly bucket: Bucket;
public readonly cloudfrontDistribution: Distribution;
public readonly bucketDeployment: BucketDeployment;
constructor(scope: Construct, id: string, props: CloudfrontS3WebsiteProps) {
super(scope, id);
const { bucketName, sourcePath } = props;
this.bucket = new Bucket(this, bucketName, {
bucketName,
websiteIndexDocument: 'index.html',
publicReadAccess: false,
blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
removalPolicy: RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});
this.cloudfrontDistribution = new Distribution(this, `SiteDistribution-${bucketName}`, {
defaultRootObject: 'index.html',
minimumProtocolVersion: SecurityPolicyProtocol.TLS_V1_2_2021,
defaultBehavior: {
origin: S3BucketOrigin.withOriginAccessControl(this.bucket),
compress: true,
allowedMethods: AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
},
});
this.bucketDeployment = new BucketDeployment(this, `BucketDeployment-${bucketName}`, {
sources: [Source.asset(sourcePath)],
destinationBucket: this.bucket,
distribution: this.cloudfrontDistribution,
distributionPaths: ['/*'],
});
}
}
Usage
Using this is as simple as it gets. Just drop it into your stack like this:
import { CloudfrontS3Website } from '@andrelopesmds/cloudfront-s3-website'
new CloudfrontS3Website(this, 'MyStaticSite', {
bucketName: 'my-bucket-name',
sourcePath: './dist/'
})
And just like that — you get a secure static website with CloudFront and S3, all set up and deployed with minimal hassle.
Building custom constructs like this isn’t just about saving a few lines of code. It’s about:
At the end of the day, it frees you and your team to focus on building features, not wrestling with infrastructure.
When your AWS CDK projects grow, you’ll spot patterns in how you build your infrastructure. Instead of repeating yourself (or letting teams do their own thing), build custom constructs to clean things up.
Start small, make it solid, and share it with your team. Before you know it, you’ll have a toolkit that saves time, reduces errors, and keeps everyone on the same page.
It’s a simple way to make infrastructure feel more like code — exactly what CDK is about. Your L3 custom constructs slash repetition, lock in standards, and most importantly, help you Get Shit Done.
Links: