Deploying Node.js Applications with AWS OpsWorks - part 3

This article is the last one of this series. The first artice describes how to write a small Chef recipe to deploy a Node.js application. The second article describes how to extract Chef recipe logic into Custom Resources. Now, in this third article, I will show you how to use the injected Chef data of AWS OpsWorks to deploy the correct app.

tl;dr

  • within the first and second article we created a Custom Resource that can setup and deploy
  • create an OpsWorks Stack, Layer and App
  • create a wrapper OpsWork Node-Deploy cookbook that uses OpsWorks' injected data with our Custom Resources
  • configure the Layers' Setup and Deploy Events to trigger the corresponding wrapper Cookbook Recipes

AWS OpsWorks

Before we can use the data by AWS OpsWorks we should step back and get ourselves familiar with the AWS OpsWorks concepts. For more detailed information you can read through the official AWS OpsWorks documentation.

AWS OpsWorks defines the three primary entities Stack, Layer and App, which should describe your setup. One can understand the Stack as a group of things that belong together. With a Stack, we configure general options like the Chef version, location of your Chef recipes, default Security Groups / IAM Roles, etc. Usually, for production, we at blogfoster use a Stack to group one API with its dependencies, e.g. API, database, ELB (Load Balancer). On the other side for our stage environment, we put all APIs into one "Stage" Stack.

The following picture shows an example Stack setup Alu for the eu-west-1 region.

stack example

A Layer defines one specific item of your Stack. It is kind of a template for your instances. With a Layer we configure which Chef recipes to run, whether it's connected to an ELB, additional volumes, specific Security Groups, etc. When defining a Layer for one of our APIs we always give it the same name as the API name, so we can easier identify which API is deployed within which Layer. As mentioned before a Layer can also be an ELB or a database (RDS). After creating a normal Layer we can use it to start Instances based on this Layer.

The following picture shows all Layers that belong to our "Alu" Stack. As you can see we defined a Load-Balancer, a Layer for the App itself and a Redis (currently OpsWorks does not support to link ElastiCache).

layer example

The third entity is the App. The App defines a deployable unit. With an App, we configure its source (usually git, git ssh key and git branch), environment variables, data sources and more. For us at blogfoster an App usually relates to one specific GitHub repository, which is one API. Unfortunately, when defining an App it is not possible to relate it to a Layer, that's why we name the App as the Layer so we can distinguish which App belongs to which Layer. Of course, it is also possible to deploy multiple Apps on one Layer.

The following picture shows the Setup of our "alu" App. We can see the link to the git repository, git ssh key (marked as protected value) and some environment variables, which could also possibly contain protected values.

app example

AWS OpsWorks Chef Run Events

Now that we have a rough overview of the OpsWorks Entities, we should also get familiar with the Chef Runs that OpsWorks kicks in for specific Events. Again you should check the AWS docs for more detailed information.

Basically, in a Layer, one can configure which Chef Recipes should be executed for a specific Event.

For now, there are two Events interesting for us, which is the Setup and Deploy Event. The Setup Event is fired when we set up the machine the first time. The Deploy Event is also fired when the machine is setup the first time (directly after the Setup) and whenever you deploy an App.

OpsWorks Specific Deploy Cookbook

So we configured an OpsWorks Setup and heard a little bit about OpsWorks Events, we should now look how we can combine this with our existing Chef Deployment Recipes.

The first thing we need to understand is, that looking at our deployment, OpsWorks is only injecting the configured data into our chef run, but not more. So it's not automatically fetching a git repository or setting any environment variables, etc. But this fact should not scare us, but instead give us the good feeling that we can control the deployment flow as we want it.

Let's quickly recap what we have so far:

  • node_app_setup Custom Resource, that takes:
    • nodejsversion
    • nodejschecksum
  • node_app_deploy Custom Resouce, that takes:
    • sshkey
    • dir
    • gitrepository
    • gitrevision
    • runcmd
    • run_environment

