---
# System prepended metadata

title: 12 factor apps - Dynaconf
tags: [' dynaconf', ' flask', python, ' django']

---

---
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?

![](https://i.imgur.com/jSywBTU.jpg)


:::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`

![vault web](https://i.imgur.com/Hqsu8ls.png)

### 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: 


