Um guia desenvolvido pelo Heroku para SaaS
(Software entregue como serviço)
99% de tudo o que consumimos na internet.
How to:
Configuração é tudo o que é provável variar entre deploys (homologação, produção, ambientes de desenvolvimento, etc).
Uma aplicação que tem todas as configurações corretamente definidas pode ter seu código aberto ao público sem comprometer as credenciais.
settings.py
USERNAME = "admin"
PASSWORD = "1234"
SERVER_IP = "192.168.0.10"
DB_NAME = "customers"
app.py
import settings
print(settings.USERNAME)
print(settings.PASSWORD)
print(settings.SERVER_IP)
print(settings.DB_NAME)
Quais os problemas desta abordagem? ler variáveis de ambiente!
import os
USERNAME = os.environ.get("admin")
PASSWORD = int(os.environ.get('MEU_PASSWORD'))
...
# E mais 200 variaveis....
Existem libs para facilitar:
PASSWORD = config('PASSWORD', cast=int)
Mas vc ainda terá que explicitamente efetuar o carregamento de cada valor em cada uma das suas 200 variaveis de config, e se tiver dicionarios aninhados terá que fazer em cada nivel.
VALUE = config('VALUE', cast=str)
VALUE_2 = config('VALUE_2', cast=int)
VALUE_3 = config('VALUE_3', cast=float)
...
VALUE_200 = config('VALUE_200', cast=bool)
development, staging e production como separar?
MCGYVERISM ⬇
# settings.py
import os
USERNAME = "admin"
PASSWORD = int(os.environ.get('MEU_PASSWORD'))
SERVER_IP = "192.168.0.10"
DB_NAME = "customers"
# E mais 200 variaveis....
ENV = os.environ.get('MEU_ENV')
if ENV == 'production':
from production_settings import * # noqa
elif ENV == 'staging':
from staging_settings import * # noqa
# production_settings.py
SERVER_IP = 'prodserver.com'
# staging_settings.py
SERVER_IP = 'stagingserver.com'
não faça isso
Oprojeto é django e você agora tem as variaveis:
DEBUG = False
DATABASES = {
'default': {
'NAME': 'db',
'ENGINE': 'module.foo.engine',
'ARGS': {'timeout': 30}
}
}
como usar env vars para sobrescrever? ⬇
import os
true_values = ['True', 'true', '1', 'on', 'enabled']
DEBUG = os.environ.get('DEBUG') in true_values
DATABASES = {
'default': {
'NAME': os.environ.get('DB_NAME', 'db'),
'ENGINE': os.environ.get('DB_ENGINE', 'module...'),
'ARGS': {'timeout': os.environ.get('DB_TIMEOUT', 30)}
}
}
Funciona!
…mas imagina que vc precisa fazer isso em mais 200 variavéis com tipos complexos?
import hvac # Vault Project Client for Python
client = hvac.Client()
client = hvac.Client(url='localhost', token='myroot')
# Guardando chaves no vault
client.write('secret/snakes', type='pythons', lease='1h')
# lendo chaves do vault
print(client.read('secret/snakes'))
É tranquilo de acessar mas será preciso escrever o código de cliente e então gerenciar manualmente os ambientes, as coleções e leases.
docs: http://dynaconf.readthedocs.io
Curiosidade, pq se chama Dynaconf?
# opções: [all,yaml,ini,redis,vault]
pip install 'dynaconf[yaml,vault,redis]'
$ export DYNACONF_HELLO=world
$ dynaconf list
Working in development environment
HELLO: 'world'
DYNACONF_
é o prefixo padrão mas pode ser alterado.
O Dynaconf procura na arvore de dirtórios do projeto por arquivos nomeados settings.*
a extensão pode ser yaml,toml,py,json,ini,xml,cfg
Criamos o arquivo ou usamos o Dynaconf CLI.
$ dynaconf init -f yaml \
-v username=admin \
-v server_ip=localhost \
-v db_name=customers \
-s password=1234
⬇
O dynaconf irá criar um arquivo settings.yaml
default:
DB_NAME: default
SERVER_IP: default
USERNAME: default
development:
DB_NAME: customers
SERVER_IP: localhost
USERNAME: admin
Repare que o password não está incluido neste arquivo onde ele está?
⬇
.secrets.yaml
default:
PASSWORD: default
development:
PASSWORD: 1234
pra quê serve este .secrets.* ?
⬇
.gitignore
# Ignore dynaconf secret files
.secrets.*
Ficou mais fácil de ignorar o arquivo .secrets.*
para não ser enviado para o git e então cada ambiente pode ter seu arquivo de secrets.
O mais recomendado é usar o Vault Server que veremos adiante.
⬇
Mantendo apenas os arquivos .yaml
veremos se funcionou.
$ dynaconf list
Working in development environment
DB_NAME: 'customers'
SERVER_IP: 'localhost'
USERNAME: 'admin'
PASSWORD: 1234
⬇
Para adicionar mais environments podemos simplesmente editar os arquivos .yaml
e adicionar nossos envs, ex: ['production']
Mas também podemos usar o CLI:
$ dynaconf write yaml -e production \
-v server_ip=prodserver -s password=9999
⬇
# settings.yaml
default:
DB_NAME: default
SERVER_IP: default
USERNAME: default
development:
DB_NAME: customers
SERVER_IP: localhost
USERNAME: admin
production:
SERVER_IP: prodserver
# secrets.yaml
default:
PASSWORD: default
development:
PASSWORD: 1234
production:
PASSWORD: 9999
⬇
Para listar somente as variavéis de production:
$ export ENV_FOR_DYNACONF=production
$ dynaconf list
Working in production environment
DB_NAME: 'default'
SERVER_IP: 'prodserver'
USERNAME: 'default'
PASSWORD: 9999
Dynaconf está inicializado com sucesso! e agora?
⬇
Se tentarmos executar o programa app.py
$ python app.py
Traceback (most recent call last):
File "app.py", line 1, in <module>
import settings
ModuleNotFoundError: No module named 'settings'
Precisamos alterar o programa para usar
dynaconf.settings
no lugar de
settings
-import settings
+from dynaconf import settings
⬇
Repare que a linha
assert isinstance(settings.PASSWORD, int)
continua funcionando, isto ocorre pois o Dynaconf automaticamente detecta o tipo de dados das variáveis.
Para alternar entre os ambientes basta exportar a variavel ENV_FOR_DYNACONF
export ENV_FOR_DYNACONF=production
ou então colocar em seu arquivo .env
⬇
Recapitulando
Para implantar o Dynaconf basta:
from dynaconf import settings
O guia 12 factor apps deixa bem claro que não é o ideal ter configurações em arquivos como fizemos anteriormente.
⬇
Às vezes, as aplicações incluem a configuração em grupos nomeados, chamados de ambientes que remetem a deploys específicos, tais como os ambientes development, test, e production. Quanto mais deploys da aplicação são criados, novos nomes de ambiente são necessários, tais como staging ou qa.
⬇
Em uma app doze-fatores, env vars são controles granulares, cada um totalmente ortogonal às outras env vars. Elas nunca são agrupadas como “environments”, mas em vez disso são gerenciadas independentemente para cada deploy. Este é um modelo que escala sem problemas à medida que o app naturalmente se expande em muitos deploys durante seu ciclo de vida.
⬇
Dynaconf prioriza variáveis de ambiente!
Isto significa que seu projeto pode ter os arquivos settings.*
apenas para armazenar os valores [default]
e então as variáveis de ambiente sempre serão a fonte definitiva de configurações.
Deixando apenas os valores default
# settings.yaml
default:
DB_NAME: default
SERVER_IP: default
USERNAME: default
# .secrets.yaml
default:
PASSWORD: default
$ dynaconf list
Working in development environment
DB_NAME: 'default'
SERVER_IP: 'default'
USERNAME: 'default'
PASSWORD: 'default'
⬇
No ambiente onde a aplicação irá rodar
estes valores estarão nas variáveis de ambiente.
DYNACONF_PASSWORD=8888 DYNACONF_DB_NAME=foo dynaconf list
Working in development environment
DB_NAME: 'foo'
SERVER_IP: 'default'
USERNAME: 'default'
PASSWORD: 8888
ou
$ DYNACONF_PASSWORD=8888 DYNACONF_DB_NAME=foo python app.py
default
8888
default
foo
⬇
Ao invés de ficar passando esses valores na linha de comando pode persistir exportando diretamente em seu processo de deploy.
$ export DYNACONF_PASSWORD=7777
$ export DYNACONF_DB_NAME=mydb
Ou colocar no arquivo .env
na raiz do projeto.
DYNACONF_PASSWORD=7777
DYNACONF_DB_NAME=mydb
o Dynaconf utiliza o formato toml
ao ler as variáveis de ambiente, isso quer dizer que:
export DYNACONF_STR=Bruno == `str('Bruno')`
export DYNACONF_INT=36 == `int(36)`
export DYNACONF_FLOAT=42.1 == `float(42.1)`
export DYNACONF_BOOL=true == `bool(True)`
export DYNACONF_LIST=[1,2,3] == `list([1,2,3])`
export DYNACONF_DICT={foo="bar"} == `dict({"foo": "bar"})`
Dá para forçar o tipo se for preciso:
export DYNACONF_STR_NUM="'32'" == `str('32')`
export DYNACONF_INT_NUM="'@int 32'" == `int(32)`
⬇
E além disso o dynaconf tem suporte a nested types
, lembra o exemplo dos databases do Django?
default:
DATABASES:
default:
NAME: db
ENGINE: module.foo.engine
ARGS:
timeout: 30
Podemos alterar o ENGINE
via environment variables usando __
(dunder):
export DYNACONF_DATABASES__default__ENGINE=other.module
veja mais no exemplo de Django no final.
Agora imagine no nosso app.py
rodando em cloud e se conectando no banco de dados 192.168.0.1
default:
...
SERVER_IP: 192.168.0.1
...
Agora imagine que a aplicação está replicada em 200 instancias para aguentar a carga e então você precisa mudar o SERVER_IP
para 10.10.10.1
E agora?
⬇
settings.yaml
Mas… happens!
Você e o reviewer não prestaram atenção e na verdade tinha que mudar também o USERNAME
- arquivos de settings
+ servidor Redis, Etcd, Vault, etc...
O Dynaconf já possui suporte built in para Redis e Vault mas é muito tranquilo customizar adicionando seu próprio loader.
⬇
Localmente podemos usar o docker:
$ docker run -d -p 6379:6379 redis
Isso vai subir um Redis em localhost:6379 que é o suficiente para desenvolver e testar.
⬇
Configs dinâmicas no Redis.
$ export REDIS_ENABLED_FOR_DYNACONF=1
$ dynaconf write redis \
-v server_ip=10.1.1.1 -v username=newuser -s password=8989
Data successful written to redis
$ dynaconf list
Working in development environment
DB_NAME: 'default'
SERVER_IP: '10.1.1.1'
USERNAME: 'newuser'
PASSWORD: 8989
$ python app.py
newuser
8989
10.1.1.1
default
⬇
agora ao invés de alterar os valores em cada uma das instancias basta alterar diretamente no servidor Redis e então todas as instâncias irão ler do mesmo local.
use
settings.get('SERVER_IP', fresh=True)
para forçar a leitura da variavel atualizada diretamente do Redis ou
export FRESH_VARS_FOR_DYNACONF=['SERVER_IP']
para forçar essa variável a ser sempre fresh!
O Vault é um servidor de segredos da HashiCorp ele resolve algumas questões na gestão de valores sensiveis como passwords e tokens.
⬇
Para desenvolvimento e testes:
$ docker run -d -e 'VAULT_DEV_ROOT_TOKEN_ID=myroot' -p 8200:8200 vault
⬇
Env vars para ativar o vault.
# .env
VAULT_ENABLED_FOR_DYNACONF=1
VAULT_TOKEN_FOR_DYNACONF=myroot
Para escrever no vault server
$ dynaconf write vault \
-s password=secret -s server_ip=server.com -e production
Data successful written to vault
⬇
Agora pode acessar o painel web do vault via http://localhost:8200 usando o token myroot
pip install Flask
export FLASK_APP=app.py
export FLASK_ENV=development
app.py
from flask import Flask
app = Flask(__name__)
app.config['USERNAME'] = 'Bruno'
@app.route('/')
def hello():
return app.config['USERNAME']
$ flask run
...
http://localhost:5000/
⬇
Como fazer para o app.config
ler seus valores através do dynaconf?
settings.toml
[default]
username = 'Bruno'
from flask import Flask
from dynaconf import FlaskDynaconf
app = Flask(__name__)
FlaskDynaconf(app)
@app.route('/')
def hello():
return app.config.USERNAME
⬇
ou env vars
export FLASK_USERNAME='Guido Van Rossum'
ao usar a extensão flask o prefixo para variaveis de ambiente passa a ser FLASK ao invés de DYNACONF_
Todos os recursos do Dynaconf funcionam normalmente dentro do Flask
pip install django
django-admin startproject django_app
cd django_app
python manage.py runserver
acesse: http://localhost:8000/
no settings.py
do django_app o DEBUG
está ativado veja: http://localhost:8000/naoexiste vamos mudar isso usando o Dynaconf
⬇
Passo 1 ative o dynaconf.
$dynaconf init --django django_app/settings.py
Cofiguring your Dynaconf environment
django_app/settings.py is found do you want to add dynaconf? [y/N]: y
Pronto agora o Dynaconf está ativado no seu Django e você pode usar.
⬇
export DJANGO_DEBUG=false
export DJANGO_ALLOWED_HOSTS=['*']
python manage.py runserver
Agora toda variavel exportada com o prefixo DJANGO_
será lida pelo dynaconf e acessivel via o django.conf.settings