Configuração do projeto dentro e fora do Kubernetes

Recentemente, escrevi uma resposta sobre a vida do projeto em Docker e a depuração de código fora dele, 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.

Configuração do projeto dentro e fora do Kubernetes

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 a necessidade de logging e monitoramento centralizados, 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 os chamados Segredos e e maneiras de trabalhar com eles. 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-secret

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

Juntos, 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. Entre, 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

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster