I thought I’d write about how fanriff.com serves its Ember.js index from its Sinatra.rb API, and how web clients interacts with the back-end once the application is loaded. It started to run a little long so I’ve broken it up into parts. This first chapter will cover some background information on Ember, the fanriff.com web architecture, and how we deploy our Ember index to our stack.
- Site only “loads” once; actions taken by the user modify the DOM without triggering page refreshes
- Data is retrieved asynchronously via XMLHttpRequest (XHR) calls, and asynchronously-changing data can automatically update the DOM
- Interactivity and responsiveness more closely resemble the user experience enabled by “native” desktop and mobile apps (at least in theory)
- Computationally-expensive operations—such as sorting and filtering complex collections—burn the user’s CPU cycles instead of your own, making it easier and cheaper to scale to more users (although this can be considered a disadvantage for slower devices)
- Overall data transfer is less because a web client typically only needs to download the assets (images, scripts, templates, and stylesheets) once, at boot time, and JSON-encoded data is typically smaller than rendered HTML containing the same content
- Server-side APIs can be shared between web, iOS, Android, and other clients
fanriff.com’s stack, simplified, looks something like this:
- HAproxy “Load Balancer”
- TLS termination
- DoS protection
- Load-balancing reverse proxy (to Varnish)
- Varnish “Web Application Accelerator”
- Content caching and compression
- Load-balancing reverse proxy (to Puma)
- Puma “Ruby Web Server”
- Rack application server
- Sinatra.rb RESTful APIs
Static assets (images, scripts, templates, and stylesheets), with the exception of the Ember index, are served via Amazon.com’s CloudFront content delivery network (CDN) from an S3 bucket. We use MariaDB and Memcached as our backing stores, but those are outside the scope of this write-up!
Ember, First Boot
With that in mind, here is the request/response flow that fanriff.com implements to serve its index page:
- User requests fanriff.com, and the web client looks up the IP address of our edge and validates the TLS certificate
- If the user goes to http://fanriff.com, http://www.fanriff.com, or https://www.fanriff.com, our edge will redirect them to https://fanriff.com
- If the user has previously visited fanriff.com, the web client will know (via a previously-seen Strict-Transport-Security header) to only load the secure version of the site
- If the user has previously visited fanriff.com, the web client will know (via a previously-seen Public-Key-Pins header) to only accept certain TLS certificates
- Our edge serves up our certificate with a strict subset of TLS protocols and ciphers, a time-stamped OCSP response, protocol negotiation, and with TLS compression and session tickets disabled (for security reasons)
- Our edge also implements TLS session caching (to speed up subsequent requests), and basic DoS protection (to prevent our site from being easily overwhelmed by a bad actor)
- Our edge passes the request on to our caching layer, which rewrites the URI of the request to “/” (unless it’s an XHR call, which we’ll discuss in the next chapter), and looks to see if the Ember index is in its internal store
- If the user has previously visited fanriff.com, the web client will send a If-None-Match header on the request, and our caching layer will compare the value of this header to the cached content’s ETag and respond with a 304 status code if they match
- If it’s not available in its internal store, our caching layer passes the request on to an application server, which pulls the current Ember index out of an external key-value store and extracts any http-equiv meta tags to present as first-class response headers (we’ll discuss the why of all this in the next section)
- Our caching layer compresses the response and adds it to its internal store if it’s new, and inspects the Accept-Encoding header on the original request to determine if it should serve compressed or uncompressed content
- Our edge adds up-to-date Strict-Transport-Security and Public-Key-Pins headers to the outgoing response
At this point, you’re probably wondering why on Earth we’re serving the index out of a key-value store. Why not treat it like any other asset and serve it from the CDN? Or why not serve it from Sinatra as a static page? Why go through all this rigamarole? Well, the reasons are pretty straightforward:
- We want to serve it from the same domain as the API
When you visit a site, its domain name becomes the “origin” for all future requests involving that site. Due to the web’s same-origin policy, certain restrictions apply if you try to make requests from one domain to another, e.g. from example.com to api.example.com (and yes, subdomains are considered different origins). Specifically, a non-GET, cross-domain XMLHttpRequest (XHR) call requires a preflight OPTIONS request that responds with an Access-Control-Allow-Origin (ACAO) header, and the header must include the origin (or a matching wildcard) attempting the call. This is called cross-origin resource sharing (CORS), and it’s no fun at all. Additionally, any cookies must be set with wildcard domains (e.g. “.example.com”) if you want to be able to access their contents from both the client and the server, which decreases cookie security and complicates traceability (i.e. it makes it more difficult to answer the question “what set this cookie?”).
You can avoid the whole bugaboo by serving the index from the same domain as the API. You won’t need preflight OPTIONS requests or ACAO headers, and you can set a single, non-wildcard domain on your cookies. (We’ll still have to deal with CORS later to be able to load assets from our CDN, but we’re not making non-GET XHR calls to or setting cookies on the CDN so it’s much, much simpler.)
- We want to deploy it quickly and autonomously
- We want to preview new versions before they go live, and easily revert if something goes wrong
Another benefit of having the Ember index and its past versions in a key-value store is that we can quickly and easily revert a change if something goes wrong, and we can stage future versions of it in there as well and preview them with a URL parameter.
- We want to present http-equiv meta tags as first-class response headers
Finally, there are some response headers that are managed by Ember and presented as http-equiv meta tags, but some of the features of these headers can only be enabled if they are presented as first-class response headers. Specifically, Ember can generate your Content-Security-Policy (or Content-Security-Policy-Report-Only) header and include it in your index as an http-equiv meta tag, but you can’t specify the report-uri or frame-ancestors directives in a meta tag, per the specification. This solution lets you have the best of both worlds: Ember manages it, and Ruby “upgrades” the meta tag(s) to real headers when serving the page. (We’ll talk more about Content-Security-Policy in the next chapter.)
This potentially opens up the ability for the front-end development team to manage other response headers, but with great power…
To sweeten the deal, the cost of achieving all of the above “wants” is quite minimal. We already have a big, beautiful stack for the API that includes TLS termination, caching, and a Ruby application server. Furthermore, Ember has a wonderful addon (and associated plugins) called ember-cli-deploy that works hand-in-hand with Ember’s build system to fully automate the process of deploying your assets to S3 (or other object stores) and your index to Redis (or other key-value stores). The documentation even includes sample Ruby (and Node.js) code for retrieving and serving the index; I’ve made my version, which includes logic to set the ETag and extract http-equiv meta tags, available in a Gist (and here’s one if you’re using MySQL instead of Redis). It doesn’t get any easier than this!
It’s worth listing in full at this point everything that Ember takes care of at deploy time; all you have to do (once you’ve configured your plugins) is type
ember deploy production and you’re off:
- Compiling, minimizing, and compressing assets (your code and any third-party code you’ve imported into your application, including Ember itself)
- Fingerprinting assets (so multiple versions can coexist in S3, which plays nicely with the revert and preview feature discussed above)
- Rewriting assets so links to other assets include any CDN URL and reflect the fingerprinted filename (e.g.
<img src="https://example.cloudfront.net/assets/logo-0da0b89.pnginstead of
- Uploading assets to S3
- Hashing the final script and stylesheet files and adding subresource integrity attributes to
<linktags in the index page (we’ll talk more about subresource integrity in the next chapter)
- Generating the Content-Security-Policy header and adding the http-equiv meta tag to the index page
- Uploading the new revision of the index page to Redis
- Optionally activating the new revision of the index page in Redis (or this can be done separately at a later time)
- Dispatching any notifications or other tasks important to your workflow (e.g. sending a message to a Slack channel or triggering cache invalidation)
Not to sound too much like I’ve drank the Kool-Aid, but it’s pretty amazing when it all comes together.
The web client has established trust mechanisms with our edge and obtained a copy of the index, and so it is free to continue booting the site, which includes the following steps:
- Validate the Content-Security-Policy
- Retrieve assets from the CDN with cross-origin resource sharing
- Validate script and stylesheet hashes against subresource integrity attributes
- Establish or resume a session (or something session-like) with the back-end
I’ll be covering these topics (as well as how XHR calls are routed through our stack) in the next chapter of this write-up. Thank you for reading, and please let me know in the comments if you have any questions!