Konfiguracja projektu wewnątrz i na zewnątrz Kubernetes

Niedawno napisałem odpowiedź na temat życia projektu w kontenerach i debugowania kodu poza nimi, gdzie wspomniałem, że można stworzyć własny system konfiguracyjny, aby usługa dobrze działała w Kubernetes, pobierała sekrety i uruchamiała się lokalnie, w tym zupełnie poza Dockerem. Nic trudnego, ale opisany "przepis" może się komuś przydać 🙂 Kod w Pythonie, ale logika nie jest związana z językiem.

Konfiguracja projektu wewnątrz i na zewnątrz Kubernetes

Tło pytania jest następujące: był sobie projekt, na początku był małym monolitem z narzędziami i skryptami, ale z czasem rósł, dzielił się na usługi, które z kolei stały się mikroserwisami, a potem jeszcze skalowały się. Początkowo wszystko to działało na surowych VPS-ach, procesy konfiguracji i wdrażania kodu były zautomatyzowane przy użyciu Ansible, a dla każdej usługi tworzono plik konfiguracyjny YAML z odpowiednimi ustawieniami i kluczami, który był również wykorzystywany do lokalnych uruchomień, co było bardzo wygodne, ponieważ ten plik konfiguracyjny ładował się do globalnego obiektu dostępnego z dowolnego miejsca w projekcie.

Jednak wzrost liczby mikroserwisów, ich powiązań oraz potrzeba centralnego logowania i monitorowania, zapowiadały przeprowadzkę do Kubernetes, która wciąż jest w toku. Oprócz pomocy w rozwiązaniu wymienionych zadań, Kubernetes oferuje własne podejścia do zarządzania infrastrukturą, w tym tzw. Sekrety i sposoby pracy z nimi. Mechanizm jest standardowy i niezawodny, dlatego w dosłownym sensie grzechem byłoby z niego nie skorzystać! Jednocześnie chciałbym zachować nasz bieżący format pracy z konfiguracją: po pierwsze, jednolitość użycia w różnych mikroserwisach projektu, a po drugie, możliwość uruchamiania kodu na lokalnej maszynie, korzystając z jednego prostego pliku konfiguracyjnego.

W związku z tym mechanizm budowy obiektu konfiguracji został dostosowany, aby mógł działać zarówno z naszym klasycznym plikiem konfiguracyjnym, jak i z sekretami z Kubernetes. Ponadto ustalono bardziej rygorystyczną strukturę konfiguracji, mówiąc językiem Pythona 3, taką jak:

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

Ostatecznie, końcowa konfiguracja to słownik z nazwanymi sekcjami, z których każda jest słownikiem z wartościami prostych typów. Sekcje opisują konfigurację i dostęp do zasobów określonego typu. Przykład fragmentu naszej konfiguracji:

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"

Przy tym, pole engine baz danych można ustawić na SQLite, a redis skonfigurować na mock, podając też nazwę pliku do zapisania – te parametry są poprawnie rozpoznawane i przetwarzane, co pozwala łatwo uruchamiać kod lokalnie do debugowania, testów jednostkowych i wszelkich innych potrzeb. Jest to dla nas szczególnie istotne, ponieważ tych innych potrzeb jest wiele – część naszego kodu jest przeznaczona do różnorodnych obliczeń analitycznych, uruchamiana nie tylko na serwerach z orkiestracją, ale także różnymi skryptami, i na komputerach analityków, którzy muszą opracowywać i debugować złożone strumienie przetwarzania danych bez zastanawiania się nad kwestiami backendowymi. Przy okazji, nie zaszkodzi podzielić się tym, że nasze główne narzędzia, w tym kod kompozycji konfiguracji, są instalowane przez setup.py – razem to łączy nasz kod w jednolitą ekosystem, niezależną od platformy i sposobu użycia.

Opis poda w Kubernetes wygląda tak:

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

To znaczy, w każdym sekrecie opisana jest jedna sekcja. Sekrety są tworzone w ten sposób:

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

Wspólnie prowadzi to do stworzenia plików YAML w ścieżce /etc/secrets/db-main/section_name.yaml

A do lokalnych uruchomień używany jest konfiguracja znajdująca się w głównym katalogu projektu lub w ścieżce wskazanej w zmiennej środowiskowej. Kod odpowiedzialny za te udogodnienia można zobaczyć w spoilerze.

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

Logika jest tutaj dość prosta: łączymy duże konfiguracje z katalogu projektu i z ścieżki przez zmienną środowiskową oraz małe sekcje konfiguracyjne z sekretów K8s, a następnie nieco je wstępnie przetwarzamy. Dodatkowo dodajemy kilka zmiennych. Zauważam, że przy wyszukiwaniu plików z sekretów używane jest ograniczenie głębokości, ponieważ K8s w każdym sekrecie tworzy dodatkowy ukryty folder, w którym same sekrety są przechowywane, a jeden poziom wyżej znajduje się po prostu odnośnik.

Mam nadzieję, że to, co opisałem, okaże się przydatne dla kogoś 🙂 Przyjmuję wszelkie komentarze i sugestie dotyczące bezpieczeństwa lub innych kwestii w celu poprawy. Ciekawi mnie również opinia społeczności; być może warto dodać wsparcie dla ConfigMaps (w naszym projekcie na razie ich nie używamy) i umieścić kod na GitHubie / PyPI? Osobiście uważam, że takie rzeczy są zbyt indywidualne dla projektów, aby były uniwersalne, a wystarczy małe podpatrzenie cudzych realizacji, jak ta tutaj, oraz omówienie niuansów, porad i najlepszych praktyk, które mam nadzieję zobaczyć w komentarzach 😉

Tylko zarejestrowani użytkownicy mogą brać udział w ankiecie. Zaloguj się, proszę.

Czy warto publikować jako projekt / bibliotekę?

  • 0,0%Tak, używałbym / wniosłbym wkład

  • 33,3%Tak, brzmi świetnie

  • 41,7%Nie, kto potrzebuje, zrobi to sam w swoim formacie i pod własne potrzeby

  • 25,0%Powstrzymam się od odpowiedzi

Zagłosowało 12 użytkowników. 3 użytkowników wstrzymało się od głosu.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster