Sitri = Vault + Pydantic: continuation of the saga, local development.
Tags: python, config, management, vault
Published: 2020-12-15 21:55:24

![](https://habrastorage.org/webt/y1/ul/v_/y1ulv_0bvlzlcmj1-z9mun6sl58.jpeg)

# Background

In the previous article I wrote about how to configure your application using [Sitri](https://sitri.readthedocs.io/en/latest/), however, I missed the point with local development, since you will agree that it is not very convenient deploying a Vault locally, and storing a local config in a common Vault, especially if several people are working on a project, is doubly inconvenient.

In [Sitri](https://sitri.readthedocs.io/en/latest/) this problem is solved quite simply - using the local mode for your settings classes, that is, you do not even have to rewrite anything or duplicate code, and the structure json file for local mode will almost completely repeat the structure of secrets.

So, well, now, let's add literally a couple of lines of code to our [project](https://github.com/Egnod/article_sitri_vault_pydantic) + I'll show you how you can work with all this if your project is run locally in docker-compose...

# Preparing the code

To begin with, let's agree that local\_mode is true when *ENV* = "local" :) 

Next, I propose to slightly edit our *provider\_config.py* and create a *BaseConfig* class there from which we will inherit in our *Config* settings classes. We will do this in order not to duplicate the code, that is, the settings classes themselves will contain only what is specific to them.

```python
import hvac
from sitri.providers.contrib.system import SystemConfigProvider
from sitri.providers.contrib.vault import VaultKVConfigProvider
from sitri.settings.contrib.vault import VaultKVSettings

configurator = SystemConfigProvider(prefix="superapp")
ENV = configurator.get("env")

is_local_mode = ENV == "local"
local_mode_file_path = configurator.get("local_mode_file_path")


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}",
)


class BaseConfig(VaultKVSettings.VaultKVSettingsConfig):
    provider = provider
    local_mode = is_local_mode
    local_provider_args = {"json_path": local_mode_file_path}
```

A bit about *local_provider_args* in this field we specify the arguments for creating an instance of *JsonConfigProvider*, they will be validated and this dictionary must match the schema, so don't worry - this is not some dirty trick. However, if you want to create an instance of the local provider yourself, then you just put it in the optional *local_provider* field.

Now, we can easily inherit the config classes from the base one. For example, a settings class for connecting to Kafka would look like this:

```python
from typing import Any, Dict

from pydantic import Field
from sitri.settings.contrib.vault import VaultKVSettings

from superapp.config.provider_config import BaseConfig, configurator


class KafkaSettings(VaultKVSettings):
    mechanism: str = Field(..., vault_secret_key="auth_mechanism")
    brokers: str = Field(...)
    auth_data: Dict[str, Any] = Field(...)

    class Config(BaseConfig):
        default_secret_path = "kafka"
        default_mount_point = f"{configurator.get('app_name')}/common"

        local_mode_path_prefix = "kafka"
```

As you can see, the required changes are minimal. *local_mode_path_prefix* we specify that the structure of the general config is saved in our json file. Now, let's write this json-file for the local configuration:

```json
{
    "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"
    }
}
```

... Well, or just copy and paste it from the end of the last article. As you can see, everything is very simple here. For our further research, rename *main.py* in the root of the project to *\_\_main\_\_.py* so that you can run the package with a command from docker-compose.

# Put the application into the container and enjoy the build

The first thing we should do is write a small Dockerfile:

```dockerfile
FROM python:3.8.3-buster

ENV PYTHONUNBUFFERED=1 \
    POETRY_VIRTUALENVS_CREATE=false \
    POETRY_VIRTUALENVS_IN_PROJECT=false \
    POETRY_NO_INTERACTION=1


RUN pip install poetry

RUN mkdir /superapp/
WORKDIR /superapp/

COPY ./pyproject.toml ./poetry.lock /superapp/
RUN poetry install --no-ansi

WORKDIR /
```

Here we just install the dependencies and that's it, since it is for local development, we do not copy the project code.

Next, we need an env file with the variables required for local-mode:

```.env
SUPERAPP_ENV=local
SUPERAPP_LOCAL_MODE_FILE_PATH=/config.json
SUPERAPP_APP_NAME=superapp
```

As you can see, nothing superfluous, no configuration information is needed for Vault, since in local mode, the application will not even try to "knock" on Vault.

And the last thing we need to write is the docker-compose.yml file itself:

```yaml
# docker-compose config for local development
version: '3'
services:
  superapp:
    command: python3 -m superapp
    restart: always
    build:
      context: ./
      dockerfile: Dockerfile
    volumes:
      - ./superapp:/superapp
      - ./config.json:/config.json
    env_file:
      - .env.local
```

Everything is simple here too. We put our json file in the root, as write above in the environment variable for the container.


Now, launch:

```bash
docker-compose up
```

```bash
Creating article_sitri_vault_pydantic_superapp_1 ... done
Attaching to article_sitri_vault_pydantic_superapp_1

superapp_1  | 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'})

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

As you can see, everything started successfully and the information from our json file successfully passed all checks and became settings for the local version of the application, yuhhu!

Code of this "continuation" I put in a separate branch of the repository, so you can take a look at how it all looks after the changes: [branch](https://github.com/Egnod/article_sitri_vault_pydantic/tree/local_mode_example)