How to create L1,L2 and L3 CDK Constructs ? and When to use it ?

Subbusainath Rengasamy
8 min readFeb 11, 2024

--

An Anime character coding in his desktop
Coding is fun

Introduction

After a gap, here I am! with one of the key concepts to understand while using the CDK constructs in your CDK codes. In this blog, I am going to talk about the details of the CDK constructs and how to use them better to ease our resource creation.

Pre-Requisites

Y’all need these pre-requisites to follow along with this blog.

  • Code Editor of your choice ( I use VSCode )
  • AWS Account ( Required )
  • AWS CDK ( Required )
  • Install CDK using following command. I am using WSL. So, I chose NPM
  • npm install -g aws-cdk
  • Initialize your CDK project with a sample app by executing this command in your desired folder. We are going to use this sample app in this blog going further. I am choosing typescript as the coding language for this blog. But you can choose whatever language CDK supports and that you like!
  • cdk init sample-app --language=typescript
  • Once, you executed the above-mentioned command. You will see files like these in the folder where you initiate the project. For Example,
  • Basic Knowledge of AWS Resources

What is CDK ?

CDK stands for Cloud Development Kit, which is an open source framework created by AWS to use it as an IaC (Infrastructure as a Code) that uses common programming languages to create AWS resources. Under the hood by provisioning it through AWS Cloudformation. In simple terms, you will be using a common programming language to create an AWS resource without worrying about YAML files or JSON files.

What is Constructs in CDK ? What are the different level of CDK Constructs ?

It contains code components that represent the AWS resources and their configurations, which are reusable. If anyone wants to create any resources from AWS, we can use these constructs to create those resources at ease whenever we want to. It’s like a building block that we use to build certain types of buildings while we are playing.

There are 3 different levels of CDK Constructs.

  • L1, which are very low-level constructs that directly map to AWS Cloudformation.
  • L2, which are higher-level constructs that are built on top of L1, It simplifies the configuration and management of AWS resources by providing convenient APIs.
  • And finally, L3 is a pre-configured construct that encapsulates some best practices and common use cases. It uses “patterns” (known as)

CDK Constructs enable you to define and deploy AWS resources in a programmatic and scalable manner, making it easier to manage infrastructure as code and automate the provisioning of resources in your AWS environment.

L1 , L2 and L3 Constructs — Examples

In this, we are going to create an API that returns hello world by creating apigateway and lambda in L1 constructs.

import { Stack, StackProps } from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda'
import * as apigatewayv2 from 'aws-cdk-lib/aws-apigatewayv2'
import { Construct } from 'constructs';
export class CdkConstructsStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
// this is l1 construct code for apigateway
const helloApi = new apigatewayv2.CfnApi(this, "helloAPi", {
apiKeySelectionExpression: "string",
basePath: "/",
body: JSON,
bodyS3Location: {
bucket: "string",
etag: "string",
key: "string",
version: "string",
},
corsConfiguration: {
allowCredentials: false,
allowHeaders: ["Content-Type/applicaton-json"],
allowMethods: ["GET"],
allowOrigins: ["CORS"],
exposeHeaders: ["Content-Type/applicatio-json"],
maxAge: 1,
},
credentialsArn: "string",
description: "basic template to for l1 construct for an api",
disableExecuteApiEndpoint: false,
disableSchemaValidation: false,
failOnWarnings: true,
name: "helloApiL1Construct",
protocolType: "string",
routeKey: "string",
routeSelectionExpression: "string",
tags: {
"name": "sai"
},
target: "string",
version: "1.0",
})
// this is l1 construct code for lambda
const helloLambda = new lambda.CfnFunction(this, "hello-world-l1", {
architectures: ["arm_64"],
code: {
imageUri: "string",
s3Bucket: "string",
s3Key: "string",
s3ObjectVersion: "string",
zipFile: "string",
},
description: "this is just an hello world lambda using l1 constructs",
environment: {
},
ephemeralStorage: {
size: 512,
},
fileSystemConfigs: [],
functionName: "helloFunction",
handler: "string",
imageConfig: {
command: [],
entryPoint: [],
workingDirectory: "string",
},
kmsKeyArn: "string",
layers: [],
loggingConfig: {
applicationLogLevel: "string",
logFormat: "string",
logGroup: "string",
systemLogLevel: "string",
},
memorySize: 512,
packageType: "string",
reservedConcurrentExecutions: 4,
role: "string", // Required
runtime: "NODEJS20.X",
runtimeManagementConfig: {
runtimeVersionArn: "string",
updateRuntimeOn: "string",
},
snapStart: {
applyOn: "string",
},
tags: [],
timeout: 60,
vpcConfig: {
ipv6AllowedForDualStack: false,
securityGroupIds: [],
subnetIds: [],
},
})
// this is for l1 construct for lamnda integration with api
const helloApiIntegration = new apigatewayv2.CfnIntegration(this, "apiIntegr", {
apiId: helloApi.attrApiId, // Required
connectionId: "string",
connectionType: "string",
contentHandlingStrategy: "string",
credentialsArn: "string",
description: "string",
integrationMethod: "GET",
integrationSubtype: "string",
integrationType: "AWS_PROXY", // Required
integrationUri: helloLambda.attrArn,
passthroughBehavior: "string",
payloadFormatVersion: "string",
requestParameters: JSON,
requestTemplates: JSON,
responseParameters: JSON,
templateSelectionExpression: "string",
timeoutInMillis: 29,
tlsConfig: {
serverNameToVerify: "string",
},
})

