DEV Community

Cover image for Deploying a static website to AWS with an external domain using the CDK
Jake Langford for Twilsoft

Posted on

Deploying a static website to AWS with an external domain using the CDK

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

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

// 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,
});
Enter fullscreen mode Exit fullscreen mode

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.

// 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(),
});
Enter fullscreen mode Exit fullscreen mode

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 (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 to allow Cloudfront to access the bucket we made earlier. We'll use the Origin Access Identity Module and assign it to the bucket.

// 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
    )
  ],
}));
Enter fullscreen mode Exit fullscreen mode

Next up we're ready to configure the actual Cloudfront dsitribution. We'll need to tell it to use the certificate we made earlier, so we'll do that here too.

// 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,
      }],
    }
  ]
});
Enter fullscreen mode Exit fullscreen mode

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. This module takes your files and stores them in our bucket during CDK deployment.

// 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: ['/*'],
});
Enter fullscreen mode Exit fullscreen mode

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 and finding your distribution domain as seen below.
the Cloudfront console

Here's what that looks like with my registrar:
a registrar dns control panel

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

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: ["/*"],
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Discussion (0)