Deploying a Simple Node.js Application with NixOps

In a previous post I described how Nix could be used to easily set up a development environment without the use of virtual machines alternatives like Vagrant require. As an example I used setting up a development environment for a simple “Hello world” node.js application. At the very end I teased that the work we did could easily be reused to actually deploy our node.js application:

Because we’re essentially unifying development environments and deployment environments, we have now also specified a lot of information that NixOps needs to actually deploy this application to a server. We’ll get into that in a future post.

It’s time to make good on that promise. Let’s see how we can deploy our awesome hello world application to a newly created VirtualBox VM. Then, when we convinced ourselves this works correctly, let’s reuse the same Nix configuraton to deploy to EC2

Let’s reconsider the default.nix Nix expression I outlined in the previous post. Since that post, node 0.10.8 came out, so I tweaked it a little bit and added some more documentation:

# This file defines a function that takes a single optional argument 'pkgs'
# If pkgs is not set, it defaults to importing the nixpkgs found in NIX_PATH
{ pkgs ? import <nixpkgs> {} }:
let
   # Convenience alias for the standard environment
   stdenv = pkgs.stdenv;
in rec {
  # Defines our node.js package
  nodejs = stdenv.mkDerivation {
    name = "nodejs-0.10.7";
    # Where to download sources from
    src = pkgs.fetchurl {
      url = http://nodejs.org/dist/v0.10.8/node-v0.10.8.tar.gz;
      sha256 = "0m43y7ipd6d89dl97nvrwkx1zss3fdb9835509dyziycr1kggxpd";
    };
    # Dependencies for building node.js (Python and utillinux on Linux, just Python on Mac)
    buildInputs = [ pkgs.python ] ++ stdenv.lib.optional stdenv.isLinux pkgs.utillinux;
    # Hack to make it build on Mac
    preConfigure = stdenv.lib.optionalString stdenv.isDarwin ''export PATH=/usr/bin:/usr/sbin:$PATH'';
  };
  # Defines our application package
  app = stdenv.mkDerivation {
    name = "application";
    # The source code is stored in our 'app' directory
    src = ./app;
    # Our package depends on the nodejs package defined above
    buildInputs = [ nodejs ];
    # This is useful for using this package with --run-env: the PORT environment variable
    PORT = "8888";
    # Our application has no ./configure script nor Makefile, installing simply involves
    # copying files from the source directory (set as cwd) to the designated output directory ($out).
    installPhase = ''
      mkdir -p $out
      cp -r * $out/
    '';
  };
}

We can, just like before still use it for development purposes:

$ nix-build -A app --run-env

If you previously checked the older version of the application still relying on node.js 0.10.7, updating to latest version and executing the above command will first download and compile the latest and greatest node.js version ensuring you’re running exactly the same version of all dependencies as everybody else.

As a next step, let’s see how we can actually deploy this application to “the cloud.” We’ll start with a local cloud taking shape of a VirtualBox machine. For this we need NixOps installed, the Nix tool for cloud deployment as well as VirtualBox itself. The NixOps manual has installation instructions, if you’re on a Mac, I recommend you use my script so install both Nix and NixOps in one go.

To define our deployment we need to create two extra Nix expressions: one describing the “network” that our application will run in, and a second that describes the “physical” infrastructure that our application will run at, where each server from our network is mapped to a resource, e.g. an EC2 instance or a virtualbox VM.

Let’s start with our network definition file, network.nix:

{ # Name of our deployment network.description = “HelloWorld”; # Enable rolling back to previous versions of our infrastructure network.enableRollback = true; # It consists of a single server named ‘helloserver’ helloserver = # Every server gets passed a few arguments, including a reference # to nixpkgs (pkgs) { config, pkgs, … }: let # We import our custom packages from ./default passing pkgs as argument packages = import ./default.nix { pkgs = pkgs; }; # This is the nodejs version specified in default.nix nodejs = packages.nodejs; # And this is the application we’d like to deploy app = packages.app; in { # We’ll be running our application on port 8080, because a regular # user cannot bind to port 80 # Then, using some iptables magic we’ll forward traffic designated to port 80 to 8080 networking.firewall.enable = true; # We will open up port 22 (SSH) as well otherwise we’re locking ourselves out networking.firewall.allowedTCPPorts = [ 80 8080 22 ]; networking.firewall.allowPing = true; # Port forwarding using iptables networking.firewall.extraCommands = ” iptables -t nat -A PREROUTING -p tcp —dport 80 -j REDIRECT —to-port 8080 ”; # To run our node.js program we’re going to use a systemd service # We can configure the service to automatically start on boot and to restart # the process in case it crashes systemd.services.helloserver = { description = “Hello world application”; # Start the service after the network is available after = [ “network.target” ]; # We’re going to run it on port 8080 in production environment = { PORT = “8080”; }; serviceConfig = { # The actual command to run ExecStart = “${nodejs}/bin/node ${app}/server.js”; # For security reasons we’ll run this process as a special ‘nodejs’ user User = “nodejs”; Restart = “always”; }; }; # And lastly we ensure the user we run our application as is created users.extraUsers = { nodejs = { }; }; }; }

This is not the shortest network definition one could write, but it does a few nifty things. What it does (as you can see from the comments) is define a network of just one machine named ‘helloserver’, this server will be configured as follows:

  1. Its firewall is switched on, blocking all ports but three: 80 (where we’d like to access our application on), 8080 (the port the application listens to) and 22 (to still allow SSH access for deployment purposes).
  2. A firewall rule is setup to forward all traffic to port 80 to port 8080.
  3. A systemd service starts our application on boot and ensures it keeps running (i.e. restarts it if it crashes). The application will be run as Unix user ‘nodejs’
  4. An extra user ‘nodejs’ is defined.

The last piece of the puzzle is a Nix expression mapping each server from our network (in our case just ‘helloserver’) to a resource. We will write two such mappings, one to VirtualBox VMs, and another to EC2. So we can use the VirtualBox one for testing purposes and when we’re ready to push our application live, we can use the EC2 one. Here’s the one for VirtualBox named infrastructure-vbox.nix, it should be pretty self-explanatory:

{
  helloserver =
    { deployment.targetEnv = "virtualbox";
      deployment.virtualbox.memorySize = 1024;
    };
}

All that’s left is to create our deployment:

$ nixops create network.nix infrastructure-vbox --name node-vbox

And then deploy it:

$ nixops deploy -d node-vbox

What will happen when you execute deploy is the following:

After a while, a VirtualBox instance should be running with your application.

Node
vbox

To see where the application is running:

$ nixops info -d node-vbox

Which, among other things shows and IP that should now be accessible. If we’d like to ssh into our newly created VM:

$ nixops ssh -d node-vbox helloserver

If we would like we can destroy the VM easily too:

$ nixops destroy -d node-vbox

Next, let’s deploy for realzies. Let’s deploy to EC2 to allow the whole world to enjoy our amazing application.

For this you need an AWS account. Before we start you need to do only one thing via the console, as far as I’m aware NixOps cannot (yet) do this by itself: setup a security group that allows access from anywhere on port 22 and port 80:

Securitygroup

Then, we define a mapping from our existing network.nix machines to EC as follows:

let # Insert your AWS access key here accessKey = “yourkey”; in { # Mapping of our ‘helloserver’ machine helloserver = { resources, … }: { deployment.targetEnv = “ec2”; # We’ll be deploying a micro instance to Virginia deployment.ec2.region = “us-east-1”; deployment.ec2.instanceType = “t1.micro”; deployment.ec2.accessKeyId = accessKey; # We’ll let NixOps generate a keypair automatically deployment.ec2.keyPair = resources.ec2KeyPairs.helloapp-kp.name; # This should be the security group we just created deployment.ec2.securityGroups = [ “zef-test” ]; }; # Here we create a keypair in the same region as our deployment resources.ec2KeyPairs.helloapp-kp = { region = “us-east-1”; accessKeyId = accessKey; }; }

In the above expression, no AWS secret key is provided, you need to put that in your ~/.ec2-keys file where each line specifies a access key, followed by the secret key, e.g.:

youraccesskey yoursecretkey

Then, the process to deploy is much the same as to VirtualBox:

$ nixops create network.nix infrastructure-ec2.nix --name node-ec2$ nixops deploy -d node-ec2

This process may take a bit longer, as a lot of files will have to be uploaded the first time. Subsequent deployments should be very quick, though. After deployment finishes you can see where it’s running:

$ nixops info -d node-ec2 Network name: node-ec2 Network UUID: c7b994c2-ca91-11e2-b9f7-14109fe17209 Network description: HelloWorld Nix expressions: /Users/zef/git/nodejs-nix/network.nix /Users/zef/git/nodejs-nix/infrastructure-ec2.nix +——————-+————————-+——————————————+————————————————————————————-+————————+ | Name | Status | Type | Resource Id | IP address | +——————-+————————-+——————————————+————————————————————————————-+————————+ | helloserver | Up / Up-to-date | ec2 [us-east-1c; t1.micro] | i-4cdfa526 | 54.224.155.207 | | helloapp-kp | Up / Up-to-date | ec2-keypair [us-east-1] | charon-c7b994c2-ca91-11e2-b9f7-14109fe17209-helloapp-kp | | +——————-+————————-+——————————————+————————————————————————————-+————————+

After you make some changes to your application, updating to the new version is quick and easy. Just run deploy again. If you like, you can deploy to virtualbox first for testing:

$ nixops deploy -d node-vbox

and then deploy to EC2:

$ nixops deploy -d node-ec2

As Nix’ deployments are very incremental, only components that changed are uploaded. One cool feature of Nix (and with that NixOS and NixOps) is the ability rollback an entire deployment. Let’s say you didn’t test your software well enough and have introduced a very serious bug. Or, you made a complex change to your VM setup that didn’t work out well. With NixOps you can instantly roll back to the previous version of your deployment (across machines). We enabled this with the network.enableRollback = true setting in our network.nix. To see the past list of “generations” (previous versions of our deployment), let’s run:

$ nixops list-generations -d node-ec2 # or node-vbox
   1   2013-06-02 10:09:50
   2   2013-06-02 10:12:06   (current)

This will return previous versions and the time they were deployed, as well, which one is current. To roll back to the first generation, we now use the rollback command:

$ nixops rollback -d node-ec2 1

And within seconds your old version will be running again. This is made possible by the feature of Nix that I described in my previous post: the ability to keep multiple versions of software installed side by side. When we deployed a new version, the old version never left the building, it was still there. Switching back to the old version is therefore as simple as switching a symlink.

In a future post I’ll show how to deploy more complex applications with Nix, such as a load-balanced node.js application with some npm dependencies, talking to a Redis database.