Deploy SPA to AWS

We will do S3, Cloudfront(CDN), custom domain, SSL, and www redirection

Photo by Tyler Lastovich on Unsplash

EDIT: I think you can safely refer to this post instead since the author seemed to really know what he/she was doing. Anything that isn’t covered there, you can try it here. 🌞

Create an IAM user

If you haven’t already got a separate IAM user with restricted permissions, we will create one and give her permissions to perform tasks on S3.

Go to ‘IAM’ service -> Click ‘Add user’
On the first step, give it a name -> Check the ‘Programmatic access’ box -> Click ‘Next: Permissions’

Check the ‘AWS Management Console access’ box too if you want this user to be able to login and manage their restricted version of ‘AWS Management Console’ via a url that looks like So I suppose that’s the power of IAM — it lets you create and manage multiple users, each with differently level of access/permissions. Howver, from the standpoint of a lone side project, this is an overloaded feature which I failed to grasp until I watched a really dumbed-down Youtube video about it…But hey we just created a IAM user and using root’s access and secret keys is a huge taboo, so we are really getting off on the right foot! Anyway, let’s move on.

Now, keep clicking next/accept the defaults until you come to this page:

Click the ‘Download .csv’

And note down this user’s Access key and Secret access key because we are going to need them later when configuring ‘aws-cli’. But nevermind all that for now. Let’s move on.

Let’s explore our new ‘sum-thing-wong’ user
In ‘Permissions’ tab -> Click that ‘Add inline policy’
In the ‘JSON’ tab -> Paste the content below in the editor -> Click ‘Review policy’
"Version": "2012-10-17",
"Statement": [
"Effect": "Allow",
"Action": [
"Resource": [

So the ‘policy’ above is essentially saying: This IAM user has permission to upload and download files and ‘ACL’(don’t remove these! It’d break the upload! i.e. Permission Denied error. I learnt that the hard way..), and delete files, and well ‘ListBucket’, on your bucket, in this case (rename it to yours!) and its nested files(i.e. /*).

And finally,

Enter a name for this policy and click ‘Create policy’

And that’s it with this IAM stuff. Next, we will create S3 bucket so we can upload our react/front-end stuff to it later!

Create a S3 bucket for static website hosting

Btw here is a rare straightforward tutorial from AWS that don’t suck you into infinite rabbit hole. But I like pictures, so here we go:

Go to S3 service and ‘Create bucket’.
Enter your bucket name(I gave my FE domain name as the name), then click ‘Create’. Leave everything else be.
Let’s jump into your new bucket.
Click ‘Properties’ and click ‘Static website hosting’ card
Take note of the ‘Endpoint’ value(minus the ‘http://’ bit)! Gonna need it for Cloudfront later. And do all that in green boxes and click ‘Save’.
Go to ‘Permission’ tab -> ‘Block public access’ -> Click ‘Edit’ -> Turn OFF all the blocking

Admittedly, there might be a way where I didn’t have to turn OFF all blocking of public access here but currently I have no idea. Will update if/when I do :)

Go to ‘Bucket Policy’ -> Paste the content provided below -> Click ‘Save’
"Version": "2012-10-17",
"Statement": [
"Sid": "PublicReadAccess",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "*"

Change the red underlined bit to the name of your bucket name.

And now we are done setting up our S3 bucket.

Before we move on to Cloudfront to serve this bucket from CDN, let’s see how we will deploy/upload our dist/build folder’s files to this bucket from our local machine 🚀

Upload static assets to a S3 bucket


We are going to need a tool called aws-cli . You just need to stick to the official tutorial below and you are done with that.

Configure aws-cli

Dig out the access and secrets keys that we obtained before when creating the IAM user, and plug them in when you run aws configure :

$ aws configure
AWS Secret Access Key [None]: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
Default region name [None]: us-west-2
Default output format [None]: json

Create a npm script for deployment

In your package.json ‘s ‘scripts’ property, add the following script to automate:

  1. Building/bundling your web app into a build folder.
  2. Delete all existing files in the S3 bucket that will contain our fresh static assets.
  3. Upload all the fresh stuff in the build folder to a S3 bucket called and allow public to read the files.
"deploy-client": "npm run build && aws s3 sync --delete build/ s3:// --acl public-read",

So now every time you want to deploy your front-end, you will just do

npm run deploy-client

🎊 🎈 ✊

Setup Cloudfront(CDN) to serve your bucket

Before we start, you can check out this tutorial from AWS regarding this matter. But if you want to follow by pictures, then read on :)

OK now we are going to setup a Cloudfront ‘distribution’ to serve our web app’s bucket’s contents! Navigate to the ‘Cloudfront’ service and do all this:

Yes click that
Then click that
Do that
Do all that if you wanna win
you wanna win right??? yea do that

[CORRECTION]: In the ‘Object Caching’ field above, just go with the default ‘Use Origin Cache Headers’ so Cloudfront will respect the cache-control header you can set, possibly in the npm deploy script:

"upload-assets": "aws s3 sync ./client/dist/ s3:// --delete --exclude 'index.html' --metadata-directive REPLACE --cache-control public,max-age=31557600,s-maxage=86400","upload-index-html":  "aws s3 cp ./client/dist/index.html s3:// --metadata-directive REPLACE --cache-control no-cache,no-store,must-revalidate",
Enter ‘index.html’ for ‘Default Root Object’

See here if you wondered why we entered endpoint string for the ‘Origin domain name’ field when we could have selected our bucket in the autocomplete dropdown.

We will largely ignore the ‘Distribution Settings’ for now. We will explore that abit later to setup custom domain pointing to our Cloudfront — rather than to access our app, we do .

And click the ‘Create Distribution’ button! It will take 5–15mins to be deployed to all edges, and once it’s done, you will have a URL like serving your bucket from the endpoint given to the ‘Origin domain name’ field above.

Setup Custom Domain and SSL/HTTPS

Click one of these to enter a cloudfront distribution
Click ‘Edit’
Enter your custom domain name in ‘Alternate Domain Names’

And click ‘Request or Import a Certificate with ACM’ button to let AWS generates a SSL cert for us.

Clicking the button will bring us to the ‘ACM’ service…

Fill in your ‘’ and ‘’ domains, and click ‘Next’
Let’s validate via DNS! And simple proceed to the very end to click ‘Confirm and request’ button

Then you will see something like this:

Collapsing one of the row will show you this:

The ‘Name’ is the ‘Subdomain’, ‘Type’ is the DNS record type, ‘Value’ is the value to map to

Now you need to add a CNAME record in your domain name registrar like Namecheap, Hover, GoDaddy, or in Lightsail and Digital Ocean if you have moved your nameservers over there.

Once you have added the record, it will take a quick while to successfully validate upon which the status will show ‘Issued’.

OK now you can go back to the ‘Edit distribution’ page and do this:

Your previously created cert will show up in the dropdown! Select it and save

Finally, one last thing to do to get over with this chore is head back to your domain name registrar(Namecheap etc.) and add another CNAME record that maps your custom domain to the cloudfront domain that fronted your web app’s bucket. The cloudfront’s domain name you can find it here:

OK I lied, we should do something to handle when users land in our site in which case we will redirect them to our . The benefit is purportedly search engine won’t list your www site too.

OK let’s do it.

You will create another S3 bucket with a name like . We are not going to do anything with this bucket but

  1. Turn off all the public access blocks
  2. Redirect this bucket to our bucket!
Redirect this bucket to bucket without www

Then you will create another Cloudfront distribution for this www bucket by following the same steps we did for a distribution before.

And create a CNAME record for this domain too.

Add security headers to your app

You know like those headers provided by the popular ‘helmet’ library but now you have to do it in another AWS service called Lambda@Edge that integrates with Cloudfront that is now serving your stuff.

Below is a straightforward official tutorial:

Final words

If you are just building yet another 90% of the apps out there, avoid AWS by considering other options first like Digital Ocean, in particularly, Render which costs much more and significantly less bandwidth offered, but it’s a lifesaver for those building 90% of the apps, in which case though, paradoxically, you should consider AWS! Because it saves you money, the free tier provides generous runway for the potential doom and gloom, you get to learn AWS(hello recruiter!), you got nothing to lose assuming you hadn’t quit your day job(right?), and the world is not missing anything too! But anyway, with the benefit of hindsight, I would say this post has reduced the gap between AWS and the one-click deployment platforms for those currently, for whatever reason, ended up in AWS .🙏

‘Untitled’ by Kheoh Yee Wei



Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store