Using a CDN to Deploy Vue.js and Flask

Using a CDN to Deploy Vue.js and Flask

Recently we added a UI on top of one of our machine-learning (ML) automation tools to give the ML team greater visibility into the inner workings of its pipelines. The original goal was to keep this UI as simple and straightforward as possible — we added a couple Flask endpoints to expose relevant data and used Bootstrap and jQuery to display everything — but over time, and with all the cool new CLI features, we’ve opted to rework that UI with Vue.js and our internal design library Anodyne that’s used across the rest of PathAI’s applications.

This migration certainly improved the users’ experience, but it also helped us separate out the complicated deployment process of the backend with the expanding frontend. We could keep the existing Flask application and create a separate repo for the new Vue.js UI, complete with all of the testing frameworks and consistent design components that we didn’t have for the old UI.

The finished product!

This post describes how we set up a Vue.js frontend, coupled it with a Flask backend, and continue to manage the relevant static assets with AWS S3 and Amazon CloudFront.

Getting started

To have our Flask app serve up the Vue.js app, we needed to do a couple things:

  1. Use Webpack to bundle the Vue.js app into css/ and js/ directories that can be imported into the Flask app.
  2. Put those bundled files somewhere accessible to the Flask app, and make the retrieval of those files fast and secure.
  3. Configure the Flask app to fall back to the Vue.js router when a user accesses a page in the app.

Bundling our Vue.js application

Luckily for us, Webpack is installed automatically when you use the Vue.js CLI to set up an application, so step 1 was as simple as running:

vue-cli-service build

or

yarn run build

in the app’s directory. Webpack works as a module bundler for Javascript apps, as described in its documentation:

When Webpack processes your application, it internally builds a dependency graph which maps every module your project needs and generates one or more bundles.

What this gives us is a convenient dist/ directory that houses a bundled version of the Vue.js app in the form of CSS and JS files.

Managing static assets with AWS

In the spirit of keeping our frontend and backend decoupled, we wanted to keep these bundled files somewhere that could be updated independently of backend changes. We use a number of AWS services daily at PathAI for data storage, virtual environments, log management, etc., and we also already use AWS S3 and Amazon’s content-delivery-network service CloudFront to store and deliver static content for our client-facing products. Given the performance benefits provided by a CDN and the fact that we have existing methods in our Flask app for accessing / uploading data in S3, storing our bundled files there seemed like the most straightforward approach.

With each push to master in the Vue.js app’s repo, its CI pipeline can run tests on the app, run the Webpack build, and then push the contents of the newly created dist/ directory up to a designated bucket in S3, which will be accessible at a simple cdn.mydomain.com/<bucket-name>/<file-name>.

Just your average pipeline

Using a CDN definitely improves the time it takes to deliver static content to your browser, but one challenge in this solution was ensuring that the application is always receiving the latest version of the files. Our deploy script could have just built and replaced the bundled CSS/JS files in S3 each time we made changes to the UI, but if the Flask app is referencing the same file name, the contents of cdn.mydomain.com/<bucket-name>/main.css, for example, might remain cached on the CDN for some unforeseeable or undesirable amount of time — CloudFront defaults to 24 hours.

Older versions might also be cached in the browser if the file name remains the same. To make sure the user is always seeing the latest version of frontend code, we wrote a script that pushes up all of the CSS/JS files to S3 with unique names (like main.d708e4c2.css, generated during the Webpack build step) and includes a generated manifest.json file to identify which of the unique file names is the latest version:

{
    “css/app.css”: “css/app.27cee0c8.css”,
    “js/app.js”: “js/app.7bfe0aac.js”
}

Integrating Vue.js with Flask

From the Flask side of things, we wanted the app to pull down the latest bundled frontend code every time the backend starts up, and preferably at some frequency afterwards, too. It can do this by reading the manifest.json file in S3 and generating the full link to the correct file version. With APScheduler, this method can be called every ten minutes to update the file links when new frontend changes are pushed to master. The downside here is that the app won’t immediately notice new updates to our frontend code, but we felt that a ten-minute delay at most was acceptable; if an update needs to happen asap, we can always restart the Flask app to pull in the latest changes.

Here’s a simple version of this setup with only two files included in the manifest:

import boto3
from apscheduler.schedulers.background import BackgroundScheduler
# Update static frontend files to latest version from manifest (runs every 10 minutes)
def get_static_ui_files():
  with app.app_context():
    s3 = boto3.resource(‘s3’)
    manifest = s3.Object(‘bucket-name’, ‘manifest.json’).get()[‘Body’].read().decode(‘utf-8’)
    manifestJSON = json.loads(manifest)
    current_app._static_js = manifestJSON[‘js/main.js’]
    current_app._static_css = manifestJSON[‘css/main.css’]
get_static_ui_files()
scheduler = BackgroundScheduler()
scheduler.add_job(func=get_static_ui_files, trigger=’interval’, minutes=10)
scheduler.start()

The last step is where we tell Flask to defer to the Vue.js router when users try to access the app. If a user enters a URL that doesn’t match any static assets or existing endpoints in the Flask app, a “catch-all” fallback route will serve up an index.html page including the CSS and JS files from our Vue.js app. We can use Flask’s render_template method to pass in the bundled file names we retrieved from the manifest:

@app.route(‘/’, defaults={‘path’: ‘’})
@app.route(‘/<path:path>’)
def catch_all(path):
  with app.app_context():
    static_js = getattr(current_app, ‘_static_js’, None)
    static_css = getattr(current_app, ‘_static_css’, None)
    if (static_js is None or static_css is None):
      get_static_ui_files()
    return render_template(
      ‘index.html’,
      staticJS=current_app._static_js,
      staticCSS=current_app._static_css)

And then our index.html template pulls in the static content:

<!DOCTYPE html>
<html lang=en>
  <head>
    <title>UI</title>
    <link href=”https://cdn.mydomain.com/{{ staticCSS }}” rel=stylesheet>
  </head>
  <body>
    <div id=app></div>
    <script src=”https://cdn.mydomain.com/bucket-name/{{ staticJS }}”></script>
  </body>
</html>

The result is a spiffy new UI that’s consistent with the other Vue-based applications we build for our customers, significantly simpler to manage as a growing application, and much easier to navigate for the ML engineers who work with it every day. 🎉