Building a Secure & Scalable S3 and CloudFront Architecture with AWS CDK
23 March 2025, 5 min read
In today's cloud-first world, delivering static assets (like images, scripts, and stylesheets) quickly and securely is paramount. AWS offers a powerful combination of Amazon S3 for storage and Amazon CloudFront for content delivery. In this post, we’ll walk through a TypeScript CDK stack that sets up an S3 bucket with robust security controls and a CloudFront distribution configured with modern best practices.
Overview
Our AWS CDK stack creates two primary resources:
- Amazon S3 Bucket: Serves as the origin for static assets. We enforce best practices by blocking public access and setting strict bucket policies.
- Amazon CloudFront Distribution: Provides global content delivery with low latency. CloudFront fetches objects from our S3 bucket using an origin access control, ensuring that content is delivered securely.
By using AWS CDK, the infrastructure is defined as code, enabling you to version, review, and deploy your cloud resources consistently.
The Code Walkthrough
Below is the complete code snippet of our stack:
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
import * as origins from "aws-cdk-lib/aws-cloudfront-origins";
export class S3CloudFrontStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const stack = this;
const env = props?.env;
const stage = stack.node.tryGetContext("stage");
const stageConfig = stack.node.tryGetContext(stage);
stageConfig["stage"] = stage;
const region = env?.region;
const s3CorsRule: s3.CorsRule = {
allowedMethods: [s3.HttpMethods.GET, s3.HttpMethods.HEAD],
allowedOrigins: ["*"],
allowedHeaders: ["*"],
maxAge: 300,
};
// Create an S3 bucket with all public access blocked
const s3Bucket = new s3.Bucket(stack, `S3Bucket-${stage}`, {
bucketName: `my-bucket-${stage}`,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
accessControl: s3.BucketAccessControl.PRIVATE,
cors: [s3CorsRule],
objectOwnership: s3.ObjectOwnership.BUCKET_OWNER_ENFORCED,
});
// Create Cloudfront distribution with S3 as Origin
const distribution = new cloudfront.Distribution(
stack,
`cf-Distribution-${stage}`,
{
defaultBehavior: {
origin: origins.S3BucketOrigin.withOriginAccessControl(s3Bucket, {
originPath: "/public",
}),
},
}
);
// Output the CloudFront distribution domain name so you can use it to serve your images
new cdk.CfnOutput(stack, "DistributionDomainName", {
value: distribution.distributionDomainName,
});
}
}
Let’s break down the essential parts.
Setting Up the S3 Bucket
Security First
The S3 bucket is created with a focus on security:
Public Access Blocked:
UsingblockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL
ensures that no public policies can be applied, reducing the risk of unintentional exposure.Private Access Control:
TheaccessControl
property is set toPRIVATE
, meaning only authenticated and authorized requests can access the bucket.Object Ownership:
By enforcings3.ObjectOwnership.BUCKET_OWNER_ENFORCED
, the bucket owner retains control over all objects, regardless of who uploads them. This simplifies permissions management.
CORS Configuration
The provided CORS rule allows only GET
and HEAD
methods, making it suitable for serving static assets while keeping the configuration as restrictive as possible. For development, broad rules (such as allowing all origins and headers) might be acceptable. However, in production, you may wish to restrict these settings further.
Configuring CloudFront Distribution
The CloudFront distribution is set up to use the S3 bucket as its origin:
Origin Access Control:
Instead of the legacy Origin Access Identity (OAI), the stack usesorigins.S3BucketOrigin.withOriginAccessControl
to establish secure communication between CloudFront and the S3 bucket. This modern approach simplifies the management of permissions.Origin Path:
By settingoriginPath: "/public"
, CloudFront is configured to only serve content from thepublic
folder in the S3 bucket. This is useful if your bucket contains both public and private assets, ensuring that only the intended files are accessible through the CDN.
Leveraging Context for Flexibility
The stack uses context variables such as stage
to dynamically configure the environment. This makes the stack reusable across different stages (e.g., development, staging, production) without hardcoding environment-specific values.
const stage = stack.node.tryGetContext("stage");
const stageConfig = stack.node.tryGetContext(stage);
stageConfig["stage"] = stage;
By injecting context values, you can adapt bucket names, distribution identifiers, and other configurations to meet your environment’s requirements.
Outputting Useful Information
The final part of the stack outputs the CloudFront distribution’s domain name. This output is essential because it allows you to easily integrate the distribution into your application or DNS configuration.
new cdk.CfnOutput(stack, "DistributionDomainName", {
value: distribution.distributionDomainName,
});
This makes it convenient to reference the endpoint for serving your content directly from CloudFront.
Considerations & Best Practices
Context Validation:
Ensure that thestage
context is provided when deploying the stack. Adding validation or default values can help prevent misconfigurations.Unused Variables:
In this example, theregion
variable is defined but not used. If not needed, consider removing such variables to keep your code clean.Origin Path Impact:
Remember that theoriginPath
parameter restricts the distribution to a specific folder (/public
in this case). Confirm that this aligns with your folder structure and deployment strategy.Tightening CORS:
Although the current CORS rule works for a variety of use cases, refining these settings for production can help further secure your static assets.
Conclusion
Integrating S3 with CloudFront using AWS CDK is a robust solution for hosting and delivering static assets securely. This stack not only demonstrates how to establish a secure connection between S3 and CloudFront using modern features like origin access control but also highlights best practices such as enforcing strict access policies and leveraging context for multi-environment deployments.
By following this approach, you can ensure that your content is delivered quickly to users worldwide while maintaining tight security over your data. Whether you’re building a personal project or scaling up a commercial application, this stack serves as a solid foundation for your cloud infrastructure.
Happy coding!