Try   HackMD

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.
  2. Declaração de dependencias.
    • pip, poetry, lock files, pipenv, etc..
  3. Configurações por ambiente.
    • Dynaconf, Python Decouple, Everett

Gerenciando settings sem o Dynaconf

Arquivo settings sem o Dynaconf

settings.py

USERNAME = "admin"
PASSWORD = "1234"
SERVER_IP = "192.168.0.10"
DB_NAME = "customers"

app.py

import settings

print(settings.USERNAME)
print(settings.PASSWORD)
print(settings.SERVER_IP)
print(settings.DB_NAME)
  • Password exposto para o git
    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
  • O formato do password é str ou int?

Como resolver esses problemas sem o Dynaconf?

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
ler variáveis de ambiente!

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

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).

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?

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

MCGYVERISM

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

production_settings.py

SERVER_IP = 'prodserver.com'

staging_settings.py

SERVER_IP = 'stagingserver.com'

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
não faça isso
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

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:

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.

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)}
    }
}

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Funciona!
mas imagina que vc precisa fazer isso em mais 200 variavéis com tipos complexos?
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

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.

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
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Primeiro instale o dynaconf

# opções: [all,yaml,ini,redis,vault]
pip install 'dynaconf[yaml,vault,redis]'

Quick demo com env vars

$ 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.

$ 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

default:
  DB_NAME: default
  SERVER_IP: default
  USERNAME: default
development:
  DB_NAME: customers
  SERVER_IP: localhost
  USERNAME: admin

Repare que o password não está incluido neste arquivo onde ele está?

.secrets.yaml

default:
  PASSWORD: default
development:
  PASSWORD: 1234

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
pra quê serve este .secrets.* ?

.gitignore

# Ignore dynaconf secret files
.secrets.*

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)

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
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

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Hora de ver se funcionou!

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

$ dynaconf write yaml -v server_ip=prodserver -s password=9999 -e production

então settings.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

default:
  PASSWORD: default
development:
  PASSWORD: 1234
production:
  PASSWORD: 9999

E então para listar somente as variavéis de production:

$ ENV_FOR_DYNACONF=production dynaconf list

Working in production environment 
DB_NAME: 'default'
SERVER_IP: 'prodserver'
USERNAME: 'default'
PASSWORD: 9999

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Dynaconf está inicializado com sucesso! e agora?

Se tentarmos executar o programa app.py

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

-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

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
também é possivel mudar essa variavel para algo mais amigavel tipo MEUAPP_ENV=development

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.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
12 factor app/Config

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
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

default:
  DB_NAME: default
  SERVER_IP: default
  USERNAME: default

.secrets.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.

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

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

default:
  DATABASES:
    default:
      NAME: db
      ENGINE: module.foo.engine
      ARGS:
        timeout: 30

Podemos alterar o ENGINE via environment variables usando __ (dunder):

export DYNACONF_DATABASES__default__ENGINE=other.module

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
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

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

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
E agora?

  • Altera os arquivos settings.yaml
  • Abre um Pull Request
  • Espera o code review
    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
  • PR merged
    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
  • Deploy efetuado
    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
  • 200 instancias reiniciadas

Mas

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
happens!

Você e o reviewer não prestaram atenção e na verdade tinha que mudar também o USERNAME

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

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:

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

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.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
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:

$ 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

VAULT_ENABLED_FOR_DYNACONF=1
VAULT_TOKEN_FOR_DYNACONF=myroot

Para escrever no vault server

$ 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

Extensões

Flask

pip install Flask
export FLASK_APP=app.py
export FLASK_ENV=development

app.py

from flask import Flask


app = Flask(__name__)
app.config['USERNAME'] = 'Bruno'


@app.route('/')
def hello():
    return app.config['USERNAME']
$ flask run
...
http://localhost:5000/

Como fazer para o app.config ler seus valores através do dynaconf?

settings.toml

[default]
username = 'Bruno'
from flask import Flask
from dynaconf import FlaskDynaconf


app = Flask(__name__)
FlaskDynaconf(app)


@app.route('/')
def hello():
    return app.config.USERNAME

ou

export FLASK_USERNAME='Guido Van Rossum'

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
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

pip install django
django-admin startproject django_app
cd django_app
python manage.py runserver

acesse: http://localhost:8000/

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.

$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

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Pronto agora o Dynaconf está ativado no seu Django e você pode usar.

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

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →