Русский
Русский
English
Статистика
Реклама

Конфигурируем сервис с помощью Vault и Pydantic

image


Предисловие


В данной статье я расскажу о конфигурации для вашей сервисов с помощью связки Vault (KV и пока только первой версии, т.е. без версионирования секретов) и Pydantic (Settings) под патронажем Sitri.


Итак, допустим, что у нас есть приложение superapp с заведёнными конфигами в Vault и аутентификацией с помощью approle, примерно так настроим (настройку policies для доступа к секрет-энжайнам и к самим секретам я оставлю за кадром, так как это достаточно просто и статья не об этом):


Key                        Value---                        -----bind_secret_id             truelocal_secret_ids           falsepolicies                   [superapp_service]secret_id_bound_cidrs      <nil>secret_id_num_uses         0secret_id_ttl              0stoken_bound_cidrs          []token_explicit_max_ttl     0stoken_max_ttl              30mtoken_no_default_policy    falsetoken_num_uses             50token_period               0stoken_policies             [superapp_service]token_ttl                  20mtoken_type                 default

Прим.: естественно, что если у вас есть возможность и приложение выходит в боевой режим, то secret_id_ttl лучше делать не бесконечным, выставляя 0 секунд.


SuperApp требует конфигурации: подключения к базе данных, подключение к kafka и faust конфигурации для работы кластера воркеров.


Подготовим Sitri


В базовой документации библиотеки есть простой пример, конфигурирования через vault-провайдер, однако, он не охватывает все возможности и может быть полезным, если ваше приложение конфигурируется достаточно легко.


Итак, для начала сконфигурируем vault-провайдер в условном файле provider_config.py:


import hvac  from sitri.providers.contrib.vault import VaultKVConfigProvider  from sitri.providers.contrib.system import SystemConfigProvider  configurator = SystemConfigProvider(prefix="superapp")  ENV = configurator.get("env")  def vault_client_factory() -> hvac.Client:      client = hvac.Client(url=configurator.get("vault_api"))      client.auth_approle(          role_id=configurator.get("role_id"),    secret_id=configurator.get("secret_id"),    )      return client  provider = VaultKVConfigProvider(      vault_connector=vault_client_factory, mount_point=f"{configurator.get('app_name')}/{ENV}"  )

В данном случае мы достаём из среды с помощью системного провайдера несколько переменных для конфигурирования подключения к vault, т.е. изначально должны быть экспортированы следующие переменные:


export SUPERAPP_ENV=devexport SUPERAPP_APP_NAME=superappexport SUPERAPP_VAULT_API=https://your-vault-host.domainexport SUPERAPP_ROLE_ID=535b268d-b858-5fb9-1e3e-79068ca77e27 # Примерexport SUPERAPP_SECRET_ID=243ab423-12a2-63dc-3d5d-0b95b1745ccf # Пример

В примере предполагается, что базовый mount_point к вашим секретам для определённой среды будет содержать имя приложения и имя среды, поэтому мы и экспортировали SUPERAPP_ENV. Путь до секретов отдельных частей приложения мы будем определять в settings-классах далее, поэтому в провайдере secret_path мы оставляем пустым.


Классы настроек


Начнём по пунктам и разнесём три класса настроек (БД, Kafka, Faust) по трём разным файлам.


Настройки БД


from pydantic import Field  from sitri.settings.contrib.vault import VaultKVSettings  from superapp.config.provider_config import provider  class DBSettings(VaultKVSettings):      user: str = Field(..., vault_secret_key="username")      password: str = Field(...)      host: str = Field(...)      port: int = Field(...)      class Config:          provider = provider          default_secret_path = "db"

Итак, как видите, конфиг. данные для базы у нас достаточно простые. Этот класс будет по-умолчанию смотреть в секрет superapp/dev/db, так, как мы указали в config классе, в остальном здесь простые pydantic поля, но в одном из них присутствует extra-аргумент vault_secret_key он нужен тогда, когда ключ в секрете не совпадает по имени с pydantic полем в нашем классе, если его не указывать, то провайдер будет искать ключ по имени поля.


Например, в нашем тестовом приложении, предполагается, что в секрете superapp/dev/db, есть ключи password и username, но мы хотим, чтобы последний был помещён в поле user для удобства и краткости.


Поместим в вышеозначенный секрет следующие данные для примера:


{  "host": "testhost",  "password": "testpassword",  "port": "1234",  "username": "testuser"}

Для первого класса из тройки, я покажу, как легко можно всё это запустить, чтобы данные собрались сами:


db_settings = DBSettings()pprint(db_settings.dict())# -> # {#     "host": "testhost",#     "password": "testpassword",#     "port": 1234,#     "user": "testuser"# }

Настройки Kafka


from typing import Dict, Any  from pydantic import Field  from sitri.settings.contrib.vault import VaultKVSettings  from superapp.config.provider_config import provider, configurator  class KafkaSettings(VaultKVSettings):      mechanism: str = Field(..., vault_secret_key="auth_mechanism")      brokers: str = Field(...)      auth_data: Dict[str, Any] = Field(...)      class Config:          provider = provider          default_secret_path = "kafka"          default_mount_point = f"{configurator.get('app_name')}/common"

В данном случае, представим, что инстанс кафки для разных сред нашего сервиса один, поэтому секрет хранится по пути superapp/common/kafka


{  "auth_data": "{\"password\": \"testpassword\", \"username\": \"testuser\"}",  "auth_mechanism": "SASL_PLAINTEXT",  "brokers": "kafka://test"}

