aws cdk s3


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.

The code will build off the work done in the first two articles of the “Working with the TypeScript AWS CDK” series.

Prerequisites

Getting started

We will clone the working repo from the previous blog post as a starting point:

                  

$ git clone https://github.com/okeeffed/using-the-aws-cdk-with-localstack-and-aws-cdk-local building-a-cdn-with-s3-cloudfront-and-the-aws-cdk $ cd building-a-cdn-with-s3-cloudfront-and-the-aws-cdk $ npm i

At this stage, we will have the app in a basic working state.

In order to create the CDN stack, we will require the following AWS CDK libraries:

                  

@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

                  

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:

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:

With that outline, we are now ready to create our new stack.

Adding the CDN stack 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:

                  

$ npm run cdk synth npm run cdk synth > [email protected] cdk > cdk "synth" Resources: democdkassetsbucketA19A67A4: Type: AWS::S3::Bucket Properties: BucketName: demo-cdk-assets-bucket UpdateReplacePolicy: Delete DeletionPolicy: Delete Metadata: aws:cdk:path: CdnStack/demo-cdk-assets-bucket/Resource democdkassetsbucketPolicy674BC594: Type: AWS::S3::BucketPolicy Properties: Bucket: Ref: democdkassetsbucketA19A67A4 PolicyDocument: Statement: - Action: s3:GetObject Effect: Allow Principal: CanonicalUser: Fn::GetAtt: - cdnDistributionOrigin1S3Origin52472F64 - S3CanonicalUserId Resource: Fn::Join: - "" - - Fn::GetAtt: - democdkassetsbucketA19A67A4 - Arn - /* Version: "2012-10-17" Metadata: aws:cdk:path: CdnStack/demo-cdk-assets-bucket/Policy/Resource cdnDistributionOrigin1S3Origin52472F64: Type: AWS::CloudFront::CloudFrontOriginAccessIdentity Properties: CloudFrontOriginAccessIdentityConfig: Comment: Identity for CdnStackcdnDistributionOrigin123067AEF Metadata: aws:cdk:path: CdnStack/cdnDistribution/Origin1/S3Origin/Resource cdnDistribution5DCBB4A4: Type: AWS::CloudFront::Distribution Properties: DistributionConfig: Aliases: - example-cdn.your-domain.com DefaultCacheBehavior: CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6 Compress: true TargetOriginId: CdnStackcdnDistributionOrigin123067AEF ViewerProtocolPolicy: allow-all Enabled: true HttpVersion: http2 IPV6Enabled: true Origins: - DomainName: Fn::GetAtt: - democdkassetsbucketA19A67A4 - RegionalDomainName Id: CdnStackcdnDistributionOrigin123067AEF S3OriginConfig: OriginAccessIdentity: Fn::Join: - "" - - origin-access-identity/cloudfront/ - Ref: cdnDistributionOrigin1S3Origin52472F64 ViewerCertificate: AcmCertificateArn: arn:aws:acm:[redacted] MinimumProtocolVersion: TLSv1.2_2019 SslSupportMethod: sni-only Metadata: aws:cdk:path: CdnStack/cdnDistribution/Resource CDNARecord9F02271B: Type: AWS::Route53::RecordSet Properties: Name: example-cdn.your-domain.com. Type: A AliasTarget: DNSName: Fn::GetAtt: - cdnDistribution5DCBB4A4 - DomainName HostedZoneId: Fn::FindInMap: - AWSCloudFrontPartitionHostedZoneIdMap - Ref: AWS::Partition - zoneId HostedZoneId: [redacted] Metadata: aws:cdk:path: CdnStack/CDNARecord/Resource AliasRecord851000D2: Type: AWS::Route53::RecordSet Properties: Name: example-cdn.your-domain.com. Type: AAAA AliasTarget: DNSName: Fn::GetAtt: - cdnDistribution5DCBB4A4 - DomainName HostedZoneId: Fn::FindInMap: - AWSCloudFrontPartitionHostedZoneIdMap - Ref: AWS::Partition - zoneId HostedZoneId: [redacted] Metadata: aws:cdk:path: CdnStack/AliasRecord/Resource CDKMetadata: Type: AWS::CDK::Metadata Properties: Analytics: v2:deflate64:H4sIAAAAAAAAE3VQy24CMQz8Fu4hsKoQPXZZVKmHqtXyBaljkFkao8QpqqL8OwmpVC6cPB57xo9Od91aL2cv5hLmYKdFAvao004MTGpgF8RHEDVi4OgB1bB37+Z8Jneo8J4uzZaE2GVVzVJ40mkTYUKp1T/UwiefCH7/6ZZnBSeOdu/ZiU5bKqPpK1ZH9eHpQK4HwBDeLDohucmHKnitgkcd9zZZeY6Cq7JYP2I51LYbKtqV5XpjTMtyzsqxRX0Mi59upZ/Li46BaO5jcf5GPbZ4Bbh/xoU/AQAA Metadata: aws:cdk:path: CdnStack/CDKMetadata/Default Condition: CDKMetadataAvailable Mappings: AWSCloudFrontPartitionHostedZoneIdMap: aws: zoneId: [redacted] aws-cn: zoneId: [redacted] Conditions: CDKMetadataAvailable: Fn::Or: - Fn::Or: - Fn::Equals: - Ref: AWS::Region - af-south-1 - Fn::Equals: - Ref: AWS::Region - ap-east-1 - Fn::Equals: - Ref: AWS::Region - ap-northeast-1 - Fn::Equals: - Ref: AWS::Region - ap-northeast-2 - Fn::Equals: - Ref: AWS::Region - ap-south-1 - Fn::Equals: - Ref: AWS::Region - ap-southeast-1 - Fn::Equals: - Ref: AWS::Region - ap-southeast-2 - Fn::Equals: - Ref: AWS::Region - ca-central-1 - Fn::Equals: - Ref: AWS::Region - cn-north-1 - Fn::Equals: - Ref: AWS::Region - cn-northwest-1 - Fn::Or: - Fn::Equals: - Ref: AWS::Region - eu-central-1 - Fn::Equals: - Ref: AWS::Region - eu-north-1 - Fn::Equals: - Ref: AWS::Region - eu-south-1 - Fn::Equals: - Ref: AWS::Region - eu-west-1 - Fn::Equals: - Ref: AWS::Region - eu-west-2 - Fn::Equals: - Ref: AWS::Region - eu-west-3 - Fn::Equals: - Ref: AWS::Region - me-south-1 - Fn::Equals: - Ref: AWS::Region - sa-east-1 - Fn::Equals: - Ref: AWS::Region - us-east-1 - Fn::Equals: - Ref: AWS::Region - us-east-2 - Fn::Or: - Fn::Equals: - Ref: AWS::Region - us-west-1 - Fn::Equals: - Ref: AWS::Region - us-west-2 $ npm run cdk deploy > [email protected] cdk > cdk "deploy" This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening). Please confirm you intend to make the following modifications: IAM Statement Changes ┌───┬────────────────────────────┬────────┬──────────────┬────────────────────────────┬───────────┐ │ │ Resource │ Effect │ Action │ Principal │ Condition │ ├───┼────────────────────────────┼────────┼──────────────┼────────────────────────────┼───────────┤ │ + │ ${demo-cdk-assets-bucket.A │ Allow │ s3:GetObject │ CanonicalUser:${cdnDistrib │ │ │ │ rn}/* │ │ │ ution/Origin1/S3Origin.S3C │ │ │ │ │ │ │ anonicalUserId} │ │ └───┴────────────────────────────┴────────┴──────────────┴────────────────────────────┴───────────┘ (NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299) Do you wish to deploy these changes (y/n)? y CdnStack: deploying... CdnStack: creating CloudFormation changeset... ✅ CdnStack Stack ARN: arn:aws:cloudformation:us-east-1:[REDACTED]

Manually testing the stack

We can use the AWS CLI to test our CDN endpoint.

                  

"scripts": { "s3": "aws s3 sync ./assets s3://demo-cdk-assets-bucket" }

This script will sync assets in our assets folder to the root of our S3 bucket.

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 > [email protected] 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:

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):

                  

import { expect as expectCDK, matchTemplate, MatchStyle, } from "@aws-cdk/assert"; import * as cdk from "@aws-cdk/core"; import * as CdnStack from "../lib/cdn-stack"; test("CDN Stack", () => { const app = new cdk.App(); // WHEN const stack = new CdnStack.CdnStack(app, "MyTestStack"); // THEN expectCDK(stack).to( matchTemplate( { Resources: { democdkassetsbucketA19A67A4: { Type: "AWS::S3::Bucket", Properties: { BucketName: "demo-cdk-assets-bucket", }, UpdateReplacePolicy: "Delete", DeletionPolicy: "Delete", }, democdkassetsbucketPolicy674BC594: { Type: "AWS::S3::BucketPolicy", Properties: { Bucket: { Ref: "democdkassetsbucketA19A67A4", }, PolicyDocument: { Statement: [ { Action: "s3:GetObject", Effect: "Allow", Principal: { CanonicalUser: { "Fn::GetAtt": [ "cdnDistributionOrigin1S3Origin52472F64", "S3CanonicalUserId", ], }, }, Resource: { "Fn::Join": [ "", [ { "Fn::GetAtt": [ "democdkassetsbucketA19A67A4", "Arn", ], }, "/*", ], ], }, }, ], Version: "2012-10-17", }, }, }, cdnDistributionOrigin1S3Origin52472F64: { Type: "AWS::CloudFront::CloudFrontOriginAccessIdentity", Properties: { CloudFrontOriginAccessIdentityConfig: { Comment: "Identity for MyTestStackcdnDistributionOrigin1E3DABCBC", }, }, }, cdnDistribution5DCBB4A4: { Type: "AWS::CloudFront::Distribution", Properties: { DistributionConfig: { Aliases: [process.env.ZONE_NAME!], DefaultCacheBehavior: { CachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6", Compress: true, TargetOriginId: "MyTestStackcdnDistributionOrigin1E3DABCBC", ViewerProtocolPolicy: "allow-all", }, Enabled: true, HttpVersion: "http2", IPV6Enabled: true, Origins: [ { DomainName: { "Fn::GetAtt": [ "democdkassetsbucketA19A67A4", "RegionalDomainName", ], }, Id: "MyTestStackcdnDistributionOrigin1E3DABCBC", S3OriginConfig: { OriginAccessIdentity: { "Fn::Join": [ "", [ "origin-access-identity/cloudfront/", { Ref: "cdnDistributionOrigin1S3Origin52472F64", }, ], ], }, }, }, ], ViewerCertificate: { AcmCertificateArn: process.env.ACM_ARN!, MinimumProtocolVersion: "TLSv1.2_2019", SslSupportMethod: "sni-only", }, }, }, }, CDNARecord9F02271B: { Type: "AWS::Route53::RecordSet", Properties: { Name: `${process.env.ZONE_NAME!}.`, Type: "A", AliasTarget: { DNSName: { "Fn::GetAtt": ["cdnDistribution5DCBB4A4", "DomainName"], }, HostedZoneId: { "Fn::FindInMap": [ "AWSCloudFrontPartitionHostedZoneIdMap", { Ref: "AWS::Partition", }, "zoneId", ], }, }, HostedZoneId: "Z1Z1MST5IFCUVI", }, }, AliasRecord851000D2: { Type: "AWS::Route53::RecordSet", Properties: { Name: `${process.env.ZONE_NAME!}.`, Type: "AAAA", AliasTarget: { DNSName: { "Fn::GetAtt": ["cdnDistribution5DCBB4A4", "DomainName"], }, HostedZoneId: { "Fn::FindInMap": [ "AWSCloudFrontPartitionHostedZoneIdMap", { Ref: "AWS::Partition", }, "zoneId", ], }, }, HostedZoneId: process.env.HOSTED_ZONE_ID!, }, }, }, Mappings: { AWSCloudFrontPartitionHostedZoneIdMap: { aws: { zoneId: "Z2FDTNDATAQYW2", }, "aws-cn": { zoneId: "Z3RFFRIM2A3IF5", }, }, }, }, MatchStyle.EXACT ) ); });

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 > [email protected] 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 > [email protected] 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.

Resources and further reading

Dennis O’Keeffe

Get fresh posts + news direct to your inbox.