Building A CDN With S3, Cloudfront And The AWS CDK
Published: Aug 8, 2021
Last updated: Aug 8, 2021
This post is #3 of a multi-part series on the AWS CDK with TypeScript written during Melbourne lockdown #6.
This post will use the AWS TypeScript CDK to deploy an S3 bucket to hold media assets, a CloudFront distribution for a content delivery network for those assets and setup a Aaaa record for that CDN through Route53.
npm i @aws-cdk/core @aws-cdk/aws-cloudfront @aws-cdk/aws-cloudfront-origins @aws-cdk/aws-s3 @aws-cdk/aws-certificatemanager @aws-cdk/aws-route53 @aws-cdk/aws-route53-targets
The repository that we cloned already has a upgrade script supplied. We can use this to ensure our CDK packages are at parity:
npm run upgrade
We are now at a stage to write our CDN stack.
Plan for the CDK stack
There are a few steps we need to take to create our CDN distribution.
The order that we want to go when creating our stack:
We want to create an S3 bucket to host the assets.
We want to ensure we get a reference to the ACM certificate from the ARN.
We want to instantiate a new CloudFront distruction with a custom domain and that certificate.
We need to grab the hosted zone from the attributes of the custom domain tha we created.
We need to assign an ARecord and AaaaRecord for the zone targeting the CloudFront distribution.
With that outline, we are now ready to create our new stack.
Adding the CDN stack code
Create a new file at lib/cdn-stack.ts.
$ touch lib/cdn-stack.ts
Afterwards, we can add the following code:
import "dotenv/config";
import * as cdk from "@aws-cdk/core";
import * as cloudfront from "@aws-cdk/aws-cloudfront";
import * as origins from "@aws-cdk/aws-cloudfront-origins";
import * as s3 from "@aws-cdk/aws-s3";
import * as acm from "@aws-cdk/aws-certificatemanager";
import * as route53 from "@aws-cdk/aws-route53";
import * as targets from "@aws-cdk/aws-route53-targets";
export class CdnStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// ... ready to add our code here
}
}
We are now ready to work through each of the steps.
Create an S3 bucket
We will create a bucket demo-cdk-assets-bucket to host the assets with a DESTROY policy for when we tear down the assets.
import "dotenv/config";
import * as cdk from "@aws-cdk/core";
import * as cloudfront from "@aws-cdk/aws-cloudfront";
import * as origins from "@aws-cdk/aws-cloudfront-origins";
import * as s3 from "@aws-cdk/aws-s3";
import * as acm from "@aws-cdk/aws-certificatemanager";
import * as route53 from "@aws-cdk/aws-route53";
import * as targets from "@aws-cdk/aws-route53-targets";
export class CdnStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Create the S3 bucket to host assets
const assetsBucket = new s3.Bucket(this, "demo-cdk-assets-bucket", {
bucketName: "demo-cdk-assets-bucket",
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
}
}
Get reference to the ACM certificate
import "dotenv/config";
import * as cdk from "@aws-cdk/core";
import * as cloudfront from "@aws-cdk/aws-cloudfront";
import * as origins from "@aws-cdk/aws-cloudfront-origins";
import * as s3 from "@aws-cdk/aws-s3";
import * as acm from "@aws-cdk/aws-certificatemanager";
import * as route53 from "@aws-cdk/aws-route53";
import * as targets from "@aws-cdk/aws-route53-targets";
export class CdnStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const assetsBucket = new s3.Bucket(this, "demo-cdk-assets-bucket", {
bucketName: "demo-cdk-assets-bucket",
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
// Get the certificate
const certificate = acm.Certificate.fromCertificateArn(
this,
"Certificate",
// found using aws acm list-certificates --region us-east-1
"arn:aws:acm:your-zone:your-id"
);
}
}
Instantiate a new CloudFront distribution
import "dotenv/config";
import * as cdk from "@aws-cdk/core";
import * as cloudfront from "@aws-cdk/aws-cloudfront";
import * as origins from "@aws-cdk/aws-cloudfront-origins";
import * as s3 from "@aws-cdk/aws-s3";
import * as acm from "@aws-cdk/aws-certificatemanager";
import * as route53 from "@aws-cdk/aws-route53";
import * as targets from "@aws-cdk/aws-route53-targets";
export class CdnStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const assetsBucket = new s3.Bucket(this, "demo-cdk-assets-bucket", {
bucketName: "demo-cdk-assets-bucket",
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
const certificate = acm.Certificate.fromCertificateArn(
this,
"Certificate",
"arn:aws:acm:your-zone:your-id"
);
// Create new CloudFront Distribution
const cf = new cloudfront.Distribution(this, "cdnDistribution", {
defaultBehavior: { origin: new origins.S3Origin(assetsBucket) },
domainNames: ["example-cdn.your-domain.com"],
certificate,
});
}
}
Get the hosted zone
import "dotenv/config";
import * as cdk from "@aws-cdk/core";
import * as cloudfront from "@aws-cdk/aws-cloudfront";
import * as origins from "@aws-cdk/aws-cloudfront-origins";
import * as s3 from "@aws-cdk/aws-s3";
import * as acm from "@aws-cdk/aws-certificatemanager";
import * as route53 from "@aws-cdk/aws-route53";
import * as targets from "@aws-cdk/aws-route53-targets";
export class CdnStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const assetsBucket = new s3.Bucket(this, "demo-cdk-assets-bucket", {
bucketName: "demo-cdk-assets-bucket",
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
const certificate = acm.Certificate.fromCertificateArn(
this,
"Certificate",
"arn:aws:acm:your-zone:your-id"
);
const cf = new cloudfront.Distribution(this, "cdnDistribution", {
defaultBehavior: { origin: new origins.S3Origin(assetsBucket) },
domainNames: ["example-cdn.your-domain.com"],
certificate,
});
// Get the zone
const zone = route53.HostedZone.fromHostedZoneAttributes(
this,
"dennisokeeffe-zone",
{
zoneName: "example-cdn.your-domain.com",
hostedZoneId: "your-zone-id",
}
);
}
}
Assign ARecord and AaaaRecord to CloudFront distribution
import "dotenv/config";
import * as cdk from "@aws-cdk/core";
import * as cloudfront from "@aws-cdk/aws-cloudfront";
import * as origins from "@aws-cdk/aws-cloudfront-origins";
import * as s3 from "@aws-cdk/aws-s3";
import * as acm from "@aws-cdk/aws-certificatemanager";
import * as route53 from "@aws-cdk/aws-route53";
import * as targets from "@aws-cdk/aws-route53-targets";
export class CdnStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const assetsBucket = new s3.Bucket(this, "demo-cdk-assets-bucket", {
bucketName: "demo-cdk-assets-bucket",
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
const certificate = acm.Certificate.fromCertificateArn(
this,
"Certificate",
"arn:aws:acm:your-zone:your-id"
);
const cf = new cloudfront.Distribution(this, "cdnDistribution", {
defaultBehavior: { origin: new origins.S3Origin(assetsBucket) },
domainNames: ["example-cdn.your-domain.com"],
certificate,
});
const zone = route53.HostedZone.fromHostedZoneAttributes(
this,
"dennisokeeffe-zone",
{
zoneName: "example-cdn.your-domain.com",
hostedZoneId: "your-zone-id",
}
);
// Adding out A Record code
new route53.ARecord(this, "CDNARecord", {
zone,
target: route53.RecordTarget.fromAlias(new targets.CloudFrontTarget(cf)),
});
new route53.AaaaRecord(this, "AliasRecord", {
zone,
target: route53.RecordTarget.fromAlias(new targets.CloudFrontTarget(cf)),
});
}
}
Adding the stack to the CDK app
We are now ready to add the stack to our app!
Inside of bin/aws-cdk-with-typescript-foundations.ts, adjust the code to be the following:
#!/usr/bin/env node
import "source-map-support/register";
import * as cdk from "@aws-cdk/core";
import { CdnStack } from "../lib/cdn-stack";
const app = new cdk.App();
new CdnStack(app, "CdnStack", {
/* If you don't specify 'env', this stack will be environment-agnostic.
* Account/Region-dependent features and context lookups will not work,
* but a single synthesized template can be deployed anywhere. */
/* Uncomment the next line to specialize this stack for the AWS Account
* and Region that are implied by the current CLI configuration. */
// env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
/* Uncomment the next line if you know exactly what Account and Region you
* want to deploy the stack to. */
// env: { account: '123456789012', region: 'us-east-1' },
/* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */
});
Synthesizing and deploying the stack
We can run through our usual lifecycle to syntheize and deploy the stack:
This script will sync assets in our assets folder to the root of our S3 bucket.
Create a new assets folder:
$ mkdir assets
Then add any assets in that you want. I have added the icon for my website workingoutloud.dev for the demonstration.
Once that is done, run the command npm run s3 and let the assets sync. If successful, you should get something similar to the following:
$ npm run s3
> aws-cdk-with-typescript-foundations@0.1.0 s3
> aws s3 sync ./assets s3://demo-cdk-assets-bucket
upload: assets/wol-dev-300.png to s3://demo-cdk-assets-bucket/wol-dev-300.png
If successful, you can now go to https://example-cdn.your-domain.com/wol-dev-300.png and see your resulting image:
Example CDN success with networking information
Writing a test for our stack
We can also add a test for our stack under test/cdk-stack.test.ts. This section is more for completion sake, but please note that I have adjusted some of the test to take environment variables for the sake of redacting critical information (albeit it is not that critical):
Running npm t test/cdn-stack.test.ts or npm run test test/cdn-stack.test.ts would display our test succeeding:
$ npm t test/cdn-stack.test.ts
> aws-cdk-with-typescript-foundations@0.1.0 test
> jest "test/cdn-stack.test.ts"
PASS test/cdn-stack.test.ts
✓ CDN Stack (144 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.78 s, estimated 4 s
Ran all test suites matching /test\/cdn-stack.test.ts/i.
Teardown
Always be sure to tear down your resources when you are done with them. We purposely made the bucket destroyable for this reason in this demo.
Note: your bucket will need to be empty to be destroyed. Ensure you login and empty the bucket first.
$ npm run cdk destroy
> aws-cdk-with-typescript-foundations@0.1.0 cdk
> cdk "destroy"
Are you sure you want to delete: CdnStack (y/n)? y
CdnStack: destroying...
✅ CdnStack: destroyed
Summary
Today's post demonstrated how to create a simple CDN with the AWS TypeScript CDK.
To improve upon from here, you could look to add in ways to resize assets on the fly or build a pipeline to process images and place them into the S3 bucket.