Hosting a static site on AWS using S3 and CloudFront

A few years ago, Michael Berkowski gently scolded me for hosting a site on HTTP — not HTTPS. I decided that the easiest way to fix this (ignoring Let’s Encrypt for now) was to instead host the site, a static site that hasn’t been updated in years, on AWS. Specifically, to host the site using S3 and CloudFront.

The domain was, related to a road trip adventure that didn’t go exactly as planned.

Since that time, I’ve migrated several other sites to AWS, using S3 to store the files and CloudFront as the front-end CDN. I’ve learned a few things in the process, including several of the things that can go wrong. I’ve also created a YouTube video on the process, for people who want to see this step-by-step: Hosting a Static HTML Site on AWS S3.

I followed the instructions here in the Amazon CloudFront Developer Guide: Getting started with a secure static website. There are a few key points that I want to highlight:

Use AWS’s Route 53 to handle DNS
One of the first key points is to ensure that AWS’s Route 53 is handling the DNS for the domain. To do this, I manually configured a Hosted Zone in Route 53, and then updated the nameservers with my domain registrar.

Note that creating a hosted zone will generate a SOA (Start-of-Authority) and an NS (Nameserver) record for you. The values in the NS record can be used to update your registrar. A couple notes:

  • Make a copy of your original nameserver records. If something goes wrong, you can always revert to those values.
  • Once you update the nameserver records, your website (and other services offered on your domain, if any) will stop working.

There is a way around the latter problem: you can copy all of the DNS records from your existing service over to Route 53. I’m thinking A and AAAA records, CNAME records, MX records if any. However, with those records in your hosted zone, some of the later CloudFormation steps will fail. I decided I could tolerate some downtime for the sites I was migrating.

Create an Apex Alias and include your Hosted Zone ID
After creating the hosted zone in Route 53 and updating the nameservers with my registrar, I clicked the convenient “Launch on AWS” button in the Using the AWS CloudFormation console section of the instructions. That link loads a CloudFormation template from AWS S3: Launch on AWS

The first page is entirely pre-populated, so I clicked Next.

On the second page, I set:

Stack Name: redbuswashere-static-site
Subdomain: www
CreateApex: yes
HostedZoneId: [the hosted zone ID from Route 53]

The first time I went through this process, I wasn’t sure what CreateApex meant. This field description says “Create an Apex Alias in CloudFront distribution – yes/no”, but I still wasn’t sure. When I launched the site, I was able to access it at, but not at (without the www). The DNS zone apex is the same level as the SOA and NS records, and creating an apex alias here enables users to reach the site with or without the www.

The HostedZoneId is generated by Route 53 when you configure a hosted zone. You should be able to find this value under the hosted zone details. Note that the form does not require you to fill out this field, but if you leave it blank the CloudFormation template will fail!

Add a tag, update the rollback options
On the third page, there are a few options that aren’t as critical, but I changed some options.

I know tags are important to help organize cloud resources, but I haven’t created a robust system of tagging yet. I just added a “site” tag and set it to “” so that I can located associated AWS resources easily.

For “Stack Failure Options,” I left “Roll back all stack resources” selected. I also selected “Delete all newly created resources.” Hopefully everything will go right the first time, but I can tell you that I’ve had to delete a number of orphaned S3 buckets that were left behind after previous failures where I did not have that option selected.

I left the Advanced Options alone.

How much will this cost?
A couple years ago, the fourth page used to include a cost estimator. It seems that feature has disappeared since then. At the time, I tried out a few numbers for S3, Route 53, and CloudFront and came up with an estimate of $0.56 (USD) per month. Costs will vary based on the size of your site and how much traffic you get, but I can tell you that for these relatively small and low-traffic sites I am actually paying about $0.52 (USD) per month. The majority of that cost is for Route 53.

I reviewed the rest of the info, acknowledged that CloudFormation might need some additional capabilities, and clicked Create Stack. The creation take a little while, I’ve seen anything from a few minutes to close to an hour.

Placeholder content: I am a static website!
Once I made a few initial mistakes (adding extra records to Route 53, not selecting CreateApex, leaving out the HostedZoneId), it worked! I visited and saw a placeholder page with this content:

I am a static website!

Great, huh? Here's a link to another page.

The CloudFormation template creates two associated S3 buckets, one for the logs with s3bucketlogs in the name and one for the website files with s3bucketroot in the name. It should also include part of your CloudFormation stack name, so a good concise name there will help you find the appropriate S3 buckets.

You can add content to the S3 bucket directly via the AWS web console. It looks like on many systems you can drag-and-drop files from your filesystem. In my demo I was on a Lubuntu VM running Firefox and drag-and-drop didn’t work. Selecting multiple files worked, but I was only able to select one folder at a time. This may be easier for you depending on your setup.

I decided to use the AWS CLI tool instead.

Be sure to specify a valid AWS region
Installing the AWS CLI tools on Linux is fairly straightforward, I followed their user guide: Install the AWS CLI version 2 on Linux

Once installed, to configure the AWS CLI, run:

aws configure

It will prompt you for four items:

  1. AWS Access Key ID
  2. AWS Secret Access Key
  3. Default region name
  4. Default output format

Then, navigate to a folder that represents your web document root (or a copy of it) and run the following, replacing the long yourdomain-static-site-customreso-s3bucketroot-8wb7kjdm89qa value with your S3 bucket name:

aws s3 sync ./ s3://yourdomain-static-site-customreso-s3bucketroot-8wb7kjdm89qa/

The first time I ran this, it failed. I had specified an invalid AWS region (I tried us-east, instead of us-east-1). The aws configure tool doesn’t check any of the values you enter. If you do run into such an error, try running aws configure again. It should remember your previous values for AWS Access Key ID and AWS Secret Access Key (just press Enter to keep the old values), but let you update the Default region name.

The next time I ran the command, it succeeded! Note that if you had already uploaded all of the files via the AWS console, aws s3 sync won’t show any output. It will only notify you of changes.

Cache Invalidation
I had uploaded all the website content, but when I reloaded I still got the “I am a static website!” placeholder. That’s due to CloudFront caching. That will take care of itself eventually, but you might like to verify that it worked.

You can navigate to CloudFront in the AWS console, select the distribution (basically, the domain) for your website. The names are gibberish, but the “Alternate domain names” and “Origins” columns should help you identify the correct entry if you have multiple sites. If you click on the linked distribution ID and select the “Invalidations” tab you can create a new invalidation. In my case, only the index.html page was affected, so I entered:


That initial forward-slash (/) is important, although the error message is actually helpful if you leave it off (“One or more paths must start with a /”).

Directory indexes
Depending on the structure of your site, you probably rely on links to directory names. You might link to /section1/ instead of /section1/index.html and rely on the webserver to handle figuring that out.

CloudFront doesn’t do that for you automatically. Visitors attempting to access /section1/ will get a 403 Forbidden error. I didn’t realize this for the first couple sites I migrated because they were shallow sites, just a lot of HTML files at the top level. When I migrated some more complex sites, this issue became apparent.

There’s a relatively straightforward way to address this, which I describe in a different post: DirectoryIndex on a static HTML site hosted by AWS.

The first couple sites I migrated in 2021 were not affected by this, but recent sites have been. CloudFront automatically includes a content-security-policy header that is fairly strict:

default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'

If your site is linking to assets from different sites (or even different subdomains of your own site), the browser will block those due to the content-security-policy header. Many sites load Javascript files like jQuery from,, or Many sites include images embedded from other domains.

You can adjust the content-security-policy by navigating to CloudFront and selecting “Policies” (instead of “Distributions”). Under “Policies” select the “Response headers” tab. There should be an entry for your site (or at least newer sites). When editing the policy, you have the option to disable the Content-Security-Policy. I don’t recommend this. You should be able to add appropriate values to script-src and img-src (even img-src * has value if object-src is set to 'none'). See Mozilla’s docs on Content Security Policy (CSP) for more details.

Happy hosting!
I’m excited about this inexpensive and relatively easy way to host static HTML sites. I hope I’ve run into most of the possible errors and that this post is helpful. Let me know if there are other errors you’ve run into!

Leave a Reply

Your email address will not be published. Required fields are marked *