Introduction

Django doesn't have a default strategy for organizing settings files.

One of the more common approaches is to create a set of baseline settings and create Python modules to extend them -- either with local overrides, or with pre-built configurations -- for each environment in which the project project may run.

This approach is simple enough, but file-based configurations are annoying when deploying to Kubernetes, and overloading settings can lead to subtle bugs. And in the case of pre-built configurations you still have to deal with secrets somehow.

We prefer using a single settings.py for everything and to rely on the environment to provide environment-specific configurations. Environment variables are operative system agnostic, easy to change, play well with Kubernetes, and allow us to separate the configuration from the code.

Working with environment variables, however, can be a bit tricky. There's very limited support for different data types, and os.environ leaves a lot to be desired.

Django environ

django-environ is a package that contains a couple of convenient features for working with environment variables in Django projects. It supports plenty of types (e.g. str, bool, url, json, path), can provide default values, source .env files, and more.

Consider the following settings file:

from pathlib import Path

import environ

BASE_DIR = Path(__file__).resolve().parent.parent

env = environ.Env()

# Read local `.env` file if it exists.
environ.Env.read_env(BASE_DIR / ".env")

DEBUG = env.bool('DEBUG', default=False)

The use of an environment variable in the example above allows us to control the DEBUG-setting in a couple of different ways:

  • DEBUG=True ./manage.py runserver
  • export DEBUG=True before running a command.
  • Add DEBUG=True to a file named .env in the project root.
  • Expose a ConfigMap or Secret in Kubernetes as an environment variable.

Keep configuration to a minimum, add default values wherever possible, and keep the configuration as similar as possible between different environments to make errors less likely:

KEYCLOAK_URI = env.str('KEYCLOAK_URI', default="<https://kc.auth.svc.cluster.tld/>")
KEYCLOAK_REALM = env.str('KEYCLOAK_REALM', default="default_realm_name")
KEYCLOAK_CLIENT_ID = env.str('KEYCLOAK_CLIENT_ID', default="service_name")
KEYCLOAK_CLIENT_SECRET = env.str('KEYCLOAK_CLIENT_SECRET')

In the above scenario all environments (e.g. local, development, staging, production) use the same internal URI to the server, realm, and client id. With only the client secret to configure we've reduced the risk of misconfiguration.

Another useful feature is support for more complex data structures. You can, for example, assign a database connection string to an environment variable:

DATABASE_URL="psql://user:password@host:port/dbname"

And read it as a database configuration dict:

DATABASES = {
    'default': env.db(),
}

Take a look at the API Reference for some inspiration.

Want to know more about how we can work together and launch a successful digital energy service?