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!