At blogfoster we love AWS, Node.js and the philosophy of microservices. As we built more and more microservice, we needed to come up with an automated deployment.
Our current development workflow is based on Docker, but we’re quite experienced with Chef and AWS Opsworks, so we wanted to start with these tools, to quickly built a solid solution. Nevertheless, we will try to use a Docker-based solution for the future as we know Docker deployments are amazingly fast.
In this first article, we want to present a simple approach for deploying Node.js applications using Chef.
The application setup
Before going into details it’s always about assumptions, and so does this article. The following deployment works well in cases you rarely need to scale and are searching for a simple deployment strategy on AWS.
For applications that need to scale more often, I would advise you to read about ami-backed deployments. Also in the context of microservices, for us it is ok if a service is unavailable for a few seconds.
Opsworks Node.js deployment
As AWS Opsworks started supporting Chef 12, they dropped the support for their built-in cookbooks (which imho, was the right decision, as the Chef deployment becomes more transparent now). Sure you can download their cookbooks and just use them, but looking at the code reveals that they’re not dead-simple.
So modifying them to your needs might be challenging. Ignoring that, the setup is not using standard distro tools like
upstart/systemd on Ubuntu and it’s trying to do a Capistrano-like deployment (as it’s using the internal Chef resource
Don’t get me wrong, these cookbooks are battle-proofed in production for quite some time, and a lot of effort was put into them, so I will not argue against them, but for me they are too bloated.
I mean getting the full code (git repository) and downloading all node dependencies for every deployment for a medium sized project on a
t2.micro instance could easily take more than getting a coffee playing a round of dart and getting the second coffee.
Not actually continuous deployment …
Simple deployments with Chef
So how could a simple deployment look like? Let’s collect some bullet points:
- get the code through
- install external node packages
- run the app
Should be simple, right? The devil is in the detail!
What does it mean to install something? Ideally, if the particular version is not there install it, otherwise skip this step. This helps for the first two steps, but how do you get the code from git?
First, we need to provide credentials, e.g. using a ssh key file, then sync/check-out the explicit branch. Next, and one of the most interesting parts: installing external node packages. To keep the deployment run as short as possible, we have to download all the dependencies on the first run, but for all following deployments we should just download those dependencies that were updated. So what does it mean for real?
Nothing more than:
npm prune and
npm install! The npm prune command removes any out-dated dependencies and npm install by it’s nature installs packages, but also skips those that are already installed (ok, npm-shrinkwrap performs the fastest but without it, it’s also fine).
And last but not least, how should we run the app? It should be failure safe when crashing! Since we’re running our software on Ubuntu 14.04 we use the built-in tool upstart, which can handle all of that. Talking about running the app, we should remember to also restart our app if there are any changes to code, dependencies or run environment!
Let’s recap the strategy:
- install git if it’s not there
- skip it if git is already installed
- install specific nodejs version
- skip it if it’s already installed
get the code through
- provide github access using a ssh key file
- sync the latest code for given branch or tag
install external node packages
- download all dependencies if they’re not there
- only intall updated dependencies if there were installed already once, and remove out-dated once (npm prune && install)
run the app
- make us of upstart to start the node app
- restart the app in case the code changes, any dependency were updated or changes to the run environment
- (1) install
- (2) install
node.default['nodejs']['install_method'] = 'binary' node.default['nodejs']['version'] = '5.10.0' node.default['nodejs']['binary']['checksum']['linux_x64'] = '...' include_recipe 'nodejs'
- (3) get the code through
file '/root/.ssh/id_rsa' do mode '0400' content '...' end git '/opt/app' do repository '<repository>' revision '<revision>' end
- (4) install external node packages
execute 'npm prune' do cwd '/opt/app' end execute 'npm install' do cwd '/opt/app' end
- (5) run the app
### /templates/default/app.conf.erb description "Upstart Job for the App service" start on (local-filesystems and net-device-up IFACE!=lo) stop on shutdown setuid root chdir /opt/app env NODE_ENV=production env PORT=8080 respawn exec npm start
template '/etc/init/app.conf' do notifies :stop, 'service[app]', :delayed notifies :start, 'service[app]', :delayed end service 'app' do provider Chef::Provider::Service::Upstart action :start subscibes :restart, 'git[/opt/app]', :delayed subscibes :restart, 'execute[npm prune]', :delayed subscibes :restart, 'execute[npm install]', :delayed end
And that’s already it!
Ok with that I wasn’t honest, in real life you will probably create an app user with dropped privileges and do all the steps with that user (ssh key file, git, npm …) and you might need to create a known_hosts entry for github initially, but then we’re done :)
So let’s review what we did: We deployed a Node.js app using Chef with a few simple steps, which are installing
nodejs, the code, npm packages and lastly we also started the app using upstart.
blogfoster’s vision is to build an ecosystem for bloggers where they can get all the tools and support they need to become successful with their blogs. We use React, Redux, Webpack, SASS, ES6 and more to build an enjoyable platform for thousand of bloggers. Do you want to work with the newest technologies? We are constantly looking for people as passionate as we are. Join our team, let's work together.