Skip to main content

Static Front-ends with Angular and S3

· 9 min read
Software Engineer

Abstract. This article details the architecture and configuration of an Angular application served using AWS S3 and Cloudfront.


Note

The architecture of my website has evolved since this was written and no longer relies on Angular. It is still compiled into a static front-end and many of these concepts still apply.

As large front-end frameworks increasingly become the norm these days, I became interested in creating a new personal website. One that is more dynamic and easier to maintain. This article details the engineering of a new architecture for my website based on Angular and hosted at low-cost in AWS S3. I will go over the configuration of the infrastructure required to host a site in S3 using multiple domains, handling HTTPS and redirection, and some nuances of using S3 to host an Angular application. I will also briefly discuss an approach to a server less back-end based on AWS Lambda at little additional cost.

Background

Development of a personal website has been a long-running hobby of mine that began with a site built in PHP on Apache Webserver hosted on AWS EC2 and eventually migrating to a Jekyll-based static site hosted on GitHub pages. The migration from EC2 to a static site was driven by a desire to do less server maintenance, simplify deployment, reduce security risk, and decrease cost. At the time, it made sense. The cost of a the dedicated EC2 instance was about $15/month. PHP and Apache Webserver were overkill for something that can be best described as a low-traffic blog.

But there were downsides too. Jekyll was the primary framework supported by the GitHub pages (GitHub pages was free, so there was a lot of motivation to use that as a hosting platform) but Jekyll is based on Ruby, a language I’ve never really spent any professional time working in. While my limited Ruby knowledge did not have a major impact, my lack of familiarity with the tech stack did add more maintenance time than the site should have taken.

The site was also very static. The Jekyll framework made writing Markdown based articles easy, but anything more dynamic was largely done in HTML and vanilla JavaScript. It just wasn’t meant for that. Combining all these challenges with a desire to have more dynamic behavior and tighter control over the application from development to deployment, it was time to architect a new solution.

Architecture

The new website is built on Angular and hosted as static website in AWS S3. Cloudfront is used to provide SSL and HTTP to HTTPS redirection. It’s worth noting that Route 53 is used to manage DNS and the domain names point to Cloudfront distributions.

Back-end APIs and resources are minimal. The front-end application (as of this writing) doesn’t depend on any API back-end or server-side resources. But his may come up in the future for things like a contact form or simple web apps. The back-end functionality I have added for some small projects is a serverless approach built on AWS API Gateway and AWS Lambda with DynamoDB. Since usage is low, the resources stay within the AWS Free Tier at no added cost.

Figure 1 - Simplified Architecture Diagram

Fig. 1 - Simplified Architecture Diagram

Infrastructure Configuration

The overall website routes domain names using Route 53 to Cloudfront distributions for each top-level domain and domain alias (CNAME). The Cloudfront distributions are configured as a one-to-one mapping to S3 buckets for each domain. Redirection between the various domains is handled by the S3 bucket configuration. The diagram below summarizes this mapping.

Fig. 2 - Resource Routing Diagram The configuration details for each AWS service are captured in the sections below.

Route 53 / Cloudfront Mapping

Each top-level domain and alias map directly to a Cloudfront distribution. Records are defined in Route 53 to map the root (@) record and “www” record for both jdkaplan.com and jdkaplan.xyz to Cloudfront distributions. A Cloudfront distribution is defined for each of these records.

Cloudfront Configuration

A Cloudfront distribution is configured for each of the four domain records defined above. Each distribution maps to an S3 bucket which is configured to serve static content. The four distributions have minor differences in their configurations which are outlined here:

DomainDescription
Primaryjdkaplan.comMaps to the jdkaplan.com bucket which serves static web content.
Secondarywww.jdkaplan.com, jdkaplan.xyz, www.jdkaplan.xyzThese each map to an S3 bucket with corresponding names. However, it should be noted that the S3 bucket’s static website URL was used as the origin rather than selecting the S3 bucket in the provided dropdown. This was required to make forwarding work correctly.

A key challenge I encountered here was around domain forwarding errors where accessing the secondary domains resulted in an “Access Denied” error from Cloudfronta. I spent most of my time on this debugging this specific issue, so it deserves a discussion.

I finally found a solution outlined in this article but the technical reasoning for it is unclear. It appears that using the auto-filled S3 bucket name from the dropdown when defining the distribution origin results in an access denied issue when accessing the site from one secondary domains. However, using the full URL provided by S3 to access the static website resolves this issue. You MUST use the static website URL for the S3 bucket as the distribution origin for secondary domains NOT the S3 bucket that is selectable in the origin dropdown.

