Using n as a Tool to Provision Node.js

April 12th, 2014 Permalink

When provisioning servers with a tool like Chef, you really don't want to find yourself in the position of building Node.js from source. It takes a long time, especially if you're working with something like an AWS instance where there usually isn't a great deal of computational horsepower under the hood. Unfortunately the standard situation for Node.js on any given Linux platform is that packages in the default repositories are ancient releases and tools like the Node.js cookbook for Chef can have issues when it comes to installing a recent binary distribution.

There are many ways around this issue, but one of the easier approaches is to use the n tool as a basis for your provisioning setup. The tool must be built from source, but that takes very little time at all. After it is installed, n then manages installation of the right Node.js binary distribution for your current platform.

Provisioning via Bash Script

The bash script below illustrates the desired outcome, with the caveat that it is specific to Debian distributions. If you are familiar with rpm-based distributions it won't be difficult to adapt it, however.

#!/bin/bash
#
# Provisioning script to install Node.js using n.
#
# This must run as root.
#

N_VERSION=1.2.1
# Install the latest stable version.
NODE_VERSION=stable

# Remove downloaded files.
function cleanup() {
  rm -rf n-${N_VERSION}
  rm -f ${N_VERSION}.tar.gz
}

# --------------------------------------------------------------------------
# Install build tools.
# --------------------------------------------------------------------------

apt-get update
apt-get install -y build-essential
# Curl is needed by n.
apt-get install -y curl

# --------------------------------------------------------------------------
# Install n and Node.js.
# --------------------------------------------------------------------------

# Clean out any leftovers from an earlier provisioning run.
cleanup

# Obtain n and install it.
wget https://github.com/tj/n/archive/v${N_VERSION}.tar.gz
tar zxf v${N_VERSION}.tar.gz
cd n-${N_VERSION}
make install
cd ..

# Install the Node.js binary distribution.
n $NODE_VERSION

cleanup

Provisioning via Chef Cookbook

Since n really is a very simple build that only depends on the presence of build tools and curl, making a cross-distribution cookbook is easy. You can find such a cookbook over at GitHub. The core of it is contained in these files, showing that it just recapitulates the actions of the bash script above, but in a way that will work for any Linux distribution:

metadata.rb:

name              'n-and-nodejs'
license           'MIT'
description       'Install Node.js version manager n and Node.js.'
version           '0.0.1'
recipe            'n-and-nodejs', 'Install Node.js version manager n and Node.js.'

# We will be building n from source.
depends 'build-essential'
# n uses curl to download Node.js binaries.
depends 'curl'

%w{
  fedora
  redhat
  centos
  ubuntu
  debian
  amazon
  suse
  scientific
  oracle
  smartos}.each do |os|
  supports os
end

attribute 'n-and-nodejs/n/version',
  :display_name => 'n version.',
  :description => 'Version to install.',
  :default => '1.2.1'

attribute 'n-and-nodejs/nodejs/version',
  :display_name => 'Node.js version',
  :description => 'Version to install; "stable" means the latest stable version.',
  :default => 'stable'

recipes/default.rb:

#
# Install n and Node.js.
#

# ----------------------------------------------------------------------------
# Install n by building from source.
# ----------------------------------------------------------------------------

# Obtain the source from GitHub.
remote_file "/tmp/#{node['n-and-nodejs']['n']['version']}.tar.gz" do
  source "https://github.com/tj/n/archive/v#{node['n-and-nodejs']['n']['version']}.tar.gz"
  action :create_if_missing
end

# Build and install.
execute "tar zxf #{node['n-and-nodejs']['n']['version']}.tar.gz" do
  cwd "/tmp"
end
execute "make install" do
  cwd "/tmp/n-#{node['n-and-nodejs']['n']['version']}"
end

# --------------------------------------------------------------------------
# Install Node.js
# --------------------------------------------------------------------------

execute "n #{node['n-and-nodejs']['nodejs']['version']}"

To use the cookbook, add it and its dependencies to your run list:

run_list [
  'recipe[build-essential]',
  'recipe[curl]',
  'recipe[n-and-nodejs]'
]

Then set up the attributes appropriately. For example:

default_attributes(
  "n-and-nodejs" => {
    "n" => {
      "version" => "1.2.1"
    },
    "nodejs" => {
      # Install a specific version.
      # "version" => "0.10.26",
      #
      # Install the latest stable version.
      "version" => "stable"
    }
  }
)