Класс настройки поймёт комплексный тип данных Dict[str, Any] и распарсит его в словарь, то есть при заполнении наших настроек будут следующие данные:


{    "auth_data":    {        "password": "testpassword",        "username": "testuser"    },    "brokers": "kafka://test",    "mechanism": "SASL_PLAINTEXT"}

Так же, если секрет будет задан напрямую в json, например так:


{  "auth_data": {    "password": "testpassword",    "username": "testuser"  },  "auth_mechanism": "SASL_PLAINTEXT",  "brokers": "kafka://test"}

То класс настроек тоже сможет правильно разложить данные.


P.S.
Так же, secret_path и mount_point можно задавать на уровне полей, чтобы провайдер запросил конкретные значения из разных секретов (если это требуется). Приведу цитату с приоритезацией пути секрета и точки монтирования из документации:


Secret path prioritization:
  1. vault_secret_path (Field arg)
  2. default_secret_path (Config class field)
  3. secret_path (provider initialization optional arg)


Mount point prioritization:
  1. vault_mount_point (Field arg)
  2. default_mount_point (Config class field)
  3. mount_point (provider initialization optional arg)


Настройки Faust и отдельных воркеров


from typing import Dict  from pydantic import Field, BaseModel  from sitri.settings.contrib.vault import VaultKVSettings  from superapp.config.provider_config import provider  class AgentConfig(BaseModel):      partitions: int = Field(...)      concurrency: int = Field(...)  class FaustSettings(VaultKVSettings):      app_name: str = Field(...)      default_partitions_count: int = Field(..., vault_secret_key="partitions_count")      default_concurrency: int = Field(..., vault_secret_key="agent_concurrency")      agents: Dict[str, AgentConfig] = Field(default=None, vault_secret_key="agents_specification")      class Config:          provider = provider          default_secret_path = "faust"

superapp/dev/faust:


{  "agent_concurrency": "5",  "app_name": "superapp-workers",  "partitions_count": "10"}

В данном случае, по-умолчанию у нас есть глобальные значения кол-ва партиций в кафке и concurrency для агентов. Таким образом, по-умолчанию наши настройки будут выгружены так:


{  "agents": None,  "app_name": "superapp-workers",  "default_concurrency": 5,  "default_partitions_count": 10}

Например, у нас есть агент X с настройками:


{  "partitions": 5,  "concurrency": 2}

Наш секрет в связи с этим должен выглядеть следующим образом:


{  "agent_concurrency": "5",  "agents_specification": {    "X": {      "concurrency": "2",      "partitions": "5"    }  },  "app_name": "superapp-workers",  "partitions_count": "10"}

Как и ожидалось данные корректно смапились и типы значений были преобразованы так, как указано в модели AgentConfig:


{    "agents":    {        "X":        {            "concurrency": 2,            "partitions": 5        }    },    "app_name": "superapp-workers",    "default_concurrency": 5,    "default_partitions_count": 10}

Совмещаем в единый конфиг класс


from pydantic import BaseModel, Field  from superapp.config.database_settings import DBSettings  from superapp.config.faust_settings import FaustSettings  from superapp.config.kafka_settings import KafkaSettings  class AppSettings(BaseModel):      db: DBSettings = Field(default_factory=DBSettings)      faust: FaustSettings = Field(default_factory=FaustSettings)      kafka: KafkaSettings = Field(default_factory=KafkaSettings)

Совместим наши классы настроек в одну модель, применив default_factory для автоматического сбора при инициализации модели всех наших данных.


Давайте запустим наш код и проверим, как всё сработается вместе:


from superapp.config import AppSettings  config = AppSettings()  print(config)  print(config.dict())

Получаем общий вывод всей конфигурации приложения:


db=DBSettings(user='testuser', password='testpassword', host='testhost', port=1234) faust=FaustSettings(app_name='superapp-workers', default_partitions_count=10, default_concurrency=5, agents={'X': AgentConfig(partitions=5, concurrency=2)}) kafka=KafkaSettings(mechanism='SASL_PLAINTEXT', brokers='kafka://test', auth_data={'password': 'testpassword', 'username': 'testuser'})

{    "db":    {        "host": "testhost",        "password": "testpassword",        "port": 1234,        "user": "testuser"    },    "faust":    {        "agents":        {            "X":            {                "concurrency": 2,                "partitions": 5            }        },        "app_name": "superapp-workers",        "default_concurrency": 5,        "default_partitions_count": 10    },    "kafka":    {        "auth_data":        {            "password": "testpassword",            "username": "testuser"        },        "brokers": "kafka://test",        "mechanism": "SASL_PLAINTEXT"    }}

Счастье, радость, восторг!


У нас получилась такая структура тест-проекта:


superapp config    app_settings.py    database_settings.py    faust_settings.py    __init__.py    kafka_settings.py    provider_config.py __init__.py main.py

Послесловие


Как видите настройка достаточно проста с Sitri, после неё мы получаем чёткую схему конфигурации с нужными нам типами данных у значений, даже если в vault по-умолчанию они хранились строками.


Пишите комментарии по поводу либы, кода или общие впечатления. Буду рад любому отзыву!


P.S. Код из статьи я залил на github https://github.com/Egnod/article_sitri_vault_pydantic

Источник: habr.com
К списку статей
Опубликовано: 09.12.2020 04:19:27
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Python

Devops

Микросервисы

Vault

Pydantic

Configuration management

Config

Hashicorp

Hashicorp vault

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru