Configuração do projeto dentro e fora do Kubernetes

Eu escrevi recentemente responda sobre a vida do projeto no Docker e depuração de código fora dele, onde ele mencionou brevemente que você pode criar seu próprio sistema de configuração para que o serviço funcione bem no Kuber, obtenha segredos e seja executado convenientemente localmente, mesmo fora do Docker. Nada complicado, mas a “receita” descrita pode ser útil para alguém :) O código está em Python, mas a lógica não está vinculada à linguagem.

Configuração do projeto dentro e fora do Kubernetes

O pano de fundo da questão é o seguinte: era uma vez um projeto, no início era um pequeno monólito com utilitários e scripts, mas com o tempo foi crescendo, dividido em serviços, que por sua vez passaram a ser divididos em microsserviços, e então ampliado. No início, tudo isso foi feito em VPS simples, cujos processos de configuração e implantação de código foram automatizados usando Ansible, e cada serviço foi compilado com uma configuração YAML com as configurações e chaves necessárias, e um arquivo de configuração semelhante foi usado para lançamentos locais, o que foi muito conveniente, pois .k essa configuração é carregada em um objeto global, acessível de qualquer lugar do projeto.

No entanto, o crescimento do número de microsserviços, suas conexões e necessidade de registro e monitoramento centralizados, prenunciou uma mudança para Kuber, que ainda está em andamento. Juntamente com a assistência na resolução dos problemas mencionados, o Kubernetes oferece suas abordagens para gerenciamento de infraestrutura, incluindo os chamados segredos и maneiras de trabalhar com eles. O mecanismo é padrão e confiável, então é literalmente um pecado não usá-lo! Mas, ao mesmo tempo, gostaria de manter meu formato atual para trabalhar com a configuração: primeiro, usá-lo uniformemente nos diferentes microsserviços do projeto e, segundo, poder executar o código na máquina local usando um simples arquivo de configuração.

Nesse sentido, o mecanismo de construção de um objeto de configuração foi modificado para poder funcionar tanto com nosso arquivo de configuração clássico quanto com segredos do Kuber. Também foi especificada uma estrutura de configuração mais rígida, na linguagem do terceiro Python, conforme segue:

Dict[str, Dict[str, União[str, int, float]]]

Ou seja, o cogfig final é um dicionário com seções nomeadas, cada uma delas 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 parte 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: [email protected]
  pw: "SuperHardPassword"

Ao mesmo tempo, o campo engine bancos de dados podem ser instalados no SQLite, e redis definido como mock, especificando também o nome do arquivo a ser salvo - esses parâmetros são reconhecidos e processados ​​corretamente, o que facilita a execução local do código para depuração, testes unitários e quaisquer outras necessidades. Isso é especialmente importante para nós porque existem muitas outras necessidades - parte do nosso código é destinada a diversos cálculos analíticos, funciona não apenas em servidores com orquestração, mas também com vários scripts, e em computadores de analistas que precisam trabalhar e depure pipelines complexos de processamento de dados sem se preocupar com problemas de back-end. A propósito, não faria mal nenhum compartilhar que nossas principais ferramentas, incluindo o código de layout de configuração, são instaladas via setup.py – juntos, isso une nosso código em um único ecossistema, independente de plataforma e método de uso.

A descrição de um pod Kubernetes é assim:

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, cada segredo descreve uma seção. Os próprios 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 resulta na criação de arquivos YAML ao longo do caminho /etc/secrets/db-main/section_name.yaml

E para lançamentos locais, é usada a configuração, localizada no diretório raiz do projeto ou ao longo do caminho especificado na variável de ambiente. O código responsável por essas conveniências 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):
    '''
    Parse YAML config with multiple section
    '''
    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):
    '''
    Parse YAML config with a single section
    '''
    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 = {
        # To debug config load code
        KEY_LOG: [],
        # To debug other code
        KEY_DBG: is_yes(os.environ.get('DEBUG')),
    }
    # For simple local runs
    CONFIG_SIMPLE = os.path.join(PROJECT_DIR, 'config.yaml')
    parse_big_config(config, CONFIG_SIMPLE)
    # For container's tests
    CONFIG_ENVVAR = os.environ.get('CONFIG')
    if CONFIG_ENVVAR is not None:
        if not parse_big_config(config, CONFIG_ENVVAR):
            raise ValueError(
                f'No config file from EnvVar:n'
                f'{CONFIG_ENVVAR}'
            )
    # For K8s secrets
    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()
    # Preprocess
    for key, data in config.items():
        if key.startswith('db_'):
            if data['engine'] == 'sqlite':
                data['filename'] = os.path.join(PROJECT_DIR, data['filename'])
    # To verify correctness
    if config[KEY_DBG]:
        print(f'** Loaded config:n{yaml.dump(config)}')
    else:
        print(f'** Loaded config from: {config[KEY_LOG]}')
    return config

config = build_config()

A lógica aqui é bastante simples: combinamos configurações grandes do diretório do projeto e caminhos por variável de ambiente e pequenas seções de configuração dos segredos do Kuber e, em seguida, pré-processamos um pouco. Além de algumas variáveis. Observo que ao procurar arquivos em segredos, uma limitação de profundidade é usada, pois o K8s cria uma pasta oculta em cada segredo onde os próprios segredos são armazenados, e apenas um link está localizado em um nível superior.

Espero que o que está descrito seja útil para alguém :) Quaisquer comentários e recomendações sobre segurança ou outras áreas de melhoria são aceitos. A opinião da comunidade também é interessante, talvez valha a pena adicionar suporte para ConfigMaps (nosso projeto ainda não os utiliza) e publicar o código no GitHub/PyPI? Pessoalmente, acho que essas coisas são muito individuais para que os projetos sejam universais, e uma espiada nas implementações de outras pessoas, como a dada aqui, e uma discussão de nuances, dicas e melhores práticas, que espero ver nos comentários , é o suficiente 😉

Apenas usuários registrados podem participar da pesquisa. Entrarpor favor

Devo publicar como projeto/biblioteca?

  • 0,0%Sim, eu usaria /contribution0

  • 33,3%Sim, isso parece ótimo4

  • 41,7%Não, quem precisa fazer sozinho no seu formato e de acordo com suas necessidades5

  • 25,0%Vou me abster de responder3

12 usuários votaram. 3 usuários se abstiveram.

Fonte: habr.com

Adicionar um comentário