← Journal
· 9 min #aws#cdk#iac

L1, L2 and L3 CDK constructs — and when to use each

A practical mental model for the three CDK construct levels and when each one is the right tool.

Cross-post Originally published on Medium ↗

Originally published on Medium.

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

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.

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.

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;  /** Lambda Permission to define */
  lambdaPermission: Record;\}
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 ?

Where to use L2 Constructs & where not ?

Where to use L3 Constructs & where not ?

💡 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

Newsletter · low-volume

A note when something new lands.

Long-form essays on agentic AI, MLOps, and production systems. No drips, no funnels — one mail when there is something worth reading.

No spam. Unsubscribe anytime.