Tuesday, June 16, 2015

Is NODE_ENV an Anti-Pattern?

(In which I make the argument that using a configuration file (or two) is a better idea than setting the NODE_ENV environment variable and propose the use of the SN Props package to make this process easier.)

Like everyone else in the node community, I started out using the NODE_ENV environment variable to set various details in my apps. I would do something like this to start an app:
$ NODE_ENV=dev /usr/bin/node foo.js
The shell would set the environment variable NODE_ENV to the string "dev" and launch the foo.js node application. Inside foo.js, we would do something like this:
var port, host; 
if( "prod" === process.env.NODE_ENV ) {
  port = 80; host = "0.0.0.0";
} else if( "dev" === process.env.NODE_ENV ) {
  port = 8080; host = "127.0.0.1";
} else {
  console.log( "ERROR: unsupported NODE_ENV value: " +
               process.env.NODE_ENV );
  process.exit(1);
}
 
// insert other code here 
server.listen( port, host );

And honestly, there's nothing seriously wrong with this. It lets you express different application behavior based on whether you're running in development-mode or if you've deployed to production. But then I saw this:
var port, host, db_pass; 
if( "prod" === process.env.NODE_ENV ) {
  port = 80; host = "0.0.0.0"; db_pass = "tq6TJwFR";
} else if( "dev" === process.env.NODE_ENV ) {
  port = 8080; host = "127.0.0.1"; db_pass = "2qpwRfKB
";
} else {
  console.log( "ERROR: unsupported NODE_ENV value: " +
               process.env.NODE_ENV );
  process.exit(1);
}
 
// insert code to connect to the database here
server.listen( port, host );
 And where did I see this? In the source repository, of course.

Now I don't want to be too snobbish, I've written apps that check the password into the source repo before. At the time I thought it was a simple, throw-away app that no one would use after a couple months. By the time the wiley black-hats found this in the source repo, the app would be long-retired. Except that's never the way the world works. The code got used by a different group in the organization and then sold to a third party. By the time I heard what had happened, one of the third parties had lined up a SAS-70 audit they may have failed because of a fixed password in the system.

Moral of the story? Don't hard-code database passwords into your app.

"But what does this have to do with NODE_ENV being an anti-pattern?" you ask.

Simple, using NODE_ENV to set application behavior makes it easy for a developer to do bad things (like hard-coding dev & production environment passwords into the app.) In my apps, I've replaced the use of NODE_ENV with what I call the "Concatenated Config" pattern.

Avoiding Application Brittleness with the Concatenated Config Pattern

Instead of setting my config parameters from an environment variable inside the code, I use multiple JSON files to hold config settings and import them as a javascript object. The SN Props package does all the heavy lifting for you. So now, I launch an app like this:
$ node ./bar.js \
    --config file:///opt/bar/dev.json \
    --config file:///opt/bar/db_dev.json
SN Props reads the command line looking for URLs pointing to JSON files. In this example, it grabs the files /opt/bar/dev.json and /opt/bar/db_dev.json, smooshes them together and passes them to your app via a callback. Here's what the code in bar.js looks like:
require( 'sn-props' ).read( function( props ) {
  // db code goes here
  // http server code goes here
} );
And the contents of the two config files look something like this:
/opt/bar/production.json:
{
  "listen": {
    "port": 8080,
    "host": "127.0.0.1"
  }
}
/opt/bar/db_db002.json:
{
  "mysql": {
    "host": "127.0.0.1",
    "port": 3306,
    "user": "dev",
    "pass": "goats!"
  }
}
and, of course, you probably want to define other configs to use in production:
/opt/bar/production.json:
{
  "listen": {
    "port": 80,
    "host": "0.0.0.0"
  }
}
/opt/bar/db_db002.json:
{
  "mysql": {
    "host": "db002.internal.example.com",
    "port": 3306,
    "user": "bar",
    "pass": "JC7VwyguUqHm8D3J"
  }
}
And if you trust your internal infrastructure, you can replace references to file: URLs with references to http: URLs. You can probably figure out what this does:
$ node ./bar.js \
    --config https://config.example.com/dev.json \
    --config file:///opt/bar/db_dev.json
Let's Recap:

The benefits of this pattern are:

  1. It discourages hard-coding passwords and other config information into the applications source. (i.e. - it discourages something that would make your app brittle and possibly insecure.)
  2. It lets your DevOps team change configuration details like DB particulars & which port and IP address your app listens on without having to touch the app code.
  3. If you trust your internal infrastructure, you can put all of your app configuration information on a single http(s) instance.
There are a few draw-backs, but they're relatively mild:
  1. Command line invocations are a little longer
  2. There are (potentially) multiple places to look when there's a configuration error
  3. A malformed JSON config file will cause your app to not launch.