Getting Started with Ember CLI Deploy and Lightning Pack

So you’re sold on The Lightning Strategy for deployment and its promise of fast, zero-downtime deploys and you’re ready to get started. Perfect. This article will show you, step by step, how to implement it.

Readers new to The Lightning Strategy: If fast, zero-downtime deploys have caught your interest, check out Luke Melia's RailsConf talk and come back when you're ready to get started!

Step 1: Install Ember CLI Deploy

ember install ember-cli-deploy  

Ember CLI Deploy is a deployment pipeline. It provides hooks for plugins to do perform individual deployment tasks.

Installation will generate a new file at config/deploy.js for you. That’s a sample deploy config file. But before we look at that, you’ll want to install some plugins or, in our case, a plugin pack.

Step 2: Install Lightning Pack

ember install ember-cli-deploy-lightning-pack  

Ember CLI Deploy Lightning Pack installs all the plugins needed to implement The Lightning Strategy. As a refresher, that means: plugins to build, compress and upload only modified assets to S3, upload index.html to Redis, and list and handle revisions.

During installation you'll be asked whether you want to overwrite config/deploy.js. Do overwrite as it'll replace the original with one customized for Lightning Pack.

With Ember CLI Deploy and Lightning Pack now installed, it's time to configure your deploy.

Step 3: Configure Your Deploy

While the deploy config generated by Lightning Pack is certainly thoughtful, at Isle of Code we found it a bit unconventional.

We prefer using conventional deploy target names like development and production, enforcing the convention of specifying the Redis URL through Ember CLI Deploy's built-in dotEnv support, previewing new deploys on production rather than in a QA or staging environment, and activating development revisions on deploy.

With that said, at Isle, our config/deploy.js at this point looks like this:

const VALID_DEPLOY_TARGETS = [  
  'development',
  'production'
];

module.exports = function(deployTarget) {  
  var ENV = {
    build: {},
    pipeline: {},
    redis: {
      url: process.env.REDIS_URL,
      allowOverwrite: true,
      keyPrefix: 'your-app:index'
    },
    s3: {
      prefix: 'your-app'
    }
  };
  if (VALID_DEPLOY_TARGETS.indexOf(deployTarget) === -1) {
    throw new Error('Invalid deployTarget ' + deployTarget);
  }

  if (deployTarget === 'development') {
    ENV.build.environment = 'development';
    ENV.pipeline.activateOnDeploy = true;
    ENV.plugins = ['build', 'redis'];
    ENV.redis.revisionKey = 'development';
  }

  if (deployTarget === 'production') {
    ENV.build.environment = 'production';
    ENV.s3.accessKeyId = process.env.AWS_KEY;
    ENV.s3.secretAccessKey = process.env.AWS_SECRET;
    ENV.s3.bucket = 'your-bucket';
    ENV.s3.region = 'your-region';
  }

  return ENV;
}

It's not important to follow our approach but be sure to set your S3 Bucket and Region in config/deploy.js.

Next you'll need to update ember-cli-build.js to enable fingerprinting and add the base URL to prepend for assets for both your development and production environment.

var env = EmberApp.env() || 'development';

var fingerprintOptions = {  
  enabled: true
};

switch (env) {  
  case 'development':
    fingerprintOptions.prepend = 'http://localhost:4200/';
  break;
  case 'production':
    fingerprintOptions.prepend = 'https://amazonaws.com/bucket/';
  break;
};

var app = new EmberApp(defaults, {  
  //...

  fingerprint: fingerprintOptions
});

And then create a .env.deploy.production file in your project root to store sensitive environment specific data like your AWS Key and Secret, and Redis URL.

For the example above, a sample .env.deploy.production would look like this:

AWS_KEY=XXXXXXXXXXXXXXXXXXXX  
AWS_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX  
REDIS_URL=redis://:password@host:1234  

Heroku Users: Due to a bug in ember-cli-deploy-redis 0.1.1, a Heroku Redis URL containing a username placeholder will raise the following error: Unhandled rejection Error: ERR invalid DB index error to the console even though the Redis deploy succeeded. Until the bug is resolved simply remove the username placeholder from Heroku Redis URLs like above.

A sample .env.deploy.development would look like this:

REDIS_URL=redis://localhost:6379  

Your deploy is now configured. But before you can perform one you'll need to configure your API to serve your index.html from Redis.

Step 4: Configure Your API to Serve index.html from Redis

While it's not a requirement to serve index.html from your API, doing so removes the need for CORS.

To serve index.html from Redis, you'll need to update the code that currently serves your index. Below, the examples use Rails but should be easily adapted to your frame work of choice.

class RootController < ApplicationController  
  def index
    render text: html
  end

  private

  def html
    $redis.get "your-app:index:#{current_revision_key}"
  end

  def current_revision_key
    $redis.get 'your-app:index:current'
  end
end  

Note: The example above assumes a Redis connection set up in an initializer and stored in $redis.

Now with your API configured to serve your index.html from Redis, you're ready to deploy.

Step 5: Deploy to Development

With both your local Redis and Rails Server running, run:

ember deploy development  

You should receive a message telling you the development revision has been updated. And if you visit the route for the controller you'll find you index.html now being served from Redis.

Step 6: Deploy to Production

To ensure zero-downtime while switching to serving index.html from Redis. Run the following before deploying your updated API to ensure there's an index.html already in Redis:

ember deploy production  

You should receive a message telling you the revision has been deployed but not activated along with a command to activate it. Go ahead and activate it. Then deploy your API to production.

When it's finished deploying it will now be serving your index.html from Redis and future Ember deploys will be lightning fast.

Bonus: Improve Your Production Deploy Activation Flow

Now that your deploys are lightning fast, wouldn't it be nice to be able to preview your revisions before activating them? With just a couple of changes we can add that.

First you'll need to update your Rails app to accept a revision_key param and use it instead of the current revision key if provided. And while we're at it, raise an error if the key can't be found.

class RootController < ApplicationController  
  def index
    render text: html
  end

  private

  def html
    revision or raise revision_not_found
  end

  def revision
    $redis.get "your-app:index:#{revision_key}"
  end

  def revision_key
    params[:revision_key] || current_revision_key
  end

  def current_revision_key
    $redis.get 'your-app:index:current'
  end

  def revision_not_found
    ActiveRecord::RecordNotFound.new(
      "Cannot find revision with key: #{revision_key}"
    )
  end
end  

Deploy your Rails app now and the next time you deploy to production you'll be able to copy the unactivated revision key as a param and preview it. But wouldn't it be nicer if Ember CLI Deploy returned a URL for you to copy and paste instead? That's also a quick change.

Simply open config/deploy.js and add this to your production deploy configuration and be sure to set indexUrl to the URL where your index is served:

ENV.redis.didDeployMessage = function(context){  
  const INDEX_URL = 'https://example.com';
  const REVISION_KEY = context.revisionData && context.revisionData.revisionKey;
  const ACTIVATED_REVISION_KEY = context.revisionData && context.revisionData.activatedRevisionKey;
  if (REVISION_KEY && !ACTIVATED_REVISION_KEY) {
    return `Deployed but did not activate revision ${REVISION_KEY}.\n` +
         `- To preview, visit: ${INDEX_URL}?revision_key=${REVISION_KEY}\n` +
         `- To activate, run: ember deploy:activate ${context.deployTarget} --revision=${REVISION_KEY}`;
  }
};

Now when you run ember deploy production you'll receive a post-deploy message with a preview URL like this:

- Deployed but did not activate revision XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.
- To preview, visit: https://example.com?revision_key=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
- To activate, run: ember deploy:activate production --revision=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Extra Bonus: Use Ember CLI Deploy with Your Development Workflow

During development, if you find yourself having to run ember deploy development constantly because your API needs to inject some initial state into your Ember app or because some pages are served by your API, Ember CLI Deploy can also be used for your development workflow.

With the Ember CLI Deploy development workflow, when running ember server, index.html will be pushed to to Redis whenever it’s changed. It's also simple to set up.

First update ember-cli-build.js to run a development deploy on build:

var app = new EmberApp(defaults, {  
  //...
  emberCLIDeploy: {
    runOnPostBuild: (env === 'development') ? 'development' : false,
    configFile: 'config/deploy.js',
  }
});

Then update config/deploy.js to set your development deploy to use just the Redis plugin since ember server has already built your app and set the Redis distribution directory:

if (deployTarget === 'development') {  
  //...
  ENV.plugins = ['redis'];
  //...
  ENV.redis.distDir = function(context) {
    return context.commandOptions.buildDir;
  };
}

And finally update .ember-cli to set the Live Reload Base URL to point to your Ember server rather be relative to your index.html:

"live-reload-base-url": "http://localhost:4200/"

Now when running ember server each change to index.html will be automatically pushed to Redis and reloaded with Live Reload!

Happy deploying and developing!

Jordan Yee

Jordan (@jordan_yee) is a Ruby and Javascript developer. Before Isle, he was a developer at Unspace and head of software and services at Healthsphere. In his spare time he meditates on design.

Toronto
About Isle of Code:

We’re a Javascript/Ember firm in Toronto + Chicago, est. 2012. We provide on-demand development for Mobile Apps, Websites, iBeacons/ hardware, existing codebases and team augmentation / Ember leadership.

read more