owned this note
owned this note
Published
Linked with GitHub
---
title: 12 factor apps - dynaconf
tags: dynaconf, python, django, flask
description: View the slide with "Slide Mode".
slideOptions:
theme: serif
progress: true
---
# 12 factor apps Dynaconf
<!-- Put the link to this slide here so people can follow -->
slide: https://hackmd.io/@rochacbruno/
---
## Bruno Rocha
- Quality Engineer @ **Red Hat**
- Member @ **PSF** :snake:
- Python, Rust
- Testes, Devops, Flask
- :bicyclist: :bread:
---
### Tópicos
- O que é **12 factor app**
- A importância das **Configurações**
- Como resolver apenas com **Just Python**
- **Dynaconf** para gestão de config e ambientes
---
## A Aplicação doze-fatores
### :globe_with_meridians: https://12factor.net/pt_br/
:::info
Um guia desenvolvido pelo Heroku para SaaS
(Software entregue como serviço)
*99% de tudo o que consumimos na internet.*
:::
---
### C.D.O
How to:
- **Code**
- **Deploy**
- **Operate**
---
### Garantias 12 fatores
- Deploy inicial **declarativo** (Orquestração).
- Contrato claro com **sistema operacional** e ambientes suportados.
- **Portável** em plataformas de nuvem distintas.
- Minimiza a divergência entre **ambientes** (pro e dev).
- **Escala** com mínimo esforço.
---
### Mas eu só vou falar de 3 fatores
1. Base de código versionada e distribuida.
- **Git**: cada serviço em seu repositório.
2. Declaração de dependencias.
- **pip, poetry**, lock files, pipenv, etc..
3. Configurações por ambiente.
- **Dynaconf**, Python Decouple, Everett
4. Leia do 4 ao 12 em https://12factor.net/pt_br/
---
#### A importância das configurações :wrench:
:::info
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 em Just Python
`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)
```
<span>
Quais os problemas desta abordagem?<br><!-- .element: class="fragment" data-fragment-index="1" -->
</span>
<span style="color:red;">
Password exposto para o git.<br><!-- .element: class="fragment" data-fragment-index="2" --></span>
<span style="color:red;">
O formato do password é `str` ou `int`?<br><!-- .element: class="fragment" data-fragment-index="3" -->
</span>
---
#### Resolvendo estes problemas com Just Python
==:bulb: ler variáveis de ambiente!==
```python
import os
USERNAME = os.environ.get("admin")
PASSWORD = int(os.environ.get('MEU_PASSWORD'))
...
# E mais 200 variaveis....
```
<span>
Existem libs para facilitar: <!-- .element: class="fragment" data-fragment-index="1" -->
</span>
<span>
<pre><code class="python hljs">PASSWORD = config('PASSWORD', cast=int)</code></pre><br><!-- .element: class="fragment" data-fragment-index="2" -->
</span>
<span style="color:red;">
Quais os problemas desta abordagem?<br><!-- .element: class="fragment" data-fragment-index="3" --></span>
<span style="color:green;">
Nenhum! ⬇ <br><!-- .element: class="fragment" data-fragment-index="4" -->
</span>
----
:::warning
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.
:::
```python
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)
```
---
##### Multiplos ambientes em Just Python
==development==, ==staging== e ==production== como separar?

:::success
**MCGYVERISM** ⬇
:::
----
```py
# 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
```
```py
# production_settings.py
SERVER_IP = 'prodserver.com'
```
```py
# staging_settings.py
SERVER_IP = 'stagingserver.com'
```
:::danger
:arrow_up: não faça isso :-1:
:::
---
#### Env Vars e tipos complexos com Just Python
Oprojeto é **django** e você agora tem as variaveis:
```python
DEBUG = False
DATABASES = {
'default': {
'NAME': 'db',
'ENGINE': 'module.foo.engine',
'ARGS': {'timeout': 30}
}
}
```
:::warning
:question: como usar env vars para sobrescrever? ⬇
:::
----
```py
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)}
}
}
```
:::warning
:ok: Funciona!
...mas imagina que vc precisa fazer isso em mais 200 variavéis com tipos complexos? :cry:
:::
---
#### Gestão de segredos com Just Python :lock:
```python
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'))
```
:::warning
É tranquilo de acessar mas será preciso escrever o código de cliente e então gerenciar manualmente os ambientes, as coleções e leases.
:::
---
# Dynaconf
docs: http://dynaconf.readthedocs.io
- Gestor de configurações
- Extensões para Flask e Django
- Suporte a Redis e Vault Server
- Fácil de ser customizado
- we :heart: env vars
---
:fireworks: Curiosidade, pq se chama Dynaconf?

---
## Quick Start
#### Instalação
```bash
# opções: [all,yaml,ini,redis,vault]
pip install 'dynaconf[yaml,vault,redis]'
```
#### Env vars com Dynaconf
```bash
$ export DYNACONF_HELLO=world
$ dynaconf list
Working in development environment
HELLO: 'world'
```
> `DYNACONF_` é o prefixo padrão mas pode ser alterado.
---
#### Gestão de settings com Dynaconf
:::info
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.
```bash
$ 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
```
:::warning
:question: pra quê serve este .secrets.* ?
:::
⬇
----
`.gitignore`
```shell
# Ignore dynaconf secret files
.secrets.*
```
:::success
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.
:::
:bulb: O mais recomendado é usar o Vault Server que veremos adiante.
⬇
----
Mantendo apenas os arquivos `.yaml` veremos 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 -e production \
-v server_ip=prodserver -s password=9999
```
⬇
----
```yaml
# settings.yaml
default:
DB_NAME: default
SERVER_IP: default
USERNAME: default
development:
DB_NAME: customers
SERVER_IP: localhost
USERNAME: admin
production:
SERVER_IP: prodserver
```
```yaml
# secrets.yaml
default:
PASSWORD: default
development:
PASSWORD: 1234
production:
PASSWORD: 9999
```
⬇
----
Para listar somente as variavéis de production:
```shell
$ export ENV_FOR_DYNACONF=production
$ dynaconf list
Working in production environment
DB_NAME: 'default'
SERVER_IP: 'prodserver'
USERNAME: 'default'
PASSWORD: 9999
```
:::success
:bulb: 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
`dynaconf.settings`
no lugar de
`settings`
```diff
-import settings
+from dynaconf import settings
```
⬇
----
Repare que a linha
```python
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`
```bash
export ENV_FOR_DYNACONF=production
```
ou então colocar em seu arquivo `.env`
⬇
----
:::info
**Recapitulando**
Para implantar o Dynaconf basta:
1. instalar o **dynaconf**
2. opcionalmente criar arquivos settings.*
(mas também funciona só com env vars)
3. Alterar uma única linha de código no seu programa:
```python
from dynaconf import settings
```
:::
---
### Variaveis de ambiente
:::info
O guia **12 factor apps** deixa bem claro que não é o **ideal** ter configurações em arquivos como fizemos anteriormente.
:::
⬇
----
:::warning
À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.
:::
⬇
----
:::warning
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.
:::
: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.
---
Deixando apenas os valores `default`
```yaml
# settings.yaml
default:
DB_NAME: default
SERVER_IP: default
USERNAME: default
```
```yaml
# .secrets.yaml
default:
PASSWORD: default
```
```bash
$ dynaconf list
```
```bash
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.
```bash
DYNACONF_PASSWORD=8888 DYNACONF_DB_NAME=foo dynaconf list
Working in development environment
DB_NAME: 'foo'
SERVER_IP: 'default'
USERNAME: 'default'
PASSWORD: 8888
```
ou
```bash
$ DYNACONF_PASSWORD=8888 DYNACONF_DB_NAME=foo python app.py
default
8888
default
foo
```
⬇
----
:::info
:bulb: 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
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:
```bash
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:
```bash
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?
```yaml
default:
DATABASES:
default:
NAME: db
ENGINE: module.foo.engine
ARGS:
timeout: 30
```
:::info
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!
:::danger
Você e o reviewer não prestaram atenção e na verdade tinha que mudar também o `USERNAME` :neutral_face:
:::
---
## Redis #ftw
```diff
- 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.
⬇
----
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.
⬇
----
Configs dinâmicas no Redis.
```bash
$ 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
```
```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
:trophy: 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.
:::
:::info
:bulb: use
```python
settings.get('SERVER_IP', fresh=True)
```
para forçar a leitura da variavel atualizada diretamente do Redis ou
```bash
export FRESH_VARS_FOR_DYNACONF=['SERVER_IP']
```
para forçar essa variável a ser sempre fresh!
:::
---
### Secrets (Vault)
:::info
:lock: O Vault é um servidor de segredos da 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**
⬇
----
Para desenvolvimento e testes:
```bash
$ docker run -d -e 'VAULT_DEV_ROOT_TOKEN_ID=myroot' -p 8200:8200 vault
```
⬇
----
:::info
Env vars 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`

---
#### 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 env vars
```bash
export FLASK_USERNAME='Guido Van Rossum'
```
:::info
:bulb: 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`
---
http://dynaconf.readthedocs.io
:smile: