Deploying Ember-CLI apps

outdated-info-warning



Outdated info warning! If you are using ember-cli to develop your ember application you should be using ember-cli-deploy instead of the workflow described in this post.

Ember-CLI is a great toolset that makes it easy to build web applications with Ember.js. However it is not that obvious how to take an application built with ember-cli and deploy it into a production environment and update it regularly.

There already exists a small section in the ember-cli docs about deployment of ember-cli applications to heroku but in this blog post I want to go into further detail in how one can handle production deployments of ember apps based on ember-cli including caching assets, deployment of code updates and integration inside of bigger applicaton (e.g. a Ruby- or Node-application).

This write up is heavily based on the ideas of Luke Melia and his presentation on how to do Lightning Fast Deployment(s) of Rails-backed Javascript Apps from this years Railsconf. Please have look at the video before diving into this post it will make it much clearer what we are trying to achieve:

First of all we should talk about what the goals of our deployment workflow are. In essence we want to

  • Serve our ember-cli applications running in production-mode
  • Have a scripted workflow to handle deploying of new code to 'production'
  • Cache assets as agressively as possible
  • Have the ability to rollback changes if something goes wrong
  • Integrate into the context of a bigger application or serve our app as a static page
  • Have a workflow based on Javascript tooling (e.g. Grunt or similar)

The Idea

As already mentioned we will follow Luke Melia's guidance and deploy our javascript-assets to Amazon's Simple Storage Service (S3). S3 is pretty cheap for hosting static assets and integration with Amazon's Cloudfront-CDN Service is also really easy to do. Our bootstrap index.html will be stored in a Key-Value store and served via a Backend-Service. We could of course also serve the index.html-file directly from S3 but jumping the hoop through a server-backend we control enables us to build a preview mechanism for code changes, gives us the possibility to inject additional meta information like user's access tokens, handle a/b-testing of components and provides us with a way to rollback deployments more easily if we screw up a deployment.

The necessary deployment steps are:

  1. Build the application.
  2. Upload all necessary assets to S3.
  3. Upload bootstrap index.html to a key-value store.
  4. Activate the newly uploaded index.html as the version that all our users will get served by default when visiting our app.

This Guide will walk you through building up a Gruntfile that automates this whole process. After we are finished with writing our Gruntfile I will also go into detail on how to integrate the whole thing into our server code base that serves html to our users.

Building the application

Ember-CLI already comes with a task to build our application in a production ready state. Just run ember build --environment production and Ember-CLI will put all the files you need into the dist-folder of your application.

I assume you are using S3 in combination with a CDN so don't forget to add prepend-property to your fingerprinting section in ember-cli's Brocfile. This will prepend your CDN-URL to all fingerprinted assets.

var EmberApp = require('ember-cli/lib/broccoli/ember-app');

var app = new EmberApp({  
  fingerprint: {
    prepend: 'https://<id>.cloudfront.net/'
  }
});

// ...

module.exports = app.toTree();

Automating this process via Grunt is straight forward. If you are not familiar with Grunt you can have a look at the Grunt Getting Started guide but I assume some experience with using the tool. Our Gruntfile could look like this:

module.exports = function(grunt) {  
  grunt.initConfig({
    shell: {
      build: {
        command: 'ember build --environment production'
      }
    }
  });

  grunt.registerTask('default', ['shell']);

  grunt.loadNpmTasks('grunt-shell');
}

We are using the grunt-shell command to run the ember build-command. So now everytime we run grunt or grunt shell:build grunt will run the ember build-command for us and store the application files in the dist-folder. Great!

Uploading to S3

I also won't go into detail how to setup a bucket in Amazon's S3-Service. But assuming you have a bucket where you can upload your assets we can use Grunt to automate syncing assets from your development machine to the cloud®.

At first I created a json-file that holds the necessary credential information. We don't want to have our access secrets in our Gruntfile because the Gruntfile will be checked into source control. We can then use the readJSON-functionality in the grunt.file-namespace to access the information:

{
  "key": "<your key>",
  "secret": "<your secret>",
  "bucket": "<your bucket name>"
}

We will use the grunt-aws-plugin to upload to S3:

var EXPIRE_IN_2030 = new Date('2030');  
var TWO_YEAR_CACHE_PERIOD_IN_SEC = 60 * 60 * 24 * 365 * 2;

module.exports = function(grunt) {  
  grunt.initConfig({
    aws: grunt.file.readJSON('grunt-aws.json'),
    // ...
    s3: {
      options: {
        accessKeyId: '<%= aws.key %>',
        secretAccessKey: '<%= aws.secret %>',
        bucket: '<%= aws.bucket %>',
        headers: {
          CacheControl: TWO_YEAR_CACHE_PERIOD_IN_SEC,
          Expires: EXPIRE_IN_2030
        },
      },
      build: {
        cwd: 'dist',
        src: 'assets/**'
      }
    }
  });

  grunt.registerTask('default', ['shell', 's3:build']);

  grunt.loadNpmTasks('grunt-shell');
  grunt.loadNpmTasks('grunt-aws');
}

Running s3:build-task will now upload everything in the dist/assets-folder to S3. We also set the correct Cache-Headers for the fingerprinted files to cache as agressively as possible. The grunt-aws-plugin is quite clever and only uploads files that don't exist yet on S3. This makes for pretty fast uploads when only changing application code and not adding a lot of other assets like images etc.

Upload the bootstrap-index file

The next thing we need to do is to upload our bootstrap index.html to a key-value store. Like Luke Melia we will be using redis for this. You could of course use any other Key-Value store if you like.

To upload the bootstrap file to redis we will use my grunt-rt-deployinator-plugin. I looked around npm for some time to find a plugin that already uploads html files to redis and achieves what we need to do. After some time I settled with writing my own plugin because I wanted to use a tested project and I did not find a project that had tests. The grunt-rt-deployinator-plugin uses my deployinator-plugin internally. If you wanted to use another key-value store you could add an adapter for your specific store in this project.

As with the s3-task we move the necessary config values into an external json-file:

{
  "manifest": {
    "manifest": "firstiwaslike",
    "manifestSize": 10
  },

  "redis": {
    "development": {
      "host": "localhost",
      "port": 6379
    },

    "staging": {
      "host": "<your staging host>",
      "port": 6379
    },

    "production": {
      "host": "<your production host>",
      "port": 6379
    }
  }
}

grunt-rt-deployinator gives us a few additonal tasks we can use in our Gruntfile. We can use the upload-task to upload our bootstrap index-file.

// ...
var deployConfig = require('./deploy.json');  
var manifestSettings = deployConfig.manifest;

module.exports = function(grunt) {  
  var environment = grunt.option('environment');

  if (typeof environment === 'undefined') {
    environment = 'development';
  }

  var redisConfig = deployConfig.redis[environment];
  grunt.initConfig({
    // ...
    upload: {
      options: {
        storeConfig: redisConfig
      },
      bootstrapIndex: {
        options: manifestSettings,
        files: [
          { src: 'dist/index.html' }
        ]
      }
    },
    listUploads: {
      options: {
        storeConfig: redisConfig
      },
      dist: {
        options: manifestSettings
      }
    },
    deploy: {
      options: {
        storeConfig: redisConfig
      },
      dist: {
        options: manifestSettings
      }
    }
  });

  // ...
  grunt.loadNpmTasks('grunt-rt-deployinator');

}

We added the idea of 'environments' to our deploy.json and to our Gruntfile. If we run the upload task and specify an environment Grunt will use the appropriate configuration values from deploy.json. For example:

grunt upload --environment=production

Per default it will run every command with the development configuration.

Assuming you have redis running locally you can now run grunt upload:bootstrapIndex and your index.html-file will be uploaded to your local redis instance.

» grunt upload

Running "upload:bootstrapIndex" (upload) task  
>> Upload: firstiwaslike:b8c52ba successful!

With grunt-rt-deployinator you can also have look what manifests you currently have uploaded to redis (it uses the current GIT-SHA as an identifier what version of index.html is deployed).

» grunt listUploads                                

Running "listUploads:dist" (listUploads) task  
>> Last 10 deploys:
>>
>> |    Git-SHA
>> |
>> |    firstiwaslike:b8c52ba
>>
>> # => - current

Done, without errors.  

As you can see we have revision firstiwaslike:b8c52ba uploaded to redis. We can now access the index file via redis-cli.

» redis-cli                                          
127.0.0.1:6379> GET firstiwaslike:b8c52ba  
(The content of your index.html page)

We will access the file directly via our preview functionality from our server backend later in this blog post.

Activating a specific bootstrap index

grunt-rt-deployinator also provides a task for activating (deploying) a specific bootstrap index. This assumes you are using a server-backend that will serve your users the bootstrap index file stored under <your-manifest-name>:current in redis (More on that later).

After specifing the deploy-task in your Gruntfile you can run:

» grunt deploy --key=firstiwaslike:b8c52ba           
Running "deploy:dist" (deploy) task  
>> Deploy successfull!
>> Run `grunt listUploads` to see which revision is current

Done, without errors.  

We can do as grunt-rt-deployinator tells us and look at the output of grunt listUploads. It will show us which revision is currently set to the current revision. I uploaded a second revision to make it clearer what grunt-rt-deployinator can do:

» grunt listUploads                                   
Running "listUploads:dist" (listUploads) task  
>> Last 10 deploys:
>>
>> |    Git-SHA
>> |
>> |    firstiwaslike:345508c
>> | => firstiwaslike:b8c52ba
>>
>> # => - current

Done, without errors.  

We have now created a Gruntfile that gives us all the necessary tasks to automate the deployment of an ember-cli application.

We have used the grunt-rt-deployinator-plugin to handle the deployment of a bootstrap-index-file to a key-value-store. The last part which is missing is how to integrate this into a real world application and a quick walkthrough how a real world deployment would look like with grunt-rt-deployinator.

Building the Backend

To make our lives as easy as possible I created a small sinatra application that serves our bootstrap index-files:

require 'sinatra'  
require 'redis'

def bootstrap_index(index_key)  
  redis = Redis.new
  index_key ||= redis.get('firstiwaslike:current')
  redis.get(index_key)
end

get '/' do  
  content_type 'text/html'
  bootstrap_index(params[:index_key])
end  

This is very simple. We will serve our ember-cli application on / when connecting to our sinatra application. As default behaviour sinatra will serve the revision that is currently marked as firstiwaslike:current. If we provide a query-parameter called index_key however and pass it the value of another uploaded revision sinatra will serve this revision instead.

This is really useful in real world scenarios because it gives you a way to upload production code, test it out on production by adding the index_key-parameter to your requests and then activate the newly uploaded revision whenever you feel that everything is working correctly. This creates a really relaxed workflow for production deployments.

Yeah! Great success! Internet High Five!
InternetHighFive

Of course rolling back changes is as easy as just marking an old revision that you know works as the current revision again.

» grunt listUploads                                                       
Running "listUploads:dist" (listUploads) task  
>> Last 10 deploys:
>>
>> |    Git-SHA
>> |
>> | => firstiwaslike:b9c0854
>> |    firstiwaslike:6538e48
>> |    firstiwaslike:345508c
>> |    firstiwaslike:b8c52ba
>>
>> # => - current

Done, without errors.

» grunt deploy --key=firstiwaslike:6538e48                                                                 
Running "deploy:dist" (deploy) task  
>> Deploy successfull!
>> Run `grunt listUploads` to see which revision is current

Done, without errors.

» grunt listUploads                                                       
Running "listUploads:dist" (listUploads) task  
>> Last 10 deploys:
>>
>> |    Git-SHA
>> |
>> |    firstiwaslike:b9c0854
>> | => firstiwaslike:6538e48
>> |    firstiwaslike:345508c
>> |    firstiwaslike:b8c52ba
>>
>> # => - current

Done, without errors.  

Wrapping up

That's it basically. With this workflow you can easily automate your ember-cli application deployments with grunt and as a bonus preview deployments on your production site and easily rollback deployments if something went wrong. I will update the documentation of both the deployinator- and grunt-rt-deployinator-projects in the coming week so it is easier for you to integrate them in your workflow if they seem useful to you. And please don't hesitate to file issues if you encounter any bugs.

If you found this post useful or have any questions feel free to comment below. You can of course also contact me via Twitter via my handle @levelbossmike.

comments powered by Disqus