Configurazione del progetto all'interno e all'esterno di Kubernetes

Ho scritto di recente risposta sulla vita del progetto in Docker e sul debug del codice al di fuori di esso, dove ha brevemente menzionato che è possibile creare il proprio sistema di configurazione in modo che il servizio funzioni bene in Kuber, estragga i segreti e funzioni comodamente localmente, anche al di fuori di Docker. Niente di complicato, ma la “ricetta” descritta potrebbe essere utile a qualcuno :) Il codice è in Python, ma la logica non è legata al linguaggio.

Configurazione del progetto all'interno e all'esterno di Kubernetes

Lo sfondo della domanda è questo: c'era una volta un progetto, all'inizio era un piccolo monolite con utilità e script, ma col tempo è cresciuto, diviso in servizi, che a loro volta hanno cominciato a dividersi in microservizi, e poi ingrandito. Inizialmente, tutto ciò è stato fatto su VPS nudo, i processi di impostazione e distribuzione del codice su cui sono stati automatizzati utilizzando Ansible, e ogni servizio è stato compilato con una configurazione YAML con le impostazioni e le chiavi necessarie e un file di configurazione simile è stato utilizzato per lanci locali, il che è stato molto conveniente, perché .k questa configurazione viene caricata in un oggetto globale, accessibile da qualsiasi punto del progetto.

Tuttavia, la crescita del numero di microservizi, delle loro connessioni e necessità di registrazione e monitoraggio centralizzati, prefigurava un passaggio a Kuber, che è ancora in corso. Insieme all'assistenza nella risoluzione dei problemi menzionati, Kubernetes offre i suoi approcci alla gestione dell'infrastruttura, incluso i cosiddetti Segreti и modi per lavorare con loro. Il meccanismo è standard e affidabile, quindi è letteralmente un peccato non usarlo! Ma allo stesso tempo, vorrei mantenere il mio formato attuale per lavorare con la configurazione: in primo luogo, usarlo in modo uniforme in diversi microservizi del progetto e, in secondo luogo, poter eseguire il codice sulla macchina locale utilizzando un semplice file di configurazione.

A questo proposito, il meccanismo per costruire un oggetto di configurazione è stato modificato per poter funzionare sia con il nostro file di configurazione classico che con i segreti di Kuber. È stata inoltre specificata una struttura di configurazione più rigida, nel linguaggio del terzo Python, come segue:

Dict[str, Dict[str, Unione[str, int, float]]]

Cioè, il cogfig finale è un dizionario con sezioni denominate, ognuna delle quali è un dizionario con valori di tipi semplici. E le sezioni descrivono la configurazione e l'accesso alle risorse di un certo tipo. Un esempio di un pezzo della nostra configurazione:

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"

Allo stesso tempo, il campo engine i database possono essere installati su SQLite e redis impostato mock, specificando anche il nome del file da salvare: questi parametri vengono riconosciuti ed elaborati correttamente, il che facilita l'esecuzione del codice in locale per debugging, unit testing e qualsiasi altra esigenza. Questo è particolarmente importante per noi perché ci sono molte altre esigenze: parte del nostro codice è destinata a vari calcoli analitici, funziona non solo su server con orchestrazione, ma anche con vari script e sui computer degli analisti che devono elaborare ed eseguire il debug di pipeline di elaborazione dati complesse senza preoccuparsi dei problemi di backend. A proposito, non sarebbe male condividere che i nostri strumenti principali, incluso il codice del layout di configurazione, vengono installati tramite setup.py – insieme questo unisce il nostro codice in un unico ecosistema, indipendente dalla piattaforma e dal metodo di utilizzo.

La descrizione di un pod Kubernetes è simile alla seguente:

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

Cioè, ogni segreto descrive una sezione. I segreti stessi sono creati in questo modo:

apiVersion: v1
kind: Secret
metadata:
  name: db-main-secret
type: Opaque
stringData:
  db_main.yaml: |
    engine: sqlite
    filename: main.sqlite3

Insieme, ciò si traduce nella creazione di file YAML lungo il percorso /etc/secrets/db-main/section_name.yaml

E per i lanci locali, viene utilizzata la configurazione, situata nella directory principale del progetto o lungo il percorso specificato nella variabile di ambiente. Il codice responsabile di queste comodità può essere visto nello 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()

La logica qui è abbastanza semplice: combiniamo configurazioni di grandi dimensioni dalla directory del progetto e percorsi per variabile di ambiente e piccole sezioni di configurazione dai segreti di Kuber, quindi le preelaboriamo un po'. Oltre ad alcune variabili. Noto che quando si cercano file dai segreti, viene utilizzata una limitazione di profondità, poiché K8 crea una cartella nascosta in ogni segreto in cui sono archiviati i segreti stessi e solo un collegamento si trova a un livello superiore.

Spero che quanto descritto possa essere utile a qualcuno :) Si accettano commenti e suggerimenti riguardanti la sicurezza o altre aree di miglioramento. Interessante anche il parere della community, forse vale la pena aggiungere il supporto per ConfigMaps (il nostro progetto non li utilizza ancora) e pubblicare il codice su GitHub / PyPI? Personalmente, penso che queste cose siano troppo individuali perché i progetti siano universali, e una piccola sbirciatina alle implementazioni di altre persone, come quella fornita qui, e una discussione su sfumature, suggerimenti e migliori pratiche, che spero di vedere nei commenti , basta 😉

Solo gli utenti registrati possono partecipare al sondaggio. AccediPer favore.

Dovrei pubblicare come progetto/biblioteca?

  • 0,0%Sì, utilizzerei /contribution0

  • 33,3%Sì, sembra fantastico4

  • 41,7%No, chi deve farlo da solo nel proprio formato e in base alle proprie esigenze5

  • 25,0%Mi asterrò dal rispondere3

12 utenti hanno votato. 3 utenti si sono astenuti.

Fonte: habr.com

Aggiungi un commento