Using Juju to Deploy your Node.js Application

One of Juju's main use cases is to deploy your application directly out of version control and into a cloud. Since Juju supports local and remote clouds, this makes for a nice workflow that enables you to rev your app quickly on your local machine and then deploy to the cloud.

In this tutorial we will deploy a Node.js/MongoDB application directly from github. The application will be behind an HAProxy service so that we can horizontally scale when needed.

We will then set up a local-to-cloud workflow with our application so we can do continuous deployment. Since we deploy locally in the exact same way as we deploy to a cloud, this is a powerful method for developing your application in an environment that more closely resembles production.

Before moving on you should have gone through the Getting Started section and installed and configured Juju.

Basic Usage of the Node.js Charm

First, create a configuration file myapp.yaml to add info about your app pointing to your github repo:

node-app:
  app_url: https://github.com/yourapplication

If you have not already bootstrapped an environment, do so:

juju bootstrap

Then wait a few minutes while your cloud spins up. Then deploy some basic services:

juju deploy --config myapp.yaml node-app myapp
juju deploy mongodb
juju deploy haproxy

relate them

juju add-relation mongodb myapp
juju add-relation myapp haproxy

open it up to the outside world

juju expose haproxy

Find the haproxy instance's public URL from

juju status

(or attach it to an elastic IP via the AWS console) and open it up in a browser.

scale up your app (to 10 nodes for example)

juju add-unit -n 10 myapp

and scale it back down

juju remove-unit myapp/9 myapp/8 myapp/7 myapp/6 myapp/5 myapp/4 myapp/3 myapp/2 myapp/1

Local to Cloud Workflow

The previous example deploys your application quickly to the cloud, in this example we will show how to hack and test on an application locally on your laptop and then push out to the public cloud.

We need to configure 2 environments, a local one and a public cloud one.

  1. Configure the local provider on your machine.
  2. Configure a public or private cloud on your machine.
  3. AWS
  4. HP Cloud
  5. OpenStack

In this example the local environment is named local and we'll deploy to an AWS environment called amazon. First let's switch to the local environment and bootstrap.

juju switch local
juju bootstrap

Create a configuration file myapp.yaml to add info about your app pointing to your github repo:

sample-node:
  app_url: https://github.com/yourapplication

Then deploy some basic services:

juju deploy --config ~/myapp.yaml node-app myapp
juju deploy mongodb

relate them

juju add-relation mongodb myapp

Now open up your browser and go to http://localhost to get your application loaded in your browser.

Continuous Deployment

Continue to write your code, push to git as you land features and fixes. When you're ready to test it you can tell Juju to check the git repository again:

juju set myapp app_branch=https://github.com/yourapplication

The charm will then fetch the latest code and you can refresh your browser at http://localhost.

Repeat pushing to git and using the juju set command to keep a live instance of your application running in your local environment.

Push to your Public/Private Cloud

After you've repeatedly upgraded your application locally it's time to push it out to a place where your coworkers can see your app in all it's glory, let's push this to AWS. Same exact commands as before, just to a different environment:

juju switch amazon
juju bootstrap
juju deploy --config ~/myapp.yaml node-app myapp
juju deploy mongodb
juju add-relation mongodb myapp

Since we're on a public cloud and not on a local provider we need to explicitly expose the service and get its public IP:

juju expose myapp
juju status myapp

And put the ec2 URL in your browser. If you want to enable some horizontal scalability to your application you can do so, even after you've deployed!

juju deploy haproxy
juju add-relation haproxy myapp
juju expose haproxy
juju unexpose myapp

And then get the public IP from the haproxy instead (notice how we've unexposed your application so that only haproxy is serving the public internet):

juju status haproxy

Now you can juju add-unit myapp and juju remove-unit myapp based on load.

Tearing it all down

The local containers survive reboots and do not go away until you explicitly tear the environment down. Now that your coworkers have seen your great application let's also stop spending money:

juju destroy-environment -e amazon
juju destroy-environment -e local

Charm Details

This section is to explain how the charm works and is provided here as a reference.

What the charm does

During the install hook,

  • installs node/npm
  • clones your node app from the repo specified in app_repo
  • runs npm if your app contains package.json
  • configures networking if your app contains config/config.js
  • waits to be joined to a mongodb service

when related to a mongodb service, the formula

  • configures db access if your app contains config/config.js
  • starts your node app as a service

Charm configuration

Configurable aspects of the charm are listed in config.yaml and can be set by either editing the default values directly in the yaml file or passing a myapp.yaml configuration file during deployment

juju deploy --config ~/myapp.yaml node-app myapp

Some of these parameters are used directly by the charm, and some are passed through to the node app using config/config.js.

Application configuration

The formula looks for config/config.js in your app which starts off looking something like this

module.exports = config = {
  "name" : "mynodeapp"
  ,"listen_port" : 8000
  ,"mongo_host" : "localhost"
  ,"mongo_port" : 27017
}

and gets modified with contextually correct configuration information during either deployment (via the install hook) or relation to another service (relation-changed hook).

This config can be used from within your application using snippets like

...
var config = require('./config/config')
...
new mongo.Server(config.mongo_host, config.mongo_port, {}),
...
server.listen(config.listen_port);
...

Alternatively you could use a Procfile in root directory like this:

web: node app.js

and then get the environment variables from the running environment like this:

app.set('port', process.env.PORT);

The defined environment variables are:

NAME
PORT
NODE_ENV
MONGO_HOST
MONGO_PORT
MONGO_REPLSET

Network access

This charm does not open any public ports itself. The intention is to relate it to a proxy service like haproxy, which will in turn open port 80 to the outside world. This allows for instant horizontal scalability.

If your node app is itself a proxy and you want it directly exposed, this can easily be done by adding

open-port $app_port

to the bottom of the install hook, and then once your stack is started, you expose

juju expose myapp

it to the outside world.

By default, juju services within the same environment can talk to each other on any port over internal network interfaces.

Making this work with your node.js app

This charm makes some strong assumptions about the structure of the node application (config/config.js) that might not apply to your app. Please treat this formula as a template that you can fork and modify to suit your needs.

The biggest difference between how the charm behaves for different kind of apps is application startup. A simple application will want to start upon install (startup code goes in the install hook), whereas some applications will not want to start up until a database has be associated (startup code goes in the db-relation-joined hooks).