
Introduction
Constructs are the fundamental building blocks of AWS CDK. A Construct defines a cloud resource and contains everything necessary to create that resource.
But how does the CDK determine which Construct to create first and which ones to create later, to ensure that our resources work as intended? For example, a VPC must be created before launching an EC2 instance within it.
Why can some resources can be created with a single function, while others require multiple functions? For instance, we create an EC2 instance using just one function, but setting up Elasticache requires several different functions.
The concepts of Construct Trees and Construct Layers to answer these questions.
Construct Tree
from aws_cdk import ( Stack, App, Environment, aws_s3 as s3 ) from constructs import Construct class S3SimpleStack(Stack): def __init__(self, scope: Construct, id: str, **kwargs) -> None: super().__init__(scope, id, **kwargs) # Create S3 bucket s3.Bucket(self, "L2", versioned=False ) def main(): app = App() S3SimpleStack(app, "S3SimpleStack", env=Environment() ) app.synth() if __name__ == "__main__": main()
All components in AWS CDK, except for App, are passed a
value. By passing scope
scope
into components, CDK defines a hierarchical relationship among them, which is known as the Construct Tree.
At the top of the Construct Tree is the App. Within the App, there can be one or more Stacks. Each Stack contains one or more Constructs. Constructs, in turn, can hold L1 Constructs or L2 Constructs, continuing downward to form the Construct Tree.
The purpose of the Construct Tree is to enable CDK to create resources in the correct order, ensuring that dependencies are resolved efficiently.

In addition to the first parameter passed into a Construct, which is the scope
, there is a second parameter called the ID
. This value is used to uniquely identify a Construct within the scope. AWS CDK utilizes the ID
from the top of the Construct Tree down to the child Constructs to create a unique identifier for each Construct.
For example, in the code above, the unique identifier for the S3 Construct might be S3SimpleStack-L2.

Construct Layer
All Constructs are defined within the CDK library and are categorized into three levels:
- L1 Constructs
- L2 Constructs
- L3 Constructs

L1 Constructs
Introduction
L1 Constructs represent the lowest level in the hierarchy. They directly map to specific AWS CloudFormation resources. The names of L1 Construct functions typically start with
. When using L1 Constructs, you must specify all the properties of the resource, similar to how you would when using CloudFormation.Cfn
Firstly, If you don’t set up CDK AWS. You can go to the link
For example, Elasticache was created using an L1 Construct, the function to create Elasticache is called NewCfnCacheCluster
from aws_cdk import ( Stack, aws_ec2 as ec2, aws_elasticache as elasticache ) from constructs import Construct class QuestionCacheStack(Stack): def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: super().__init__(scope, construct_id, **kwargs) # ... (assuming vpc is created earlier) # Security Group sg = ec2.SecurityGroup(self, "CacheSG", allow_all_outbound=True, vpc=vpc ) # Add ingress rule sg.add_ingress_rule( peer=ec2.Peer.any_ipv4(), connection=ec2.Port( from_port=6379, to_port=6379, string_representation="Redis", protocol=ec2.Protocol.ALL ), description="Redis Port", remote_rule=False ) # Elasticache Cluster elasticache.CfnCacheCluster(self, "Cache", engine="redis", cache_node_type="cache.t2.micro", num_cache_nodes=1, vpc_security_group_ids=[sg.security_group_id] )
To enable access to Elasticache, you need to create additional resources such as a Security Group. When using L1 Constructs, you are required to have a deep understanding of the resource’s properties in CloudFormation to configure them correctly.
When to Use L1 Constructs
- When you need complete control over resources:
- You must configure every property of a resource, and L2 or L3 Constructs don’t provide the necessary customization.
- Example: Customizing advanced Elasticache settings or defining complex IAM policies.
- When L2 or L3 doesn’t support specific features:
- AWS CDK may not yet have L2 or L3 support for the resource or functionality you need.
- When migrating from CloudFormation to CDK:
- L1 Constructs map directly to CloudFormation resources, making the transition simpler.
- When optimizing for cost or performance:
- You need to fine-tune each component of a resource for specific system requirements.
L2 Constructs
Introduction
The next level is L2 Constructs, which represent specific AWS resources rather than CloudFormation resources. L2 Constructs are composed of multiple pre-written L1 Constructs, making it easier to create resources without writing extensive code. Using L2 Constructs requires only a few simple properties, and you don’t need to understand the detailed properties of the underlying resources as you do with L1 Constructs.
For example, creating an S3 bucket using an L2 Construct:
from aws_cdk import ( Stack, aws_s3 as s3 ) from constructs import Construct class S3SimpleStack(Stack): def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: super().__init__(scope, construct_id, **kwargs) # Create S3 bucket s3.Bucket(self, "L2", versioned=False )
When to Use L2 Constructs
- When you want to deploy AWS resources quickly:
- Use L2 Constructs for resources like S3, DynamoDB, or Lambda with minimal configuration.
- Example: Creating an S3 bucket with default settings.
- When simplifying resource management:
- Avoid dealing with the complexity of CloudFormation properties.
- When working with commonly used AWS resources:
- L2 Constructs are designed to handle most typical use cases.
L3 Constructs
Introduction
The highest level is L3 Constructs, also known as Patterns. L3 Constructs are designed to help you create familiar infrastructure patterns that combine multiple AWS resources.
For instance, an Amazon ECS service with an Application Load Balancer (ALB). Instead of writing L2 Constructs for ECS and ALB separately with various configurations to connect them, you can simply use an L3 Construct to streamline the process.
from aws_cdk import ( aws_ecs as ecs, aws_ecs_patterns as ecs_patterns, aws_ec2 as ec2 ) # Within your Stack class: load_balanced_fargate_service = ecs_patterns.ApplicationLoadBalancedFargateService( self, "Service", cluster=cluster, # Assuming 'cluster' is defined elsewhere memory_limit_mib=1024, desired_count=1, cpu=512, task_image_options=ecs_patterns.ApplicationLoadBalancedTaskImageOptions( image=ecs.ContainerImage.from_registry("amazon/amazon-ecs-sample") ), task_subnets=ec2.SubnetSelection( subnets=[ ec2.Subnet.from_subnet_id( self, "subnet", "VpcISOLATEDSubnet1Subnet38A17FV3" ) ] ), load_balancer_name="application-lb-name" )
We learn how to use each Construct and what Layer it has through the API Reference.
When to Use L3 Constructs
- When building complex infrastructure patterns:
- Use L3 Constructs to deploy familiar architecture patterns that combine multiple AWS resources. For example, creating an Amazon ECS service with an Application Load Balancer (ALB).
- When saving development time:
- L3 Constructs reduce the time needed to write and configure code for complex setups.
- When prioritizing simplicity and maintainability:
- L3 Constructs keeps your code concise and focuses on business logic rather than resource configuration.
- When deploying end-to-end solutions:
- You want to ensure all resources are correctly integrated without much manual configuration.
Summary
We have explored the concepts of Construct Tree and Construct Layers. As a general rule, it is recommended to use L2 Constructs for easier application development with CDK. We can combine L1, L2, and L3 Constructs to achieve your goals.
References
RELATED POSTS
View all