Deploying a static website to AWS with an external domain using the CDK
Tags: cdk, aws, webdev, typescript
Published: 2021-12-08 16:44:49

---
title: Deploying a static website to AWS with an external domain using the CDK
published: true
description: A concise guide on how to use CDK to deploy a static website to S3, serve it over https with Cloudfront and use a custom external domain.
tags: CDK, AWS, webdev, typescript
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tfcxk6wtqvxj6ajjl1j7.png
---

## The why
AWS makes it really convenient when it comes to building scalable, easily deployable, applications. Especially when it's supported by strong documentation. I find the problems start to arise when you leave the amazon ecosystem. The documentation falls apart.

I experienced that this week when I wanted to deploy a static website to S3 but use a domain that I registered elsewhere. There are countless tutorials and docs on how to do it if your domain is registered with amazon, but I couldn't find much for my scenario. Hopefully I can fix that.

## Tutorial
### Getting Started
***TLDR?*** *Scroll down to [the end of the article](#complete-code) to see the finished code example*

For this tutorial, I'll assume you are familiar with the CDK and Typescript, although I'm sure the concepts apply to other tools. For the sake of this tutorial, I'll be building a single CDK stack with everything we need.

It's important to know that your stack MUST be in `us-east-1` as this is where Cloudfront looks for certificates.

Here's what we'll start with:
*Be sure to check the comments in the code examples*
```ts
import { Stack, StackProps, Aws } from 'aws-cdk-lib';

const domainName = 'example.com';

export class TutorialStack extends Stack {
  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);
        // Our stack contents will go here
    }
}
```

### Configuring S3
The first thing we need to add to our stack is somewhere to put your website. For that we'll need to create an [s3 bucket](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.Bucket.html).

```ts
// top of file:
import { Bucket, BlockPublicAccess } from 'aws-cdk-lib/aws-s3';

// in the stack:
const bucket = new Bucket(this, 'SiteBucket', {
  bucketName: domainName, // bucket name MUST be the domain name
  websiteIndexDocument: 'index.html', // your sites main page
  websiteErrorDocument: 'index.html', // for simplicity
  publicReadAccess: false, // we'll use Cloudfront to access
  blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
});
```

### Serving it up
#### Certificate
To serve our website securely, we'll need to request a certificate from amazon for our domain using [the certificate module](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-certificatemanager-readme.html).

```ts
// top of file:
import { Certificate, CertificateValidation } from 'aws-cdk-lib/aws-certificatemanager';

// in the stack:
const cert = new Certificate(this, 'Certificate', {
  domainName: domainName,
  validation: CertificateValidation.fromDns(),
});
```
**Note**: *The first time you deploy your stack, it will appear to hang on creation of the certificate because you need to [prove you own the domain](https://docs.aws.amazon.com/acm/latest/userguide/dns-validation.html#dns-validation-console) (Choose option B). Don't worry if the deployment fails. Just rerun it when you're ready.*

#### Cloudfront
So we've got our website in the bucket and we have the certificate to securely serve it up. Let's get it up and running.

Firstly, we need to create a [special user](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html) to allow Cloudfront to access the bucket we made earlier. We'll use the [Origin Access Identity Module](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-cloudfront.OriginAccessIdentity.html) and assign it to the bucket.
```ts
// top of file: (we'll use a lot of these imports later)
import { CloudFrontAllowedMethods, CloudFrontWebDistribution,
OriginAccessIdentity, SecurityPolicyProtocol,
SSLMethod, ViewerCertificate } from 'aws-cdk-lib/aws-cloudfront';
import { CanonicalUserPrincipal, PolicyStatement }
from 'aws-cdk-lib/aws-iam';

// in the stack:
const cloudfrontOAI = new OriginAccessIdentity(this,
  'CloudfrontOAI',
  {comment: `Cloudfront OAI for ${domainName}`},
);

bucket.addToResourcePolicy(new PolicyStatement({
  actions: ['s3:GetObject'],
  resources: [bucket.arnForObjects('*')],
  principals: [
    new CanonicalUserPrincipal(
      cloudfrontOAI
        .cloudFrontOriginAccessIdentityS3CanonicalUserId
    )
  ],
}));
```

Next up we're ready to configure the actual [Cloudfront dsitribution](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-cloudfront.CloudFrontWebDistribution.html). We'll need to tell it to [use the certificate](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-cloudfront.ViewerCertificate.html) we made earlier, so we'll do that here too.
```ts
// top of file:
import { Metric } from 'aws-cdk-lib/aws-cloudwatch';
// we imported the rest of the things we need in the last step

// in the stack:
const viewerCert = ViewerCertificate.fromAcmCertificate({
  certificateArn: cert.certificateArn,
  env: {
    region: Aws.REGION,
    account: Aws.ACCOUNT_ID,
  },
  node: this.node,
  stack: this,
  metricDaysToExpiry: () => new Metric({
    namespace: 'TLS viewer certificate validity',
    metricName: 'TLS Viewer Certificate expired',
  })
},
{
  sslMethod: SSLMethod.SNI,
  securityPolicy: SecurityPolicyProtocol.TLS_V1_1_2016,
  aliases: [domainName],
});

const distribution = new CloudFrontWebDistribution(this,
  'SiteDistribution', {
  viewerCertificate: viewerCert,
  originConfigs: [
    {
      s3OriginSource: {
        s3BucketSource: bucket,
        originAccessIdentity: cloudfrontOAI
      },
      behaviors: [{
        isDefaultBehavior: true,
        compress: true,
        allowedMethods:
          CloudFrontAllowedMethods.GET_HEAD_OPTIONS,
      }],
    }
  ]
});
```
### Deploying your site
So now we have somewhere to put our website and a way to serve it up, we need to actually put it there. We'll use the [Bucket Deployment module](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3-deployment.BucketDeployment.html). This module takes your files and stores them in our bucket during CDK deployment.

```ts
// top of file:
import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment';

// in the stack:
new BucketDeployment(this, 'Website Deployment', {
// Here goes the path to your website files.
// The path is relative to the root folder of your CDK app.
  sources: [Source.asset('../ui/build')],
  destinationBucket: bucket,
  distribution,
  distributionPaths: ['/*'],
});
```

### Domain
So that's it for the AWS side of things. Go ahead and deploy. But we haven't pointed our domain name to our Cloudfront distribution. Unfortunately, this will be different depending on your registrar, but I'll help as best I can.

In essence, you'll need to create a CNAME record and point your domain at the cloudfront url. You can find that by going to the [Cloudfront control panel](https://console.aws.amazon.com/cloudfront/v3/home?region=eu-west-1#/distributions) and finding your distribution domain as seen below.
![the Cloudfront console](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7y05kwp3kmu11dsrmrv7.png)

Here's what that looks like with my registrar:
![a registrar dns control panel](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zqliuvz7drxxm97m9kx3.png)

### Conclusion
That should be your lot. If you've followed the steps here, you should be able to securely visit your website with your custom domain name.

Please let me know if I've made any mistakes or if I can improve this guide in any way.

So to recap, We:
- Created a bucket to store the site in.
- Created a certificate to serve the site over https.
  - We proved to AWS we own the domain.
- Created a special user to access the site.
- Created a viewer certificate so Cloudfront can use the certificate we made before.
- Created our Cloudfront distribution.
- Deployed our site to s3.
- Made a CNAME record with our registrar and pointed it at out Cloudfront domain.

### Complete Code
```ts
import { Stack, StackProps, Aws } from "aws-cdk-lib";
import { Construct } from "constructs";
import { Bucket, BlockPublicAccess } from "aws-cdk-lib/aws-s3";
import { BucketDeployment, Source } from "aws-cdk-lib/aws-s3-deployment";
import {
  Certificate,
  CertificateValidation,
} from "aws-cdk-lib/aws-certificatemanager";
import {
  CloudFrontAllowedMethods,
  CloudFrontWebDistribution,
  OriginAccessIdentity,
  SecurityPolicyProtocol,
  SSLMethod,
  ViewerCertificate,
} from "aws-cdk-lib/aws-cloudfront";
import { CanonicalUserPrincipal, PolicyStatement } from "aws-cdk-lib/aws-iam";
import { Metric } from "aws-cdk-lib/aws-cloudwatch";

const domainName = "example.com";

export class TutorialStack extends Stack {
  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);

    const bucket = new Bucket(this, "SiteBucket", {
      bucketName: domainName,
      websiteIndexDocument: "index.html",
      websiteErrorDocument: "index.html",
      publicReadAccess: false,
      blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
    });

    const cert = new Certificate(this, "Certificate", {
      domainName: domainName,
      validation: CertificateValidation.fromDns(),
    });

    const cloudfrontOAI = new OriginAccessIdentity(this, "CloudfrontOAI", {
      comment: `Cloudfront OAI for ${domainName}`,
    });

    bucket.addToResourcePolicy(
      new PolicyStatement({
        actions: ["s3:GetObject"],
        resources: [bucket.arnForObjects("*")],
        principals: [
          new CanonicalUserPrincipal(
            cloudfrontOAI.cloudFrontOriginAccessIdentityS3CanonicalUserId
          ),
        ],
      })
    );

    const viewerCert = ViewerCertificate.fromAcmCertificate(
      {
        certificateArn: cert.certificateArn,
        env: {
          region: Aws.REGION,
          account: Aws.ACCOUNT_ID,
        },
        node: this.node,
        stack: this,
        metricDaysToExpiry: () =>
          new Metric({
            namespace: "TLS viewer certificate validity",
            metricName: "TLS Viewer Certificate expired",
          }),
      },
      {
        sslMethod: SSLMethod.SNI,
        securityPolicy: SecurityPolicyProtocol.TLS_V1_1_2016,
        aliases: [domainName],
      }
    );

    const distribution = new CloudFrontWebDistribution(
      this,
      "SiteDistribution",
      {
        viewerCertificate: viewerCert,
        originConfigs: [
          {
            s3OriginSource: {
              s3BucketSource: bucket,
              originAccessIdentity: cloudfrontOAI,
            },
            behaviors: [
              {
                isDefaultBehavior: true,
                compress: true,
                allowedMethods: CloudFrontAllowedMethods.GET_HEAD_OPTIONS,
              },
            ],
          },
        ],
      }
    );

    new BucketDeployment(this, "DeploySite", {
      sources: [Source.asset("../ui/build")],
      destinationBucket: bucket,
      distribution,
      distributionPaths: ["/*"],
    });
  }
}
```