Less confusion with direnv

Separate config and code Link to heading

When starting out on a fresh project we always prioritise speed over correctness/best practice. This is no more evident when it comes to configuration in code. Often in our eagerness to bring our idea to life we store our configuration as close to our code as possible. Things like database credentials and API tokens are hard coded as constants in our source code to reduce friction.

The Twelve-Factor App highlights this as a violation of the twelve-factor app, which requires “strict separation of config from code”. There are several approaches to implement the separation of config and code. For example, in Django, configuration is included in the settings.py module. One approach would be to have a development settings file checked into the repo and have a production module for when the application is deployed. Whilst much better than storing database passwords in constant variables it is still risky. You could imagine with this solution how the production file could be accidentally committed to a repo and exposed to the outside world.

A tidier solution would be to have a settings module that reads in environment specific values from the environment. In Python, os.environ can be used to implement this solution. There are other more sophisticated solutions, such as python-env.

For Django, we would have one settings.py file which reads secret values from the environment. There are several ways to add variables to your environment.

Configuring Apps using Environment Variables Link to heading

An initial solution would be to add the secret environment variables to .bashrc or .zshrc. This would set the environment variables globally and doesn’t scale very well, it becomes more difficult if you worked on two or more projects that had the same variables to be set.

A better solution is to have a collection of scripts to set and unset project environment variables.

# set.sh
export DATABASE_NAME=database_name
export DATABASE_PASSWORD=my_secret_password
export DATABASE_USER=user

# unset.sh
unset DATABASE_NAME
unset DATABASE_PASSWORD
unset DATABASE_USER

When starting work on a project you would run the set.sh script. Once finished we’d run the unset.sh script before beginning work on another codebase.

This is better than having global environment variables but requires the developer to remember to run the scripts before and after working on a codebase.

If only there was a way to do this for us automatically?

Using direnv for Development Link to heading

This is where direnv comes in. Once configured and activated it automatically sets and un-sets environment variables when the developer changes into the project directory.

direnv works with both macOS and Linux. It comes with a bash installer that simplifies the installation process.

curl -sfL https://direnv.net/install.sh | bash

Once installed direnv then needs to be configured to hook into your shell. The instructions for this can be found here.

To use direnv simply create a .envrc file in the root of project. The .envrc will contain all the environment variables you wish to export into the environment.

export DATABASE_NAME=database_name
export DATABASE_PASSWORD=my_secret_password
export DATABASE_USER=user

As a security precaution direnv won’t automatically activate these environment variables initially. On adding the environment variables to the .envrc file the developer must run direnv allow to signal to direnv that it is okay to export the environment variables in the file. This command only needs to be run if the file changes.

Now every time you change into the directory (or child directory) the environment variables will be automatically exported. Similarly, on leaving the directory the environment variables will be unset.

Since the application settings are being read from environment variables no code has to be changed to accommodate deployment to production.

direnv has more settings to improve your workflow. For example it has the edit sub-command that will open the .envrc file in your editor choice and automatically run direnv allow when the file is saved. To take advantage of this sub-command ensure that the EDITOR environment variable is set.

export EDITOR=vim
direnv edit

In this example .envrc will be opened with vim, upon saving and exiting vim, direnv allow is run.

direnv also comes with a set of functions that make command tasks easier to complete. PATH_add is a function that can be used in .envrc files to easily prepend paths to the users $PATH. Imagine your project contains a bin directory with a set of helper scripts that you want to call only when working on your current project. In the .envrc file we could call the PATH_add function to add ./bin to our PATH only when in the project directory.

# .envrc
PATH_add ./bin

direnv contains lots of other useful functions in its standard library.