# 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? ![Mcgyver](https://i.imgur.com/jSywBTU.jpg) :::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? ![](https://i.imgur.com/zmOfE1I.jpg) --- ## 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` ![vault web](https://i.imgur.com/Hqsu8ls.png) --- #### 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:
{"metaMigratedAt":"2023-06-15T00:04:47.032Z","metaMigratedFrom":"YAML","title":"12 factor apps - dynaconf","breaks":true,"description":"View the slide with \"Slide Mode\".","slideOptions":"{\"theme\":\"serif\",\"progress\":true}","contributors":"[{\"id\":\"82bc2a95-a263-40b7-9664-a10bdb2cf109\",\"add\":18839,\"del\":3458}]"}
    805 views