What is this all about?
This is the story of how we changed our Ember.js application deployment one night while no one was looking. This is not a strictly technical story, and the details may not matter; what is important is the journey we took and the steps you can follow to get your app deploying however you want.
Some Background (feel free to skip)
Our application - Ilios - is an installed solution and the code is in two parts. The API and
management code is written in PHP and is installed by medical schools all over the world. Each installation is a little
bit different, but they all require and consume our Ember.js frontend application. We don't ship the frontend with the
API codebase, it is provide separately and updated far more frequently. In the past we have shipped and index.html
file
with links to our AWS Cloudfront distribution containing the assets. This way the assets are stored in a CDN where
they can be loaded quickly by users and easily kept up to date by our team without needing to understand each
individual installation.
Our Goals (the whole point of this)
In order to take advantage of http/2 push and service workers we need to change the way the frontend is deployed. Our goal is to ship a single archive file containing the frontend assets including the app code, service workers, images, styles etc…
This archive must be tagged in such a way that we can have many versions in the wild at the same time with a default active version. Because the code for our API does change it must be possible to activate versions in such a way that v23 is the default frontend for API v1.1 and v34 is the default frontend for API v1.3.
Once the archive is downloaded and extracted by the API server it must be simple to parse and customize the files so that
scripts and styles can be sent to the browser using http/2 push LINK
headers and service workers can be placed at the
root of our domain (where they must be do be effective).
Because we are constantly adding new features and fixing bugs we need to be able to release versions continuously.
TLDR
We want to ship a magical box to our customers and we want to ship it all the time.
Our Journey
Starts with a plan on a whiteboard
Distribute
- Build the app
- Archive the build artifacts
- Tag the archive with a version which combines both a unique ID for the code as well as the API version it works with
- Upload the archive to an S3 bucket
- Mark the most recent version so it is easy to find
Consume
- Download and extract the archive at
- a) the most recent version
- b) a specific version from the past
- c) dev build we want to test
- Parse the the index file to extract configuration and assets
- Serve a modified
index.html
file to users
Leads to Ember-cli-deploy
This isn't a surprise ember-cli-deploy
has both the builtin tools and a plugin ecosystem to make deploying Ember.js apps
very manageable. All we need to do is assemble a list of plugins that can meet our needs.
As is the case with many ember addons, our journey really starts with Ember Observer
where we find there is a whole category dedicated to plugins for ember-cli-deploy
.
Some investigation and perusal of the deploy
docs and a few minutes in the #ec-deploy
channel in the
Ember Slack Team will lead us to some standard choices and well supported solutions.
ember-cli-deploy
is the foundation we can build onember-cli-deploy-build
builds our appember-cli-deploy-revision-data
can tag and activate versionsember-cli-deploy-s3
uploads our assets and makes them public in an S3 bucket
A search for 'archive' in Ember Observer leads us to:
ember-cli-deploy-archive
which takes our build assets and outputs a single tar archive
These combined knock out several of our needs.
Further investigation of ember-cli-deploy-s3
leads us to it's companion
ember-cli-deploy-s3-index
which not only uploads to S3, it also manages versioning and activation.
And while looking around we also stumble upon ember-cli-deploy-json-config
which conveniently parses our messy
index.html
file and outputs nice human and machine readable JSON
that will be way easier for our API server to consume.
Looks like we don't have to write any code at all! Just install and configure some plugins just the way we want and in the end just need to run a few instal commands:
ember install ember-cli-deploy
ember install ember-cli-deploy-archive
ember install ember-cli-deploy-build
ember install ember-cli-deploy-display-revisions
ember install ember-cli-deploy-json-config
ember install ember-cli-deploy-revision-data
ember install ember-cli-deploy-s3-index
Which helpfully creates a config/deploy.js
file for us with some useful defaults:
module.exports = function(deployTarget) {
var ENV = {
build: {}
// include other plugin configuration that applies to all deploy targets here
};
if (deployTarget === 'development') {
ENV.build.environment = 'development';
// configure other plugins for development deploy target here
}
if (deployTarget === 'staging') {
ENV.build.environment = 'production';
// configure other plugins for staging deploy target here
}
if (deployTarget === 'production') {
ENV.build.environment = 'production';
// configure other plugins for production deploy target here
}
// Note: if you need to build some configuration asynchronously, you can return
// a promise that resolves with the ENV object instead of returning the
// ENV object synchronously.
return ENV;
};
We just need to configure our AWS info and we will something working. Note: I removed targets here as well as comments for brevity.
module.exports = function(deployTarget) {
var ENV = {
build: {}
's3-index': {
accessKeyId: '<our-access-key>',
secretAccessKey: '<our-secret>',
bucket: '<our-bucket-name>',
region: '<our-bucket-region>'
},
};
return ENV;
};
Next step is to try it out:
ember deploy development --verbose
...
...
Pipeline complete
And when we got look in our S3 bucket low and behold there is an build.tar:HASH file in there! We're really getting somewhere.
That version is nice, but remember we really need to know what API versions this build is compatible with. Oh, I see
there is a prefix
option we can use.
's3-index': {
...
prefix: 'v1.22'
},
Deploy again and we now have a bucket with
build.tar:HASH (from our first deploy)
v1.22/
build.tar:NEWHASH (from our second deploy)
That is exactly what the Doctor ordered. Check that off our list and lets download that archive and see what it has.
Hmmm… it has an index.html
file, but no index.json
file. Looking back
at the --verbose
output we can see why
+- didBuild
| |
| +- archive
- saving tarball of tmp/deploy-dist to tmp/deploy-archive/build.tar
- tarball ok
| |
| +- json-config
- generating `tmp/deploy-dist/index.json` from `tmp/deploy-dist/index.html`
- generated: `tmp/deploy-dist/index.json`
- added `index.json` to `context.distFiles`
The build.tar
file is getting generated before the index.json
file has been created. Since both of these plugins are
doing their work inside the didBuild
hook we just need to swap the order they run in. We can find out how to do that
in the docs at http://ember-cli-deploy.com/docs/v1.0.x/configuration/#advanced-plugin-configuration
Looks like we just add a pipeline
object to our deploy config like:
pipeline: {
runOrder: {
'archive': { after: 'json-config' },
},
},
and… done! running ember deploy production
again gives us exactly the build.tar we're looking for.
The End
Seriously. That's how the story ends. No brave journey into the darkness of code comments to figure out just how this is supposed to work. It just works. TM. It's so simple we can just add it as a command at the end of our CI process and never think about it again.
Afterwords
I'm 100% head over heals in love with ember-cli-deploy
. I find it incredibly freeing to be able to tinker with our
deployment process to find just the right setup without needing to design and script each pice every time. The plugin
ecosystem gives me the building blocks I can use to assemble our pipeline in whatever way makes sense so we can test new
ideas and strategies fast and with very little friction.
I hope this was a useful overview of one deployment journey. Want to share your own? Tell me about a mistake I made or shower me with praise? Leave a comment here or find me at @jrjohnson_.