Recentemente ho scritto , 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.

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 , 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 e . 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-secretCioè, 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.sqlite3Insieme, 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. , 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
