[DevOps, Kubernetes, Python] Конфигурация проекта внутри и вне Kubernetes

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
18-Окт-2020 01:30

Недавно я написал ответ о жизни проекта в Докерах и отладке кода вне него, где мельком упомянул о том, что можно сделать свою систему конфигурирования, чтобы сервис и в Кубере хорошо работал, подтягивал секреты, и локально удобно запускался, в том числе вообще вне Докера. Ничего сложного, но описанный "рецепт" может кому-то пригодится :) Код на Питоне, но логика к языку не привязана.

Предыстория вопроса такова: жил-был один проект, сначала он был маленьким монолитом с утилитами и скриптами, но со временем рос, делился на сервисы, которые в свою очередь стали делиться на микросервисы, а потом ещё и скейлиться. Поначалу это всё выполнялось на голых 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

SPL
__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, которое я надеюсь увидеть в комментариях ;)
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_devops, #_kubernetes, #_python, #_python, #_devops, #_kubernetes, #_docker, #_configuration_management, #_yaml, #_devops, #_kubernetes, #_python
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 19:17
Часовой пояс: UTC + 5