Конфігурація проекту всередині та поза Kubernetes

Нещодавно я написав відповідь про життя проекту в Докерах та налагодження коду поза ним, Де миттєво згадав про те, що можна зробити свою систему конфігурування, щоб сервіс і в Кубер добре працював, підтягував секрети, і локально зручно запускався, в тому числі взагалі поза Докером. Нічого складного, але описаний "рецепт" може комусь стане в нагоді 🙂 Код на Пітоні, але логіка до мови не прив'язана.

Конфігурація проекту всередині та поза Kubernetes

Передісторія питання така: жив-був один проект, спочатку він був маленьким монолітом з утилітами та скриптами, але згодом ріс, ділився на сервіси, які у свою чергу стали ділитися на мікросервіси, а потім ще й скейлі. Спочатку це все виконувалося на голих VPS, процеси налаштування та розгортання коду на яких були автоматизовані за допомогою Ansible, і кожному сервісу складався YAML-конфіг з потрібними налаштуваннями та ключами, і аналогічний конфіг-файл використовувався для локальних запусків, що було дуже зручно. .до цього конфіг вантажиться у глобальний об'єкт, доступний з будь-якого місця у проекті.

Однак зростання кількості мікросервісів, їх зв'язків, а також потреба в централізованому логуванні та моніторингу, передвіщали переїзд до Кубера, який досі ще в процесі. Разом з допомогою у вирішенні згаданих завдань, Kubernetes пропонує свої підходи до управління інфраструктурою, у тому числі т.н. Секрети и способи роботи з ними. Механізм стандартний і надійний, тому буквально гріх ним не скористатися! Але при цьому хотілося б зберегти свій поточний формат роботи з конфігом: по-перше, однаково використовувати його в різних мікросервісах проекту, а по-друге, мати можливість запускати код на локальній машині, використовуючи один простий конфіг-файл.

У зв'язку з цим механізм побудови об'єкта-конфігурації був доопрацьований так, щоб уміти працювати як з нашим класичним конфіг-файлом, так і з секретами з Кубера. Також була задана жорсткіша структура конфіга, говорячи мовою третього Пітона, така:

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

Тобто, підсумковий когфіг - це словник з іменованими секціями, кожна з яких є словником зі значеннями з простих типів. А секції описують конфігурацію та доступи до ресурсів певного виду. Приклад шматка нашого конфігу:

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"

При цьому поле engine баз даних можна встановити на SQLite, а redis налаштувати на mock, вказавши ще ім'я файлу для збереження, – ці параметри коректно розпізнаються та обробляються, що дозволяє легко запускати код локально для налагодження, юніт-тестування та інших потреб. Нам це особливо актуально тому, що цих інших потреб багато – частина нашого коду призначена для різноманітних аналітичних розрахунків, запускається не тільки на серверах з оркестрацією, але й різними скриптами, та на комп'ютерах аналітиків, яким потрібно опрацьовувати та налагоджувати складні конвеєри обробки даних, не парячись бекендерськими питаннями. До речі, не зайвим буде поділитися тим, що наші основні інструменти, включаючи код компонування конфігу, встановлюються через setup.py - разом це об'єднує наш код в єдину екосистему, яка не залежить від платформи та способу використання.

Опис пода в Kubernetes виглядає так:

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

Тобто, у кожному секреті описано одну секцію. Самі секрети створюються так:

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

Разом це призводить до створення YAML-файлів шляхом /etc/secrets/db-main/section_name.yaml

А для локальних запусків використовується конфіг, розташований у кореневій директорії проекту або шляхом, зазначеним у змінній оточенні. Код, відповідальний за ці зручності, можна побачити в спойлері.

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

Логіка тут досить проста: об'єднуємо великі конфіги з директорії проекту та шляхи змінної оточення, і невеликі конфіги-секції із секретів Кубера, а потім трохи їх передробляємо. Плюс деякі змінні. Зауважу, що при пошуку файлів із секретів використовується обмеження глибини, тому що K8s у кожному секреті створює ще приховану папку, де самі секрети і зберігається, а рівнем вище просто посилання.

Сподіваюся, описане виявиться комусь корисним 🙂 Приймаються будь-які коментарі та рекомендації щодо безпеки або інших моментів на покращення. Також цікава думка спільноти, чи можливо варто додати підтримку ConfigMaps (у нашому проекті вони поки не використовується) і оформити код на ГітХабі / PyPI? Особисто я думаю, що такі речі є надто індивідуальними для проектів, щоб бути універсальними, і досить невеликого підглядання на чужі реалізації, на кшталт наведеної тут, та обговорення нюансів, порад та best practices, яке я сподіваюся побачити в коментарях 😉

Тільки зареєстровані користувачі можуть брати участь в опитуванні. Увійдіть, будь ласка.

Чи варто публікувати проект/бібліотеку?

  • 0,0%Так, я б використав / контрибутил0

  • 33,3%Так, звучить здорово4

  • 41,7%Ні, кому треба зроблять самі у своєму форматі та під свої потреби5

  • 25,0%Утримаюся від відповіді3

Проголосували 12 користувачів. Утрималися 3 користувача.

Джерело: habr.com

Додати коментар або відгук