Amazon Web Services Cloud Development Kit (AWS CDK) is an abstraction layer working to generate Cloud Formation templates. It is a programmatic interface that can be used in tandem with command line tools to compile (JavaScript, TypeScript, Python, and others) into cloud formation templates.
This is incredibly powerful. You can code a Front End application (React, Vue, Angular) and also your ENTIRE AWS stack within the same language, same repository, creating and deploy it using the same cicd tools. This reduces code silos, as a Front End developer you no longer have a mysterious AWS environment, and as the person or team managing the Front End Stack you can standardize and implement your organization’s best practices. This is a big shift left for developers so let’s go through an example.
Static Website – S3 + CloudFront + Route 53
One common pattern we at H3 put in place is the hosting of a static website in an S3 bucket, content served through Cloud Front (CF), and DNS managed by Route 53. The diagram below shows a rough architectural diagram.

Following an http request’s path, it first hits the DNS provider, Route 53 in our case. Traffic is directed from Route 53 to the CloudFront distribution via an A record within a hosted zone (e.g., would be directed to 5kj22l3ksjas.cloudfront.net/index.html
- CloudFront has a caching layer. So sometimes the S3 object wont be retrieved.
- If the S3 bucket is a static website, then pages like would result in a 404 error. We implement a simple Lambda Edge functions to check if request paths without the extension are objects in our S3 bucket (i.e., is the object in out S3 bucket). If so then we return that object rather than the file
blog
Each of these constructs, Route 53, CloudFront, S3, Lambda can be defined within the CDK. We will be writing our example using TypeScript. We will also assume you have some basic knowledge about the CDK, its setup and how to use it so we can get right into creating our stack library. If you don’t have that background knowledge see the AWS docs.
import * as cdk from '@aws-cdk/core';
import * as s3 from '@aws-cdk/aws-s3';
import * as cloudfront from '@aws-cdk/aws-cloudfront';
import * as acm from '@aws-cdk/aws-certificatemanager';
import * as lambda from '@aws-cdk/aws-lambda';
import * as path from 'path';
const H3_CERTIFICATE_ARN = 'arn-for-your-certification'
export class StaticSite extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
/**
* Force stack to be created in us-east-1 (lambda and cf restriction)
*/
super(scope, id, {
...props,
env: {
...props?.env,
region: 'us-east-1'
}
});
// ... stack definition
}
}Above is the basic stack definition. We will be using the S3, CloudFront, ACM, and Lambda constructs. Since we have already created a wildcard certification we don’t have to create one for this stack. It seems like the best way to handle certification may be to create them beforehand. Since you may not have Route 53 handle your DNS records and creating a certificate takes some time. If you are not using AWS to manage your DNS you will have to certify your domain with AWS Certification Manager. Then after the initial deployment add an A record to route traffic from your domain to the CloudFront instance.
First let’s just create an S3 bucket with default and index.html
import * as cdk from '@aws-cdk/core';
import * as s3 from '@aws-cdk/aws-s3';
import * as cloudfront from '@aws-cdk/aws-cloudfront';
import * as acm from '@aws-cdk/aws-certificatemanager';
import * as lambda from '@aws-cdk/aws-lambda';
import * as path from 'path';
const H3_CERTIFICATE_ARN = 'arn-for-your-certification'
export class StaticSite extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
/**
* Force stack to be created in us-east-1 (lambda and cf restriction)
*/
super(scope, id, {
...props,
env: {
...props?.env,
region: 'us-east-1'
}
});
const URL = 'cdktest.example.com'
const bucket = new s3.Bucket(this, URL, {
websiteErrorDocument: '404.html',
websiteIndexDocument: 'index.html',
removalPolicy: cdk.RemovalPolicy.DESTROY,
publicReadAccess: true,
cors: [{
allowedOrigins: ['*'],
allowedMethods: [
s3.HttpMethods.GET,
s3.HttpMethods.POST,
s3.HttpMethods.PUT,
s3.HttpMethods.DELETE,
s3.HttpMethods.HEAD
]
}],
})
}
}We can deploy this stack by running the command or npx cdk deploy
import * as cdk from '@aws-cdk/core';
import * as s3 from '@aws-cdk/aws-s3';
import * as cloudfront from '@aws-cdk/aws-cloudfront';
import * as acm from '@aws-cdk/aws-certificatemanager';
import * as lambda from '@aws-cdk/aws-lambda';
import * as path from 'path';
const H3_CERTIFICATE_ARN = 'arn-for-your-certification'
export class StaticSite extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
/**
* Force stack to be created in us-east-1 (lambda and cf restriction)
*/
super(scope, id, {
...props,
env: {
...props?.env,
region: 'us-east-1'
}
});
const URL = 'cdktest.example.com'
const bucket = new s3.Bucket(this, URL, {
websiteErrorDocument: '404.html',
websiteIndexDocument: 'index.html',
removalPolicy: cdk.RemovalPolicy.DESTROY,
publicReadAccess: true,
cors: [{
allowedOrigins: ['*'],
allowedMethods: [
s3.HttpMethods.GET,
s3.HttpMethods.POST,
s3.HttpMethods.PUT,
s3.HttpMethods.DELETE,
s3.HttpMethods.HEAD
]
}],
})
const cf = new cloudfront.CloudFrontWebDistribution(this, `cf-${URL}`, {
defaultRootObject: 'index.html',
originConfigs: [{
s3OriginSource: { s3BucketSource: bucket },
behaviors: [{
cachedMethods: cloudfront.CloudFrontAllowedCachedMethods.GET_HEAD_OPTIONS,
isDefaultBehavior: true,
allowedMethods: cloudfront.CloudFrontAllowedMethods.GET_HEAD_OPTIONS,
}],
}]
});
}
}This is a very simple CloudFront distribution. Let’s fill it out with some custom Behaviors for error pages (line 58-ish), and setup the certification (line 47 and line 91-ish).
import * as cdk from '@aws-cdk/core';
import * as s3 from '@aws-cdk/aws-s3';
import * as cloudfront from '@aws-cdk/aws-cloudfront';
import * as acm from '@aws-cdk/aws-certificatemanager';
import * as lambda from '@aws-cdk/aws-lambda';
import * as path from 'path';
const H3_CERTIFICATE_ARN = 'arn-for-your-certification'
export class StaticSite extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
/**
* Force stack to be created in us-east-1 (lambda and cf restriction)
*/
super(scope, id, {
...props,
env: {
...props?.env,
region: 'us-east-1'
}
});
const URL = 'cdktest.highwaythreesolutions.com'
const bucket = new s3.Bucket(this, URL, {
websiteErrorDocument: '404.html',
websiteIndexDocument: 'index.html',
removalPolicy: cdk.RemovalPolicy.DESTROY,
publicReadAccess: true,
cors: [{
allowedOrigins: ['*'],
allowedMethods: [
s3.HttpMethods.GET,
s3.HttpMethods.POST,
s3.HttpMethods.PUT,
s3.HttpMethods.DELETE,
s3.HttpMethods.HEAD
]
}],
})
/**
* Load Site Certification object for association with Cloud Front instance
*/
const siteCertificate = acm.Certificate.fromCertificateArn(this, 'site-cert', H3_CERTIFICATE_ARN)
/**
* Define Cloud Front Instance
*/
const cf = new cloudfront.CloudFrontWebDistribution(this, `cf-${URL}`, {
defaultRootObject: 'index.html',
errorConfigurations: [{
errorCode: 404,
errorCachingMinTtl: 10,
responseCode: 404,
responsePagePath: '/404.html'
},
{
errorCode: 400,
errorCachingMinTtl: 10,
responseCode: 400,
responsePagePath: '/404.html'
},
{
errorCode: 403,
errorCachingMinTtl: 10,
responseCode: 403,
responsePagePath: '/404.html'
},
{
errorCode: 405,
errorCachingMinTtl: 10,
responseCode: 405,
responsePagePath: '/404.html'
},
{
errorCode: 500,
errorCachingMinTtl: 10,
responseCode: 500,
responsePagePath: '/404.html'
}],
originConfigs: [{
s3OriginSource: { s3BucketSource: bucket },
behaviors: [{
cachedMethods: cloudfront.CloudFrontAllowedCachedMethods.GET_HEAD_OPTIONS,
isDefaultBehavior: true,
allowedMethods: cloudfront.CloudFrontAllowedMethods.GET_HEAD_OPTIONS,
}],
}],
viewerCertificate: cloudfront.ViewerCertificate.fromAcmCertificate(
siteCertificate, {
aliases: [URL],
securityPolicy: cloudfront.SecurityPolicyProtocol.TLS_V1,
sslMethod: cloudfront.SSLMethod.SNI
}
),
});
}
}Okay great! Now if this stack is deployed we’ll have a secured HTTP site, custom error messages, that serves a static site. In 100-ish lines of code! Now for some optional fun stuff. Since this is a static site, we should handle the request like a webserver would, directing traffic from to /page.html
The Lambda function is really simple, it just checks if the request URI for the absence of a .html
'use strict';
exports.handler = async (event) => {
const request = event.Records[0].cf.request;
if (/^(/+([dws+_-!@=#$%&*]+))+$/g.test(request.uri)) {
request.uri = request.uri + '.html';
return request;
}
return request;
};and the Stack definition will now look like the following (changes on line 52-56 and line 99-102)
import * as cdk from '@aws-cdk/core';
import * as s3 from '@aws-cdk/aws-s3';
import * as cloudfront from '@aws-cdk/aws-cloudfront';
import * as acm from '@aws-cdk/aws-certificatemanager';
import * as lambda from '@aws-cdk/aws-lambda';
import * as path from 'path';
const H3_CERTIFICATE_ARN = 'arn-for-your-certification';
export class StaticSite extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
/**
* Force stack to be created in us-east-1 (lambda and cf restriction)
*/
super(scope, id, {
...props,
env: {
...props?.env,
region: 'us-east-1'
}
});
const URL = 'cdktest.highwaythreesolutions.com';
const bucket = new s3.Bucket(this, URL, {
websiteErrorDocument: '404.html',
websiteIndexDocument: 'index.html',
removalPolicy: cdk.RemovalPolicy.DESTROY,
publicReadAccess: true,
cors: [{
allowedOrigins: ['*'],
allowedMethods: [
s3.HttpMethods.GET,
s3.HttpMethods.POST,
s3.HttpMethods.PUT,
s3.HttpMethods.DELETE,
s3.HttpMethods.HEAD
]
}],
});
/**
* Load Site Certification object for association with Cloud Front instance
*/
const siteCertificate = acm.Certificate.fromCertificateArn(this, 'site-cert', H3_CERTIFICATE_ARN);
const nonHtmlRequestFunction = new lambda.Function( this, 'LambdaEdgeRedirect', {
runtime: lambda.Runtime.NODEJS_12_X,
handler: 'index.handler',
code: lambda.Code.fromAsset(path.join(__dirname, 'lambdas', 'static-web-hosting'))
});
/**
* Define Cloud Front Instance
*/
const cf = new cloudfront.CloudFrontWebDistribution(this, `cf-${URL}`, {
defaultRootObject: 'index.html',
errorConfigurations: [{
errorCode: 404,
errorCachingMinTtl: 10,
responseCode: 400,
responsePagePath: '/404.html'
},
{
errorCode: 400,
errorCachingMinTtl: 10,
responseCode: 400,
responsePagePath: '/404.html'
},
{
errorCode: 403,
errorCachingMinTtl: 10,
responseCode: 400,
responsePagePath: '/404.html'
},
{
errorCode: 405,
errorCachingMinTtl: 10,
responseCode: 400,
responsePagePath: '/404.html'
},
{
errorCode: 500,
errorCachingMinTtl: 10,
responseCode: 400,
responsePagePath: '/404.html'
}],
originConfigs: [{
s3OriginSource: { s3BucketSource: bucket },
behaviors: [{
cachedMethods: cloudfront.CloudFrontAllowedCachedMethods.GET_HEAD_OPTIONS,
isDefaultBehavior: true,
allowedMethods: cloudfront.CloudFrontAllowedMethods.GET_HEAD_OPTIONS,
lambdaFunctionAssociations:[{
eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST,
lambdaFunction: nonHtmlRequestFunction.currentVersion
}]
}],
}],
viewerCertificate: cloudfront.ViewerCertificate.fromAcmCertificate(siteCertificate, {
aliases: [URL],
securityPolicy: cloudfront.SecurityPolicyProtocol.TLS_V1,
sslMethod: cloudfront.SSLMethod.SNI
}),
});
}
}Adding A Record to Route 53
If your DNS provider is AWS (Route 53), then we can create the A record with the following lines of code:
const zone = r53.HostedZone.fromLookup(this, 'H3-SiteZones', {
domainName: DOMAIN_NAME
});
new r53.ARecord(this, 'DistributionRecord', {
recordName: URL,
zone: zone,
target: r53.RecordTarget.fromAlias({
bind: () => ({
hostedZoneId: r53T.CloudFrontTarget.getHostedZoneId(cf),
dnsName: cf.distributionDomainName
})
})
});where is defined from the import import * as r53 from '@aws-cdk/aws-route53'
Deployment options
By adding the following code snippet you can deploy a folder as your static site.
new s3deploy.BucketDeployment(this, 'DeployWebsite', {
sources: [s3deploy.Source.asset(path.join(__dirname, '..', 'website-dist'))],
destinationBucket: bucket,
retainOnDelete: false
});The example stack has been updated with some quality of life/reusability improvements to in the Github Repo. I encourage you to check out the updates.