Recentemente, escrevi , onde mencionei rapidamente que é possível criar seu próprio sistema de configuração, para que o serviço funcione bem no Kubernetes, busque segredos e seja facilmente iniciado localmente, incluindo até mesmo fora do Docker. Não é nada complicado, mas a "receita" descrita pode ser útil para alguém 🙂 O código é em Python, mas a lógica não está atrelada a uma linguagem específica.

A história da questão é a seguinte: havia um projeto, inicialmente era um pequeno monolito com utilitários e scripts, mas com o tempo cresceu, foi dividido em serviços, os quais por sua vez foram repartidos em microsserviços, e depois ainda ampliados. No início, tudo isso rodava em VPS bare metal, e os processos de configuração e deployment do código foram automatizados com Ansible, e para cada serviço foi elaborado um configurador YAML com as configurações e chaves necessárias, e um arquivo de configuração semelhante era utilizado para execuções locais, o que era muito conveniente, pois essa configuração é carregada em um objeto global disponível em qualquer parte do projeto.
No entanto, o aumento do número de microsserviços, suas interconexões, bem como , prenunciavam a migração para o Kubernetes, que ainda está em andamento. Juntamente com a ajuda para resolver as questões mencionadas, o Kubernetes oferece suas abordagens para gerenciar a infraestrutura, incluindo e . O mecanismo é padrão e confiável, portanto, é literalmente um pecado não aproveitá-lo! Mas, ao mesmo tempo, gostaria de manter meu formato atual de trabalho com a configuração: por um lado, usá-lo de maneira consistente em diferentes microsserviços do projeto, e por outro, ter a possibilidade de executar o código na máquina local usando um único arquivo de configuração simples.
Com isso em mente, o mecanismo de construção do objeto de configuração foi aprimorado para funcionar tanto com nosso arquivo de configuração clássico quanto com os segredos do Kubernetes. Também foi estabelecida uma estrutura de configuração mais rígida, falando na linguagem do Python 3, assim:
Dict[str, Dict[str, Union[str, int, float]]]
Ou seja, a configuração final é um dicionário com seções nomeadas, cada uma das quais é um dicionário com valores de tipos simples. E as seções descrevem a configuração e o acesso a recursos de um determinado tipo. Um exemplo de um trecho da nossa configuração:
adminka:
django_secret: "ExtraLongAndHardCode"
db_main:
engine: mysql
host: 256.128.64.32
user: cool_user
password: "SuperHardPassword"
redis:
host: 256.128.64.32
pw: "SuperHardPassword"
port: 26379
smtp:
server: smtp.gmail.com
port: 465
email: info@test.com
pw: "SuperHardPassword"Neste caso, o campo engine do banco de dados pode ser configurado para SQLite, enquanto redis configura-se para mock, especificando ainda o nome do arquivo para armazenamento – esses parâmetros são reconhecidos e processados corretamente, permitindo que o código seja executado localmente para depuração, testes unitários e outras necessidades. Isso é particularmente relevante para nós, pois existem muitas outras necessidades – parte do nosso código é destinada a diversos cálculos analíticos, sendo executada não apenas em servidores com orquestração, mas também através de vários scripts e em computadores de analistas que precisam desenvolver e depurar complexos pipelines de processamento de dados sem se preocupar com questões de backend. Aliás, não seria demais compartilhar que nossas principais ferramentas, incluindo o código de configuração, são instaladas através de setup.py – juntos, isso une nosso código em um ecossistema coeso, independente de plataforma e forma de uso.
A descrição do pod no Kubernetes é a seguinte:
containers:
- name : enter-api
image: enter-api:latest
ports:
- containerPort: 80
volumeMounts:
- name: db-main-secret-volume
mountPath: /etc/secrets/db-main
volumes:
- name: db-main-secret-volume
secret:
secretName: db-main-secretOu seja, em cada segredo é descrita uma seção. Os segredos são criados assim:
apiVersion: v1
kind: Secret
metadata:
name: db-main-secret
type: Opaque
stringData:
db_main.yaml: |
engine: sqlite
filename: main.sqlite3Juntos, isso leva à criação de arquivos YAML no caminho /etc/secrets/db-main/section_name.yaml
E para execuções locais, usa-se um arquivo de configuração localizado no diretório raiz do projeto ou no caminho especificado na variável de ambiente. O código responsável por essas facilidades pode ser visto no spoiler.
config.py
__author__ = 'AivanF'
__copyright__ = 'Copyright 2020, AivanF'
import os
import yaml
__all__ = ['config']
PROJECT_DIR = os.path.abspath(__file__ + 3 * '\/..')
SECRETS_DIR = '\/etc\/secrets'
KEY_LOG = '_config_log'
KEY_DBG = 'debug'
def is_yes(value):
if isinstance(value, str):
value = value.lower()
if value in ('1', 'on', 'yes', 'true'):
return True
else:
if value in (1, True):
return True
return False
def update_config_part(config, key, data):
if key not in config:
config[key] = data
else:
config[key].update(data)
def parse_big_config(config, filename):
'''
Analisar configuração YAML com várias seções
'''
if not os.path.isfile(filename):
return False
with open(filename) as f:
config_new = yaml.safe_load(f.read())
for key, data in config_new.items():
update_config_part(config, key, data)
config[KEY_LOG].append(filename)
return True
def parse_tiny_config(config, key, filename):
'''
Analisar configuração YAML com uma única seção
'''
with open(filename) as f:
config_tiny = yaml.safe_load(f.read())
update_config_part(config, key, config_tiny)
config[KEY_LOG].append(filename)
def combine_config():
config = {
# Para depurar o código de carregamento de configuração
KEY_LOG: [],
# Para depurar outro código
KEY_DBG: is_yes(os.environ.get('DEBUG')),
}
# Para execuções locais simples
CONFIG_SIMPLE = os.path.join(PROJECT_DIR, 'config.yaml')
parse_big_config(config, CONFIG_SIMPLE)
# Para testes de container
CONFIG_ENVVAR = os.environ.get('CONFIG')
if CONFIG_ENVVAR is not None:
if not parse_big_config(config, CONFIG_ENVVAR):
raise ValueError(
f'Nenhum arquivo de configuração da EnvVar:n'
f'{CONFIG_ENVVAR}'
)
# Para segredos do K8s
for path, dirs, files in os.walk(SECRETS_DIR):
depth = path[len(SECRETS_DIR):].count(os.sep)
if depth > 1:
continue
for file in files:
if file.endswith('.yaml'):
filename = os.path.join(path, file)
key = file.rsplit('.', 1)[0]
parse_tiny_config(config, key, filename)
return config
def build_config():
config = combine_config()
# Pré-processar
for key, data in config.items():
if key.startswith('db_'):
if data['engine'] == 'sqlite':
data['filename'] = os.path.join(PROJECT_DIR, data['filename'])
# Para verificar a correção
if config[KEY_DBG]:
print(f'** Configuração carregada:n{yaml.dump(config)}')
else:
print(f'** Configuração carregada de: {config[KEY_LOG]}')
return config
config = build_config()A lógica aqui é bastante simples: unimos grandes configurações do diretório do projeto e do caminho pela variável de ambiente, e pequenas seções de configuração dos segredos do Kuber, e então fazemos um pouco de pré-processamento. Além de algumas variáveis. Vale ressaltar que ao buscar arquivos dos segredos, é utilizado um limite de profundidade, uma vez que o K8s cria uma pasta oculta em cada segredo, onde os próprios segredos são armazenados, e um nível acima está apenas o link.
Espero que o que foi descrito seja útil para alguém 🙂 Aceito quaisquer comentários e recomendações sobre segurança ou outras melhorias. Também estou interessado na opinião da comunidade, talvez valerá a pena adicionar suporte a ConfigMaps (que ainda não estão sendo usados em nosso projeto) e publicar o código no GitHub / PyPI? Pessoalmente, acredito que essas coisas são muito individuais para cada projeto para serem universais, e um simples olhar nas implementações de outras pessoas, como a que foi apresentada aqui, combinado com discussões sobre nuances, conselhos e melhores práticas, que espero ver nos comentários 😉
Apenas usuários registrados podem participar da pesquisa. , por favor.
Devo publicar como projeto/biblioteca?
0,0%Sim, eu usaria / contribuiria
33,3%Sim, parece ótimo
41,7%Não, quem precisa faz no seu próprio formato e para suas necessidades
25,0%Vou me abster de comentar
12 usuários votaram. 3 usuários se abstiveram.
Fonte: habr.com
