Skip to main content

Domain Routing

Now we'll route the domain name to the website via AWS's CDN.

So far, the code in aws-infra/lib/aws-infra-stack.ts is pretty generic and could easily be adapted to setup other websites. Let's try to keep it flexible by not hard-coding the details of our domain and certificate.

First, let's enhance the props interface so this information can be provided when configuring the environment:

aws-infra/lib/aws-infra-stack.ts
import * as cdk from "aws-cdk-lib";
// other imports ...

interface SiteStackProps extends cdk.StackProps {
domainName: string;
certificateArn: string;
}

export class AwsInfraStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: SiteStackProps) {
super(scope, id, props);

const { domainName, certificateArn } = props;
const websiteDir = `${__dirname}/../..`;

// ... remaining code is the same as before ...

then pass them in over in aws-infra/bin/aws-infra.ts (filling in the correct values for your website):

aws-infra/bin/aws-infra.ts
#!/usr/bin/env node
import "dotenv/config";
import "source-map-support/register";
import * as cdk from "aws-cdk-lib";
import { AwsInfraStack } from "../lib/aws-infra-stack";

const app = new cdk.App();
new AwsInfraStack(app, "AwsInfraStack", {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
},
domainName: "YOUR_DOMAIN", // example: "siteonaws.click"
certificateArn: "arn:aws:acm:us-east-1:YOUR_ARN",
});

You can find the certificate ARN in the AWS console by clicking into the certificate details under "List certificates" in the AWS Certificate Manager, near where you requested the certificate.

tip

domainName and certificateArn could be configured as environment variables or CDK context values in more complex setups.

Now that CDK has the domainName and certificateArn available, we can finish setting up the domain.

We'll route requests for the site's domain name to the CDN by creating a DNS "A" record using Route 53 (AWS's DNS system). We also need to configure the CDN to serve traffic for this domain, which requires you to configure the CDN with the domain's certificate (this is probably to prevent domains that you don't control from routing to your site).

The CDN is also configured to automatically route all http traffic to https. Everyone gets a secure connection when browsing the site. 😎

Here's the final version of the CDK stack that serves the site over HTTPS with a CDN using a purchased domain name (new code is highlighted):

aws-infra/lib/aws-infra-stack.ts
import * as cdk from "aws-cdk-lib";
import type { Construct } from "constructs";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as s3deploy from "aws-cdk-lib/aws-s3-deployment";
import { execSync } from "node:child_process";
import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
import * as origins from "aws-cdk-lib/aws-cloudfront-origins";
import * as certificateManager from "aws-cdk-lib/aws-certificatemanager";
import * as route53 from "aws-cdk-lib/aws-route53";
import * as targets from "aws-cdk-lib/aws-route53-targets";

interface SiteStackProps extends cdk.StackProps {
domainName: string;
certificateArn: string;
}

export class AwsInfraStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: SiteStackProps) {
super(scope, id, props);

const { domainName, certificateArn } = props;
const websiteDir = `${__dirname}/../..`;

console.log("Building the website for deployment...");
execSync("npm run build", { cwd: websiteDir, stdio: "inherit" });

// Find the existing hosted zone (the DNS provider)
const zone = route53.HostedZone.fromLookup(this, "HostedZone", {
domainName: `${domainName}.`, // see warning below
});

// Find the existing certificate
const certificate = certificateManager.Certificate.fromCertificateArn(
this,
"Certificate",
certificateArn
);

const websiteBucket = new s3.Bucket(this, "Bucket", {
websiteIndexDocument: "index.html",
websiteErrorDocument: "404.html",
publicReadAccess: true,
blockPublicAccess: {
blockPublicPolicy: false,
blockPublicAcls: false,
ignorePublicAcls: false,
restrictPublicBuckets: false,
},
});

const cdn = new cloudfront.Distribution(this, "CDN", {
defaultBehavior: {
origin: new origins.S3StaticWebsiteOrigin(websiteBucket),
// Redirect http to https:
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
},
// Configure the CDN for the domain + certificate:
domainNames: [domainName],
certificate,
});

// Route the site's domain name to the CDN
new route53.ARecord(this, "ARecord", {
zone,
recordName: domainName,
target: route53.RecordTarget.fromAlias(new targets.CloudFrontTarget(cdn)),
ttl: cdk.Duration.minutes(10),
});

new s3deploy.BucketDeployment(this, "Deploy", {
sources: [s3deploy.Source.asset(`${websiteDir}/build`)],
destinationBucket: websiteBucket,
distribution: cdn, // invalidate CDN cache
});

new cdk.CfnOutput(this, "S3BucketUrl", {
value: websiteBucket.bucketWebsiteUrl,
});

new cdk.CfnOutput(this, "CdnUrl", {
value: `https://${cdn.domainName}`,
});

new cdk.CfnOutput(this, "WebsiteUrl", {
value: `https://${domainName}`,
});
}
}

That should do it! Deploying this step was pretty slow again (around 10 minutes), but I saw in the AWS console under CloudFront that my certificate was set up with the CDN after just a few minutes, and I was able to start browsing the site at https://siteonaws.click while it was still completing the deploy.

At this point you should have everything you need to run a static website on AWS. You can rerun cdk deploy whenever you make changes to the website contents.

warning

According to docs and examples, this code should work:

route53.HostedZone.fromLookup(this, "HostedZone", { domainName });

But it doesn't work for me. I debugged with the AWS CLI by running:

aws route53 list-hosted-zones

which showed:

{
"HostedZones": [
{
"Id": "/hostedzone/Z0123456790GXZ",
"Name": "siteonaws.click.",
...
}
]
}

See the extra "." at the end of the domain name? That is apparently common. Domains I've purchased through AWS are all like this. `${domainName}.` appends a "." to match the hosted zone's actual name:

route53.HostedZone.fromLookup(this, "HostedZone", {
domainName: `${domainName}.`,
});

Maybe you will have the opposite problem and need to remove the trailing ".". If you get an error about "hosted zone not found", try using just { domainName } for the third arg.