Projektkonfiguration innerhalb und außerhalb von Kubernetes

Ich habe kürzlich geschrieben Antwort zum Projektleben in Docker und zum Debuggen von Code außerhalb davon, wo er kurz erwähnte, dass Sie Ihr eigenes Konfigurationssystem erstellen können, damit der Dienst in Kuber gut funktioniert, Geheimnisse abruft und bequem lokal läuft, sogar außerhalb von Docker ganz. Nichts Kompliziertes, aber das beschriebene „Rezept“ könnte für jemanden nützlich sein :) Der Code ist in Python, aber die Logik ist nicht an die Sprache gebunden.

Projektkonfiguration innerhalb und außerhalb von Kubernetes

Der Hintergrund der Frage ist folgender: Es war einmal ein Projekt, zunächst war es ein kleiner Monolith mit Dienstprogrammen und Skripten, aber im Laufe der Zeit wuchs es, unterteilt in Dienste, die wiederum in Mikrodienste unterteilt wurden, und dann vergrößert. Zunächst wurde dies alles auf einem reinen VPS durchgeführt, wobei die Prozesse zum Einrichten und Bereitstellen von Code mithilfe von Ansible automatisiert wurden. Jeder Dienst wurde mit einer YAML-Konfiguration mit den erforderlichen Einstellungen und Schlüsseln kompiliert und eine ähnliche Konfigurationsdatei wurde dafür verwendet Lokale Starts, was sehr praktisch war, da diese Konfiguration in ein globales Objekt geladen wird, auf das von überall im Projekt aus zugegriffen werden kann.

Das Wachstum der Anzahl der Microservices, ihrer Verbindungen usw Bedarf an zentraler Protokollierung und Überwachung, ließ einen Wechsel zu Kuber ahnen, der noch im Gange ist. Neben der Unterstützung bei der Lösung der genannten Probleme bietet Kubernetes seine Ansätze für das Infrastrukturmanagement an, darunter sogenannte Geheimnisse и Möglichkeiten, mit ihnen zu arbeiten. Der Mechanismus ist standardmäßig und zuverlässig, daher ist es buchstäblich eine Sünde, ihn nicht zu verwenden! Gleichzeitig möchte ich aber mein aktuelles Format für die Arbeit mit der Konfiguration beibehalten: Erstens, um es einheitlich in verschiedenen Microservices des Projekts zu verwenden, und zweitens, um den Code auf dem lokalen Computer mit einer einfachen Funktion ausführen zu können Konfigurationsdatei.

In diesem Zusammenhang wurde der Mechanismus zum Erstellen eines Konfigurationsobjekts geändert, um sowohl mit unserer klassischen Konfigurationsdatei als auch mit Geheimnissen von Kuber arbeiten zu können. Außerdem wurde eine strengere Konfigurationsstruktur in der Sprache des dritten Pythons wie folgt angegeben:

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

Das heißt, das letzte Zahnrad ist ein Wörterbuch mit benannten Abschnitten, von denen jeder ein Wörterbuch mit Werten aus einfachen Typen ist. Und Abschnitte beschreiben die Konfiguration und den Zugriff auf Ressourcen eines bestimmten Typs. Ein Beispiel für einen Teil unserer Konfiguration:

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"

Gleichzeitig ist das Feld engine Datenbanken können auf SQLite installiert werden, und redis einstellen mock, wobei auch der Name der zu speichernden Datei angegeben wird – diese Parameter werden korrekt erkannt und verarbeitet, was es einfach macht, den Code lokal für Debugging, Unit-Tests und andere Zwecke auszuführen. Dies ist für uns besonders wichtig, da es viele andere Anforderungen gibt – ein Teil unseres Codes ist für verschiedene analytische Berechnungen gedacht, er läuft nicht nur auf Servern mit Orchestrierung, sondern auch mit verschiedenen Skripten und auf den Computern von Analysten, die durcharbeiten müssen und debuggen Sie komplexe Datenverarbeitungspipelines, ohne sich um Backend-Probleme kümmern zu müssen. Übrigens würde es nicht schaden, mitzuteilen, dass unsere Haupttools, einschließlich des Konfigurationslayoutcodes, über installiert werden setup.py – Zusammen vereint dies unseren Code in einem einzigen Ökosystem, unabhängig von Plattform und Nutzungsmethode.

Die Beschreibung eines Kubernetes-Pods sieht folgendermaßen aus:

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

Das heißt, jedes Geheimnis beschreibt einen Abschnitt. Die Geheimnisse selbst werden wie folgt erstellt:

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

Zusammen führt dies zur Erstellung von YAML-Dateien entlang des Pfades /etc/secrets/db-main/section_name.yaml

Und für lokale Starts wird die Konfiguration verwendet, die sich im Stammverzeichnis des Projekts oder entlang des in der Umgebungsvariablen angegebenen Pfads befindet. Der für diese Annehmlichkeiten verantwortliche Code ist im Spoiler zu sehen.

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()

Die Logik hier ist ganz einfach: Wir kombinieren große Konfigurationen aus dem Projektverzeichnis und Pfaden nach Umgebungsvariablen und kleine Konfigurationsabschnitte aus Kuber-Geheimnissen und verarbeiten sie dann ein wenig vor. Plus einige Variablen. Ich stelle fest, dass bei der Suche nach Dateien aus Geheimnissen eine Tiefenbeschränkung verwendet wird, da K8s in jedem Geheimnis einen versteckten Ordner erstellt, in dem die Geheimnisse selbst gespeichert sind, und sich lediglich ein Link auf einer höheren Ebene befindet.

Ich hoffe, dass das, was beschrieben wird, für jemanden nützlich sein wird :) Alle Kommentare und Empfehlungen zur Sicherheit oder anderen Bereichen mit Verbesserungsbedarf werden akzeptiert. Interessant ist auch die Meinung der Community. Vielleicht lohnt es sich, Unterstützung für ConfigMaps hinzuzufügen (unser Projekt verwendet sie noch nicht) und den Code auf GitHub / PyPI zu veröffentlichen? Persönlich denke ich, dass solche Dinge zu individuell sind, als dass Projekte universell sein könnten, und ein kleiner Blick auf die Implementierungen anderer Leute, wie die hier gegebene, und eine Diskussion von Nuancen, Tipps und Best Practices, die ich hoffentlich in den Kommentaren sehen kann , reicht 😉

An der Umfrage können nur registrierte Benutzer teilnehmen. Einloggenbitte.

Soll ich als Projekt/Bibliothek veröffentlichen?

  • 0,0%Ja, ich würde /contribution0 verwenden

  • 33,3%Ja, das klingt großartig4

  • 41,7%Nein, wer muss es schon selbst tun, in seinem eigenen Format und passend zu seinen Bedürfnissen5

  • 25,0%Ich verzichte auf eine Antwort3

12 Benutzer haben abgestimmt. 3 Benutzer enthielten sich der Stimme.

Source: habr.com

Kommentar hinzufügen