Recent am scris , unde am menționat în trecere că se poate crea propriul sistem de configurare, astfel încât serviciul să funcționeze bine și în Kubernetes, să tragă secrete și să pornească local convenabil, inclusiv complet în afara Docker-ului. Nu este nimic complicat, dar "rețeta" descrisă ar putea fi utilă cuiva 🙂 Codul este în Python, dar logica nu este legată de limbaj.

Preistoria întrebării este astfel: a existat un proiect, la început a fost un monolit mic cu utilitare și scripturi, dar, în timp, a crescut, s-a împărțit în servicii, care la rândul lor au început să se împartă în microservicii și apoi să scaleze. La început, totul se desfășura pe VPS-uri bare, procesul de configurare și desfășurare a codului fiind automatizat cu ajutorul Ansible, iar fiecărui serviciu i s-a realizat un fișier de configurare YAML cu setările și cheile necesare, iar un fișier de configurare similar a fost utilizat pentru pornirile locale, ceea ce a fost foarte convenabil, deoarece acest fișier de configurare se încarcă în un obiect global, accesibil din orice loc în proiect.
Cu toate acestea, creșterea numărului de microservicii, a conexiunilor acestora și , preziceau mutarea în Kubernetes, care este încă în desfășurare. Împreună cu ajutorul în soluționarea acestor sarcini menționate, Kubernetes oferă propriile abordări pentru gestionarea infrastructurii, inclusiv și . Mecanismul este standard și fiabil, așa că, în sens literal, este păcat să nu-l folosești! Dar în același timp, ne-ar plăcea să păstrăm formatul nostru curent de lucru cu configurația: pe de o parte, să-l folosim uniform în diferite microservicii ale proiectului, iar pe de altă parte, să avem posibilitatea de a rula codul pe mașina locală folosind un singur fișier de configurare simplu.
În acest sens, mecanismul de construcție a obiectului de configurare a fost îmbunătățit astfel încât să poată lucra atât cu fișierul nostru clasic de configurație, cât și cu secretele din Kubernetes. De asemenea, a fost definită o structură mai riguroasă a fișierului de configurare, vorbind în limbajul Python 3, aceasta fiind:
Dict[str, Dict[str, Union[str, int, float]]]
Adică, configurația finală este un dicționar cu secțiuni denumite, fiecare dintre acestea fiind un dicționar cu valori din tipuri simple. iar secțiunile descriu configurația și accesul la resursele de un anumit tip. Exemplu de parte din configurația noastră:
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"În acest context, câmpul engine baza de date poate fi setată pe SQLite, iar redis configurat pe mock, specificând, de asemenea, numele fișierului pentru salvare – aceste setări sunt corect recunoscute și procesate, permițând astfel rularea facilă a codului local pentru depanare, teste unitare și alte necesități. Acest lucru este deosebit de relevant pentru noi, deoarece avem multe astfel de necesități – o parte din codul nostru este destinat unor calcule analitice diverse, care sunt rulate nu doar pe servere cu orchestration, ci și pe diverse scripturi și pe calculatoarele analiștilor, care trebuie să dezvolte și să debuggeze procese complexe de prelucrare a datelor fără a fi nevoiți să se preocupe de problemele de back-end. Apropo, nu ar fi rău să subliniem că principalele noastre instrumente, inclusiv codul pentru configurarea fișierului, sunt instalate prin setup.py – toate acestea îmbină codul nostru într-un ecosistem unitar, independent de platformă și de modul de utilizare.
Descrierea pod-ului în Kubernetes arată așa:
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-secretAdică, în fiecare secret este descrisă o secțiune. Secretele sunt create astfel:
apiVersion: v1
kind: Secret
metadata:
name: db-main-secret
type: Opaque
stringData:
db_main.yaml: |
engine: sqlite
filename: main.sqlite3Împreună, aceasta duce la crearea fișierelor YAML pe calea /etc/secrets/db-main/section_name.yaml
Și pentru rulările locale se folosește un config plasat în directorul rădăcină al proiectului sau pe calea specificată în variabila de mediu. Codul responsabil pentru aceste facilități poate fi văzut în 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()Logica este destul de simplă: combinăm configurațiile mari din directorul proiectului și din calea variabilei de mediu, și configurațiile mici din secretele Kubernetes, apoi le preprocesăm puțin. De asemenea, folosim câteva variabile. Menționez că, în timpul căutării fișierelor din secrete, se folosește o limită de adâncime, deoarece Kubernetes creează un folder ascuns în fiecare secret, unde sunt stocate secretele, iar la nivelul superior se află doar un link.
Sper că cele descrise vor fi utile cuiva 🙂 Orice comentarii și sugestii referitoare la securitate sau alte aspecte de îmbunătățire sunt binevenite. De asemenea, sunt interesat de opinia comunității, poate ar trebui să adăugăm suport pentru ConfigMaps (în proiectul nostru nu se folosește deocamdată) și să publicăm codul pe GitHub / PyPI? Personal, cred că astfel de lucruri sunt prea individuale pentru proiecte, pentru a fi universale, și este suficient să te uiți puțin la implementările celorlalți, precum cea prezentată aici, și să discutăm despre nuanțe, sfaturi și cele mai bune practici, pe care sper să le văd în comentarii 😉
Numai utilizatorii înregistrați pot participa la sondaj. , vă rugăm.
Ar trebui să publicăm ca proiect / bibliotecă?
0,0%Da, aș folosi / contribui
33,3%Da, sună bine
41,7%Nu, cine dorește va face singur în formatul și pentru nevoile sale
25,0%Mă abțin de la răspuns
Au votat 12 utilizatori. S-au abținut 3 utilizatori.
Sursa: habr.com
