Deploy a static site to AWS S3 and CloudFront using AWS CDK
Tags: aws, typescript, webdev, tutorial
Published: 2023-01-26 09:44:44

Amazon Web Services (AWS) is the most comprehensive and widely adopted cloud platform in the world. Using the AWS Cloud Development Kit (CDK), we can easily define our cloud infrastructure as code (using TypeScript) and host our site on S3, serving it with Amazon's high-performance CloudFront CDN.

**Full Source Code available on GitHub**

{% embed https://github.com/htmnk/aws-cdk-static-site-starter %}

## Prerequisites

- ☑&nbsp;Node.js (LTS recommended), Git and TypeScript (globally via npm) installed
- ☑&nbsp;Have an AWS account and access to the [AWS Management Console](https://aws.amazon.com/console/)

## Todo

- ☐&nbsp;[**Create a demo static site**](#create-a-demo-static-site)
    
build a site to a ready-to-deploy folder, I will be using [Hugo](https://gohugo.io/) today, but any framework of your choice will suffice
- ☐&nbsp;[**Setup a new AWS CDK project**](#setup-a-new-aws-cdk-project)

   install everything [AWS CDK](https://docs.aws.amazon.com/cdk/v2/guide/home.html) related, configure AWS credentials and set up a new CDK TypeScript project
- ☐&nbsp;[**Define AWS resources for the deployment**](#define-aws-resources-for-the-deployment)

  deep dive into the CDK app, take a look at [stacks](https://docs.aws.amazon.com/cdk/v2/guide/stacks.html) and define all the necessary [CDK constructs](https://docs.aws.amazon.com/cdk/v2/guide/constructs.html) for a static site deploy (`s3.bucket`, `cloudfront.Distribution`, etc.)
- ☐&nbsp;[**Deploy the site**](#deploy-the-site)

  run our first deployment with the [CDK Toolkit commands](https://docs.aws.amazon.com/cdk/v2/guide/cli.html), view the deployed stack and resources inside the AWS Management Console
- ☐&nbsp;[**Security and Domain Name**](#security-and-domain-name)

  scan our site with [Mozilla Observatory](https://observatory.mozilla.org/) and improve our grade by registering a domain name, enabling HTTPS, adding a certificate and setting security headers

## Create a demo static site

> :warning: If you already have a static site ready, you can skip this todo and proceed to [**Setup a new AWS CDK project**](#setup-a-new-aws-cdk-project).

For the purposes of this tutorial, we will not be focusing on fine-tuning the site contents. Today, I will be deploying a simple static blog and have chosen to use [Hugo](https://gohugo.io/) as my framework (other popular alternatives for this purpose include [11ty](https://www.11ty.dev/), [Astro](https://astro.build/), and [Jekyll](https://jekyllrb.com/)). I don't need any complex user interactions, so a static site generator is sufficient for my needs.
If you plan to use Hugo as well, make sure it is [installed](https://gohugo.io/installation) on your machine. Then, open up a terminal (on Windows, it's recommended to use an Unix command line terminal or Powershell; [Git Bash](https://www.atlassian.com/git/tutorials/git-bash) is usually the easiest option),

and run

```sh
hugo version
# hugo v0.103.0+extended darwin/arm64 BuildDate=unknown
```
to verify your installation. You should get a similar output to the one above.

Now that the hard part is done, we can generate our site by running

```sh
hugo new site my-blog
```

Running the command above should generate a new `my-blog` folder within the current directory. Let's move inside that folder and pull in a theme from GitHub (you can see a full list of available themes [here](https://themes.gohugo.io/)). I'll be using the [Paper](https://themes.gohugo.io/themes/hugo-paper/) theme today

```sh
cd my-blog
git init
git submodule add https://github.com/nanxiaobei/hugo-paper themes/paper
```

You should see a new subfolder under `themes`, in my case it is called `themes/paper`. Since we are using Git submodules to manage themes, you should also see a new `.gitmodules` file being generated in the root directory.

While we're at it, let's also add a `.gitignore` file for Hugo. Create a new file

```sh
touch .gitignore
```

Similar to other frameworks, we do not want to include any build files or automatically generated files in our repository. In Hugo, the build folder is called `/public` by default, so let's add that to our `.gitignore` file to exclude it from version control

```sh
# Generated files by hugo
/public/
# Temporary lock file while building
/.hugo_build.lock
```

Now that we've taken care of the Git-related stuff, let's apply the theme to our project. To do this, we just need to add a new line to the configuration file. Open up `config.toml` and add the following line

```toml
theme = "paper"
```
[View on GitHub](https://github.com/htmnk/aws-cdk-static-site-starter/blob/main/config.toml#L4)

To add some content, use the `new` command to create a new Markdown file inside the `content` folder

```sh
hugo new posts/my-first-post.md
```

Hugo has excellent Markdown support and will automatically convert the contents of the file into a new page at `/posts/my-first-post/`. Let's verify that by starting up Hugo's development server:

```sh
hugo server -D
# ...
# Web Server is available at http://localhost:1313/
# ...
```

When you start the server, you should see some basic information output to the terminal. Look for the `localhost` URL. If you are not running anything on Hugo's default [port](https://gohugo.io/commands/hugo_server/#options) (`1313`), the site should now be available at that URL. Open it up in a browser. With the Paper theme, the posts should also be visible on the home page by default. Visit `/posts/my-first-post` and edit the `/content/posts/my-first-post.md` file, then save your changes. Hugo should rebuild the site without stopping the server, and you should see your changes reflected on the site.

All right we are ready to ship this thing :rocket:, run
```sh
hugo -D
```

By default, when you use `hugo new` to create a new post, Hugo treats it as a draft and sets the `draft: true` flag. To include content marked as a draft in the build, we need to specify the `-D` option. All of the build files will be located in the `/public` directory, and they should be **ready to deploy**.

**Done**.

- ☑&nbsp;Create a demo static site

## Setup a new AWS CDK project

To simplify interactions with AWS services, consider installing the [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html). This will provide you with a set of command-line tools that allow you to easily manage your AWS resources from the terminal.

> Access keys consist of an access key ID and secret access key, which are used to sign programmatic requests that you make to AWS. If you don't have access keys, you can create them from the AWS Management Console.

Before we can use the AWS CDK, we need to set up AWS credentials for our account. There are several ways to do this, but for our purposes, we will use the _"Creating a key pair"_ method as described in the AWS documentation. Here's how to do it, sign in to the AWS Management Console and

1. Open the [IAM console](https://console.aws.amazon.com/iam).
2. In the navigation pane, choose _**Users**_ and click the _**Add Users**_ button.
3. Type in a _**User name**_ for the key pair, I'm going with "my-blog-cdk". Check _**Access key - Programmatic access**_ as the _**AWS credential type**_ and hit _**Next: Permissions**_.
4. Now, you could dive deeper with permissions in this step if you have to take security a bit more serious, I'm going to keep it simple, select _**Attach existing policies directly**_ and check _**AdministratorAccess**_. Click _**Next: Tags**_.
5. We can skip _**Tags**_ and click _**Next: Review**_.
6. Finally click _**Create user**_.
7. Now you can either download the ._**csv**_ file and use that for the configuration or simply copy-paste the _**access key ID**_ and _**secret access key**_ pair. :exclamation: Be careful with exposing the keys if you followed my steps and chose _**AdministratorAccess**_ for permissions, since anyone could access your AWS resources with the key pair.

Now that we have our AWS credentials, we can configure them for use with the AWS CDK. The easiest way to do this is to run the AWS CLI [configure](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html) command. On MacOS, the configuration file will be stored at `~/.aws/credentials`, so you may need to use sudo to run this command (for more information on configuration and credential file settings, see this [page](<(https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html)>)). Alternatively, you can create the `.aws` files manually if you are not using the AWS CLI

```sh
sudo aws configure
# AWS Access Key ID [None]: AKIAIOSFODNN7EXAMPLE
# AWS Secret Access Key [None]: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
# Default region name [None]: us-east-1
# Default output format [None]: json
```

When prompted, enter your access key ID and secret access key from the key pair that you created earlier. For the region name, enter `us-east-1`, and for the output format, enter `json`. If you run `aws configure` again or `cat ~/.aws/credentials`, you should see your configured credentials.

Alright we got the keys :key:, let's move on and install the [AWS CDK Toolkit](https://docs.aws.amazon.com/cdk/v2/guide/work-with.html) (globally) with `npm`

```sh
npm install -g aws-cdk
```

If you were following the [previous todo](#create-a-demo-static-site), our project folder structure currently looks something like this

```sh
my-blog
│   ...
├── public # Hugo build folder
│   ...
└── .gitignore
```

The name of the build folder (in this case, `public`) is not particularly important, but it's important that we specify the correct path to the folder when we configure our deployment later. Let's create a new `cdk` folder at the same level as the `public` folder and initialize a new empty CDK project using the [cdk init](https://docs.aws.amazon.com/cdk/v2/guide/cli.html#cli-init) command

```sh
mkdir cdk
cd cdk
sudo cdk init app --language typescript
```

In addition to other generated files and folders, we should focus on the `cdk.json` file first. This is a [configuration file](https://docs.aws.amazon.com/cdk/v2/guide/cli.html#cli-config) for the CDK, and it tells the CDK Toolkit how to execute our app. It contains information such as the programming language being used, the app's entry point, and other runtime settings.

> If one of `cdk.json` or `~/.cdk.json` exists, options specified there will be used as defaults. Settings in `cdk.json` take precedence.

When we choose `TypeScript` as the programming language with `cdk init`, it sets the `app` option to `"app": "npx ts-node --prefer-ts-exts bin/cdk.ts",`. This specifies the command that will be used to execute the CDK application.

The command specified in the `app` option uses [ts-node](https://github.com/TypeStrong/ts-node) by default, which is an execution engine for Node.js that allows you to run TypeScript code directly. The `--prefer-ts-exts` flag prevents `ts-node` from prioritizing precompiled `.js` files and will always import the TypeScript source code instead, if it is available. This is useful if you are also using `tsc` (the TypeScript compiler) alongside the `app` option. The `bin/cdk.ts` file is the entry point for our CDK app, which defines the main function that will be executed when the app is run.

To make it easier to use certain values throughout our CDK app, let's define some environment variables. Create a new `.env` file inside the `cdk` folder and add the following lines (we will use these later)

```sh
CDK_REGION=us-east-1
# Add your AWS Account ID here,
# can be grabbed from AWS Management Console when,
# clicking on your profile in the top right dropdown
CDK_ACCOUNT=2383838383
# Optional, if you are planning to purchase or have a domain
# ready to use
DOMAIN_NAME=e53nec.com
```

Even though our `.env` file does not contain any sensitive information, it is a good practice to exclude it from version control. To do this, open the `cdk/.gitignore` file and add the following line

```sh
# env files
.env*
```

To make it easier to access the environment variables defined in the `.env` file, we will use the [dotenv](https://github.com/motdotla/dotenv) module. This module loads environment variables from a `.env` file into our CDK app. To install dotenv, run the following command

```sh
npm install dotenv --save
```

To use dotenv in our CDK app, we need to import and configure it in the entry point file (`bin/cdk.ts`). At the top of this file, add the following lines

```ts
import * as dotenv from 'dotenv'
dotenv.config()
...
import * as cdk from 'aws-cdk-lib'
```
[View on GitHub](https://github.com/htmnk/aws-cdk-static-site-starter/blob/main/cdk/bin/cdk.ts#L3)

This will import the dotenv module and use the `config` function to load the environment variables from the `.env` file. We will be able to access these variables throughout our CDK app using `process.env.VARIABLE_NAME`.

To verify that the environment variables are being loaded correctly, we can log them to the console and execute the app with `ts-node`. Inside `bin/cdk.ts`, add the following line below the dotenv configuration

```ts
console.log(process.env.CDK_REGION) // Remove if it's working
...
const app = new cdk.App() // Initialize a CDK application
```

and run

```sh
npx ts-node bin/cdk.ts
```

You should see `us-east-1` printed to the console. Now that we have set up our CDK project and configured the environment variables, we are ready to start building our CDK app by writing CDK constructs.

**Done**.

- ☑&nbsp;Setup a new AWS CDK project

## Define AWS resources for the deployment

At the top level of every CDK App we define one or multiple [stacks](https://docs.aws.amazon.com/cdk/v2/guide/stacks.html). Stacks are units of deployments, they are used for organizing and grouping the AWS resources together to a single deployment. In a TypeScript CDK app, any instance of the `cdk.Stack` class represents a stack. You can see an example of this with the `CdkStack` class in the `bin/cdk.ts` file

```ts
...
import { CdkStack } from '../lib/cdk-stack';
...
new CdkStack(app, 'CdkStack', {
  // ...
})
```

The `CdkStack` class extends the root `cdk.Stack` class, which is imported from the `aws-cdk-lib` module in the `lib/cdk-stack.ts` file. This allows us to define custom behavior for our stack in addition to the functionality provided by the base `cdk.Stack` class

```ts
export class CdkStack extends cdk.Stack
```

Inside stacks, we define [constructs](https://docs.aws.amazon.com/cdk/v2/guide/constructs.html), which are the basic building blocks of a CDK app. Constructs represent a _cloud component_ (I'm going to use the terms _construct_ and _components_ interchangeably), such as an [S3 Bucket](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3.Bucket.html) or a [CloudFront Distribution](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudfront.Distribution.html). These components are turned into AWS resources after the CDK app is deployed.

For those who prefer visual representations, the composition of an app with stacks and constructs can be summarized with the following diagram from the AWS docs

![AWS CDK structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ug7dcjkufif95y410wne.png)

We are now going to add some constructs to our stacks. The following structure is optional, but we will define a `BaseStack` which will contain all the components that can be reused across multiple site deployments and a `BlogStack` for components specific to our blog. This separation of reusable and specific components allows for greater flexibility and modularity in our app in case we plan on having multiple deployments in the future.

Let's rename the file `lib/cdk-stack.ts` to `lib/base-stack.ts` and the exported `CdkStack` class to `BaseStack`. We will remove all unnecessary comments and add our first construct to the `BaseStack`, which will be the storage for the site: an [S3 Bucket](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3.Bucket.html) :bucket:

```ts
import * as cdk from 'aws-cdk-lib'
import * as s3 from 'aws-cdk-lib/aws-s3'
import { Construct } from 'constructs'

export class BaseStack extends cdk.Stack {
  private _bucket: s3.Bucket

  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props)
    this._bucket = new s3.Bucket(this, 'BaseBucket')
  }
}
```
[View on GitHub](https://github.com/htmnk/aws-cdk-static-site-starter/blob/main/cdk/lib/base-stack.ts#L35)

> Constructs are implemented in classes that extend the [Construct](https://docs.aws.amazon.com/cdk/api/v2/docs/constructs.Construct.html) base class. You define a construct by instantiating the class.

We have instantiated the `s3.Bucket` class, thus defining a new `Bucket` construct. Inside our `BaseStack`, we will also define private properties for the constructs. These will be exposed and passed down to the child `BlogStack` later. All constructs take three parameters when they are initialized: the `scope`, the `id`, and the `props`

```ts
new s3.Bucket(this, 'BaseBucket', {})
```

- **scope** - The first parameter is the `scope`, which is the construct's parent or owner. This can be either a stack or another construct (in our case, the `BaseStack` will be the owner of the `Bucket` construct). In JavaScript (TypeScript), we use the `this` keyword to represent the current object for the scope.
- **id** - The second parameter is the `id`, which is a unique identifier within the current scope.
- **props** - The third parameter is the `props`, which is a set of properties that define the construct's configuration. In most cases, constructs provide sensible defaults, and if all `props` elements are optional, you can omit the `props` parameter completely (which is what we did with the bucket above).

The first two parameters will be repetitive across the whole CDK app. To keep the construct ids unique, I will use this simple `StackNameConstructName` pattern.

When deploying a static site to the S3 bucket, we could either

1. [Enable static website hosting](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3.Bucket.html#websiteindexdocument) by adding the `websiteIndexDocument` property to our Bucket construct. This would require changing the defaults and some additional configuration when serving the site with CloudFront.
> If the bucket is configured for website hosting, the CloudFront origin will be configured to use the bucket as an HTTP server origin and will use the bucket's configured website redirects and error handling. Otherwise, the origin is created as a bucket origin and will use CloudFront's redirect and error handling.

2. Keep the `s3.bucket` as a bucket origin for CloudFront. :white_check_mark:

We are going to stick with the defaults and continue with the **2nd** (bucket origin) option. We could pretty much achieve the same final result with the first one (see [using a website endpoint as the origin](https://aws.amazon.com/premiumsupport/knowledge-center/cloudfront-serve-static-website/)). There are also some caveats to both options, we will go through the latter ones.

The [default props](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3.Bucket.html) for the Bucket should then just work fine. The only interesting ones to us are the `blockPublicAccess` and the `removalPolicy` properties. If you are only playing around and plan on destroying the bucket later make sure to modify the `removalPolicy` and the `autoDeleteObjects` properties based on your needs

```ts
// This will also destroy the bucket when running cdk destroy later
this._bucket = new s3.Bucket(this, 'BaseBucket', {
  removalPolicy: RemovalPolicy.DESTROY,
  autoDeleteObjects: true,
})
```

To allow users to access the site's content through CloudFront, we can add another construct called an `OriginAccessIdentity`. This is a special CloudFront user that can be associated with an S3 bucket origin. Since the default for `blockPublicAccess` is `s3.BlockPublicAccess.BLOCK_ALL`, this is necessary.

```ts
import { aws_cloudfront as cf } from 'aws-cdk-lib'
...
export class BaseStack extends cdk.Stack {
  ...
  private _originAccessIdentity: cf.OriginAccessIdentity
  ...
  this._originAccessIdentity = new cf.OriginAccessIdentity(this, 'BaseCfOriginAccessIdentity')
}

```
[View on GitHub](https://github.com/htmnk/aws-cdk-static-site-starter/blob/main/cdk/lib/base-stack.ts#L36)

Next, we need to grant our Origin Access Identity (OAI) bucket read permissions. We can do this by adding the user to the bucket resource policy with the [addToResourcePolicy()](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3.Bucket.html#addwbrtowbrresourcewbrpolicypermission) method, passing in a new [IAM statement](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_iam.PolicyStatement.html) and attaching our OAI to the policy statement principals.

```ts
import * as iam from 'aws-cdk-lib/aws-iam'
...
this._bucket.addToResourcePolicy(
  new iam.PolicyStatement({
    actions: ['s3:GetObject'],
    resources: [this._bucket.arnForObjects('*')],
    principals: [
      new iam.CanonicalUserPrincipal(
        this._originAccessIdentity.cloudFrontOriginAccessIdentityS3CanonicalUserId
      ),
    ],
  })
)
```
[View on GitHub](https://github.com/htmnk/aws-cdk-static-site-starter/blob/main/cdk/lib/base-stack.ts#L37)

That should be all the permission stuff done. Now we have only have one little problem left that we can solve within our `BaseStack`.

If you have not been following from the beginning, [I used Hugo](#create-a-demo-static-site) to generate the site. I have this build folder structure (which is common when using static site generators) for my pages

```sh
public
│   ...
├── posts
│   └── my-first-post
│       └── index.html
│   ...
└── index.html
```

Since we chose the bucket origin option, we must request the file at `/posts/my-first-post/index.html` directly, and our page routing will not work (this should be handled automatically if the static website hosting option is enabled). However, if we want to access the posts using the `/posts/my-first-post/` URL, we can use a [CloudFront Function](https://aws.amazon.com/blogs/aws/introducing-cloudfront-functions-run-your-code-at-the-edge-with-low-latency-at-any-scale/) to make this happen.

To begin, create a new folder called `cdk/functions` and add a new file to it called `cdk/functions/reqRewrite.js`. By default, a new CDK project includes a `*.js` line in the `.gitignore` file, which ignores all JavaScript files. In order to ensure that our function code is tracked and committed to the repository, we can add a new rule `!/functions/*`, which grabs back all of the `.js` files inside the `functions` directory.

In this file, we will rewrite all page requests by checking if the request does not contain a dot (e.g. `.css`, `.js`). This will help us determine if we are requesting a file or not. If the request does not contain a dot, it means that we are not requesting a file, and we can proceed with the rewrite and append `.index.html` to the requested URI

```js
function rewritePageUri(uri) {
  var isDir = uri.endsWith('/')
  // Handle both "/posts/my-post/" and "/posts/my-post"
  return uri + (isDir ? '' : '/') + 'index.html'
}

function handler(event) {
  var request = event.request

  // Requesting a page ("/" or "/posts/my-post")
  if (!request.uri.includes('.')) {
    // Append index.html to the requested uri
    request.uri = rewritePageUri(request.uri)
  }

  return request
}
```
[View on GitHub](https://github.com/htmnk/aws-cdk-static-site-starter/blob/main/cdk/functions/reqRewrite.js#L1)

All the page requests will now serve the respective `.index.html` files.

> :warning: The code provided above is designed to work with a simple build folder structure, such as `my-page/index.html` or similar. If you have a different build folder structure, you will need to modify the `rewritePageUri()` function so that it appends the correct `.html` file to the requested URI. This will ensure that the correct page is displayed when a user navigates to your site.

Let's go ahead and attach our function to a CloudFront function associations list (we will drop this list into our CloudFront construct later). To do this, create a new `_functionAssociations` property and add a new construct as follows

```ts
...
export class BaseStack extends cdk.Stack {
  private _functionAssociations: cf.FunctionAssociation[]
  ...
  this._functionAssociations = [
    {
      eventType: cf.FunctionEventType.VIEWER_REQUEST,
      function: new cf.Function(this, 'BaseRequestRewriteFunction', {
        code: cf.FunctionCode.fromFile({ filePath: 'functions/reqRewrite.js' }),
      }),
    },
  ]
}
```
[View on GitHub](https://github.com/htmnk/aws-cdk-static-site-starter/blob/main/cdk/lib/base-stack.ts#L48)

It is important to note that we need to create a `VIEWER_REQUEST` type function in order to modify the request before it reaches the origin(see [Introducing CloudFront Functions](https://aws.amazon.com/blogs/aws/introducing-cloudfront-functions-run-your-code-at-the-edge-with-low-latency-at-any-scale/)). To create a `VIEWER_REQUEST` type function, we can use the [Function](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudfront.Function.html) construct, which allows us to specify the source code of the function from a file (such as the `reqRewrite.js` file we created earlier). We can also pass the code in as an inline string if we prefer.

In order to make our base stack resources accessible to other CDK stacks, let's create and export a `BaseStackInterface`

```ts
...
export interface BaseStackResources {
  bucket: s3.Bucket
  originAccessIdentity: cf.OriginAccessIdentity
  functionAssociations: cf.FunctionAssociation[]
}

export class BaseStack extends cdk.Stack {
...
```
[View on GitHub](https://github.com/htmnk/aws-cdk-static-site-starter/blob/main/cdk/lib/base-stack.ts#L8)

and add a public `resources()` method to the `BaseStack`

```ts
...
private _functionAssociations: cf.FunctionAssociation[]

resources(): BaseStackResources {
  return {
    bucket: this._bucket,
    originAccessIdentity: this._originAccessIdentity,
    functionAssociations: this._functionAssociations,
  }
}

constructor(scope: Construct, id: string, props?: cdk.StackProps) {
...
```
[View on GitHub](https://github.com/htmnk/aws-cdk-static-site-starter/blob/main/cdk/lib/base-stack.ts#L23)

By doing this, we can ensure that our child stacks are able to leverage the resources created in the `BaseStack`, making our cdk project more modular and reusable.

Great, it looks like we have all the base resources we need. Now, let's create our stack. To do this, open the `bin/cdk.ts` file and instantiate the `BaseStack` class as follows (make sure to use unique stack IDs if you are planning on deploying multiple CDK applications to the same AWS account)

```ts
import { BaseStack } from '../lib/base-stack'
...
const app = new cdk.App()
const baseStack = new BaseStack(app, 'AwsStaticSiteStarterBaseStack')
```
[View on GitHub](https://github.com/htmnk/aws-cdk-static-site-starter/blob/main/cdk/bin/cdk.ts#L15)

It is a good practice to ensure that our stacks are deployed to the desired `account` and `region` every time. To accomplish this, we can use the `cdk.StackProps.env` and retrieve the values from our `.env` file, and pass them as properties to our stacks. This approach is recommended for production environments, as it ensures consistency and eliminates potential issues that may arise when working with other team members who may have different accounts configured

```ts
...
const sharedProps: cdk.StackProps = {
  // Set the region/account fields of env to either a concrete
  // value to select the indicated environment (recommended for production stacks)
  env: { account: process.env.CDK_ACCOUNT, region: process.env.CDK_REGION },
}
const baseStack = new BaseStack(app, 'AwsStaticSiteStarterBaseStack', { ...sharedProps })
...
```
[View on GitHub](https://github.com/htmnk/aws-cdk-static-site-starter/blob/main/cdk/bin/cdk.ts#L15)

Now that we have finished preparing resources for the CloudFront construct, we can move on to creating our `BlogStack`.

To get started, create a new file called `lib/blog-stack.ts` and define a new class called `BlogStack`. The stack props should contain the additional `BaseStackResources` props. We can then destructure the props to get the individual resource instances as follows

```ts
import type { Construct } from 'constructs'
import type { BaseStackResources } from './base-stack'
import * as cdk from 'aws-cdk-lib'

export class BlogStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: cdk.StackProps & BaseStackResources) {
    super(scope, id, props)
    const { bucket, originAccessIdentity, functionAssociations } = props
  }
}
```
[View on GitHub](https://github.com/htmnk/aws-cdk-static-site-starter/blob/main/cdk/lib/blog-stack.ts#L15)

Now that we have defined our `BlogStack` class, we can create an instance of it inside `bin/cdk.ts` and pass in the resources as props

```ts
...
import { BlogStack } from '../lib/blog-stack'

...
new BlogStack(app, 'AwsStaticSiteStarterBlogStack', { ...sharedProps, ...baseStack.resources() })
```
[View on GitHub](https://github.com/htmnk/aws-cdk-static-site-starter/blob/main/cdk/bin/cdk.ts#L17)

To finish setting up the `BlogStack`, let's go back to `lib/blog-stack.ts` and create a [CloudFront distribution](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudfront.Distribution.html). We are going to specify a few props here

- `defaultRootObject` - The root `index.html` object we want to request from the S3 origin when a viewer requests the root URL for our distribution.
- `defaultBehavior`
  - `functionAssociations` - The request rewrite function that we created earlier.
  - `origin` - A new `S3Origin` construct with our bucket and the `originAccessIdentity` from our base stack.

```ts
...
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins'
import { aws_cloudfront as cf } from 'aws-cdk-lib'
...
const cfDistribution = new cf.Distribution(this, 'BlogCfDistribution', {
  defaultRootObject: 'index.html',
  defaultBehavior: {
    functionAssociations: [...functionAssociations],
    origin: new origins.S3Origin(bucket, {
      originAccessIdentity,
      originPath: '/blog',
    }),
  },
})
```
[View on GitHub](https://github.com/htmnk/aws-cdk-static-site-starter/blob/main/cdk/lib/blog-stack.ts#L18)

Finally, we will also need a [BucketDeployment](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3_deployment.BucketDeployment.html) construct to deploy our site to our S3 bucket (populate the S3 bucket with our `public` folder). As for the [props](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3_deployment.BucketDeployment.html#construct-props), the `destinationBucket` and the `distribution` are the obvious ones. The ones that need a little bit more attention would be

- `distributionPaths` - The file paths to invalidate in the CloudFront distribution. This is important to specify because if the cache is not invalidated, users may continue to see the old content, which can lead to a variety of issues, such as incorrect display of content, broken links, JavaScript errors, etc.
- `destinationKeyPrefix` - This is to organize our objects in the bucket by placing them in a specific subdirectory (`blog`). In case we would want to deploy multiple apps to the same bucket, we could specify different prefixes for different apps.
- `sources` - The `sources` property allows us to specify the files or directories that we want to deploy to the bucket. An array of [ISource](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3_deployment.ISource.html) objects is expected, so we can use the `s3deploy.Source.asset()` with the node.js `path.join()` to build the path to our `public` directory.

```ts
...
import { join } from 'path'
import * as s3deploy from 'aws-cdk-lib/aws-s3-deployment'
...
new s3deploy.BucketDeployment(this, 'BlogBucketDeployment', {
  destinationBucket: bucket,
  distribution: cfDistribution,
  distributionPaths: ['/*'],
  destinationKeyPrefix: 'blog',
  sources: [s3deploy.Source.asset(join(__dirname, '..', '..', 'public'))],
})
```
[View on GitHub](https://github.com/htmnk/aws-cdk-static-site-starter/blob/main/cdk/lib/blog-stack.ts#L40)

The `BaseStack` and `BlogStack` structure that I chose should now be clearer. If we decide to deploy another site, we can easily reuse resources from the `BaseStack` and add an additional child stack (similar to or the same as the `BlogStack`) as needed. We can then modify the paths and prefixes as needed.

---

{% embed https://dev.to/htmnk/caching-site-assets-with-aws-cdk-s3-snippet-3hp9 %}

Deploying to CloudFront CDN results in a significant improvement over other alternatives due to Amazon's edge cache. To further improve our site performance, we must set [Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) headers that control caching of our assets inside browsers. You may want to consider the snippet I provided above, which extends the `BucketDeployment` (drop-in [SiteBucketDeployment](https://gist.github.com/htmnk/507ece0ecf98d1dd0cb4aa94f53a291b#file-cdk-stack-ts) construct) with several additional properties that enable you to set different `Cache-Control` headers according to the specified file extensions.

---

Well that was easy, let's ship this thing for real now.

**Done**.

- ☑&nbsp;Define AWS resources for the deployment

## Deploy the site

If this is your first time deploying a CDK app on your AWS account, you may also need to run the [cdk bootstrap](https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.html) command. This command creates the necessary resources in your account to support CDK deployments, such as an S3 bucket for storing templates and assets. A simple

```sh
cdk bootstrap
```

should work if your credentials are configured. You should also get a warning when trying to deploy without bootstrapping first.

There are also some useful commands that we can run to ensure that the cdk app is in good shape and we can catch any potential issues early on:

- `cdk diff` - This command compares the current state of the application with the deployed state and shows you the differences between them. This can help you understand what changes will be made to your resources during the next deployment.
- `cdk synth` - Generates the CloudFormation template for your stack, which can be useful for reviewing the template, or even debugging and troubleshooting issues related to your stack deployment.

In addition to the built-in CDK commands, we can also include a custom `build` command to catch any errors or issues with the application's code before deployment. Since it is not part of the CDK toolchain, and we are using `ts-node`, we can replace the build script in the `package.json` file with the command we used in the previous section

```json
...
"scripts": {
  "build": "npx ts-node bin/cdk.ts",
},
...
```
[View on GitHub](https://github.com/htmnk/aws-cdk-static-site-starter/blob/main/cdk/package.json#L8)

and run

```sh
npm run build
```

Alright it's finally time to deploy. As we have multiple stacks within the app, it is necessary to use the `cdk deploy --all` option when deploying our app

```sh
# sudo cdk deploy --all
cdk deploy --all
```

When deploying the CDK application for the first time, it may take a while as all the resources defined in the stacks will be created. During the deployment process, you may be prompted with confirmation messages, asking you to confirm the creation of certain resources.

If the deployment process is successful, you should see a green checkmark with the stack id in the console

```sh
...

✅ AwsStaticSiteStarterBlogStack

✨ Deployment time: 326.15s

...
```

Let's explore the deployed resources in the AWS Management Console. We can first visit the [AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/Welcome.html) service that displays a list of all the stacks in our AWS account, along with the resources that are associated with each stack

1. Open the AWS Management Console
2. In the top navigation bar, click on the Services menu and search/select CloudFormation.
3. In the CloudFormation console, you will see a list of all the stacks in your AWS account.
4. To view the resources associated with a stack, click on the stack name.

We can also view the properties and status of each resource in our stack, as well as perform various actions such as updating or deleting the stack. This is particularly useful when integrating CDK into a continuous integration and deployment (CI/CD) workflow. If a deployment gets stuck, we can use the AWS Management Console to take manual action to fix the issue.

To access our deployed blog, we will need to use the URL provided by CloudFront. To find the HTTP URL of our CloudFront distribution

1. Select/Search CloudFront from the services menu.
2. In the CloudFront console, you will see a list of all the CloudFront distributions in your AWS account.
3. Click on the distribution that was created by your CDK app.
4. In the distribution details page, you will see the Domain Name of the distribution, and it will take the form of _**xxxxxxx.cloudfront.net**_. We can use this URL to access our blog using HTTP. Go ahead and paste the url inside your browser.

If you are not using Hugo as we did in the first section, your site may already be working as intended. With Hugo it should be completely broken at this stage, we have to go back to our `config.toml` and update the [baseURL](https://gohugo.io/getting-started/configuration/#baseurl) with our generated URL

```toml
baseURL = 'http://xxxxxxx.cloudfront.net'
...
```
[View on GitHub](https://github.com/htmnk/aws-cdk-static-site-starter/blob/main/config.toml#L1)

Let's rebuild and redeploy the site again (make sure you are inside the root directory)

```sh
hugo -D
cd cdk
cdk deploy --all
```

The redeploy should be noticeably quicker this time because we are only updating the contents of the site, not the constructs.

🎉 That's it!

> :warning: If you encounter issues when attempting to redeploy and the `cdk diff` command shows changes to the `AWS::Cloudfront::Function .zip`, one solution is to make a small modification to the function code, such as adding a comment. Another option is to use the inline version of the function code when creating the CloudFront function. This may resolve the issue and allow for successful redeployment.

For those of you that are only playing around and don't want to keep the deployed stacks, you can use the `cdk destroy` command to delete all the resources that were created by the deployment. You might have to edit and redeploy the `s3.bucket` construct (the bucket needs to be empty) so that it gets properly destroyed

```ts
this._bucket = new s3.Bucket(this, 'BaseBucket', {
  removalPolicy: RemovalPolicy.DESTROY,
  autoDeleteObjects: true,
})
```

and run

```sh
cdk destroy --all
```

Alternatively you can also delete the contents of the bucket inside the AWS Management console and destroy the stack manually with CloudFormation.

If you have purchased a domain through AWS and want to take your site to production by enabling HTTPS, this final section below is for you. Even if you are not planning to purchase a domain at this time, you may still want to read along to learn about how to set up HTTPS and other security related features using the CDK.

**Done**.

- ☑&nbsp;Deploy the site

## Security and Domain Name

Let's evaluate the security of our site by using [Mozilla Observatory](https://observatory.mozilla.org/) to obtain a security grade. Simply paste your CloudFront `http://xxxxxxx.cloudfront.net/` URL to the input. At this stage, it is likely that we will receive an **"F"** rating, similar to a failing grade in college. However, unlike academic studies, it does not require extensive effort to improve our score. The most important and immediate step is to implement HTTPS, which is now a standard even for websites that do not transmit sensitive data. Failure to do so can result in Google Chrome flagging the website as insecure, which is not an acceptable outcome for a publicly accessible site.

Before diving deep into certificates and HTTPS, we can perform a security checklist by following the suggestions of Mozilla Observatory. One way to do this is by using the [ResponseHeadersPolicy](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudfront.ResponseHeadersPolicy.html) construct to add several security headers to our CloudFront distribution

These headers include (checkout the [Web Security Cheat Sheet](https://infosec.mozilla.org/guidelines/web_security#web-security-cheat-sheet) for more details, the importance and implementation difficulty of each one):

- `Content-Security-Policy` - used to restrict the resources that a browser can load for a given page
- `Content-Type-Options` - prevents the browser from interpreting files as something other than declared by the content type
- `Frame-Options` - prevents clickjacking by ensuring that the content is not embedded into other sites
- `Referrer-Policy` - allows us to control the value of the referrer header
- `Strict-Transport-Security` - used to enforce secure connections to the server
- `X-XSS-Protection` - helps prevent cross-site scripting attacks

Go back to our `cdk/lib/base-stack.ts` and create the `ResponseHeadersPolicy` construct (we will skip CSP in this article)

```ts
export interface BaseStackResources {
  ...
  responseHeadersPolicy: cf.ResponseHeadersPolicy
}

export class BaseStack extends cdk.Stack {
  ...
  private _responseHeadersPolicy: cf.ResponseHeadersPolicy

  resources(): BaseStackResources {
    return {
      ...
      responseHeadersPolicy: this._responseHeadersPolicy,
    }
  }

  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    ...
    this._responseHeadersPolicy = new cf.ResponseHeadersPolicy(this, 'BaseResponseHeadersPolicy', {
      responseHeadersPolicyName: 'CustomBaseResponseHeadersPolicy',
      securityHeadersBehavior: {
        /**
         * @todo ResponseHeadersPolicy CSP (for you, in case you want to have * a perfect grade)
         *
         * When using Hugo, implementing a proper Content Security Policy (CSP) can be
         * challenging when there is limited control over the scripts included on the site.
         */
        // contentSecurityPolicy: { contentSecurityPolicy: 'default-src https:;', override: true },
        contentTypeOptions: { override: true },
        frameOptions: { frameOption: cf.HeadersFrameOption.DENY, override: true },
        referrerPolicy: {
          referrerPolicy: cf.HeadersReferrerPolicy.NO_REFERRER,
          override: true,
        },
        strictTransportSecurity: {
          override: true,
          accessControlMaxAge: Duration.days(365 * 2),
          includeSubdomains: true,
          preload: true,
        },
        xssProtection: { override: true, protection: true, modeBlock: true },
      },
    })
    ...
```
[View on GitHub](https://github.com/htmnk/aws-cdk-static-site-starter/blob/main/cdk/lib/base-stack.ts#L56)

Before proceeding further, ensure that your domain name is registered with Amazon Route 53. This process also creates a [hosted zone](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/hosted-zones-working-with.html) with your domain name as part of the registration. You can follow the guide [How to Register a Domain Name with Amazon Route 53](https://aws.amazon.com/getting-started/hands-on/get-a-domain/)) if you haven't done so already.

We can utilize the `HostedZone.fromLookup()` method to create a `HostedZone` construct, which is required to specify for our certificate. Simply pass in your domain name, which should also be stored in the `.env` file at this point if you have been following the guide from the beginning

```ts
...
import * as route53 from 'aws-cdk-lib/aws-route53'
...
export interface BaseStackResources {
  ...
  zone: route53.IHostedZone
}

export class BaseStack extends cdk.Stack {
  ...
  private _zone: route53.IHostedZone

  resources(): BaseStackResources {
    return {
      ...
      zone: this._zone,
    }
  }

  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
  ...
  this._zone = route53.HostedZone.fromLookup(this, 'BaseHostedZone', {
    domainName: process.env.DOMAIN_NAME ?? '',
  })
```
[View on GitHub](https://github.com/htmnk/aws-cdk-static-site-starter/blob/main/cdk/lib/base-stack.ts#L81)

Next, we will once again proceed to the `lib/blog-stack.ts` file to bring everything together in our CloudFront distribution. We can retrieve the `zone` and `responseHeadersPolicy` constructs we created previously from the props. We can also grab the shared `env` prop for the `region`. Additionally, let's assign the value of `process.env.DOMAIN_NAME` to a variable, as we will be using it in multiple places within the `BlogStack`.

```ts
...
super(scope, id, props)
const { env, zone, responseHeadersPolicy, bucket, originAccessIdentity, functionAssociations } = props
const domainName = process.env.DOMAIN_NAME ?? ''
...
```
[View on GitHub](https://github.com/htmnk/aws-cdk-static-site-starter/blob/main/cdk/lib/blog-stack.ts#L15)

Alright, let's pass in the base stack constructs and specify some additional parameters for the CloudFront distribution. These include:

- [defaultBehavior.responseHeadersPolicy](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudfront.BehaviorOptions.html#responseheaderspolicy) - this is where we will pass in our `responseHeadersPolicy` construct
- [defaultBehavior.viewerProtocolPolicy](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudfront.BehaviorOptions.html#viewerprotocolpolicy) - set to `cf.ViewerProtocolPolicy.REDIRECT_TO_HTTPS` to redirect all HTTP requests to HTTPS
- [priceClass](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudfront.Distribution.html#priceclass) - set to `cf.PriceClass.PRICE_CLASS_ALL` for all CloudFront locations
- [domainNames](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudfront.Distribution.html#domainnames) - an array containing our site's domain name, passed in as `[domainName]`
- [certificate](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudfront.Distribution.html#certificate) - it is necessary to specify the certificate that should be used for HTTPS connections. This is done by passing in an instance of the `Certificate` construct as a property of the `Distribution` construct. The certificate is then associated with the distribution, and CloudFront uses it to authenticate connections to our website. The certificate must be located in N. Virginia `us-east-1`.
- [errorResponses](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudfront.Distribution.html#errorresponses) - CloudFront, with our configuration, will return a `403` error for any missing requests. This includes the standard `404` page responses. To properly handle these errors, we can use this behavior to remap the response and return a `/404.html` page. Keep in mind that this page must exist on your site.

```ts
...
import * as acm from 'aws-cdk-lib/aws-certificatemanager'

...

const cfDistribution = new cf.Distribution(this, 'BlogCfDistribution', {
  defaultRootObject: 'index.html',
  defaultBehavior: {
    functionAssociations: [...functionAssociations],
    origin: new origins.S3Origin(bucket, {
      originAccessIdentity,
      originPath: '/blog',
    }),
    responseHeadersPolicy,
    viewerProtocolPolicy: cf.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
  },
  priceClass: cf.PriceClass.PRICE_CLASS_ALL,
  domainNames: [domainName],
  certificate: new acm.DnsValidatedCertificate(this, 'BlogCertificate', {
    domainName,
    // Support www. + domain name
    subjectAlternativeNames: ['www.' + domainName],
    hostedZone: zone,
    region: env?.region,
  }),
  errorResponses: [{ httpStatus: 403, responseHttpStatus: 404, responsePagePath: '/404.html' }],
})
```
[View on GitHub](https://github.com/htmnk/aws-cdk-static-site-starter/blob/main/cdk/lib/blog-stack.ts#L18)

Next up: an [ARecord](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_route53.ARecord.html). An alias record in Route 53 is a type of record that can be used to map a domain name to selected AWS resources, such as an Elastic Load Balancer, CloudFront distribution, or an S3 bucket. We can use this construct to map our domain name to a [CloudFrontTarget](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_route53_targets.CloudFrontTarget.html)

```ts
...
import * as route53 from 'aws-cdk-lib/aws-route53'
import * as route53targets from 'aws-cdk-lib/aws-route53-targets'

...
new route53.ARecord(this, 'BlogAliasRecord', {
  zone,
  recordName: domainName,
  target: route53.RecordTarget.fromAlias(new route53targets.CloudFrontTarget(cfDistribution)),
})
```
[View on GitHub](https://github.com/htmnk/aws-cdk-static-site-starter/blob/main/cdk/lib/blog-stack.ts#L48)

Not crucial, but we can also use the [HttpsRedirect](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_route53_patterns.HttpsRedirect.html) construct to redirect requests made to the www version of our domain to the non-www version. This is often done to ensure that users accessing our site are directed to a consistent URL, and to improve SEO. By using this construct, we can easily configure a redirect from `www.domainName.com` to `domainName.com` for all requests made to our site

```ts
...
import { aws_cloudfront as cf, aws_route53_patterns } from 'aws-cdk-lib'

...
new aws_route53_patterns.HttpsRedirect(this, 'BlogWwwToNonWww', {
  zone,
  recordNames: ['www.' + domainName],
  targetDomain: domainName,
})
```
[View on GitHub](https://github.com/htmnk/aws-cdk-static-site-starter/blob/main/cdk/lib/blog-stack.ts#L54)

The construct uses CloudFront and S3 behind the scenes, so an additional CloudFront distribution and S3 bucket will be created.

That should be everything we need for now. Let's try a redeploy with the command `cdk deploy --all`. If the deployment is successful, you should be able to access your site at your purchased domain over HTTPS.

After deploying our CDK app, we can view the created certificate in the [AWS Certificate Manager](https://docs.aws.amazon.com/acm/latest/userguide/acm-overview.html) and the created records inside the hosted zone in the [AWS Route 53](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/Welcome.html) service.

Let's also run a scan on the site using Mozilla Observatory again. We should see an improvement in the security grade, and should now receive a grade of **"B"** this time.

🥳 Here's the final result [e53nec.com](https://e53nec.com/).

**Done**.

- ☑&nbsp;Security and Domain Name

## What's next

- [Caching Site Assets with AWS CDK & S3](https://e53nec.com/snippets/caching-site-assets-with-aws-cdk-s3/)