Using and Creating AWS CDK Constructs
Learn how to build scalable infrastructure with AWS CDK constructs: dive into L1, L2, and L3 constructs, deploy S3 buckets, and streamline your AWS development.
Jan 30, 2025 • 6 Minute Read
The AWS Cloud Development Kit is ideal for building out your infrastructure alongside your code, with constructs forming the building blocks of your application's infrastructure definitions, representing one or more resources.
Constructs come in three levels: L1, L2, and L3. In this guide, the resources being deployed are simple: Three public S3 buckets, deployed via TypeScript, each representing a construct level.
This guide assumes you already have the CDK installed, bootstrapped, and configured to work with your AWS account. To follow along, create a new project in your preferred working directory:
mkdir bucket-constructs
cdk init app --language typescript
L1 Constructs: Working with Raw Building Blocks
Now let's begin with the lowest level constructs: L1 constructs, also called CFN resources. L1 CDK constructs are closest to the CloudFormation the CDK converts our code into, offering little to no abstraction.
If the CloudFormation for a public bucket resource looks like:
L1PublicBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: l1-public-bucket
PublicAccessBlockConfiguration:
BlockPublicAcls: false
BlockPublicPolicy: false
IgnorePublicAcls: false
RestrictPublicBuckets: false
Then the CDK L1 construct for the same thing resembles:
const l1bucket = new CfnBucket(this, 'L1PublicBucket', {
bucketName: 'l1-public-bucket',
publicAccessBlockConfiguration: {
blockPublicAcls: false,
blockPublicPolicy: false,
ignorePublicAcls: false,
restrictPublicBuckets: false,
},
removalPolicy: RemovalPolicy.DESTROY
});
Notice how it uses the CfnBucket resource to define the same properties as the CloudFormation definition, with nothing obfuscated and no shortcuts. In fact, it would probably be simpler to just write the CloudFormation in YAML if all of the CDK was like this. But it's not—generally, you won't be using L1 constructs when writing your code, these are just what lies under the more abstracted L2 and L3 constructs.
Now add this code to lib/bucket-constructs-stack.ts file, updating the imports to reflect the use of the CfnBucket construct, as well as the use of a removal policy.
The file should look like:
import * as cdk from 'aws-cdk-lib';
import { CfnBucket } from 'aws-cdk-lib/aws-s3';
import { RemovalPolicy } from 'aws-cdk-lib';
import { Construct } from 'constructs';
export class BucketConstructsStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?:
cdk.StackProps) {
super(scope, id, props);
// L1 Public Bucket Example
const l1bucket = new CfnBucket(this,
'L1PublicBucket', {
bucketName: 'l1-public-bucket',
publicAccessBlockConfiguration: {
blockPublicAcls: false,
blockPublicPolicy: false,
ignorePublicAcls: false,
restrictPublicBuckets: false,
},
});
l1bucket.applyRemovalPolicy(RemovalPolicy.DESTROY);
}
}
And deploy the code:
cdk deploy
A single bucket is created. Notice how no changes in the bucket's policy were made: The creation and configuration is entirely limited to the bucket itself, not any objects within it. L1 constructs are very specific—no shorthand here.
L2 Constructs: Simplifying Infrastructure Management
The most common construct type you will likely encounter and use is the L2 construct. These constructs are created by the AWS CDK team and essentially streamline the L1 construct so it's easier to use and nicer to look at.
For a public S3 bucket, our L2 construct might look like:
const l2bucket = new Bucket(this, 'L2PublicBucket', {
publicReadAccess: true,
blockPublicAccess: new BlockPublicAccess({
blockPublicAcls: false,
blockPublicPolicy: false,
ignorePublicAcls: false,
restrictPublicBuckets: false,
}),
removalPolicy: RemovalPolicy.DESTROY,
});
Now, it doesn't look like you saved yourself from writing lines of code here, but it does demonstrate an area in which the L1 construct lacks: Notably, while the L1 construct makes a bucket public, it doesn't make the individual objects public; you would need to expand the code to include a bucket policy construct to do that. This is unnecessary in an L2 construct—that level of access is condensed into the publicReadAccess: true line.
Now update the entire bucket constructs stack to look like:
import * as cdk from 'aws-cdk-lib';
import { CfnBucket } from 'aws-cdk-lib/aws-s3';
import { Bucket, BlockPublicAccess } from
'aws-cdk-lib/aws-s3';
import { RemovalPolicy } from 'aws-cdk-lib';
import { Construct } from 'constructs';
export class BucketConstructsStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?:
cdk.StackProps) {
super(scope, id, props);
// L1 Public Bucket Example
const l1bucket = new CfnBucket(this,
'L1PublicBucket', {
publicAccessBlockConfiguration: {
blockPublicAcls: false,
blockPublicPolicy: false,
ignorePublicAcls: false,
restrictPublicBuckets: false,
},
});
l1bucket.applyRemovalPolicy(RemovalPolicy.DESTROY);
// L2 Bucket Bucket Example
const l2bucket = new Bucket(this,
'L2PublicBucket', {
publicReadAccess: true,
blockPublicAccess: new BlockPublicAccess({
blockPublicAcls: false,
blockPublicPolicy: false,
ignorePublicAcls: false,
restrictPublicBuckets: false,
}),
removalPolicy: RemovalPolicy.DESTROY,
});
}
}
And deploy:
cdk deploy
Notice that this time you're prompted about an IAM statement change that allows the s3:GetObject action against the bucket, making it truly public without having to add a second construct.
L3 Constructs: Creating Reusable Patterns
L3 constructs take you a step even further than above. These constructs can be self-written, come from AWS or other resources, and let you repeat patterns, combining one or more L1 and L2 constructs to even further streamline your CDK code. For example, let's say you need to create public buckets in a clean, repeatable way, with the most minimal amount of code.
For this, you'll want to create a new file under lib, called public-bucket.ts. This is where you'll place your L3 construct configuration: The underlying code that will be referenced when actually calling the L3 construct.
This looks a lot like creating a stack, but instead of extending the cdk.Stack, you're working with the Construct library:
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Bucket, BlockPublicAccess } from
'aws-cdk-lib/aws-s3';
export class PublicS3Bucket extends Construct {
public readonly bucket: Bucket;
constructor(scope: Construct, id: string) {
super(scope, id);
this.bucket = new Bucket(this, 'Bucket', {
publicReadAccess: true,
removalPolicy: cdk.RemovalPolicy.DESTROY,
blockPublicAccess: new BlockPublicAccess({
blockPublicAcls: false,
ignorePublicAcls: false,
blockPublicPolicy: false,
restrictPublicBuckets: false,
}),
});
}
}
Looks a lot like the code for an L2 construct, right? And it is: Because it's essentially acting as a template for how you'll deploy a public S3 bucket, so you don't have to type it out each time.
So to use this pattern, return to lib/bucket-constructs-stack.ts, and add the following to the imports block:
import { PublicS3Bucket } from './public-bucket-construct';
Then to use the L3 construct, just add the following after the L2 construct definition:
new PublicS3Bucket(this, 'L3PublicBucket');
Notice how with a little care up front, it takes the 10-line public bucket definition down to a single, shorthand line that you can reference again and again.
And if you deploy:
cdk deploy
Then the CDK will deploy a third bucket with the same settings as the second—including that IAM change.
So, to be real concise, you can update the entire stack definition to look like:
import * as cdk from 'aws-cdk-lib';
import { PublicS3Bucket } from './public-bucket-
construct';
import { Construct } from 'constructs';
export class BucketConstructsStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?:
cdk.StackProps) {
super(scope, id, props);
new PublicS3Bucket(this, 'L1PublicBucket');
new PublicS3Bucket(this, 'L2PublicBucket');
new PublicS3Bucket(this, 'L3PublicBucket');
}
}
Much shorter, right? Although it does lose the effect of providing you an example of L1-L3 constructs to reference.
And, of course, remember to clean up when you're done:
cdk destroy
Key Takeaway on AWS CDK Constructs
If you've deployed anything with the CDK you've used a construct, but it can be easy to gloss over what level you're working with, and easy to miss out on the power that leveraging L3 constructs—either others' or your own—can provide by creating reusable, repeatable code.
Want to see these constructs and some others in action? Check out the Deploying Applications with the AWS CDK course.