Hosting Angular in AWS S3 for Realz

Dean Fiala
7 min readApr 3, 2023

--

We recently inherited three angular apps that all used the same rails API as a backend. Our standard deployment process creates a container for a rails app and spins up a task in an AWS ECS to run that container. I started working on creating a docker file to host and expose the rails API and the three angular apps. I got it working after a fashion, but it was rather fragile and retooling our Continuous Integration/Continuous Deployment (CI/CD) process to handle this one-off beast was looking even more challenging. While searching the interwebs for some guidance I stumbled across a suggestion to host an Angular app in an S3 bucket.

I slapped my forehead. “Of course, an Angular app itself is nothing but static files — no need to get fancy and throw javascript apps into docker containers — I can just make static websites.” I had previously created static sites using AWS S3 buckets. I had also implemented CloudFront distributions to support https and custom domains for these sites (because few users want to see blah.s3-website-us-east-1.amazonaws.com as their app’s domain).

So I created a bucket, set the permissions, built the app locally, uploaded the dist files to the bucket, created the CloudFront distribution with https and the desired domain and voila — there was the login screen for the app.

Of course, it didn’t work. The first issue I noticed was that clicking on the login button caused a 404 error. Why? Because there is no login file — and hence while the angular app handled the call, the dumb website wanted to show a file named login. After a little searching, I found the solution and again slapped my forehead. The Angular app is a Single Page App (SPA)— it should always end up on index.html. Fortunately, there’s a simple way in CloudFront to make it this happen — creating custom error responses…

CloudFront Custom Error Response Editor

Instead of showing the 404 error, it will redirect to index.html with an OK status. The Angular app would have access to any real errors and could handle them appropriately. The static website would happily just continue to show the index page. So I created custom error responses for status codes 400, 403, 404, 405, 500, 502. While app still didn’t do anything — at least it wasn’t throwing naked errors.

Then it was time to hook up an Angular app to the API. Deploying the API was easy — our CI/CD process is built to deploy Rails apps to AWS. After adding some configurations to our CD app and pressing a button, the API was soon up and running, awaiting a call from an Angular app.

I mistakenly thought it would be fairly trivial to configure the Angular app to call the API. Silly me. After cycling through more approaches than I care to enumerate, I was stuck. No matter what I did, I kept getting 405 (Method not Allowed) errors from CloudFront when I tried to login. From the logs it was clear that no call was even made to the API. I played with CORS headers in CloudFront and CORS settings in the API, hacked the heck out of the server config in Angular. Finally, in desperation, I tried to abuse the proxy.config.json in the angular app— but nothing worked.

I could call the Rails API successfully from Postman, and even a local build of the Angular app, but on the static site in S3 — 405, 405, 405 errors.

Changing my search query slightly, I alighted upon the solution. Buried in an answer to this stack overflow question was what I had been missing. I needed something outside of the Angular app to handle the routing to the API properly. If you’re running Angular on a web server, you’d do this in nginx, but how accomplish the same thing with S3 and CloudFront?

“It’s the origins stupid!”. My forehead needed another slap, but it was rather bruised at this point, so I just read in delight…

Create a second Origin in CloudFront, pointing to api.example.com. Leave "Origin Path" blank, because it does not do what you might assume.

Create a new Cache Behavior in CloudFront, with the Path Pattern of /api*. Point this to the newly-created origin.

CloudFront will send all requests for /api* to api.example.com and everything else to the default Cache Behavior Origin, which would be the bucket.

I added the origin and corresponding behavior to the distribution, and waited for the CloudFront to deploy the updated version. I then went back to Angular app, entered my credentials and hit the Login button and — boom — I was in. Two bits of configuration were all that was required.

Before I could locate an ice pack for for my forehead, I tried to click a link to another page and received a 401 Unauthorized error — what the what? I had just logged in. I confirmed that the auth token was correct in the Angular app. Somehow it wasn’t getting passed along to the API. Sure enough — when I turned on some logging — there was no authorization header in the request to the API.

Enlightened by my previous discovery of behavior magic — I returned to the /api * behavior saw the Cache Key and Origin Requests section…

CloudFront Behavior Cache Key and Origin Requests Configuration

My first instinct was that I need to change the Origin Request Policy to include the authorization header. That lasted all of a minute when I was confronted with this option in the Include Headers dropdown.

Authorization Header Option

Duh, makes perfect sense. The cache key should contain the authorization token so you don’t go spewing sensitive data to unauthorized users via the cache.

The default Cache Policy is CachingOptimized — which as you can see below sends absolutely nothing to the origin.

So I created a new Cache Policy called AuthPolicy and added the authorization token header to the policy. I updated the behavior to use the new policy — and now I could click around in the app as the authorized user I knew I was.

But I noticed something — the filters weren’t um, filtering. I flashed back to the cache key settings problem and remembered seeing Query strings — None. I updated the AuthPolicy to include All Query strings and now the app was actually working as intended. I could have also created an Origin Request policy to pass along the query strings, but since I had already created the cache AuthPolicy, I simply used that.

TL/DR

For those more interested in the destination than the journey, here’s what is needed to host an Angular app in an S3 bucket with a working Rails API backend. Assuming you have a Rails API set up already…

  1. Create an S3 Bucket
    - turn on public access
    - set up a bucket policy to allow Get object
    - set up as a static website
  2. Build the angular app and upload the content of the dist folder to the bucket
  3. Create a CloudFront Distribution
    - set the origin domain that to the S3 Bucket url (example.s3-website-us-east-1.amazonaws.com)
    - Add an Alternate Domain Name and set it to the domain that you want your users to use to access the app
    - Set the Custom SSL Certificate associated with the domain used in the step above
  4. Add an origin to the Distribution for the API, setting the origin domain to the Rails API domain
  5. Create a Behavior to route the API requests
    - with an Path Pattern of /api*
    - select the API origin for the origin
    - allow GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE methods
    - create a Cache Policy that includes the Authorization header and includes the Query string
  6. Set up custom error responses for the 400, 403, 404, 405 and 500 http codes
    - set the response page path as /index.html
    -
    set the the http response code to 200
  7. Add the appropriate DNS entries to Route 53
    - the api domain should alias the load balancer
    - the angular domain should alias the CloudFront distribution
  8. Once the CloudFront distribution has finished deploying, you should be able go the url of the domain specified above and find you have an Angular app that works properly with its Rails API

Conclusion

The above demonstrates how to successfully deploy a fully functional Angular app with an API backend to an S3 bucket (with a little help from CloudFront distributions). Though it is not as trivial as dropping some files into an S3 bucket and calling it a day, using behaviors and custom error responses to route the web calls to the proper origins makes it rather straightforward.

While there are a number of articles on how to deploy an Angular app to an AWS S3 bucket, none of them details how to call a backend API from that app. As that seems a foundational piece of every Angular app, I hope this article bridges that gap.

Finally, my eternal gratitude to Michael — sqlbot whose stackoverflow answer introduced me to the power of CloudFront origins and kept me from causing further damage to my forehead.

--

--