Error Handling for Angular Routing

Because the website is built on Angular and most routing is handled by the client-side application, it was necessary to add custom error handling behavior in Cloudfront for 403 and 404 errors. To do this, we added custom error handlers on the Error Handling tab for the jdkaplan.com distribution (it wasn’t necessary to add this to other distributions because they redirect to jdkaplan.com). In this case we add a handler for both 403 and 404. Each of them should redirect to /index.html and return a 200 status.

Other Configuration Notes

The jdkaplan.com distribution is configured with index.html as the default root object. No default root object is defined for the secondary distributions.

SSL certificates are provisioned using Amazon’s Certificate Manager. Configuration uses a step-by-step wizard and DNS verification is largely automated is using Route 53 for DNS management.

S3 Configuration

Four S3 buckets are defined: jdkaplan.com, www.jdkaplan.com, jdkaplan.xyz, and www.jdkaplan.xyz. Of these, jdkaplan.com is the primary and contains the website content. The other buckets are used for the secondary domains and redirect to the primary domain.

All of buckets are configure as static website hosts. The primary bucket contains the built Angular app and serves index.html as the default object. To allow Angular to perform client-side routing, 403 and 404 errors are redirected to index.html. Error redirection behavior is defined in Cloudfront, not S3 (see Cloudfront configuration above). The primary bucket also requires the public access be allowed and all objects in the bucket are made public.

The secondary domain buckets are empty. They are configured as static websites, but the “redirect to another domain” option is selected instead of serving this content from the buckets. The www.jdkaplan.com and jdkaplan.xyz buckets redirect to jdkaplan.com. The www.jdkaplan.xyz bucket redirects to jdkaplan.xyz, which in turn redirects to jdkaplan.com (this was simply to minimize the need for future changes if jdkaplan.com were moved or jdkaplan.xyz was used as a secondary redundant infrastructure).

Infrastructure Cost Estimation

In addition to the technical, I wanted to share my cost estimate. I think it is critical to any project to have some idea of the expected cost. While I wasn’t looking to develop a perfect estimate here, I wanted to get a rough order-of-magnitude estimate before I pursued this architecture to understand what I was committing to. The results were pleasantly surprising.

The following table breaks down the expected costs by AWS service. There are a few important assumptions to note:

Domain costs were excluded from the final result because they are billed annually. The built Angular app is ~2-2.5MB in size The website gets 100 views per month (this is a very rough guess, I have no analytics data to support this yet). It should also be noted that costs are broken out by subsystem and back-end costs are expected to remain within the free tier.

ItemSubsystemBilling UnitCost per UnitQuantityTotal Cost / mo.
Domain NamesInfrastructureDomain / year$12.002$1.001
Hosted ZonesInfrastructureZone / month$0.502$1.00
Cloudfront DistributionsFront-End$/GB (outbound tx)$0.0850.252$0.02
S3 StorageFront-EndGB$0.0230.0024$0.013
S3 Data TransferFront-End$ / 1,000 requests$0.00041$0.00043
API GatewayBack-End-Free Tier--
LambdaBack-End-Free Tier--
DatabaseBack-End-Free Tier--
Total$1.03

1 Billed annually, excluded from total 2A size estimate of 2.5MB was used for the assumed size of the website. An assumed 100 visits/mo. was used as a starting value for early usage. 3 An actual value of 0.000056wascomputedandroundeduptothenearestbillablecent.AllS3costsweregroupedtogetherandroundeduptothenearestbillablecentinthetotalsincethetotalS3costsdonotexceed0.000056 was computed and rounded up to the nearest billable cent. All S3 costs were grouped together and rounded up to the nearest billable cent in the total since the total S3 costs do not exceed 0.01. 1.03permonth!Thatsnotbad.Whatweseehereisthatthewebsite,withoutdomainnamesorDNS,onlycosts1.03 per month! That’s not bad. What we see here is that the website, without domain names or DNS, only costs 0.03/month to run. By comparison, hosting the site on an EC2 instance would cost several dollars per month. The primary cost drivers within that $0.03 are size of the built application and amount of data transferred.

Conclusion / Summary

This approach provides good scalability, no server maintenance, and global-scale performance with Cloudfront-edge caching at a very low cost. Reduced security risk and easy deployment make maintenance a much easier task. The initial configuration took some time to figure out and set up, but it’s a non-recurring effort.

Going forward, Angular allows me to write maintainable, well-structured code in TypeScript while transpiling into a static front-end deliverable through S3. I can create forms, graphs, and other dynamic pages with relative ease, but handle all this on the client-side.