meshBlog

Cloud native deployment for Single Page Applications

By Johannes Rudolph30. January 2018

Single Page Applications (SPAs) are a popular way to deliver modern web apps. With SPAs, users download executable Javascript code that dynamically renders content in their browser, rather than retrieving pre-rendered pages from a server application. The server application is freed from rendering HTML and instead is only responsible for providing a re-usable API to its data. In this post, we're going to look at how we can leverage Cloud Foundry to do cloud native deployments for SPAs.

Delivering SPAs on Cloud Foundry

To deliver an SPA to its users, a web server only needs to serve the static assets making up the SPA. This also means that the web server can leverage HTTP caching to optimize delivery. On Cloud Foundry, the best way to serve an SPA is by using the Staticfile buildpack. A cf push using this buildpack will bundle your artifacts with an nginx server. To get started, let\'s assume you have all files of your web-root in your current working directory. We now create a short manifest.yml for Cloud Foundry:

---
applications:
- name: my-spa
  instances: 1
  memory: 64M
  buildpack: https://github.com/cloudfoundry/staticfile-buildpack.git

All we need to do now is to execute cf push and your SPA will be up and running in the cloud under a randomly-assigned URL shortly.

Scaling your SPA

The nginx container built for serving your files does not need a lot of memory, which makes it very cost-efficient to run on Cloud Foundry. To achieve high-availability and scale the service under load, we can easily use Cloud Foundry to scale it to two or more instances using cf scale my-spa -i $X.

Injecting Configuration

Cloud native applications should adhere to the 12-factor principles. One of the twelve factors is configuration through the environment. This allows us to deploy the same artifacts to different environments (e.g. dev and production) while keeping all configuration data separately. Cloud Foundry optimally supports workloads that implement this principle by providing configuration through environment variables.

At runtime, we need to inject this configuration data into the application. Because we want to keep serving our application as static SPA assets through nginx for optimum performance, there\'s no server side code execution like with PHP or a different backend language. However, we can still achieve dynamic injection of configuration data through the use of environment variables and server side includes.

The staticfile buildpack has a lot of useful configuration options. The most powerful of course is that it allows us to provide our own template for nginx.conf, which is evaluated during staging. In this template, we can access environment variables configured for the app on Cloud Foundry. For reference, here\'s the default config used by the buildpack.

For our custom template, we\'ll start with this portion of an nginx.conf file:

server {
  listen <%= ENV["PORT"] %>;
  server_name localhost;

  <% if ENV["INJECT_ENVIRONMENT"] %>
  location /inject-environment {
    default_type application/json;
    return 200 '<%= ENV["INJECT_ENVIRONMENT"] %>';
  }
  <% end %>

location / {
  root <%= ENV["APP_ROOT"] %>/public;
  index index.html index.htm Default.htm;
  ssi on;
 }
}

This creates a location mapping in nginx for the path /inject-environment that will respond with HTTP Status Code 200 OK and a static string read from the INJECT_ENVIRONMENT environment variable. This is useful if we can buid our SPA to retrieve this configuration data at runtime. But what if we want to load configuration data before the JavaScript in our application executes?

We can leverage an HTML feature called server-side-includes for this, which we enabled using the ssi on instruction in the nginx.conf above. In the index.html document that loads the javascript code of our SPA, we add a SSI instruction to include the string returned by the /inject-environment endpoint:

<!--#include virtual="/inject-environment" -->

Because a server-side-include does just plain text concatenation, we need to define our environment variable to be a "smart" string. So let\'s make it a <script> tag that executes some javascript that will put a JSON object into the global window. The easiest way to define this variable is to edit our app\'s Cloud Foundry manifest file:

---
applications:
- name: my-spa
  instances: 1
  memory: 64M
  buildpack: https://github.com/cloudfoundry/staticfile-buildpack.git
  env:
    INJECT_ENVIRONMENT: |
      <script type="text/javascript">window["INJECTED_ENVIRONMENT"] = {
        production: false,
        baseUrls: {
          api: "https://api.example.com"
        }
      };</script>

To access configuration data, your SPA can simply pick it up from the global window["INJECTED_ENVIRONMENT"] variable.

Pro tip: To dynamically reconfigure your application on Cloud Foundry at runtime, change your environment variables and restage the application. To stage the new version of the application while keeping the current one available, you can use the bg-restage cli plugin.

Monitoring

Cloud Foundry automatically collects your application\'s requests as they pass through the Cloud Foundry router. Entries like this are tagged with [RTR/$] in the logs. Additionally, Cloud Foundry will also collect all logs emitted by nginx, indicated by the [APP/$] tag in the logs. Here\'s how that looks like in practice:

Retrieving logs for my-spa panel in org meshstack / space production as demo@meshcloud.io...

2018-01-28T15:46:59.99+0100 [APP/PROC/WEB/0] OUT 95.222.25.157, 172.16.105.69 - https - - - [28/Jan/2018:14:46:59 +0000] "GET / HTTP/1.1" 200 1044
2018-01-28T15:46:59.99+0100 [RTR/0] OUT panel.meshcloud.io - [28/01/2018:14:46:59.994 +0000] "GET / HTTP/1.1" 200 0 1032 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36" 172.16.105.69:38656 x_forwarded_for:"95.222.25.157, 172.16.105.69" x_forwarded_proto:"https" vcap_request_id:908529e7-3f5a-4a5c-40c2-d79419a3e5ae response_time:0.003193066 app_id:bc1e736b-6b8c-48b0-a390-8c8ac1aeae0a app_index:0

You can customize this logging in your nginx.conf file using the log_format configuration option.