Running multi-environment apps in node.js

It's a common paradigm, at least in web development (and I can't really speak for anything else), to have different "environments" for production and development, so developers who are working on new features are not affecting other users when they inevitably break stuff. Having separated environments also allows you to control the boundaries of those environments better, for instance for testing purposes, or to avoid rate limiting on APIs. It also allows for parallel development between frontend and API.

So what do you do with different environments? At Manta, we actually have four environments: production and development, as you might expect, plus integrated-development (called "devint") and smoketest. There is also sort of a staging environment, but it's more or less identical to production (which is the point). We also use to have a test environment, but it turned out that was redundant given the way our dev environment works. Our dev environment is completely isolated. Our API calls go to a fake api which returns predictable data, making both automated and manually testing much easier. Smoketest is an end-to-end testing environment that runs a Jenkins test suite and doubles as a final stop for code before production where product owners and project managers can test features before they go live. Devint is a combination of development and smoketest. It uses local frontend code (running on localhost), but service calls point to the same API that smoketest uses. It's a good way to test frontend changes using real data.

Setting up multiple environments

To manage these environments, we have four different configuration files in our frontend node server, all named config/app.<environment>.json. There are a lot of different ways to set up environment-specific configuration, but they usually all have this "environment-file" mechanism. In node, the results of calling require on a file are cached, so you could actually set up a really simple configuration manager that worked something like this:

var env = process.env.NODE_ENV;  
module.exports = require('./config/app.' + env + '.json');  

The first time you require this file, it will figure out which configuration file to export based on the value of the environment variable NODE_ENV. And that value will be cached. So if this file were called "config.js", when you called require('./config.js'), you'd get a configuration object back, and every time you required that file again, you'd get the same object. It won't try to recalculate the environment (which would still be the same, but there's no reason to do that more once). But saying that this file is "cached" is actually misleading because it's actually more than that. Node "caches" objects by storing them in a cache object. Try this:

console.log(require.cache);  

You'll get an object of every file you've required in your currently running app (absolutely resolved) and the exports of those files. So your config file might show up there as:

'/home/username/code/some-project/config.js': {  
  foo: 'bar',
  hello: 'world'
}

Why does this matter? Because that means that your configuration object is literally the same object every time you require it. You'll get a reference to the original "config.js" exports back. So you could actually modify this object and still perpetuate your changes to every other place that requires the same config object. For instance,

var config = require('./config.js');  
config.helloWorld = true;  

Now every other file that requires config.js will have access to the helloWorld property of that config. I say all this to illustrate a point, not to suggest this is a thing to do because, generally speaking, your configuration should be static (that is, it should not change at run time). But one thing this let's you do is modify the config at start up to add calculated values. I would recommend limiting this to your configuration loader, so you could expand that previous file to something like this:

var env = process.env.NODE_ENV;  
var config = require('./config/app.' + env + '.json');  
config.someCalculatedValue = require('./calculate-value')(config);  
module.exports = config;  

What are the practical use cases of this? We do this kind of thing to load build-generated configuration (like the git sha of the current commit) and to merge in a/b test configuration (also named with the environment in the name).

Inheriting

One common short-coming in this kind of configuration is keeping these config files DRY. We found we would often include the same configuration in all four config files (or in three of the four). One way around this is to use inheritance for your configuration. That is, keep all your configuration values in your production config and only put the values that are different in your other configuration files then use a deep extend to merge these objects (I like to use config-extend). Let's see what that would look like:

var extend = require('config-extend');  
var env = process.env.NODE_ENV;  
var config = require('./config/app.production.json');  
if (env !== 'production') {  
  extend(config, require('./config/app.' + env + '.json'));
}
config.someCalculatedValue = require('./calculate-value')(config);  
module.exports = config;  

Here we load the production config first and then, if we're not in production, merge the correct environment's config into that (so that common values from the environment config will replace those in the production config). Don't forget to npm install config-extend if you do this.

Nconf

There are lots of configuration managers for node, but the above approach should work fine in basically every use case and should probably be preferred most of the time since it minimizes the number of dependencies. Manta uses nconf, which in some ways is just syntactic sugar for the above. It also has built-in functions to grab all the current environment variables and arguments to the script (process.env and process.argv). Of course, you could do this yourself in a fairly straightforward way (something like extend(config, process.env)). But let's see what hierarchical inheritance looks like with nconf. Here's a shortened version of ours (with additional comments):

var nconf = require('nconf');  
var path = require('path');

// Get an absolute path to the root (we keep our loading file
// in lib, so we need to resolve to "..")
var dir = path.resolve(__dirname, '..');

// Put this in a function that can be called
exports.initialize = function() {  
  // If unset, make it development. This way, you don't have
  // to start your dev server with NODE_ENV=development.
  process.env.NODE_ENV = process.env.NODE_ENV || 'development';

  // Load argv and env first (makes overriding config easier)
  nconf.argv().env();

  var env = nconf.get('NODE_ENV'); // or process.env.NODE_ENV

  // Start with production
  var files = [dir + '/config/app.production.json'];

  // And then if we're not in production, add the right config too
  if (env !== 'production') {
    files.push(dir + '/config/app.' + env + '.json');
  }

  // This will handle merging for us
  var memory = new nconf.Memory({ loadFrom: files });

  // Add this store into nconf so we can access it later
  nconf.add('memory', memory);

  /* Additional setup occurs . . . */

  return nconf;
};

Reading the docs again for the first time in probably over a year, it looks like you could also call nconf.file twice instead of creating an instance of nconf.Memory. I'm not sure whether this is a new feature or just a feature that was previously unclear in the docs. But it would look like this:

// Load non-prod first since with nconf, first in wins
if (env !== 'production') {  
  nconf.file(env, dir + '/config/app.' + env + '.json');
}

// Now load production
nconf.file('production', dir + '/config/app.production.json');  

Whatever you choose, I definitely suggest writing a test to assert that your config is correct if you decide to incorporate inheritance. If you use the nconf.Memory approach, you can access the configuration via stores.memory.store. So test setup might look like this:

// Reset NODE_ENV to the right thing after each test
var currentEnv;  
beforeEach(function() {  
  currentEnv = process.env.NODE_ENV;
});
afterEach(function() {  
  process.env.NODE_ENV = currentEnv;
});

describe('nconf', function() {  
  it('has the right configuration in production', function() {
    process.env.NODE_ENV = 'production';
    var subject = require('./lib/nconf');
    var config = subject.stores.memory.store;
    // Now use assert or expect or should or whatever testing
    // mechanism you prefer to test the values in the config.
  });

  it('has the right configuration in development', function() {
    process.env.NODE_ENV = 'development';
    // etc.
  });
});

You could also use nconf.get('key') to access config values, which will work whether you've used nconf.Memory or nconf.file.

Note that, with this inheritance approach, blanking out a value is not as simple as leaving it out of the configuration file. You need to include it with an empty value that is still valid JSON. For instance, if your production file sets IS_PROD: true, you need to override that in other configs with IS_PROD: false.

Conclusion

The best thing about configuration inheritance is that you typically only need to add a value once to supply it to all environments. The one downside is that it is not always immediately clear where a configuration value is set from (since it could be multiple places), but it's actually pretty quick to adjust to this, so I still believe it's worth doing. Most developers agree that the fewer places you have to do the same thing in code the better. This is not an ultimatum, of course, and as far as being DRY goes, your configuration is probably not the biggest problem area. But it's also probably not the most difficult to refactor, and every once in a while, you need an easy win.

comments powered by Disqus