Building Reusable Constructs in AWS CDK with Python
Move beyond basic stacks by learning how to create and publish your own reusable L3 constructs for AWS CDK, covering best practices for building modular and testable infrastructure components.
The AWS CDK allows us to define our infrastructure in code, but its true power is unlocked when we start building our own reusable components. While basic stacks are great for single applications, creating custom, high-level constructs is the key to building scalable, maintainable, and consistent infrastructure across many projects.
In the CDK world, these are often called L3 (Level 3) constructs. Let's dive into what they are and how to build one in Python.
Understanding the Levels of Constructs
The AWS CDK has a layered model for its constructs:
- L1 (Level 1) Constructs: These are the lowest-level constructs, mapping directly to raw CloudFormation resources (e.g.,
CfnBucket
). They are verbose and require you to configure everything manually. - L2 (Level 2) Constructs: These are the curated, high-level constructs that we use most often (e.g.,
s3.Bucket
). They provide sensible defaults, best practices, and helper methods, making them much easier to work with. - L3 (Level 3) Constructs: These are custom constructs that you build. An L3 construct is essentially a pattern—a composition of one or more L1 and L2 constructs designed to represent a complete piece of your architecture, like a static website, a serverless API, or a containerized service.
By creating L3 constructs, you can encapsulate best practices and complex configurations into a single, easy-to-use component.
Building a Reusable Static Website Construct
Let's build a practical L3 construct: a StaticWebsite
that provisions an S3 bucket for hosting, a CloudFront distribution for content delivery, and an Origin Access Identity (OAI) to secure the bucket.
Step 1: Set Up the Construct Class
First, create a new Python file for your construct. The class should inherit from constructs.Construct
.
from constructs import Construct
from aws_cdk import (
aws_s3 as s3,
aws_cloudfront as cloudfront,
aws_cloudfront_origins as origins,
RemovalPolicy
)
class StaticWebsite(Construct):
def __init__(self, scope: Construct, id: str, **kwargs):
super().__init__(scope, id)
# Our infrastructure definitions will go here
Step 2: Define the S3 Bucket
Inside the __init__
method, create the S3 bucket that will host the website content. We'll configure it for website hosting and set a RemovalPolicy
to ensure the bucket is deleted when the stack is destroyed (useful for development).
# Inside __init__
bucket = s3.Bucket(self, "WebsiteBucket",
website_index_document="index.html",
public_read_access=False, # We will use CloudFront to serve content
block_public_access=s3.BlockPublicAccess.BLOCK_ALL,
removal_policy=RemovalPolicy.DESTROY,
auto_delete_objects=True
)
Step 3: Create the CloudFront Distribution
Next, create the CloudFront distribution. We'll set up an Origin Access Identity (OAI) to allow CloudFront to securely access the private S3 bucket.
# Inside __init__
origin_access_identity = cloudfront.OriginAccessIdentity(self, "OAI")
bucket.grant_read(origin_access_identity)
distribution = cloudfront.Distribution(self, "Distribution",
default_root_object="index.html",
default_behavior=cloudfront.BehaviorOptions(
origin=origins.S3Origin(bucket, origin_access_identity=origin_access_identity),
viewer_protocol_policy=cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS
),
price_class=cloudfront.PriceClass.PRICE_CLASS_100 # Use a cost-effective price class
)
Step 4: Expose Outputs (Optional but Recommended)
To make your construct useful, you can expose important values like the bucket name or the distribution domain name as public properties.
# At the end of __init__
self.bucket = bucket
self.distribution_domain_name = distribution.distribution_domain_name
Step 5: Using Your New Construct
Now, you can use your StaticWebsite
construct in any of your CDK stacks just like you would use a standard L2 construct.
from aws_cdk import Stack
from constructs import Construct
from .static_website_construct import StaticWebsite # Import your construct
class MyWebsiteStack(Stack):
def __init__(self, scope: Construct, id: str, **kwargs):
super().__init__(scope, id, **kwargs)
website = StaticWebsite(self, "MyStaticWebsite")
# You can now access the outputs
# CfnOutput(self, "BucketName", value=website.bucket.bucket_name)
# CfnOutput(self, "CloudFrontURL", value=f"https://{website.distribution_domain_name}")
Conclusion
By encapsulating this pattern into a StaticWebsite
construct, you've created a reusable, best-practice component that can be deployed with a single line of code. This is the essence of building with L3 constructs.
As your infrastructure grows, you can apply this pattern to any part of your architecture—serverless APIs, ECS services, data processing pipelines, and more. Building a library of custom L3 constructs is one of the most effective ways to scale your Infrastructure as Code efforts and ensure consistency across your organization.