---
tags: python, dynaconf, flask, django
---
# 12 factor apps - Dynaconf
## Agenda
- 12 factor apps
- Configurações sem o Dynaconf
- Usando Dynaconf para resolver
## 12 factor apps
Um guia desenvolvido pelo Heroku para SaaS (Software entregue como serviço) a.k.a: 99% de tudo o que consumimos na internet.
Garantias:
- Deploy inicial declarativo (Orquestração)
- **Ansible**, **Terraform**
- Contrato claro com sistema operacional e ambientes suportados
- **Containers**
- Portável em plataformas de nuvem distintas (agnostico a interfaces de ambiente)
- **Kubernetes**, **configurações dinâmicas**
- Minimiza a divergência entre produção e desenvolvimento permitindo a entrega continua.
- **Containers**, **Microserviços**, **Configurações dinâmicas**
- Escala com mínimo esforço
- **Kubernetes**, **auto scaling**
Os 12 fatores https://12factor.net/pt_br/
1. Base de código versionada e distribuida.
- Github, cada serviço em seu repositório.
3. Declaração de dependencias.
- pip, poetry, lock files, pipenv, etc..
5. Configurações por ambiente.
- **Dynaconf**, Python Decouple, Everett
## Gerenciando settings sem o Dynaconf
### Arquivo settings sem o Dynaconf
`settings.py`
```py
USERNAME = "admin"
PASSWORD = "1234"
SERVER_IP = "192.168.0.10"
DB_NAME = "customers"
```
`app.py`
```py
import settings
print(settings.USERNAME)
print(settings.PASSWORD)
print(settings.SERVER_IP)
print(settings.DB_NAME)
```
:::danger
- Password exposto para o git :key:
- O formato do password é `str` ou `int`?
:::
#### Como resolver esses problemas sem o Dynaconf?
:bulb: ler variáveis de ambiente!
`settings.py`
```python
import os
USERNAME = "admin"
PASSWORD = int(os.environ.get('MEU_PASSWORD'))
SERVER_IP = "192.168.0.10"
DB_NAME = "customers"
# E mais 200 variaveis....
```
> algumas libs como o Python-Decouple oferecem uma solução parecida onde vc utiliza o decouple para a leitura das variaveis de ambiente. `PASSWORD = decouple.config('PASSWORD', cast=int)`.
:::warning
Qual o problema desta abordagem? **nenhum**!!!
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.
:::
#### Como lidar com multiplos ambientes sem o Dynaconf?
Então você tem ambiente de `development`, `staging` e `production` como fazer com o arquivo de settings?

