[Python, DevOps, Микросервисы] Конфигурируем сервис с помощью Vault и Pydantic
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Предисловие
В данной статье я расскажу о конфигурации для вашей сервисов с помощью связки Vault (KV и пока только первой версии, т.е. без версионирования секретов) и Pydantic (Settings) под патронажем Sitri.
Итак, допустим, что у нас есть приложение superapp с заведёнными конфигами в Vault и аутентификацией с помощью approle, примерно так настроим (настройку policies для доступа к секрет-энжайнам и к самим секретам я оставлю за кадром, так как это достаточно просто и статья не об этом):
Key Value
--- -----
bind_secret_id true
local_secret_ids false
policies [superapp_service]
secret_id_bound_cidrs <nil>
secret_id_num_uses 0
secret_id_ttl 0s
token_bound_cidrs []
token_explicit_max_ttl 0s
token_max_ttl 30m
token_no_default_policy false
token_num_uses 50
token_period 0s
token_policies [superapp_service]
token_ttl 20m
token_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=dev
export SUPERAPP_APP_NAME=superapp
export SUPERAPP_VAULT_API=https://your-vault-host.domain
export 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:
- vault_secret_path (Field arg)
- default_secret_path (Config class field)
- secret_path (provider initialization optional arg)
Mount point prioritization:
- vault_mount_point (Field arg)
- default_mount_point (Config class field)
- 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
===========
Похожие новости:
- [Программирование, Хранилища данных, DevOps] The Rules for Data Processing Pipeline Builders
- [Python, Параллельное программирование, Разработка под Linux] Клиент-серверный IPC при помощи Python multiprocessing
- [Системное администрирование, DevOps] Очередная встреча сообщества MonHouse состоится завтра, 09.12.2020, в 19:00
- [Amazon Web Services, DevOps, Kubernetes] Приглашаем на DINS DevOps EVENING (online): теги в AWS и namespace в Kubernetes
- [Системное администрирование, IT-инфраструктура, Серверное администрирование, DevOps] Prometheus и VictoriaMetrics: отказоустойчивая инфраструктура для хранения метрик (перевод)
- [Системное администрирование, Серверное администрирование, DevOps, Kubernetes] Docker is deprecated — и как теперь быть?
- [Профессиональная литература] Книга «Простой Python. Современный стиль программирования. 2-е изд.»
- [Python] Типовые ошибки на собеседовании
- [Хранение данных, Хранилища данных, DevOps, Облачные сервисы] Как использовать объектное S3-хранилище Mail.ru Cloud Solutions для хранения бэкапов Veeam
- [Системное администрирование, Серверное администрирование, DevOps, Kubernetes] Контейнеризация понятным языком: хранение данных и безопасность в Kubernetes, зачем нужен Ansible
Теги для поиска: #_python, #_devops, #_mikroservisy (Микросервисы), #_vault, #_pydantic, #_python, #_configuration_management, #_config, #_hashicorp, #_hashicorp_vault, #_python, #_devops, #_mikroservisy (
Микросервисы
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 21-Ноя 21:58
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Предисловие В данной статье я расскажу о конфигурации для вашей сервисов с помощью связки Vault (KV и пока только первой версии, т.е. без версионирования секретов) и Pydantic (Settings) под патронажем Sitri. Итак, допустим, что у нас есть приложение superapp с заведёнными конфигами в Vault и аутентификацией с помощью approle, примерно так настроим (настройку policies для доступа к секрет-энжайнам и к самим секретам я оставлю за кадром, так как это достаточно просто и статья не об этом): Key Value
--- ----- bind_secret_id true local_secret_ids false policies [superapp_service] secret_id_bound_cidrs <nil> secret_id_num_uses 0 secret_id_ttl 0s token_bound_cidrs [] token_explicit_max_ttl 0s token_max_ttl 30m token_no_default_policy false token_num_uses 50 token_period 0s token_policies [superapp_service] token_ttl 20m token_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=dev
export SUPERAPP_APP_NAME=superapp export SUPERAPP_VAULT_API=https://your-vault-host.domain export 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:
Mount point prioritization:
Настройки 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 =========== Похожие новости:
Микросервисы ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 21-Ноя 21:58
Часовой пояс: UTC + 5