// this is l1 construct of iam for lambda with apigateway
const lambdaPolicy = new lambda.CfnPermission(this, "id", {
action: "ALLOW", // Required
eventSourceToken: "string",
functionName: helloLambda.attrArn, // Required
functionUrlAuthType: "string",
principal: "AWS_IAM", // Required
sourceAccount: "string",
sourceArn: helloApi.attrApiEndpoint,
})
}
}

In the above-mentioned snippet, you can see the exact properties that are similarly available in cloudfromation template for each resource. Which gives you the advantage of creating the resources with customization.

Same as above, hello world endpoint, but this time we used L2 Construct to create that resource.

// this is l2 construct  code for apigateway
const helloApi = new RestApi(this, 'helloApiId', {
restApiName: `helloRestApiName`,
description: "This is an api using L2 Construct",
endpointTypes: [EndpointType.EDGE]
})
    // this is l2 construct for Lambda
const helloLambda = new Function(this, 'helloFunction', {
handler: "hello-handler.handler",
runtime: Runtime.NODEJS_20_X,
code: Code.fromAsset("./lib/functions/hello-handler.ts"),
description: "this is a helloLambda using L2 Construct",
architecture: Architecture.ARM_64,
functionName: "L2HelloLambdaFunction"
})
// this is lambda integration using l2 construct const root = helloApi.root.addResource('hello')
root.addMethod('GET', new LambdaIntegration(helloLambda))

// this is lambda permission using l2 construct
helloLambda.addPermission("lambdaPermissionForApiGateway", {
principal: new ServicePrincipal("apigateway.amazonaws.com"),
action: "lambda.InvokeFunction",
sourceArn: helloApi.arnForExecuteApi('GET', "/hello", "DEV")
})

But there are fewer properties with the abstracted layer and fewer lines of code with some of the default options.

So far, we have repeated code to create a simple hello-world endpoint. We are wasting so much time creating a simple endpoint along with lambda integration. Let’s reduce this code repetition using the L3 construct.

First, we need to create our custom-patterned apigateway lambda construct like this:

import * as cdk from 'constructs';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { ServicePrincipal } from 'aws-cdk-lib/aws-iam';
export interface ApiGatewayLambdaConstructProps {
/** Name of the API Gateway */
readonly apiName: string;
/** Lambda function to integrate with */
readonly lambdaFunction: lambda.Function;
/** Resources and methods to define (optional) */
readonly resources?: Map<string, apigateway.MethodOptions>;
/** Lambda Permission to define */
lambdaPermission: Record<string, lambda.Permission>;
}
export class ApiGatewayLambdaConstruct extends cdk.Construct {
constructor(scope: cdk.Construct, id: string, props: ApiGatewayLambdaConstructProps) {
super(scope, id);
const api = new apigateway.RestApi(this, props.apiName, {
endpointTypes: [apigateway.EndpointType.EDGE]
});
// Add resources and methods based on props
if (props.resources) {
for (const [resourcePath, methodOptions] of props.resources.entries()) {
const resource = api.root.addResource(resourcePath);
resource.addMethod('GET', new apigateway.LambdaIntegration(props.lambdaFunction));
}
}
props.lambdaPermission["permission"] = {
principal: new ServicePrincipal("apigateway.amazonaws.com"),
action: "lambda.InvokeFunction",
sourceArn: api.arnForExecuteApi('GET', "/hello", 'DEV')
}
}
}

Once we build our custom construct, we can import it into our cdk stack like normal stacks.

// this is l2 construct for Lambda
const helloLambda = new Function(this, 'helloFunction', {
handler: "hello-handler.handler",
runtime: Runtime.NODEJS_20_X,
code: Code.fromAsset("./lib/functions/hello-handler.ts"),
description: "this is a helloLambda using L2 Construct",
architecture: Architecture.ARM_64,
functionName: "L2HelloLambdaFunction"
})
    // this to create /hello endpoint with lambda integration using L3 Construct     new ApiGatewayLambdaConstruct(scope, 'HelloApiGatewayLambdaConstruct', {
apiName: "helloApi",
lambdaFunction: helloLambda,
lambdaPermission: {
"resource": {
principal: new ServicePrincipal("apigateway.amazonaws.com"),
action: "lambda.InvokeFunction",
}
}
})
}

Like these, a few lines! We have now created an endpoint along with the lambda integration. It is very customizable and reusable. This is how we will be able to create the L3 construct. In the next section, we will be talking about when and where to use these constructs.

Where to use L1 Constructs & where not ?

  • If you need to have maximum granularity on the AWS resources for every single property, As I have mentioned before, L1 Constructs has a direct map with AWS Cloudformation.
  • If you need to try out any new services that have been released by AWS recently, You can try it out in L1 constructs before it is available in high-level constructs like L2 and L3.
  • It helps you troubleshoot issues with high-level constructs. Since both the L2 and L3 constructs are built on top of the L1 constructs,
  • If you want to try out unique cases that required to access specific Cloudformation properties.
  • If you don’t have in-depth knowledge about the cloudformation properties of AWS resources, then L1 is not suitable for you to use. Instead, use L2 or L3.
  • Maintaining resource creation is very hard in L1 constructs.
  • The portability issue is there. Since it is tightly coupled with cloudformation-related.

Where to use L2 Constructs & where not ?

  • If you don’t want to create any AWS resources from scratch by providing all information into the constructs similar to L1 Constructs and are willing to use some default configurations in the desired resources, you can use L2 Constructs.
  • Use it if you want to have consistency and a set of standards in your resource creation without any complexity.
  • Use it when you want to imply security practices for your AWS resources.
  • Use it when you don’t want to waste your time without a need.
  • It has limited configuration flexibility. So, if you want to use it for some edge-case scenarios, It is not suitable.
  • Troubleshooting the issue might be tricky since it adds more abstraction to resource creation. Just remember this while you are using L2 constructs.

Where to use L3 Constructs & where not ?

  • Use it when you want to reduce the complexity of creating resources with common patterns and redundant boilerplate code. It helps you maintain your resource stacks hustle-free.
  • Use it when you want to enforce a pattern for creating common infrastructure patterns, which saves you time. For example, as shown in the above L3 Construct Code Snippets Example, creating an ApiGateway with an endpoint backed by a lambda function.
  • Use it when you want to reuse it across the stacks. This will improve code consistency.
  • Use it when you want to create your own custom constructs that are not yet covered by the existing constructs. It gives you full control and flexibility over your infrastructure.
  • L3 constructs are not suitable if you want to have fine-grained access controls on your AWS resources. Because L3 constructs might be too restrictive.
  • It is not suitable if it does not have any common use cases where it is not effective as well.

💡 You can even publish your custom construct on https://constructs.dev/contribute

Conclusion

Always remember to understand the use of constructs when you use them for your purpose. CDK is a powerful tool; use it responsibly. Like in the movie Spiderman, where Uncle Ben advises Spiderman

With great power comes great responsibility

See you in next blog !

💡 official docs link for CDK : https://docs.aws.amazon.com/cdk/v2/guide/home.html

You can follow me on social medias:

Instagram : https://instagram.com/_valandhavaney_

X : https://x.com/@SubbuSainath

Personal Site: https://subbusainathr.bio.link

LinkedIn: www.linkedin.com/in/subbusainath-rengasamy-02609b188/www.linkedin.com/in/subbusainath-rengasamy-02609b188/

Github: github.com/subbusainath

--

--