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