heldercorreia.com

autodidata, programador e consultor

Segredos no ambiente

Daniel Greenfeld e Audrey, no seu livro de boas práticas de Django, advocam the one true way [1], apresentado pelo co-BDFL do Django [2] Jacob Kaplan-Moss.

A ideia é simples. Separar as configurações por diferentes ambientes e guardar os valores mais sensíveis em variáveis de ambiente, fora do Git.

Porém, sem o uso de containers, como isolamos (verdadeiramente) essas variáveis de projeto para projeto?

The One True Way

Não é o meu propósito mencionar porque esta é a forma mais correta de organizar as configurações do Django. O Jacob Kaplan-Moss dá alguma explicação na sua apresentação.

Em vez de um ficheiro settings.py criamos um pacote com um módulo por ambiente:

settings
|-- __init__.py
|-- base.py
|-- dev.py
|-- staging.py
`-- prod.py

O base.py tem todas as configurações comuns, e os outros ficheiros fazem override a configurações específicas:

# base.py
INSTALLED_APPS = [...]

# dev.py
from .base import *
INSTALLED_APPS += ['debug_toolbar']

O Django permite especificar qual o módulo de configuração que deve usar, seja através da opção --settings= do django-admin.py (ou manage.py) ou da variável de ambiente DJANGO_SETTINGS_MODULE.

—settings=

Há quem sugira eliminar o ficheiro manage.py, que essencialmente é como o django-admin.py, mas cria um valor por defeito para a variável de ambiente DJANGO_SETTINGS_MODULE e adiciona o diretório onde se encontra, ao sys.path.

Então ambos os seguintes comandos são a mesma coisa, assumindo que o módulo configurado no manage.py é settings.dev:

$ python manage.py shell
$ # ou
$ django-admin.py shell --pythonpath=`pwd` --settings=settings.dev

DJANGO_SETTINGS_MODULE

Torna-se aborrecido ter que adicionar o --pythonpath e --settings em todos os comandos, portanto é aqui que a variável de ambiente ajuda.

Então assumindo que usamos o virtualenv, podemos eliminar a necessidade de estar sempre a definir essas duas opções:

$ add2virtualenv . # para adicionar o caminho atual ao sys.path
$ echo "export DJANGO_SETTINGS_MODULE=settings.dev" > $VIRTUAL_ENV/bin/postactivate
$ echo "unset DJANGO_SETTINGS_MODULE" > $VIRTUAL_ENV/bin/postdeactivate
$ django-admin.py shell

Assim funciona de qualquer sítio, desde que o virtualenv esteja ativo.

Problemas

Quando me deparei com isto pela primeira vez, achei um bocado chato estar a adicionar e a remover as variáveis de ambiente no virtualenv dessa forma.

Não basta o módulo das configurações, temos também que adicionar outras variáveis como as credenciais da base de dados, do email e outra informação que precisa estar no ambiente.

Também não é a forma mais limpa, porque as variáveis não estão realmente isoladas do resto do sistema. Como uso o virtualenvwrapper, por vezes mudo de um ambiente para outro através da função workon:

(project1) /Projects/project1/ $ workon project2
(project2) /Projects/project2/ $ echo $VIRTUAL_ENV
project2

Assim, mudo de um projeto para outro, incluindo para o seu respetivo diretório com muita facilidade.

Mas, uma vez que ainda estou na mesma tab do terminal, se eu não removi todas as variáveis que defini no ambiente anterior, elas continuam definidas neste e isso pode ser difícil depurar.

Apesar disso, o que me fez realmente passar a usar a alternativa, foi quando tive que meter em produção no Apache. Os processos que correm pelo Apache não lêem as variáveis de ambiente do sistema, é preciso defini-las na própria configuração do VirtualHost com as diretivas SetEnv.

Melhor alternativa

Daniel Greenfeld e Audrey propõem o uso de um ficheiro com formato simples, tipo JSON, para ler as variáveis de configuração nas settings, em vez de variáveis de ambiente, para casos onde não as podemos usar, como o Apache.

Por um lado, tenho projetos no Heroku, onde é incentivado o uso de variáveis de ambiente. Por outro, também uso o Apache, onde devo usar um ficheiro JSON. Eu até acho útil o uso de variáveis de ambiente, mas também o uso de um ficheiro simples que define essas mesmas variáveis. Então decidi juntar as duas soluções numa só.

Agora uso este sistema para qualquer projeto Django, e funciona em qualquer ambiente.

Em vez do JSON, eu uso um ficheiro ainda mais simples na raiz do projeto, com simples pares chave e valor. Depois com python, percorro esse ficheiro e insiro as variáveis no ambiente local com os.environ[key] = value:

# core/utils.py

import os
from unipath import Path

def load_environment(env_str = None, file = '.env'):
    """
    Set default environment variables from a string, or file.

    Must have the following syntax::

        VARIABLE1=value1
        VARIABLE2=value2
    """

    if not env_str:
        try:
            env_str = Path(file).read_file()
        except IOError:
            return

    for line in env_str.splitlines():
        key, value = line.split('=', 1)
        os.environ[key] = value

Uso essa função onde preciso das variáveis de ambiente, i.e., manage.py e wsgi.py.

manage.py

#!/usr/bin/env python
import sys

if __name__ == "__main__":
    from core.utils import load_environment
    load_environment()

    from django.core.management import execute_from_command_line
    execute_from_command_line(sys.argv)

wsgi.py

# Load environment
from core.utils import load_environment
load_environment()

# Get WSGI handler
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()

base.py

import os

...

SECRET_KEY = os.environ['SECRET_KEY']

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': os.environ['DB_NAME'],
        'USER': os.environ['DB_USER'],
        'PASSWORD': os.environ['DB_PASS'],
        'HOST': os.environ['DB_HOST'],
        'PORT': os.environ['DB_PORT'],
    },
}

...

.env

DJANGO_SETTINGS_MODULE=settings.dev
DB_NAME=db1
DB_USER=root
DB_PASS=
DB_HOST=
DB_PORT=
EMAIL_HOST_USER=
EMAIL_HOST_PASSWORD=
DEFAULT_FROM_EMAIL=
SECRET_KEY=fjqDE]jnVjRbU9mva9,MewN*;=AwsKshgt5jmc9mq@t(_))0f5

Foreman

Há um motivo de ter escolhido o nome .env para o ficheiro das variáveis. É suportado nativamente pelo foreman. Quem o usa, nem precisa da função load_environment() (e.g. é usado pelo Heroku).

[1]Slides 50 e 51.
[2]Reformado do título de BDFL desde janeiro.

Comentários