So all we need to do, is to put the OpsWorks data into these Resources! Enough talking, let's code:

# attribute
#   setup attributes
default['opsworks-node-app']['nodejs']['version'] = '6.5.0'  
default['opsworks-node-app']['nodejs']['checksum'] = '...'

#   deploy attributes
default['opsworks-node-app']['basedir'] = '/opt'  
default['opsworks-node-app']['run-cmd'] = 'npm start'  

So, first of all, we defined some default attributes, which define a common nodejs version for all projects and its checksum. We also defined a base directory where all apps should be cloned into and a default start command.

The next step is to create a common setup recipe we can configure to run with the Setup Event.

# recipes: setup.rb
node_app_setup 'node-app' do  
  nodejs_version node['opsworks-node-app']['nodejs']['version']
  nodejs_checksum node['opsworks-node-app']['nodejs']['checksum']
end  

So this one was simple, we just use our node_app_setup Custom Resource and give it static attribute values. Note that within a Layer or App Deployment we can always pass in custom attributes, which would override these settings. So we're still flexible for all Layers, even if they share the same Recipe to run!
This recipe will be running with the Setup Event.

The next Recipe will be running with the Deploy Event. This Recipe will be a little more complex as we will now read the OpsWorks data.

# recipes: deploy.rb

# get own instances + layer name
instance = search('aws_opsworks_instance', 'self:true').first  
layer = search('aws_opsworks_layer', "layer_id:#{instance['layer_ids'].first}").first

# user layer name as app
app_data = search('aws_opsworks_app', "name:#{layer['name']}").first  
fail 'could not find app' unless app_data

# deploy the application
node_app_deploy 'node-app' do  
  ssh_key app_data['app_source']['ssh_key']
  dir ::File.join(node['opsworks-node-app']['basedir'], app_data['name'])
  git_repository app_data['app_source']['url']
  git_revision app_data['app_source']['revision']
  run_cmd node['opsworks-node-app']['run-cmd']
  run_environment app_data['environment']
end  

Ok let's break it down:

instance = search('aws_opsworks_instance', 'self:true').first  
layer = search('aws_opsworks_layer', "layer_id:#{instance['layer_ids'].first}").first  

First we load the data of the layer connected to the currently running instances. To do so we load the instance data and then find the layer that contains this instance.

As described earlier we're naming our layers as we name our apps, so we can now load the app with this same name. As a safety mechanism, we're stopping the Chef Run if we couldn't find any app data.

app_data = search('aws_opsworks_app', "name:#{layer['name']}").first  
fail 'could not find app' unless app_data  

Now we have all the data we need to feed our node_app_deploy Custom Resource:

node_app_deploy 'node-app' do  
  # extract the ssh key from the app data
  ssh_key app_data['app_source']['ssh_key']

  # use the given base dir + the app name as the deploy directory
  dir ::File.join(node['opsworks-node-app']['basedir'], app_data['name'])

  # get the git reposity url from the app data ...
  git_repository app_data['app_source']['url']

  # ... as well as the git revision (git branch)
  git_revision app_data['app_source']['revision']

  # use the default run command we configured in the attributes before
  run_cmd node['opsworks-node-app']['run-cmd']

  # get all environment variables from the app data
  run_environment app_data['environment']
end  

Let's assume we call the cookbook opsworks-node-app then we can configure it now in the Layer.

layer-recipes

And that's already it! Now we can launch an Instance within our Layer and it will setup Node.JS and deploy our app to it.

Conclusion

Wow, that was a journey, we started with a small Chef Recipe, then wrapped it into a Custom Resource and now learned a bit about an OpsWorks Setup (Stack, Layer, App). Then we wrote a wrapper OpsWorks Cookbook that takes the injected data and passes it to the Custom Resource. At the end we configured the OpsWorks Layer to run our Recipes with the Setup and Deploy Events.

I hope this series helps you to set up a small Chef Deployment with AWS OpsWorks.

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.