# Building A CDN With S3, Cloudfront And The AWS CDK

> Use the TypeScript AWS CDK to deploy an S3 Bucket fronted by a CloudFront CDN and Route53 record.

- Canonical: https://dennisokeeffe.com/blog/2021-08-08-building-a-cdn-with-s3-cloudfront-and-the-aws-cdk
- Published: 2021-08-08
- Updated: 2021-08-08
- Tags: aws, aws cdk, typescript, cdn, s3, cloudfront, route53

> This post is #3 of a multi-part series on the [AWS CDK with TypeScript](https://blog.dennisokeeffe.com/blog/series/working-with-the-type-script-aws-cdk) written during Melbourne lockdown #6.

This post will use the [AWS TypeScript CDK](https://docs.aws.amazon.com/cdk/latest/guide/work-with-cdk-typescript.html) 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"](https://blog.dennisokeeffe.com/blog/series/working-with-the-type-script-aws-cdk) series.

Source code can be found [here](https://github.com/okeeffed/building-a-cdn-with-s3-cloudfront-and-the-aws-cdk).

## Prerequisites

1. Basic familiarity with [AWS CDK for TypeScript](https://docs.aws.amazon.com/cdk/latest/guide/work-with-cdk-typescript.html).
2. Read the first two posts in the ["Working with the TypeScript AWS CDK"](https://blog.dennisokeeffe.com/blog/series/working-with-the-type-script-aws-cdk) series.
   - [AWS CDK With TypeScript Foundations](https://blog.dennisokeeffe.com/blog/2021-08-06-aws-cdk-with-typescript-foundations)
   - [Using The AWS CDK With LocalStack And `aws-cdk-local`](https://blog.dennisokeeffe.com/blog/2021-08-07-using-the-aws-cdk-with-localstack-and-aws-cdk-local)
3. If you want to create the Route53 record, you will need to get the ACM ARN for your certificate and have a hosted zone ID ready.

## Getting started

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

```s
$ 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:

```s
@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
```

We can install these prior to doing any work:

```s
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:

```s
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:

1. We want to create an S3 bucket to host the assets.
2. We want to ensure we get a reference to the ACM certificate from the ARN.
3. We want to instantiate a new CloudFront distruction with a custom domain and that certificate.
4. We need to grab the hosted zone from the attributes of the custom domain tha we created.
5. 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`.

```s
$ touch lib/cdn-stack.ts
```

Afterwards, we can add the following code:

```ts
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.

```ts
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

```ts
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

```ts
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

```ts
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

```ts
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:

```ts
#!/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:

```s
$ npm run cdk synth
npm run cdk synth

> aws-cdk-with-typescript-foundations@0.1.0 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
> aws-cdk-with-typescript-foundations@0.1.0 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]
```

Once successful, we are ready to test some assets.

## Manually testing the stack

We can use the [AWS CLI]() to test our CDN endpoint.

Add the following script to `package.json` under `scripts`:

```json
"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.

Create a new assets folder:

```s
$ mkdir assets
```

Then add any assets in that you want. I have added the icon for my website [workingoutloud.dev](https://www.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:

```s
$ 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](https://cdn.dennisokeeffe.com/assets/2021-08-08-building-a-cdn-with-s3-cloudfront-and-the-aws-cdk/1-cdn-example.png)

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

```ts
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:

```ts
$ 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.

```s
$ 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.

## Resources and further reading

1. [AWS CDK for TypeScript](https://docs.aws.amazon.com/cdk/latest/guide/work-with-cdk-typescript.html)
2. [workingoutloud.dev](https://www.workingoutloud.dev/)
3. ["Working with the TypeScript AWS CDK" series](https://blog.dennisokeeffe.com/blog/series/working-with-the-type-script-aws-cdk)
4. [AWS CDK With TypeScript Foundations](https://blog.dennisokeeffe.com/blog/2021-08-06-aws-cdk-with-typescript-foundations)
5. [Using The AWS CDK With LocalStack And `aws-cdk-local`](https://blog.dennisokeeffe.com/blog/2021-08-07-using-the-aws-cdk-with-localstack-and-aws-cdk-local)
6. [Final repository code](https://github.com/okeeffed/building-a-cdn-with-s3-cloudfront-and-the-aws-cdk)

_Photo credit: [`roadtripwithraj`](https://unsplash.com/@roadtripwithraj)_
