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

Recentemente ho scritto una risposta sulla vita di un progetto in Docker e sul debug del codice al di fuori di esso, dove ho accennato brevemente alla possibilità di creare un sistema di configurazione per far funzionare bene il servizio anche su Kubernetes, estraendo segreti e avviandosi facilmente in locale, anche al di fuori di Docker. Niente di complicato, ma la "ricetta" descritta potrebbe tornare utile a qualcuno 🙂 Il codice è in Python, ma la logica non è legata a un linguaggio specifico.

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

La storia del problema è la seguente: c'era un progetto che inizialmente era un piccolo monolite con utilità e script, ma col tempo è cresciuto, suddividendosi in servizi che a loro volta si sono trasformati in microservizi, e poi sono stati scalati. All'inizio tutto ciò veniva eseguito su VPS nudi, i processi di configurazione e distribuzione del codice erano automatizzati con Ansible, e a ciascun servizio veniva creato un file di configurazione YAML con impostazioni e chiavi necessarie, e un file di configurazione simile veniva utilizzato per le esecuzioni locali, il che era molto comodo, poiché questo file di configurazione veniva caricato in un oggetto globale, accessibile da qualsiasi parte del progetto.

Tuttavia, la crescita del numero di microservizi, delle loro interconnessioni, così come la necessità di un logging e monitoraggio centralizzati, preannunciava il passaggio a Kubernetes, che è ancora in fase di implementazione. Oltre all'assistenza nella risoluzione dei compiti sopra citati, Kubernetes offre i suoi approcci per la gestione dell'infrastruttura, inclusi i cosiddetti Segreti e i metodi per lavorare con essi. Il meccanismo è standard e affidabile, quindi, in senso letterale, è un peccato non utilizzarlo! Tuttavia, desidero mantenere il mio attuale formato di lavoro con il configuration file: da un lato, usarlo in modo uniforme tra i vari microservizi del progetto, e dall'altro, avere la possibilità di eseguire il codice sulla macchina locale utilizzando un semplice file di configurazione.

Pertanto, il meccanismo di costruzione dell'oggetto di configurazione è stato migliorato per poter lavorare sia con il nostro classico file di configurazione, sia con i segreti di Kubernetes. È stata anche definita una struttura di configurazione più rigida, che in termini di Python 3 può essere descritta come:

Dict[str, Dict[str, Union[str, int, float]]]

Cioè, la configurazione finale è un dizionario con sezioni nominate, ognuna delle quali è un dizionario con valori di tipi semplici. Le sezioni descrivono la configurazione e gli accessi a risorse di un certo tipo. Ecco un esempio 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: info@test.com
  pw: "SuperHardPassword"

Inoltre, il campo engine del database può essere impostato su SQLite, e redis configurato su mock, specificando anche un altro nome di file per il salvataggio, questi parametri vengono riconosciuti e trattati correttamente, permettendo di eseguire facilmente il codice localmente per il debug, i test unitari e qualsiasi altra esigenza. Questo è particolarmente rilevante per noi, poiché queste altre esigenze sono molte: parte del nostro codice è destinata a vari calcoli analitici, viene eseguito non solo su server con orchestrazione, ma anche attraverso vari script e sui computer degli analisti, che devono sviluppare e testare complessi pipeline di elaborazione dati senza doversi preoccupare delle questioni di backend. A proposito, è utile condividere che i nostri principali strumenti, incluso il codice di configurazione, vengono installati attraverso setup.py – insieme unisce il nostro codice in un'unica ecosistema, indipendente dalla piattaforma e dal modo di utilizzo.

La descrizione del pod in Kubernetes appare così:

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è, in ogni segreto è descritta una sezione. I segreti stessi vengono 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, questo porta alla creazione di file YAML nel percorso /etc/secrets/db-main/section_name.yaml

E per le esecuzioni locali viene utilizzata una configurazione situata nella directory radice del progetto o nel percorso specificato nella variabile d'ambiente. Il codice responsabile di queste comodità è visibile nel 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 è piuttosto semplice: uniamo le configurazioni principali dalla directory del progetto e il percorso della variabile di ambiente, e configuriamo piccole sezioni dai segreti di K8s, per poi preelaborarle un po'. Inoltre, alcune variabili. Va notato che nella ricerca di file dai segreti viene utilizzato un limite di profondità, poiché K8s crea in ogni segreto una cartella nascosta, in cui sono conservati i segreti, mentre a un livello superiore c'è solo un collegamento.

Spero che quanto descritto possa risultare utile a qualcuno 🙂 Accetto qualsiasi commento e raccomandazione riguardo alla sicurezza o ad altri aspetti per migliorare. Sono anche curioso del parere della comunità, forse dovremmo aggiungere il supporto per ConfigMaps (nel nostro progetto non sono ancora utilizzati) e pubblicare il codice su GitHub / PyPI? Personalmente, penso che queste cose siano troppo individuali per i progetti per essere universali e sia sufficiente dare un’occhiata ad altre implementazioni, come quella qui presente, e discutere delle sfumature, dei consigli e delle best practices, che spero di vedere nei commenti 😉

Solo gli utenti registrati possono partecipare al sondaggio. Accedi, per favore.

Vale la pena pubblicarlo come progetto / libreria?

  • 0,0%Sì, lo userei / contribuirei.

  • 33,3%Sì, sembra fantastico.

  • 41,7%No, chi ha bisogno lo farà da solo nel proprio formato e per le proprie esigenze5

  • 25,0%Mi astengo dal rispondere3

Hanno votato 12 utenti. 3 utenti si sono astenuti.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, server VPS VDS 🔥 Acquista hosting affidabile per siti web con protezione DDoS, server VPS VDS | ProHoster