Конфигурация проекта внутри и вне 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: info@test.com
      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
    • 41,2%Да, звучит здорово7
    • 41,2%Нет, кому надо сделают сами в своём формате и под свои нужды7
    • 17,6%Воздержусь от ответа3
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 2

      0

      Можно воспользоваться https://github.com/microconfig/microconfig например

        0

        Выглядит классно! Хотя для нас этот инструмент выглядит излишне мощным решением, т.к большинство наших микросервисов на Питоне и потому лаконичнее управляются средством на самом языке, но MicroConfig выглядит более универсальным, и о нём явно стоит знать, спасибо :)

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое