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:

  • All virtual machines will be created if they don’t already exist
  • The entire system configuration (based on NixOS) will be prepared either locally (if you’re running 64-bit Linux) or on one of the VMs (and downloaded to the local Nix store afterwards)
  • The system configuration for each system will be pushed to each system (incrementally, so only the parts that the machine doesn’t already contain)
  • Once all configurations are present on all machines, they are simultaneously activated everywhere.

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.

Got something to say?
  1. Hi Zef – I’m loving this series! I’m starting to get myself fully switched over to NixOS, and it’s been really good to see that my choice is a sensible one. I haven’t yet used NixOps, but this looks really slick. Looking forward to the next post :)

Comments are closed now.