Single-Page Apps on AWS, Part 1: Hosting a Website on S3

Paul Lessing
11 min readApr 16, 2018

AWS is really useful. Its free tier allows developers who are running things for their personal projects, or on a small scale, to deploy and run code without having to commit to whole servers for their infrastructure.

One such use case is hosting a Single Page App (SPA), built in a JavaScript framework such as Angular or React. For these apps, you don’t need a server — all you need is somewhere that will serve a set of static files, and all the website logic is done client-side by JavaScript. Buying a Droplet at DigitalOcean would be overkill, especially given that you’d still need to configure everything (although if you do need a VPS, I can highly recommend them). And none of the static or PHP hosting websites I’ve ever used have given me a good developer experience. Plus, AWS’s free tier means I end up paying significantly less for these kinds of websites — my current running costs total something along the lines of 30¢/month.

In this guide we will be setting up a static website hosted on S3, with www to non-www redirection, SSL, and caching. At the end of this article we will have set up a domain, S3 buckets for containing all the files your SPA needs, and all the config for ensuring the SPA works smoothly. All you need to do is upload your SPA into the bucket.

In Part 2, we will set up an API that your SPA can use, and that you can deploy to with minimal effort.

In Part 3, we will discuss strategies for deploying the SPA to S3.

On to part 1. We need to set up our DNS server, get an SSL certificate, create S3 buckets, and set up a caching layer to redirect to those buckets.

Step 1: DNS with CloudFlare

We will use CloudFlare for managing our DNS. Aside from the usual advantages such as caching and DDOS protection, its main reason here is that as a DNS server it allows us to use CNAME entries for the root-level domain, something that most other DNS providers don’t. This means we will be able to point our primary domain at an Amazon CloudFront distribution whose IP we don’t know and it will work correctly even if CloudFront changes their IPs. This is what Route53 does in the Amazon Guide to Static Website Hosting using S3, but Route53 is at time of writing not included in the AWS Free Tier, so comparatively it’s quite expensive just for saving us a little bit of configuration.

Once you’ve registered on CloudFlare, click “Add Site” to add your domain. Enter your domain name, click through the confirmation page, then choose the free plan. Carry over any DNS settings you may already have — make sure to choose “bypass CloudFlare” for any that may suffer from having CloudFlare intercepting and caching traffic.

Do not set up any new DNS entries yet. If there is an A record for the domain we’re setting up, you should remove it.

Go to the “Caching” tab and enable Development Mode. This will stop CloudFlare from caching anything, which comes in handy when we’re setting up the buckets later. Any misconfigurations won’t be cached so we can see what we’ve done wrong (or when it starts going right!) Development Mode automatically expires after a few hours, so you don’t need to worry about forgetting to turn it off, but if you’re taking longer than that remember to turn it back on.

CloudFlare: Development mode stops us from getting cached results while we set up and test all our settings.

Step 2: Get an SSL Certificate

Go to https://eu-west-1.console.aws.amazon.com/acm/home to set up an SSL certificate. Certificates can only be issued from a few regions, which do not need to match the region of your buckets, so if you get an error pick any region that works (I use US East, N. Virginia).

Click through “Provision certificates — Get Started,” and select “Request a public certificate.”

Enter all the domain names for which you want this certificate to work. You will want the root and www, but you may want some others too that you’ll be using later from AWS (such as an api endpoint accessed via AWS lambda). You can add a wildcard subdomain by entering *.your-domain.com if you need it.

ACM: Set up all subdomains you think you might need in the future.

Choose DNS validation on the next screen. This is a really simple way to get the validation to work. Review your settings and confirm.

ACM: Validation screen for the SSL certificate

On the next screen (validation), you will see the subdomains you selected. For each of these domains, copy the Name and Value as a new CNAME record into CloudFlare. Make sure you copy the entire name, including the final trailing dot. Disable the “Traffic will go through CloudFlare” feature (orange cloud).

CloudFlare: Set up the DNS records…
CloudFlare: Completed DNS record

Once you have set up all the DNS records, complete the SSL certificate wizard on AWS. On the resulting page, your domains might already have been verified; if they have not (showing “Pending validation”), click the “Refresh” button in the top right. CloudFlare is quite fast, so this only took a few minutes for me, but your mileage may vary. It definitely should not take longer than a day.

ACM: Refresh the page until all domains are validated.

Step 3: Create S3 Buckets

Next you need to create your S3 buckets. Go to the S3 manager for your region where we will create two buckets: One for yourdomain.com, one for the www variant. We will use the first one for storing the files, and the second one for redirecting to the first one. Since the second one does not contain any files that need to be served to users, I have found it to also be a useful place to store logs.

You now need to decide which way you want to redirect your buckets. I’ll be referring to the bucket with the naked domain name as the primary bucket, and the one with www as the secondary one, since I prefer redirecting www to the naked domain name. If you would like it the other way round just swap primary and secondary in the following instructions.

The primary bucket should be named as the full domain name, e.g. yourdomain.com, and the secondary one should be www.yourdomain.com. The settings are the same except for the permissions (step 3).
Note: Since we’ll be using CloudFront, the buckets don’t need to have these specific names. The bucket name only has to match the domain name when you’re directly pointing your CNAME records at the domain for the bucket. It is, however, convenient to have them matching so you know where to look for them!

S3: Create the buckets for your domain

You don’t need to choose any of the optional properties in step 2.

S3: Bucket properties. Skip all of these

For the primary bucket, set the public permissions to grant public read access and give no system permissions.

S3: Bucket permissions (primary bucket)

For the secondary bucket, don’t grant public permissions, but grant log access:

S3: Bucket permissions (secondary bucket)

Create and upload an index.html file to the primary bucket so we will be able to see whether things are working later.

In the primary bucket, set up Static Website Hosting by going to Properties, selecting Static Website Hosting, and setting it to “Use this bucket to host a website.” Both the index document and the error document should be index.html so that your SPA will always be able to handle the response even if a file has not been found.

S3: Static Website Settings for the primary bucket

In the secondary bucket, set up website hosting, but this time you will set it to always redirect to the primary bucket.

S3: Static Website Settings for the secondary bucket

The index.html file does not need to exist, because we’re using the following Redirection rules:

<RoutingRules>
<RoutingRule>
<Redirect>
<Protocol>https</Protocol>
<HostName>paullessing.com</HostName>
<HttpRedirectCode>302</HttpRedirectCode>
</Redirect>
</RoutingRule>
</RoutingRules>

This means that while the bucket theoretically can serve files, in reality it will redirect every request (since there is no <Condition>) to the bare URL.

You can test if your setup works by going to the bucket URLs: The primary URL (http://your-domain.com.s3-website.eu-west-2.amazonaws.com) should serve your index.html file, and the secondary one (http://www.your-domain.com.s3-...) should redirect in the browser to the primary one (which won’t work yet, as we’ll be setting that up next).

Step 4: CloudFront Distributions

Now we will set up the CloudFront distributions, which handle our SSL redirection, caching, and most importantly mapping our domain name to the correct bucket.

Go to the CloudFront Distributions Console and create a new distribution, selecting Web as the distribution type.

The Origin Domain Name field has a dropdown which will autocomplete your bucket name; do not use the autocomplete! Setting the Origin domain name to the bucket ID (which the autocomplete does) will stop the distribution from correctly mapping Static Website traffic to the bucket. Instead, go to the Static Website settings for your bucket and copy the long URL ( http://your-domain.com.s3-website.eu-west-2.amazonaws.com) into the field. It will populate the Origin ID field for you. Leave Origin Path and Custom Headers blank.

CloudFront: Origin Settings. Do not use the autocomplete for Domain Name

In the Default Cache Behavior Settings category, the only setting you need to change is the HTTPS redirect behaviour:

CloudFront: Default Cache Behavior Settings

Under Distribution Settings, enter your primary domain name under the Alternate Domain Names (CNAMEs) field. Do not enter your secondary domain name — if you do that, then all requests to either domain will be cached by the same distribution and you won’t get redirection for www domains (the www version will behave the same as the non-www version without redirection.) If you do want that, then add the other domain as a CNAME, skip creating the second distribution later in this section, and use the same CloudFront server for primary and secondary domains in step 6.

For your SSL certificate, use “Custom SSL Certificate” and select the certificate we set up in step 2.

CloudFront: Distribution Settings

You don’t need to edit any other settings in this category; in particular leave the Default Root Object field blank.

Create the distribution to return to the distributions list, and create a second distribution for the secondary domain. Use the same settings as the primary distribution, but set the Origin Domain Name to the secondary bucket, and the Alternate Domain Names (CNAMEs) to the secondary domain.

Step 5: Error Responses

Now we need to add an Error Response rule to our primary distribution. When our bucket cannot find a URL that we give it (e.g. /foobar, it will still serve the index.html file we have set up (this is what our Error Page config did in the bucket settings). However, it will serve it with a 404 error code, which can confuse browsers and scrapers. We will be setting up a rule that tells CloudFront to always serve 404s with a 200 error code, because we know that all files will either be present, or be served the index.html file (which is a success).

In the CloudFront Distributions List, click on the ID for your primary distribution. Go to the “Error Pages” tab, and click “Create Custom Error Response.” On the next page, select “404” for the HTTP Error Code, leave the TTL as the default, choose Customize Error Response — yes, and set the Response Page Path to /index.html with a Response Code of 200.

CloudFront: Settings for the Custom Error Response

This will cause CloudFront to serve any file that the bucket reports as Not Found with a 200 status code and the index file instead.

Step 6: Direct your domain at CloudFront

CloudFront: Distribution List

You now need to go to CloudFlare and set your domains to redirect to CloudFront. For each domain from the CloudFlare list, copy the Domain Name value, and create a new CNAME record in CloudFlare pointing to the correct CloudFront domain.

CloudFlare: Adding the root domain as a CNAME

Use CNAME with a domain of @ for the root domain, and plain www for the seondary domain.

CloudFlare: Domain records for CloudFlare-to-CloudFront

That’s it! Once your DNS has propagated (this may take a while, but usually no longer than an hour) you should be able to visit your site and see your index.html file!

Summary

We have set up a DNS zone on CloudFlare which uses CNAME records to redirect traffic to the two CloudFront distributions we have set up for our two S3 buckets. These distributions use SSL certificates issued by ACM. Both buckets are Static Website Hosting buckets; the secondary one always redirects to the primary one. The primary bucket will serve index.html for all requests that do not map to files inside the bucket. For all error pages we set the status code to 200 in CloudFront and serve index.html anyway.

And that, fundamentally, is it. If you build your SPA so that it serves its content from an index.html file and upload all the content to the primary bucket, your website should work!

Join us in part 2, where we discuss how to use Serverless and Amazon Lambda with the API Gateway to host the API for this app.

Bonus: How to force refresh the caches

CloudFront is a caching layer, and as such will serve cached versions of your files for a while after you have updated them. Sometimes you want to update things more quickly, for example if you’re testing new configuration. You can manually clear the cache for CloudFront, which they call an “Invalidation”.

Note: CloudFront invalidations will cost you, but only after the first 1000 paths each month. This means that as long as you’re not doing a lot of these each day, you should be fine.

To invalidate a distribution (usually your primary one), go to to it in the console and click on its ID. In the details page, choose the “Invalidations” tab and select “Create Invalidation”. Enter the paths of the objects you want to invalidate. CloudFront recommends using versioned files so you don’t have to do this, but if your index.html is the one containing the links to all the versioned file you still need to invalidate it!

Since the limit is “First 1000 paths per month” and I don’t tend to care too much about performance when I’m force-updating, I just enter /* as the path. This will invalidate all files in this distribution.

CloudFront: Invalidating all files in a Distribution

Once that’s done, wait for the Invalidation to run, and you should see the updates on your page.

--

--