:::success
**MCGYVERISM**
:::
`settings.py`
```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`
```py
SERVER_IP = 'prodserver.com'
```
`staging_settings.py`
```py
SERVER_IP = 'stagingserver.com'
```
:::danger
:arrow_up: não faça isso :-1:
:::
#### Variáveis de ambiente e tipos complexos de dados
Imagine então que o seu projeto é **django** e você agora tem uma variáveis assim:
```python
DEBUG = False
DATABASES = {
'default': {
'NAME': 'db',
'ENGINE': 'module.foo.engine',
'ARGS': {'timeout': 30}
}
}
```
Então agora você quer usar variáveis de ambiente para sobrescrever esses valores.
```py
import os
DEBUG = os.environ.get('DEBUG') in ['True', 'true', '1', 'on', 'enabled']
DATABASES = {
'default': {
'NAME': os.environ.get('DATABASES_NAME', 'db'),
'ENGINE': os.environ.get('DATABASES_ENGINE', 'module.foo.engine'),
'ARGS': {'timeout': os.environ.get('DATABASES_TIMEOUT', 30)}
}
}
```
:::warning
:ok: Funciona!
...mas imagina que vc precisa fazer isso em mais 200 variavéis com tipos complexos? :cry:
:::
#### Gestão segura de segredos sem Dynaconf
**Vault Server**
É tranquilo de acessar mas será preciso escrever o código de cliente e então gerenciar manualmente os ambientes, as coleções e leases.
```python
import hvac
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'))
```
---
## Usando Dynaconf para resolver :wrench:
Primeiro instale o dynaconf
```bash
# opções: [all,yaml,ini,redis,vault]
pip install 'dynaconf[yaml,vault,redis]'
```
### Quick demo com env vars
```bash
$ export DYNACONF_HELLO=world
$ dynaconf list
Working in development environment
HELLO: 'world'
```
### Armazenando settings com Dynaconf
Crie o arquivo `settings.yaml` (pode ser qualquer outro formato suportado: yaml, toml, py, json, ini, xml, etc...)
ou utilize o `$ dynaconf init` na raiz do projeto.
```shell
$ 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`
```yaml
default:
DB_NAME: default
SERVER_IP: default
USERNAME: default
development:
DB_NAME: customers
SERVER_IP: localhost
USERNAME: admin
```
:::info
Repare que o **password** não está incluido neste arquivo onde ele está?
:::
`.secrets.yaml`
```yaml
default:
PASSWORD: default
development:
PASSWORD: 1234
```
> :question: pra quê serve este .secrets.* ?
`.gitignore`
```shell
# Ignore dynaconf secret files
.secrets.*
```
:::success
Nada de especial, mas agora fica 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)
:::
> :flashlight: podemos manter o `settings.py` pois o dynaconf vai ler este arquivo, mas o recomendado é ter arquivos estáticos como configurações e não usar arquivos Python que permitem a mistura de lógica com dados.
Apague os arquivos `settings.py` e `production_settings.py`
:wrench: Hora de ver se funcionou!
```shell
$ 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:
```shell
$ dynaconf write yaml -v server_ip=prodserver -s password=9999 -e production
```
então `settings.yaml`
```yaml
default:
DB_NAME: default
SERVER_IP: default
USERNAME: default
development:
DB_NAME: customers
SERVER_IP: localhost
USERNAME: admin
production:
SERVER_IP: prodserver
```
e `.secrets.yaml`
```yaml
default:
PASSWORD: default
development:
PASSWORD: 1234
production:
PASSWORD: 9999
```
E então para listar somente as variavéis de production:
```shell
$ ENV_FOR_DYNACONF=production dynaconf list
Working in production environment
DB_NAME: 'default'
SERVER_IP: 'prodserver'
USERNAME: 'default'
PASSWORD: 9999
```
:::success
:fireworks: Dynaconf está inicializado com sucesso! e agora?
:::
Se tentarmos executar o programa `app.py`
```shell
$ 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 o `dynaconf.settings` no lugar do `settings` e isso é muito fácil:
```diff
-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`
> :flashlight: também é possivel mudar essa variavel para algo mais amigavel tipo `MEUAPP_ENV=development`
:::info
**Recapitulando**
Para implantar o Dynaconf basta:
1. instalar o dynaconf
2. opcionalmente ter alguns arquivos settings e secrets (mas também funciona só com env vars)
3. Alterar uma única linha de código no seu programa `from dynaconf import settings`
:::
### Variaveis de ambiente
O guia **12 factor apps** deixa bem claro que não é o **ideal** ter configurações em arquivos como fizemos anteriormente.
> Outro aspecto do gerenciamento de configuração é o agrupamento. Às vezes, as aplicações incluem a configuração em grupos nomeados (muitas vezes chamados de ambientes) que remetem a deploys específicos, tais como os ambientes development, test, e production. Este método não escala de forma limpa: quanto mais deploys da aplicação são criados, novos nomes de ambiente são necessários, tais como staging ou qa. A medida que o projeto cresce ainda mais, desenvolvedores podem adicionar seus próprios ambientes especiais como joes-staging, resultando em uma explosão combinatória de configurações que torna o gerenciamento de deploys da aplicação muito frágil.
> Em uma aplicação 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.
>
>
> :notebook: [12 factor app/Config](https://12factor.net/pt_br/config "12 factor app config")
:::success
:trophy: 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.
Exemplo:
1. Primeiro vamos limpar os nossos arquivos
`settings.yaml`
```yaml
default:
DB_NAME: default
SERVER_IP: default
USERNAME: default
```
`.secrets.yaml`
```yaml
default:
PASSWORD: default
```
E rodar `dynaconf list`
```
Working in development environment
DB_NAME: 'default'
SERVER_IP: 'default'
USERNAME: 'default'
PASSWORD: 'default'
```
E então no ambiente onde a aplicação irá rodar estes valores serão lidos através das 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.
```bash
$ export DYNACONF_PASSWORD=7777
$ export DYNACONF_DB_NAME=mydb
```
Ou colocar no arquivo `.env` na raiz do projeto.
`.env`
```env
DYNACONF_PASSWORD=7777
DYNACONF_DB_NAME=mydb
```
#### Variáveis de ambiente e tipos complexos
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"})`
E também dá para forçar o tipo quando necessário usando multi quotes:
- `export DYNACONF_STR_NUM="'32'"` == `str('32')`
e além disso o dynaconf tem suporte a `nested types`, lembra o exemplo dos databases do Django?
```yaml
default:
DATABASES:
default:
NAME: db
ENGINE: module.foo.engine
ARGS:
timeout: 30
```
Podemos alterar o `ENGINE` via environment variables usando `__` (dunder):
```bash
export DYNACONF_DATABASES__default__ENGINE=other.module
```
:::info
:heavy_plus_sign: veja mais no exemplo de Django no final.
:::
### Configurações dinâmicas
Agora imagine no nosso `app.py` rodando em **cloud** e se conectando no banco de dados `192.168.0.1`
```yaml
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`
:::danger
:construction_worker: E agora?
:::
- Altera os arquivos `settings.yaml`
- Abre um Pull Request
- Espera o code review :eyeglasses:
- PR merged :ok_hand:
- Deploy efetuado :computer:
- 200 instancias reiniciadas
Mas... :shit: happens!
Você e o reviewer não prestaram atenção e na verdade tinha que mudar também o `USERNAME` :neutral_face:
#### Configurações dinâmicas com Redis ou Etcd
Ao invés das configurações ficarem armazenadas em arquivos ou no ambiente de cada uma das instancias podemos usar um servidor centralizado de configurações `Redis` ou `etcd` ou qualquer bancod e dados de chave-valor.
O Dynaconf já possui suporte built in para **Redis** e **Vault** mas é muito tranquilo customizar adicionando seu próprio loader.
##### Redis
1. Tenha um servidor redis em execução na sua plataforma/ambiente.
Localmente podemos usar o docker:
```shell
$ docker run -d -p 6379:6379 redis
```
Isso vai subir um Redis em `localhost:6379` que é o suficiente para desenvolver e testar.
Agora vamos escrever as nossas configs dinâmicas no Redis.
`.env`
```
REDIS_ENABLED_FOR_DYNACONF=1
```
então
```
$ dynaconf write redis -v server_ip=10.1.1.1 -v username=newuser -s password
=8989
Data successful written to redis
```
```bash
$ 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
```
:::success
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.
:::
> :flashlight: 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!
### Secrets (Vault)
O Vault é um servidor de segredos desenvolvido pela HashiCorp ele resolve algumas questões na gestão de valores sensiveis como passwords e tokens.
- É possivel **trancar** o vault para que ele seja lido apenas durante o deploy
- É possivel ter data de validade dos valores lidos (lease), forçando uma nova leitura no vault a cada x minutos.
- É possivel gerenciar os segredos via web panel
É excelente para times muito grandes, ao invés de fornecer as senhas de produção para todos os devs cada dev tem apenas acesso a uma instancia vault de desenvolvimento e ao fazer deploy apenas o orqustrador de deploy tem acesso as senhas reais via vault.
Para desenvolvimento e testes:
```bash
$ docker run -d -e 'VAULT_DEV_ROOT_TOKEN_ID=myroot' -p 8200:8200 vault
```
Altere o arquivo `.env` ou exporte as variaveis para ativar o vault.
`.env`
```env
VAULT_ENABLED_FOR_DYNACONF=1
VAULT_TOKEN_FOR_DYNACONF=myroot
```
Para escrever no vault server
```bash
$ 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`

### Extensões
#### Flask
```bash
pip install Flask
export FLASK_APP=app.py
export FLASK_ENV=development
```
`app.py`
```py
from flask import Flask
app = Flask(__name__)
app.config['USERNAME'] = 'Bruno'
@app.route('/')
def hello():
return app.config['USERNAME']
```
```bash
$ flask run
...
http://localhost:5000/
```
Como fazer para o `app.config` ler seus valores através do dynaconf?
`settings.toml`
```toml
[default]
username = 'Bruno'
```
```python
from flask import Flask
from dynaconf import FlaskDynaconf
app = Flask(__name__)
FlaskDynaconf(app)
@app.route('/')
def hello():
return app.config.USERNAME
```
ou
```bash
export FLASK_USERNAME='Guido Van Rossum'
```
> :flashlight: 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**
#### Django
```bash
pip install django
django-admin startproject django_app
cd django_app
python manage.py runserver
```
acesse: http://localhost:8000/
:::danger
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.
```bash
$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
```
:+1: **Pronto** agora o Dynaconf está ativado no seu Django e você pode usar.
```shell
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`
:smile: