Projektkonfiguration innerhalb und außerhalb von Kubernetes

Kürzlich habe ich eine Antwort über das Leben des Projekts in Docker und das Debuggen von Code außerhalb davon geschrieben, wo ich beiläufig erwähnt habe, dass man sein eigenes Konfigurationssystem erstellen kann, damit der Dienst auch in Kubernetes gut funktioniert, Geheimnisse zieht und lokal bequem ausgeführt wird, einschließlich völlig außerhalb von Docker. Nichts Kompliziertes, aber das beschriebene „Rezept“ könnte für jemandem nützlich sein 🙂 Der Code ist in Python, aber die Logik ist nicht an die Sprache gebunden.

Projektkonfiguration innerhalb und außerhalb von Kubernetes

Die Vorgeschichte der Frage ist wie folgt: Es gab ein Projekt, zunächst war es ein kleines Monolith mit Dienstprogrammen und Skripten, aber im Laufe der Zeit wuchs es, teilte sich in Dienste, die wiederum in Mikrodienste aufgeteilt wurden, und dann wurde es sogar noch skalierbar. Zunächst wurde alles auf nackten VPS durchgeführt, die Prozesse zur Einrichtung und Bereitstellung von Code wurden mit Ansible automatisiert, und für jeden Dienst wurde eine YAML-Konfiguration mit den benötigten Einstellungen und Schlüsseln erstellt, und eine ähnliche Konfigurationsdatei wurde für lokale Ausführungen verwendet, was sehr bequem war, da diese Konfiguration in ein globales Objekt geladen wird, das von überall im Projekt zugänglich ist.

Jedoch deutete das Wachstum der Zahl der Mikrodienste, ihrer Verbindungen sowie der Bedarf an zentralisiertem Logging und Monitoring, auf einen Umzug zu Kubernetes hin, der sich bis heute noch in der Umsetzung befindet. Neben der Unterstützung bei der Lösung der genannten Aufgaben bietet Kubernetes seine eigenen Ansätze zur Verwaltung der Infrastruktur, einschließlich der sogenannten Geheimnisse und und der Methoden zur Arbeit mit ihnen. Der Mechanismus ist standardisiert und zuverlässig, weshalb es im wahrsten Sinne des Wortes eine Sünde wäre, ihn nicht zu nutzen! Aber dabei wollte ich mein aktuelles Format der Arbeit mit der Konfiguration beibehalten: Erstens, sie einheitlich in verschiedenen Mikrodiensten des Projekts zu verwenden, und zweitens, die Möglichkeit zu haben, den Code auf der lokalen Maschine mit einer einfachen Konfigurationsdatei zu starten.

In diesem Zusammenhang wurde der Mechanismus zum Aufbau des Konfigurationsobjekts so verbessert, dass er sowohl mit unserer klassischen Konfigurationsdatei als auch mit den Geheimnissen aus Kubernetes arbeiten kann. Außerdem wurde eine striktere Struktur der Konfiguration festgelegt, ausgedrückt in der Sprache von Python 3, die so aussieht:

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

Das heißt, die endgültige Konfiguration ist ein Wörterbuch mit benannten Abschnitten, von denen jeder ein Wörterbuch mit Werten aus einfachen Typen ist. Die Abschnitte beschreiben die Konfiguration und den Zugriff auf Ressourcen bestimmter Art. Hier ein Beispielausschnitt aus 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: info@test.com
  pw: "SuperHardPassword"

Dabei ist das Feld engine der Datenbank so konfigurierbar auf SQLite, und redis einzustellen auf mock, indem auch der Dateiname für die Speicherung angegeben wird – diese Parameter werden korrekt erkannt und verarbeitet, was es einfach macht, den Code lokal zum Debuggen, für Unit-Tests und für andere Bedürfnisse auszuführen. Dies ist besonders relevant für uns, da es viele solcher Bedürfnisse gibt – ein Teil unseres Codes ist für verschiedene analytische Berechnungen gedacht, die nicht nur auf einem orchestrierten Server, sondern auch mit verschiedenen Skripten und auf den Computern von Analysten ausgeführt werden, die komplexe Datenverarbeitungspipelines entwickeln und debuggen müssen, ohne sich um Backend-Fragen kümmern zu müssen. Übrigens ist es nicht schlecht zu erwähnen, dass unsere wichtigsten Tools, einschließlich des Codes zur Zusammenstellung der Konfiguration, über setup.py – zusammen bringt dies unseren Code in ein einheitliches Ökosystem, das unabhängig von der Plattform und der Art der Nutzung ist.

Die Beschreibung des Pods in Kubernetes sieht so 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, in jedem Geheimnis ist ein Abschnitt beschrieben. Die Geheimnisse selbst werden so 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 unter dem Pfad /etc/secrets/db-main/section_name.yaml

Und für lokale Ausführungen wird die Konfiguration verwendet, die im Stammverzeichnis des Projekts oder unter dem in der Umgebungsvariablen angegebenen Pfad liegt. Den Code, der für diese Annehmlichkeiten verantwortlich ist, kann man im Spoiler 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):
    '''
    Konfigurieren Sie YAML mit mehreren Abschnitten
    '''
    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):
    '''
    Konfigurieren Sie YAML mit einem einzelnen Abschnitt
    '''
    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 = {
        # Um den Code zum Laden der Konfiguration zu debuggen
        KEY_LOG: [],
        # Um anderen Code zu debuggen
        KEY_DBG: is_yes(os.environ.get('DEBUG')),
    }
    # Für einfache lokale Ausführungen
    CONFIG_SIMPLE = os.path.join(PROJECT_DIR, 'config.yaml')
    parse_big_config(config, CONFIG_SIMPLE)
    # Für Container-Tests
    CONFIG_ENVVAR = os.environ.get('CONFIG')
    if CONFIG_ENVVAR is not None:
        if not parse_big_config(config, CONFIG_ENVVAR):
            raise ValueError(
                f'Keine Konfigurationsdatei von EnvVar:n'
                f'{CONFIG_ENVVAR}'
            )
    # Für K8s-Geheimnisse
    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()
    # Vorverarbeitung
    for key, data in config.items():
        if key.startswith('db_'):
            if data['engine'] == 'sqlite':
                data['filename'] = os.path.join(PROJECT_DIR, data['filename'])
    # Um die Richtigkeit zu überprüfen
    if config[KEY_DBG]:
        print(f'** Geladene Konfiguration:n{yaml.dump(config)}')
    else:
        print(f'** Geladene Konfiguration von: {config[KEY_LOG]}')
    return config

config = build_config()

Die Logik hier ist ziemlich einfach: Wir kombinieren große Konfigurationen aus dem Projektverzeichnis und dem Pfad durch die Umgebungsvariable sowie kleine Konfigurationsabschnitte aus den Kubernetes-Geheimnissen und verarbeiten sie dann ein wenig vor. Außerdem einige Variablen. Ich möchte anmerken, dass bei der Suche nach Dateien aus den Geheimnissen eine Tiefenbegrenzung verwendet wird, da K8s in jedem Geheimnis einen versteckten Ordner erstellt, in dem die Geheimnisse gespeichert sind, während sich eine Ebene höher nur ein Link befindet.

Ich hoffe, dass das Beschriebene für jemanden nützlich ist 🙂 Kommentare und Empfehlungen zur Sicherheit oder zu anderen Verbesserungspunkten sind willkommen. Interessant wäre auch die Meinung der Community, ob man Unterstützung für ConfigMaps hinzufügen sollte (in unserem Projekt werden sie bisher nicht verwendet) und den Code auf GitHub / PyPI bereitstellen sollte? Persönlich denke ich, dass solche Dinge zu individuell für Projekte sind, um universell zu sein, und dass es ausreichend ist, einen kleinen Blick auf die Implementierungen anderer zu werfen, wie die hier gezeigte, und Nuancen, Ratschläge und Best Practices zu diskutieren, die ich hoffe in den Kommentaren zu sehen 😉

Nur registrierte Benutzer können an der Umfrage teilnehmen. Bitte einloggen.

Sollte ich es als Projekt / Bibliothek veröffentlichen?

  • 0,0%Ja, ich würde es verwenden / beitragen

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

  • 41,7%Nein, wer es braucht, wird es selbst in seinem Format und für seine Bedürfnisse tun

  • 25,0%Ich werde mich enthalten

12 Benutzer haben abgestimmt. 3 Benutzer haben sich enthalten.

Quelle: habr.com

60GB SSD 8Gb DDR4