Недавно я написал ответ о жизни проекта в Докерах и отладке кода вне него, где мельком упомянул о том, что можно сделать свою систему конфигурирования, чтобы сервис и в Кубере хорошо работал, подтягивал секреты, и локально удобно запускался, в том числе вообще вне Докера. Ничего сложного, но описанный "рецепт" может кому-то пригодится 🙂 Код на Питоне, но логика к языку не привязана.
Предыстория вопроса такова: жил-был один проект, сначала он был маленьким монолитом с утилитами и скриптами, но со временем рос, делился на сервисы, которые в свою очередь стали делиться на микросервисы, а потом ещё и скейлиться. Поначалу это всё выполнялось на голых 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
А для локальных запусков используется конфиг, расположенный в корневой директории проекта или по пути, указанному в переменной окружения. Код, ответственный за эти удобства, можно лицезреть в спойлере.
__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, которое я надеюсь увидеть в комментариях 😉
ссылка на оригинал статьи https://habr.com/ru/post/523432/
Добавить комментарий